Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rework of extcopy, storing the offsets alongside the code #57

Draft
wants to merge 2 commits into
base: verkle-trie-proof-in-block-rebased
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/rawdb/accessors_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,25 @@ func ReadCodeWithPrefix(db ethdb.KeyValueReader, hash common.Hash) []byte {
return data
}

func ReadPushDataOffsets(db ethdb.KeyValueReader, hash common.Hash) []byte {
data, _ := db.Get(pdKey(hash))
return data
}

// WriteCode writes the provided contract code database.
func WriteCode(db ethdb.KeyValueWriter, hash common.Hash, code []byte) {
if err := db.Put(codeKey(hash), code); err != nil {
log.Crit("Failed to store contract code", "err", err)
}
}

// WritePushDataOffsets writes the provided contract code database.
func WritePushDataOffsets(db ethdb.KeyValueWriter, hash common.Hash, offsets []byte) {
if err := db.Put(pdKey(hash), offsets); err != nil {
log.Crit("Failed to store pushdata offsets for contract code", "err", err)
}
}

// DeleteCode deletes the specified contract code from the database.
func DeleteCode(db ethdb.KeyValueWriter, hash common.Hash) {
if err := db.Delete(codeKey(hash)); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions core/rawdb/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ func codeKey(hash common.Hash) []byte {
return append(CodePrefix, hash.Bytes()...)
}

// pdKey = CodePrefix + hash + "pd", so that it's right after the
// code in the database.
func pdKey(hash common.Hash) []byte {
return append(codeKey(hash), "pd"...)
}

// IsCodeKey reports whether the given byte slice is the key of contract code,
// if so return the raw code hash as well.
func IsCodeKey(key []byte) (bool, []byte) {
Expand Down
17 changes: 15 additions & 2 deletions core/state/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const (
codeCacheSize = 64 * 1024 * 1024
)

var (
errCodeNotFound = errors.New("no code found")
)

// Database wraps access to tries and contract code.
type Database interface {
// OpenTrie opens the main account trie.
Expand Down Expand Up @@ -181,7 +185,7 @@ func (db *cachingDB) ContractCode(addrHash, codeHash common.Hash) ([]byte, error
db.codeSizeCache.Add(codeHash, len(code))
return code, nil
}
return nil, errors.New("not found")
return nil, errCodeNotFound
}

// ContractCodeWithPrefix retrieves a particular contract's code. If the
Expand Down Expand Up @@ -265,7 +269,16 @@ func (db *VerkleDB) ContractCode(addrHash, codeHash common.Hash) ([]byte, error)
db.codeSizeCache.Add(codeHash, len(code))
return code, nil
}
return nil, errors.New("not found")
return nil, errCodeNotFound
}

func (db *VerkleDB) ContractCodePushData(codeHash common.Hash) ([]byte, error) {
pdoffsets := rawdb.ReadPushDataOffsets(db.db.DiskDB(), codeHash)
if len(pdoffsets) == 0 {
return nil, errCodeNotFound
}

return pdoffsets, nil
}

// ContractCodeSize retrieves a particular contracts code's size.
Expand Down
17 changes: 15 additions & 2 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,15 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash {
return common.BytesToHash(stateObject.CodeHash())
}

func (s *StateDB) GetCodePushDataOffsets(addr common.Address) []byte {
//stateObject := s.getStateObject(addr)
//if stateObject != nil {
//return stateObject.Code(s.db)
//}
//return nil
panic("not implemented")
}

// GetState retrieves a value from the given account's storage trie.
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
stateObject := s.getStateObject(addr)
Expand Down Expand Up @@ -494,7 +503,9 @@ func (s *StateDB) updateStateObject(obj *stateObject) {
}

