Refactor witness-accumulation in EVM (#42)

* make push dynamically-charged.  charge witness gas costs for push.  refactor evm witness gas charging to move logic for touching a range of bytecode into a helper method 'touchEachChunksAndChargeGas'

* add witness gas calculation for CodeCopy, ExtCodeCopy, SLoad back to gas_table.go

* witness gas charging for CALL

* remove explicit reference to evm.TxContext

* core/vm: make touchEachChunksAndCharge gas handle nil code value

* core/vm: call implementation, separate out witnesses into touch/set

* some fixes

* remove witness touching from opCall: this will go in evm.go

* remove witness touching for call from gas_table.go

* (hopefully) fix tests

* add SSTORE witness charging that was removed mistakenly

* charge witness gas for call

* clean up and comment touchEachChunksAndChargeGas

* make suggested changes

* address remaining points

* fix build issues

* remove double-charging for contract creation witness gas charging
This commit is contained in:
jwasinger
2021-12-16 00:21:59 -10:00
committed by GitHub
parent 6af78cba9e
commit 99ebf767b9
8 changed files with 188 additions and 206 deletions

View File

@@ -19,6 +19,7 @@ package vm
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
trieUtils "github.com/ethereum/go-ethereum/trie/utils"
"github.com/holiman/uint256"
@@ -343,9 +344,10 @@ func opReturnDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte
func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
slot := scope.Stack.peek()
cs := uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20()))
if interpreter.evm.accesses != nil {
if interpreter.evm.Accesses != nil {
index := trieUtils.GetTreeKeyCodeSize(slot.Bytes())
interpreter.evm.TxContext.Accesses.TouchAddress(index, uint256.NewInt(cs).Bytes())
statelessGas := interpreter.evm.Accesses.TouchAddressAndChargeGas(index, uint256.NewInt(cs).Bytes())
scope.Contract.UseGas(statelessGas)
}
slot.SetUint64(cs)
return nil, nil
@@ -368,63 +370,92 @@ func opCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([
if overflow {
uint64CodeOffset = 0xffffffffffffffff
}
uint64CodeEnd, overflow := new(uint256.Int).Add(&codeOffset, &length).Uint64WithOverflow()
if overflow {
uint64CodeEnd = 0xffffffffffffffff
}
if interpreter.evm.accesses != nil {
copyCodeFromAccesses(scope.Contract.Address(), uint64CodeOffset, uint64CodeEnd, memOffset.Uint64(), interpreter, scope)
} else {
codeCopy := getData(scope.Contract.Code, uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
touchEachChunks(uint64CodeOffset, uint64CodeEnd, codeCopy, scope.Contract, interpreter.evm)
paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(scope.Contract.Code, uint64CodeOffset, length.Uint64())
if interpreter.evm.Accesses != nil {
touchEachChunksAndChargeGas(copyOffset, nonPaddedCopyLength, scope.Contract.Address().Bytes()[:], scope.Contract, interpreter.evm.Accesses)
}
scope.Memory.Set(memOffset.Uint64(), uint64(len(paddedCodeCopy)), paddedCodeCopy)
return nil, nil
}
// Helper function to touch every chunk in a code range
func touchEachChunks(start, end uint64, code []byte, contract *Contract, evm *EVM) {
for chunk := start / 31; chunk <= end/31 && chunk <= uint64(len(code))/31; chunk++ {
index := trieUtils.GetTreeKeyCodeChunk(contract.Address().Bytes(), uint256.NewInt(chunk))
count := uint64(0)
end := (chunk + 1) * 31
// Look for the first code byte (i.e. no pushdata)
for ; count < 31 && end+count < uint64(len(contract.Code)) && !contract.IsCode(chunk*31+count); count++ {
}
var value [32]byte
value[0] = byte(count)
if end > uint64(len(code)) {
end = uint64(len(code))
}
copy(value[1:], code[chunk*31:end])
evm.Accesses.TouchAddress(index, value[:])
// 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, contract *Contract, accesses *types.AccessWitness) 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(contract.Code))) {
return 0
}
}
var (
statelessGasCharged uint64
startLeafOffset uint64
endLeafOffset uint64
startOffset uint64
endOffset uint64
numLeaves uint64
code []byte
index [32]byte
)
if contract != nil {
code = contract.Code[:]
}
// 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(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)
// copyCodeFromAccesses perform codecopy from the witness, not from the db.
func copyCodeFromAccesses(addr common.Address, codeOffset, codeEnd, memOffset uint64, in *EVMInterpreter, scope *ScopeContext) {
chunk := codeOffset / 31
endChunk := codeEnd / 31
start := codeOffset % 31 // start inside the first code chunk
offset := uint64(0) // memory offset to write to
// XXX uint64 overflow in condition check
for end := uint64(31); chunk < endChunk; chunk, start = chunk+1, 0 {
// case of the last chunk: figure out how many bytes need to
// be extracted from the last chunk.
if chunk+1 == endChunk {
end = codeEnd % 31
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 contract != nil {
// 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(contract.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)
}
}
// TODO make a version of GetTreeKeyCodeChunk without the bigint
index := common.BytesToHash(trieUtils.GetTreeKeyCodeChunk(addr[:], uint256.NewInt(chunk)))
h := in.evm.accesses[index]
//in.evm.Accesses.TouchAddress(index.Bytes(), h[1+start:1+end])
scope.Memory.Set(memOffset+offset, end-start, h[1+start:end])
offset += 31 - start
statelessGasCharged += accesses.TouchAddressAndChargeGas(index[:], value)
}
return statelessGasCharged
}
func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
@@ -439,18 +470,12 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
if overflow {
uint64CodeOffset = 0xffffffffffffffff
}
uint64CodeEnd, overflow := new(uint256.Int).Add(&codeOffset, &length).Uint64WithOverflow()
if overflow {
uint64CodeEnd = 0xffffffffffffffff
}
addr := common.Address(a.Bytes20())
if interpreter.evm.accesses != nil {
copyCodeFromAccesses(addr, uint64CodeOffset, uint64CodeEnd, memOffset.Uint64(), interpreter, scope)
if interpreter.evm.Accesses != nil {
log.Warn("setting witness values for extcodecopy is not currently implemented")
} else {
codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
touchEachChunks(uint64CodeOffset, uint64CodeEnd, codeCopy, scope.Contract, interpreter.evm)
}
return nil, nil
@@ -579,10 +604,11 @@ func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by
hash := common.Hash(loc.Bytes32())
val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash)
loc.SetBytes(val.Bytes())
// Get the initial value as it might not be present
index := trieUtils.GetTreeKeyStorageSlot(scope.Contract.Address().Bytes(), loc)
interpreter.evm.TxContext.Accesses.TouchAddress(index, val.Bytes())
if interpreter.evm.Accesses != nil {
index := trieUtils.GetTreeKeyStorageSlot(scope.Contract.Address().Bytes(), loc)
interpreter.evm.Accesses.TouchAddressAndChargeGas(index, val.Bytes())
}
return nil, nil
}
@@ -907,9 +933,11 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by
*pc += 1
if *pc < codeLen {
scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc])))
// touch next chunk if PUSH1 is at the boundary. if so, *pc has
// advanced past this boundary.
if *pc%31 == 0 {
if interpreter.evm.Accesses != nil && *pc%31 == 0 {
// touch next chunk if PUSH1 is at the boundary. if so, *pc has
// advanced past this boundary.
// touch push data by adding the last byte of the pushdata
var value [32]byte
chunk := *pc / 31
@@ -924,7 +952,8 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by
}
copy(value[1:], scope.Contract.Code[chunk*31:endMin])
index := trieUtils.GetTreeKeyCodeChunk(scope.Contract.Address().Bytes(), uint256.NewInt(chunk))
interpreter.evm.TxContext.Accesses.TouchAddressAndChargeGas(index, nil)
statelessGas := interpreter.evm.Accesses.TouchAddressAndChargeGas(index, nil)
scope.Contract.UseGas(statelessGas)
}
} else {
scope.Stack.push(integer.Clear())
@@ -947,43 +976,15 @@ func makePush(size uint64, pushByteSize int) executionFunc {
endMin = startMin + pushByteSize
}
if interpreter.evm.Accesses != nil {
statelessGas := touchEachChunksAndChargeGas(uint64(startMin), uint64(pushByteSize), scope.Contract.Address().Bytes()[:], scope.Contract, interpreter.evm.Accesses)
scope.Contract.UseGas(statelessGas)
}
integer := new(uint256.Int)
scope.Stack.push(integer.SetBytes(common.RightPadBytes(
scope.Contract.Code[startMin:endMin], pushByteSize)))
// touch push data by adding the last byte of the pushdata
var value [32]byte
chunk := uint64(endMin-1) / 31
count := uint64(0)
// Look for the first code byte (i.e. no pushdata)
for ; count < 31 && !scope.Contract.IsCode(chunk*31+count); count++ {
}
value[0] = byte(count)
copy(value[1:], scope.Contract.Code[chunk*31:endMin])
index := trieUtils.GetTreeKeyCodeChunk(scope.Contract.Address().Bytes(), uint256.NewInt(chunk))
interpreter.evm.TxContext.Accesses.TouchAddressAndChargeGas(index, nil)
// in the case of PUSH32, the end data might be two chunks away,
// so also get the middle chunk. There is a boundary condition
// check (endMin > 2) in the case the code is a single PUSH32
// insctruction, whose immediate are just 0s.
if pushByteSize == 32 && endMin > 2 {
chunk = uint64(endMin-2) / 31
count = uint64(0)
// Look for the first code byte (i.e. no pushdata)
for ; count < 31 && !scope.Contract.IsCode(chunk*31+count); count++ {
}
value[0] = byte(count)
end := (chunk + 1) * 31
if end > uint64(len(scope.Contract.Code)) {
end = uint64(len(scope.Contract.Code))
}
copy(value[1:], scope.Contract.Code[chunk*31:end])
index := trieUtils.GetTreeKeyCodeChunk(scope.Contract.Address().Bytes(), uint256.NewInt(chunk))
interpreter.evm.TxContext.Accesses.TouchAddressAndChargeGas(index, nil)
}
*pc += size
return nil, nil
}