Skip to content

Commit 9635d76

Browse files
authored
Introduce the ciphertext registry (ethereum#2)
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.
1 parent 7804a56 commit 9635d76

File tree

7 files changed

+371
-218
lines changed

7 files changed

+371
-218
lines changed

core/vm/contracts.go

Lines changed: 151 additions & 85 deletions
Large diffs are not rendered by default.

core/vm/contracts_stateful.go

Lines changed: 0 additions & 84 deletions
This file was deleted.

core/vm/evm.go

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ type (
4141
GetHashFunc func(uint64) common.Hash
4242
)
4343

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

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

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

167-
// GetStateDB returns the EVM's StateDB
168-
func (evm *EVM) GetStateDB() StateDB {
169-
return evm.StateDB
170-
}
171-
172-
// GetBlockContext returns the EVM's BlockContext
173-
func (evm *EVM) GetBlockContext() BlockContext {
174-
return evm.Context
175-
}
176-
177164
// Call executes the contract associated with the addr with the given input as
178165
// parameters. It also handles any necessary value transfer required and takes
179166
// the necessary steps to create accounts and reverses the state in case of an
@@ -225,12 +212,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
225212
}
226213

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

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

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

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

525498
// Create creates a new contract using code as deployment code.
526499
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
527-
contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
528-
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
500+
nonce := evm.StateDB.GetNonce(caller.Address())
501+
502+
// Create the actual contract.
503+
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
504+
ret, contractAddr, leftOverGas, err = evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
505+
if err != nil {
506+
return
507+
}
508+
509+
// Create a separate contract that would be used for protected storage.
510+
// Return the actual contract's return value and contract address.
511+
protectedStorageContractAddr := crypto.CreateProtectedStorageContractAddress(contractAddr)
512+
_, _, leftOverGas, err = evm.create(caller, &codeAndHash{}, leftOverGas, big.NewInt(0), protectedStorageContractAddr, CREATE)
513+
return
529514
}
530515

531516
// Create2 creates a new contract using code as deployment code.

core/vm/instructions.go

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/ethereum/go-ethereum/common"
2323
"github.com/ethereum/go-ethereum/core/types"
24+
"github.com/ethereum/go-ethereum/crypto"
2425
"github.com/ethereum/go-ethereum/params"
2526
"github.com/holiman/uint256"
2627
"golang.org/x/crypto/sha3"
@@ -513,23 +514,70 @@ func opMstore8(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]
513514
return nil, nil
514515
}
515516

516-
var locationModulus *uint256.Int
517+
// Ciphertext metadata is stored in protected storage, in a 32-byte slot.
518+
// Currently, we only utilize 16 bytes from the slot.
519+
type ciphertextMetadata struct {
520+
refCount uint64
521+
length uint64
522+
}
523+
524+
func (m ciphertextMetadata) serialize() [32]byte {
525+
u := uint256.NewInt(0)
526+
u[0] = m.refCount
527+
u[1] = m.length
528+
return u.Bytes32()
529+
}
517530

