Skip to content

Commit

Permalink
Introduce the ciphertext registry (ethereum#2)
Browse files Browse the repository at this point in the history
This commit introduces the ciphertext registry, with its three main
components:
 * protected storage space for persistent registry entries
 * ciphertext handles
 * in-memory ciphertext registry that persist for the lifetime of the
   transaction/call

The protected storage space is implemented as a separate contract,
linked to the actual one. When we create a contract, we take its address
and run it through SHA256 to get the address of the corresponding
protected storage contract. See more in evm.go, Create().

A ciphertext handle is the SHA256 hash of a ciphertext. It is generated
by the call to the `verifyCiphertext()` precompiled contract.
If a handle is stored in contract storage, we persist the actual
ciphertext in protected storage, along with some metadata. One piece of
metadata is the reference count. In essense, if a handle is stored in
contract storage via SSTORE, the refcount is bumped by 1. If a handle is
overwritten, the refcount is reduced by 1. Additionally, we
automatically verify any handle that points to a ciphertext on SLOAD.

Verifying a ciphertext essentially means storing it in an in-memory map
from hash(ciphertext) => {ciphertext, verified_at_stack_depth}. A
ciphertext is verified only for a particular stack depth and further on.
On the RETURN opcode, we remove entries from the map that are no longer
verified. More information to follow on that approach.

More features and code cleanup will be added in future commits.
  • Loading branch information
dartdart26 committed Dec 1, 2022
1 parent 7804a56 commit 9635d76
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 218 deletions.
236 changes: 151 additions & 85 deletions core/vm/contracts.go

Large diffs are not rendered by default.

84 changes: 0 additions & 84 deletions core/vm/contracts_stateful.go

This file was deleted.

55 changes: 20 additions & 35 deletions core/vm/evm.go
Expand Up @@ -41,8 +41,8 @@ type (
GetHashFunc func(uint64) common.Hash
)

func (evm *EVM) precompile(addr common.Address) (StatefulPrecompiledContract, bool) {
var precompiles map[common.Address]StatefulPrecompiledContract
func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
var precompiles map[common.Address]PrecompiledContract
switch {
case evm.chainRules.IsBerlin:
precompiles = PrecompiledContractsBerlin
Expand Down Expand Up @@ -121,8 +121,6 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
// some arbitrary in-memory state that lives as long as the enclosing EVM
inMemoryState []byte
}

// NewEVM returns a new EVM. The returned EVM is not thread safe and should
Expand All @@ -145,7 +143,6 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) {
evm.TxContext = txCtx
evm.StateDB = statedb
evm.inMemoryState = []byte{}
}

// Cancel cancels any running EVM operation. This may be called concurrently and
Expand All @@ -164,16 +161,6 @@ func (evm *EVM) Interpreter() *EVMInterpreter {
return evm.interpreter
}

// GetStateDB returns the EVM's StateDB
func (evm *EVM) GetStateDB() StateDB {
return evm.StateDB
}

// GetBlockContext returns the EVM's BlockContext
func (evm *EVM) GetBlockContext() BlockContext {
return evm.Context
}

// Call executes the contract associated with the addr with the given input as
// parameters. It also handles any necessary value transfer required and takes
// the necessary steps to create accounts and reverses the state in case of an
Expand Down Expand Up @@ -225,12 +212,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
}

if isPrecompile {
// If it is the set memory state call, set the input to the EVM's in-memory state.
// Return whatever the precompiled contract returns.
if addr == common.BytesToAddress([]byte{20}) {
evm.inMemoryState = input
}
ret, gas, err = RunStatefulPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
ret, gas, err = RunPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
Expand Down Expand Up @@ -293,7 +275,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunStatefulPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
ret, gas, err = RunPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
} else {
addrCopy := addr
// Initialise a new contract and set the code that is to be used by the EVM.
Expand Down Expand Up @@ -334,7 +316,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunStatefulPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
ret, gas, err = RunPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
} else {
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
Expand Down Expand Up @@ -383,16 +365,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}

