diff --git a/.gitmodules b/.gitmodules index 7c78791c78..c3cb5fc5fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,3 +20,6 @@ [submodule "nitro-testnode"] path = nitro-testnode url = https://github.com/OffchainLabs/nitro-testnode.git +[submodule "bold"] + path = bold + url = https://github.com/OffchainLabs/bold.git diff --git a/Dockerfile b/Dockerfile index b966125b66..1a59930d94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,6 +75,7 @@ COPY ./contracts/package.json ./contracts/yarn.lock ./contracts/ COPY ./solgen/gen.go ./solgen/ COPY ./fastcache ./fastcache COPY ./go-ethereum ./go-ethereum +COPY ./bold ./bold COPY --from=brotli-wasm-export / target/ COPY --from=contracts-builder workspace/contracts/build/contracts/src/precompiles/ contracts/build/contracts/src/precompiles/ COPY --from=contracts-builder workspace/contracts/node_modules/@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json contracts/ @@ -179,6 +180,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ COPY go.mod go.sum ./ COPY go-ethereum/go.mod go-ethereum/go.sum go-ethereum/ COPY fastcache/go.mod fastcache/go.sum fastcache/ +COPY bold/go.mod bold/go.sum bold/ RUN go mod download COPY . ./ COPY --from=contracts-builder workspace/contracts/build/ contracts/build/ @@ -251,6 +253,7 @@ USER root RUN rm -f /home/user/target/machines/latest COPY --from=prover-export /bin/jit /usr/local/bin/ COPY --from=node-builder /workspace/target/bin/deploy /usr/local/bin/ +COPY --from=node-builder /workspace/target/bin/bold-deploy /usr/local/bin/ COPY --from=node-builder /workspace/target/bin/seq-coordinator-invalidate /usr/local/bin/ COPY --from=module-root-calc /workspace/target/machines/latest/machine.wavm.br /home/user/target/machines/latest/ COPY --from=module-root-calc /workspace/target/machines/latest/until-host-io-state.bin /home/user/target/machines/latest/ diff --git a/Makefile b/Makefile index 4221100961..0f696462b3 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ push: lint test-go .make/fmt all: build build-replay-env test-gen-proofs @touch .make/all -build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daserver datool seq-coordinator-invalidate nitro-val seq-coordinator-manager) +build: $(patsubst %,$(output_root)/bin/%, nitro deploy bold-deploy relay daserver datool seq-coordinator-invalidate nitro-val seq-coordinator-manager) @printf $(done) build-node-deps: $(go_source) build-prover-header build-prover-lib build-jit .make/solgen .make/cbrotli-lib @@ -170,6 +170,9 @@ $(output_root)/bin/nitro: $(DEP_PREDICATE) build-node-deps $(output_root)/bin/deploy: $(DEP_PREDICATE) build-node-deps go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/deploy" +$(output_root)/bin/bold-deploy: $(DEP_PREDICATE) build-node-deps + go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/bold-deploy" + $(output_root)/bin/relay: $(DEP_PREDICATE) build-node-deps go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/relay" diff --git a/arbitrator/prover/src/lib.rs b/arbitrator/prover/src/lib.rs index e4ea7a06c5..42a94a6f81 100644 --- a/arbitrator/prover/src/lib.rs +++ b/arbitrator/prover/src/lib.rs @@ -89,10 +89,17 @@ unsafe fn arbitrator_load_machine_impl( } #[no_mangle] -pub unsafe extern "C" fn arbitrator_load_wavm_binary(binary_path: *const c_char) -> *mut Machine { +pub unsafe extern "C" fn arbitrator_load_wavm_binary( + binary_path: *const c_char, + always_merkleize: u8, +) -> *mut Machine { let binary_path = cstr_to_string(binary_path); let binary_path = Path::new(&binary_path); - match Machine::new_from_wavm(binary_path) { + let mut merkleize = false; + if always_merkleize == 1 { + merkleize = true; + } + match Machine::new_from_wavm(binary_path, merkleize) { Ok(mach) => Box::into_raw(Box::new(mach)), Err(err) => { eprintln!("Error loading binary: {}", err); diff --git a/arbitrator/prover/src/machine.rs b/arbitrator/prover/src/machine.rs index 0849312f3d..8f687aca5f 100644 --- a/arbitrator/prover/src/machine.rs +++ b/arbitrator/prover/src/machine.rs @@ -1165,7 +1165,7 @@ impl Machine { Ok(mach) } - pub fn new_from_wavm(wavm_binary: &Path) -> Result { + pub fn new_from_wavm(wavm_binary: &Path, always_merkleize: bool) -> Result { let f = BufReader::new(File::open(wavm_binary)?); let decompressor = brotli2::read::BrotliDecoder::new(f); let mut modules: Vec = bincode::deserialize_from(decompressor)?; @@ -1191,6 +1191,16 @@ impl Machine { MerkleType::Function, module.funcs.iter().map(Function::hash).collect(), )); + if always_merkleize { + module.memory.cache_merkle_tree(); + } + } + let mut modules_merkle = None; + if always_merkleize { + modules_merkle = Some(Merkle::new( + MerkleType::Module, + modules.iter().map(Module::hash).collect(), + )); } let mut mach = Machine { status: MachineStatus::Running, @@ -1199,7 +1209,7 @@ impl Machine { internal_stack: Vec::new(), frame_stack: Vec::new(), modules, - modules_merkle: None, + modules_merkle, global_state: Default::default(), pc: ProgramCounter::default(), stdio_output: Vec::new(), diff --git a/arbnode/dataposter/data_poster.go b/arbnode/dataposter/data_poster.go index 266131a6b9..be3f2bc6e2 100644 --- a/arbnode/dataposter/data_poster.go +++ b/arbnode/dataposter/data_poster.go @@ -256,10 +256,11 @@ func (p *DataPoster) Sender() common.Address { } func (p *DataPoster) MaxMempoolTransactions() uint64 { - if p.usingNoOpStorage { - return 1 - } - return p.config().MaxMempoolTransactions + // if p.usingNoOpStorage { + // return 1 + // } + // return p.config().MaxMempoolTransactions + return 1000 } // Does basic check whether posting transaction with specified nonce would @@ -491,6 +492,18 @@ func (p *DataPoster) PostTransaction(ctx context.Context, dataCreatedAt time.Tim return fullTx, p.sendTx(ctx, nil, &queuedTx) } +func (p *DataPoster) PostSimpleTransactionAutoNonce(ctx context.Context, to common.Address, calldata []byte, gasLimit uint64, value *big.Int) (*types.Transaction, error) { + nonce, _, err := p.GetNextNonceAndMeta(ctx) + if err != nil { + return nil, err + } + return p.PostSimpleTransaction(ctx, nonce, to, calldata, gasLimit, value) +} + +func (p *DataPoster) PostSimpleTransaction(ctx context.Context, nonce uint64, to common.Address, calldata []byte, gasLimit uint64, value *big.Int) (*types.Transaction, error) { + return p.PostTransaction(ctx, time.Now(), nonce, nil, to, calldata, gasLimit, value, nil) +} + // the mutex must be held by the caller func (p *DataPoster) saveTx(ctx context.Context, prevTx, newTx *storage.QueuedTransaction) error { if prevTx != nil && prevTx.Data.Nonce != newTx.Data.Nonce { @@ -826,7 +839,7 @@ var DefaultDataPosterConfig = DataPosterConfig{ WaitForL1Finality: true, TargetPriceGwei: 60., UrgencyGwei: 2., - MaxMempoolTransactions: 10, + MaxMempoolTransactions: 1000, MinTipCapGwei: 0.05, MaxTipCapGwei: 5, NonceRbfSoftConfs: 1, @@ -840,7 +853,7 @@ var DefaultDataPosterConfig = DataPosterConfig{ var DefaultDataPosterConfigForValidator = func() DataPosterConfig { config := DefaultDataPosterConfig - config.MaxMempoolTransactions = 1 // the validator cannot queue transactions + config.MaxMempoolTransactions = 1000 return config }() @@ -850,7 +863,7 @@ var TestDataPosterConfig = DataPosterConfig{ WaitForL1Finality: false, TargetPriceGwei: 60., UrgencyGwei: 2., - MaxMempoolTransactions: 10, + MaxMempoolTransactions: 1000, MinTipCapGwei: 0.05, MaxTipCapGwei: 5, NonceRbfSoftConfs: 1, diff --git a/arbnode/node.go b/arbnode/node.go index c6e117e700..34abd1b2f0 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -12,8 +12,12 @@ import ( "strings" "time" + solimpl "github.com/OffchainLabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/OffchainLabs/bold/challenge-manager" flag "github.com/spf13/pflag" + modes "github.com/OffchainLabs/bold/challenge-manager/types" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" @@ -91,6 +95,7 @@ type Config struct { TransactionStreamer TransactionStreamerConfig `koanf:"transaction-streamer" reload:"hot"` Maintenance MaintenanceConfig `koanf:"maintenance" reload:"hot"` ResourceMgmt resourcemanager.Config `koanf:"resource-mgmt" reload:"hot"` + Bold staker.BoldConfig `koanf:"bold" reload:"hot"` } func (c *Config) Validate() error { @@ -118,6 +123,9 @@ func (c *Config) Validate() error { if err := c.Staker.Validate(); err != nil { return err } + if err := c.Bold.Validate(); err != nil { + return err + } return nil } @@ -158,6 +166,7 @@ var ConfigDefault = Config{ MessagePruner: DefaultMessagePrunerConfig, BlockValidator: staker.DefaultBlockValidatorConfig, Feed: broadcastclient.FeedConfigDefault, + Bold: staker.DefaultBoldConfig, Staker: staker.DefaultL1ValidatorConfig, SeqCoordinator: DefaultSeqCoordinatorConfig, DataAvailability: das.DefaultDataAvailabilityConfig, @@ -546,6 +555,89 @@ func createNodeImpl( statelessBlockValidator = nil } + if config.Bold.Enable { + dp, err := StakerDataposter( + ctx, + rawdb.NewTable(arbDb, storage.StakerPrefix), + l1Reader, + txOptsValidator, + configFetcher, + syncMonitor, + ) + if err != nil { + return nil, err + } + rollupBindings, err := rollupgen.NewRollupUserLogic(deployInfo.Rollup, l1client) + if err != nil { + return nil, fmt.Errorf("could not create rollup bindings: %w", err) + } + chalManager, err := rollupBindings.ChallengeManager(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("could not get challenge manager: %w", err) + } + assertionChain, err := solimpl.NewAssertionChain(ctx, deployInfo.Rollup, chalManager, txOptsValidator, l1client, solimpl.NewDataPosterTransactor(dp)) + if err != nil { + return nil, fmt.Errorf("could not create assertion chain: %w", err) + } + blockChallengeLeafHeight := l2stateprovider.Height(config.Bold.BlockChallengeLeafHeight) + bigStepHeight := l2stateprovider.Height(config.Bold.BigStepLeafHeight) + smallStepHeight := l2stateprovider.Height(config.Bold.SmallStepLeafHeight) + stateManager, err := staker.NewStateManager( + statelessBlockValidator, + config.Bold.MachineLeavesCachePath, + []l2stateprovider.Height{ + blockChallengeLeafHeight, + bigStepHeight, + smallStepHeight, + }, + config.Bold.ValidatorName, + ) + if err != nil { + return nil, fmt.Errorf("could not create state manager: %w", err) + } + providerHeights := []l2stateprovider.Height{blockChallengeLeafHeight} + for i := uint64(0); i < config.Bold.NumBigSteps; i++ { + providerHeights = append(providerHeights, bigStepHeight) + } + providerHeights = append(providerHeights, smallStepHeight) + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + providerHeights, + stateManager, + nil, + ) + postingInterval := time.Second * time.Duration(config.Bold.AssertionPostingIntervalSeconds) + scanningInteval := time.Second * time.Duration(config.Bold.AssertionScanningIntervalSeconds) + confirmingInterval := time.Second * time.Duration(config.Bold.AssertionConfirmingIntervalSeconds) + edgeWakeInterval := time.Second * time.Duration(config.Bold.EdgeTrackerWakeIntervalSeconds) + opts := []challengemanager.Opt{ + challengemanager.WithName(config.Bold.ValidatorName), + challengemanager.WithMode(modes.MakeMode), // TODO: Customize. + challengemanager.WithAssertionPostingInterval(postingInterval), + challengemanager.WithAssertionScanningInterval(scanningInteval), + challengemanager.WithAssertionConfirmingInterval(confirmingInterval), + challengemanager.WithEdgeTrackerWakeInterval(edgeWakeInterval), + challengemanager.WithAddress(txOptsValidator.From), + } + if config.Bold.API { + opts = append(opts, challengemanager.WithAPIEnabled(fmt.Sprintf("%s:%d", config.Bold.APIHost, config.Bold.APIPort), config.Bold.APIDBPath)) + } + manager, err := challengemanager.New( + ctx, + assertionChain, + provider, + assertionChain.RollupAddress(), + opts..., + ) + if err != nil { + return nil, fmt.Errorf("could not create challenge manager: %w", err) + } + provider.UpdateAPIDatabase(manager.Database()) + go manager.Start(ctx) + } + var blockValidator *staker.BlockValidator if config.ValidatorRequired() { blockValidator, err = staker.NewBlockValidator( @@ -611,7 +703,7 @@ func createNodeImpl( confirmedNotifiers = append(confirmedNotifiers, messagePruner) } - stakerObj, err = staker.NewStaker(l1Reader, wallet, bind.CallOpts{}, config.Staker, blockValidator, statelessBlockValidator, nil, confirmedNotifiers, deployInfo.ValidatorUtils, fatalErrChan) + stakerObj, err = staker.NewStaker(l1Reader, wallet, bind.CallOpts{}, config.Staker, blockValidator, statelessBlockValidator, nil, confirmedNotifiers, deployInfo.ValidatorUtils, deployInfo.Bridge, fatalErrChan) if err != nil { return nil, err } diff --git a/arbos/block_processor.go b/arbos/block_processor.go index 4864a31255..7f6c6ea851 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -125,6 +125,27 @@ func NoopSequencingHooks() *SequencingHooks { } } +type ProduceConfig struct { + evil bool + interceptDepositGweiAmount *big.Int +} +type ProduceOpt func(*ProduceConfig) + +func WithEvilProduction() ProduceOpt { + return func(pc *ProduceConfig) { + pc.evil = true + } +} + +func WithInterceptDepositSize(depositGwei *big.Int) ProduceOpt { + return func(pc *ProduceConfig) { + pc.interceptDepositGweiAmount = depositGwei + } +} + +// By default, intercept and modify any Arbitrum deposits with a value of a 1M gwei, or 0.001 ETH. +var DefaultEvilInterceptDepositGweiAmount = big.NewInt(1_000_000 * params.GWei) // 0.001 ETH + func ProduceBlock( message *arbostypes.L1IncomingMessage, delayedMessagesRead uint64, @@ -133,6 +154,7 @@ func ProduceBlock( chainContext core.ChainContext, chainConfig *params.ChainConfig, batchFetcher arbostypes.FallibleBatchFetcher, + opts ...ProduceOpt, ) (*types.Block, types.Receipts, error) { var batchFetchErr error txes, err := ParseL2Transactions(message, chainConfig.ChainID, func(batchNum uint64, batchHash common.Hash) []byte { @@ -156,9 +178,40 @@ func ProduceBlock( txes = types.Transactions{} } + produceCfg := &ProduceConfig{ + interceptDepositGweiAmount: DefaultEvilInterceptDepositGweiAmount, + } + for _, o := range opts { + o(produceCfg) + } + + var modifiedTxs []*types.Transaction + if produceCfg.evil { + modifiedTxs = make([]*types.Transaction, 0, len(txes)) + for _, tx := range txes { + txData, ok := tx.GetInner().(*types.ArbitrumDepositTx) + if !ok { + // We only intercept Arbitrum deposit txs at the moment. + modifiedTxs = append(modifiedTxs, tx) + continue + } + if txData.Value.Cmp(produceCfg.interceptDepositGweiAmount) != 0 { + modifiedTxs = append(modifiedTxs, tx) + continue + } + newValue := new(big.Int).Add(txData.Value, big.NewInt(params.GWei)) + log.Info(fmt.Sprintf("Modified tx value in evil validator with value %d, to value %d as hex %#x and %#x", txData.Value.Uint64(), newValue.Uint64(), txData.Value.Bytes(), newValue.Bytes())) + txData.Value = newValue + modified := types.NewTx(txData) + modifiedTxs = append(modifiedTxs, modified) + } + } else { + modifiedTxs = txes + } + hooks := NoopSequencingHooks() return ProduceBlockAdvanced( - message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, + message.Header, modifiedTxs, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, ) } diff --git a/bold b/bold new file mode 160000 index 0000000000..9634e77979 --- /dev/null +++ b/bold @@ -0,0 +1 @@ +Subproject commit 9634e779790f3529527c911ed17bfdfa77563818 diff --git a/cmd/bold-deploy/main.go b/cmd/bold-deploy/main.go new file mode 100644 index 0000000000..3beac284f5 --- /dev/null +++ b/cmd/bold-deploy/main.go @@ -0,0 +1,279 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + retry "github.com/OffchainLabs/bold/runtime" + "github.com/OffchainLabs/bold/solgen/go/mocksgen" + rollupgen "github.com/OffchainLabs/bold/solgen/go/rollupgen" + challenge_testing "github.com/OffchainLabs/bold/testing" + "github.com/OffchainLabs/bold/testing/setup" + + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/cmd/genericconf" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/headerreader" + "github.com/offchainlabs/nitro/validator/server_common" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/cmd/util" +) + +func main() { + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false))) + glogger.Verbosity(log.LvlDebug) + log.Root().SetHandler(glogger) + log.Info("deploying rollup") + + ctx := context.Background() + + l1conn := flag.String("l1conn", "", "l1 connection") + l1keystore := flag.String("l1keystore", "", "l1 private key store") + l1privatekey := flag.String("l1privatekey", "", "l1 private key") + deployAccount := flag.String("l1DeployAccount", "", "l1 seq account to use (default is first account in keystore)") + ownerAddressString := flag.String("ownerAddress", "", "the rollup owner's address") + sequencerAddressString := flag.String("sequencerAddress", "", "the sequencer's address") + loserEscrowAddressString := flag.String("loserEscrowAddress", "", "the address which half of challenge loser's funds accumulate at") + wasmmoduleroot := flag.String("wasmmoduleroot", "", "WASM module root hash") + wasmrootpath := flag.String("wasmrootpath", "", "path to machine folders") + l1passphrase := flag.String("l1passphrase", "passphrase", "l1 private key file passphrase") + outfile := flag.String("l1deployment", "deploy.json", "deployment output json file") + l1ChainIdUint := flag.Uint64("l1chainid", 1337, "L1 chain ID") + l2ChainConfig := flag.String("l2chainconfig", "l2_chain_config.json", "L2 chain config json file") + l2ChainName := flag.String("l2chainname", "", "L2 chain name (will be included in chain info output json file)") + l2ChainInfo := flag.String("l2chaininfo", "l2_chain_info.json", "L2 chain info output json file") + txTimeout := flag.Duration("txtimeout", 10*time.Minute, "Timeout when waiting for a transaction to be included in a block") + prod := flag.Bool("prod", false, "Whether to configure the rollup for production or testing") + + // Bold specific flags. + numBigSteps := flag.Uint("numBigSteps", 2, "Number of big steps in the rollup") + blockChallengeLeafHeight := flag.Uint64("blockChallengeLeafHeight", 1<<5, "block challenge edge leaf height") + bigStepLeafHeight := flag.Uint64("bigStepLeafHeight", 1<<14, "big step edge leaf height") + smallSteapLeafHeight := flag.Uint64("smallStepLeafHeight", 1<<14, "small step edge leaf height") + minimumAssertionPeriodBlocks := flag.Uint64("minimumAssertionPeriodBlocks", 1, "minimum number of blocks between assertions") + // Half a day of blocks as 12 seconds per block. + confirmPeriodBlocks := flag.Uint64("confirmPeriodBlocks", 3600, "challenge period") + challengeGracePeriodBlocks := flag.Uint64("challengeGracePeriodBlocks", 3, "challenge grace period in which security council can take action") + baseStake := flag.Uint64("baseStake", 1, "base-stake size") + + flag.Parse() + l1ChainId := new(big.Int).SetUint64(*l1ChainIdUint) + + if *prod { + if *wasmmoduleroot == "" { + panic("must specify wasm module root when launching prod chain") + } + } + if *l2ChainName == "" { + panic("must specify l2 chain name") + } + + var l1TransactionOpts *bind.TransactOpts + var err error + if *l1privatekey != "" { + privKey, err := crypto.HexToECDSA(*l1privatekey) + if err != nil { + flag.Usage() + log.Error("error parsing l1 private key") + panic(err) + } + l1TransactionOpts, err = bind.NewKeyedTransactorWithChainID(privKey, l1ChainId) + if err != nil { + flag.Usage() + log.Error("error creating l1 tx opts") + panic(err) + } + } else { + wallet := genericconf.WalletConfig{ + Pathname: *l1keystore, + Account: *deployAccount, + Password: *l1passphrase, + PrivateKey: *l1privatekey, + } + l1TransactionOpts, _, err = util.OpenWallet("l1", &wallet, l1ChainId) + if err != nil { + flag.Usage() + log.Error("error reading keystore") + panic(err) + } + } + + l1client, err := ethclient.Dial(*l1conn) + if err != nil { + flag.Usage() + log.Error("error creating l1client") + panic(err) + } + + if !common.IsHexAddress(*sequencerAddressString) && len(*sequencerAddressString) > 0 { + panic("specified sequencer address is invalid") + } + if !common.IsHexAddress(*ownerAddressString) { + panic("please specify a valid rollup owner address") + } + if *prod && !common.IsHexAddress(*loserEscrowAddressString) { + panic("please specify a valid loser escrow address") + } + + sequencerAddress := common.HexToAddress(*sequencerAddressString) + ownerAddress := common.HexToAddress(*ownerAddressString) + loserEscrowAddress := common.HexToAddress(*loserEscrowAddressString) + if sequencerAddress != (common.Address{}) && ownerAddress != l1TransactionOpts.From { + panic("cannot specify sequencer address if owner is not deployer") + } + + var moduleRoot common.Hash + if *wasmmoduleroot == "" { + locator, err := server_common.NewMachineLocator(*wasmrootpath) + if err != nil { + panic(err) + } + moduleRoot = locator.LatestWasmModuleRoot() + } else { + moduleRoot = common.HexToHash(*wasmmoduleroot) + } + if moduleRoot == (common.Hash{}) { + panic("wasmModuleRoot not found") + } + + headerReaderConfig := headerreader.DefaultConfig + headerReaderConfig.TxTimeout = *txTimeout + + chainConfigJson, err := os.ReadFile(*l2ChainConfig) + if err != nil { + panic(fmt.Errorf("failed to read l2 chain config file: %w", err)) + } + var chainConfig params.ChainConfig + err = json.Unmarshal(chainConfigJson, &chainConfig) + if err != nil { + panic(fmt.Errorf("failed to deserialize chain config: %w", err)) + } + + arbSys, _ := precompilesgen.NewArbSys(types.ArbSysAddress, l1client) + l1Reader, err := headerreader.New(ctx, l1client, func() *headerreader.Config { return &headerReaderConfig }, arbSys) + if err != nil { + panic(fmt.Errorf("failed to create header reader: %w", err)) + } + l1Reader.Start(ctx) + defer l1Reader.StopAndWait() + + stakeToken, _, _, err := mocksgen.DeployTestWETH9( + l1TransactionOpts, + l1Reader.Client(), + "Weth", + "WETH", + ) + if err != nil { + panic(err) + } + genesisExecutionState := rollupgen.ExecutionState{ + GlobalState: rollupgen.GlobalState{}, + MachineStatus: 1, + } + genesisInboxCount := big.NewInt(0) + anyTrustFastConfirmer := common.Address{} + totalLevels := *numBigSteps + 2 + miniStakeValues := make([]*big.Int, totalLevels) + for i := 1; i <= int(totalLevels); i++ { + miniStakeValues[i] = big.NewInt(int64(i)) + } + rollupConfig := challenge_testing.GenerateRollupConfig( + *prod, + moduleRoot, + l1TransactionOpts.From, + chainConfig.ChainID, + loserEscrowAddress, + miniStakeValues, + stakeToken, + genesisExecutionState, + genesisInboxCount, + anyTrustFastConfirmer, + challenge_testing.WithLayerZeroHeights(&protocol.LayerZeroHeights{ + BlockChallengeHeight: *blockChallengeLeafHeight, + BigStepChallengeHeight: *bigStepLeafHeight, + SmallStepChallengeHeight: *smallSteapLeafHeight, + }), + challenge_testing.WithNumBigStepLevels(uint8(*numBigSteps)), + challenge_testing.WithConfirmPeriodBlocks(*confirmPeriodBlocks), + challenge_testing.WithChallengeGracePeriodBlocks(*challengeGracePeriodBlocks), + challenge_testing.WithChainConfig(string(chainConfigJson)), + challenge_testing.WithBaseStakeValue(new(big.Int).SetUint64(*baseStake)), + ) + deployedAddresses, err := setup.DeployFullRollupStack( + ctx, + l1Reader.Client(), + l1TransactionOpts, + l1TransactionOpts.From, + rollupConfig, + false, // do not use mock bridge. + false, // do not use a mock one step prover + ) + if err != nil { + flag.Usage() + log.Error("error deploying on l1") + panic(err) + } + rollup, err := rollupgen.NewRollupAdminLogicTransactor(deployedAddresses.Rollup, l1Reader.Client()) + if err != nil { + panic(err) + } + _, err = retry.UntilSucceeds[*types.Transaction](ctx, func() (*types.Transaction, error) { + return rollup.SetMinimumAssertionPeriod(l1TransactionOpts, big.NewInt(int64(*minimumAssertionPeriodBlocks))) // 1 Ethereum block between assertions + }) + if err != nil { + panic(err) + } + + // We then have the validator itself authorize the rollup and challenge manager + // contracts to spend its stake tokens. + deployData, err := json.Marshal(deployedAddresses) + if err != nil { + panic(err) + } + if err := os.WriteFile(*outfile, deployData, 0600); err != nil { + panic(err) + } + parentChainIsArbitrum := l1Reader.IsParentChainArbitrum() + chainsInfo := []chaininfo.ChainInfo{ + { + ChainName: *l2ChainName, + ParentChainId: l1ChainId.Uint64(), + ParentChainIsArbitrum: &parentChainIsArbitrum, + ChainConfig: &chainConfig, + RollupAddresses: &chaininfo.RollupAddresses{ + Bridge: deployedAddresses.Bridge, + Inbox: deployedAddresses.Inbox, + SequencerInbox: deployedAddresses.SequencerInbox, + Rollup: deployedAddresses.Rollup, + ValidatorUtils: deployedAddresses.ValidatorUtils, + ValidatorWalletCreator: deployedAddresses.ValidatorWalletCreator, + StakeToken: stakeToken, + DeployedAt: deployedAddresses.DeployedAt, + }, + }, + } + chainsInfoJson, err := json.Marshal(chainsInfo) + if err != nil { + panic(err) + } + fmt.Printf("%s\n", chainsInfoJson) + if err := os.WriteFile(*l2ChainInfo, chainsInfoJson, 0600); err != nil { + panic(err) + } +} diff --git a/cmd/chaininfo/chain_info.go b/cmd/chaininfo/chain_info.go index 13e586ced2..b02c83ab7a 100644 --- a/cmd/chaininfo/chain_info.go +++ b/cmd/chaininfo/chain_info.go @@ -120,5 +120,6 @@ type RollupAddresses struct { UpgradeExecutor common.Address `json:"upgrade-executor"` ValidatorUtils common.Address `json:"validator-utils"` ValidatorWalletCreator common.Address `json:"validator-wallet-creator"` + StakeToken common.Address `json:"stake-token"` DeployedAt uint64 `json:"deployed-at"` } diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index f23fbc90de..15dc56dbf4 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -234,11 +234,8 @@ func mainImpl() int { var dataSigner signature.DataSignerFunc var l1TransactionOptsValidator *bind.TransactOpts var l1TransactionOptsBatchPoster *bind.TransactOpts - // If sequencer and signing is enabled or batchposter is enabled without - // external signing sequencer will need a key. - sequencerNeedsKey := (nodeConfig.Node.Sequencer && !nodeConfig.Node.Feed.Output.DisableSigning) || - (nodeConfig.Node.BatchPoster.Enable && nodeConfig.Node.BatchPoster.DataPoster.ExternalSigner.URL == "") - validatorNeedsKey := nodeConfig.Node.Staker.OnlyCreateWalletContract || nodeConfig.Node.Staker.Enable && !strings.EqualFold(nodeConfig.Node.Staker.Strategy, "watchtower") + sequencerNeedsKey := (nodeConfig.Node.Sequencer && !nodeConfig.Node.Feed.Output.DisableSigning) || nodeConfig.Node.BatchPoster.Enable + validatorNeedsKey := nodeConfig.Node.Staker.OnlyCreateWalletContract || nodeConfig.Node.Bold.Enable || nodeConfig.Node.Staker.Enable && !strings.EqualFold(nodeConfig.Node.Staker.Strategy, "watchtower") l1Wallet.ResolveDirectoryNames(nodeConfig.Persistent.Chain) defaultL1WalletConfig := conf.DefaultL1WalletConfig @@ -254,16 +251,29 @@ func mainImpl() int { if nodeConfig.Node.Staker.ParentChainWallet == defaultValidatorL1WalletConfig && nodeConfig.Node.BatchPoster.ParentChainWallet == defaultBatchPosterL1WalletConfig { if sequencerNeedsKey || validatorNeedsKey || l1Wallet.OnlyCreateKey { - l1TransactionOpts, dataSigner, err = util.OpenWallet("l1", l1Wallet, new(big.Int).SetUint64(nodeConfig.ParentChain.ID)) - if err != nil { - flag.Usage() - log.Crit("error opening parent chain wallet", "path", l1Wallet.Pathname, "account", l1Wallet.Account, "err", err) - } - if l1Wallet.OnlyCreateKey { - return 0 + if nodeConfig.Node.BatchPoster.ParentChainWallet.PrivateKey != "" { + privKey, err := crypto.HexToECDSA(nodeConfig.Node.BatchPoster.ParentChainWallet.PrivateKey) + if err != nil { + log.Crit("Failed to parse bold validator private key", "err", err) + } + opts, err := bind.NewKeyedTransactorWithChainID(privKey, new(big.Int).SetUint64(nodeConfig.ParentChain.ID)) + if err != nil { + log.Crit("Failed to create bold validator opts from private key", "err", err) + } + l1TransactionOptsBatchPoster = opts + l1TransactionOptsValidator = opts + } else { + l1TransactionOpts, dataSigner, err = util.OpenWallet("l1", l1Wallet, new(big.Int).SetUint64(nodeConfig.ParentChain.ID)) + if err != nil { + flag.Usage() + log.Crit("error opening parent chain wallet", "path", l1Wallet.Pathname, "account", l1Wallet.Account, "err", err) + } + if l1Wallet.OnlyCreateKey { + return 0 + } + l1TransactionOptsBatchPoster = l1TransactionOpts + l1TransactionOptsValidator = l1TransactionOpts } - l1TransactionOptsBatchPoster = l1TransactionOpts - l1TransactionOptsValidator = l1TransactionOpts } } else { if *l1Wallet != defaultL1WalletConfig { diff --git a/execution/gethexec/block_recorder.go b/execution/gethexec/block_recorder.go index a0f6d837e4..9b004ab8d3 100644 --- a/execution/gethexec/block_recorder.go +++ b/execution/gethexec/block_recorder.go @@ -136,6 +136,11 @@ func (r *BlockRecorder) RecordBlockCreation( // Re-fetch the batch instead of using our cached cost, // as the replay binary won't have the cache populated. msg.Message.BatchGasCost = nil + opts := make([]arbos.ProduceOpt, 0) + if r.execEngine.evil { + opts = append(opts, arbos.WithEvilProduction()) + opts = append(opts, arbos.WithInterceptDepositSize(r.execEngine.interceptDepositGweiAmount)) + } block, _, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, @@ -144,6 +149,7 @@ func (r *BlockRecorder) RecordBlockCreation( chaincontext, chainConfig, batchFetcher, + opts..., ) if err != nil { return nil, err diff --git a/execution/gethexec/executionengine.go b/execution/gethexec/executionengine.go index 58e91a197e..e9fd05fb92 100644 --- a/execution/gethexec/executionengine.go +++ b/execution/gethexec/executionengine.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + "math/big" "sync" "testing" "time" @@ -40,15 +41,36 @@ type ExecutionEngine struct { nextScheduledVersionCheck time.Time // protected by the createBlocksMutex - reorgSequencing bool + reorgSequencing bool + evil bool + interceptDepositGweiAmount *big.Int } -func NewExecutionEngine(bc *core.BlockChain) (*ExecutionEngine, error) { - return &ExecutionEngine{ - bc: bc, - resequenceChan: make(chan []*arbostypes.MessageWithMetadata), - newBlockNotifier: make(chan struct{}, 1), - }, nil +type Opt func(*ExecutionEngine) + +func WithEvilExecution() Opt { + return func(exec *ExecutionEngine) { + exec.evil = true + } +} + +func WithInterceptDepositSize(depositGwei *big.Int) Opt { + return func(exec *ExecutionEngine) { + exec.interceptDepositGweiAmount = depositGwei + } +} + +func NewExecutionEngine(bc *core.BlockChain, opts ...Opt) (*ExecutionEngine, error) { + exec := &ExecutionEngine{ + bc: bc, + resequenceChan: make(chan []*arbostypes.MessageWithMetadata), + newBlockNotifier: make(chan struct{}, 1), + interceptDepositGweiAmount: arbos.DefaultEvilInterceptDepositGweiAmount, + } + for _, o := range opts { + o(exec) + } + return exec, nil } func (s *ExecutionEngine) SetRecorder(recorder *BlockRecorder) { @@ -442,6 +464,11 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith statedb.StartPrefetcher("TransactionStreamer") defer statedb.StopPrefetcher() + opts := make([]arbos.ProduceOpt, 0) + if s.evil { + opts = append(opts, arbos.WithEvilProduction()) + opts = append(opts, arbos.WithInterceptDepositSize(s.interceptDepositGweiAmount)) + } block, receipts, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, @@ -450,6 +477,7 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith s.bc, s.bc.Config(), s.streamer.FetchBatch, + opts..., ) return block, statedb, receipts, err diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index 7dd6e301fe..a4ddafcbb6 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/big" "reflect" "sync/atomic" "testing" @@ -17,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" @@ -39,6 +41,8 @@ func DangerousConfigAddOptions(prefix string, f *flag.FlagSet) { } type Config struct { + Evil bool `koanf:"evil"` + EvilInterceptDepositGwei uint64 `koanf:"evil-intercept-deposit-gwei"` ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` RecordingDatabase arbitrum.RecordingDatabaseConfig `koanf:"recording-database"` @@ -98,6 +102,7 @@ var ConfigDefault = Config{ Caching: DefaultCachingConfig, Dangerous: DefaultDangerousConfig, Forwarder: DefaultNodeForwarderConfig, + EvilInterceptDepositGwei: 1_000_000, // 1M gwei or 0.001 ETH. } func ConfigDefaultNonSequencerTest() *Config { @@ -148,7 +153,12 @@ func CreateExecutionNode( configFetcher ConfigFetcher, ) (*ExecutionNode, error) { config := configFetcher() - execEngine, err := NewExecutionEngine(l2BlockChain) + opts := make([]Opt, 0) + if config.Evil { + opts = append(opts, WithEvilExecution()) + opts = append(opts, WithInterceptDepositSize(new(big.Int).SetUint64(config.EvilInterceptDepositGwei*params.GWei))) + } + execEngine, err := NewExecutionEngine(l2BlockChain, opts...) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 598c48e920..ac44d5bd65 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,10 @@ replace github.com/VictoriaMetrics/fastcache => ./fastcache replace github.com/ethereum/go-ethereum => ./go-ethereum +replace github.com/OffchainLabs/bold => ./bold + require ( + github.com/OffchainLabs/bold v0.0.0-00010101000000-000000000000 github.com/Shopify/toxiproxy v2.1.4+incompatible github.com/alicebob/miniredis/v2 v2.21.0 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 @@ -21,7 +24,7 @@ require ( github.com/codeclysm/extract/v3 v3.0.2 github.com/dgraph-io/badger/v3 v3.2103.2 github.com/enescakir/emoji v1.0.0 - github.com/ethereum/go-ethereum v1.10.26 + github.com/ethereum/go-ethereum v1.12.0 github.com/fatih/structtag v1.2.0 github.com/gdamore/tcell/v2 v2.6.0 github.com/google/go-cmp v0.5.9 @@ -72,7 +75,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.7.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect @@ -181,6 +184,7 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 // indirect github.com/klauspost/compress v1.16.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect @@ -208,6 +212,7 @@ require ( github.com/libp2p/zeroconf/v2 v2.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.53 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect @@ -244,11 +249,12 @@ require ( github.com/quic-go/webtransport-go v0.5.2 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rhnvrm/simples3 v0.6.1 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/samber/lo v1.36.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/urfave/cli/v2 v2.24.1 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect @@ -260,6 +266,7 @@ require ( github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.7.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.7.0 // indirect @@ -306,20 +313,19 @@ require ( ) require ( - github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect - github.com/VictoriaMetrics/fastcache v1.6.0 // indirect + github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect - github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-redis/redis/v8 v8.11.4 github.com/go-stack/stack v1.8.1 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/uuid v1.3.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect - github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -329,11 +335,11 @@ require ( github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rs/cors v1.7.0 // indirect - github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/tklauser/go-sysconf v0.3.5 // indirect - github.com/tklauser/numcpus v0.2.2 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) diff --git a/go.sum b/go.sum index 1fb8a405f0..c54b1936cc 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,6 @@ github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fT github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/Stebalien/go-bitfield v0.0.1/go.mod h1:GNjFpasyUVkHMsfEOk8EFLJ9syQ6SI+XWrX9Wf2XH0s= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -170,8 +168,8 @@ github.com/btcsuite/btcd v0.0.0-20190605094302-a0d1e3e36d50/go.mod h1:3J08xEfcug github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= @@ -400,8 +398,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -414,6 +412,8 @@ github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Px github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= @@ -614,8 +614,8 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -879,6 +879,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -958,6 +960,8 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ= github.com/libp2p/go-addr-util v0.0.2/go.mod h1:Ecd6Fb3yIuLzq4bD7VcywcVSBtefcAwnUISBM3WG15E= github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ= @@ -1208,6 +1212,9 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -1485,8 +1492,9 @@ github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8d github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 h1:ZyM/+FYnpbZsFWuCohniM56kRoHRB4r5EuIzXEYkpxo= github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1510,8 +1518,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -1589,7 +1597,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= @@ -1597,10 +1606,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= -github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= -github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= -github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= -github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= @@ -1683,6 +1692,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= @@ -1947,6 +1958,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1986,7 +1998,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2006,6 +2017,7 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= diff --git a/nitro-testnode b/nitro-testnode index aee6ceff9c..013f299c71 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit aee6ceff9c9d3fb2749da55a7d7842f23d1bfc8e +Subproject commit 013f299c71de6dbbcc30f01aecdc17e1effb16be diff --git a/staker/block_validator.go b/staker/block_validator.go index 108d6d1d49..0d07402ca0 100644 --- a/staker/block_validator.go +++ b/staker/block_validator.go @@ -78,6 +78,8 @@ type BlockValidator struct { type BlockValidatorConfig struct { Enable bool `koanf:"enable"` + Evil bool `koanf:"evil"` + EvilInterceptDepositGwei uint64 `koanf:"evil-intercept-deposit-gwei"` ValidationServer rpcclient.ClientConfig `koanf:"validation-server" reload:"hot"` ValidationPoll time.Duration `koanf:"validation-poll" reload:"hot"` PrerecordedBlocks uint64 `koanf:"prerecorded-blocks" reload:"hot"` @@ -124,6 +126,7 @@ var DefaultBlockValidatorConfig = BlockValidatorConfig{ PendingUpgradeModuleRoot: "latest", FailureIsFatal: true, Dangerous: DefaultBlockValidatorDangerousConfig, + EvilInterceptDepositGwei: 1_000_000, // 1M gwei or 0.001 ETH } var TestBlockValidatorConfig = BlockValidatorConfig{ @@ -136,6 +139,7 @@ var TestBlockValidatorConfig = BlockValidatorConfig{ PendingUpgradeModuleRoot: "latest", FailureIsFatal: true, Dangerous: DefaultBlockValidatorDangerousConfig, + EvilInterceptDepositGwei: 1_000_000, // 1M gwei or 0.001 ETH } var DefaultBlockValidatorDangerousConfig = BlockValidatorDangerousConfig{ diff --git a/staker/challenge-cache/cache.go b/staker/challenge-cache/cache.go new file mode 100644 index 0000000000..62123e0e8d --- /dev/null +++ b/staker/challenge-cache/cache.go @@ -0,0 +1,275 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE +/* +* Package challengecache stores validator state roots for L2 states within +challenges in text files using a directory hierarchy structure for efficient lookup. Each file +contains a list of state roots (32 byte hashes), concatenated together as bytes. +Using this structure, we can namespace state roots by message number and big step challenge. + +Once a validator computes the set of machine state roots for a given challenge move the first time, +it will write the roots to this filesystem hierarchy for fast access next time these roots are needed. + +Use cases: +- State roots for a big step challenge from message N to N+1 +- State roots 0 to M for a big step challenge from message N to N+1 +- State roots for a small step challenge from message N to N+1, and big step M to M+1 +- State roots 0 to P for a small step challenge from message N to N+1, and big step M to M+1 + + wavm-module-root-0xab/ + message-num-70/ + roots.txt + subchallenge-level-0-big-step-100/ + roots.txt + subchallenge-level-1-big-step-100/ + roots.txt + +We namespace top-level block challenges by wavm module root. Then, we can retrieve +the state roots for any data within a challenge or associated subchallenge based on the hierarchy above. +*/ + +package challengecache + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +var ( + ErrNotFoundInCache = errors.New("no found in challenge cache") + ErrFileAlreadyExists = errors.New("file already exists") + ErrNoStateRoots = errors.New("no state roots being written") + stateRootsFileName = "state-roots" + wavmModuleRootPrefix = "wavm-module-root" + messageNumberPrefix = "message-num" + bigStepPrefix = "big-step" + challengeLevelPrefix = "subchallenge-level" + srvlog = log.New("service", "bold-history-commit-cache") +) + +// HistoryCommitmentCacher can retrieve history commitment state roots given lookup keys. +type HistoryCommitmentCacher interface { + Get(lookup *Key, numToRead uint64) ([]common.Hash, error) + Put(lookup *Key, stateRoots []common.Hash) error +} + +// Cache for history commitments on disk. +type Cache struct { + baseDir string +} + +// New cache from a base directory path. +func New(baseDir string) *Cache { + return &Cache{ + baseDir: baseDir, + } +} + +// Key for cache lookups includes the wavm module root of a challenge, as well +// as the heights for messages and big steps as needed. +type Key struct { + WavmModuleRoot common.Hash + MessageHeight protocol.Height + StepHeights []l2stateprovider.Height +} + +// Get a list of state roots from the cache up to a certain index. State roots are saved as files in the directory +// hierarchy for the cache. If a file is not present, ErrNotFoundInCache +// is returned. +func (c *Cache) Get( + lookup *Key, + numToRead uint64, +) ([]common.Hash, error) { + fName, err := determineFilePath(c.baseDir, lookup) + if err != nil { + return nil, err + } + if _, err := os.Stat(fName); err != nil { + srvlog.Warn("Cache miss", log.Ctx{"fileName": fName}) + return nil, ErrNotFoundInCache + } + srvlog.Debug("Cache hit", log.Ctx{"fileName": fName}) + f, err := os.Open(fName) + if err != nil { + return nil, err + } + defer func() { + if err := f.Close(); err != nil { + log.Error("Could not close file after reading", "err", err, "file", fName) + } + }() + return readStateRoots(f, numToRead) +} + +// Put a list of state roots into the cache. +// State roots are saved as files in a directory hierarchy for the cache. +// This function first creates a temporary file, writes the state roots to it, and then renames the file +// to the final directory to ensure atomic writes. +func (c *Cache) Put(lookup *Key, stateRoots []common.Hash) error { + // We should error if trying to put 0 state roots to disk. + if len(stateRoots) == 0 { + return ErrNoStateRoots + } + fName, err := determineFilePath(c.baseDir, lookup) + if err != nil { + return err + } + // We create a tmp file to write our state roots to first. If writing fails, + // we don't want to leave a half-written file in our cache directory. + // Once writing succeeds, we rename in an atomic operation to the correct file name + // in the cache directory hierarchy. + tmp := os.TempDir() + tmpFName := filepath.Join(tmp, fName) + dir := filepath.Dir(tmpFName) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("could not make tmp directory %s: %w", dir, err) + } + f, err := os.Create(tmpFName) + if err != nil { + return err + } + defer func() { + if err := f.Close(); err != nil { + log.Error("Could not close file after writing", "err", err, "file", fName) + } + }() + if err := writeStateRoots(f, stateRoots); err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(fName), os.ModePerm); err != nil { + return fmt.Errorf("could not make file directory %s: %w", fName, err) + } + // If the file writing was successful, we rename the file from the tmp directory + // into our cache directory. This is an atomic operation. + // For more information on this atomic write pattern, see: + // https://stackoverflow.com/questions/2333872/how-to-make-file-creation-an-atomic-operation + return Move(tmpFName /* old */, fName /* new */) +} + +// Reads 32 bytes at a time from a reader up to a specified height. If none, then read all. +func readStateRoots(r io.Reader, numToRead uint64) ([]common.Hash, error) { + br := bufio.NewReader(r) + stateRoots := make([]common.Hash, 0) + buf := make([]byte, 0, 32) + for totalRead := uint64(0); totalRead < numToRead; totalRead++ { + n, err := br.Read(buf[:cap(buf)]) + if err != nil { + // If we try to read but reach EOF, we break out of the loop. + if err == io.EOF { + break + } + return nil, err + } + buf = buf[:n] + if n != 32 { + return nil, fmt.Errorf("expected to read 32 bytes, got %d bytes", n) + } + stateRoots = append(stateRoots, common.BytesToHash(buf)) + } + if protocol.Height(numToRead) > protocol.Height(len(stateRoots)) { + return nil, fmt.Errorf( + "wanted to read %d roots, but only read %d state roots", + numToRead, + len(stateRoots), + ) + } + return stateRoots, nil +} + +func writeStateRoots(w io.Writer, stateRoots []common.Hash) error { + for i, rt := range stateRoots { + n, err := w.Write(rt[:]) + if err != nil { + return err + } + if n != len(rt) { + return fmt.Errorf( + "for state root %d, wrote %d bytes, expected to write %d bytes", + i, + n, + len(rt), + ) + } + } + return nil +} + +/* +* +When provided with a cache lookup struct, this function determines the file path +for the data requested within the cache directory hierarchy. The folder structure +for a given filesystem challenge cache will look as follows: + + wavm-module-root-0xab/ + message-num-70/ + roots.txt + subchallenge-level-0-big-step-100/ + roots.txt +*/ +func determineFilePath(baseDir string, lookup *Key) (string, error) { + key := make([]string, 0) + key = append(key, fmt.Sprintf("%s-%s", wavmModuleRootPrefix, lookup.WavmModuleRoot.Hex())) + key = append(key, fmt.Sprintf("%s-%d", messageNumberPrefix, lookup.MessageHeight)) + for challengeLevel, height := range lookup.StepHeights { + key = append(key, fmt.Sprintf( + "%s-%d-%s-%d", + challengeLevelPrefix, + challengeLevel+1, // subchallenges start at 1, as level 0 is the block challenge level. + bigStepPrefix, + height, + ), + ) + + } + key = append(key, stateRootsFileName) + return filepath.Join(baseDir, filepath.Join(key...)), nil +} + +// Move function that is robust against cross-device link errors. Credits to: +// https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b +func Move(source, destination string) error { + err := os.Rename(source, destination) + if err != nil && strings.Contains(err.Error(), "cross-device link") { + return moveCrossDevice(source, destination) + } + return err +} + +func moveCrossDevice(source, destination string) error { + src, err := os.Open(source) + if err != nil { + return err + } + dst, err := os.Create(destination) + if err != nil { + src.Close() + return err + } + _, err = io.Copy(dst, src) + src.Close() + dst.Close() + if err != nil { + return err + } + fi, err := os.Stat(source) + if err != nil { + os.Remove(destination) + return err + } + err = os.Chmod(destination, fi.Mode()) + if err != nil { + os.Remove(destination) + return err + } + os.Remove(source) + return nil +} diff --git a/staker/challenge-cache/cache_test.go b/staker/challenge-cache/cache_test.go new file mode 100644 index 0000000000..53b8bf85c8 --- /dev/null +++ b/staker/challenge-cache/cache_test.go @@ -0,0 +1,318 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE +package challengecache + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "strings" + "testing" + + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/ethereum/go-ethereum/common" +) + +var _ HistoryCommitmentCacher = (*Cache)(nil) + +func TestCache(t *testing.T) { + basePath := t.TempDir() + if err := os.MkdirAll(basePath, os.ModePerm); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(basePath); err != nil { + t.Fatal(err) + } + }) + cache := New(basePath) + key := &Key{ + WavmModuleRoot: common.BytesToHash([]byte("foo")), + MessageHeight: 0, + StepHeights: []l2stateprovider.Height{l2stateprovider.Height(0)}, + } + t.Run("Not found", func(t *testing.T) { + _, err := cache.Get(key, 0) + if !errors.Is(err, ErrNotFoundInCache) { + t.Fatal(err) + } + }) + t.Run("Putting empty root fails", func(t *testing.T) { + if err := cache.Put(key, []common.Hash{}); !errors.Is(err, ErrNoStateRoots) { + t.Fatalf("Unexpected error: %v", err) + } + }) + want := []common.Hash{ + common.BytesToHash([]byte("foo")), + common.BytesToHash([]byte("bar")), + common.BytesToHash([]byte("baz")), + } + err := cache.Put(key, want) + if err != nil { + t.Fatal(err) + } + got, err := cache.Get(key, 3) + if err != nil { + t.Fatal(err) + } + if len(got) != len(want) { + t.Fatalf("Wrong number of roots. Expected %d, got %d", len(want), len(got)) + } + for i, rt := range got { + if rt != want[i] { + t.Fatalf("Wrong root. Expected %#x, got %#x", want[i], rt) + } + } +} + +func TestReadWriteStateRoots(t *testing.T) { + t.Run("read up to, but had empty reader", func(t *testing.T) { + b := bytes.NewBuffer([]byte{}) + _, err := readStateRoots(b, 100) + if err == nil { + t.Fatal("Wanted error") + } + if !strings.Contains(err.Error(), "only read 0 state roots") { + t.Fatal("Unexpected error") + } + }) + t.Run("read single root", func(t *testing.T) { + b := bytes.NewBuffer([]byte{}) + want := common.BytesToHash([]byte("foo")) + b.Write(want.Bytes()) + roots, err := readStateRoots(b, 1) + if err != nil { + t.Fatal(err) + } + if len(roots) == 0 { + t.Fatal("Got no roots") + } + if roots[0] != want { + t.Fatalf("Wrong root. Expected %#x, got %#x", want, roots[0]) + } + }) + t.Run("Three roots exist, want to read only two", func(t *testing.T) { + b := bytes.NewBuffer([]byte{}) + foo := common.BytesToHash([]byte("foo")) + bar := common.BytesToHash([]byte("bar")) + baz := common.BytesToHash([]byte("baz")) + b.Write(foo.Bytes()) + b.Write(bar.Bytes()) + b.Write(baz.Bytes()) + roots, err := readStateRoots(b, 2) + if err != nil { + t.Fatal(err) + } + if len(roots) != 2 { + t.Fatalf("Expected two roots, got %d", len(roots)) + } + if roots[0] != foo { + t.Fatalf("Wrong root. Expected %#x, got %#x", foo, roots[0]) + } + if roots[1] != bar { + t.Fatalf("Wrong root. Expected %#x, got %#x", bar, roots[1]) + } + }) + t.Run("Fails to write enough data to writer", func(t *testing.T) { + m := &mockWriter{wantErr: true} + err := writeStateRoots(m, []common.Hash{common.BytesToHash([]byte("foo"))}) + if err == nil { + t.Fatal("Wanted error") + } + m = &mockWriter{wantErr: false, numWritten: 16} + err = writeStateRoots(m, []common.Hash{common.BytesToHash([]byte("foo"))}) + if err == nil { + t.Fatal("Wanted error") + } + if !strings.Contains(err.Error(), "expected to write 32 bytes") { + t.Fatalf("Got wrong error kind: %v", err) + } + }) +} + +type mockWriter struct { + wantErr bool + numWritten int +} + +func (m *mockWriter) Write(_ []byte) (n int, err error) { + if m.wantErr { + return 0, errors.New("something went wrong") + } + return m.numWritten, nil +} + +type mockReader struct { + wantErr bool + err error + roots []common.Hash + readIdx int + bytesRead int +} + +func (m *mockReader) Read(out []byte) (n int, err error) { + if m.wantErr { + return 0, m.err + } + if m.readIdx == len(m.roots) { + return 0, io.EOF + } + copy(out, m.roots[m.readIdx].Bytes()) + m.readIdx++ + return m.bytesRead, nil +} + +func Test_readStateRoots(t *testing.T) { + t.Run("Unexpected error", func(t *testing.T) { + want := []common.Hash{ + common.BytesToHash([]byte("foo")), + common.BytesToHash([]byte("bar")), + common.BytesToHash([]byte("baz")), + } + m := &mockReader{wantErr: true, roots: want, err: errors.New("foo")} + _, err := readStateRoots(m, 1) + if err == nil { + t.Fatal(err) + } + if !strings.Contains(err.Error(), "foo") { + t.Fatalf("Unexpected error: %v", err) + } + }) + t.Run("EOF, but did not read as much as was expected", func(t *testing.T) { + want := []common.Hash{ + common.BytesToHash([]byte("foo")), + common.BytesToHash([]byte("bar")), + common.BytesToHash([]byte("baz")), + } + m := &mockReader{wantErr: true, roots: want, err: io.EOF} + _, err := readStateRoots(m, 100) + if err == nil { + t.Fatal(err) + } + if !strings.Contains(err.Error(), "wanted to read 100") { + t.Fatalf("Unexpected error: %v", err) + } + }) + t.Run("Reads wrong number of bytes", func(t *testing.T) { + want := []common.Hash{ + common.BytesToHash([]byte("foo")), + common.BytesToHash([]byte("bar")), + common.BytesToHash([]byte("baz")), + } + m := &mockReader{wantErr: false, roots: want, bytesRead: 16} + _, err := readStateRoots(m, 2) + if err == nil { + t.Fatal(err) + } + if !strings.Contains(err.Error(), "expected to read 32 bytes, got 16") { + t.Fatalf("Unexpected error: %v", err) + } + }) + t.Run("Reads all until EOF", func(t *testing.T) { + want := []common.Hash{ + common.BytesToHash([]byte("foo")), + common.BytesToHash([]byte("bar")), + common.BytesToHash([]byte("baz")), + } + m := &mockReader{wantErr: false, roots: want, bytesRead: 32} + got, err := readStateRoots(m, 3) + if err != nil { + t.Fatal(err) + } + if len(want) != len(got) { + t.Fatal("Wrong number of roots") + } + for i, rt := range got { + if rt != want[i] { + t.Fatal("Wrong root") + } + } + }) +} + +func Test_determineFilePath(t *testing.T) { + type args struct { + baseDir string + key *Key + } + tests := []struct { + name string + args args + want string + wantErr bool + errContains string + }{ + { + name: "OK", + args: args{ + baseDir: "", + key: &Key{ + MessageHeight: 100, + StepHeights: []l2stateprovider.Height{l2stateprovider.Height(50)}, + }, + }, + want: "wavm-module-root-0x0000000000000000000000000000000000000000000000000000000000000000/message-num-100/subchallenge-level-1-big-step-50/state-roots", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := determineFilePath(tt.args.baseDir, tt.args.key) + if (err != nil) != tt.wantErr { + t.Logf("got: %v, and key %+v, got %s", err, tt.args.key, got) + if !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("Expected %s, got %s", tt.errContains, err.Error()) + } + t.Errorf("determineFilePath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf( + "determineFilePath() = %v, want %v", + got, + tt.want, + ) + } + }) + } +} + +func BenchmarkCache_Read_32Mb(b *testing.B) { + b.StopTimer() + basePath := os.TempDir() + if err := os.MkdirAll(basePath, os.ModePerm); err != nil { + b.Fatal(err) + } + b.Cleanup(func() { + if err := os.RemoveAll(basePath); err != nil { + b.Fatal(err) + } + }) + cache := New(basePath) + key := &Key{ + WavmModuleRoot: common.BytesToHash([]byte("foo")), + MessageHeight: 0, + StepHeights: []l2stateprovider.Height{l2stateprovider.Height(0)}, + } + numRoots := 1 << 20 + roots := make([]common.Hash, numRoots) + for i := range roots { + roots[i] = common.BytesToHash([]byte(fmt.Sprintf("%d", i))) + } + if err := cache.Put(key, roots); err != nil { + b.Fatal(err) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + readUpTo := uint64(1 << 20) + roots, err := cache.Get(key, readUpTo) + if err != nil { + b.Fatal(err) + } + if len(roots) != numRoots { + b.Fatalf("Wrong number of roots. Expected %d, got %d", numRoots, len(roots)) + } + } +} diff --git a/staker/manager.go b/staker/manager.go new file mode 100644 index 0000000000..725b18e610 --- /dev/null +++ b/staker/manager.go @@ -0,0 +1,113 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE +package staker + +import ( + "context" + "time" + + solimpl "github.com/OffchainLabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/OffchainLabs/bold/challenge-manager" + "github.com/OffchainLabs/bold/challenge-manager/types" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/OffchainLabs/bold/solgen/go/challengeV2gen" + "github.com/OffchainLabs/bold/solgen/go/rollupgen" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/arbutil" +) + +var BoldModes = map[string]types.Mode{ + "watchtower-mode": types.WatchTowerMode, + "resolve-mode": types.ResolveMode, + "defensive-mode": types.DefensiveMode, + "make-mode": types.MakeMode, +} + +func NewManager( + ctx context.Context, + rollupAddress common.Address, + txOpts *bind.TransactOpts, + callOpts bind.CallOpts, + client arbutil.L1Interface, + statelessBlockValidator *StatelessBlockValidator, + config *BoldConfig, +) (*challengemanager.Manager, error) { + userLogic, err := rollupgen.NewRollupUserLogic( + rollupAddress, client, + ) + if err != nil { + return nil, err + } + challengeManagerAddr, err := userLogic.RollupUserLogicCaller.ChallengeManager( + &bind.CallOpts{Context: ctx}, + ) + if err != nil { + return nil, err + } + chain, err := solimpl.NewAssertionChain( + ctx, + rollupAddress, + challengeManagerAddr, + txOpts, + client, + solimpl.NewChainBackendTransactor(client), + ) + if err != nil { + return nil, err + } + managerBinding, err := challengeV2gen.NewEdgeChallengeManager(challengeManagerAddr, client) + if err != nil { + return nil, err + } + numBigStepLevel, err := managerBinding.NUMBIGSTEPLEVEL(&callOpts) + if err != nil { + return nil, err + } + challengeLeafHeights := make([]l2stateprovider.Height, numBigStepLevel+2) + for i := uint8(0); i <= numBigStepLevel+1; i++ { + leafHeight, err := managerBinding.GetLayerZeroEndHeight(&callOpts, i) + if err != nil { + return nil, err + } + challengeLeafHeights[i] = l2stateprovider.Height(leafHeight.Uint64()) + } + + stateManager, err := NewStateManager( + statelessBlockValidator, + config.MachineLeavesCachePath, + challengeLeafHeights, + config.ValidatorName, + ) + if err != nil { + return nil, err + } + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + challengeLeafHeights, + stateManager, + nil, + ) + manager, err := challengemanager.New( + ctx, + chain, + provider, + rollupAddress, + challengemanager.WithName(config.ValidatorName), + challengemanager.WithMode(BoldModes[config.Mode]), + challengemanager.WithAssertionPostingInterval(time.Duration(config.AssertionPostingIntervalSeconds)), + challengemanager.WithAssertionScanningInterval(time.Duration(config.AssertionScanningIntervalSeconds)), + challengemanager.WithAssertionConfirmingInterval(time.Duration(config.AssertionConfirmingIntervalSeconds)), + challengemanager.WithEdgeTrackerWakeInterval(time.Duration(config.EdgeTrackerWakeIntervalSeconds)), + challengemanager.WithAddress(txOpts.From), + ) + if err != nil { + return nil, err + } + provider.UpdateAPIDatabase(manager.Database()) + return manager, nil +} diff --git a/staker/staker.go b/staker/staker.go index 4f35c1bc9a..4cda149455 100644 --- a/staker/staker.go +++ b/staker/staker.go @@ -24,6 +24,8 @@ import ( "github.com/offchainlabs/nitro/arbnode/redislock" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/cmd/genericconf" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/staker/txbuilder" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/headerreader" @@ -74,6 +76,7 @@ func L1PostingStrategyAddOptions(prefix string, f *flag.FlagSet) { type L1ValidatorConfig struct { Enable bool `koanf:"enable"` + Bold BoldConfig `koanf:"bold"` Strategy string `koanf:"strategy"` StakerInterval time.Duration `koanf:"staker-interval"` MakeAssertionInterval time.Duration `koanf:"make-assertion-interval"` @@ -141,6 +144,7 @@ func (c *L1ValidatorConfig) Validate() error { var DefaultL1ValidatorConfig = L1ValidatorConfig{ Enable: true, + Bold: DefaultBoldConfig, Strategy: "Watchtower", StakerInterval: time.Minute, MakeAssertionInterval: time.Hour, @@ -253,6 +257,7 @@ type Staker struct { bringActiveUntilNode uint64 inboxReader InboxReaderInterface statelessBlockValidator *StatelessBlockValidator + bridge *bridgegen.IBridge fatalErr chan<- error } @@ -287,6 +292,7 @@ func NewStaker( stakedNotifiers []LatestStakedNotifier, confirmedNotifiers []LatestConfirmedNotifier, validatorUtilsAddress common.Address, + bridgeAddress common.Address, fatalErr chan<- error, ) (*Staker, error) { @@ -303,6 +309,13 @@ func NewStaker( if config.StartValidationFromStaked && blockValidator != nil { stakedNotifiers = append(stakedNotifiers, blockValidator) } + var bridge *bridgegen.IBridge + if config.Bold.Enable { + bridge, err = bridgegen.NewIBridge(bridgeAddress, client) + if err != nil { + return nil, err + } + } return &Staker{ L1Validator: val, l1Reader: l1Reader, @@ -314,6 +327,7 @@ func NewStaker( lastActCalledBlock: nil, inboxReader: statelessBlockValidator.inboxReader, statelessBlockValidator: statelessBlockValidator, + bridge: bridge, fatalErr: fatalErr, }, nil } @@ -428,6 +442,13 @@ func (s *Staker) Start(ctxIn context.Context) { if err != nil { log.Warn("error updating latest wasm module root", "err", err) } + switchedToBoldProtocol, err := s.checkAndSwitchToBoldStaker(ctxIn) + if err != nil { + log.Error("staker: error in checking switch to bold staker", "err", err) + } + if switchedToBoldProtocol { + s.StopAndWait() + } arbTx, err := s.Act(ctx) if err == nil && arbTx != nil { _, err = s.l1Reader.WaitForTxApproval(ctx, arbTx) @@ -486,6 +507,48 @@ func (s *Staker) Start(ctxIn context.Context) { } return s.config.StakerInterval }) + s.CallIteratively(func(ctx context.Context) time.Duration { + // Using ctxIn instead of ctx since, ctxIn will be passed on to bold staker + // and ctx will be cancelled after the switch to bold staker. + switchedToBoldProtocol, err := s.checkAndSwitchToBoldStaker(ctxIn) + if err != nil { + log.Error("staker: error in checking switch to bold staker", "err", err) + } + if switchedToBoldProtocol { + s.StopAndWait() + } + return s.config.StakerInterval + }) +} + +func (s *Staker) checkAndSwitchToBoldStaker(ctx context.Context) (bool, error) { + switchedToBoldProtocol := false + if s.config.Bold.Enable { + callOpts := s.getCallOpts(ctx) + rollupAddress, err := s.bridge.Rollup(callOpts) + if err != nil { + return false, err + } + userLogic, err := rollupgen.NewRollupUserLogic(rollupAddress, s.client) + if err != nil { + return false, err + } + _, err = userLogic.ExtraChallengeTimeBlocks(callOpts) + if err != nil { + // Switch to Bold protocol since ExtraChallengeTimeBlocks does not exist in bold protocol. + auth, err := s.builder.Auth(ctx) + if err != nil { + return false, err + } + boldManager, err := NewManager(ctx, rollupAddress, auth, *callOpts, s.client, s.statelessBlockValidator, &s.config.Bold) + if err != nil { + return false, err + } + boldManager.Start(ctx) + switchedToBoldProtocol = true + } + } + return switchedToBoldProtocol, nil } func (s *Staker) IsWhitelisted(ctx context.Context) (bool, error) { diff --git a/staker/state_provider.go b/staker/state_provider.go new file mode 100644 index 0000000000..e97b35a882 --- /dev/null +++ b/staker/state_provider.go @@ -0,0 +1,436 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE +package staker + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/containers/option" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + + "github.com/offchainlabs/nitro/arbutil" + challengecache "github.com/offchainlabs/nitro/staker/challenge-cache" + "github.com/offchainlabs/nitro/validator" +) + +var ( + _ l2stateprovider.ProofCollector = (*StateManager)(nil) + _ l2stateprovider.L2MessageStateCollector = (*StateManager)(nil) + _ l2stateprovider.MachineHashCollector = (*StateManager)(nil) + _ l2stateprovider.ExecutionProvider = (*StateManager)(nil) +) + +var ( + ErrChainCatchingUp = errors.New("chain catching up") +) + +type BoldConfig struct { + Enable bool `koanf:"enable"` + Evil bool `koanf:"evil"` + Mode string `koanf:"mode"` + BlockChallengeLeafHeight uint64 `koanf:"block-challenge-leaf-height"` + BigStepLeafHeight uint64 `koanf:"big-step-leaf-height"` + SmallStepLeafHeight uint64 `koanf:"small-step-leaf-height"` + NumBigSteps uint64 `koanf:"num-big-steps"` + ValidatorName string `koanf:"validator-name"` + MachineLeavesCachePath string `koanf:"machine-leaves-cache-path"` + AssertionPostingIntervalSeconds uint64 `koanf:"assertion-posting-interval-seconds"` + AssertionScanningIntervalSeconds uint64 `koanf:"assertion-scanning-interval-seconds"` + AssertionConfirmingIntervalSeconds uint64 `koanf:"assertion-confirming-interval-seconds"` + EdgeTrackerWakeIntervalSeconds uint64 `koanf:"edge-tracker-wake-interval-seconds"` + API bool `koanf:"api"` + APIHost string `koanf:"api-host"` + APIPort uint16 `koanf:"api-port"` + APIDBPath string `koanf:"api-db-path"` +} + +var DefaultBoldConfig = BoldConfig{ + Enable: false, + Evil: false, + Mode: "make-mode", + BlockChallengeLeafHeight: 1 << 5, + BigStepLeafHeight: 1 << 8, + SmallStepLeafHeight: 1 << 10, + NumBigSteps: 3, + ValidatorName: "default-validator", + MachineLeavesCachePath: "/tmp/machine-leaves-cache", + AssertionPostingIntervalSeconds: 30, + AssertionScanningIntervalSeconds: 30, + AssertionConfirmingIntervalSeconds: 60, + EdgeTrackerWakeIntervalSeconds: 1, +} + +func (c *BoldConfig) Validate() error { + return nil +} + +type StateManager struct { + validator *StatelessBlockValidator + historyCache challengecache.HistoryCommitmentCacher + challengeLeafHeights []l2stateprovider.Height + validatorName string + sync.RWMutex +} + +func NewStateManager( + val *StatelessBlockValidator, + cacheBaseDir string, + challengeLeafHeights []l2stateprovider.Height, + validatorName string, +) (*StateManager, error) { + historyCache := challengecache.New(cacheBaseDir) + sm := &StateManager{ + validator: val, + historyCache: historyCache, + challengeLeafHeights: challengeLeafHeights, + validatorName: validatorName, + } + return sm, nil +} + +// AgreesWithExecutionState If the state manager locally has this validated execution state. +// Returns ErrNoExecutionState if not found, or ErrChainCatchingUp if not yet +// validated / syncing. +func (s *StateManager) AgreesWithExecutionState(ctx context.Context, state *protocol.ExecutionState) error { + if state.GlobalState.PosInBatch != 0 { + return fmt.Errorf("position in batch must be zero, but got %d: %+v", state.GlobalState.PosInBatch, state) + } + // We always agree with the genesis batch. + batchIndex := state.GlobalState.Batch + if batchIndex == 0 && state.GlobalState.PosInBatch == 0 { + return nil + } + // We always agree with the init message. + if batchIndex == 1 && state.GlobalState.PosInBatch == 0 { + return nil + } + + // Because an execution state from the assertion chain fully consumes the preceding batch, + // we actually want to check if we agree with the last state of the preceding batch, so + // we decrement the batch index by 1. + batchIndex -= 1 + + totalBatches, err := s.validator.inboxTracker.GetBatchCount() + if err != nil { + return err + } + + // If the batch index is >= the total number of batches we have in our inbox tracker, + // we are still catching up to the chain. + if batchIndex >= totalBatches { + return ErrChainCatchingUp + } + messageCount, err := s.validator.inboxTracker.GetBatchMessageCount(batchIndex) + if err != nil { + return err + } + validatedGlobalState, err := s.findGlobalStateFromMessageCountAndBatch(messageCount, l2stateprovider.Batch(batchIndex)) + if err != nil { + return err + } + // We check if the block hash and send root match at our expected result. + if state.GlobalState.BlockHash != validatedGlobalState.BlockHash || state.GlobalState.SendRoot != validatedGlobalState.SendRoot { + return l2stateprovider.ErrNoExecutionState + } + return nil +} + +// ExecutionStateAfterBatchCount Produces the l2 state to assert at the message number specified. +// Makes sure that PosInBatch is always 0 +func (s *StateManager) ExecutionStateAfterBatchCount(ctx context.Context, batchCount uint64) (*protocol.ExecutionState, error) { + if batchCount == 0 { + return nil, errors.New("batch count cannot be zero") + } + batchIndex := batchCount - 1 + messageCount, err := s.validator.inboxTracker.GetBatchMessageCount(batchIndex) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, fmt.Errorf("%w: batch count %d", l2stateprovider.ErrChainCatchingUp, batchCount) + } + return nil, err + } + globalState, err := s.findGlobalStateFromMessageCountAndBatch(messageCount, l2stateprovider.Batch(batchIndex)) + if err != nil { + return nil, err + } + executionState := &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState(globalState), + MachineStatus: protocol.MachineStatusFinished, + } + // If the execution state did not consume all messages in a batch, we then return + // the next batch's execution state. + if executionState.GlobalState.PosInBatch != 0 { + executionState.GlobalState.Batch += 1 + executionState.GlobalState.PosInBatch = 0 + } + return executionState, nil +} + +func (s *StateManager) StatesInBatchRange( + fromHeight, + toHeight l2stateprovider.Height, + fromBatch, + toBatch l2stateprovider.Batch, +) ([]common.Hash, error) { + // Check the integrity of the arguments. + if fromBatch >= toBatch { + return nil, fmt.Errorf("from batch %v cannot be greater than or equal to batch %v", fromBatch, toBatch) + } + if fromHeight > toHeight { + return nil, fmt.Errorf("from height %v cannot be greater than to height %v", fromHeight, toHeight) + } + // Compute the total desired hashes from this request. + totalDesiredHashes := (toHeight - fromHeight) + 1 + + // Get the from batch's message count. + prevBatchMsgCount, err := s.validator.inboxTracker.GetBatchMessageCount(uint64(fromBatch) - 1) + if err != nil { + return nil, err + } + executionResult, err := s.validator.streamer.ResultAtCount(prevBatchMsgCount) + if err != nil { + return nil, err + } + startState := validator.GoGlobalState{ + BlockHash: executionResult.BlockHash, + SendRoot: executionResult.SendRoot, + Batch: uint64(fromBatch), + PosInBatch: 0, + } + machineHashes := []common.Hash{machineHash(startState)} + states := []validator.GoGlobalState{startState} + + for batch := fromBatch; batch < toBatch; batch++ { + batchMessageCount, err := s.validator.inboxTracker.GetBatchMessageCount(uint64(batch)) + if err != nil { + return nil, err + } + messagesInBatch := batchMessageCount - prevBatchMsgCount + + // Obtain the states for each message in the batch. + for i := uint64(0); i < uint64(messagesInBatch); i++ { + msgIndex := uint64(prevBatchMsgCount) + i + messageCount := msgIndex + 1 + executionResult, err := s.validator.streamer.ResultAtCount(arbutil.MessageIndex(messageCount)) + if err != nil { + return nil, err + } + // If the position in batch is equal to the number of messages in the batch, + // we do not include this state. Instead, we break and include the state + // that fully consumes the batch. + if i+1 == uint64(messagesInBatch) { + break + } + state := validator.GoGlobalState{ + BlockHash: executionResult.BlockHash, + SendRoot: executionResult.SendRoot, + Batch: uint64(batch), + PosInBatch: i + 1, + } + states = append(states, state) + machineHashes = append(machineHashes, machineHash(state)) + } + + // Fully consume the batch. + executionResult, err := s.validator.streamer.ResultAtCount(batchMessageCount) + if err != nil { + return nil, err + } + state := validator.GoGlobalState{ + BlockHash: executionResult.BlockHash, + SendRoot: executionResult.SendRoot, + Batch: uint64(batch) + 1, + PosInBatch: 0, + } + states = append(states, state) + machineHashes = append(machineHashes, machineHash(state)) + prevBatchMsgCount = batchMessageCount + } + for uint64(len(machineHashes)) < uint64(totalDesiredHashes) { + machineHashes = append(machineHashes, machineHashes[len(machineHashes)-1]) + } + return machineHashes[fromHeight : toHeight+1], nil +} + +func machineHash(gs validator.GoGlobalState) common.Hash { + return crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes()) +} + +func (s *StateManager) findGlobalStateFromMessageCountAndBatch(count arbutil.MessageIndex, batchIndex l2stateprovider.Batch) (validator.GoGlobalState, error) { + var prevBatchMsgCount arbutil.MessageIndex + var err error + if batchIndex > 0 { + prevBatchMsgCount, err = s.validator.inboxTracker.GetBatchMessageCount(uint64(batchIndex) - 1) + if err != nil { + return validator.GoGlobalState{}, err + } + if prevBatchMsgCount > count { + return validator.GoGlobalState{}, errors.New("bad batch provided") + } + } + res, err := s.validator.streamer.ResultAtCount(count) + if err != nil { + return validator.GoGlobalState{}, fmt.Errorf("%s: could not check if we have result at count %d: %w", s.validatorName, count, err) + } + return validator.GoGlobalState{ + BlockHash: res.BlockHash, + SendRoot: res.SendRoot, + Batch: uint64(batchIndex), + PosInBatch: uint64(count - prevBatchMsgCount), + }, nil +} + +// L2MessageStatesUpTo Computes a block history commitment from a start L2 message to an end L2 message index +// and up to a required batch index. The hashes used for this commitment are the machine hashes +// at each message number. +func (s *StateManager) L2MessageStatesUpTo( + _ context.Context, + fromHeight l2stateprovider.Height, + toHeight option.Option[l2stateprovider.Height], + fromBatch, + toBatch l2stateprovider.Batch, +) ([]common.Hash, error) { + var to l2stateprovider.Height + if !toHeight.IsNone() { + to = toHeight.Unwrap() + } else { + blockChallengeLeafHeight := s.challengeLeafHeights[0] + to = blockChallengeLeafHeight + } + items, err := s.StatesInBatchRange(fromHeight, to, fromBatch, toBatch) + if err != nil { + return nil, err + } + return items, nil +} + +// CollectMachineHashes Collects a list of machine hashes at a message number based on some configuration parameters. +func (s *StateManager) CollectMachineHashes( + ctx context.Context, cfg *l2stateprovider.HashCollectorConfig, +) ([]common.Hash, error) { + s.Lock() + defer s.Unlock() + prevBatchMsgCount, err := s.validator.inboxTracker.GetBatchMessageCount(uint64(cfg.FromBatch - 1)) + if err != nil { + return nil, fmt.Errorf("could not get batch message count at %d: %w", cfg.FromBatch, err) + } + messageNum := (prevBatchMsgCount + arbutil.MessageIndex(cfg.BlockChallengeHeight)) + cacheKey := &challengecache.Key{ + WavmModuleRoot: cfg.WasmModuleRoot, + MessageHeight: protocol.Height(messageNum), + StepHeights: cfg.StepHeights, + } + if s.historyCache != nil { + cachedRoots, err := s.historyCache.Get(cacheKey, cfg.NumDesiredHashes) + switch { + case err == nil: + return cachedRoots, nil + case !errors.Is(err, challengecache.ErrNotFoundInCache): + return nil, err + } + } + entry, err := s.validator.CreateReadyValidationEntry(ctx, messageNum) + if err != nil { + return nil, err + } + input, err := entry.ToInput() + if err != nil { + return nil, err + } + execRun, err := s.validator.execSpawner.CreateBoldExecutionRun(cfg.WasmModuleRoot, uint64(cfg.StepSize), input).Await(ctx) + if err != nil { + return nil, err + } + ctxCheckAlive, cancelCheckAlive := ctxWithCheckAlive(ctx, execRun) + defer cancelCheckAlive() + stepLeaves := execRun.GetLeavesWithStepSize(uint64(cfg.FromBatch), uint64(cfg.MachineStartIndex), uint64(cfg.StepSize), cfg.NumDesiredHashes) + result, err := stepLeaves.Await(ctxCheckAlive) + if err != nil { + return nil, err + } + log.Info(fmt.Sprintf("Finished gathering machine hashes for request %+v", cfg)) + // Do not save a history commitment of length 1 to the cache. + if len(result) > 1 && s.historyCache != nil { + if err := s.historyCache.Put(cacheKey, result); err != nil { + if !errors.Is(err, challengecache.ErrFileAlreadyExists) { + return nil, err + } + } + } + return result, nil +} + +// CtxWithCheckAlive Creates a context with a check alive routine +// that will cancel the context if the check alive routine fails. +func ctxWithCheckAlive(ctxIn context.Context, execRun validator.ExecutionRun) (context.Context, context.CancelFunc) { + // Create a context that will cancel if the check alive routine fails. + // This is to ensure that we do not have the validator froze indefinitely if the execution run + // is no longer alive. + ctx, cancel := context.WithCancel(ctxIn) + // Create a context with cancel, so that we can cancel the check alive routine + // once the calling function returns. + ctxCheckAlive, cancelCheckAlive := context.WithCancel(ctxIn) + go func() { + // Call cancel so that the calling function is canceled if the check alive routine fails/returns. + defer cancel() + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctxCheckAlive.Done(): + return + case <-ticker.C: + // Create a context with a timeout, so that the check alive routine does not run indefinitely. + ctxCheckAliveWithTimeout, cancelCheckAliveWithTimeout := context.WithTimeout(ctxCheckAlive, 5*time.Second) + err := execRun.CheckAlive(ctxCheckAliveWithTimeout) + if err != nil { + cancelCheckAliveWithTimeout() + return + } + cancelCheckAliveWithTimeout() + } + } + }() + return ctx, cancelCheckAlive +} + +// CollectProof Collects osp of at a message number and OpcodeIndex . +func (s *StateManager) CollectProof( + ctx context.Context, + wasmModuleRoot common.Hash, + fromBatch l2stateprovider.Batch, + blockChallengeHeight l2stateprovider.Height, + machineIndex l2stateprovider.OpcodeIndex, +) ([]byte, error) { + prevBatchMsgCount, err := s.validator.inboxTracker.GetBatchMessageCount(uint64(fromBatch) - 1) + if err != nil { + return nil, err + } + messageNum := (prevBatchMsgCount + arbutil.MessageIndex(blockChallengeHeight)) + entry, err := s.validator.CreateReadyValidationEntry(ctx, messageNum) + if err != nil { + return nil, err + } + input, err := entry.ToInput() + if err != nil { + return nil, err + } + execRun, err := s.validator.execSpawner.CreateExecutionRun(wasmModuleRoot, input).Await(ctx) + if err != nil { + return nil, err + } + ctxCheckAlive, cancelCheckAlive := ctxWithCheckAlive(ctx, execRun) + defer cancelCheckAlive() + oneStepProofPromise := execRun.GetProofAt(uint64(machineIndex)) + return oneStepProofPromise.Await(ctxCheckAlive) +} diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index acd86f8627..dbc7bf854d 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -4,9 +4,11 @@ package staker import ( + "bytes" "context" "errors" "fmt" + "math/big" "regexp" "sync" "testing" @@ -22,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbstate" ) @@ -279,6 +282,16 @@ func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e * return fmt.Errorf("error while trying to read delayed msg for proving: %w", err) } e.DelayedMsg = delayedMsg + + if v.config.Evil { + interceptGweiAmount := new(big.Int).SetUint64(v.config.EvilInterceptDepositGwei * params.GWei) + // Tweak the delayed message. + if bytes.Contains(delayedMsg, interceptGweiAmount.Bytes()) { + newValue := new(big.Int).Add(interceptGweiAmount, big.NewInt(params.GWei)) + modified := bytes.Replace(delayedMsg, interceptGweiAmount.Bytes(), newValue.Bytes(), 1) + e.DelayedMsg = modified + } + } } for _, batch := range e.BatchInfo { if len(batch.Data) <= 40 { diff --git a/system_tests/assertion_on_large_number_of_block_test.go b/system_tests/assertion_on_large_number_of_block_test.go new file mode 100644 index 0000000000..9bde353260 --- /dev/null +++ b/system_tests/assertion_on_large_number_of_block_test.go @@ -0,0 +1,402 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE + +//go:build assertion_on_large_number_of_batch_test +// +build assertion_on_large_number_of_batch_test + +package arbtest + +import ( + "context" + "encoding/json" + "math/big" + "os" + "testing" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + solimpl "github.com/OffchainLabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/OffchainLabs/bold/challenge-manager" + modes "github.com/OffchainLabs/bold/challenge-manager/types" + "github.com/OffchainLabs/bold/containers/option" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/OffchainLabs/bold/math" + "github.com/OffchainLabs/bold/solgen/go/mocksgen" + "github.com/OffchainLabs/bold/solgen/go/rollupgen" + "github.com/OffchainLabs/bold/testing" + "github.com/OffchainLabs/bold/testing/setup" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/offchainlabs/nitro/arbcompress" + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbstate" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/validator/server_common" + "github.com/offchainlabs/nitro/validator/valnode" +) + +// Helps in testing the feasibility of assertion after the protocol upgrade. +func TestAssertionOnLargeNumberOfBlocks(t *testing.T) { + setupStartTime := time.Now().Unix() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + l2node, assertionChain := setupAndPostBatches(t, ctx) + + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + l2node.Execution, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = stateless.Start(ctx) + Require(t, err) + + challengeLeafHeights := []l2stateprovider.Height{ + l2stateprovider.Height(uint64(1 << 26)), // blockChallengeLeafHeight = 67108864 + l2stateprovider.Height(uint64(1 << 11)), // bigStepChallengeLeafHeight = 2048 + l2stateprovider.Height(uint64(1 << 20)), // smallStepChallengeLeafHeight = 1048576 + } + manager, err := staker.NewStateManager(stateless, t.TempDir(), challengeLeafHeights, "test") + Require(t, err) + provider := l2stateprovider.NewHistoryCommitmentProvider( + manager, + manager, + manager, + challengeLeafHeights, + manager, + ) + + challengeManager, err := challengemanager.New( + ctx, + assertionChain, + assertionChain.Backend(), + provider, + assertionChain.RollupAddress(), + challengemanager.WithName("test"), + challengemanager.WithMode(modes.DefensiveMode), + challengemanager.WithAssertionPostingInterval(time.Hour), + challengemanager.WithAssertionScanningInterval(time.Hour), + challengemanager.WithEdgeTrackerWakeInterval(time.Second), + ) + poster := challengeManager.AssertionManager() + assertion, err := poster.PostAssertion(ctx) + Require(t, err) + setupEndTime := time.Now().Unix() + print("Time taken for setup:") + print(setupEndTime - setupStartTime) + + assertion, err = poster.PostAssertion(ctx) + Require(t, err) + assertionPostingEndTime := time.Now().Unix() + print("Time taken to post assertion:") + print(assertionPostingEndTime - setupEndTime) + startHeight, endHeight, wasmModuleRoot, topLevelClaimEndBatchCount := testCalculatingBlockChallengeLevelZeroEdge(t, ctx, assertionChain, assertion, provider) + levelZeroEdgeEndTime := time.Now().Unix() + print("Time taken Calculating BlockChallenge LevelZeroEdge:") + print(levelZeroEdgeEndTime - assertionPostingEndTime) + testCalculatingBlockChallengeLevelZeroEdgeBisection(t, ctx, provider, startHeight, endHeight, wasmModuleRoot, topLevelClaimEndBatchCount) + bisectionOfLevelZeroEdgeEndTime := time.Now().Unix() + print("Time taken Calculating BlockChallenge LevelZeroEdge Bisection:") + print(bisectionOfLevelZeroEdgeEndTime - levelZeroEdgeEndTime) + +} +func testCalculatingBlockChallengeLevelZeroEdgeBisection( + t *testing.T, + ctx context.Context, + provider *l2stateprovider.HistoryCommitmentProvider, + startHeight uint64, + endHeight uint64, + wasmModuleRoot common.Hash, + topLevelClaimEndBatchCount uint64, +) { + bisectTo, err := math.Bisect(startHeight, endHeight) + Require(t, err) + _, err = provider.HistoryCommitment( + ctx, + &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: wasmModuleRoot, + FromBatch: 0, + ToBatch: l2stateprovider.Batch(topLevelClaimEndBatchCount), + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: l2stateprovider.Height(0), + UpToHeight: option.Some[l2stateprovider.Height](l2stateprovider.Height(bisectTo)), + }, + ) + + Require(t, err) + _, err = provider.PrefixProof( + ctx, + &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: wasmModuleRoot, + FromBatch: 0, + ToBatch: l2stateprovider.Batch(topLevelClaimEndBatchCount), + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: l2stateprovider.Height(bisectTo), + UpToHeight: option.Some[l2stateprovider.Height](l2stateprovider.Height(endHeight)), + }, + l2stateprovider.Height(bisectTo), + ) + Require(t, err) +} + +func testCalculatingBlockChallengeLevelZeroEdge( + t *testing.T, + ctx context.Context, + assertionChain protocol.Protocol, + assertion protocol.Assertion, + provider *l2stateprovider.HistoryCommitmentProvider, +) (uint64, uint64, common.Hash, uint64) { + + creationInfo, err := assertionChain.ReadAssertionCreationInfo(ctx, assertion.Id()) + Require(t, err) + + startCommit, err := provider.HistoryCommitment( + ctx, + &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: creationInfo.WasmModuleRoot, + FromBatch: 0, + ToBatch: l2stateprovider.Batch(0), + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: l2stateprovider.Height(0), + UpToHeight: option.Some[l2stateprovider.Height](l2stateprovider.Height(0)), + }, + ) + Require(t, err) + levelZeroBlockEdgeHeight := uint64(1 << 26) + Require(t, err) + + endCommit, err := provider.HistoryCommitment( + ctx, + &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: creationInfo.WasmModuleRoot, + FromBatch: 0, + ToBatch: l2stateprovider.Batch(creationInfo.InboxMaxCount.Uint64()), + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: l2stateprovider.Height(0), + UpToHeight: option.Some[l2stateprovider.Height](l2stateprovider.Height(levelZeroBlockEdgeHeight)), + }, + ) + Require(t, err) + _, err = provider.PrefixProof( + ctx, + &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: creationInfo.WasmModuleRoot, + FromBatch: 0, + ToBatch: l2stateprovider.Batch(creationInfo.InboxMaxCount.Uint64()), + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: l2stateprovider.Height(0), + UpToHeight: option.Some[l2stateprovider.Height](l2stateprovider.Height(levelZeroBlockEdgeHeight)), + }, + l2stateprovider.Height(0), + ) + Require(t, err) + return startCommit.Height, endCommit.Height, creationInfo.WasmModuleRoot, creationInfo.InboxMaxCount.Uint64() +} +func setupAndPostBatches(t *testing.T, ctx context.Context) (*arbnode.Node, *solimpl.AssertionChain) { + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false))) + glogger.Verbosity(log.LvlInfo) + log.Root().SetHandler(glogger) + + initialBalance := new(big.Int).Lsh(big.NewInt(1), 250) + l1Info := NewL1TestInfo(t) + l1Info.GenerateGenesisAccount("deployer", initialBalance) + l1Info.GenerateGenesisAccount("asserter", initialBalance) + l1Info.GenerateGenesisAccount("sequencer", initialBalance) + l1Info.GenerateGenesisAccount("RollupOwner", initialBalance) + + chainConfig := params.ArbitrumDevTestChainConfig() + l1Info, l1Backend, _, _ := createTestL1BlockChain(t, l1Info) + conf := arbnode.ConfigDefaultL1Test() + conf.BlockValidator.Enable = false + conf.BatchPoster.Enable = false + conf.InboxReader.CheckDelay = time.Second + + var valStack *node.Node + _, valStack = createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + configByValidationNode(t, conf, valStack) + + l1TransactionOpts := l1Info.GetDefaultTransactOpts("RollupOwner", ctx) + stakeToken, tx, tokenBindings, err := mocksgen.DeployTestWETH9( + &l1TransactionOpts, + l1Backend, + "Weth", + "WETH", + ) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1Backend, tx) + Require(t, err) + value, _ := new(big.Int).SetString("10000", 10) + l1TransactionOpts.Value = value + tx, err = tokenBindings.Deposit(&l1TransactionOpts) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1Backend, tx) + Require(t, err) + l1TransactionOpts.Value = nil + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1Backend, tx) + Require(t, err) + rollupAddresses, assertionChain := deployBoldContractsAndTokenBinding(t, ctx, l1Info, l1Backend, chainConfig.ChainID, stakeToken) + l1Info.SetContract("Bridge", rollupAddresses.Bridge) + l1Info.SetContract("SequencerInbox", rollupAddresses.SequencerInbox) + l1Info.SetContract("Inbox", rollupAddresses.Inbox) + initMessage := getInitMessage(ctx, t, l1Backend, rollupAddresses) + + l2Info, l2Stack, l2ChainDb, l2ArbDb, l2Blockchain := createL2BlockChainWithStackConfig(t, nil, "", chainConfig, initMessage, nil, nil) + + fatalErrChan := make(chan error, 10) + execConfigFetcher := func() *gethexec.Config { return gethexec.ConfigDefaultTest() } + execNode, err := gethexec.CreateExecutionNode(ctx, l2Stack, l2ChainDb, l2Blockchain, l1Backend, execConfigFetcher) + Require(t, err) + l2Node, err := arbnode.CreateNode(ctx, l2Stack, execNode, l2ArbDb, NewFetcherFromConfig(conf), l2Blockchain.Config(), l1Backend, rollupAddresses, nil, nil, nil, fatalErrChan) + Require(t, err) + err = l2Node.Start(ctx) + Require(t, err) + + l2Info.GenerateAccount("Destination") + + rollup, err := rollupgen.NewRollupAdminLogic(l2Node.DeployInfo.Rollup, l1Backend) + Require(t, err) + deployAuth := l1Info.GetDefaultTransactOpts("RollupOwner", ctx) + _, err = rollup.SetMinimumAssertionPeriod(&deployAuth, big.NewInt(0)) + Require(t, err) + + emptyArray, err := rlp.EncodeToBytes([]uint8{0}) + Require(t, err) + var out []byte + for i := 0; i < arbstate.MaxSegmentsPerSequencerMessage-1; i++ { + out = append(out, emptyArray...) + } + batch := []uint8{0} + compressed, err := arbcompress.CompressWell(out) + Require(t, err) + batch = append(batch, compressed...) + + txOpts := l1Info.GetDefaultTransactOpts("deployer", ctx) + simpleAddress, simple := deploySimple(t, ctx, txOpts, l1Backend) + seqInbox, err := bridgegen.NewSequencerInbox(rollupAddresses.SequencerInbox, l1Backend) + Require(t, err) + tx, err = seqInbox.SetIsBatchPoster(&deployAuth, simpleAddress, true) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l1Backend, tx) + Require(t, err) + for i := 0; i < 3; i++ { + tx, err = simple.PostManyBatches(&txOpts, rollupAddresses.SequencerInbox, batch, big.NewInt(300)) + Require(t, err) + receipt, err = EnsureTxSucceeded(ctx, l1Backend, tx) + Require(t, err) + + nodeSeqInbox, err := arbnode.NewSequencerInbox(l1Backend, rollupAddresses.SequencerInbox, 0) + Require(t, err) + batches, err := nodeSeqInbox.LookupBatchesInRange(ctx, receipt.BlockNumber, receipt.BlockNumber) + Require(t, err) + if len(batches) != 300 { + Fatal(t, "300 batch not found after PostManyBatches") + } + err = l2Node.InboxTracker.AddSequencerBatches(ctx, l1Backend, batches) + Require(t, err) + _, err = l2Node.InboxTracker.GetBatchMetadata(0) + Require(t, err, "failed to get batch metadata after adding batch:") + } + return l2Node, assertionChain +} + +func deployBoldContractsAndTokenBinding( + t *testing.T, + ctx context.Context, + l1info info, + backend *ethclient.Client, + chainId *big.Int, + stakeToken common.Address, +) (*chaininfo.RollupAddresses, *solimpl.AssertionChain) { + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + locator, err := server_common.NewMachineLocator("") + Require(t, err) + + cfg := challenge_testing.GenerateRollupConfig( + false, + locator.LatestWasmModuleRoot(), + l1TransactionOpts.From, + chainId, + common.Address{}, + big.NewInt(1), + stakeToken, + rollupgen.ExecutionState{ + GlobalState: rollupgen.GlobalState{}, + MachineStatus: 1, + }, + big.NewInt(0), + common.Address{}, + ) + config, err := json.Marshal(params.ArbitrumDevTestChainConfig()) + if err != nil { + return nil, nil + } + cfg.ChainConfig = string(config) + + addresses, err := setup.DeployFullRollupStack( + ctx, + backend, + &l1TransactionOpts, + l1info.GetAddress("sequencer"), + cfg, + false, + true, + ) + Require(t, err) + + asserter := l1info.GetDefaultTransactOpts("asserter", ctx) + chain, err := solimpl.NewAssertionChain( + ctx, + addresses.Rollup, + &asserter, + backend, + ) + Require(t, err) + + chalManager, err := chain.SpecChallengeManager(ctx) + Require(t, err) + chalManagerAddr := chalManager.Address() + seed, _ := new(big.Int).SetString("1000", 10) + value, _ := new(big.Int).SetString("10000", 10) + tokenBindings, err := mocksgen.NewTestWETH9(stakeToken, backend) + Require(t, err) + tx, err := tokenBindings.TestWETH9Transactor.Transfer(&l1TransactionOpts, asserter.From, seed) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, addresses.Rollup, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, chalManagerAddr, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + return &chaininfo.RollupAddresses{ + Bridge: addresses.Bridge, + Inbox: addresses.Inbox, + SequencerInbox: addresses.SequencerInbox, + Rollup: addresses.Rollup, + ValidatorUtils: addresses.ValidatorUtils, + ValidatorWalletCreator: addresses.ValidatorWalletCreator, + DeployedAt: addresses.DeployedAt, + }, chain +} diff --git a/system_tests/bold_challenge_protocol_test.go b/system_tests/bold_challenge_protocol_test.go new file mode 100644 index 0000000000..32b7d521a9 --- /dev/null +++ b/system_tests/bold_challenge_protocol_test.go @@ -0,0 +1,737 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +// race detection makes things slow and miss timeouts +//go:build challengetest && !race + +package arbtest + +import ( + "bytes" + "context" + "encoding/json" + "io" + "math/big" + "os" + "testing" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + solimpl "github.com/OffchainLabs/bold/chain-abstraction/sol-implementation" + challengemanager "github.com/OffchainLabs/bold/challenge-manager" + modes "github.com/OffchainLabs/bold/challenge-manager/types" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/OffchainLabs/bold/solgen/go/bridgegen" + "github.com/OffchainLabs/bold/solgen/go/mocksgen" + "github.com/OffchainLabs/bold/solgen/go/rollupgen" + challenge_testing "github.com/OffchainLabs/bold/testing" + "github.com/OffchainLabs/bold/testing/setup" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/offchainlabs/nitro/arbcompress" + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/arbstate" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/statetransfer" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/util/signature" + "github.com/offchainlabs/nitro/validator/server_common" + "github.com/offchainlabs/nitro/validator/valnode" +) + +// One Arbitrum block had 1,849,212,947 total opcodes. The closest, higher power of two +// is 2^31. So we if we make our small step heights 2^20, we need 2048 big steps +// to cover the block. With 2^20, our small step history commitments will be approx +// 32 Mb of state roots in memory at once. +var ( + blockChallengeLeafHeight = uint64(1 << 5) // 32 + bigStepChallengeLeafHeight = uint64(1 << 5) // 5 big step levels, 2^5 each, with small step equaling to 2^31 total. + smallStepChallengeLeafHeight = uint64(1 << 6) +) + +func TestBoldProtocol(t *testing.T) { + t.Cleanup(func() { + Require(t, os.RemoveAll("/tmp/good")) + Require(t, os.RemoveAll("/tmp/evil")) + }) + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + l2chainConfig := params.ArbitrumDevTestChainConfig() + l2info := NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(l2chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + ownerBal := big.NewInt(params.Ether) + ownerBal.Mul(ownerBal, big.NewInt(1_000_000)) + l2info.GenerateGenesisAccount("Owner", ownerBal) + + _, l2nodeA, _, _, l1info, _, l1client, l1stack, assertionChain, stakeTokenAddr := createTestNodeOnL1ForBoldProtocol(t, ctx, true, nil, l2chainConfig, nil, l2info) + defer requireClose(t, l1stack) + defer l2nodeA.StopAndWait() + + // Every 10 seconds, send an L1 transaction to keep the chain moving. + go func() { + delay := time.Second * 10 + for { + select { + case <-ctx.Done(): + return + default: + time.Sleep(delay) + balance := big.NewInt(params.GWei) + TransferBalance(t, "Faucet", "Asserter", balance, l1info, l1client, ctx) + latestBlock, err := l1client.BlockNumber(ctx) + Require(t, err) + if latestBlock > 150 { + delay = time.Second + } + } + } + }() + + _, l2nodeB, assertionChainB := create2ndNodeWithConfigForBoldProtocol(t, ctx, l2nodeA, l1stack, l1info, &l2info.ArbInitData, arbnode.ConfigDefaultL1Test(), nil, stakeTokenAddr) + defer l2nodeB.StopAndWait() + + nodeAMessage, err := l2nodeA.Execution.HeadMessageNumber() + Require(t, err) + nodeBMessage, err := l2nodeB.Execution.HeadMessageNumber() + Require(t, err) + if nodeAMessage != nodeBMessage { + Fatal(t, "node A L2 genesis hash", nodeAMessage, "!= node B L2 genesis hash", nodeBMessage) + } + + deployAuth := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + TransferBalance(t, "Faucet", "Asserter", balance, l1info, l1client, ctx) + TransferBalance(t, "Faucet", "EvilAsserter", balance, l1info, l1client, ctx) + l1authB := l1info.GetDefaultTransactOpts("EvilAsserter", ctx) + + t.Log("Setting the minimum assertion period") + rollup, err := rollupgen.NewRollupAdminLogicTransactor(assertionChain.RollupAddress(), l1client) + Require(t, err) + tx, err := rollup.SetMinimumAssertionPeriod(&deployAuth, big.NewInt(0)) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + rollup, err = rollupgen.NewRollupAdminLogicTransactor(assertionChainB.RollupAddress(), l1client) + Require(t, err) + tx, err = rollup.SetMinimumAssertionPeriod(&deployAuth, big.NewInt(0)) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + + valCfg := valnode.TestValidationConfig + valCfg.UseJit = false + _, valStack := createTestValidationNode(t, ctx, &valCfg) + blockValidatorConfig := staker.TestBlockValidatorConfig + + statelessA, err := staker.NewStatelessBlockValidator( + l2nodeA.InboxReader, + l2nodeA.InboxTracker, + l2nodeA.TxStreamer, + l2nodeA.Execution, + l2nodeA.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = statelessA.Start(ctx) + Require(t, err) + + statelessB, err := staker.NewStatelessBlockValidator( + l2nodeB.InboxReader, + l2nodeB.InboxTracker, + l2nodeB.TxStreamer, + l2nodeB.Execution, + l2nodeB.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = statelessB.Start(ctx) + Require(t, err) + + stateManager, err := staker.NewStateManager( + statelessA, + "/tmp/good", + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + "good", + ) + Require(t, err) + + stateManagerB, err := staker.NewStateManager( + statelessB, + "/tmp/evil", + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + "evil", + ) + Require(t, err) + + chainB, err := solimpl.NewAssertionChain( + ctx, + assertionChain.RollupAddress(), + &l1authB, + l1client, + ) + Require(t, err) + + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + honestSeqInbox := l1info.GetAddress("SequencerInbox") + evilSeqInbox := l1info.GetAddress("EvilSequencerInbox") + honestSeqInboxBinding, err := bridgegen.NewSequencerInbox(honestSeqInbox, l1client) + Require(t, err) + evilSeqInboxBinding, err := bridgegen.NewSequencerInbox(evilSeqInbox, l1client) + Require(t, err) + + // Post batches to the honest and evil sequencer inbox that are internally equal. + // This means the honest and evil sequencer inboxes will agree with all messages in the batch. + totalMessagesPosted := int64(0) + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) + makeBoldBatch(t, l2nodeA, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + l2info.Accounts["Owner"].Nonce = 0 + makeBoldBatch(t, l2nodeB, l2info, l1client, &sequencerTxOpts, evilSeqInboxBinding, evilSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + // Next, we post another batch, this time containing more messages. + // We diverge at message index 5 within the evil node's batch. + l2info.Accounts["Owner"].Nonce = 5 + numMessagesPerBatch = int64(10) + makeBoldBatch(t, l2nodeA, l2info, l1client, &sequencerTxOpts, honestSeqInboxBinding, honestSeqInbox, numMessagesPerBatch, divergeAt) + l2info.Accounts["Owner"].Nonce = 5 + divergeAt = int64(5) + makeBoldBatch(t, l2nodeB, l2info, l1client, &sequencerTxOpts, evilSeqInboxBinding, evilSeqInbox, numMessagesPerBatch, divergeAt) + totalMessagesPosted += numMessagesPerBatch + + bcA, err := l2nodeA.InboxTracker.GetBatchCount() + Require(t, err) + bcB, err := l2nodeB.InboxTracker.GetBatchCount() + Require(t, err) + msgA, err := l2nodeA.InboxTracker.GetBatchMessageCount(bcA - 1) + Require(t, err) + msgB, err := l2nodeB.InboxTracker.GetBatchMessageCount(bcB - 1) + Require(t, err) + + t.Logf("Node A batch count %d, msgs %d", bcA, msgA) + t.Logf("Node B batch count %d, msgs %d", bcB, msgB) + + // Wait for both nodes' chains to catch up. + nodeAExec, ok := l2nodeA.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + nodeBExec, ok := l2nodeB.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + for { + nodeALatest := nodeAExec.Backend.APIBackend().CurrentHeader() + nodeBLatest := nodeBExec.Backend.APIBackend().CurrentHeader() + isCaughtUp := nodeALatest.Number.Uint64() == uint64(totalMessagesPosted) + areEqual := nodeALatest.Number.Uint64() == nodeBLatest.Number.Uint64() + if isCaughtUp && areEqual { + if nodeALatest.Hash() == nodeBLatest.Hash() { + Fatal(t, "node A L2 hash", nodeALatest, "matches node B L2 hash", nodeBLatest) + } + break + } + } + + // Wait for the validator to validate the batches. + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + totalMessageCount, err := l2nodeA.InboxTracker.GetBatchMessageCount(totalBatches - 1) + Require(t, err) + + // Wait until the validator has validated the batches. + for { + _, err1 := l2nodeA.TxStreamer.ResultAtCount(totalMessageCount) + nodeAHasValidated := err1 == nil + + _, err2 := l2nodeB.TxStreamer.ResultAtCount(totalMessageCount) + nodeBHasValidated := err2 == nil + + if nodeAHasValidated && nodeBHasValidated { + break + } + } + + provider := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManager, + ) + + evilProvider := l2stateprovider.NewHistoryCommitmentProvider( + stateManagerB, + stateManagerB, + stateManagerB, + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + stateManagerB, + ) + + manager, err := challengemanager.New( + ctx, + assertionChain, + l1client, + provider, + assertionChain.RollupAddress(), + challengemanager.WithName("honest"), + challengemanager.WithMode(modes.DefensiveMode), + challengemanager.WithAssertionPostingInterval(time.Hour), + challengemanager.WithAssertionScanningInterval(time.Hour), + challengemanager.WithEdgeTrackerWakeInterval(time.Second), + ) + Require(t, err) + + t.Log("Honest party posting assertion at batch 1, pos 0") + + poster := manager.AssertionManager() + _, err = poster.PostAssertion(ctx) + Require(t, err) + + t.Log("Honest party posting assertion at batch 2, pos 0") + expectedWinnerAssertion, err := poster.PostAssertion(ctx) + Require(t, err) + + managerB, err := challengemanager.New( + ctx, + chainB, + l1client, + evilProvider, + assertionChain.RollupAddress(), + challengemanager.WithName("evil"), + challengemanager.WithMode(modes.DefensiveMode), + challengemanager.WithAssertionPostingInterval(time.Hour), + challengemanager.WithAssertionScanningInterval(time.Hour), + challengemanager.WithEdgeTrackerWakeInterval(time.Second), + ) + Require(t, err) + + t.Log("Evil party posting assertion at batch 2, pos 0") + posterB := managerB.AssertionManager() + _, err = posterB.PostAssertion(ctx) + Require(t, err) + + manager.Start(ctx) + managerB.Start(ctx) + + rollupUserLogic, err := rollupgen.NewRollupUserLogic(assertionChain.RollupAddress(), l1client) + Require(t, err) + for { + expected, err := rollupUserLogic.GetAssertion(&bind.CallOpts{Context: ctx}, expectedWinnerAssertion.Unwrap().Id().Hash) + if err != nil { + t.Logf("Error getting assertion: %v", err) + continue + } + // Wait until the assertion is confirmed. + if expected.Status == uint8(2) { + t.Log("Expected assertion was confirmed") + return + } + time.Sleep(time.Second * 5) + } +} + +func createTestNodeOnL1ForBoldProtocol( + t *testing.T, + ctx context.Context, + isSequencer bool, + nodeConfig *arbnode.Config, + chainConfig *params.ChainConfig, + stackConfig *node.Config, + l2info_in info, +) ( + l2info info, currentNode *arbnode.Node, l2client *ethclient.Client, l2stack *node.Node, + l1info info, l1backend *eth.Ethereum, l1client *ethclient.Client, l1stack *node.Node, + assertionChain *solimpl.AssertionChain, stakeTokenAddr common.Address, +) { + if nodeConfig == nil { + nodeConfig = arbnode.ConfigDefaultL1Test() + } + nodeConfig.ParentChainReader.OldHeaderTimeout = time.Minute * 10 + if chainConfig == nil { + chainConfig = params.ArbitrumDevTestChainConfig() + } + nodeConfig.BatchPoster.DataPoster.MaxMempoolTransactions = 0 + fatalErrChan := make(chan error, 10) + l1info, l1client, l1backend, l1stack = createTestL1BlockChain(t, nil) + var l2chainDb ethdb.Database + var l2arbDb ethdb.Database + var l2blockchain *core.BlockChain + l2info = l2info_in + if l2info == nil { + l2info = NewArbTestInfo(t, chainConfig.ChainID) + } + + l1info.GenerateAccount("RollupOwner") + l1info.GenerateAccount("Sequencer") + l1info.GenerateAccount("User") + l1info.GenerateAccount("Asserter") + l1info.GenerateAccount("EvilAsserter") + + SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ + l1info.PrepareTx("Faucet", "RollupOwner", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "Sequencer", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "User", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "Asserter", 30000, big.NewInt(9223372036854775807), nil), + l1info.PrepareTx("Faucet", "EvilAsserter", 30000, big.NewInt(9223372036854775807), nil), + }) + + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + stakeToken, tx, tokenBindings, err := mocksgen.DeployTestWETH9( + &l1TransactionOpts, + l1client, + "Weth", + "WETH", + ) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + stakeTokenAddr = stakeToken + value, ok := new(big.Int).SetString("10000", 10) + if !ok { + t.Fatal(t, "could not set value") + } + l1TransactionOpts.Value = value + tx, err = tokenBindings.Deposit(&l1TransactionOpts) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + l1TransactionOpts.Value = nil + + addresses, assertionChainBindings := deployContractsOnly(t, ctx, l1info, l1client, chainConfig.ChainID, stakeToken) + + l1info.SetContract("Bridge", addresses.Bridge) + l1info.SetContract("SequencerInbox", addresses.SequencerInbox) + l1info.SetContract("Inbox", addresses.Inbox) + + _, l2stack, l2chainDb, l2arbDb, l2blockchain = createL2BlockChainWithStackConfig(t, l2info, "", chainConfig, getInitMessage(ctx, t, l1client, addresses), stackConfig, nil) + assertionChain = assertionChainBindings + var sequencerTxOptsPtr *bind.TransactOpts + var dataSigner signature.DataSignerFunc + if isSequencer { + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + sequencerTxOptsPtr = &sequencerTxOpts + dataSigner = signature.DataSignerFromPrivateKey(l1info.GetInfoWithPrivKey("Sequencer").PrivateKey) + } + + if !isSequencer { + nodeConfig.BatchPoster.Enable = false + nodeConfig.DelayedSequencer.Enable = false + } + + AddDefaultValNode(t, ctx, nodeConfig, true) + + execConfig := gethexec.ConfigDefaultTest() + Require(t, execConfig.Validate()) + execConfigFetcher := func() *gethexec.Config { return execConfig } + execNode, err := gethexec.CreateExecutionNode(ctx, l2stack, l2chainDb, l2blockchain, l1client, execConfigFetcher) + Require(t, err) + + currentNode, err = arbnode.CreateNode( + ctx, l2stack, execNode, l2arbDb, NewFetcherFromConfig(nodeConfig), l2blockchain.Config(), l1client, + addresses, sequencerTxOptsPtr, sequencerTxOptsPtr, dataSigner, fatalErrChan, + ) + Require(t, err) + + Require(t, currentNode.Start(ctx)) + + l2client = ClientForStack(t, l2stack) + + StartWatchChanErr(t, ctx, fatalErrChan, currentNode) + + return +} + +func deployContractsOnly( + t *testing.T, + ctx context.Context, + l1info info, + backend *ethclient.Client, + chainId *big.Int, + stakeToken common.Address, +) (*chaininfo.RollupAddresses, *solimpl.AssertionChain) { + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + locator, err := server_common.NewMachineLocator("") + Require(t, err) + wasmModuleRoot := locator.LatestWasmModuleRoot() + + loserStakeEscrow := common.Address{} + miniStake := big.NewInt(1) + genesisExecutionState := rollupgen.ExecutionState{ + GlobalState: rollupgen.GlobalState{}, + MachineStatus: 1, + } + genesisInboxCount := big.NewInt(0) + anyTrustFastConfirmer := common.Address{} + cfg := challenge_testing.GenerateRollupConfig( + false, + wasmModuleRoot, + l1TransactionOpts.From, + chainId, + loserStakeEscrow, + miniStake, + stakeToken, + genesisExecutionState, + genesisInboxCount, + anyTrustFastConfirmer, + challenge_testing.WithLayerZeroHeights(&protocol.LayerZeroHeights{ + BlockChallengeHeight: blockChallengeLeafHeight, + BigStepChallengeHeight: bigStepChallengeLeafHeight, + SmallStepChallengeHeight: smallStepChallengeLeafHeight, + }), + challenge_testing.WithNumBigStepLevels(uint8(5)), // TODO: Hardcoded. + challenge_testing.WithConfirmPeriodBlocks(uint64(150)), // TODO: Hardcoded. + ) + config, err := json.Marshal(params.ArbitrumDevTestChainConfig()) + Require(t, err) + cfg.ChainConfig = string(config) + addresses, err := setup.DeployFullRollupStack( + ctx, + backend, + &l1TransactionOpts, + l1info.GetAddress("Sequencer"), + cfg, + false, // do not use mock bridge. + false, // do not use a mock one step prover + ) + Require(t, err) + + asserter := l1info.GetDefaultTransactOpts("Asserter", ctx) + evilAsserter := l1info.GetDefaultTransactOpts("EvilAsserter", ctx) + chain, err := solimpl.NewAssertionChain( + ctx, + addresses.Rollup, + &asserter, + backend, + ) + Require(t, err) + + chalManager, err := chain.SpecChallengeManager(ctx) + Require(t, err) + chalManagerAddr := chalManager.Address() + seed, ok := new(big.Int).SetString("1000", 10) + if !ok { + t.Fatal("not ok") + } + value, ok := new(big.Int).SetString("10000", 10) + if !ok { + t.Fatal(t, "could not set value") + } + tokenBindings, err := mocksgen.NewTestWETH9(stakeToken, backend) + Require(t, err) + tx, err := tokenBindings.TestWETH9Transactor.Transfer(&l1TransactionOpts, asserter.From, seed) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, addresses.Rollup, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&asserter, chalManagerAddr, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + tx, err = tokenBindings.TestWETH9Transactor.Transfer(&l1TransactionOpts, evilAsserter.From, seed) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&evilAsserter, addresses.Rollup, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + tx, err = tokenBindings.TestWETH9Transactor.Approve(&evilAsserter, chalManagerAddr, value) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + return &chaininfo.RollupAddresses{ + Bridge: addresses.Bridge, + Inbox: addresses.Inbox, + SequencerInbox: addresses.SequencerInbox, + Rollup: addresses.Rollup, + ValidatorUtils: addresses.ValidatorUtils, + ValidatorWalletCreator: addresses.ValidatorWalletCreator, + DeployedAt: addresses.DeployedAt, + }, chain +} + +func create2ndNodeWithConfigForBoldProtocol( + t *testing.T, + ctx context.Context, + first *arbnode.Node, + l1stack *node.Node, + l1info *BlockchainTestInfo, + l2InitData *statetransfer.ArbosInitializationInfo, + nodeConfig *arbnode.Config, + stackConfig *node.Config, + stakeTokenAddr common.Address, +) (*ethclient.Client, *arbnode.Node, *solimpl.AssertionChain) { + fatalErrChan := make(chan error, 10) + l1rpcClient := l1stack.Attach() + l1client := ethclient.NewClient(l1rpcClient) + firstExec, ok := first.Execution.(*gethexec.ExecutionNode) + if !ok { + Fatal(t, "not geth execution node") + } + chainConfig := firstExec.ArbInterface.BlockChain().Config() + addresses, assertionChain := deployContractsOnly(t, ctx, l1info, l1client, chainConfig.ChainID, stakeTokenAddr) + + l1info.SetContract("EvilBridge", addresses.Bridge) + l1info.SetContract("EvilSequencerInbox", addresses.SequencerInbox) + l1info.SetContract("EvilInbox", addresses.Inbox) + + if nodeConfig == nil { + nodeConfig = arbnode.ConfigDefaultL1NonSequencerTest() + } + nodeConfig.ParentChainReader.OldHeaderTimeout = 10 * time.Minute + nodeConfig.BatchPoster.DataPoster.MaxMempoolTransactions = 0 + if stackConfig == nil { + stackConfig = createStackConfigForTest(t.TempDir()) + } + l2stack, err := node.New(stackConfig) + Require(t, err) + + l2chainDb, err := l2stack.OpenDatabase("chaindb", 0, 0, "", false) + Require(t, err) + l2arbDb, err := l2stack.OpenDatabase("arbdb", 0, 0, "", false) + Require(t, err) + + AddDefaultValNode(t, ctx, nodeConfig, true) + + dataSigner := signature.DataSignerFromPrivateKey(l1info.GetInfoWithPrivKey("Sequencer").PrivateKey) + txOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + initReader := statetransfer.NewMemoryInitDataReader(l2InitData) + initMessage := getInitMessage(ctx, t, l1client, first.DeployInfo) + + execConfig := gethexec.ConfigDefaultTest() + Require(t, execConfig.Validate()) + + l2blockchain, err := gethexec.WriteOrTestBlockChain(l2chainDb, nil, initReader, chainConfig, initMessage, execConfig.TxLookupLimit, 0) + Require(t, err) + + execConfigFetcher := func() *gethexec.Config { return execConfig } + execNode, err := gethexec.CreateExecutionNode(ctx, l2stack, l2chainDb, l2blockchain, l1client, execConfigFetcher) + Require(t, err) + l2node, err := arbnode.CreateNode(ctx, l2stack, execNode, l2arbDb, NewFetcherFromConfig(nodeConfig), l2blockchain.Config(), l1client, addresses, &txOpts, &txOpts, dataSigner, fatalErrChan) + Require(t, err) + + Require(t, l2node.Start(ctx)) + + l2client := ClientForStack(t, l2stack) + + StartWatchChanErr(t, ctx, fatalErrChan, l2node) + + return l2client, l2node, assertionChain +} + +func makeBoldBatch( + t *testing.T, + l2Node *arbnode.Node, + l2Info *BlockchainTestInfo, + backend *ethclient.Client, + sequencer *bind.TransactOpts, + seqInbox *bridgegen.SequencerInbox, + seqInboxAddr common.Address, + messagesPerBatch, + divergeAtIndex int64, +) { + ctx := context.Background() + + batchBuffer := bytes.NewBuffer([]byte{}) + for i := int64(0); i < messagesPerBatch; i++ { + value := i + if i == divergeAtIndex { + value++ + } + err := writeTxToBatchBold(batchBuffer, l2Info.PrepareTx("Owner", "Destination", 1000000, big.NewInt(value), []byte{})) + Require(t, err) + } + compressed, err := arbcompress.CompressWell(batchBuffer.Bytes()) + Require(t, err) + message := append([]byte{0}, compressed...) + + seqNum := new(big.Int).Lsh(common.Big1, 256) + seqNum.Sub(seqNum, common.Big1) + tx, err := seqInbox.AddSequencerL2BatchFromOrigin0(sequencer, seqNum, message, big.NewInt(1), common.Address{}, big.NewInt(0), big.NewInt(0)) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + nodeSeqInbox, err := arbnode.NewSequencerInbox(backend, seqInboxAddr, 0) + Require(t, err) + batches, err := nodeSeqInbox.LookupBatchesInRange(ctx, receipt.BlockNumber, receipt.BlockNumber) + Require(t, err) + if len(batches) == 0 { + Fatal(t, "batch not found after AddSequencerL2BatchFromOrigin") + } + err = l2Node.InboxTracker.AddSequencerBatches(ctx, backend, batches) + Require(t, err) + _, err = l2Node.InboxTracker.GetBatchMetadata(0) + Require(t, err, "failed to get batch metadata after adding batch:") +} + +func writeTxToBatchBold(writer io.Writer, tx *types.Transaction) error { + txData, err := tx.MarshalBinary() + if err != nil { + return err + } + var segment []byte + segment = append(segment, arbstate.BatchSegmentKindL2Message) + segment = append(segment, arbos.L2MessageKind_SignedTx) + segment = append(segment, txData...) + err = rlp.Encode(writer, segment) + return err +} diff --git a/system_tests/staker_test.go b/system_tests/staker_test.go index 6ac8dfddcb..2231f6dbd0 100644 --- a/system_tests/staker_test.go +++ b/system_tests/staker_test.go @@ -9,6 +9,7 @@ package arbtest import ( "context" + "encoding/json" "errors" "fmt" "math/big" @@ -22,12 +23,20 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + mocksgen_bold "github.com/OffchainLabs/bold/solgen/go/mocksgen" + rollupgen_bold "github.com/OffchainLabs/bold/solgen/go/rollupgen" + challenge_testing "github.com/OffchainLabs/bold/testing" + "github.com/OffchainLabs/bold/testing/setup" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbnode/dataposter/storage" "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" @@ -36,6 +45,7 @@ import ( "github.com/offchainlabs/nitro/util" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/colors" + "github.com/offchainlabs/nitro/validator/server_common" "github.com/offchainlabs/nitro/validator/valnode" ) @@ -190,6 +200,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) nil, nil, l2nodeA.DeployInfo.ValidatorUtils, + l2nodeA.DeployInfo.Bridge, nil, ) Require(t, err) @@ -230,6 +241,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) nil, nil, l2nodeB.DeployInfo.ValidatorUtils, + l2nodeB.DeployInfo.Bridge, nil, ) Require(t, err) @@ -251,6 +263,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) nil, nil, l2nodeA.DeployInfo.ValidatorUtils, + l2nodeA.DeployInfo.Bridge, nil, ) Require(t, err) @@ -429,3 +442,204 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool) func TestStakersCooperative(t *testing.T) { stakerTestImpl(t, false, false) } + +func TestStakerSwitchDuringRollupUpgrade(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + stakerImpl, builder := setupNonBoldStaker(t, ctx) + deployAuth := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx) + err := stakerImpl.Initialize(ctx) + Require(t, err) + stakerImpl.Start(ctx) + if stakerImpl.Stopped() { + t.Fatal("Old protocol staker not started") + } + + rollupAddresses := deployBoldContracts(t, ctx, builder.L1Info, builder.L1.Client, builder.chainConfig.ChainID, deployAuth) + + upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(builder.L2.ConsensusNode.DeployInfo.UpgradeExecutor, builder.L1.Client) + Require(t, err) + bridgeABI, err := abi.JSON(strings.NewReader(bridgegen.BridgeABI)) + Require(t, err) + + updateRollupAddressCalldata, err := bridgeABI.Pack("updateRollupAddress", rollupAddresses.Rollup) + Require(t, err) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, builder.L2.ConsensusNode.DeployInfo.Bridge, updateRollupAddressCalldata) + Require(t, err) + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + time.Sleep(time.Second) + + if !stakerImpl.Stopped() { + t.Fatal("Old protocol staker not stopped after rollup upgrade") + } +} + +func setupNonBoldStaker(t *testing.T, ctx context.Context) (*staker.Staker, *NodeBuilder) { + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder.L2Info = NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(builder.chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + builder.Build(t) + l2node := builder.L2.ConsensusNode + l1info := builder.L1Info + l1client := builder.L1.Client + + builder.BridgeBalance(t, "Faucet", big.NewInt(1).Mul(big.NewInt(params.Ether), big.NewInt(10000))) + + deployAuth := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + l1info.GenerateAccount("Validator") + TransferBalance(t, "Faucet", "Validator", balance, l1info, l1client, ctx) + l1auth := l1info.GetDefaultTransactOpts("Validator", ctx) + + upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(l2node.DeployInfo.UpgradeExecutor, builder.L1.Client) + Require(t, err) + rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) + Require(t, err) + + setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) + Require(t, err) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setMinAssertPeriodCalldata) + Require(t, err) + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l1client, tx) + Require(t, err) + valConfig := staker.DefaultL1ValidatorConfig + valConfig.Strategy = "WatchTower" + valConfig.Bold = staker.DefaultBoldConfig + valConfig.Bold.Enable = true + valConfig.StakerInterval = 100 * time.Millisecond + + dp, err := arbnode.StakerDataposter(ctx, rawdb.NewTable(l2node.ArbDB, storage.StakerPrefix), l2node.L1Reader, &l1auth, NewFetcherFromConfig(arbnode.ConfigDefaultL1NonSequencerTest()), nil) + if err != nil { + t.Fatalf("Error creating validator dataposter: %v", err) + } + valWallet, err := validatorwallet.NewContract(dp, nil, l2node.DeployInfo.ValidatorWalletCreator, l2node.DeployInfo.Rollup, l2node.L1Reader, &l1auth, 0, func(common.Address) {}, func() uint64 { return valConfig.ExtraGas }) + Require(t, err) + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + l2node.Execution, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = stateless.Start(ctx) + Require(t, err) + stakerImpl, err := staker.NewStaker( + l2node.L1Reader, + valWallet, + bind.CallOpts{}, + valConfig, + nil, + stateless, + nil, + nil, + l2node.DeployInfo.ValidatorUtils, + l2node.DeployInfo.Bridge, + nil, + ) + Require(t, err) + return stakerImpl, builder +} + +func deployBoldContracts( + t *testing.T, + ctx context.Context, + l1info info, + backend *ethclient.Client, + chainId *big.Int, + deployAuth bind.TransactOpts, +) *chaininfo.RollupAddresses { + stakeToken, tx, tokenBindings, err := mocksgen_bold.DeployTestWETH9( + &deployAuth, + backend, + "Weth", + "WETH", + ) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + value, _ := new(big.Int).SetString("1000000", 10) + deployAuth.Value = value + tx, err = tokenBindings.Deposit(&deployAuth) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + deployAuth.Value = nil + Require(t, err) + _, err = EnsureTxSucceeded(ctx, backend, tx) + Require(t, err) + + initialBalance := new(big.Int).Lsh(big.NewInt(1), 200) + l1info.GenerateGenesisAccount("deployer", initialBalance) + l1info.GenerateGenesisAccount("asserter", initialBalance) + l1info.GenerateGenesisAccount("sequencer", initialBalance) + SendWaitTestTransactions(t, ctx, backend, []*types.Transaction{ + l1info.PrepareTx("Faucet", "RollupOwner", 30000, initialBalance, nil)}) + l1TransactionOpts := l1info.GetDefaultTransactOpts("RollupOwner", ctx) + locator, err := server_common.NewMachineLocator("") + Require(t, err) + + miniStakeValues := []*big.Int{ + big.NewInt(1), + big.NewInt(2), + big.NewInt(3), + } + cfg := challenge_testing.GenerateRollupConfig( + false, + locator.LatestWasmModuleRoot(), + l1TransactionOpts.From, + chainId, + common.Address{}, + miniStakeValues, + stakeToken, + rollupgen_bold.ExecutionState{ + GlobalState: rollupgen_bold.GlobalState{}, + MachineStatus: 1, + }, + big.NewInt(0), + common.Address{}, + ) + config, err := json.Marshal(params.ArbitrumDevTestChainConfig()) + if err != nil { + return nil + } + cfg.ChainConfig = string(config) + + addresses, err := setup.DeployFullRollupStack( + ctx, + backend, + &l1TransactionOpts, + l1info.GetAddress("sequencer"), + cfg, + false, + false, + ) + Require(t, err) + + return &chaininfo.RollupAddresses{ + Bridge: addresses.Bridge, + Inbox: addresses.Inbox, + SequencerInbox: addresses.SequencerInbox, + Rollup: addresses.Rollup, + ValidatorUtils: addresses.ValidatorUtils, + ValidatorWalletCreator: addresses.ValidatorWalletCreator, + DeployedAt: addresses.DeployedAt, + } +} diff --git a/system_tests/state_provider_test.go b/system_tests/state_provider_test.go new file mode 100644 index 0000000000..aa2fc1475c --- /dev/null +++ b/system_tests/state_provider_test.go @@ -0,0 +1,316 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/offchainlabs/bold/blob/main/LICENSE + +// race detection makes things slow and miss timeouts +//go:build challengetest && !race + +package arbtest + +import ( + "context" + "errors" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/validator/valnode" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/containers/option" + l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" + "github.com/OffchainLabs/bold/solgen/go/bridgegen" + prefixproofs "github.com/OffchainLabs/bold/state-commitments/prefix-proofs" + mockmanager "github.com/OffchainLabs/bold/testing/mocks/state-provider" +) + +func TestStateProvider_BOLD_Bisections(t *testing.T) { + t.Parallel() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + l2node, l1info, l2info, l1stack, l1client, stateManager := setupBoldStateProvider(t, ctx) + defer requireClose(t, l1stack) + defer l2node.StopAndWait() + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + seqInbox := l1info.GetAddress("SequencerInbox") + seqInboxBinding, err := bridgegen.NewSequencerInbox(seqInbox, l1client) + Require(t, err) + + // We will make two batches, with 5 messages in each batch. + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) // No divergence. + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + numMessagesPerBatch = int64(10) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + totalMessageCount, err := l2node.InboxTracker.GetBatchMessageCount(totalBatches - 1) + Require(t, err) + + // Wait until the validator has validated the batches. + for { + if _, err := l2node.TxStreamer.ResultAtCount(totalMessageCount); err == nil { + break + } + } + + historyCommitter := l2stateprovider.NewHistoryCommitmentProvider( + stateManager, + stateManager, + stateManager, []l2stateprovider.Height{ + 1 << 5, + 1 << 5, + 1 << 5, + }, + stateManager, + ) + bisectionHeight := l2stateprovider.Height(16) + request := &l2stateprovider.HistoryCommitmentRequest{ + WasmModuleRoot: common.Hash{}, + FromBatch: 1, + ToBatch: 3, + UpperChallengeOriginHeights: []l2stateprovider.Height{}, + FromHeight: 0, + UpToHeight: option.Some(bisectionHeight), + } + bisectionCommitment, err := historyCommitter.HistoryCommitment(ctx, request) + Require(t, err) + + request.UpToHeight = option.None[l2stateprovider.Height]() + packedProof, err := historyCommitter.PrefixProof(ctx, request, bisectionHeight) + Require(t, err) + + data, err := mockmanager.ProofArgs.Unpack(packedProof) + Require(t, err) + preExpansion, ok := data[0].([][32]byte) + if !ok { + Fatal(t, "wrong type") + } + + hashes := make([]common.Hash, len(preExpansion)) + for i, h := range preExpansion { + hash := h + hashes[i] = hash + } + + computed, err := prefixproofs.Root(hashes) + Require(t, err) + if computed != bisectionCommitment.Merkle { + Fatal(t, "wrong commitment") + } +} + +func TestStateProvider_BOLD(t *testing.T) { + t.Parallel() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + l2node, l1info, l2info, l1stack, l1client, stateManager := setupBoldStateProvider(t, ctx) + defer requireClose(t, l1stack) + defer l2node.StopAndWait() + l2info.GenerateAccount("Destination") + sequencerTxOpts := l1info.GetDefaultTransactOpts("Sequencer", ctx) + + seqInbox := l1info.GetAddress("SequencerInbox") + seqInboxBinding, err := bridgegen.NewSequencerInbox(seqInbox, l1client) + Require(t, err) + + // We will make two batches, with 5 messages in each batch. + numMessagesPerBatch := int64(5) + divergeAt := int64(-1) // No divergence. + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + makeBoldBatch(t, l2node, l2info, l1client, &sequencerTxOpts, seqInboxBinding, seqInbox, numMessagesPerBatch, divergeAt) + + bridgeBinding, err := bridgegen.NewBridge(l1info.GetAddress("Bridge"), l1client) + Require(t, err) + totalBatchesBig, err := bridgeBinding.SequencerMessageCount(&bind.CallOpts{Context: ctx}) + Require(t, err) + totalBatches := totalBatchesBig.Uint64() + totalMessageCount, err := l2node.InboxTracker.GetBatchMessageCount(totalBatches - 1) + Require(t, err) + + // Wait until the validator has validated the batches. + for { + if _, err := l2node.TxStreamer.ResultAtCount(totalMessageCount); err == nil { + break + } + } + + t.Run("StatesInBatchRange", func(t *testing.T) { + fromBatch := l2stateprovider.Batch(1) + toBatch := l2stateprovider.Batch(3) + fromHeight := l2stateprovider.Height(0) + toHeight := l2stateprovider.Height(14) + stateRoots, err := stateManager.StatesInBatchRange(fromHeight, toHeight, fromBatch, toBatch) + Require(t, err) + + if stateRoots.Length() != 15 { + Fatal(t, "wrong number of state roots") + } + firstState := states[0] + if firstState.Batch != 1 && firstState.PosInBatch != 0 { + Fatal(t, "wrong first state") + } + lastState := states[len(states)-1] + if lastState.Batch != 1 && lastState.PosInBatch != 0 { + Fatal(t, "wrong last state") + } + }) + t.Run("AgreesWithExecutionState", func(t *testing.T) { + // Non-zero position in batch shoould fail. + err = stateManager.AgreesWithExecutionState(ctx, &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState{ + Batch: 0, + PosInBatch: 1, + }, + MachineStatus: protocol.MachineStatusFinished, + }) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !strings.Contains(err.Error(), "position in batch must be zero") { + Fatal(t, "wrong error message") + } + + // Always agrees with genesis. + err = stateManager.AgreesWithExecutionState(ctx, &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState{ + Batch: 0, + PosInBatch: 0, + }, + MachineStatus: protocol.MachineStatusFinished, + }) + Require(t, err) + + // Always agrees with the init message. + err = stateManager.AgreesWithExecutionState(ctx, &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState{ + Batch: 1, + PosInBatch: 0, + }, + MachineStatus: protocol.MachineStatusFinished, + }) + Require(t, err) + + // Chain catching up if it has not seen batch 10. + err = stateManager.AgreesWithExecutionState(ctx, &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState{ + Batch: 10, + PosInBatch: 0, + }, + MachineStatus: protocol.MachineStatusFinished, + }) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !errors.Is(err, staker.ErrChainCatchingUp) { + Fatal(t, "wrong error") + } + + // Check if we agree with the last posted batch to the inbox. + result, err := l2node.TxStreamer.ResultAtCount(totalMessageCount) + Require(t, err) + + state := &protocol.ExecutionState{ + GlobalState: protocol.GoGlobalState{ + BlockHash: result.BlockHash, + SendRoot: result.SendRoot, + Batch: 3, + PosInBatch: 0, + }, + MachineStatus: protocol.MachineStatusFinished, + } + err = stateManager.AgreesWithExecutionState(ctx, state) + Require(t, err) + + // See if we agree with one batch immediately after that and see that we fail with + // "ErrChainCatchingUp". + state.GlobalState.Batch += 1 + + err = stateManager.AgreesWithExecutionState(ctx, state) + if err == nil { + Fatal(t, "should not agree with execution state") + } + if !errors.Is(err, staker.ErrChainCatchingUp) { + Fatal(t, "wrong error") + } + }) + t.Run("ExecutionStateAfterBatchCount", func(t *testing.T) { + _, err = stateManager.ExecutionStateAfterBatchCount(ctx, 0) + if err == nil { + Fatal(t, "should have failed") + } + if !strings.Contains(err.Error(), "batch count cannot be zero") { + Fatal(t, "wrong error message") + } + + execState, err := stateManager.ExecutionStateAfterBatchCount(ctx, totalBatches) + Require(t, err) + + // We should agree with the last posted batch to the inbox based on our + // retrieved execution state. + err = stateManager.AgreesWithExecutionState(ctx, execState) + Require(t, err) + }) +} + +func setupBoldStateProvider(t *testing.T, ctx context.Context) (*arbnode.Node, *BlockchainTestInfo, *BlockchainTestInfo, *node.Node, *ethclient.Client, *staker.StateManager) { + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + l2chainConfig := params.ArbitrumDevTestChainConfig() + l2info := NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(l2chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + ownerBal := big.NewInt(params.Ether) + ownerBal.Mul(ownerBal, big.NewInt(1_000_000)) + l2info.GenerateGenesisAccount("Owner", ownerBal) + + _, l2node, _, _, l1info, _, l1client, l1stack, _, _ := createTestNodeOnL1ForBoldProtocol(t, ctx, true, nil, l2chainConfig, nil, l2info) + + valnode.TestValidationConfig.UseJit = false + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + l2node.Execution, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = stateless.Start(ctx) + Require(t, err) + + stateManager, err := staker.NewStateManager( + stateless, + "", + []l2stateprovider.Height{ + l2stateprovider.Height(blockChallengeLeafHeight), + l2stateprovider.Height(bigStepChallengeLeafHeight), + l2stateprovider.Height(smallStepChallengeLeafHeight), + }, + "", + ) + Require(t, err) + return l2node, l1info, l2info, l1stack, l1client, stateManager +} diff --git a/system_tests/validation_mock_test.go b/system_tests/validation_mock_test.go index d9c302b33f..1c2c06bad9 100644 --- a/system_tests/validation_mock_test.go +++ b/system_tests/validation_mock_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbutil" @@ -85,6 +86,10 @@ func (s *mockSpawner) WriteToFile(input *validator.ValidationInput, expOut valid return containers.NewReadyPromise[struct{}](struct{}{}, nil) } +func (s *mockSpawner) CreateBoldExecutionRun(wasmModuleRoot common.Hash, stepSize uint64, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { + return containers.NewReadyPromise[validator.ExecutionRun](nil, nil) +} + type mockValRun struct { containers.Promise[validator.GoGlobalState] root common.Hash @@ -116,6 +121,11 @@ func (r *mockExecRun) GetStepAt(position uint64) containers.PromiseInterface[*va }, nil) } +func (r *mockExecRun) GetLeavesWithStepSize(machineStartIndex, stepSize, numDesiredLeaves uint64, claimId common.Hash) containers.PromiseInterface[[]common.Hash] { + // TODO: Add mock implementation for GetLeavesWithStepSize + return containers.NewReadyPromise[[]common.Hash](nil, nil) +} + func (r *mockExecRun) GetLastStep() containers.PromiseInterface[*validator.MachineStepResult] { return r.GetStepAt(mockExecLastPos) } @@ -130,6 +140,10 @@ func (r *mockExecRun) PrepareRange(uint64, uint64) containers.PromiseInterface[s return containers.NewReadyPromise[struct{}](struct{}{}, nil) } +func (r *mockExecRun) CheckAlive(ctx context.Context) error { + return nil +} + func (r *mockExecRun) Close() {} func createMockValidationNode(t *testing.T, ctx context.Context, config *server_arb.ArbitratorSpawnerConfig) (*mockSpawner, *node.Node) { diff --git a/validator/execution_state.go b/validator/execution_state.go index 092fbe2908..a7e4480027 100644 --- a/validator/execution_state.go +++ b/validator/execution_state.go @@ -18,6 +18,13 @@ type GoGlobalState struct { PosInBatch uint64 } +func (g GoGlobalState) String() string { + return fmt.Sprintf( + "BlockHash: %s, SendRoot: %s, Batch: %d, PosInBatch: %d", + g.BlockHash.Hex(), g.SendRoot.Hex(), g.Batch, g.PosInBatch, + ) +} + type MachineStatus uint8 const ( diff --git a/validator/interface.go b/validator/interface.go index 5785ac4de1..eae29b4217 100644 --- a/validator/interface.go +++ b/validator/interface.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/util/containers" ) @@ -23,14 +24,17 @@ type ValidationRun interface { type ExecutionSpawner interface { ValidationSpawner CreateExecutionRun(wasmModuleRoot common.Hash, input *ValidationInput) containers.PromiseInterface[ExecutionRun] + CreateBoldExecutionRun(wasmModuleRoot common.Hash, stepSize uint64, input *ValidationInput) containers.PromiseInterface[ExecutionRun] LatestWasmModuleRoot() containers.PromiseInterface[common.Hash] WriteToFile(input *ValidationInput, expOut GoGlobalState, moduleRoot common.Hash) containers.PromiseInterface[struct{}] } type ExecutionRun interface { GetStepAt(uint64) containers.PromiseInterface[*MachineStepResult] + GetLeavesWithStepSize(fromBatch, machineStartIndex, stepSize, numDesiredLeaves uint64) containers.PromiseInterface[[]common.Hash] GetLastStep() containers.PromiseInterface[*MachineStepResult] GetProofAt(uint64) containers.PromiseInterface[[]byte] PrepareRange(uint64, uint64) containers.PromiseInterface[struct{}] Close() + CheckAlive(ctx context.Context) error } diff --git a/validator/server_api/valiation_api.go b/validator/server_api/valiation_api.go index ca5aafcee2..c22eb5e781 100644 --- a/validator/server_api/valiation_api.go +++ b/validator/server_api/valiation_api.go @@ -69,6 +69,23 @@ func NewExecutionServerAPI(valSpawner validator.ValidationSpawner, execution val } } +func (a *ExecServerAPI) CreateBoldExecutionRun(ctx context.Context, wasmModuleRoot common.Hash, stepSize uint64, jsonInput *ValidationInputJson) (uint64, error) { + input, err := ValidationInputFromJson(jsonInput) + if err != nil { + return 0, err + } + execRun, err := a.execSpawner.CreateBoldExecutionRun(wasmModuleRoot, stepSize, input).Await(ctx) + if err != nil { + return 0, err + } + a.runIdLock.Lock() + defer a.runIdLock.Unlock() + newId := a.nextId + a.nextId++ + a.runs[newId] = &execRunEntry{execRun, time.Now()} + return newId, nil +} + func (a *ExecServerAPI) CreateExecutionRun(ctx context.Context, wasmModuleRoot common.Hash, jsonInput *ValidationInputJson) (uint64, error) { input, err := ValidationInputFromJson(jsonInput) if err != nil { @@ -142,6 +159,19 @@ func (a *ExecServerAPI) GetStepAt(ctx context.Context, execid uint64, position u return MachineStepResultToJson(res), nil } +func (a *ExecServerAPI) GetLeavesWithStepSize(ctx context.Context, execid, fromBatch, fromStep, stepSize, numDesiredLeaves uint64) ([]common.Hash, error) { + run, err := a.getRun(execid) + if err != nil { + return nil, err + } + leavesInRange := run.GetLeavesWithStepSize(fromBatch, fromStep, stepSize, numDesiredLeaves) + res, err := leavesInRange.Await(ctx) + if err != nil { + return nil, err + } + return res, nil +} + func (a *ExecServerAPI) GetProofAt(ctx context.Context, execid uint64, position uint64) (string, error) { run, err := a.getRun(execid) if err != nil { @@ -172,6 +202,14 @@ func (a *ExecServerAPI) ExecKeepAlive(ctx context.Context, execid uint64) error return nil } +func (a *ExecServerAPI) CheckAlive(ctx context.Context, execid uint64) error { + run, err := a.getRun(execid) + if err != nil { + return err + } + return run.CheckAlive(ctx) +} + func (a *ExecServerAPI) CloseExec(execid uint64) { a.runIdLock.Lock() defer a.runIdLock.Unlock() diff --git a/validator/server_api/validation_client.go b/validator/server_api/validation_client.go index d6143ca917..a6382dd2f3 100644 --- a/validator/server_api/validation_client.go +++ b/validator/server_api/validation_client.go @@ -7,12 +7,10 @@ import ( "sync/atomic" "time" - "github.com/offchainlabs/nitro/validator" - "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/util/stopwaiter" - + "github.com/offchainlabs/nitro/validator" "github.com/offchainlabs/nitro/validator/server_common" "github.com/ethereum/go-ethereum/common" @@ -108,6 +106,22 @@ func NewExecutionClient(config rpcclient.ClientConfigFetcher, stack *node.Node) } } +func (c *ExecutionClient) CreateBoldExecutionRun(wasmModuleRoot common.Hash, stepSize uint64, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { + return stopwaiter.LaunchPromiseThread[validator.ExecutionRun](c, func(ctx context.Context) (validator.ExecutionRun, error) { + var res uint64 + err := c.client.CallContext(ctx, &res, Namespace+"_createBoldExecutionRun", wasmModuleRoot, stepSize, ValidationInputToJson(input)) + if err != nil { + return nil, err + } + run := &ExecutionClientRun{ + client: c, + id: res, + } + run.Start(c.GetContext()) // note: not this temporary thread's context! + return run, nil + }) +} + func (c *ExecutionClient) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { return stopwaiter.LaunchPromiseThread[validator.ExecutionRun](c, func(ctx context.Context) (validator.ExecutionRun, error) { var res uint64 @@ -157,6 +171,10 @@ func (r *ExecutionClientRun) SendKeepAlive(ctx context.Context) time.Duration { return time.Minute // TODO: configurable } +func (r *ExecutionClientRun) CheckAlive(ctx context.Context) error { + return r.client.client.CallContext(ctx, nil, Namespace+"_checkAlive", r.id) +} + func (r *ExecutionClientRun) Start(ctx_in context.Context) { r.StopWaiter.Start(ctx_in, r) r.CallIteratively(r.SendKeepAlive) @@ -177,6 +195,17 @@ func (r *ExecutionClientRun) GetStepAt(pos uint64) containers.PromiseInterface[* }) } +func (r *ExecutionClientRun) GetLeavesWithStepSize(fromBatch, machineStartIndex, stepSize, numDesiredLeaves uint64) containers.PromiseInterface[[]common.Hash] { + return stopwaiter.LaunchPromiseThread[[]common.Hash](r, func(ctx context.Context) ([]common.Hash, error) { + var resJson []common.Hash + err := r.client.client.CallContext(ctx, &resJson, Namespace+"_getLeavesWithStepSize", r.id, fromBatch, machineStartIndex, stepSize, numDesiredLeaves) + if err != nil { + return nil, err + } + return resJson, err + }) +} + func (r *ExecutionClientRun) GetProofAt(pos uint64) containers.PromiseInterface[[]byte] { return stopwaiter.LaunchPromiseThread[[]byte](r, func(ctx context.Context) ([]byte, error) { var resString string diff --git a/validator/server_arb/execution_run.go b/validator/server_arb/execution_run.go index 255d42ab16..f652cf8745 100644 --- a/validator/server_arb/execution_run.go +++ b/validator/server_arb/execution_run.go @@ -7,27 +7,37 @@ import ( "context" "fmt" "sync" + "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/util/containers" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" + "github.com/offchainlabs/nitro/validator/server_common" ) type executionRun struct { stopwaiter.StopWaiter - cache *MachineCache - close sync.Once + cache *MachineCache + initialMachineGetter func(context.Context, ...server_common.MachineLoaderOpt) (MachineInterface, error) + config *MachineCacheConfig + close sync.Once } // NewExecutionChallengeBackend creates a backend with the given arguments. // Note: machineCache may be nil, but if present, it must not have a restricted range. func NewExecutionRun( ctxIn context.Context, - initialMachineGetter func(context.Context) (MachineInterface, error), + initialMachineGetter func(context.Context, ...server_common.MachineLoaderOpt) (MachineInterface, error), config *MachineCacheConfig, ) (*executionRun, error) { exec := &executionRun{} exec.Start(ctxIn, exec) + exec.initialMachineGetter = initialMachineGetter + exec.config = config exec.cache = NewMachineCache(exec.GetContext(), initialMachineGetter, config) return exec, nil } @@ -50,35 +60,151 @@ func (e *executionRun) PrepareRange(start uint64, end uint64) containers.Promise func (e *executionRun) GetStepAt(position uint64) containers.PromiseInterface[*validator.MachineStepResult] { return stopwaiter.LaunchPromiseThread[*validator.MachineStepResult](e, func(ctx context.Context) (*validator.MachineStepResult, error) { - var machine MachineInterface - var err error - if position == ^uint64(0) { - machine, err = e.cache.GetFinalMachine(ctx) - } else { - // todo cache last machine - machine, err = e.cache.GetMachineAt(ctx, position) + return e.intermediateGetStepAt(ctx, position) + }) +} + +func (e *executionRun) GetLeavesWithStepSize(fromBatch, machineStartIndex, stepSize, numDesiredLeaves uint64) containers.PromiseInterface[[]common.Hash] { + return stopwaiter.LaunchPromiseThread[[]common.Hash](e, func(ctx context.Context) ([]common.Hash, error) { + if stepSize == 1 { + e.cache = NewMachineCache(e.GetContext(), e.initialMachineGetter, e.config, server_common.WithAlwaysMerkleize()) + log.Info("Enabling Merkleization of machines for faster hashing. However, advancing to start index might take a while...") } + log.Info(fmt.Sprintf("Starting BOLD machine computation at index %d", machineStartIndex)) + machine, err := e.cache.GetMachineAt(ctx, machineStartIndex) if err != nil { return nil, err } - machineStep := machine.GetStepCount() - if position != machineStep { - machineRunning := machine.IsRunning() - if machineRunning || machineStep > position { - return nil, fmt.Errorf("machine is in wrong position want: %d, got: %d", position, machine.GetStepCount()) + log.Info(fmt.Sprintf("Advanced machine to index %d, beginning hash computation", machineStartIndex)) + // If the machine is starting at index 0, we always want to start at the "Machine finished" global state status + // to align with the state roots that the inbox machine will produce. + var stateRoots []common.Hash + + if machineStartIndex == 0 { + gs := machine.GetGlobalState() + log.Info(fmt.Sprintf("Start global state for machine index 0: %+v", gs), log.Ctx{ + "fromBatch": fromBatch, + }) + hash := crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes()) + stateRoots = append(stateRoots, hash) + } else { + // Otherwise, we simply append the machine hash at the specified start index. + stateRoots = append(stateRoots, machine.Hash()) + } + startHash := stateRoots[0] + + // If we only want 1 state root, we can return early. + if numDesiredLeaves == 1 { + return stateRoots, nil + } + + logInterval := numDesiredLeaves / 20 // Log every 5% progress + if logInterval == 0 { + logInterval = 1 + } + + start := time.Now() + for numIterations := uint64(0); numIterations < numDesiredLeaves; numIterations++ { + // The absolute opcode position the machine should be in after stepping. + position := machineStartIndex + stepSize*(numIterations+1) + + // Advance the machine in step size increments. + if err := machine.Step(ctx, stepSize); err != nil { + return nil, fmt.Errorf("failed to step machine to position %d: %w", position, err) + } + if numIterations%logInterval == 0 || numIterations == numDesiredLeaves-1 { + progressPercent := (float64(numIterations+1) / float64(numDesiredLeaves)) * 100 + log.Info( + fmt.Sprintf( + "Subchallenge machine hash progress: %.2f%% - %d of %d leaves computed", + progressPercent, + numIterations+1, + numDesiredLeaves, + ), + log.Ctx{ + "fromBatch": fromBatch, + "machinePosition": numIterations*stepSize + machineStartIndex, + "timeSinceStart": time.Since(start), + "stepSize": stepSize, + "startHash": startHash, + "machineStartIndex": machineStartIndex, + "numDesiredLeaves": numDesiredLeaves, + }, + ) + } + + // If the machine reached the finished state, we can break out of the loop and append to + // our state roots slice a finished machine hash. + machineStep := machine.GetStepCount() + if validator.MachineStatus(machine.Status()) == validator.MachineStatusFinished { + gs := machine.GetGlobalState() + hash := crypto.Keccak256Hash([]byte("Machine finished:"), gs.Hash().Bytes()) + stateRoots = append(stateRoots, hash) + break + } + // Otherwise, if the position and machine step mismatch and the machine is running, something went wrong. + if position != machineStep { + machineRunning := machine.IsRunning() + if machineRunning || machineStep > position { + return nil, fmt.Errorf("machine is in wrong position want: %d, got: %d", position, machineStep) + } } + stateRoots = append(stateRoots, machine.Hash()) } - result := &validator.MachineStepResult{ - Position: machineStep, - Status: validator.MachineStatus(machine.Status()), - GlobalState: machine.GetGlobalState(), - Hash: machine.Hash(), + log.Info( + "Machine finished execution, gathered all the necessary hashes", + log.Ctx{ + "fromBatch": fromBatch, + "stepSize": stepSize, + "startHash": startHash, + "machineStartIndex": machineStartIndex, + "numDesiredLeaves": numDesiredLeaves, + "finishedHash": stateRoots[len(stateRoots)-1], + "finishedGlobalState": fmt.Sprintf("%+v", machine.GetGlobalState()), + }, + ) + + // If the machine finished in less than the number of hashes we anticipate, we pad + // to the expected value by repeating the last machine hash until the state roots are the correct + // length. + lastStateRoot := stateRoots[len(stateRoots)-1] + for len(stateRoots) < int(numDesiredLeaves) { + stateRoots = append(stateRoots, lastStateRoot) } - return result, nil + return stateRoots[:numDesiredLeaves], nil }) } +func (e *executionRun) intermediateGetStepAt(ctx context.Context, position uint64) (*validator.MachineStepResult, error) { + var machine MachineInterface + var err error + if position == ^uint64(0) { + machine, err = e.cache.GetFinalMachine(ctx) + } else { + // todo cache last machina + machine, err = e.cache.GetMachineAt(ctx, position) + } + if err != nil { + return nil, err + } + machineStep := machine.GetStepCount() + if position != machineStep { + machineRunning := machine.IsRunning() + if machineRunning || machineStep > position { + return nil, fmt.Errorf("machine is in wrong position want: %d, got: %d", position, machine.GetStepCount()) + } + + } + result := &validator.MachineStepResult{ + Position: machineStep, + Status: validator.MachineStatus(machine.Status()), + GlobalState: machine.GetGlobalState(), + Hash: machine.Hash(), + } + return result, nil +} + func (e *executionRun) GetProofAt(position uint64) containers.PromiseInterface[[]byte] { return stopwaiter.LaunchPromiseThread[[]byte](e, func(ctx context.Context) ([]byte, error) { machine, err := e.cache.GetMachineAt(ctx, position) @@ -92,3 +218,7 @@ func (e *executionRun) GetProofAt(position uint64) containers.PromiseInterface[[ func (e *executionRun) GetLastStep() containers.PromiseInterface[*validator.MachineStepResult] { return e.GetStepAt(^uint64(0)) } + +func (e *executionRun) CheckAlive(ctx context.Context) error { + return nil +} diff --git a/validator/server_arb/machine_cache.go b/validator/server_arb/machine_cache.go index 23fcdef6d6..9373aaac0d 100644 --- a/validator/server_arb/machine_cache.go +++ b/validator/server_arb/machine_cache.go @@ -9,6 +9,7 @@ import ( "fmt" "sync" + "github.com/offchainlabs/nitro/validator/server_common" flag "github.com/spf13/pflag" ) @@ -46,13 +47,13 @@ func MachineCacheConfigConfigAddOptions(prefix string, f *flag.FlagSet) { } // `initialMachine` won't be mutated by this function. -func NewMachineCache(ctx context.Context, initialMachineGetter func(context.Context) (MachineInterface, error), config *MachineCacheConfig) *MachineCache { +func NewMachineCache(ctx context.Context, initialMachineGetter func(context.Context, ...server_common.MachineLoaderOpt) (MachineInterface, error), config *MachineCacheConfig, opts ...server_common.MachineLoaderOpt) *MachineCache { cache := &MachineCache{ buildingLock: make(chan struct{}, 1), // locked on init config: config, } go func() { - zeroStepMachine, err := initialMachineGetter(ctx) + zeroStepMachine, err := initialMachineGetter(ctx, opts...) if err == nil && zeroStepMachine.GetStepCount() != 0 { zeroStepMachine.Destroy() err = errors.New("initialMachine not at step count 0") diff --git a/validator/server_arb/machine_loader.go b/validator/server_arb/machine_loader.go index 13cf0f2403..c69d90adbc 100644 --- a/validator/server_arb/machine_loader.go +++ b/validator/server_arb/machine_loader.go @@ -27,8 +27,8 @@ type ArbMachineLoader struct { } func NewArbMachineLoader(config *ArbitratorMachineConfig, locator *server_common.MachineLocator) *ArbMachineLoader { - createMachineFunc := func(ctx context.Context, moduleRoot common.Hash) (*arbMachines, error) { - return createArbMachine(ctx, locator, config, moduleRoot) + createMachineFunc := func(ctx context.Context, moduleRoot common.Hash, opts ...server_common.MachineLoaderOpt) (*arbMachines, error) { + return createArbMachine(ctx, locator, config, moduleRoot, opts...) } return &ArbMachineLoader{ MachineLoader: *server_common.NewMachineLoader[arbMachines](locator, createMachineFunc), @@ -43,8 +43,8 @@ func (a *ArbMachineLoader) GetHostIoMachine(ctx context.Context, moduleRoot comm return machines.hostIo, nil } -func (a *ArbMachineLoader) GetZeroStepMachine(ctx context.Context, moduleRoot common.Hash) (*ArbitratorMachine, error) { - machines, err := a.GetMachine(ctx, moduleRoot) +func (a *ArbMachineLoader) GetZeroStepMachine(ctx context.Context, moduleRoot common.Hash, opts ...server_common.MachineLoaderOpt) (*ArbitratorMachine, error) { + machines, err := a.GetMachine(ctx, moduleRoot, opts...) if err != nil { return nil, err } diff --git a/validator/server_arb/nitro_machine.go b/validator/server_arb/nitro_machine.go index acaf3b10e6..ea5a739fa8 100644 --- a/validator/server_arb/nitro_machine.go +++ b/validator/server_arb/nitro_machine.go @@ -22,12 +22,21 @@ import ( "github.com/offchainlabs/nitro/validator/server_common" ) -func createArbMachine(ctx context.Context, locator *server_common.MachineLocator, config *ArbitratorMachineConfig, moduleRoot common.Hash) (*arbMachines, error) { +func createArbMachine(ctx context.Context, locator *server_common.MachineLocator, config *ArbitratorMachineConfig, moduleRoot common.Hash, opts ...server_common.MachineLoaderOpt) (*arbMachines, error) { + loaderCfg := &server_common.MachineLoaderCfg{} + for _, o := range opts { + o(loaderCfg) + } binPath := filepath.Join(locator.GetMachinePath(moduleRoot), config.WavmBinaryPath) cBinPath := C.CString(binPath) defer C.free(unsafe.Pointer(cBinPath)) - log.Info("creating nitro machine", "binpath", binPath) - baseMachine := C.arbitrator_load_wavm_binary(cBinPath) + + log.Info("creating nitro machine", "binpath", binPath, "alwaysMerkleize", loaderCfg.ShouldAlwaysMerkleize()) + shouldMerkleize := C.uint8_t(0) + if loaderCfg.ShouldAlwaysMerkleize() { + shouldMerkleize = C.uint8_t(1) + } + baseMachine := C.arbitrator_load_wavm_binary(cBinPath, shouldMerkleize) if baseMachine == nil { return nil, errors.New("failed to load base machine") } diff --git a/validator/server_arb/validator_spawner.go b/validator/server_arb/validator_spawner.go index ab04942871..97aea5505d 100644 --- a/validator/server_arb/validator_spawner.go +++ b/validator/server_arb/validator_spawner.go @@ -304,8 +304,36 @@ func (v *ArbitratorSpawner) WriteToFile(input *validator.ValidationInput, expOut } func (v *ArbitratorSpawner) CreateExecutionRun(wasmModuleRoot common.Hash, input *validator.ValidationInput) containers.PromiseInterface[validator.ExecutionRun] { - getMachine := func(ctx context.Context) (MachineInterface, error) { - initialFrozenMachine, err := v.machineLoader.GetZeroStepMachine(ctx, wasmModuleRoot) + getMachine := func(ctx context.Context, opts ...server_common.MachineLoaderOpt) (MachineInterface, error) { + initialFrozenMachine, err := v.machineLoader.GetZeroStepMachine(ctx, wasmModuleRoot, opts...) + if err != nil { + return nil, err + } + machine := initialFrozenMachine.Clone() + err = v.loadEntryToMachine(ctx, input, machine) + if err != nil { + machine.Destroy() + return nil, err + } + return machine, nil + } + currentExecConfig := v.config().Execution + return stopwaiter.LaunchPromiseThread[validator.ExecutionRun](v, func(ctx context.Context) (validator.ExecutionRun, error) { + return NewExecutionRun(v.GetContext(), getMachine, ¤tExecConfig) + }) +} + +func (v *ArbitratorSpawner) CreateBoldExecutionRun( + wasmModuleRoot common.Hash, stepSize uint64, input *validator.ValidationInput, +) containers.PromiseInterface[validator.ExecutionRun] { + getMachine := func(ctx context.Context, opts ...server_common.MachineLoaderOpt) (MachineInterface, error) { + // // Pass in step size. + // log.Info(fmt.Sprintf("Creating bold execution run closure with opts: %d", len(opts))) + // if len(opts) > 0 { + // v.machineLoader = NewArbMachineLoader(&DefaultArbitratorMachineConfig, v.locator) + // log.Info("Updated machine loader for re-cache") + // } + initialFrozenMachine, err := v.machineLoader.GetZeroStepMachine(ctx, wasmModuleRoot, opts...) if err != nil { return nil, err } diff --git a/validator/server_common/machine_loader.go b/validator/server_common/machine_loader.go index f4633ebedf..38b6315c5b 100644 --- a/validator/server_common/machine_loader.go +++ b/validator/server_common/machine_loader.go @@ -2,9 +2,11 @@ package server_common import ( "context" + "fmt" "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/util/containers" ) @@ -22,14 +24,13 @@ type MachineLoader[M any] struct { mapMutex sync.Mutex machines map[common.Hash]*MachineStatus[M] locator *MachineLocator - createMachine func(ctx context.Context, moduleRoot common.Hash) (*M, error) + createMachine func(ctx context.Context, moduleRoot common.Hash, opts ...MachineLoaderOpt) (*M, error) } func NewMachineLoader[M any]( locator *MachineLocator, - createMachine func(ctx context.Context, moduleRoot common.Hash) (*M, error), + createMachine func(ctx context.Context, moduleRoot common.Hash, opts ...MachineLoaderOpt) (*M, error), ) *MachineLoader[M] { - return &MachineLoader[M]{ machines: make(map[common.Hash]*MachineStatus[M]), locator: locator, @@ -37,7 +38,23 @@ func NewMachineLoader[M any]( } } -func (l *MachineLoader[M]) GetMachine(ctx context.Context, moduleRoot common.Hash) (*M, error) { +type MachineLoaderCfg struct { + alwaysMerkleize bool +} + +func (m *MachineLoaderCfg) ShouldAlwaysMerkleize() bool { + return m.alwaysMerkleize +} + +type MachineLoaderOpt = func(cfg *MachineLoaderCfg) + +func WithAlwaysMerkleize() MachineLoaderOpt { + return func(cfg *MachineLoaderCfg) { + cfg.alwaysMerkleize = true + } +} + +func (l *MachineLoader[M]) GetMachine(ctx context.Context, moduleRoot common.Hash, opts ...MachineLoaderOpt) (*M, error) { if moduleRoot == (common.Hash{}) { moduleRoot = l.locator.LatestWasmModuleRoot() if (moduleRoot == common.Hash{}) { @@ -50,7 +67,8 @@ func (l *MachineLoader[M]) GetMachine(ctx context.Context, moduleRoot common.Has status = newMachineStatus[M]() l.machines[moduleRoot] = status go func() { - machine, err := l.createMachine(context.Background(), moduleRoot) + log.Info(fmt.Sprintf("In machine loader, calling create machine with opts %d", len(opts))) + machine, err := l.createMachine(context.Background(), moduleRoot, opts...) if err != nil { status.ProduceError(err) return diff --git a/validator/server_jit/machine_loader.go b/validator/server_jit/machine_loader.go index 5705a9a387..2376e1f0e1 100644 --- a/validator/server_jit/machine_loader.go +++ b/validator/server_jit/machine_loader.go @@ -55,7 +55,7 @@ func NewJitMachineLoader(config *JitMachineConfig, locator *server_common.Machin if err != nil { return nil, err } - createMachineThreadFunc := func(ctx context.Context, moduleRoot common.Hash) (*JitMachine, error) { + createMachineThreadFunc := func(ctx context.Context, moduleRoot common.Hash, opts ...server_common.MachineLoaderOpt) (*JitMachine, error) { binPath := filepath.Join(locator.GetMachinePath(moduleRoot), config.ProverBinPath) return createJitMachine(jitPath, binPath, config.JitCranelift, moduleRoot, fatalErrChan) } diff --git a/validator/validation_entry.go b/validator/validation_entry.go index fed1940f1f..46213eb1ac 100644 --- a/validator/validation_entry.go +++ b/validator/validation_entry.go @@ -1,6 +1,9 @@ package validator import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/offchainlabs/nitro/arbutil" ) @@ -19,3 +22,34 @@ type ValidationInput struct { DelayedMsg []byte StartState GoGlobalState } + +func (b BatchInfo) String() string { + return fmt.Sprintf("Number: %d, Data: %x", b.Number, b.Data) +} + +func (v *ValidationInput) String() string { + var buf bytes.Buffer + + buf.WriteString(fmt.Sprintf("Id: %d\n", v.Id)) + buf.WriteString(fmt.Sprintf("HasDelayedMsg: %v\n", v.HasDelayedMsg)) + buf.WriteString(fmt.Sprintf("DelayedMsgNr: %d\n", v.DelayedMsgNr)) + + // Preimages + buf.WriteString("Preimages:\n") + for t, pmap := range v.Preimages { + for h, data := range pmap { + buf.WriteString(fmt.Sprintf("\tType: %d, Hash: %s, Data: %x\n", t, h.Hex(), data)) + } + } + + // BatchInfo + buf.WriteString("BatchInfo:\n") + for _, bi := range v.BatchInfo { + buf.WriteString(fmt.Sprintf("\t%s\n", bi)) + } + + buf.WriteString(fmt.Sprintf("DelayedMsg: %x\n", v.DelayedMsg)) + buf.WriteString(fmt.Sprintf("StartState: %s\n", v.StartState)) + + return buf.String() +}