diff --git a/core/blockchain.go b/core/blockchain.go index f55adf0f5..bcbfbf92b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -671,172 +671,6 @@ func (bc *BlockChain) SetSafe(header *types.Header) { } } -// rewindHashHead implements the logic of rewindHead in the context of hash scheme. -func (bc *BlockChain) rewindHashHead(head *types.Header, root common.Hash) (*types.Header, uint64) { - var ( - limit uint64 // The oldest block that will be searched for this rewinding - beyondRoot = root == common.Hash{} // Flag whether we're beyond the requested root (no root, always true) - pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot point state - rootNumber uint64 // Associated block number of requested root - - start = time.Now() // Timestamp the rewinding is restarted - logged = time.Now() // Timestamp last progress log was printed - ) - // The oldest block to be searched is determined by the pivot block or a constant - // searching threshold. The rationale behind this is as follows: - // - // - Snap sync is selected if the pivot block is available. The earliest available - // state is the pivot block itself, so there is no sense in going further back. - // - // - Full sync is selected if the pivot block does not exist. The hash database - // periodically flushes the state to disk, and the used searching threshold is - // considered sufficient to find a persistent state, even for the testnet. It - // might be not enough for a chain that is nearly empty. In the worst case, - // the entire chain is reset to genesis, and snap sync is re-enabled on top, - // which is still acceptable. - if pivot != nil { - limit = *pivot - } else if head.Number.Uint64() > params.FullImmutabilityThreshold { - limit = head.Number.Uint64() - params.FullImmutabilityThreshold - } - for { - logger := log.Trace - if time.Since(logged) > time.Second*8 { - logged = time.Now() - logger = log.Info - } - logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start))) - - // If a root threshold was requested but not yet crossed, check - if !beyondRoot && head.Root == root { - beyondRoot, rootNumber = true, head.Number.Uint64() - } - // If search limit is reached, return the genesis block as the - // new chain head. - if head.Number.Uint64() < limit { - log.Info("Rewinding limit reached, resetting to genesis", "number", head.Number, "hash", head.Hash(), "limit", limit) - return bc.genesisBlock.Header(), rootNumber - } - // If the associated state is not reachable, continue searching - // backwards until an available state is found. - if !bc.HasState(head.Root) { - // If the chain is gapped in the middle, return the genesis - // block as the new chain head. - parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) - if parent == nil { - log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash) - return bc.genesisBlock.Header(), rootNumber - } - head = parent - - // If the genesis block is reached, stop searching. - if head.Number.Uint64() == 0 { - log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash()) - return head, rootNumber - } - continue // keep rewinding - } - // Once the available state is found, ensure that the requested root - // has already been crossed. If not, continue rewinding. - if beyondRoot || head.Number.Uint64() == 0 { - log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash()) - return head, rootNumber - } - log.Debug("Skipping block with threshold state", "number", head.Number, "hash", head.Hash(), "root", head.Root) - head = bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding - } -} - -// rewindPathHead implements the logic of rewindHead in the context of path scheme. -func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*types.Header, uint64) { - var ( - pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot block - rootNumber uint64 // Associated block number of requested root - - // BeyondRoot represents whether the requested root is already - // crossed. The flag value is set to true if the root is empty. - beyondRoot = root == common.Hash{} - - // noState represents if the target state requested for search - // is unavailable and impossible to be recovered. - noState = !bc.HasState(root) && !bc.stateRecoverable(root) - - start = time.Now() // Timestamp the rewinding is restarted - logged = time.Now() // Timestamp last progress log was printed - ) - // Rewind the head block tag until an available state is found. - for { - logger := log.Trace - if time.Since(logged) > time.Second*8 { - logged = time.Now() - logger = log.Info - } - logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start))) - - // If a root threshold was requested but not yet crossed, check - if !beyondRoot && head.Root == root { - beyondRoot, rootNumber = true, head.Number.Uint64() - } - // If the root threshold hasn't been crossed but the available - // state is reached, quickly determine if the target state is - // possible to be reached or not. - if !beyondRoot && noState && bc.HasState(head.Root) { - beyondRoot = true - log.Info("Disable the search for unattainable state", "root", root) - } - // Check if the associated state is available or recoverable if - // the requested root has already been crossed. - if beyondRoot && (bc.HasState(head.Root) || bc.stateRecoverable(head.Root)) { - break - } - // If pivot block is reached, return the genesis block as the - // new chain head. Theoretically there must be a persistent - // state before or at the pivot block, prevent endless rewinding - // towards the genesis just in case. - if pivot != nil && *pivot >= head.Number.Uint64() { - log.Info("Pivot block reached, resetting to genesis", "number", head.Number, "hash", head.Hash()) - return bc.genesisBlock.Header(), rootNumber - } - // If the chain is gapped in the middle, return the genesis - // block as the new chain head - parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding - if parent == nil { - log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash) - return bc.genesisBlock.Header(), rootNumber - } - head = parent - - // If the genesis block is reached, stop searching. - if head.Number.Uint64() == 0 { - log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash()) - return head, rootNumber - } - } - // Recover if the target state if it's not available yet. - if !bc.HasState(head.Root) { - if err := bc.triedb.Recover(head.Root); err != nil { - log.Crit("Failed to rollback state", "err", err) - } - } - log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash()) - return head, rootNumber -} - -// rewindHead searches the available states in the database and returns the associated -// block as the new head block. -// -// If the given root is not empty, then the rewind should attempt to pass the specified -// state root and return the associated block number as well. If the root, typically -// representing the state corresponding to snapshot disk layer, is deemed impassable, -// then block number zero is returned, indicating that snapshot recovery is disabled -// and the whole snapshot should be auto-generated in case of head mismatch. -func (bc *BlockChain) rewindHead(head *types.Header, root common.Hash) (*types.Header, uint64) { - if bc.triedb.Scheme() == rawdb.PathScheme { - return bc.rewindPathHead(head, root) - } - return bc.rewindHashHead(head, root) -} - // setHeadBeyondRoot rewinds the local chain to a new head with the extra condition // that the rewind must pass the specified state root. The extra condition is // ignored if it causes rolling back more than rewindLimit Gas (0 meaning infinte). @@ -857,24 +691,87 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha } defer bc.chainmu.Unlock() - var ( - // Track the block number of the requested root hash - rootNumber uint64 // (no root == always 0) - rootFound bool + // Track the block number of the requested root hash + var blockNumber uint64 // (no root == always 0) + var rootFound bool + + // Retrieve the last pivot block to short circuit rollbacks beyond it + // and the current freezer limit to start nuking it's underflown. + pivot := rawdb.ReadLastPivotNumber(bc.db) - // Retrieve the last pivot block to short circuit rollbacks beyond it - // and the current freezer limit to start nuking it's underflown. - pivot = rawdb.ReadLastPivotNumber(bc.db) - ) updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (*types.Header, bool) { // Rewind the blockchain, ensuring we don't end up with a stateless head // block. Note, depth equality is permitted to allow using SetHead as a // chain reparation mechanism without deleting any data! if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() <= currentBlock.Number.Uint64() { - var newHeadBlock *types.Header - newHeadBlock, rootNumber = bc.rewindHead(header, root) - if newHeadBlock.Number.Cmp(bc.genesisBlock.Number()) != 0 { - rootFound = (root == common.Hash{}) || newHeadBlock.Root == root + newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64()) + if newHeadBlock == nil { + log.Error("Gap in the chain, rewinding to genesis", "number", header.Number, "hash", header.Hash()) + newHeadBlock = bc.genesisBlock + } else { + // Block exists, keep rewinding until we find one with state, + // keeping rewinding until we exceed the optional threshold + // root hash + rootFound = (root == common.Hash{}) // Flag whether we're beyond the requested root (no root, always true) + lastFullBlock := uint64(0) + lastFullBlockHash := common.Hash{} + gasRolledBack := uint64(0) + + for { + if rewindLimit > 0 && lastFullBlock != 0 { + // Arbitrum: track the amount of gas rolled back and stop the rollback early if necessary + gasUsedInBlock := newHeadBlock.GasUsed() + if bc.chainConfig.IsArbitrum() { + receipts := bc.GetReceiptsByHash(newHeadBlock.Hash()) + for _, receipt := range receipts { + gasUsedInBlock -= receipt.GasUsedForL1 + } + } + gasRolledBack += gasUsedInBlock + if gasRolledBack >= rewindLimit { + blockNumber = lastFullBlock + newHeadBlock = bc.GetBlock(lastFullBlockHash, lastFullBlock) + log.Debug("Rewound to block with state but not snapshot", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + break + } + } + + // If a root threshold was requested but not yet crossed, check + if root != (common.Hash{}) && !rootFound && newHeadBlock.Root() == root { + rootFound, blockNumber = true, newHeadBlock.NumberU64() + } + if !bc.HasState(newHeadBlock.Root()) && !bc.stateRecoverable(newHeadBlock.Root()) { + log.Trace("Block state missing, rewinding further", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + if pivot == nil || newHeadBlock.NumberU64() > *pivot { + parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) + if parent != nil { + newHeadBlock = parent + continue + } + log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash()) + newHeadBlock = bc.genesisBlock + } else { + log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot) + newHeadBlock = bc.genesisBlock + } + } else if lastFullBlock == 0 { + lastFullBlock = newHeadBlock.NumberU64() + lastFullBlockHash = newHeadBlock.Hash() + } + if rootFound || newHeadBlock.NumberU64() <= bc.genesisBlock.NumberU64() { + if !bc.HasState(newHeadBlock.Root()) && bc.stateRecoverable(newHeadBlock.Root()) { + // Rewind to a block with recoverable state. If the state is + // missing, run the state recovery here. + if err := bc.triedb.Recover(newHeadBlock.Root()); err != nil { + log.Crit("Failed to rollback state", "err", err) // Shouldn't happen + } + log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + } + break + } + log.Debug("Skipping block with threshold state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "root", newHeadBlock.Root()) + newHeadBlock = bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) // Keep rewinding + } } rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash()) @@ -882,16 +779,16 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha // In theory we should update all in-memory markers in the // last step, however the direction of SetHead is from high // to low, so it's safe to update in-memory markers directly. - bc.currentBlock.Store(newHeadBlock) - headBlockGauge.Update(int64(newHeadBlock.Number.Uint64())) + bc.currentBlock.Store(newHeadBlock.Header()) + headBlockGauge.Update(newHeadBlock.Number().Int64()) // The head state is missing, which is only possible in the path-based // scheme. This situation occurs when the chain head is rewound below // the pivot point. In this scenario, there is no possible recovery // approach except for rerunning a snap sync. Do nothing here until the // state syncer picks it up. - if !bc.HasState(newHeadBlock.Root) { - if newHeadBlock.Number.Uint64() != 0 { + if !bc.HasState(newHeadBlock.Root()) { + if newHeadBlock.Number().Uint64() != 0 { log.Crit("Chain is stateless at a non-genesis block") } log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash()) @@ -982,7 +879,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.SetFinalized(nil) } - return rootNumber, rootFound, bc.loadLastState() + return blockNumber, rootFound, bc.loadLastState() } // SnapSyncCommitHead sets the current head block to the one defined by the hash diff --git a/core/vm/contracts.go b/core/vm/contracts.go index d1e5dd7a3..103c41856 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -209,7 +209,11 @@ type AdvancedPrecompile interface { // - the returned bytes, // - the _remaining_ gas, // - any error that occurred -func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { +func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64, logger *tracing.Hooks, advancedInfo *AdvancedPrecompileCall) (ret []byte, remainingGas uint64, err error) { + advanced, isAdvanced := p.(AdvancedPrecompile) + if isAdvanced { + return advanced.RunAdvanced(input, suppliedGas, advancedInfo) + } gasCost := p.RequiredGas(input) if suppliedGas < gasCost { return nil, 0, ErrOutOfGas diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 1e5cc8007..7e3851cae 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -36,7 +36,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(p, input, gas, nil) + RunPrecompiledContract(p, input, gas, nil, nil) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index b9a8055f9..dc68813b4 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(p, in, gas, nil); err != nil { + if res, _, err := RunPrecompiledContract(p, in, gas, nil, nil); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := p.RequiredGas(in) - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(p, in, gas, nil, nil) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(p, in, gas, nil, nil) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -171,7 +171,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { bench.ResetTimer() for i := 0; i < bench.N; i++ { copy(data, in) - res, _, err = RunPrecompiledContract(p, data, reqGas, nil) + res, _, err = RunPrecompiledContract(p, data, reqGas, nil, nil) } bench.StopTimer() elapsed := uint64(time.Since(start)) diff --git a/core/vm/evm.go b/core/vm/evm.go index 8e8dda5a2..7205908a9 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -223,7 +223,15 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value) if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + info := &AdvancedPrecompileCall{ + PrecompileAddress: addr, + ActingAsAddress: addr, + Caller: caller.Address(), + Value: value.ToBig(), + ReadOnly: false, + Evm: evm, + } + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer, info) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -289,7 +297,15 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + info := &AdvancedPrecompileCall{ + PrecompileAddress: addr, + ActingAsAddress: caller.Address(), + Caller: caller.Address(), + Value: value.ToBig(), + ReadOnly: false, + Evm: evm, + } + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer, info) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -341,7 +357,16 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + caller := caller.(*Contract) + info := &AdvancedPrecompileCall{ + PrecompileAddress: addr, + ActingAsAddress: caller.Address(), + Caller: caller.CallerAddress, + Value: caller.Value().ToBig(), + ReadOnly: false, + Evm: evm, + } + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer, info) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -396,7 +421,15 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount) if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + info := &AdvancedPrecompileCall{ + PrecompileAddress: addr, + ActingAsAddress: addr, + Caller: caller.Address(), + Value: new(big.Int), + ReadOnly: true, + Evm: evm, + } + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer, info) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 94307f3f0..82ac3fde5 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -945,11 +945,12 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc config.BlockOverrides.Apply(&vmctx) } // Execute the trace - if err := args.CallDefaults(api.backend.RPCGasCap(), vmctx.BaseFee, api.backend.ChainConfig().ChainID); err != nil { + gasLimitNotSetByUser := args.Gas == nil + if err := args.CallDefaults(api.backend.RPCGasCap(), vmctx.BaseFee, api.backend.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { return nil, err } var ( - msg = args.ToMessage(vmctx.BaseFee, api.backend.RPCGasCap(), block.Header(), statedb, core.MessageEthcallMode) + msg = args.ToMessage(vmctx.BaseFee, api.backend.RPCGasCap(), block.Header(), statedb, core.MessageEthcallMode, api.backend.ChainConfig().ChainID, gasLimitNotSetByUser) tx = args.ToTransaction() traceConfig *TraceConfig ) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 89369feea..a682cf06a 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -301,13 +301,13 @@ func TestInternals(t *testing.T) { byte(vm.CALL), }, tracer: mkTracer("callTracer", nil), - want: fmt.Sprintf(`{"from":"%s","gas":"0x13880","gasUsed":"0x54d8","to":"0x00000000000000000000000000000000deadbeef","input":"0x","calls":[{"from":"0x00000000000000000000000000000000deadbeef","gas":"0xe01a","gasUsed":"0x0","to":"0x00000000000000000000000000000000000000ff","input":"0x","value":"0x0","type":"CALL"}],"value":"0x0","type":"CALL"}`, originHex), + want: fmt.Sprintf(`{"beforeEVMTransfers":[{"purpose":"feePayment","from":"%s","to":null,"value":"0x13880"}],"afterEVMTransfers":[{"purpose":"gasRefund","from":null,"to":"%s","value":"0xe3a8"},{"purpose":"tip","from":null,"to":"0x0000000000000000000000000000000000000000","value":"0x54d8"}],"from":"%s","gas":"0x13880","gasUsed":"0x54d8","to":"0x00000000000000000000000000000000deadbeef","input":"0x","calls":[{"from":"0x00000000000000000000000000000000deadbeef","gas":"0xe01a","gasUsed":"0x0","to":"0x00000000000000000000000000000000000000ff","input":"0x","value":"0x0","type":"CALL"}],"value":"0x0","type":"CALL"}`, origin.Hex(), origin.Hex(), originHex), }, { name: "Stack depletion in LOG0", code: []byte{byte(vm.LOG3)}, tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)), - want: fmt.Sprintf(`{"from":"%s","gas":"0x13880","gasUsed":"0x13880","to":"0x00000000000000000000000000000000deadbeef","input":"0x","error":"stack underflow (0 \u003c=\u003e 5)","value":"0x0","type":"CALL"}`, originHex), + want: fmt.Sprintf(`{"beforeEVMTransfers":[{"purpose":"feePayment","from":"%s","to":null,"value":"0x13880"}],"afterEVMTransfers":[{"purpose":"gasRefund","from":null,"to":"%s","value":"0x0"},{"purpose":"tip","from":null,"to":"0x0000000000000000000000000000000000000000","value":"0x13880"}],"from":"%s","gas":"0x13880","gasUsed":"0x13880","to":"0x00000000000000000000000000000000deadbeef","input":"0x","error":"stack underflow (0 \u003c=\u003e 5)","value":"0x0","type":"CALL"}`, origin.Hex(), origin.Hex(), originHex), }, { name: "Mem expansion in LOG0", @@ -320,7 +320,7 @@ func TestInternals(t *testing.T) { byte(vm.LOG0), }, tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)), - want: fmt.Sprintf(`{"from":"%s","gas":"0x13880","gasUsed":"0x5b9e","to":"0x00000000000000000000000000000000deadbeef","input":"0x","logs":[{"address":"0x00000000000000000000000000000000deadbeef","topics":[],"data":"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","position":"0x0"}],"value":"0x0","type":"CALL"}`, originHex), + want: fmt.Sprintf(`{"beforeEVMTransfers":[{"purpose":"feePayment","from":"%s","to":null,"value":"0x13880"}],"afterEVMTransfers":[{"purpose":"gasRefund","from":null,"to":"%s","value":"0xdce2"},{"purpose":"tip","from":null,"to":"0x0000000000000000000000000000000000000000","value":"0x5b9e"}],"from":"%s","gas":"0x13880","gasUsed":"0x5b9e","to":"0x00000000000000000000000000000000deadbeef","input":"0x","logs":[{"address":"0x00000000000000000000000000000000deadbeef","topics":[],"data":"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","position":"0x0"}],"value":"0x0","type":"CALL"}`, origin.Hex(), origin.Hex(), originHex), }, { // Leads to OOM on the prestate tracer diff --git a/eth/tracers/internal/tracetest/testdata/call_tracer/blob_tx.json b/eth/tracers/internal/tracetest/testdata/call_tracer/blob_tx.json index 549acb1fe..9b48434e1 100644 --- a/eth/tracers/internal/tracetest/testdata/call_tracer/blob_tx.json +++ b/eth/tracers/internal/tracetest/testdata/call_tracer/blob_tx.json @@ -56,6 +56,30 @@ }, "input": "0x03f8b1820539806485174876e800825208940c2c51a0990aee1d73c1228de1586883415575088080c083020000f842a00100c9fbdf97f747e85847b4f3fff408f89c26842f77c882858bf2c89923849aa00138e3896f3c27f2389147507f8bcec52028b0efca6ee842ed83c9158873943880a0dbac3f97a532c9b00e6239b29036245a5bfbb96940b9d848634661abee98b945a03eec8525f261c2e79798f7b45a5d6ccaefa24576d53ba5023e919b86841c0675", "result": { + "beforeEVMTransfers": + [ + { + "purpose": "feePayment", + "from": "0x0c2c51a0990AeE1d73C1228de158688341557508", + "to": null, + "value": "0x2de364958" + } + ], + "afterEVMTransfers": + [ + { + "purpose": "gasRefund", + "from": null, + "to": "0x0c2c51a0990AeE1d73C1228de158688341557508", + "value": "0x0" + }, + { + "purpose": "tip", + "from": null, + "to": "0x0000000000000000000000000000000000000000", + "value": "0x200b20" + } + ], "from": "0x0c2c51a0990aee1d73c1228de158688341557508", "gas": "0x5208", "gasUsed": "0x5208", diff --git a/eth/tracers/internal/tracetest/testdata/call_tracer/inner_revert_reason.json b/eth/tracers/internal/tracetest/testdata/call_tracer/inner_revert_reason.json index bd938bc57..2e06a33f5 100644 --- a/eth/tracers/internal/tracetest/testdata/call_tracer/inner_revert_reason.json +++ b/eth/tracers/internal/tracetest/testdata/call_tracer/inner_revert_reason.json @@ -55,7 +55,7 @@ "purpose": "feePayment", "from": "0x3623191d4CCfBbdf09E8EBf6382A1F8257417bC1", "to": null, - "value": "0x254db1c2244000" + "value": "0x23f8a24459d000" } ], "afterEVMTransfers": [ @@ -63,7 +63,7 @@ "purpose": "gasRefund", "from": null, "to": "0x3623191d4CCfBbdf09E8EBf6382A1F8257417bC1", - "value": "0x2366bc81a79000" + "value": "0x22231133e19400" }, { "purpose": "tip", diff --git a/eth/tracers/internal/tracetest/testdata/call_tracer_withLog/frontier_create_outofstorage.json b/eth/tracers/internal/tracetest/testdata/call_tracer_withLog/frontier_create_outofstorage.json index c46fe080f..b2e1ea79c 100644 --- a/eth/tracers/internal/tracetest/testdata/call_tracer_withLog/frontier_create_outofstorage.json +++ b/eth/tracers/internal/tracetest/testdata/call_tracer_withLog/frontier_create_outofstorage.json @@ -77,6 +77,30 @@ "withLog": true }, "result": { + "beforeEVMTransfers": + [ + { + "purpose": "feePayment", + "from": "0x0047A8033CC6d6Ca2ED5044674Fd421F44884dE8", + "to": null, + "value": "0x13fbe85edc90000" + } + ], + "afterEVMTransfers": + [ + { + "purpose": "gasRefund", + "from": null, + "to": "0x0047A8033CC6d6Ca2ED5044674Fd421F44884dE8", + "value": "0xd52ed21cc43400" + }, + { + "purpose": "tip", + "from": null, + "to": "0xe48430c4E88A929bBa0eE3DCe284866a9937b609", + "value": "0x6a8fb3d104cc00" + } + ], "from": "0x0047a8033cc6d6ca2ed5044674fd421f44884de8", "gas": "0x1b7740", "gasUsed": "0x9274f", diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index d0e2f723b..83ec0c7af 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1130,10 +1130,11 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S // Get a new instance of the EVM. var err error - if err = args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID); err != nil { + gasLimitNotSetByUser := args.Gas == nil + if err = args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { return nil, err } - msg := args.ToMessage(blockCtx.BaseFee, globalGasCap, header, state, runMode) + msg := args.ToMessage(blockCtx.BaseFee, globalGasCap, header, state, runMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) // Arbitrum: support NodeInterface.sol by swapping out the message if needed var res *core.ExecutionResult @@ -1295,16 +1296,17 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr ErrorRatio: gasestimator.EstimateGasErrorRatio, RunScheduledTxes: runScheduledTxes, } - if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil { + gasLimitNotSetByUser := args.Gas == nil + if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { return 0, err } // Run the gas estimation andwrap any revertals into a custom return // Arbitrum: this also appropriately recursively calls another args.ToMessage with increased gasCap by posterCostInL2Gas amount - call := args.ToMessage(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode) + call := args.ToMessage(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) // Arbitrum: raise the gas cap to ignore L1 costs so that it's compute-only { - gasCap, err = args.L2OnlyGasCap(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode) + gasCap, err = args.L2OnlyGasCap(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) if err != nil { return 0, err } @@ -1723,6 +1725,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } // Ensure any missing fields are filled, extract the recipient and input data + gasLimitNotSetByUser := args.Gas == nil if err := args.setDefaults(ctx, b, true); err != nil { return nil, 0, nil, err } @@ -1753,7 +1756,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH statedb := db.Copy() // Set the accesslist to the last al args.AccessList = &accessList - msg := args.ToMessage(header.BaseFee, b.RPCGasCap(), header, statedb, core.MessageEthcallMode) + msg := args.ToMessage(header.BaseFee, b.RPCGasCap(), header, statedb, core.MessageEthcallMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) // Apply the transaction with the access list tracer tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles) diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 8f1d330e8..692de5d09 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -375,9 +375,9 @@ func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context) error { // CallDefaults sanitizes the transaction arguments, often filling in zero values, // for the purpose of eth_call class of RPC methods. -func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { +func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int, gasLimitNotSetByUser bool) error { // Reject invalid combinations of pre- and post-1559 fee styles - if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + if args.GasPrice != nil && ((args.MaxFeePerGas != nil && args.MaxFeePerGas.ToInt().Cmp(common.Big0) != 0) || (args.MaxPriorityFeePerGas != nil && args.MaxPriorityFeePerGas.ToInt().Cmp(common.Big0) != 0)) { return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") } if args.ChainID == nil { @@ -387,7 +387,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, return fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, chainID) } } - if args.Gas == nil { + if gasLimitNotSetByUser { gas := globalGasCap if gas == 0 { gas = uint64(math.MaxUint64 / 2) @@ -427,7 +427,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, } // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. -func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode) *core.Message { +func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode, chainID *big.Int, gasLimitNotSetByUser bool) *core.Message { var ( gasPrice *big.Int gasFeeCap *big.Int @@ -484,15 +484,20 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, he // ToMessage recurses once to allow ArbOS to intercept the result for all callers // ArbOS uses this to modify globalGasCap so that the cap will ignore this tx's specific L1 data costs core.InterceptRPCGasCap(&globalGasCap, msg, header, state) - return args.ToMessage(baseFee, globalGasCap, header, nil, runMode) // we pass a nil to avoid another recursion + err := args.CallDefaults(globalGasCap, baseFee, chainID, gasLimitNotSetByUser) + // If we fail to call defaults, we should panic because it's a programming error since we've already called it once + if err != nil { + panic(fmt.Sprintf("CallDefaults failed: %v", err)) + } + return args.ToMessage(baseFee, globalGasCap, header, nil, runMode, chainID, gasLimitNotSetByUser) // we pass a nil to avoid another recursion } return msg } // Raises the vanilla gas cap by the tx's l1 data costs in l2 terms. This creates a new gas cap that after // data payments are made, equals the original vanilla cap for the remaining, L2-specific work the tx does. -func (args *TransactionArgs) L2OnlyGasCap(baseFee *big.Int, gasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode) (uint64, error) { - msg := args.ToMessage(baseFee, gasCap, header, nil, runMode) +func (args *TransactionArgs) L2OnlyGasCap(baseFee *big.Int, gasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode, chainID *big.Int, gasLimitNotSetByUser bool) (uint64, error) { + msg := args.ToMessage(baseFee, gasCap, header, nil, runMode, chainID, gasLimitNotSetByUser) core.InterceptRPCGasCap(&gasCap, msg, header, state) return gasCap, nil }