if p, isPrecompile := evm.precompile(addr); isPrecompile {
// If it is the get memory state call, return the EVM's in-memory state.
if addr == common.BytesToAddress([]byte{21}) {
if gas < p.RequiredGas(input) {
ret, gas, err = nil, 0, ErrOutOfGas
} else {
ret, gas, err = evm.inMemoryState, gas-p.RequiredGas(input), nil
}
} else {
ret, gas, err = RunStatefulPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
}
ret, gas, err = RunPrecompiledContract(p, evm, caller.Address(), addr, input, gas, evm.interpreter.readOnly)
} else {
// At this point, we use a copy of address. If we don't, the go compiler will
// leak the 'contract' to the outer scope, and make allocation for 'contract'
Expand Down Expand Up @@ -524,8 +497,20 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,

// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
nonce := evm.StateDB.GetNonce(caller.Address())

// Create the actual contract.
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
ret, contractAddr, leftOverGas, err = evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
if err != nil {
return
}

// Create a separate contract that would be used for protected storage.
// Return the actual contract's return value and contract address.
protectedStorageContractAddr := crypto.CreateProtectedStorageContractAddress(contractAddr)
_, _, leftOverGas, err = evm.create(caller, &codeAndHash{}, leftOverGas, big.NewInt(0), protectedStorageContractAddr, CREATE)
return
}

// Create2 creates a new contract using code as deployment code.
Expand Down
140 changes: 128 additions & 12 deletions core/vm/instructions.go
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
"golang.org/x/crypto/sha3"
Expand Down Expand Up @@ -513,23 +514,70 @@ func opMstore8(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]
return nil, nil
}

var locationModulus *uint256.Int
// Ciphertext metadata is stored in protected storage, in a 32-byte slot.
// Currently, we only utilize 16 bytes from the slot.
type ciphertextMetadata struct {
refCount uint64
length uint64
}

func (m ciphertextMetadata) serialize() [32]byte {
u := uint256.NewInt(0)
u[0] = m.refCount
u[1] = m.length
return u.Bytes32()
}

