diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 2187cb67d2..7487148829 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -757,12 +757,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() @@ -789,7 +819,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)) @@ -841,7 +871,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 } diff --git a/giga/evmonly/precompile_adapter.go b/giga/evmonly/precompile_adapter.go index d023f88399..9a2348c166 100644 --- a/giga/evmonly/precompile_adapter.go +++ b/giga/evmonly/precompile_adapter.go @@ -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 @@ -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) } @@ -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 } @@ -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) { + return nil, false + } baseSlot := storeBaseSlot(key) length, ok := s.length(baseSlot) if !ok { @@ -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) @@ -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. @@ -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 diff --git a/giga/evmonly/precompile_gas.go b/giga/evmonly/precompile_gas.go new file mode 100644 index 0000000000..0207474a08 --- /dev/null +++ b/giga/evmonly/precompile_gas.go @@ -0,0 +1,231 @@ +package evmonly + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + + "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" +) + +const maxGas = ^uint64(0) + +type precompileGasMeter struct { + remaining uint64 + hooks *tracing.Hooks + err error +} + +func newPrecompileGasMeter(suppliedGas uint64, hooks *tracing.Hooks) *precompileGasMeter { + return &precompileGasMeter{ + remaining: suppliedGas, + hooks: hooks, + } +} + +func (m *precompileGasMeter) remainingGas() uint64 { + if m == nil { + return 0 + } + return m.remaining +} + +func (m *precompileGasMeter) charge(gas uint64, reason tracing.GasChangeReason) bool { + if m == nil { + return true + } + if m.err != nil { + return false + } + if gas == 0 { + return true + } + if m.remaining < gas { + m.fail(vm.ErrOutOfGas, reason) + return false + } + old := m.remaining + m.remaining -= gas + m.emitGasChange(old, m.remaining, reason) + return true +} + +func (m *precompileGasMeter) fail(err error, reason tracing.GasChangeReason) { + if m.err == nil { + m.err = err + } + if m.remaining == 0 { + return + } + old := m.remaining + m.remaining = 0 + m.emitGasChange(old, 0, reason) +} + +func (m *precompileGasMeter) emitGasChange(old uint64, next uint64, reason tracing.GasChangeReason) { + if m.hooks != nil && m.hooks.OnGasChange != nil { + m.hooks.OnGasChange(old, next, reason) + } +} + +func (m *precompileGasMeter) chargeKeccak(size int) bool { + sizeU64 := uint64(size) //nolint:gosec // slice/string lengths are non-negative and bounded by max int. + words := wordCount(sizeU64) + return m.charge(gasAdd(params.Keccak256Gas, gasMul(params.Keccak256WordGas, words)), tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) chargeSLoad(db *nativeStateDB, addr common.Address, slot common.Hash) bool { + if _, slotPresent := db.SlotInAccessList(addr, slot); !slotPresent { + db.AddSlotToAccessList(addr, slot) + return m.charge(params.ColdSloadCostEIP2929, tracing.GasChangeCallStorageColdAccess) + } + return m.charge(params.WarmStorageReadCostEIP2929, tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) chargeSStore(db *nativeStateDB, addr common.Address, slot common.Hash, value common.Hash) bool { + if m.remaining <= params.SstoreSentryGasEIP2200 { + m.fail(vm.ErrOutOfGas, tracing.GasChangeCallPrecompiledContract) + return false + } + cost := uint64(0) + if _, slotPresent := db.SlotInAccessList(addr, slot); !slotPresent { + cost = params.ColdSloadCostEIP2929 + db.AddSlotToAccessList(addr, slot) + } + current := db.GetState(addr, slot) + if current == value { + return m.charge(gasAdd(cost, params.WarmStorageReadCostEIP2929), tracing.GasChangeCallPrecompiledContract) + } + original := db.GetCommittedState(addr, slot) + if original == current { + if original == (common.Hash{}) { + return m.charge(gasAdd(cost, params.SstoreSetGasEIP2200), tracing.GasChangeCallPrecompiledContract) + } + if value == (common.Hash{}) { + db.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + return m.charge(gasAdd(cost, params.SstoreResetGasEIP2200-params.ColdSloadCostEIP2929), tracing.GasChangeCallPrecompiledContract) + } + m.adjustDirtySStoreRefund(db, current, original, value) + return m.charge(gasAdd(cost, params.WarmStorageReadCostEIP2929), tracing.GasChangeCallPrecompiledContract) +} + +func (m *precompileGasMeter) adjustDirtySStoreRefund(db *nativeStateDB, current common.Hash, original common.Hash, value common.Hash) { + if original != (common.Hash{}) { + if current == (common.Hash{}) { + db.SubRefund(params.SstoreClearsScheduleRefundEIP3529) + } else if value == (common.Hash{}) { + db.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + } + if original != value { + return + } + if original == (common.Hash{}) { + db.AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929) + return + } + db.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) +} + +func (m *precompileGasMeter) chargeNativeTransfer(db *nativeStateDB, from common.Address, to common.Address, amount *big.Int) bool { + if amount == nil || amount.Sign() == 0 { + return true + } + if !m.chargeAccountAccess(db, from) || !m.chargeAccountAccess(db, to) { + return false + } + if !m.charge(params.CallValueTransferGas, tracing.GasChangeCallPrecompiledContract) { + return false + } + if db.Empty(to) { + return m.charge(params.CallNewAccountGas, tracing.GasChangeCallPrecompiledContract) + } + return true +} + +func (m *precompileGasMeter) chargeAccountAccess(db *nativeStateDB, addr common.Address) bool { + if db.AddressInAccessList(addr) { + return m.charge(params.WarmStorageReadCostEIP2929, tracing.GasChangeCallPrecompiledContract) + } + db.AddAddressToAccessList(addr) + return m.charge(params.ColdAccountAccessCostEIP2929, tracing.GasChangeCallStorageColdAccess) +} + +func (m *precompileGasMeter) chargeLog(topics int, dataLen int) bool { + topicsGas := gasMul(params.LogTopicGas, uint64(topics)) //nolint:gosec // topic count is bounded by log construction. + dataGas := gasMul(params.LogDataGas, uint64(dataLen)) //nolint:gosec // log data length is bounded by memory. + return m.charge(gasAdd(params.LogGas, topicsGas, dataGas), tracing.GasChangeCallPrecompiledContract) +} + +type meteredLogSink struct { + sink precompiles.LogSink + meter *precompileGasMeter +} + +func (l meteredLogSink) AddLog(log *ethtypes.Log) { + if l.sink == nil || log == nil { + return + } + if l.meter != nil && !l.meter.chargeLog(len(log.Topics), len(log.Data)) { + return + } + l.sink.AddLog(log) +} + +func (s storageBackedStore) chargeStoreBaseSlot(key []byte) bool { + if s.meter == nil { + return true + } + return s.meter.chargeKeccak(len(storeLengthDomain) + len(key)) +} + +func (s storageBackedStore) chargeStoreChunkSlot(baseSlot common.Hash, index uint64) bool { + if s.meter == nil { + return true + } + return s.meter.chargeKeccak(len(storeChunkDomain) + len(baseSlot) + 8) +} + +func (s storageBackedStore) chargeSLoad(slot common.Hash) bool { + if s.meter == nil { + return true + } + return s.meter.chargeSLoad(s.db, s.address, slot) +} + +func (s storageBackedStore) chargeSStore(slot common.Hash, value common.Hash) bool { + if s.meter == nil { + return true + } + return s.meter.chargeSStore(s.db, s.address, slot, value) +} + +func wordCount(size uint64) uint64 { + if size == 0 { + return 0 + } + return (size + 31) / 32 +} + +func gasAdd(values ...uint64) uint64 { + total := uint64(0) + for _, value := range values { + if maxGas-total < value { + return maxGas + } + total += value + } + return total +} + +func gasMul(left uint64, right uint64) uint64 { + if left != 0 && right > maxGas/left { + return maxGas + } + return left * right +} diff --git a/giga/evmonly/precompiles/staking/staking.go b/giga/evmonly/precompiles/staking/staking.go index bae0f7ba7c..e2985bebb5 100644 --- a/giga/evmonly/precompiles/staking/staking.go +++ b/giga/evmonly/precompiles/staking/staking.go @@ -41,8 +41,7 @@ const ( StakingAddress = "0x0000000000000000000000000000000000001005" unknownMethodGas uint64 = 3000 - readGas uint64 = 3000 - writeGas uint64 = 20000 + baseGas uint64 = 700 inputByteGas uint64 = 16 bondDenom = "usei" @@ -135,15 +134,11 @@ func (p *Precompile) ABI() abi.ABI { } func (p *Precompile) RequiredGas(input []byte) uint64 { - method, _, err := p.prepare(input) + _, _, err := p.prepare(input) if err != nil { return unknownMethodGas } - gas := readGas - if isTransaction(method.Name) { - gas = writeGas - } - return gas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. + return baseGas + inputByteGas*uint64(len(input)) //nolint:gosec // input length is bounded by memory. } func (p *Precompile) Run(ctx *precompiles.Context, input []byte) ([]byte, error) {