518-
func init() {
519-
locationModulus = uint256.NewInt(0)
520-
locationModulus.SetAllOne()
521-
locationModulus[3] = 0
531+
func (m *ciphertextMetadata) deserialize(buf [32]byte) *ciphertextMetadata {
532+
u := uint256.NewInt(0)
533+
u.SetBytes(buf[:])
534+
m.refCount = u[0]
535+
m.length = u[1]
536+
return m
522537
}
523538

524-
func unprotectedLocation(loc uint256.Int) uint256.Int {
525-
return *loc.Mod(&loc, locationModulus)
539+
func newCiphertextMetadata(buf [32]byte) *ciphertextMetadata {
540+
m := ciphertextMetadata{}
541+
return m.deserialize(buf)
526542
}
527543

544+
func min(a uint64, b uint64) uint64 {
545+
if a < b {
546+
return a
547+
}
548+
return b
549+
}
550+
551+
func newInt(buf []byte) *uint256.Int {
552+
i := uint256.NewInt(0)
553+
return i.SetBytes(buf)
554+
}
555+
556+
var zero = uint256.NewInt(0).Bytes32()
557+
528558
func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
529559
loc := scope.Stack.peek()
530-
actual_loc := unprotectedLocation(*loc)
531-
hash := common.Hash(actual_loc.Bytes32())
560+
hash := common.Hash(loc.Bytes32())
532561
val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash)
562+
protectedStorage := crypto.CreateProtectedStorageContractAddress(scope.Contract.Address())
563+
protectedSlotIdx := newInt(interpreter.evm.StateDB.GetState(protectedStorage, val).Bytes())
564+
if !protectedSlotIdx.IsZero() {
565+
// If this is a ciphertext, verify it automatically.
566+
metadata := newCiphertextMetadata(protectedSlotIdx.Bytes32())
567+
ciphertext := make([]byte, metadata.length)
568+
left := metadata.length
569+
for {
570+
if left == 0 {
571+
break
572+
}
573+
bytes := interpreter.evm.StateDB.GetState(protectedStorage, protectedSlotIdx.Bytes32())
574+
toAppend := min(uint64(len(bytes)), left)
575+
left -= toAppend
576+
ciphertext = append(ciphertext, bytes[0:toAppend]...)
577+
protectedSlotIdx.AddUint64(protectedSlotIdx, 1)
578+
}
579+
interpreter.verifiedCiphertexts[val] = verifiedCiphertext{interpreter.evm.depth, ciphertext}
580+
}
533581
loc.SetBytes(val.Bytes())
534582
return nil, nil
535583
}
@@ -538,10 +586,71 @@ func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b
538586
if interpreter.readOnly {
539587
return nil, ErrWriteProtection
540588
}
541-
loc := unprotectedLocation(scope.Stack.pop())
542-
val := scope.Stack.pop()
589+
loc := scope.Stack.pop()
590+
newVal := scope.Stack.pop()
591+
newValBytes := newVal.Bytes()
592+
newValHash := common.BytesToHash(newValBytes)
593+
oldValHash := interpreter.evm.StateDB.GetState(scope.Contract.Address(), common.Hash(loc.Bytes32()))
594+
verifiedCiphertext, isVerified := interpreter.verifiedCiphertexts[newValHash]
595+
protectedStorage := crypto.CreateProtectedStorageContractAddress(scope.Contract.Address())
596+
if newValHash != oldValHash {
597+
// If the value is no longer stored in actual contract storage, garbage collect the ciphertext from protected storage or decrease the refcount by 1.
598+
existingMetadataHash := interpreter.evm.StateDB.GetState(protectedStorage, oldValHash)
599+
existingMetadataInt := newInt(existingMetadataHash.Bytes())
600+
if !existingMetadataInt.IsZero() {
601+
metadata := newCiphertextMetadata(existingMetadataInt.Bytes32())
602+
if metadata.refCount == 1 {
603+
interpreter.evm.StateDB.SetState(protectedStorage, existingMetadataHash, zero)
604+
slot := existingMetadataInt.AddUint64(existingMetadataInt, 1)
605+
slotsToZero := metadata.length / 32
606+
if metadata.length < 32 {
607+
slotsToZero++
608+
}
609+
for i := uint64(0); i < slotsToZero; i++ {
610+
interpreter.evm.StateDB.SetState(protectedStorage, slot.Bytes32(), zero)
611+
slot.AddUint64(existingMetadataInt, 1)
612+
}
613+
} else if metadata.refCount > 1 {
614+
metadata.refCount--
615+
interpreter.evm.StateDB.SetState(protectedStorage, existingMetadataHash, metadata.serialize())
616+
}
617+
}
618+
619+
// Add to protected storage or update the existing refcount.
620+
if isVerified {
621+
// If the value is a verified ciphertext, read its metadata from protected storage.
622+
metadataInt := newInt(interpreter.evm.StateDB.GetState(protectedStorage, newValHash).Bytes())
623+
metadata := ciphertextMetadata{}
624+
if metadataInt.IsZero() {
625+
// If no metadata, it means this ciphertext itself hasn't been persisted to protected storage yet. We do that as part of SSTORE.
626+
metadata.refCount = 1
627+
metadata.length = uint64(len(verifiedCiphertext.ciphertext))
628+
ciphertextSlot := newInt(newValBytes)
629+
ct := make([]byte, 32)
630+
for i, b := range verifiedCiphertext.ciphertext {
631+
if i%32 == 0 && i != 0 {
632+
interpreter.evm.StateDB.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ct))
633+
ciphertextSlot.AddUint64(ciphertextSlot, 1)
634+
ct = make([]byte, 32)
635+
} else {
636+
ct = append(ct, b)
637+
}
638+
}
639+
if len(ct) != 0 {
640+
interpreter.evm.StateDB.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ct))
641+
}
642+
} else {
643+
// If metadata exists, bump the refcount by 1.
644+
metadata := newCiphertextMetadata(interpreter.evm.StateDB.GetState(protectedStorage, newValHash))
645+
metadata.refCount++
646+
}
647+
// Save the metadata in protected storage.
648+
interpreter.evm.StateDB.SetState(protectedStorage, newValHash, metadata.serialize())
649+
}
650+
}
651+
// Set the SSTORE's value in the actual contract.
543652
interpreter.evm.StateDB.SetState(scope.Contract.Address(),
544-
loc.Bytes32(), val.Bytes32())
653+
loc.Bytes32(), newValHash)
545654
return nil, nil
546655
}
547656

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

929+
// Remove all verified ciphertexts that have depth > current depth - 1
930+
for key, verifiedCiphertext := range interpreter.verifiedCiphertexts {
931+
if verifiedCiphertext.depth > interpreter.evm.depth-1 {
932+
delete(interpreter.verifiedCiphertexts, key)
933+
}
934+
}
935+
820936
return ret, errStopToken
821937
}
822938

0 commit comments

Comments
 (0)