diff --git a/.dockerignore b/.dockerignore index e142afd073..51424900e8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ go-ethereum/tests **/*.yml contracts/build contracts/cache/ +safe-smart-account/build/ solgen/go **/node_modules diff --git a/.gitmodules b/.gitmodules index d7b61d862b..d4d26282ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "arbitrator/langs/bf"] path = arbitrator/langs/bf url = https://github.com/OffchainLabs/stylus-sdk-bf.git +[submodule "safe-smart-account"] + path = safe-smart-account + url = https://github.com/safe-global/safe-smart-account.git diff --git a/Dockerfile b/Dockerfile index 2fced8fade..22ac262f52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,8 @@ WORKDIR /workspace COPY contracts/package.json contracts/yarn.lock contracts/ RUN cd contracts && yarn install COPY contracts contracts/ +COPY safe-smart-account safe-smart-account/ +RUN cd safe-smart-account && yarn install COPY Makefile . RUN . ~/.bashrc && NITRO_BUILD_IGNORE_TIMESTAMPS=1 make build-solidity @@ -82,6 +84,7 @@ COPY ./wavmio ./wavmio COPY ./zeroheavy ./zeroheavy COPY ./contracts/src/precompiles/ ./contracts/src/precompiles/ COPY ./contracts/package.json ./contracts/yarn.lock ./contracts/ +COPY ./safe-smart-account ./safe-smart-account COPY ./solgen/gen.go ./solgen/ COPY ./fastcache ./fastcache COPY ./go-ethereum ./go-ethereum @@ -179,6 +182,7 @@ COPY ./Makefile ./ COPY ./arbitrator ./arbitrator COPY ./solgen ./solgen COPY ./contracts ./contracts +COPY ./safe-smart-account ./safe-smart-account RUN NITRO_BUILD_IGNORE_TIMESTAMPS=1 make build-replay-env FROM debian:bookworm-slim AS machine-versions @@ -226,6 +230,7 @@ COPY . ./ COPY --from=contracts-builder workspace/contracts/build/ contracts/build/ COPY --from=contracts-builder workspace/contracts/out/ contracts/out/ COPY --from=contracts-builder workspace/contracts/node_modules/@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json contracts/node_modules/@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/ +COPY --from=contracts-builder workspace/safe-smart-account/build/ safe-smart-account/build/ COPY --from=contracts-builder workspace/.make/ .make/ COPY --from=prover-header-export / target/ COPY --from=brotli-library-export / target/ diff --git a/Makefile b/Makefile index dc9b4e3ddf..b0d8116c97 100644 --- a/Makefile +++ b/Makefile @@ -494,12 +494,14 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(prover_bin) go run solgen/gen.go @touch $@ -.make/solidity: $(DEP_PREDICATE) contracts/src/*/*.sol .make/yarndeps $(ORDER_ONLY_PREDICATE) .make +.make/solidity: $(DEP_PREDICATE) safe-smart-account/contracts/*/*.sol safe-smart-account/contracts/*.sol contracts/src/*/*.sol .make/yarndeps $(ORDER_ONLY_PREDICATE) .make + yarn --cwd safe-smart-account build yarn --cwd contracts build yarn --cwd contracts build:forge:yul @touch $@ .make/yarndeps: $(DEP_PREDICATE) contracts/package.json contracts/yarn.lock $(ORDER_ONLY_PREDICATE) .make + yarn --cwd safe-smart-account install yarn --cwd contracts install @touch $@ diff --git a/safe-smart-account b/safe-smart-account new file mode 160000 index 0000000000..192c7dc672 --- /dev/null +++ b/safe-smart-account @@ -0,0 +1 @@ +Subproject commit 192c7dc67290940fcbc75165522bb86a37187069 diff --git a/solgen/gen.go b/solgen/gen.go index 92511595d7..2ad71b0c79 100644 --- a/solgen/gen.go +++ b/solgen/gen.go @@ -73,6 +73,18 @@ func main() { log.Fatal(err) } + filePathsSafeSmartAccount, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*", "*.sol", "*.json")) + if err != nil { + log.Fatal(err) + } + filePathsSafeSmartAccountOuter, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*.sol", "*.json")) + if err != nil { + log.Fatal(err) + } + + filePaths = append(filePaths, filePathsSafeSmartAccount...) + filePaths = append(filePaths, filePathsSafeSmartAccountOuter...) + modules := make(map[string]*moduleInfo) for _, path := range filePaths { diff --git a/staker/fast_confirm.go b/staker/fast_confirm.go new file mode 100644 index 0000000000..59a7443826 --- /dev/null +++ b/staker/fast_confirm.go @@ -0,0 +1,239 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +package staker + +import ( + "context" + "errors" + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/solgen/go/contractsgen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/staker/txbuilder" + "github.com/offchainlabs/nitro/util/headerreader" +) + +type FastConfirmSafe struct { + safe *contractsgen.Safe + owners []common.Address + threshold uint64 + fastConfirmNextNodeMethod abi.Method + builder *txbuilder.Builder + wallet ValidatorWalletInterface + gasRefunder common.Address + l1Reader *headerreader.HeaderReader +} + +func NewFastConfirmSafe( + callOpts bind.CallOpts, + fastConfirmSafeAddress common.Address, + builder *txbuilder.Builder, + wallet ValidatorWalletInterface, + gasRefunder common.Address, + l1Reader *headerreader.HeaderReader, +) (*FastConfirmSafe, error) { + fastConfirmSafe := &FastConfirmSafe{ + builder: builder, + wallet: wallet, + gasRefunder: gasRefunder, + l1Reader: l1Reader, + } + safe, err := contractsgen.NewSafe(fastConfirmSafeAddress, builder) + if err != nil { + return nil, err + } + fastConfirmSafe.safe = safe + owners, err := safe.GetOwners(&callOpts) + if err != nil { + return nil, err + } + + // This is needed because safe contract needs owners to be sorted. + sort.Slice(owners, func(i, j int) bool { + return owners[i].Cmp(owners[j]) < 0 + }) + fastConfirmSafe.owners = owners + threshold, err := safe.GetThreshold(&callOpts) + if err != nil { + return nil, err + } + fastConfirmSafe.threshold = threshold.Uint64() + rollupUserLogicAbi, err := rollupgen.RollupUserLogicMetaData.GetAbi() + if err != nil { + return nil, err + } + fastConfirmNextNodeMethod, ok := rollupUserLogicAbi.Methods["fastConfirmNextNode"] + if !ok { + return nil, errors.New("RollupUserLogic ABI missing fastConfirmNextNode method") + } + fastConfirmSafe.fastConfirmNextNodeMethod = fastConfirmNextNodeMethod + return fastConfirmSafe, nil +} + +func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash) error { + fastConfirmCallData, err := f.createFastConfirmCalldata(blockHash, sendRoot) + if err != nil { + return err + } + callOpts := &bind.CallOpts{Context: ctx} + // Current nonce of the safe. + nonce, err := f.safe.Nonce(callOpts) + if err != nil { + return err + } + // Hash of the safe transaction. + safeTxHash, err := f.safe.GetTransactionHash( + callOpts, + f.wallet.RollupAddress(), + big.NewInt(0), + fastConfirmCallData, + 0, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + common.Address{}, + common.Address{}, + nonce, + ) + if err != nil { + return err + } + if !f.wallet.CanBatchTxs() { + err = f.flushTransactions(ctx) + if err != nil { + return err + } + } + auth, err := f.builder.Auth(ctx) + if err != nil { + return err + } + _, err = f.safe.ApproveHash(auth, safeTxHash) + if err != nil { + return err + } + if !f.wallet.CanBatchTxs() { + err = f.flushTransactions(ctx) + if err != nil { + return err + } + } + executedTx, err := f.checkApprovedHashAndExecTransaction(ctx, fastConfirmCallData, safeTxHash) + if err != nil { + return err + } + if executedTx { + return nil + } + // If the transaction was not executed, we need to flush the transactions (for approve hash) and try again. + // This is because the hash might have been approved by another wallet in the same block, + // which might have led to a race condition. + err = f.flushTransactions(ctx) + if err != nil { + return err + } + _, err = f.checkApprovedHashAndExecTransaction(ctx, fastConfirmCallData, safeTxHash) + return err +} + +func (f *FastConfirmSafe) flushTransactions(ctx context.Context) error { + arbTx, err := f.wallet.ExecuteTransactions(ctx, f.builder, f.gasRefunder) + if err != nil { + return err + } + if arbTx != nil { + _, err = f.l1Reader.WaitForTxApproval(ctx, arbTx) + if err == nil { + log.Info("successfully executed staker transaction", "hash", arbTx.Hash()) + } else { + return fmt.Errorf("error waiting for tx receipt: %w", err) + } + } + f.builder.ClearTransactions() + return nil +} + +func (f *FastConfirmSafe) createFastConfirmCalldata( + blockHash common.Hash, sendRoot common.Hash, +) ([]byte, error) { + calldata, err := f.fastConfirmNextNodeMethod.Inputs.Pack( + blockHash, + sendRoot, + ) + if err != nil { + return nil, err + } + fullCalldata := append([]byte{}, f.fastConfirmNextNodeMethod.ID...) + fullCalldata = append(fullCalldata, calldata...) + return fullCalldata, nil +} + +func (f *FastConfirmSafe) checkApprovedHashAndExecTransaction(ctx context.Context, fastConfirmCallData []byte, safeTxHash [32]byte) (bool, error) { + var signatures []byte + approvedHashCount := uint64(0) + for _, owner := range f.owners { + if f.wallet.Address() == nil { + return false, errors.New("wallet address is nil") + } + var approved *big.Int + // No need check if wallet has approved the hash, + // since checkApprovedHashAndExecTransaction is called only after wallet has approved the hash. + if *f.wallet.Address() == owner { + approved = common.Big1 + } else { + var err error + approved, err = f.safe.ApprovedHashes(&bind.CallOpts{Context: ctx}, owner, safeTxHash) + if err != nil { + return false, err + } + } + + // If the owner has approved the hash, we add the signature to the transaction. + // We add the signature in the format r, s, v. + // We set v to 1, as it is the only possible value for a approved hash. + // We set r to the owner's address. + // We set s to the empty hash. + // Refer to the Safe contract for more information. + if approved.Cmp(common.Big1) == 0 { + approvedHashCount++ + v := uint8(1) + r := common.BytesToHash(owner.Bytes()) + s := common.Hash{} + signatures = append(signatures, r.Bytes()...) + signatures = append(signatures, s.Bytes()...) + signatures = append(signatures, v) + } + } + if approvedHashCount >= f.threshold { + auth, err := f.builder.Auth(ctx) + if err != nil { + return false, err + } + _, err = f.safe.ExecTransaction( + auth, + f.wallet.RollupAddress(), + big.NewInt(0), + fastConfirmCallData, + 0, + big.NewInt(0), + big.NewInt(0), + big.NewInt(0), + common.Address{}, + common.Address{}, + signatures, + ) + if err != nil { + return false, err + } + return true, nil + } + return false, nil +} diff --git a/staker/staker.go b/staker/staker.go index 24f5dc61e3..e3dd11dc07 100644 --- a/staker/staker.go +++ b/staker/staker.go @@ -90,6 +90,8 @@ type L1ValidatorConfig struct { ExtraGas uint64 `koanf:"extra-gas" reload:"hot"` Dangerous DangerousConfig `koanf:"dangerous"` ParentChainWallet genericconf.WalletConfig `koanf:"parent-chain-wallet"` + EnableFastConfirmation bool `koanf:"enable-fast-confirmation"` + FastConfirmSafeAddress string `koanf:"fast-confirm-safe-address"` LogQueryBatchSize uint64 `koanf:"log-query-batch-size" reload:"hot"` strategy StakerStrategy @@ -157,6 +159,8 @@ var DefaultL1ValidatorConfig = L1ValidatorConfig{ ExtraGas: 50000, Dangerous: DefaultDangerousConfig, ParentChainWallet: DefaultValidatorL1WalletConfig, + EnableFastConfirmation: false, + FastConfirmSafeAddress: "", LogQueryBatchSize: 0, } @@ -178,6 +182,8 @@ var TestL1ValidatorConfig = L1ValidatorConfig{ ExtraGas: 50000, Dangerous: DefaultDangerousConfig, ParentChainWallet: DefaultValidatorL1WalletConfig, + EnableFastConfirmation: false, + FastConfirmSafeAddress: "", LogQueryBatchSize: 0, } @@ -208,6 +214,8 @@ func L1ValidatorConfigAddOptions(prefix string, f *flag.FlagSet) { dataposter.DataPosterConfigAddOptions(prefix+".data-poster", f, dataposter.DefaultDataPosterConfigForValidator) DangerousConfigAddOptions(prefix+".dangerous", f) genericconf.WalletConfigAddOptions(prefix+".parent-chain-wallet", f, DefaultL1ValidatorConfig.ParentChainWallet.Pathname) + f.Bool(prefix+".enable-fast-confirmation", DefaultL1ValidatorConfig.EnableFastConfirmation, "enable fast confirmation") + f.String(prefix+".fast-confirm-safe-address", DefaultL1ValidatorConfig.FastConfirmSafeAddress, "safe address for fast confirmation") } type DangerousConfig struct { @@ -254,6 +262,7 @@ type Staker struct { inboxReader InboxReaderInterface statelessBlockValidator *StatelessBlockValidator fatalErr chan<- error + fastConfirmSafe *FastConfirmSafe } type ValidatorWalletInterface interface { @@ -303,6 +312,20 @@ func NewStaker( if config.StartValidationFromStaked && blockValidator != nil { stakedNotifiers = append(stakedNotifiers, blockValidator) } + var fastConfirmSafe *FastConfirmSafe + if config.EnableFastConfirmation && config.FastConfirmSafeAddress != "" { + fastConfirmSafe, err = NewFastConfirmSafe( + callOpts, + common.HexToAddress(config.FastConfirmSafeAddress), + val.builder, + wallet, + config.gasRefunder, + l1Reader, + ) + if err != nil { + return nil, err + } + } return &Staker{ L1Validator: val, l1Reader: l1Reader, @@ -315,6 +338,7 @@ func NewStaker( inboxReader: statelessBlockValidator.inboxReader, statelessBlockValidator: statelessBlockValidator, fatalErr: fatalErr, + fastConfirmSafe: fastConfirmSafe, }, nil } @@ -347,6 +371,32 @@ func (s *Staker) Initialize(ctx context.Context) error { return nil } +func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint64) error { + if !s.config.EnableFastConfirmation { + return nil + } + nodeInfo, err := s.rollup.LookupNode(ctx, number) + if err != nil { + return err + } + return s.tryFastConfirmation(ctx, nodeInfo.AfterState().GlobalState.BlockHash, nodeInfo.AfterState().GlobalState.SendRoot) +} + +func (s *Staker) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash) error { + if !s.config.EnableFastConfirmation { + return nil + } + if s.fastConfirmSafe != nil { + return s.fastConfirmSafe.tryFastConfirmation(ctx, blockHash, sendRoot) + } + auth, err := s.builder.Auth(ctx) + if err != nil { + return err + } + _, err = s.rollup.FastConfirmNextNode(auth, blockHash, sendRoot) + return err +} + func (s *Staker) getLatestStakedState(ctx context.Context, staker common.Address) (uint64, arbutil.MessageIndex, *validator.GoGlobalState, error) { callOpts := s.getCallOpts(ctx) if s.l1Reader.UseFinalityData() { @@ -850,7 +900,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv s.bringActiveUntilNode = info.LatestStakedNode + 1 } info.CanProgress = false - return nil + return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) } // Details are already logged with more details in generateNodeAction @@ -868,7 +918,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv if err != nil { return fmt.Errorf("error staking on new node: %w", err) } - return nil + return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) } // If we have no stake yet, we'll put one down @@ -890,7 +940,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv return fmt.Errorf("error placing new stake on new node: %w", err) } info.StakeExists = true - return nil + return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) case existingNodeAction: info.LatestStakedNode = action.number info.LatestStakedNodeHash = action.hash @@ -905,7 +955,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv hash: action.hash, } } - return nil + return s.tryFastConfirmationNodeNumber(ctx, action.number) } log.Info("staking on existing node", "node", action.number) // We'll return early if we already havea stake @@ -918,7 +968,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv if err != nil { return fmt.Errorf("error staking on existing node: %w", err) } - return nil + return s.tryFastConfirmationNodeNumber(ctx, action.number) } // If we have no stake yet, we'll put one down @@ -939,7 +989,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv return fmt.Errorf("error placing new stake on existing node: %w", err) } info.StakeExists = true - return nil + return s.tryFastConfirmationNodeNumber(ctx, action.number) default: panic("invalid action type") } diff --git a/system_tests/common_test.go b/system_tests/common_test.go index b0748f8639..ff184340ab 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -164,12 +164,13 @@ type NodeBuilder struct { L2Info info // L1, L2 Node parameters - dataDir string - isSequencer bool - takeOwnership bool - withL1 bool - addresses *chaininfo.RollupAddresses - initMessage *arbostypes.ParsedInitMessage + dataDir string + isSequencer bool + takeOwnership bool + withL1 bool + addresses *chaininfo.RollupAddresses + initMessage *arbostypes.ParsedInitMessage + withProdConfirmPeriodBlocks bool // Created nodes L1 *TestClient @@ -209,6 +210,11 @@ func (b *NodeBuilder) WithArbOSVersion(arbosVersion uint64) *NodeBuilder { return b } +func (b *NodeBuilder) WithProdConfirmPeriodBlocks() *NodeBuilder { + b.withProdConfirmPeriodBlocks = true + return b +} + func (b *NodeBuilder) WithWasmRootDir(wasmRootDir string) *NodeBuilder { b.valnodeConfig.Wasm.RootPath = wasmRootDir return b @@ -253,7 +259,7 @@ func (b *NodeBuilder) BuildL1(t *testing.T) { b.L1Info, b.L1.Client, b.L1.L1Backend, b.L1.Stack = createTestL1BlockChain(t, b.L1Info) locator, err := server_common.NewMachineLocator(b.valnodeConfig.Wasm.RootPath) Require(t, err) - b.addresses, b.initMessage = DeployOnTestL1(t, b.ctx, b.L1Info, b.L1.Client, b.chainConfig, locator.LatestWasmModuleRoot()) + b.addresses, b.initMessage = DeployOnTestL1(t, b.ctx, b.L1Info, b.L1.Client, b.chainConfig, locator.LatestWasmModuleRoot(), b.withProdConfirmPeriodBlocks) b.L1.cleanup = func() { requireClose(t, b.L1.Stack) } } @@ -883,7 +889,7 @@ func getInitMessage(ctx context.Context, t *testing.T, l1client client, addresse } func DeployOnTestL1( - t *testing.T, ctx context.Context, l1info info, l1client client, chainConfig *params.ChainConfig, wasmModuleRoot common.Hash, + t *testing.T, ctx context.Context, l1info info, l1client client, chainConfig *params.ChainConfig, wasmModuleRoot common.Hash, prodConfirmPeriodBlocks bool, ) (*chaininfo.RollupAddresses, *arbostypes.ParsedInitMessage) { l1info.GenerateAccount("RollupOwner") l1info.GenerateAccount("Sequencer") @@ -915,7 +921,7 @@ func DeployOnTestL1( []common.Address{l1info.GetAddress("Sequencer")}, l1info.GetAddress("RollupOwner"), 0, - arbnode.GenerateRollupConfig(false, wasmModuleRoot, l1info.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}), + arbnode.GenerateRollupConfig(prodConfirmPeriodBlocks, wasmModuleRoot, l1info.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}), nativeToken, maxDataSize, false, diff --git a/system_tests/fast_confirm_test.go b/system_tests/fast_confirm_test.go new file mode 100644 index 0000000000..d780f80414 --- /dev/null +++ b/system_tests/fast_confirm_test.go @@ -0,0 +1,513 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +// race detection makes things slow and miss timeouts +//go:build !race +// +build !race + +package arbtest + +import ( + "context" + "errors" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbnode/dataposter/externalsignertest" + "github.com/offchainlabs/nitro/arbnode/dataposter/storage" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/solgen/go/contractsgen" + "github.com/offchainlabs/nitro/solgen/go/proxiesgen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/staker/validatorwallet" + "github.com/offchainlabs/nitro/util" + "github.com/offchainlabs/nitro/validator/valnode" +) + +func TestFastConfirmation(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + srv := externalsignertest.NewServer(t) + go func() { + if err := srv.Start(); err != nil { + log.Error("Failed to start external signer server:", err) + return + } + }() + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).WithProdConfirmPeriodBlocks() + builder.L2Info = NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(builder.chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + + builder.nodeConfig.BatchPoster.MaxDelay = -1000 * time.Hour + cleanup := builder.Build(t) + defer cleanup() + + addNewBatchPoster(ctx, t, builder, srv.Address) + + builder.L1.SendWaitTestTransactions(t, []*types.Transaction{ + builder.L1Info.PrepareTxTo("Faucet", &srv.Address, 30000, big.NewInt(1).Mul(big.NewInt(1e18), big.NewInt(1e18)), nil)}) + + l2node := builder.L2.ConsensusNode + execNode := builder.L2.ExecNode + + config := arbnode.ConfigDefaultL1Test() + config.Sequencer = false + config.DelayedSequencer.Enable = false + config.BatchPoster.Enable = false + builder.execConfig.Sequencer.Enable = false + + builder.BridgeBalance(t, "Faucet", big.NewInt(1).Mul(big.NewInt(params.Ether), big.NewInt(10000))) + + deployAuth := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx) + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + builder.L1.TransferBalance(t, "Faucet", "Validator", balance, builder.L1Info) + l1auth := builder.L1Info.GetDefaultTransactOpts("Validator", ctx) + + valWalletAddrPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, &l1auth, l2node.L1Reader, true) + Require(t, err) + valWalletAddr := *valWalletAddrPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2node.DeployInfo.ValidatorWalletCreator, 0, &l1auth, l2node.L1Reader, true) + Require(t, err) + if valWalletAddr == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddr.String(), "vs", valWalletAddrCheck.String()) + } + + rollup, err := rollupgen.NewRollupAdminLogic(l2node.DeployInfo.Rollup, builder.L1.Client) + Require(t, err) + + upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(l2node.DeployInfo.UpgradeExecutor, builder.L1.Client) + Require(t, err, "unable to bind upgrade executor") + rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) + Require(t, err, "unable to parse rollup ABI") + + setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddr, srv.Address}, []bool{true, true}) + Require(t, err, "unable to generate setValidator calldata") + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setValidatorCalldata) + Require(t, err, "unable to set validators") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) + Require(t, err, "unable to generate setMinimumAssertionPeriod calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setMinAssertPeriodCalldata) + Require(t, err, "unable to set minimum assertion period") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", valWalletAddr) + Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2node.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) + Require(t, err, "unable to set anytrust fast confirmer") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + valConfig := staker.TestL1ValidatorConfig + valConfig.EnableFastConfirmation = true + parentChainID, err := builder.L1.Client.ChainID(ctx) + if err != nil { + t.Fatalf("Failed to get parent chain id: %v", err) + } + dp, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2node.ArbDB, storage.StakerPrefix), + l2node.L1Reader, + &l1auth, NewFetcherFromConfig(arbnode.ConfigDefaultL1NonSequencerTest()), + nil, + parentChainID, + ) + 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) + valConfig.Strategy = "MakeNodes" + + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + + stateless, err := staker.NewStatelessBlockValidator( + l2node.InboxReader, + l2node.InboxTracker, + l2node.TxStreamer, + execNode, + l2node.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = stateless.Start(ctx) + Require(t, err) + stakerA, err := staker.NewStaker( + l2node.L1Reader, + valWallet, + bind.CallOpts{}, + valConfig, + nil, + stateless, + nil, + nil, + l2node.DeployInfo.ValidatorUtils, + nil, + ) + Require(t, err) + err = stakerA.Initialize(ctx) + if stakerA.Strategy() != staker.WatchtowerStrategy { + err = valWallet.Initialize(ctx) + Require(t, err) + } + Require(t, err) + cfg := arbnode.ConfigDefaultL1NonSequencerTest() + signerCfg, err := externalSignerTestCfg(srv.Address, srv.URL()) + if err != nil { + t.Fatalf("Error getting external signer config: %v", err) + } + cfg.Staker.DataPoster.ExternalSigner = *signerCfg + + builder.L2Info.GenerateAccount("BackgroundUser") + tx = builder.L2Info.PrepareTx("Faucet", "BackgroundUser", builder.L2Info.TransferGas, balance, nil) + err = builder.L2.Client.SendTransaction(ctx, tx) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // Continually make L2 transactions in a background thread + backgroundTxsCtx, cancelBackgroundTxs := context.WithCancel(ctx) + backgroundTxsShutdownChan := make(chan struct{}) + defer (func() { + cancelBackgroundTxs() + <-backgroundTxsShutdownChan + })() + go (func() { + defer close(backgroundTxsShutdownChan) + err := makeBackgroundTxs(backgroundTxsCtx, builder) + if !errors.Is(err, context.Canceled) { + log.Warn("error making background txs", "err", err) + } + })() + + latestConfirmBeforeAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) + Require(t, err) + tx, err = stakerA.Act(ctx) + Require(t, err) + if tx != nil { + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + } + latestConfirmAfterAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) + Require(t, err) + if latestConfirmAfterAct <= latestConfirmBeforeAct { + Fatal(t, "staker A didn't advance the latest confirmed node") + } +} + +func TestFastConfirmationWithSafe(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + srv := externalsignertest.NewServer(t) + go func() { + if err := srv.Start(); err != nil { + log.Error("Failed to start external signer server:", err) + return + } + }() + var transferGas = util.NormalizeL2GasForL1GasInitial(800_000, params.GWei) // include room for aggregator L1 costs + + // Create a node with a large confirm period to ensure that the staker can't confirm without the fast confirmer. + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).WithProdConfirmPeriodBlocks() + builder.L2Info = NewBlockChainTestInfo( + t, + types.NewArbitrumSigner(types.NewLondonSigner(builder.chainConfig.ChainID)), big.NewInt(l2pricing.InitialBaseFeeWei*2), + transferGas, + ) + + builder.nodeConfig.BatchPoster.MaxDelay = -1000 * time.Hour + cleanupA := builder.Build(t) + defer cleanupA() + + addNewBatchPoster(ctx, t, builder, srv.Address) + + builder.L1.SendWaitTestTransactions(t, []*types.Transaction{ + builder.L1Info.PrepareTxTo("Faucet", &srv.Address, 30000, big.NewInt(1).Mul(big.NewInt(1e18), big.NewInt(1e18)), nil)}) + + l2nodeA := builder.L2.ConsensusNode + execNodeA := builder.L2.ExecNode + + config := arbnode.ConfigDefaultL1Test() + config.Sequencer = false + config.DelayedSequencer.Enable = false + config.BatchPoster.Enable = false + builder.execConfig.Sequencer.Enable = false + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{nodeConfig: config}) + defer cleanupB() + + l2nodeB := testClientB.ConsensusNode + execNodeB := testClientB.ExecNode + + nodeAGenesis := execNodeA.Backend.APIBackend().CurrentHeader().Hash() + nodeBGenesis := execNodeB.Backend.APIBackend().CurrentHeader().Hash() + if nodeAGenesis != nodeBGenesis { + Fatal(t, "node A L2 genesis hash", nodeAGenesis, "!= node B L2 genesis hash", nodeBGenesis) + } + + builder.BridgeBalance(t, "Faucet", big.NewInt(1).Mul(big.NewInt(params.Ether), big.NewInt(10000))) + + deployAuth := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx) + + balance := big.NewInt(params.Ether) + balance.Mul(balance, big.NewInt(100)) + builder.L1Info.GenerateAccount("ValidatorA") + builder.L1.TransferBalance(t, "Faucet", "ValidatorA", balance, builder.L1Info) + l1authA := builder.L1Info.GetDefaultTransactOpts("ValidatorA", ctx) + + builder.L1Info.GenerateAccount("ValidatorB") + builder.L1.TransferBalance(t, "Faucet", "ValidatorB", balance, builder.L1Info) + l1authB := builder.L1Info.GetDefaultTransactOpts("ValidatorB", ctx) + + valWalletAddrAPtr, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) + Require(t, err) + valWalletAddrA := *valWalletAddrAPtr + valWalletAddrCheck, err := validatorwallet.GetValidatorWalletContract(ctx, l2nodeA.DeployInfo.ValidatorWalletCreator, 0, &l1authA, l2nodeA.L1Reader, true) + Require(t, err) + if valWalletAddrA == *valWalletAddrCheck { + Require(t, err, "didn't cache validator wallet address", valWalletAddrA.String(), "vs", valWalletAddrCheck.String()) + } + + rollup, err := rollupgen.NewRollupAdminLogic(l2nodeA.DeployInfo.Rollup, builder.L1.Client) + Require(t, err) + + upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(l2nodeA.DeployInfo.UpgradeExecutor, builder.L1.Client) + Require(t, err, "unable to bind upgrade executor") + rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) + Require(t, err, "unable to parse rollup ABI") + + safeAddress := deploySafe(t, builder.L1, builder.L1.Client, deployAuth, []common.Address{valWalletAddrA, srv.Address}) + setValidatorCalldata, err := rollupABI.Pack("setValidator", []common.Address{valWalletAddrA, l1authB.From, srv.Address, safeAddress}, []bool{true, true, true, true}) + Require(t, err, "unable to generate setValidator calldata") + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setValidatorCalldata) + Require(t, err, "unable to set validators") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setMinAssertPeriodCalldata, err := rollupABI.Pack("setMinimumAssertionPeriod", big.NewInt(1)) + Require(t, err, "unable to generate setMinimumAssertionPeriod calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setMinAssertPeriodCalldata) + Require(t, err, "unable to set minimum assertion period") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + setAnyTrustFastConfirmerCalldata, err := rollupABI.Pack("setAnyTrustFastConfirmer", safeAddress) + Require(t, err, "unable to generate setAnyTrustFastConfirmer calldata") + tx, err = upgradeExecutor.ExecuteCall(&deployAuth, l2nodeA.DeployInfo.Rollup, setAnyTrustFastConfirmerCalldata) + Require(t, err, "unable to set anytrust fast confirmer") + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + + valConfig := staker.TestL1ValidatorConfig + valConfig.EnableFastConfirmation = true + valConfig.FastConfirmSafeAddress = safeAddress.String() + + parentChainID, err := builder.L1.Client.ChainID(ctx) + if err != nil { + t.Fatalf("Failed to get parent chain id: %v", err) + } + dpA, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2nodeB.ArbDB, storage.StakerPrefix), + l2nodeA.L1Reader, + &l1authA, NewFetcherFromConfig(arbnode.ConfigDefaultL1NonSequencerTest()), + nil, + parentChainID, + ) + if err != nil { + t.Fatalf("Error creating validator dataposter: %v", err) + } + valWalletA, err := validatorwallet.NewContract(dpA, nil, l2nodeA.DeployInfo.ValidatorWalletCreator, l2nodeA.DeployInfo.Rollup, l2nodeA.L1Reader, &l1authA, 0, func(common.Address) {}, func() uint64 { return valConfig.ExtraGas }) + Require(t, err) + valConfig.Strategy = "MakeNodes" + + _, valStack := createTestValidationNode(t, ctx, &valnode.TestValidationConfig) + blockValidatorConfig := staker.TestBlockValidatorConfig + + statelessA, err := staker.NewStatelessBlockValidator( + l2nodeA.InboxReader, + l2nodeA.InboxTracker, + l2nodeA.TxStreamer, + execNodeA, + l2nodeA.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = statelessA.Start(ctx) + Require(t, err) + stakerA, err := staker.NewStaker( + l2nodeA.L1Reader, + valWalletA, + bind.CallOpts{}, + valConfig, + nil, + statelessA, + nil, + nil, + l2nodeA.DeployInfo.ValidatorUtils, + nil, + ) + Require(t, err) + err = stakerA.Initialize(ctx) + Require(t, err) + err = valWalletA.Initialize(ctx) + Require(t, err) + cfg := arbnode.ConfigDefaultL1NonSequencerTest() + signerCfg, err := externalSignerTestCfg(srv.Address, srv.URL()) + if err != nil { + t.Fatalf("Error getting external signer config: %v", err) + } + cfg.Staker.DataPoster.ExternalSigner = *signerCfg + dpB, err := arbnode.StakerDataposter( + ctx, + rawdb.NewTable(l2nodeB.ArbDB, storage.StakerPrefix), + l2nodeB.L1Reader, + &l1authB, NewFetcherFromConfig(cfg), + nil, + parentChainID, + ) + if err != nil { + t.Fatalf("Error creating validator dataposter: %v", err) + } + valWalletB, err := validatorwallet.NewEOA(dpB, l2nodeB.DeployInfo.Rollup, l2nodeB.L1Reader.Client(), func() uint64 { return 0 }) + Require(t, err) + valConfig.Strategy = "watchtower" + statelessB, err := staker.NewStatelessBlockValidator( + l2nodeB.InboxReader, + l2nodeB.InboxTracker, + l2nodeB.TxStreamer, + execNodeB, + l2nodeB.ArbDB, + nil, + StaticFetcherFrom(t, &blockValidatorConfig), + valStack, + ) + Require(t, err) + err = statelessB.Start(ctx) + Require(t, err) + stakerB, err := staker.NewStaker( + l2nodeB.L1Reader, + valWalletB, + bind.CallOpts{}, + valConfig, + nil, + statelessB, + nil, + nil, + l2nodeB.DeployInfo.ValidatorUtils, + nil, + ) + Require(t, err) + err = stakerB.Initialize(ctx) + Require(t, err) + err = valWalletB.Initialize(ctx) + Require(t, err) + + builder.L2Info.GenerateAccount("BackgroundUser") + tx = builder.L2Info.PrepareTx("Faucet", "BackgroundUser", builder.L2Info.TransferGas, balance, nil) + err = builder.L2.Client.SendTransaction(ctx, tx) + Require(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + Require(t, err) + + // Continually make L2 transactions in a background thread + backgroundTxsCtx, cancelBackgroundTxs := context.WithCancel(ctx) + backgroundTxsShutdownChan := make(chan struct{}) + defer (func() { + cancelBackgroundTxs() + <-backgroundTxsShutdownChan + })() + go (func() { + defer close(backgroundTxsShutdownChan) + err := makeBackgroundTxs(backgroundTxsCtx, builder) + if !errors.Is(err, context.Canceled) { + log.Warn("error making background txs", "err", err) + } + })() + + latestConfirmBeforeAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) + Require(t, err) + tx, err = stakerA.Act(ctx) + Require(t, err) + if tx != nil { + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + } + latestConfirmAfterStakerAAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) + Require(t, err) + if latestConfirmAfterStakerAAct != latestConfirmBeforeAct { + Fatal(t, "staker A alone advanced the latest confirmed node", latestConfirmAfterStakerAAct, "when it shouldn't have") + } + for j := 0; j < 5; j++ { + builder.L1.TransferBalance(t, "Faucet", "Faucet", common.Big0, builder.L1Info) + } + tx, err = stakerB.Act(ctx) + Require(t, err) + if tx != nil { + _, err = builder.L1.EnsureTxSucceeded(tx) + Require(t, err) + } + latestConfirmAfterStakerBAct, err := rollup.LatestConfirmed(&bind.CallOpts{}) + Require(t, err) + if latestConfirmAfterStakerBAct <= latestConfirmBeforeAct { + Fatal(t, "staker A and B together didn't advance the latest confirmed node") + } +} + +func deploySafe(t *testing.T, l1 *TestClient, backend bind.ContractBackend, deployAuth bind.TransactOpts, owners []common.Address) common.Address { + safeAddress, tx, _, err := contractsgen.DeploySafeL2(&deployAuth, backend) + Require(t, err) + _, err = l1.EnsureTxSucceeded(tx) + Require(t, err) + safeProxyAddress, tx, _, err := proxiesgen.DeploySafeProxy(&deployAuth, backend, safeAddress) + Require(t, err) + _, err = l1.EnsureTxSucceeded(tx) + Require(t, err) + var safe *contractsgen.Safe + safe, err = contractsgen.NewSafe(safeProxyAddress, backend) + Require(t, err) + _, err = l1.EnsureTxSucceeded(tx) + Require(t, err) + tx, err = safe.Setup( + &deployAuth, + owners, + big.NewInt(2), + common.Address{}, + nil, + common.Address{}, + common.Address{}, + big.NewInt(0), + common.Address{}, + ) + Require(t, err) + _, err = l1.EnsureTxSucceeded(tx) + Require(t, err) + return safeProxyAddress +}