diff --git a/.github/workflows/_go-tests.yml b/.github/workflows/_go-tests.yml index 4131f63052..f7bd95f36d 100644 --- a/.github/workflows/_go-tests.yml +++ b/.github/workflows/_go-tests.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - test-mode: [defaults-A, defaults-B, flaky, pathdb, challenge, stylus, l3challenge] + test-mode: [defaults-A, defaults-B, flaky, pathdb, challenge, stylus, l3challenge, experimental] services: redis: image: redis @@ -147,7 +147,7 @@ jobs: ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags challengetest --run TestL3Challenge --timeout 120m --cover - # --------------------- CHALLENGE MODES --------------------- + # --------------------- STYLUS MODE -------------------------- - name: run stylus tests if: matrix.test-mode == 'stylus' @@ -155,6 +155,13 @@ jobs: ${{ github.workspace }}/.github/workflows/gotestsum.sh --tags stylustest --run TestProgramArbitrator --timeout 60m --cover + # --------------------- EXPERIMENTAL MODE -------------------- + - name: run experimental tooling tests + if: matrix.test-mode == 'experimental' + run: >- + ${{ github.workspace }}/.github/workflows/gotestsum.sh + --tags debugblock --run TestExperimental --timeout 60m --cover + # --------------------- ARCHIVE LOGS FOR ALL MODES --------------------- - name: Archive detailed run log diff --git a/Dockerfile b/Dockerfile index 3caffae53b..2458a3c0d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,7 @@ COPY ./contracts/package.json ./contracts/yarn.lock ./contracts/ COPY ./safe-smart-account ./safe-smart-account COPY ./solgen/gen.go ./solgen/ COPY ./go-ethereum ./go-ethereum +COPY ./experimental/debugblock ./experimental/debugblock COPY scripts/remove_reference_types.sh scripts/ COPY --from=brotli-wasm-export / target/ COPY --from=contracts-builder workspace/contracts-local/out/precompiles/ contracts-local/out/precompiles/ @@ -390,5 +391,11 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ USER user +FROM nitro-node AS nitro-node-experimental +USER root +COPY --from=node-builder /workspace/target/bin/nitro-experimental /usr/local/bin/ +ENTRYPOINT [ "/usr/local/bin/nitro-experimental" , "--validation.wasm.allowed-wasm-module-roots", "/home/user/nitro-legacy/machines,/home/user/target/machines"] +USER user + FROM nitro-node AS nitro-node-default # Just to ensure nitro-node-dist is default diff --git a/Makefile b/Makefile index 3b26d0b122..b23823f47e 100644 --- a/Makefile +++ b/Makefile @@ -165,7 +165,7 @@ all: build build-replay-env test-gen-proofs @touch .make/all .PHONY: build -build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daprovider daserver autonomous-auctioneer bidder-client datool blobtool el-proxy mockexternalsigner seq-coordinator-invalidate nitro-val seq-coordinator-manager dbconv genesis-generator) +build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daprovider daserver autonomous-auctioneer bidder-client datool blobtool el-proxy mockexternalsigner seq-coordinator-invalidate nitro-val seq-coordinator-manager dbconv genesis-generator nitro-experimental) @printf $(done) .PHONY: build-node-deps @@ -244,6 +244,11 @@ test-go-gas-dimensions: test-go-deps .github/workflows/gotestsum.sh --timeout 120m --run "TestDim(Log|TxOp)" --tags gasdimensionstest --nolog @printf $(done) +.PHONY: test-go-experimental +test-go-experimental: test-go-deps + .github/workflows/gotestsum.sh --timeout 120m --run TestExperimental --tags debugblock,benchsequencer --nolog + @printf $(done) + .PHONY: test-gen-proofs test-gen-proofs: \ $(arbitrator_test_wasms) \ @@ -351,6 +356,10 @@ $(output_root)/bin/seq-coordinator-manager: $(DEP_PREDICATE) build-node-deps $(output_root)/bin/dbconv: $(DEP_PREDICATE) build-node-deps go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/dbconv" +# nitro built with experimental tooling enabled +$(output_root)/bin/nitro-experimental: $(DEP_PREDICATE) build-node-deps + go build $(GOLANG_PARAMS) --tags debugblock,benchsequencer -o $@ "$(CURDIR)/cmd/nitro" + # recompile wasm, but don't change timestamp unless files differ $(replay_wasm): $(DEP_PREDICATE) $(go_source) .make/solgen mkdir -p `dirname $(replay_wasm)` diff --git a/arbos/block_processor.go b/arbos/block_processor.go index 5b602c9b06..c01c1ea499 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -24,6 +24,7 @@ import ( "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/experimental/debugblock" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -241,6 +242,10 @@ func ProduceBlockAdvanced( firstTx := types.NewTx(startTx) + if chainConfig.DebugMode() && header.Number.Uint64() == chainConfig.ArbitrumChainParams.DebugBlock { + debugblock.DebugBlockStateUpdate(statedb, expectedBalanceDelta, chainConfig) + } + for { // repeatedly process the next tx, doing redeems created along the way in FIFO order diff --git a/cmd/nitro/init.go b/cmd/nitro/init.go index 4b4da0cea5..6f2a7dbad5 100644 --- a/cmd/nitro/init.go +++ b/cmd/nitro/init.go @@ -621,6 +621,9 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo if err := dbutil.UnfinishedConversionCheck(chainData); err != nil { return nil, nil, fmt.Errorf("l2chaindata unfinished database conversion check error: %w", err) } + if config.Execution.Dangerous.DebugBlock.OverwriteChainConfig { + config.Execution.Dangerous.DebugBlock.Apply(chainConfig) + } wasmDb, err := stack.OpenDatabaseWithOptions("wasm", node.DatabaseOptions{Cache: config.Execution.Caching.DatabaseCache, Handles: config.Persistent.Handles, MetricsNamespace: "wasm/", PebbleExtraOptions: persistentConfig.Pebble.ExtraOptions("wasm"), NoFreezer: true}) if err != nil { return nil, nil, err diff --git a/cmd/util/confighelpers/configuration.go b/cmd/util/confighelpers/configuration.go index 451409cddf..b67418642e 100644 --- a/cmd/util/confighelpers/configuration.go +++ b/cmd/util/confighelpers/configuration.go @@ -209,7 +209,7 @@ func devFlagArgs() []string { "--init.empty=false", "--http.port", "8547", "--http.addr", "127.0.0.1", - "--http.api=net,web3,eth,arb,arbdebug,debug", + "--http.api=net,web3,eth,arb,arbdebug,debug,benchseq", "--node.transaction-streamer.track-block-metadata-from=1", } return args diff --git a/execution/gethexec/bench_sequencer.go b/execution/gethexec/bench_sequencer.go new file mode 100644 index 0000000000..6c008e5452 --- /dev/null +++ b/execution/gethexec/bench_sequencer.go @@ -0,0 +1,90 @@ +//go:build benchsequencer + +package gethexec + +import ( + "context" + + "github.com/ethereum/go-ethereum/log" + "github.com/offchainlabs/nitro/util/containers" + "github.com/offchainlabs/nitro/util/stopwaiter" + "github.com/spf13/pflag" +) + +func BenchSequencerConfigAddOptions(prefix string, f *pflag.FlagSet) { + f.Bool(prefix+".enable", BenchSequencerConfigDefault.Enable, "enables transaction indexer") +} + +func (c *BenchSequencerConfig) Validate() error { + if c.Enable { + log.Warn("DANGER! BenchSequencer enabled") + } + return nil +} + +func NewBenchSequencer(sequencer *Sequencer) (TransactionPublisher, interface{}) { + benchSequencer := &BenchSequencer{ + Sequencer: sequencer, + semaphore: make(chan struct{}, 1), + } + return benchSequencer, NewBenchSequencerAPI(benchSequencer) +} + +type BenchSequencer struct { + *Sequencer + semaphore chan struct{} +} + +func (s *BenchSequencer) Start(ctx context.Context) error { + // override Sequencer.Start to not start the inner sequencer + s.StopWaiter.Start(ctx, s) + s.semaphore <- struct{}{} + return nil +} + +func (s *BenchSequencer) TxQueueLength(includeRetryTxQueue bool) int { + if includeRetryTxQueue { + return len(s.Sequencer.txQueue) + s.Sequencer.txRetryQueue.Len() + } + return len(s.Sequencer.txQueue) +} + +func (s *BenchSequencer) TxRetryQueueLength() int { + return s.Sequencer.txRetryQueue.Len() +} + +func (s *BenchSequencer) CreateBlock() containers.PromiseInterface[bool] { + return stopwaiter.LaunchPromiseThread[bool](s, func(ctx context.Context) (bool, error) { + select { + // createBlock can't be run in parallel + case <-s.semaphore: + defer func() { + // release semaphore, also in case of panic + s.semaphore <- struct{}{} + }() + return s.createBlock(ctx), nil + case <-ctx.Done(): + return false, ctx.Err() + } + }) +} + +type BenchSequencerAPI struct { + benchSequencer *BenchSequencer +} + +func (a *BenchSequencerAPI) TxQueueLength(includeRetryTxQueue bool) int { + return a.benchSequencer.TxQueueLength(includeRetryTxQueue) +} + +func (a *BenchSequencerAPI) TxRetryQueueLength() int { + return a.benchSequencer.TxRetryQueueLength() +} + +func (a *BenchSequencerAPI) CreateBlock(ctx context.Context) (bool, error) { + return a.benchSequencer.CreateBlock().Await(ctx) +} + +func NewBenchSequencerAPI(benchSequencer *BenchSequencer) *BenchSequencerAPI { + return &BenchSequencerAPI{benchSequencer: benchSequencer} +} diff --git a/execution/gethexec/bench_sequencer_config.go b/execution/gethexec/bench_sequencer_config.go new file mode 100644 index 0000000000..dc5eb08ca6 --- /dev/null +++ b/execution/gethexec/bench_sequencer_config.go @@ -0,0 +1,12 @@ +// DANGER! this file is included in all builds +// DANGER! do not place any of the experimental logic and features here + +package gethexec + +type BenchSequencerConfig struct { + Enable bool `koanf:"enable"` +} + +var BenchSequencerConfigDefault = BenchSequencerConfig{ + Enable: false, +} diff --git a/execution/gethexec/bench_sequencer_stub.go b/execution/gethexec/bench_sequencer_stub.go new file mode 100644 index 0000000000..2358ffe4b1 --- /dev/null +++ b/execution/gethexec/bench_sequencer_stub.go @@ -0,0 +1,25 @@ +//go:build !benchsequencer + +package gethexec + +import ( + "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/log" +) + +func BenchSequencerConfigAddOptions(_ string, _ *pflag.FlagSet) { + // don't add any options +} + +func (c *BenchSequencerConfig) Validate() error { + if c.Enable { + log.Warn("BenchSequencer is not supported in this build") + } + return nil +} + +func NewBenchSequencer(sequencer *Sequencer) (TransactionPublisher, interface{}) { + // do nothing + return sequencer, nil +} diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index 41dc140bb0..69feb67c4d 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -30,6 +30,7 @@ import ( "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/execution" + "github.com/offchainlabs/nitro/experimental/debugblock" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/util/arbmath" @@ -127,6 +128,7 @@ type Config struct { BlockMetadataApiBlocksLimit uint64 `koanf:"block-metadata-api-blocks-limit"` VmTrace LiveTracingConfig `koanf:"vmtrace"` ExposeMultiGas bool `koanf:"expose-multi-gas"` + Dangerous DangerousConfig `koanf:"dangerous"` forwardingTarget string } @@ -155,6 +157,9 @@ func (c *Config) Validate() error { if err := c.RPC.Validate(); err != nil { return err } + if err := c.Dangerous.Validate(); err != nil { + return err + } return nil } @@ -176,6 +181,7 @@ func ConfigAddOptions(prefix string, f *pflag.FlagSet) { f.Uint64(prefix+".block-metadata-api-blocks-limit", ConfigDefault.BlockMetadataApiBlocksLimit, "maximum number of blocks allowed to be queried for blockMetadata per arb_getRawBlockMetadata query. Enabled by default, set 0 to disable the limit") f.Bool(prefix+".expose-multi-gas", false, "experimental: expose multi-dimensional gas in transaction receipts") LiveTracingConfigAddOptions(prefix+".vmtrace", f) + DangerousConfigAddOptions(prefix+".dangerous", f) } type LiveTracingConfig struct { @@ -193,6 +199,31 @@ func LiveTracingConfigAddOptions(prefix string, f *pflag.FlagSet) { f.String(prefix+".json-config", DefaultLiveTracingConfig.JSONConfig, "(experimental) Tracer configuration in JSON format") } +type DangerousConfig struct { + DebugBlock debugblock.Config `koanf:"debug-block"` + BenchSequencer BenchSequencerConfig `koanf:"bench-sequencer"` +} + +var DefaultDangerousConfig = DangerousConfig{ + DebugBlock: debugblock.ConfigDefault, + BenchSequencer: BenchSequencerConfigDefault, +} + +func DangerousConfigAddOptions(prefix string, f *pflag.FlagSet) { + debugblock.ConfigAddOptions(prefix+".debug-block", f) + BenchSequencerConfigAddOptions(prefix+".bench-sequencer", f) +} + +func (c *DangerousConfig) Validate() error { + if err := c.DebugBlock.Validate(); err != nil { + return err + } + if err := c.BenchSequencer.Validate(); err != nil { + return err + } + return nil +} + var ConfigDefault = Config{ RPC: arbitrum.DefaultConfig, TxIndexer: DefaultTxIndexerConfig, @@ -273,6 +304,7 @@ func CreateExecutionNode( log.Warn("sequencer enabled without l1 client") } + var benchSequencerService interface{} if config.Sequencer.Enable { seqConfigFetcher := func() *SequencerConfig { return &configFetcher.Get().Sequencer } sequencer, err = NewSequencer(execEngine, parentChainReader, seqConfigFetcher, parentChainID) @@ -280,6 +312,9 @@ func CreateExecutionNode( return nil, err } txPublisher = sequencer + if config.Dangerous.BenchSequencer.Enable { + txPublisher, benchSequencerService = NewBenchSequencer(sequencer) + } } else { if config.Forwarder.RedisUrl != "" { txPublisher = NewRedisTxForwarder(config.forwardingTarget, &config.Forwarder) @@ -373,6 +408,13 @@ func CreateExecutionNode( Service: eth.NewDebugAPI(eth.NewArbEthereum(l2BlockChain, chainDB)), Public: false, }) + if benchSequencerService != nil { + apis = append(apis, rpc.API{ + Namespace: "benchseq", + Service: benchSequencerService, + Public: false, + }) + } stack.RegisterAPIs(apis) diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index f8658bda21..11e86622ae 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -37,6 +37,7 @@ import ( "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/execution" + "github.com/offchainlabs/nitro/experimental/debugblock" "github.com/offchainlabs/nitro/timeboost" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/containers" @@ -63,30 +64,30 @@ var ( ) type SequencerConfig struct { - Enable bool `koanf:"enable"` - MaxBlockSpeed time.Duration `koanf:"max-block-speed" reload:"hot"` - ReadFromTxQueueTimeout time.Duration `koanf:"read-from-tx-queue-timeout" reload:"hot"` - MaxRevertGasReject uint64 `koanf:"max-revert-gas-reject" reload:"hot"` - MaxAcceptableTimestampDelta time.Duration `koanf:"max-acceptable-timestamp-delta" reload:"hot"` - SenderWhitelist []string `koanf:"sender-whitelist"` - Forwarder ForwarderConfig `koanf:"forwarder"` - QueueSize int `koanf:"queue-size"` - QueueTimeout time.Duration `koanf:"queue-timeout" reload:"hot"` - NonceCacheSize int `koanf:"nonce-cache-size" reload:"hot"` - MaxTxDataSize int `koanf:"max-tx-data-size" reload:"hot"` - NonceFailureCacheSize int `koanf:"nonce-failure-cache-size" reload:"hot"` - NonceFailureCacheExpiry time.Duration `koanf:"nonce-failure-cache-expiry" reload:"hot"` - ExpectedSurplusGasPriceMode string `koanf:"expected-surplus-gas-price-mode"` - ExpectedSurplusSoftThreshold string `koanf:"expected-surplus-soft-threshold" reload:"hot"` - ExpectedSurplusHardThreshold string `koanf:"expected-surplus-hard-threshold" reload:"hot"` - EnableProfiling bool `koanf:"enable-profiling" reload:"hot"` - Timeboost TimeboostConfig `koanf:"timeboost"` - Dangerous DangerousConfig `koanf:"dangerous"` + Enable bool `koanf:"enable"` + MaxBlockSpeed time.Duration `koanf:"max-block-speed" reload:"hot"` + ReadFromTxQueueTimeout time.Duration `koanf:"read-from-tx-queue-timeout" reload:"hot"` + MaxRevertGasReject uint64 `koanf:"max-revert-gas-reject" reload:"hot"` + MaxAcceptableTimestampDelta time.Duration `koanf:"max-acceptable-timestamp-delta" reload:"hot"` + SenderWhitelist []string `koanf:"sender-whitelist"` + Forwarder ForwarderConfig `koanf:"forwarder"` + QueueSize int `koanf:"queue-size"` + QueueTimeout time.Duration `koanf:"queue-timeout" reload:"hot"` + NonceCacheSize int `koanf:"nonce-cache-size" reload:"hot"` + MaxTxDataSize int `koanf:"max-tx-data-size" reload:"hot"` + NonceFailureCacheSize int `koanf:"nonce-failure-cache-size" reload:"hot"` + NonceFailureCacheExpiry time.Duration `koanf:"nonce-failure-cache-expiry" reload:"hot"` + ExpectedSurplusGasPriceMode string `koanf:"expected-surplus-gas-price-mode"` + ExpectedSurplusSoftThreshold string `koanf:"expected-surplus-soft-threshold" reload:"hot"` + ExpectedSurplusHardThreshold string `koanf:"expected-surplus-hard-threshold" reload:"hot"` + EnableProfiling bool `koanf:"enable-profiling" reload:"hot"` + Timeboost TimeboostConfig `koanf:"timeboost"` + Dangerous SequencerDangerousConfig `koanf:"dangerous"` expectedSurplusSoftThreshold int expectedSurplusHardThreshold int } -type DangerousConfig struct { +type SequencerDangerousConfig struct { DisableSeqInboxMaxDataSizeCheck bool `koanf:"disable-seq-inbox-max-data-size-check"` DisableBlobBaseFeeCheck bool `koanf:"disable-blob-base-fee-check"` } @@ -194,10 +195,10 @@ var DefaultSequencerConfig = SequencerConfig{ ExpectedSurplusHardThreshold: "default", EnableProfiling: false, Timeboost: DefaultTimeboostConfig, - Dangerous: DefaultDangerousConfig, + Dangerous: DefaultSequencerDangerousConfig, } -var DefaultDangerousConfig = DangerousConfig{ +var DefaultSequencerDangerousConfig = SequencerDangerousConfig{ DisableSeqInboxMaxDataSizeCheck: false, } @@ -211,7 +212,7 @@ func SequencerConfigAddOptions(prefix string, f *pflag.FlagSet) { AddOptionsForSequencerForwarderConfig(prefix+".forwarder", f) TimeboostAddOptions(prefix+".timeboost", f) - DangerousAddOptions(prefix+".dangerous", f) + SequencerDangerousAddOptions(prefix+".dangerous", f) f.Int(prefix+".queue-size", DefaultSequencerConfig.QueueSize, "size of the pending tx queue") f.Duration(prefix+".queue-timeout", DefaultSequencerConfig.QueueTimeout, "maximum amount of time transaction can wait in queue") f.Int(prefix+".nonce-cache-size", DefaultSequencerConfig.NonceCacheSize, "size of the tx sender nonce cache") @@ -237,9 +238,9 @@ func TimeboostAddOptions(prefix string, f *pflag.FlagSet) { f.Uint64(prefix+".queue-timeout-in-blocks", DefaultTimeboostConfig.QueueTimeoutInBlocks, "maximum amount of time (measured in blocks) that Express Lane transactions can wait in the sequencer's queue") } -func DangerousAddOptions(prefix string, f *pflag.FlagSet) { - f.Bool(prefix+".disable-seq-inbox-max-data-size-check", DefaultDangerousConfig.DisableSeqInboxMaxDataSizeCheck, "DANGEROUS! disables nitro checks on sequencer MaxTxDataSize against the sequencer inbox MaxDataSize") - f.Bool(prefix+".disable-blob-base-fee-check", DefaultDangerousConfig.DisableBlobBaseFeeCheck, "DANGEROUS! disables nitro checks on sequencer for blob base fee") +func SequencerDangerousAddOptions(prefix string, f *pflag.FlagSet) { + f.Bool(prefix+".disable-seq-inbox-max-data-size-check", DefaultSequencerDangerousConfig.DisableSeqInboxMaxDataSizeCheck, "DANGEROUS! disables nitro checks on sequencer MaxTxDataSize against the sequencer inbox MaxDataSize") + f.Bool(prefix+".disable-blob-base-fee-check", DefaultSequencerDangerousConfig.DisableBlobBaseFeeCheck, "DANGEROUS! disables nitro checks on sequencer for blob base fee") } type txQueueItem struct { @@ -1221,6 +1222,22 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { var startOfReadingFromTxQueue time.Time + if s.execEngine.bc.Config().DebugMode() { + chainConfig := s.execEngine.bc.Config() + if lastBlock.Number.Uint64()+1 == chainConfig.ArbitrumChainParams.DebugBlock { + // publish transaction to trigger next block + tx := debugblock.PrepareDebugTransaction(chainConfig, lastBlock) + if tx != nil { + go func() { + if err := s.PublishTransaction(ctx, tx, nil); err != nil { + log.Error("debug block: failed to publish tx", "err", err) + } else { + log.Warn("published dangerous debug block tx", "txHash", tx.Hash()) + } + }() + } + } + } for { if len(queueItems) == 1 { startOfReadingFromTxQueue = time.Now() diff --git a/experimental/debugblock/config.go b/experimental/debugblock/config.go new file mode 100644 index 0000000000..cb96867acb --- /dev/null +++ b/experimental/debugblock/config.go @@ -0,0 +1,19 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +// DANGER! this file is included in all builds +// DANGER! do not place any of the experimental logic and features here + +package debugblock + +type Config struct { + OverwriteChainConfig bool `koanf:"overwrite-chain-config"` + DebugAddress string `koanf:"debug-address"` + DebugBlockNum uint64 `koanf:"debug-blocknum"` +} + +var ConfigDefault = Config{ + OverwriteChainConfig: false, + DebugAddress: "", + DebugBlockNum: 0, +} diff --git a/experimental/debugblock/debug_block.go b/experimental/debugblock/debug_block.go new file mode 100644 index 0000000000..873c327956 --- /dev/null +++ b/experimental/debugblock/debug_block.go @@ -0,0 +1,129 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +//go:build debugblock + +package debugblock + +import ( + "crypto/ecdsa" + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/util/arbmath" + "github.com/spf13/pflag" +) + +func ConfigAddOptions(prefix string, f *pflag.FlagSet) { + f.Bool(prefix+".overwrite-chain-config", ConfigDefault.OverwriteChainConfig, "DANGEROUS! overwrites chain when opening existing database; chain debug mode will be enabled") + f.String(prefix+".debug-address", ConfigDefault.DebugAddress, "DANGEROUS! address of debug account to be pre-funded") + f.Uint64(prefix+".debug-blocknum", ConfigDefault.DebugBlockNum, "DANGEROUS! block number of injected debug block") +} + +func (c *Config) Validate() error { + if c.OverwriteChainConfig { + log.Warn("DANGER! overwrite-chain-config set, chain config will be over-written") + } + if c.DebugAddress != "" && !common.IsHexAddress(c.DebugAddress) { + return errors.New("invalid debug-address, hex address expected") + } + if c.DebugBlockNum != 0 { + log.Warn("DANGER! debug-blocknum set", "blocknum", c.DebugBlockNum) + } + return nil +} + +func (c *Config) Apply(chainConfig *params.ChainConfig) { + if c.OverwriteChainConfig { + chainConfig.ArbitrumChainParams.AllowDebugPrecompiles = true + chainConfig.ArbitrumChainParams.DebugBlock = c.DebugBlockNum + debugAddress := common.HexToAddress(c.DebugAddress) + chainConfig.ArbitrumChainParams.DebugAddress = &debugAddress + } +} + +// private key and address of account to be used by PrepareDebugTransaction +func triggerPrivateKeyAndAddress() (*ecdsa.PrivateKey, common.Address, error) { + key, err := crypto.HexToECDSA("acb2d96fc54f5db4530d6c5a6adfd10964b1b62222d875e08b68b72cc9b9935c") + if err != nil { + return nil, common.Address{}, err + } + return key, crypto.PubkeyToAddress(key.PublicKey), nil +} + +// prepares transaction used to trigger debug block creation +// the transaction needs pre-funding within DebugBlockStateUpdate (executed in the begging of debug block, before the trigger transaction) +func PrepareDebugTransaction(chainConfig *params.ChainConfig, lastHeader *types.Header) *types.Transaction { + if !chainConfig.DebugMode() { + return nil + } + privateKey, address, err := triggerPrivateKeyAndAddress() + if err != nil { + log.Error("debug block: failed to get hardcoded private key and address", "err", err) + return nil + } + transferGas := util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for L1 costs + txData := &types.DynamicFeeTx{ + To: &address, + Gas: transferGas, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(params.GWei), + Value: big.NewInt(0), + Nonce: 0, + Data: nil, + } + nextHeaderNumber := arbmath.BigAdd(lastHeader.Number, common.Big1) + arbosVersion := types.DeserializeHeaderExtraInformation(lastHeader).ArbOSFormatVersion + signer := types.MakeSigner(chainConfig, nextHeaderNumber, lastHeader.Time, arbosVersion) + tx := types.NewTx(txData) + tx, err = types.SignTx(tx, signer, privateKey) + if err != nil { + log.Error("debug block: failed to sign trigger tx", "address", address, "err", err) + return nil + } + return tx +} + +func DebugBlockStateUpdate(statedb *state.StateDB, expectedBalanceDelta *big.Int, chainConfig *params.ChainConfig) { + // fund trigger account - used to send the transaction that triggered this block and needs pre-funding to succeed (at least one successful tx is required for the block to be appended to the chain) + transferGas := util.NormalizeL2GasForL1GasInitial(800_000, 100*params.GWei) // include room for L1 costs + triggerCost := uint256.MustFromBig(new(big.Int).Mul(big.NewInt(int64(transferGas)), big.NewInt(100*params.GWei))) + _, triggerAddress, err := triggerPrivateKeyAndAddress() + if err != nil { + log.Error("debug block: failed to get hardcoded address", "err", err) + return + } + statedb.AddBalance(triggerAddress, triggerCost, tracing.BalanceChangeUnspecified) + expectedBalanceDelta.Add(expectedBalanceDelta, triggerCost.ToBig()) + + // fund debug account + if chainConfig.ArbitrumChainParams.DebugAddress != nil { + balance := uint256.MustFromBig(new(big.Int).Lsh(big.NewInt(1), 254)) + statedb.AddBalance(*chainConfig.ArbitrumChainParams.DebugAddress, balance, tracing.BalanceChangeUnspecified) + expectedBalanceDelta.Add(expectedBalanceDelta, balance.ToBig()) + log.Warn("DANGER! debug block: funding debug account", "debugAddress", chainConfig.ArbitrumChainParams.DebugAddress) + } + + // save current chain config to arbos state in case it was changed to enable debug mode and debug block + // replay binary reads chain config from arbos state, that will enable successful validation of future blocks + // (debug block will still fail validation if chain config was changed off-chain) + if serializedChainConfig, err := json.Marshal(chainConfig); err != nil { + log.Error("debug block: failed to marshal chain config", "err", err) + } else if arbStateWrite, err := arbosState.OpenSystemArbosState(statedb, nil, false); err != nil { + log.Error("debug block: failed to open arbos state for writing", "err", err) + } else if err = arbStateWrite.SetChainConfig(serializedChainConfig); err != nil { + log.Error("debug block: failed to set chain config in arbos state", "err", err) + } + log.Warn("DANGER! debug block: state update applied") +} diff --git a/experimental/debugblock/debug_block_stub.go b/experimental/debugblock/debug_block_stub.go new file mode 100644 index 0000000000..8589bb164d --- /dev/null +++ b/experimental/debugblock/debug_block_stub.go @@ -0,0 +1,41 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +//go:build !debugblock + +package debugblock + +import ( + "math/big" + + "github.com/spf13/pflag" + + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +func (c *Config) Validate() error { + if c.OverwriteChainConfig || c.DebugAddress != "" || c.DebugBlockNum != 0 { + log.Warn("debug block injection is not supported in this build") + } + return nil +} + +func (c *Config) Apply(_ *params.ChainConfig) { + // do nothing +} + +func ConfigAddOptions(_ string, _ *pflag.FlagSet) { + // don't add any of debug block options +} + +func PrepareDebugTransaction(_ *params.ChainConfig, _ *types.Header) *types.Transaction { + log.Warn("PrepareDebugTransaction is not supported in this build") + return nil +} + +func DebugBlockStateUpdate(_ *state.StateDB, _ *big.Int, _ *params.ChainConfig) { + log.Warn("DebugBlockStateUpdate is not supported in this build") +} diff --git a/go-ethereum b/go-ethereum index f71771a4a9..0f8894fefb 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit f71771a4a9b1601ece8126f3d66d5c9368bb24b4 +Subproject commit 0f8894fefbfffab219e121e2414fa9fa3864cb0e diff --git a/system_tests/bench_sequencer_stub_test.go b/system_tests/bench_sequencer_stub_test.go new file mode 100644 index 0000000000..f86053462b --- /dev/null +++ b/system_tests/bench_sequencer_stub_test.go @@ -0,0 +1,66 @@ +//go:build !benchsequencer + +package arbtest + +import ( + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/types" +) + +func TestBenchSequencerStub(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builder.execConfig.Dangerous.BenchSequencer.Enable = true + + cleanup := builder.Build(t) + defer cleanup() + + // check benchseq rpc is not available + rpcClient := builder.L2.Client.Client() + var txQueueLen int + err := rpcClient.CallContext(ctx, &txQueueLen, "benchseq_txQueueLength", false) + if err == nil { + Fatal(t, "benchseq_txQueueLength should not have succeeded") + } else if !strings.Contains(err.Error(), "the method benchseq_txQueueLength does not exist") { + Fatal(t, "benchseq_txQueueLength failed with unexpected error:", err) + } + err = rpcClient.CallContext(ctx, &txQueueLen, "benchseq_txRetryQueueLength") + if err == nil { + Fatal(t, "benchseq_txRetryQueueLength should not have succeeded") + } else if !strings.Contains(err.Error(), "the method benchseq_txRetryQueueLength does not exist") { + Fatal(t, "benchseq_txRetryQueueLength failed with unexpected error:", err) + } + var blockCreated bool + // create block with all of the transactions (they should fit) + err = rpcClient.CallContext(ctx, &blockCreated, "benchseq_createBlock") + if err == nil { + Fatal(t, "benchseq_createBlock should not have succeeded") + } else if !strings.Contains(err.Error(), "the method benchseq_createBlock does not exist") { + Fatal(t, "benchseq_createBlock failed with unexpected error:", err) + } + + // check that blocks are created automatically + startBlock, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + tx := builder.L2Info.PrepareTx("Owner", "Owner", builder.L2Info.TransferGas, big.NewInt(1), nil) + builder.L2.SendWaitTestTransactions(t, types.Transactions{tx}) + block, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + timeout := time.After(5 * time.Second) + for block <= startBlock { + select { + case <-timeout: + Fatal(t, "timeout exceeded while waiting for new block") + case <-time.After(20 * time.Millisecond): + } + block, err = builder.L2.Client.BlockNumber(ctx) + Require(t, err) + } +} diff --git a/system_tests/bench_sequencer_test.go b/system_tests/bench_sequencer_test.go new file mode 100644 index 0000000000..b09e5cc8a6 --- /dev/null +++ b/system_tests/bench_sequencer_test.go @@ -0,0 +1,98 @@ +//go:build benchsequencer + +package arbtest + +import ( + "context" + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/types" +) + +func TestExperimentalBenchSequencer(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + // we don't want any txes sent during NodeBuilder.Build as they will hang and timeout due to no blocks beeing created automatically + builder = builder.DontSendL2SetupTxes() + builder.execConfig.Dangerous.BenchSequencer.Enable = true + + cleanup := builder.Build(t) + defer cleanup() + + startBlock, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + + rpcClient := builder.L2.Client.Client() + var txSendersWg sync.WaitGroup + var txes types.Transactions + for i := 0; i < 5; i++ { + // send the transaction in separate thread as the rpc call will wait for it to be accepted by sequencer + tx := builder.L2Info.PrepareTx("Owner", "Owner", builder.L2Info.TransferGas, big.NewInt(1), nil) + txes = append(txes, tx) + txSendersWg.Add(1) + go func() { + defer txSendersWg.Done() + err := builder.L2.Client.SendTransaction(ctx, tx) + Require(t, err) + }() + + // wait for the transaction to be enqueued + var txQueueLen int + err := rpcClient.CallContext(ctx, &txQueueLen, "benchseq_txQueueLength", false) + Require(t, err) + timeout := time.After(5 * time.Second) + for txQueueLen < i+1 { + err := rpcClient.CallContext(ctx, &txQueueLen, "benchseq_txQueueLength", false) + Require(t, err) + select { + case <-timeout: + Fatal(t, "timeout exceeded while waiting for tx queue to grow") + case <-time.After(10 * time.Millisecond): + } + } + } + + block, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + if block != startBlock { + Fatal(t, "block have been created even though benchseq_createBlock hasn't been called") + } + + var blockCreated bool + // create block with all of the transactions (they should fit) + err = rpcClient.CallContext(ctx, &blockCreated, "benchseq_createBlock") + Require(t, err) + if !blockCreated { + Fatal(t, "block should have been created") + } + // check that tx queue is empty + var txQueueLen int + err = rpcClient.CallContext(ctx, &txQueueLen, "benchseq_txQueueLength", false) + Require(t, err) + if txQueueLen != 0 { + Fatal(t, "benchseq_txQueueLenght reported non empty queue, want: 0, have:", txQueueLen) + } + + txSendersWg.Wait() + for _, tx := range txes { + builder.L2.EnsureTxSucceeded(tx) + } + + block, err = builder.L2.Client.BlockNumber(ctx) + Require(t, err) + timeout := time.After(5 * time.Second) + for block != startBlock+1 { + select { + case <-timeout: + Fatal(t, "timeout exceeded while waiting for new block") + case <-time.After(20 * time.Millisecond): + } + block, err = builder.L2.Client.BlockNumber(ctx) + Require(t, err) + } +} diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 24bbf7b8ec..df683a81bc 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -448,6 +448,11 @@ func (b *NodeBuilder) TakeOwnership() *NodeBuilder { return b } +func (b *NodeBuilder) DontSendL2SetupTxes() *NodeBuilder { + b.takeOwnership = false // taking ownership requires sequencing arbdebug call + return b +} + func (b *NodeBuilder) Build(t *testing.T) func() { if b.parallelise { b.parallelise = false @@ -894,7 +899,11 @@ func (b *NodeBuilder) RestartL2Node(t *testing.T) { l1Client = b.L1.Client } consensusConfigFetcher := NewCommonConfigFetcher(b.nodeConfig) - currentNode, err := arbnode.CreateNodeFullExecutionClient(b.ctx, stack, execNode, execNode, execNode, execNode, arbDb, consensusConfigFetcher, blockchain.Config(), l1Client, b.addresses, validatorTxOpts, sequencerTxOpts, dataSigner, feedErrChan, big.NewInt(1337), nil, locator.LatestWasmModuleRoot()) + chainConfig := blockchain.Config() + if b.execConfig.Dangerous.DebugBlock.OverwriteChainConfig { + b.execConfig.Dangerous.DebugBlock.Apply(chainConfig) + } + currentNode, err := arbnode.CreateNodeFullExecutionClient(b.ctx, stack, execNode, execNode, execNode, execNode, arbDb, consensusConfigFetcher, chainConfig, l1Client, b.addresses, validatorTxOpts, sequencerTxOpts, dataSigner, feedErrChan, big.NewInt(1337), nil, locator.LatestWasmModuleRoot()) Require(t, err) Require(t, currentNode.Start(b.ctx)) @@ -2217,3 +2226,23 @@ func populateMachineDir(t *testing.T, cr *github.ConsensusRelease) string { Require(t, err) return machineDir } + +// will call foo with specified interval, until foo returns true or specified timeout elapses +// if timeout elapses fails with t.Fatal with timeoutMessage appended to the message +// note: use pollWithDeadlineDefault if you don't care much about the interval and timeout, should make it easier to globally tune the tests +func pollWithDeadline(t *testing.T, interval time.Duration, timeout time.Duration, foo func() bool) bool { + t.Helper() + deadline := time.After(timeout) + for !foo() { + select { + case <-deadline: + return false + case <-time.After(interval): + } + } + return true +} + +func pollWithDeadlineDefault(t *testing.T, foo func() bool) bool { + return pollWithDeadline(t, 20*time.Millisecond, 5*time.Second, foo) +} diff --git a/system_tests/debug_block_common_test.go b/system_tests/debug_block_common_test.go new file mode 100644 index 0000000000..7b225b4151 --- /dev/null +++ b/system_tests/debug_block_common_test.go @@ -0,0 +1,109 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +package arbtest + +import ( + "context" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func testDebugBlockInjection(t *testing.T, expectInject bool) { + t.Run("with-other-tx", func(t *testing.T) { + testDebugBlockInjectionImpl(t, expectInject, true) + }) + t.Run("without-other-tx", func(t *testing.T) { + testDebugBlockInjectionImpl(t, expectInject, false) + }) +} + +func testDebugBlockInjectionImpl(t *testing.T, expectInject bool, withOtherTx bool) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + startBlock, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + + // send a transaction to advance the chain + builder.L2Info.GenerateAccount("SomeUser") + tx := builder.L2Info.PrepareTx("Owner", "SomeUser", builder.L2Info.TransferGas, common.Big1, nil) + builder.L2.SendWaitTestTransactions(t, types.Transactions{tx}) + + // make sure that DebugUser can't send a tx just yet + builder.L2Info.GenerateAccount("DebugUser") + debugUserTx := builder.L2Info.PrepareTx("DebugUser", "SomeUser", builder.L2Info.TransferGas, common.Big1, nil) + err = builder.L2.Client.SendTransaction(ctx, debugUserTx) + if err == nil { + t.Fatal("debugUserTx shouldn't have succeeded before prefunding DebugUser account") + } + + // make sure the chain advanced + lastBlock := startBlock + advanced := pollWithDeadlineDefault(t, func() bool { + var err error + lastBlock, err = builder.L2.Client.BlockNumber(ctx) + Require(t, err) + return lastBlock > startBlock + }) + if !advanced { + t.Fatal("failed to advance chain: timeout exceeded") + } + + builder.L2.cleanup() + builder.L2.cleanup = func() {} + t.Log("l2 node stopped") + + // configure debug block injection + debugBlockNum := lastBlock + 1 + builder.execConfig.Dangerous.DebugBlock.OverwriteChainConfig = true + builder.execConfig.Dangerous.DebugBlock.DebugBlockNum = debugBlockNum + builder.execConfig.Dangerous.DebugBlock.DebugAddress = builder.L2Info.GetInfoWithPrivKey("DebugUser").Address.String() + + builder.RestartL2Node(t) + t.Log("restarted l2 node") + + if withOtherTx { + tx := builder.L2Info.PrepareTx("Owner", "SomeUser", builder.L2Info.TransferGas, common.Big1, nil) + builder.L2.SendWaitTestTransactions(t, types.Transactions{tx}) + } + + interval := 25 * time.Millisecond + timeout := 5 * time.Second + if !expectInject && !withOtherTx { + // shorter deadline for expected timeout + timeout = 100 * time.Millisecond + } + + debugBlockReached := pollWithDeadline(t, interval, timeout, func() bool { + current, err := builder.L2.Client.BlockNumber(ctx) + Require(t, err) + t.Log("current block:", current, "debug block:", debugBlockNum) + return current >= debugBlockNum + }) + + if expectInject { + if !debugBlockReached { + t.Fatalf("debug block number not reached: %v timeout exceeded", timeout) + } + // make sure that DebugUser can send a tx now + builder.L2.SendWaitTestTransactions(t, types.Transactions{debugUserTx}) + } else { + if debugBlockReached && !withOtherTx { + t.Error("debug block number reached with no other txes to advance chain") + } + // make sure that DebugUser still can't send a tx + err = builder.L2.Client.SendTransaction(ctx, debugUserTx) + if err == nil { + t.Fatal("debugUserTx shouldn't have succeeded in production build") + } + } + +} diff --git a/system_tests/debug_block_stub_test.go b/system_tests/debug_block_stub_test.go new file mode 100644 index 0000000000..ce9593255f --- /dev/null +++ b/system_tests/debug_block_stub_test.go @@ -0,0 +1,14 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +//go:build !debugblock + +package arbtest + +import ( + "testing" +) + +func TestDebugBlockInjectionStub(t *testing.T) { + testDebugBlockInjection(t, false) +} diff --git a/system_tests/debug_block_test.go b/system_tests/debug_block_test.go new file mode 100644 index 0000000000..015bc29c30 --- /dev/null +++ b/system_tests/debug_block_test.go @@ -0,0 +1,14 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/nitro/blob/master/LICENSE.md + +//go:build debugblock + +package arbtest + +import ( + "testing" +) + +func TestExperimentalDebugBlockInjection(t *testing.T) { + testDebugBlockInjection(t, true) +}