Skip to content
Open
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
34 changes: 32 additions & 2 deletions giga/evmonly/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,12 +869,42 @@ func TestExecutorRegisteredCustomPrecompile(t *testing.T) {
require.NoError(t, err)
require.Len(t, result.Txs, 1)
require.Equal(t, ethtypes.ReceiptStatusSuccessful, result.Txs[0].Status)
require.Greater(t, result.Txs[0].GasUsed, uint64(21_000+100))
require.NotEmpty(t, result.ChangeSet.Storage)

state.ApplyChangeSet(result.ChangeSet)
require.Equal(t, encodedStoredLength(2), state.GetState(customAddr, storeBaseSlot([]byte("seen"))))
}

func TestExecutorRegisteredCustomPrecompileMetersDynamicStoreGas(t *testing.T) {
chainID := big.NewInt(713715)
key, err := crypto.GenerateKey()
require.NoError(t, err)
sender := crypto.PubkeyToAddress(key.PublicKey)
customAddr := common.HexToAddress("0x0000000000000000000000000000000000001005")

state := NewMemoryState()
state.SetBalance(sender, big.NewInt(200_000_000_000_000))

rawTx := signLegacyTxWithGas(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}, 30_000)
executor := NewExecutor(Config{
CustomPrecompiles: contractPrecompileRegistry{
customAddr: storeWritePrecompile{},
},
}, WithState(state))

result, err := executor.ExecuteBlock(context.Background(), BlockRequest{
Context: blockContext(chainID),
Txs: [][]byte{rawTx},
})

require.NoError(t, err)
require.Len(t, result.Txs, 1)
require.Equal(t, ethtypes.ReceiptStatusFailed, result.Txs[0].Status)
require.True(t, errors.Is(result.Txs[0].Err, vm.ErrOutOfGas))
require.Empty(t, result.ChangeSet.Storage)
}