func init() {
locationModulus = uint256.NewInt(0)
locationModulus.SetAllOne()
locationModulus[3] = 0
func (m *ciphertextMetadata) deserialize(buf [32]byte) *ciphertextMetadata {
u := uint256.NewInt(0)
u.SetBytes(buf[:])
m.refCount = u[0]
m.length = u[1]
return m
}

func unprotectedLocation(loc uint256.Int) uint256.Int {
return *loc.Mod(&loc, locationModulus)
func newCiphertextMetadata(buf [32]byte) *ciphertextMetadata {
m := ciphertextMetadata{}
return m.deserialize(buf)
}

func min(a uint64, b uint64) uint64 {
if a < b {
return a
}
return b
}

func newInt(buf []byte) *uint256.Int {
i := uint256.NewInt(0)
return i.SetBytes(buf)
}

var zero = uint256.NewInt(0).Bytes32()

func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
loc := scope.Stack.peek()
actual_loc := unprotectedLocation(*loc)
hash := common.Hash(actual_loc.Bytes32())
hash := common.Hash(loc.Bytes32())
val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash)
protectedStorage := crypto.CreateProtectedStorageContractAddress(scope.Contract.Address())
protectedSlotIdx := newInt(interpreter.evm.StateDB.GetState(protectedStorage, val).Bytes())
if !protectedSlotIdx.IsZero() {
// If this is a ciphertext, verify it automatically.
metadata := newCiphertextMetadata(protectedSlotIdx.Bytes32())
ciphertext := make([]byte, metadata.length)
left := metadata.length
for {
if left == 0 {
break
}
bytes := interpreter.evm.StateDB.GetState(protectedStorage, protectedSlotIdx.Bytes32())
toAppend := min(uint64(len(bytes)), left)
left -= toAppend
ciphertext = append(ciphertext, bytes[0:toAppend]...)
protectedSlotIdx.AddUint64(protectedSlotIdx, 1)
}
interpreter.verifiedCiphertexts[val] = verifiedCiphertext{interpreter.evm.depth, ciphertext}
}
loc.SetBytes(val.Bytes())
return nil, nil
}
Expand All @@ -538,10 +586,71 @@ func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b
if interpreter.readOnly {
return nil, ErrWriteProtection
}
loc := unprotectedLocation(scope.Stack.pop())
val := scope.Stack.pop()
loc := scope.Stack.pop()
newVal := scope.Stack.pop()
newValBytes := newVal.Bytes()
newValHash := common.BytesToHash(newValBytes)
oldValHash := interpreter.evm.StateDB.GetState(scope.Contract.Address(), common.Hash(loc.Bytes32()))
verifiedCiphertext, isVerified := interpreter.verifiedCiphertexts[newValHash]
protectedStorage := crypto.CreateProtectedStorageContractAddress(scope.Contract.Address())
if newValHash != oldValHash {
// If the value is no longer stored in actual contract storage, garbage collect the ciphertext from protected storage or decrease the refcount by 1.
existingMetadataHash := interpreter.evm.StateDB.GetState(protectedStorage, oldValHash)
existingMetadataInt := newInt(existingMetadataHash.Bytes())
if !existingMetadataInt.IsZero() {
metadata := newCiphertextMetadata(existingMetadataInt.Bytes32())
if metadata.refCount == 1 {
interpreter.evm.StateDB.SetState(protectedStorage, existingMetadataHash, zero)
slot := existingMetadataInt.AddUint64(existingMetadataInt, 1)
slotsToZero := metadata.length / 32
if metadata.length < 32 {
slotsToZero++
}
for i := uint64(0); i < slotsToZero; i++ {
interpreter.evm.StateDB.SetState(protectedStorage, slot.Bytes32(), zero)
slot.AddUint64(existingMetadataInt, 1)
}
} else if metadata.refCount > 1 {
metadata.refCount--
interpreter.evm.StateDB.SetState(protectedStorage, existingMetadataHash, metadata.serialize())
}
}

// Add to protected storage or update the existing refcount.
if isVerified {
// If the value is a verified ciphertext, read its metadata from protected storage.
metadataInt := newInt(interpreter.evm.StateDB.GetState(protectedStorage, newValHash).Bytes())
metadata := ciphertextMetadata{}
if metadataInt.IsZero() {
// If no metadata, it means this ciphertext itself hasn't been persisted to protected storage yet. We do that as part of SSTORE.
metadata.refCount = 1
metadata.length = uint64(len(verifiedCiphertext.ciphertext))
ciphertextSlot := newInt(newValBytes)
ct := make([]byte, 32)
for i, b := range verifiedCiphertext.ciphertext {
if i%32 == 0 && i != 0 {
interpreter.evm.StateDB.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ct))
ciphertextSlot.AddUint64(ciphertextSlot, 1)
ct = make([]byte, 32)
} else {
ct = append(ct, b)
}
}
if len(ct) != 0 {
interpreter.evm.StateDB.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ct))
}
} else {
// If metadata exists, bump the refcount by 1.
metadata := newCiphertextMetadata(interpreter.evm.StateDB.GetState(protectedStorage, newValHash))
metadata.refCount++
}
// Save the metadata in protected storage.
interpreter.evm.StateDB.SetState(protectedStorage, newValHash, metadata.serialize())
}
}
// Set the SSTORE's value in the actual contract.
interpreter.evm.StateDB.SetState(scope.Contract.Address(),
loc.Bytes32(), val.Bytes32())
loc.Bytes32(), newValHash)
return nil, nil
}

Expand Down Expand Up @@ -817,6 +926,13 @@ func opReturn(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b
offset, size := scope.Stack.pop(), scope.Stack.pop()
ret := scope.Memory.GetPtr(int64(offset.Uint64()), int64(size.Uint64()))

// Remove all verified ciphertexts that have depth > current depth - 1
for key, verifiedCiphertext := range interpreter.verifiedCiphertexts {
if verifiedCiphertext.depth > interpreter.evm.depth-1 {
delete(interpreter.verifiedCiphertexts, key)
}
}

return ret, errStopToken
}

Expand Down

0 comments on commit 9635d76

Please sign in to comment.