From 678a37057570833f63b1f37dbd0ee80ea1bc39d6 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 24 Jul 2024 11:16:20 -0600 Subject: [PATCH 1/4] Try to fast confirm past nodes too --- contracts | 2 +- staker/fast_confirm.go | 26 +++++++++++++----- staker/staker.go | 60 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/contracts b/contracts index 61204dd455..6d59d872e4 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 61204dd455966cb678192427a07aa9795ff91c14 +Subproject commit 6d59d872e45eac3e43e7cb65fccc245800b8e7d5 diff --git a/staker/fast_confirm.go b/staker/fast_confirm.go index 59a7443826..7ae620838b 100644 --- a/staker/fast_confirm.go +++ b/staker/fast_confirm.go @@ -78,8 +78,11 @@ func NewFastConfirmSafe( return fastConfirmSafe, nil } -func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash) error { - fastConfirmCallData, err := f.createFastConfirmCalldata(blockHash, sendRoot) +func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash, nodeHash common.Hash) error { + if f.wallet.Address() == nil { + return errors.New("fast confirmation requires a wallet which is not setup") + } + fastConfirmCallData, err := f.createFastConfirmCalldata(blockHash, sendRoot, nodeHash) if err != nil { return err } @@ -112,6 +115,16 @@ func (f *FastConfirmSafe) tryFastConfirmation(ctx context.Context, blockHash com return err } } + + alreadyApproved, err := f.safe.ApprovedHashes(&bind.CallOpts{Context: ctx}, *f.wallet.Address(), safeTxHash) + if err != nil { + return err + } + if alreadyApproved.Cmp(common.Big1) == 0 { + _, err = f.checkApprovedHashAndExecTransaction(ctx, fastConfirmCallData, safeTxHash) + return err + } + auth, err := f.builder.Auth(ctx) if err != nil { return err @@ -162,11 +175,12 @@ func (f *FastConfirmSafe) flushTransactions(ctx context.Context) error { } func (f *FastConfirmSafe) createFastConfirmCalldata( - blockHash common.Hash, sendRoot common.Hash, + blockHash common.Hash, sendRoot common.Hash, nodeHash common.Hash, ) ([]byte, error) { calldata, err := f.fastConfirmNextNodeMethod.Inputs.Pack( blockHash, sendRoot, + nodeHash, ) if err != nil { return nil, err @@ -177,12 +191,12 @@ func (f *FastConfirmSafe) createFastConfirmCalldata( } func (f *FastConfirmSafe) checkApprovedHashAndExecTransaction(ctx context.Context, fastConfirmCallData []byte, safeTxHash [32]byte) (bool, error) { + if f.wallet.Address() == nil { + return false, errors.New("wallet address is nil") + } 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. diff --git a/staker/staker.go b/staker/staker.go index e3dd11dc07..1b1317948a 100644 --- a/staker/staker.go +++ b/staker/staker.go @@ -371,7 +371,7 @@ func (s *Staker) Initialize(ctx context.Context) error { return nil } -func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint64) error { +func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint64, hash common.Hash) error { if !s.config.EnableFastConfirmation { return nil } @@ -379,21 +379,21 @@ func (s *Staker) tryFastConfirmationNodeNumber(ctx context.Context, number uint6 if err != nil { return err } - return s.tryFastConfirmation(ctx, nodeInfo.AfterState().GlobalState.BlockHash, nodeInfo.AfterState().GlobalState.SendRoot) + return s.tryFastConfirmation(ctx, nodeInfo.AfterState().GlobalState.BlockHash, nodeInfo.AfterState().GlobalState.SendRoot, hash) } -func (s *Staker) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash) error { +func (s *Staker) tryFastConfirmation(ctx context.Context, blockHash common.Hash, sendRoot common.Hash, nodeHash common.Hash) error { if !s.config.EnableFastConfirmation { return nil } if s.fastConfirmSafe != nil { - return s.fastConfirmSafe.tryFastConfirmation(ctx, blockHash, sendRoot) + return s.fastConfirmSafe.tryFastConfirmation(ctx, blockHash, sendRoot, nodeHash) } auth, err := s.builder.Auth(ctx) if err != nil { return err } - _, err = s.rollup.FastConfirmNextNode(auth, blockHash, sendRoot) + _, err = s.rollup.FastConfirmNextNode(auth, blockHash, sendRoot, nodeHash) return err } @@ -683,6 +683,43 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { StakeExists: rawInfo != nil, } + if s.config.EnableFastConfirmation { + firstUnresolvedNode, err := s.rollup.FirstUnresolvedNode(callOpts) + if err != nil { + return nil, err + } + if latestStakedNodeNum >= firstUnresolvedNode { + lastHeader, err := s.l1Reader.LastHeader(ctx) + if err != nil { + return nil, err + } + // To check if a node is correct, we simply check if we're staked on it. + // Since we're staked on it or a later node, this will tell us if it's correct. + // To keep this call consistent with the GetNode call, we pin a specific parent chain block hash. + checkNodeCorrectCallOpts := s.getCallOpts(ctx) + checkNodeCorrectCallOpts.BlockHash = lastHeader.ParentHash + stakedOnNode, err := s.rollup.NodeHasStaker(checkNodeCorrectCallOpts, firstUnresolvedNode, walletAddressOrZero) + if err != nil { + return nil, err + } + if stakedOnNode { + nodeInfo, err := s.rollup.GetNode(checkNodeCorrectCallOpts, firstUnresolvedNode) + if err != nil { + return nil, err + } + err = s.tryFastConfirmationNodeNumber(ctx, firstUnresolvedNode, nodeInfo.NodeHash) + if err != nil { + return nil, err + } + if s.builder.BuildingTransactionCount() > 0 { + // Try to fast confirm previous nodes before working on new ones + log.Info("fast confirming previous node", "node", firstUnresolvedNode) + return s.wallet.ExecuteTransactions(ctx, s.builder, s.config.gasRefunder) + } + } + } + } + effectiveStrategy := s.config.strategy nodesLinear, err := s.validatorUtils.AreUnresolvedNodesLinear(callOpts, s.rollupAddress) if err != nil { @@ -900,7 +937,8 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv s.bringActiveUntilNode = info.LatestStakedNode + 1 } info.CanProgress = false - return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) + // We can't fast confirm a node that doesn't exist + return nil } // Details are already logged with more details in generateNodeAction @@ -918,7 +956,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 s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) + return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot, action.hash) } // If we have no stake yet, we'll put one down @@ -940,7 +978,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 s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot) + return s.tryFastConfirmation(ctx, action.assertion.AfterState.GlobalState.BlockHash, action.assertion.AfterState.GlobalState.SendRoot, action.hash) case existingNodeAction: info.LatestStakedNode = action.number info.LatestStakedNodeHash = action.hash @@ -955,7 +993,7 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv hash: action.hash, } } - return s.tryFastConfirmationNodeNumber(ctx, action.number) + return s.tryFastConfirmationNodeNumber(ctx, action.number, action.hash) } log.Info("staking on existing node", "node", action.number) // We'll return early if we already havea stake @@ -968,7 +1006,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 s.tryFastConfirmationNodeNumber(ctx, action.number) + return s.tryFastConfirmationNodeNumber(ctx, action.number, action.hash) } // If we have no stake yet, we'll put one down @@ -989,7 +1027,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 s.tryFastConfirmationNodeNumber(ctx, action.number) + return s.tryFastConfirmationNodeNumber(ctx, action.number, action.hash) default: panic("invalid action type") } From b5a0ef9f943d649530e2219c4744ca6c718cc401 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 24 Jul 2024 11:56:48 -0600 Subject: [PATCH 2/4] Update submodule pin and arbutil.L1Interface --- arbutil/wait_for_l1.go | 2 +- contracts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arbutil/wait_for_l1.go b/arbutil/wait_for_l1.go index eaa5d0790d..4b4819156d 100644 --- a/arbutil/wait_for_l1.go +++ b/arbutil/wait_for_l1.go @@ -19,12 +19,12 @@ import ( type L1Interface interface { bind.ContractBackend + bind.BlockHashContractCaller ethereum.ChainReader ethereum.ChainStateReader ethereum.TransactionReader TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) BlockNumber(ctx context.Context) (uint64, error) - CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) ChainID(ctx context.Context) (*big.Int, error) Client() rpc.ClientInterface diff --git a/contracts b/contracts index 6d59d872e4..f7894d3a6d 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 6d59d872e45eac3e43e7cb65fccc245800b8e7d5 +Subproject commit f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce From 26102c37d8b834d4b7652909877f2d42596a6a40 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 24 Jul 2024 12:13:37 -0600 Subject: [PATCH 3/4] Fix stubL1Client --- arbnode/dataposter/dataposter_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arbnode/dataposter/dataposter_test.go b/arbnode/dataposter/dataposter_test.go index f840d8c84e..172b486df0 100644 --- a/arbnode/dataposter/dataposter_test.go +++ b/arbnode/dataposter/dataposter_test.go @@ -212,6 +212,10 @@ func (c *stubL1Client) CallContractAtHash(ctx context.Context, msg ethereum.Call return []byte{}, nil } +func (c *stubL1Client) CodeAtHash(ctx context.Context, address common.Address, blockHash common.Hash) ([]byte, error) { + return []byte{}, nil +} + func (c *stubL1Client) ChainID(ctx context.Context) (*big.Int, error) { return nil, nil } From 5e0f780387633601a60fbf2304df7fb11ef48429 Mon Sep 17 00:00:00 2001 From: Lee Bousfield Date: Wed, 31 Jul 2024 20:37:40 -0500 Subject: [PATCH 4/4] Also try to fast confirm past nodes when inactive --- go.mod | 5 ++- go.sum | 3 ++ staker/staker.go | 97 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index b6ad80cb21..123d7ea59a 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,10 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + github.com/google/btree v1.1.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect +) require ( github.com/DataDog/zstd v1.4.5 // indirect diff --git a/go.sum b/go.sum index 236a4e592d..f0580a6c1d 100644 --- a/go.sum +++ b/go.sum @@ -340,7 +340,10 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXi github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= diff --git a/staker/staker.go b/staker/staker.go index 1b1317948a..3eb941c6dd 100644 --- a/staker/staker.go +++ b/staker/staker.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" + "github.com/google/btree" flag "github.com/spf13/pflag" "github.com/offchainlabs/nitro/arbnode/dataposter" @@ -246,6 +247,11 @@ type LatestConfirmedNotifier interface { UpdateLatestConfirmed(count arbutil.MessageIndex, globalState validator.GoGlobalState) } +type validatedNode struct { + number uint64 + hash common.Hash +} + type Staker struct { *L1Validator stopwaiter.StopWaiter @@ -258,6 +264,7 @@ type Staker struct { highGasBlocksBuffer *big.Int lastActCalledBlock *big.Int inactiveLastCheckedNode *nodeAndHash + inactiveValidatedNodes *btree.BTreeG[validatedNode] bringActiveUntilNode uint64 inboxReader InboxReaderInterface statelessBlockValidator *StatelessBlockValidator @@ -326,6 +333,9 @@ func NewStaker( return nil, err } } + inactiveValidatedNodes := btree.NewG(2, func(a, b validatedNode) bool { + return a.number < b.number || (a.number == b.number && a.hash.Cmp(b.hash) < 0) + }) return &Staker{ L1Validator: val, l1Reader: l1Reader, @@ -339,6 +349,7 @@ func NewStaker( statelessBlockValidator: statelessBlockValidator, fatalErr: fatalErr, fastConfirmSafe: fastConfirmSafe, + inactiveValidatedNodes: inactiveValidatedNodes, }, nil } @@ -683,12 +694,40 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { StakeExists: rawInfo != nil, } + effectiveStrategy := s.config.strategy + nodesLinear, err := s.validatorUtils.AreUnresolvedNodesLinear(callOpts, s.rollupAddress) + if err != nil { + return nil, fmt.Errorf("error checking for rollup assertion fork: %w", err) + } + if !nodesLinear { + log.Warn("rollup assertion fork detected") + if effectiveStrategy == DefensiveStrategy { + effectiveStrategy = StakeLatestStrategy + } + s.inactiveLastCheckedNode = nil + } + if s.bringActiveUntilNode != 0 { + if info.LatestStakedNode < s.bringActiveUntilNode { + if effectiveStrategy == DefensiveStrategy { + effectiveStrategy = StakeLatestStrategy + } + } else { + log.Info("defensive validator staked past incorrect node; waiting here") + s.bringActiveUntilNode = 0 + } + s.inactiveLastCheckedNode = nil + } + if effectiveStrategy <= DefensiveStrategy && s.inactiveLastCheckedNode != nil { + info.LatestStakedNode = s.inactiveLastCheckedNode.id + info.LatestStakedNodeHash = s.inactiveLastCheckedNode.hash + } + if s.config.EnableFastConfirmation { firstUnresolvedNode, err := s.rollup.FirstUnresolvedNode(callOpts) if err != nil { return nil, err } - if latestStakedNodeNum >= firstUnresolvedNode { + if info.LatestStakedNode >= firstUnresolvedNode { lastHeader, err := s.l1Reader.LastHeader(ctx) if err != nil { return nil, err @@ -698,15 +737,23 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { // To keep this call consistent with the GetNode call, we pin a specific parent chain block hash. checkNodeCorrectCallOpts := s.getCallOpts(ctx) checkNodeCorrectCallOpts.BlockHash = lastHeader.ParentHash - stakedOnNode, err := s.rollup.NodeHasStaker(checkNodeCorrectCallOpts, firstUnresolvedNode, walletAddressOrZero) + nodeInfo, err := s.rollup.GetNode(checkNodeCorrectCallOpts, firstUnresolvedNode) if err != nil { return nil, err } - if stakedOnNode { - nodeInfo, err := s.rollup.GetNode(checkNodeCorrectCallOpts, firstUnresolvedNode) + validatedNode, haveValidated := s.inactiveValidatedNodes.Get(validatedNode{ + number: firstUnresolvedNode, + hash: nodeInfo.NodeHash, + }) + confirmedCorrect := haveValidated && validatedNode.hash == nodeInfo.NodeHash + if !confirmedCorrect { + stakedOnNode, err := s.rollup.NodeHasStaker(checkNodeCorrectCallOpts, firstUnresolvedNode, walletAddressOrZero) if err != nil { return nil, err } + confirmedCorrect = stakedOnNode + } + if confirmedCorrect { err = s.tryFastConfirmationNodeNumber(ctx, firstUnresolvedNode, nodeInfo.NodeHash) if err != nil { return nil, err @@ -720,37 +767,21 @@ func (s *Staker) Act(ctx context.Context) (*types.Transaction, error) { } } - effectiveStrategy := s.config.strategy - nodesLinear, err := s.validatorUtils.AreUnresolvedNodesLinear(callOpts, s.rollupAddress) + latestConfirmedNode, err := s.rollup.LatestConfirmed(callOpts) if err != nil { - return nil, fmt.Errorf("error checking for rollup assertion fork: %w", err) + return nil, fmt.Errorf("error getting latest confirmed node: %w", err) } - if !nodesLinear { - log.Warn("rollup assertion fork detected") - if effectiveStrategy == DefensiveStrategy { - effectiveStrategy = StakeLatestStrategy + + // Clear s.inactiveValidatedNodes of any entries before or equal to latestConfirmedNode + for { + validatedNode, ok := s.inactiveValidatedNodes.Min() + if !ok { + break } - s.inactiveLastCheckedNode = nil - } - if s.bringActiveUntilNode != 0 { - if info.LatestStakedNode < s.bringActiveUntilNode { - if effectiveStrategy == DefensiveStrategy { - effectiveStrategy = StakeLatestStrategy - } - } else { - log.Info("defensive validator staked past incorrect node; waiting here") - s.bringActiveUntilNode = 0 + if validatedNode.number > latestConfirmedNode { + break } - s.inactiveLastCheckedNode = nil - } - if effectiveStrategy <= DefensiveStrategy && s.inactiveLastCheckedNode != nil { - info.LatestStakedNode = s.inactiveLastCheckedNode.id - info.LatestStakedNodeHash = s.inactiveLastCheckedNode.hash - } - - latestConfirmedNode, err := s.rollup.LatestConfirmed(callOpts) - if err != nil { - return nil, fmt.Errorf("error getting latest confirmed node: %w", err) + s.inactiveValidatedNodes.DeleteMin() } requiredStakeElevated, err := s.isRequiredStakeElevated(ctx) @@ -992,6 +1023,10 @@ func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiv id: action.number, hash: action.hash, } + s.inactiveValidatedNodes.ReplaceOrInsert(validatedNode{ + number: action.number, + hash: action.hash, + }) } return s.tryFastConfirmationNodeNumber(ctx, action.number, action.hash) }