func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) {
chainID := big.NewInt(713715)
key, err := crypto.GenerateKey()
Expand All @@ -901,7 +931,7 @@ func TestExecutorStakingPrecompileForwardsPayableValue(t *testing.T) {
)
require.NoError(t, err)
value := sei(5)
rawTx := signLegacyTx(t, key, chainID, 0, &stakingAddr, value, input)
rawTx := signLegacyTxWithGas(t, key, chainID, 0, &stakingAddr, value, input, 8_000_000)
executor := NewExecutor(Config{
CustomPrecompiles: registry,
}, WithState(state))
Expand Down Expand Up @@ -953,7 +983,7 @@ func TestExecutorStakingDelegationLifecycleE2E(t *testing.T) {
nonces := map[common.Address]uint64{}
signStakingTx := func(key *ecdsa.PrivateKey, value *big.Int, input []byte) []byte {
sender := crypto.PubkeyToAddress(key.PublicKey)
raw := signLegacyTx(t, key, chainID, nonces[sender], &stakingAddr, value, input)
raw := signLegacyTxWithGas(t, key, chainID, nonces[sender], &stakingAddr, value, input, 8_000_000)
nonces[sender]++
return raw
}
Expand Down
84 changes: 67 additions & 17 deletions giga/evmonly/precompile_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,22 @@ func (p registeredCustomPrecompile) RequiredGas(input []byte) uint64 {
}

func (p registeredCustomPrecompile) Run(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, _ *tracing.Hooks) ([]byte, error) {
return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0)
return p.run(evm, caller, input, value, readOnly, isFromDelegateCall, 0, nil)
}

func (p registeredCustomPrecompile) RunAndCalculateGas(evm *vm.EVM, caller common.Address, _ common.Address, input []byte, suppliedGas uint64, value *big.Int, hooks *tracing.Hooks, readOnly bool, isFromDelegateCall bool) ([]byte, uint64, error) {
gasCost := p.RequiredGas(input)
if suppliedGas < gasCost {
meter := newPrecompileGasMeter(suppliedGas, hooks)
if !meter.charge(p.RequiredGas(input), tracing.GasChangeCallPrecompiledContract) {
return nil, 0, vm.ErrOutOfGas
}
remainingGas := suppliedGas - gasCost
if hooks != nil && hooks.OnGasChange != nil {
hooks.OnGasChange(suppliedGas, remainingGas, tracing.GasChangeCallPrecompiledContract)
ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, meter.remainingGas(), meter)
if meter.err != nil {
return nil, meter.remainingGas(), meter.err
}
ret, err := p.run(evm, caller, input, value, readOnly, isFromDelegateCall, remainingGas)
return ret, remainingGas, err
return ret, meter.remainingGas(), err
}

func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64) ([]byte, error) {
func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, input []byte, value *big.Int, readOnly bool, isFromDelegateCall bool, remainingGas uint64, meter *precompileGasMeter) ([]byte, error) {
stateDB, ok := evm.StateDB.(*nativeStateDB)
if !ok {
return nil, errInvalidPrecompileStateDB
Expand All @@ -65,9 +64,9 @@ func (p registeredCustomPrecompile) run(evm *vm.EVM, caller common.Address, inpu
DelegateCall: isFromDelegateCall,
GasRemaining: remainingGas,
Block: evmPrecompileBlockContext(evm),
Store: storageBackedStore{db: stateDB, address: p.address},
Balances: nativeBalanceTransfer{db: stateDB},
Logs: stateDB,
Store: storageBackedStore{db: stateDB, address: p.address, meter: meter},
Balances: nativeBalanceTransfer{db: stateDB, meter: meter},
Logs: meteredLogSink{sink: stateDB, meter: meter},
}
return p.contract.Run(ctx, input)
}
Expand Down Expand Up @@ -155,13 +154,17 @@ func evmPrecompileBlockContext(evm *vm.EVM) precompiles.BlockContext {
}

type nativeBalanceTransfer struct {
db *nativeStateDB
db *nativeStateDB
meter *precompileGasMeter
}

func (t nativeBalanceTransfer) Transfer(from common.Address, to common.Address, amount *big.Int) error {
if amount == nil || amount.Sign() == 0 {
return nil
}
if t.meter != nil && !t.meter.chargeNativeTransfer(t.db, from, to, amount) {
return t.meter.err
}
if t.db.err != nil {
return t.db.err
}
Expand Down Expand Up @@ -207,9 +210,13 @@ const (
type storageBackedStore struct {
db *nativeStateDB
address common.Address
meter *precompileGasMeter
}

func (s storageBackedStore) Get(key []byte) ([]byte, bool) {
if !s.chargeStoreBaseSlot(key) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Silent no-op on out-of-gas: when chargeStoreBaseSlot (or a later charge) fails, Get returns (nil,false) — indistinguishable from "key not found" — and Set/Delete return without applying the write. Correctness here depends entirely on RunAndCalculateGas re-checking meter.err after run() returns (which it does). It works and is tested, but it's a subtle invariant: any future precompile that acts on a read's result before returning would see empty data on OOG. A short comment documenting this contract would help.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] When a gas charge fails mid-read, Get returns (nil, false) — the same signal as "key not found". A precompile could therefore branch as if the key were absent rather than aborting. This is safe here only because meter.err is set and RunAndCalculateGas unconditionally converts a set meter.err into vm.ErrOutOfGas, overriding any success the contract subsequently returns. Worth a short comment noting this conflation is intentionally masked by the meter, so a future refactor doesn't accidentally rely on the false meaning "not found" after an OOG.

return nil, false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] On out-of-gas mid-read, Get returns nil, false, which is indistinguishable from "key not found" to the precompile. This is currently safe only because meter.err is sticky (once set, every subsequent charge returns false, so further Store/Balance/Log ops become no-ops) and RunAndCalculateGas returns meter.err (ErrOutOfGas) regardless of the precompile's return value — so the tx reverts and the changeset is discarded. The invariant is subtle and load-bearing: any future code path that runs a metered precompile without checking meter.err afterward, or that acts irreversibly on a "not found" result, would silently misprice/misbehave. Consider documenting this invariant with a comment here (and at the meter.err check in RunAndCalculateGas).

}
baseSlot := storeBaseSlot(key)
length, ok := s.length(baseSlot)
if !ok {
Expand All @@ -221,13 +228,23 @@ func (s storageBackedStore) Get(key []byte) ([]byte, bool) {
chunks := chunkCount(length)
out := make([]byte, 0, int(chunks*32)) //nolint:gosec // length was bounded by max int above.
for i := uint64(0); i < chunks; i++ {
chunk := s.db.GetState(s.address, storeChunkSlot(baseSlot, i))
if !s.chargeStoreChunkSlot(baseSlot, i) {
return nil, false
}
chunkSlot := storeChunkSlot(baseSlot, i)
if !s.chargeSLoad(chunkSlot) {
return nil, false
}
chunk := s.db.GetState(s.address, chunkSlot)
out = append(out, chunk.Bytes()...)
}
return out[:int(length)], true //nolint:gosec // length was bounded by max int above.
}

func (s storageBackedStore) Set(key []byte, value []byte) {
if !s.chargeStoreBaseSlot(key) {
return
}
baseSlot := storeBaseSlot(key)
oldLength, oldOK := s.length(baseSlot)
oldChunks := uint64(0)
Expand All @@ -236,6 +253,9 @@ func (s storageBackedStore) Set(key []byte, value []byte) {
}
newLength := uint64(len(value)) //nolint:gosec // slices cannot exceed max int.
newChunks := chunkCount(newLength)
if !s.chargeSStore(baseSlot, encodedStoredLength(newLength)) {
return
}
s.db.SetState(s.address, baseSlot, encodedStoredLength(newLength))
for i := uint64(0); i < newChunks; i++ {
start := int(i * 32) //nolint:gosec // i is bounded by len(value) chunks.
Expand All @@ -245,26 +265,56 @@ func (s storageBackedStore) Set(key []byte, value []byte) {
}
var chunk common.Hash
copy(chunk[:], value[start:end])
s.db.SetState(s.address, storeChunkSlot(baseSlot, i), chunk)
if !s.chargeStoreChunkSlot(baseSlot, i) {
return
}
chunkSlot := storeChunkSlot(baseSlot, i)
if !s.chargeSStore(chunkSlot, chunk) {
return
}
s.db.SetState(s.address, chunkSlot, chunk)
}
for i := newChunks; i < oldChunks; i++ {
s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{})
if !s.chargeStoreChunkSlot(baseSlot, i) {
return
}
chunkSlot := storeChunkSlot(baseSlot, i)
if !s.chargeSStore(chunkSlot, common.Hash{}) {
return
}
s.db.SetState(s.address, chunkSlot, common.Hash{})
}
}

func (s storageBackedStore) Delete(key []byte) {
if !s.chargeStoreBaseSlot(key) {
return
}
baseSlot := storeBaseSlot(key)
length, ok := s.length(baseSlot)
if !ok {
return
}
for i := uint64(0); i < chunkCount(length); i++ {
s.db.SetState(s.address, storeChunkSlot(baseSlot, i), common.Hash{})
if !s.chargeStoreChunkSlot(baseSlot, i) {
return
}
chunkSlot := storeChunkSlot(baseSlot, i)
if !s.chargeSStore(chunkSlot, common.Hash{}) {
return
}
s.db.SetState(s.address, chunkSlot, common.Hash{})
}
if !s.chargeSStore(baseSlot, common.Hash{}) {
return
}
s.db.SetState(s.address, baseSlot, common.Hash{})
}

func (s storageBackedStore) length(baseSlot common.Hash) (uint64, bool) {
if !s.chargeSLoad(baseSlot) {
return 0, false
}
encoded := s.db.GetState(s.address, baseSlot)
if encoded == (common.Hash{}) {
return 0, false
Expand Down
Loading
Loading