if obj.dirtyCode {
if chunks, err := trie.ChunkifyCode(addr, obj.code); err == nil {
// Since the DB isn't updated with the code, don't update
// the offsets either.
if _, chunks, err := trie.ChunkifyCode(obj.code); err == nil {
for i := range chunks {
s.trie.TryUpdate(trieUtils.GetTreeKeyCodeChunk(addr[:], uint256.NewInt(uint64(i))), chunks[i][:])
}
Expand Down Expand Up @@ -996,10 +1007,12 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// Write any contract code associated with the state object
if obj.code != nil && obj.dirtyCode {
if s.trie.IsVerkle() {
if chunks, err := trie.ChunkifyCode(addr, obj.code); err == nil {
if offsets, chunks, err := trie.ChunkifyCode(obj.code); err == nil {
for i := range chunks {
s.trie.TryUpdate(trieUtils.GetTreeKeyCodeChunk(addr[:], uint256.NewInt(uint64(i))), chunks[i][:])
}

rawdb.WritePushDataOffsets(codeWriter, common.BytesToHash(obj.CodeHash()), offsets)
} else {
s.setError(err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/vm/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (bits bitvec) set16(pos uint64) {
}

// IsCode checks if the position is in a code segment.
func (bits *bitvec) IsCode(pos uint64) bool {
func (bits *bitvec) codeSegment(pos uint64) bool {
return ((*bits)[pos/8] & (0x80 >> (pos % 8))) == 0
}

Expand Down
15 changes: 4 additions & 11 deletions core/vm/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,6 @@ type ContractRef interface {
Address() common.Address
}

// AnalyzedContract is an interface for a piece of contract
// code that has undegone jumpdest analysis, and whose bytecode
// can be queried to determine if it is "contract code"
type AnalyzedContract interface {
IsCode(dest uint64) bool
}

// AccountRef implements ContractRef.
//
// Account references are used during EVM initialisation and
Expand Down Expand Up @@ -65,7 +58,7 @@ type Contract struct {
CodeAddr *common.Address
Input []byte

// is the execution frame represented by this object a contract deployment
// is the execution frame represented by this object a contract deployment
IsDeployment bool

Gas uint64
Expand Down Expand Up @@ -111,7 +104,7 @@ func (c *Contract) validJumpdest(dest *uint256.Int) bool {
func (c *Contract) IsCode(udest uint64) bool {
// Do we already have an analysis laying around?
if c.analysis != nil {
return c.analysis.IsCode(udest)
return c.analysis.codeSegment(udest)
}
// Do we have a contract hash already?
// If we do have a hash, that means it's a 'regular' contract. For regular
Expand All @@ -127,7 +120,7 @@ func (c *Contract) IsCode(udest uint64) bool {
}
// Also stash it in current contract for faster access
c.analysis = analysis
return analysis.IsCode(udest)
return analysis.codeSegment(udest)
}
// We don't have the code hash, most likely a piece of initcode not already
// in state trie. In that case, we do an analysis, and save it locally, so
Expand All @@ -136,7 +129,7 @@ func (c *Contract) IsCode(udest uint64) bool {
if c.analysis == nil {
c.analysis = codeBitmap(c.Code)
}
return c.analysis.IsCode(udest)
return c.analysis.codeSegment(udest)
}

// AsDelegate sets the contract to be a delegate call and returns the current
Expand Down
14 changes: 11 additions & 3 deletions core/vm/gas_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ func gasCodeCopy(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
if overflow {
uint64Length = 0xffffffffffffffff
}

_, offset, nonPaddedSize := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, uint64Length)
statelessGas = touchEachChunksAndChargeGas(offset, nonPaddedSize, contract.Address().Bytes()[:], nil, nil, evm.Accesses)
// TODO check it's contract.Address() and not contract.CodeHash
statelessGas = touchEachChunksAndChargeGas(offset, nonPaddedSize, contract.Address().Bytes()[:], nil, evm.Accesses, evm.StateDB.GetCodePushDataOffsets(contract.Address()))
}
usedGas, err := gasCodeCopyStateful(evm, contract, stack, mem, memorySize)
return usedGas + statelessGas, err
Expand All @@ -145,11 +147,17 @@ func gasExtCodeCopy(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
}
// note: we must charge witness costs for the specified range regardless of whether it
// is in-bounds of the actual target account code. This is because we must charge the cost
// before hitting the db to be able to now what the actual code size is. This is different
// before hitting the db to be able to know what the actual code size is. This is different
// behavior from CODECOPY which only charges witness access costs for the part of the range
// which overlaps in the account code. TODO: clarify this is desired behavior and amend the
// spec.
statelessGas = touchEachChunksAndChargeGas(uint64CodeOffset, uint64Length, targetAddr[:], nil, nil, evm.Accesses)
// XXX there is a problem here, because the DB is hit twice to
// get the offsets, once here and then once in the opcode impl.
// To make things worse, offsets are not really needed at this
// stage, only the address needs to be known. I fell this func
// has to be broken down into two: one that needs to know what
// the offsets are, and one that doesn't.
statelessGas = touchEachChunksAndChargeGas(uint64CodeOffset, uint64Length, targetAddr[:], nil, evm.Accesses, evm.StateDB.GetCodePushDataOffsets(targetAddr))
}
usedGas, err := gasExtCodeCopyStateful(evm, contract, stack, mem, memorySize)
return usedGas + statelessGas, err
Expand Down
62 changes: 18 additions & 44 deletions core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
trieUtils "github.com/ethereum/go-ethereum/trie/utils"
"github.com/holiman/uint256"
"golang.org/x/crypto/sha3"
Expand Down Expand Up @@ -372,21 +373,23 @@ func opCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([

paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(scope.Contract.Code, uint64CodeOffset, length.Uint64())
if interpreter.evm.chainConfig.IsCancun(interpreter.evm.Context.BlockNumber) {
touchEachChunksAndChargeGas(copyOffset, nonPaddedCopyLength, scope.Contract.Address().Bytes()[:], scope.Contract.Code, scope.Contract, interpreter.evm.Accesses)
// XXX cache the offsets in Contract so that I don't have to
// load it more than once.
touchEachChunksAndChargeGas(copyOffset, nonPaddedCopyLength, scope.Contract.Address().Bytes()[:], scope.Contract, interpreter.evm.Accesses, interpreter.evm.StateDB.GetCodePushDataOffsets(scope.Contract.Address()))
}
scope.Memory.Set(memOffset.Uint64(), uint64(len(paddedCodeCopy)), paddedCodeCopy)
return nil, nil
}

// touchEachChunksAndChargeGas is a helper function to touch every chunk in a code range and charge witness gas costs
func touchEachChunksAndChargeGas(offset, size uint64, address []byte, code []byte, contract AnalyzedContract, accesses *types.AccessWitness) uint64 {
func touchEachChunksAndChargeGas(offset, size uint64, address []byte, contract *Contract, accesses *types.AccessWitness, offsets []byte) uint64 {
// note that in the case where the copied code is outside the range of the
// contract code but touches the last leaf with contract code in it,
// we don't include the last leaf of code in the AccessWitness. The
// reason that we do not need the last leaf is the account's code size
// is already in the AccessWitness so a stateless verifier can see that
// the code from the last leaf is not needed.
if contract != nil && (size == 0 || offset > uint64(len(code))) {
if contract != nil && (size == 0 || offset > uint64(len(contract.Code))) {
return 0
}
var (
Expand All @@ -396,58 +399,27 @@ func touchEachChunksAndChargeGas(offset, size uint64, address []byte, code []byt
startOffset uint64
endOffset uint64
numLeaves uint64
index [32]byte
)
// startLeafOffset, endLeafOffset is the evm code offset of the first byte in the first leaf touched
// and the evm code offset of the last byte in the last leaf touched
startOffset = offset - (offset % 31)
if contract != nil && startOffset+size > uint64(len(code)) {
endOffset = uint64(len(code))
if contract != nil && startOffset+size > uint64(len(contract.Code)) {
endOffset = uint64(len(contract.Code))
} else {
endOffset = startOffset + size
}
endLeafOffset = endOffset + (endOffset % 31)
numLeaves = (endLeafOffset - startLeafOffset) / 31
chunkOffset := new(uint256.Int)
treeIndex := new(uint256.Int)

for i := 0; i < int(numLeaves); i++ {
chunkOffset.Add(trieUtils.CodeOffset, uint256.NewInt(uint64(i)))
treeIndex.Div(chunkOffset, trieUtils.VerkleNodeWidth)
var subIndex byte
subIndexMod := chunkOffset.Mod(chunkOffset, trieUtils.VerkleNodeWidth).Bytes()
if len(subIndexMod) == 0 {
subIndex = 0
} else {
subIndex = subIndexMod[0]
}
treeKey := trieUtils.GetTreeKey(address, treeIndex, subIndex)
copy(index[0:31], treeKey)
index[31] = subIndex

var value []byte
if len(code) > 0 {
// the offset into the leaf that the first PUSH occurs
var firstPushOffset uint64 = 0
// Look for the first code byte (i.e. no pushdata)
for ; firstPushOffset < 31 && firstPushOffset+uint64(i)*31 < uint64(len(code)) && !contract.IsCode(uint64(i)*31+firstPushOffset); firstPushOffset++ {
}
curEnd := (uint64(i) + 1) * 31
if curEnd > endOffset {
curEnd = endOffset
}
valueSize := curEnd - (uint64(i) * 31)
value = make([]byte, 32, 32)
value[0] = byte(firstPushOffset)

copy(value[1:valueSize+1], code[i*31:curEnd])
if valueSize < 31 {
padding := make([]byte, 31-valueSize, 31-valueSize)
copy(value[valueSize+1:], padding)
}
}
treeKey := trieUtils.GetTreeKeyCodeChunk(address, chunkOffset)
value := trie.ChunkFromCodeAndPushDataOffsets(contract.Code, offsets, i)

statelessGasCharged += accesses.TouchAddressAndChargeGas(index[:], value)
// XXX is it really needed to set the value here, in every case?
// definitely not when this functions is called from the gas code.
statelessGasCharged += accesses.TouchAddressAndChargeGas(treeKey[:], value)
}

return statelessGasCharged
Expand All @@ -467,10 +439,11 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
}
addr := common.Address(a.Bytes20())
if interpreter.evm.chainConfig.IsCancun(interpreter.evm.Context.BlockNumber) {
// XXX check that when offsets are written, they have the right length
code := interpreter.evm.StateDB.GetCode(addr)
offsets := interpreter.evm.StateDB.GetCodePushDataOffsets(addr)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64())
cb := codeBitmap(code)
touchEachChunksAndChargeGas(copyOffset, nonPaddedCopyLength, addr[:], code, &cb, interpreter.evm.Accesses)
touchEachChunksAndChargeGas(copyOffset, nonPaddedCopyLength, addr[:], scope.Contract, interpreter.evm.Accesses, offsets)
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), paddedCodeCopy)
} else {
codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
Expand Down Expand Up @@ -976,7 +949,8 @@ func makePush(size uint64, pushByteSize int) executionFunc {
}

if interpreter.evm.chainConfig.IsCancun(interpreter.evm.Context.BlockNumber) {
statelessGas := touchEachChunksAndChargeGas(uint64(startMin), uint64(pushByteSize), scope.Contract.Address().Bytes()[:], scope.Contract.Code, scope.Contract, interpreter.evm.Accesses)
offsets := interpreter.evm.StateDB.GetCodePushDataOffsets(scope.Contract.Address())
statelessGas := touchEachChunksAndChargeGas(uint64(startMin), uint64(pushByteSize), scope.Contract.Address().Bytes()[:], scope.Contract, interpreter.evm.Accesses, offsets)
scope.Contract.UseGas(statelessGas)
}

Expand Down
1 change: 1 addition & 0 deletions core/vm/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type StateDB interface {
GetCode(common.Address) []byte
SetCode(common.Address, []byte)
GetCodeSize(common.Address) int
GetCodePushDataOffsets(common.Address) []byte

AddRefund(uint64)
SubRefund(uint64)
Expand Down
3 changes: 2 additions & 1 deletion core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
if in.evm.ChainConfig().IsCancun(in.evm.Context.BlockNumber) && !contract.IsDeployment {
// if the PC ends up in a new "page" of verkleized code, charge the
// associated witness costs.
contract.Gas -= touchEachChunksAndChargeGas(pc, 1, contract.Address().Bytes()[:], contract.Code, contract, in.evm.TxContext.Accesses)
offsets := in.evm.StateDB.GetCodePushDataOffsets(contract.Address())
contract.Gas -= touchEachChunksAndChargeGas(pc, 1, contract.Address().Bytes()[:], contract, in.evm.TxContext.Accesses, offsets)
}

// TODO how can we tell if we are in stateless mode here and need to get the op from the witness
Expand Down
Loading