From 0cac9c6701db073683d3098f63524fef3c89b7f7 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Feb 2025 02:40:22 -0600 Subject: [PATCH] Add E2E Test to Prove Honest Validator Can Be a Delegated Staker (#728) --- .../endtoend/e2e_delegated_staking_test.go | 274 ++++++++++++++++++ testing/endtoend/helpers_test.go | 2 + testing/setup/rollup_stack.go | 12 +- 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 testing/endtoend/e2e_delegated_staking_test.go diff --git a/testing/endtoend/e2e_delegated_staking_test.go b/testing/endtoend/e2e_delegated_staking_test.go new file mode 100644 index 000000000..723511ce1 --- /dev/null +++ b/testing/endtoend/e2e_delegated_staking_test.go @@ -0,0 +1,274 @@ +package endtoend + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + protocol "github.com/offchainlabs/bold/chain-abstraction" + solimpl "github.com/offchainlabs/bold/chain-abstraction/sol-implementation" + cm "github.com/offchainlabs/bold/challenge-manager" + "github.com/offchainlabs/bold/challenge-manager/types" + retry "github.com/offchainlabs/bold/runtime" + "github.com/offchainlabs/bold/solgen/go/challengeV2gen" + "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/endtoend/backend" + statemanager "github.com/offchainlabs/bold/testing/mocks/state-provider" + "github.com/offchainlabs/bold/testing/setup" +) + +func TestEndToEnd_DelegatedStaking(t *testing.T) { + neutralCtx, neutralCancel := context.WithCancel(context.Background()) + defer neutralCancel() + evilCtx, evilCancel := context.WithCancel(context.Background()) + defer evilCancel() + honestCtx, honestCancel := context.WithCancel(context.Background()) + defer honestCancel() + + protocolCfg := defaultProtocolParams() + protocolCfg.challengePeriodBlocks = 25 + timeCfg := defaultTimeParams() + timeCfg.blockTime = time.Second + inboxCfg := defaultInboxParams() + + challengeTestingOpts := []challenge_testing.Opt{ + challenge_testing.WithConfirmPeriodBlocks(protocolCfg.challengePeriodBlocks), + challenge_testing.WithLayerZeroHeights(&protocolCfg.layerZeroHeights), + challenge_testing.WithNumBigStepLevels(protocolCfg.numBigStepLevels), + } + deployOpts := []setup.Opt{ + setup.WithMockBridge(), + setup.WithMockOneStepProver(), + setup.WithNumAccounts(10), + setup.WithChallengeTestingOpts(challengeTestingOpts...), + } + + simBackend, err := backend.NewSimulated(timeCfg.blockTime, deployOpts...) + require.NoError(t, err) + bk := simBackend + + rollupAddr, err := bk.DeployRollup(neutralCtx, challengeTestingOpts...) + require.NoError(t, err) + + require.NoError(t, bk.Start(neutralCtx)) + + accounts := bk.Accounts() + bk.Commit() + + rollupUserBindings, err := rollupgen.NewRollupUserLogic(rollupAddr.Rollup, bk.Client()) + require.NoError(t, err) + bridgeAddr, err := rollupUserBindings.Bridge(&bind.CallOpts{}) + require.NoError(t, err) + dataHash := common.Hash{1} + enqueueSequencerMessageAsExecutor( + t, accounts[0], rollupAddr.UpgradeExecutor, bk.Client(), bridgeAddr, seqMessage{ + dataHash: dataHash, + afterDelayedMessagesRead: big.NewInt(1), + prevMessageCount: big.NewInt(1), + newMessageCount: big.NewInt(2), + }, + ) + + baseStateManagerOpts := []statemanager.Opt{ + statemanager.WithNumBatchesRead(inboxCfg.numBatchesPosted), + statemanager.WithLayerZeroHeights(&protocolCfg.layerZeroHeights, protocolCfg.numBigStepLevels), + } + honestStateManager, err := statemanager.NewForSimpleMachine(t, baseStateManagerOpts...) + require.NoError(t, err) + + shp := &simpleHeaderProvider{b: bk, chs: make([]chan<- *gethtypes.Header, 0)} + shp.Start(neutralCtx) + + baseStackOpts := []cm.StackOpt{ + cm.StackWithMode(types.MakeMode), + cm.StackWithPollingInterval(timeCfg.assertionScanningInterval), + cm.StackWithPostingInterval(timeCfg.assertionPostingInterval), + cm.StackWithAverageBlockCreationTime(timeCfg.blockTime), + cm.StackWithConfirmationInterval(timeCfg.assertionConfirmationAttemptInterval), + cm.StackWithMinimumGapToParentAssertion(0), + cm.StackWithHeaderProvider(shp), + cm.StackWithDelegatedStaking(), // Enable delegated staking. + cm.StackWithoutAutoDeposit(), + } + + name := "honest" + + // Ensure the honest validator is a generated account that has no erc20 token balance, + // but has some ETH to pay for gas costs of BoLD. We ensure that the honest validator + // is not initially staked, and that the actual address that will be funding the honest + // validator has enough funds. + fundsCustodianOpts := accounts[1] // The 1st and 2nd accounts should be the funds' custodians. + evilFundsCustodianOpts := accounts[2] + honestTxOpts := accounts[len(accounts)-1] + evilTxOpts := accounts[len(accounts)-2] + + //nolint:gocritic + honestOpts := append( + baseStackOpts, + cm.StackWithName(name), + ) + // Ensure the funds custodian is the withdrawal address for the honest validator. + honestChain := setupAssertionChain( + t, + honestCtx, + bk.Client(), + rollupAddr.Rollup, + honestTxOpts, + solimpl.WithCustomWithdrawalAddress(fundsCustodianOpts.From), + ) + + machineDivergenceStep := uint64(1) + assertionDivergenceHeight := uint64(1) + assertionBlockHeightDifference := int64(1) + + //nolint:gocritic + evilStateManagerOpts := append( + baseStateManagerOpts, + statemanager.WithMachineDivergenceStep(machineDivergenceStep), + statemanager.WithBlockDivergenceHeight(assertionDivergenceHeight), + statemanager.WithDivergentBlockHeightOffset(assertionBlockHeightDifference), + ) + evilStateManager, err := statemanager.NewForSimpleMachine(t, evilStateManagerOpts...) + require.NoError(t, err) + + //nolint:gocritic + evilOpts := append( + baseStackOpts, + cm.StackWithName("evil"), + ) + evilChain := setupAssertionChain( + t, + evilCtx, + bk.Client(), + rollupAddr.Rollup, + evilTxOpts, + solimpl.WithCustomWithdrawalAddress(evilFundsCustodianOpts.From), + ) + + // Ensure that both validators are not yet staked. + isStaked, err := honestChain.IsStaked(honestCtx) + require.NoError(t, err) + require.False(t, isStaked) + isStaked, err = evilChain.IsStaked(evilCtx) + require.NoError(t, err) + require.False(t, isStaked) + + chalManagerAddr := honestChain.SpecChallengeManager().Address() + cmBindings, err := challengeV2gen.NewEdgeChallengeManager(chalManagerAddr, bk.Client()) + require.NoError(t, err) + stakeToken, err := cmBindings.StakeToken(&bind.CallOpts{}) + require.NoError(t, err) + requiredStake, err := honestChain.RollupCore().BaseStake(&bind.CallOpts{}) + require.NoError(t, err) + + tokenBindings, err := mocksgen.NewTestWETH9(stakeToken, bk.Client()) + require.NoError(t, err) + + balCustodian, err := tokenBindings.BalanceOf(&bind.CallOpts{}, fundsCustodianOpts.From) + require.NoError(t, err) + require.True(t, balCustodian.Cmp(requiredStake) >= 0) // Ensure funds custodian DOES have enough stake token balance. + balEvilCustodian, err := tokenBindings.BalanceOf(&bind.CallOpts{}, evilFundsCustodianOpts.From) + require.NoError(t, err) + require.True(t, balEvilCustodian.Cmp(requiredStake) >= 0) // Ensure funds custodian DOES have enough stake token balance. + + honestManager, err := cm.NewChallengeStack(honestChain, honestStateManager, honestOpts...) + require.NoError(t, err) + _ = honestManager + + evilManager, err := cm.NewChallengeStack(evilChain, evilStateManager, evilOpts...) + require.NoError(t, err) + _ = evilManager + + honestManager.Start(honestCtx) + evilManager.Start(evilCtx) + + // Next, the custodians add deposits. + // Waits until the validators are staked with a value of 0 before adding the deposit. + var isStakedWithZero bool + for honestCtx.Err() == nil && !isStakedWithZero { + isStaked, err = honestChain.IsStaked(honestCtx) + require.NoError(t, err) + time.Sleep(500 * time.Millisecond) // Don't spam the backend. + if isStaked { + isStakedWithZero = true + } + } + isStakedWithZero = false + for evilCtx.Err() == nil && !isStakedWithZero { + isStaked, err = evilChain.IsStaked(evilCtx) + require.NoError(t, err) + time.Sleep(500 * time.Millisecond) // Don't spam the backend. + if isStaked { + isStakedWithZero = true + } + } + + // Now, adds the deposit. + rollupUserLogic, err := rollupgen.NewRollupUserLogic(rollupAddr.Rollup, bk.Client()) + require.NoError(t, err) + tx, err := rollupUserLogic.AddToDeposit(fundsCustodianOpts, honestTxOpts.From, fundsCustodianOpts.From, balCustodian) + require.NoError(t, err) + _, err = bind.WaitMined(honestCtx, bk.Client(), tx) + require.NoError(t, err) + + tx, err = rollupUserLogic.AddToDeposit(evilFundsCustodianOpts, evilTxOpts.From, evilFundsCustodianOpts.From, balEvilCustodian) + require.NoError(t, err) + _, err = bind.WaitMined(evilCtx, bk.Client(), tx) + require.NoError(t, err) + + t.Log("Delegated validators now have a deposit balance") + + t.Run("expects honest validator to win challenge", func(t *testing.T) { + chainId, err := bk.Client().ChainID(honestCtx) + require.NoError(t, err) + // Wait until a challenged assertion is confirmed by time. + var confirmed bool + for neutralCtx.Err() == nil && !confirmed { + var i *rollupgen.RollupCoreAssertionConfirmedIterator + i, err = retry.UntilSucceeds(neutralCtx, func() (*rollupgen.RollupCoreAssertionConfirmedIterator, error) { + return honestChain.RollupCore().FilterAssertionConfirmed(nil, nil) + }) + require.NoError(t, err) + for i.Next() { + creationInfo, err2 := evilChain.ReadAssertionCreationInfo(evilCtx, protocol.AssertionHash{Hash: i.Event.AssertionHash}) + require.NoError(t, err2) + + var parent rollupgen.AssertionNode + parent, err = retry.UntilSucceeds(neutralCtx, func() (rollupgen.AssertionNode, error) { + return honestChain.RollupCore().GetAssertion(&bind.CallOpts{Context: neutralCtx}, creationInfo.ParentAssertionHash.Hash) + }) + require.NoError(t, err) + + tx, _, err2 := bk.Client().TransactionByHash(neutralCtx, creationInfo.TransactionHash) + require.NoError(t, err2) + sender, err2 := gethtypes.Sender(gethtypes.NewCancunSigner(chainId), tx) + require.NoError(t, err2) + honestConfirmed := sender == honestTxOpts.From + + isChallengeChild := parent.FirstChildBlock > 0 && parent.SecondChildBlock > 0 + if !isChallengeChild { + // Assertion must be a challenge child. + continue + } + // We expect the honest party to have confirmed it. + if !honestConfirmed { + t.Fatal("Evil party confirmed the assertion by challenge win") + } + confirmed = true + break + } + time.Sleep(500 * time.Millisecond) // Don't spam the backend. + } + // Once the honest, claimed assertion in the challenge is confirmed by time, we win the test. + t.Log("Assertion was confirmed by time") + }) +} diff --git a/testing/endtoend/helpers_test.go b/testing/endtoend/helpers_test.go index 7ad4db6bc..5ac6707af 100644 --- a/testing/endtoend/helpers_test.go +++ b/testing/endtoend/helpers_test.go @@ -29,6 +29,7 @@ func setupAssertionChain( backend protocol.ChainBackend, rollup common.Address, txOpts *bind.TransactOpts, + opts ...solimpl.Opt, ) *solimpl.AssertionChain { t.Helper() assertionChainBinding, err := rollupgen.NewRollupUserLogic( @@ -46,6 +47,7 @@ func setupAssertionChain( txOpts, backend, solimpl.NewChainBackendTransactor(backend), + opts..., ) require.NoError(t, err) return chain diff --git a/testing/setup/rollup_stack.go b/testing/setup/rollup_stack.go index 747719d1d..c7c358996 100644 --- a/testing/setup/rollup_stack.go +++ b/testing/setup/rollup_stack.go @@ -171,9 +171,11 @@ type ChainSetup struct { useMockBridge bool useMockOneStepProver bool numAccountsToGen uint64 + numFundedAccounts uint64 minimumAssertionPeriod int64 challengeTestingOpts []challenge_testing.Opt StateManagerOpts []statemanager.Opt + StakeTokenAddress common.Address EnableFastConfirmation bool EnableSafeFastConfirmation bool } @@ -228,6 +230,12 @@ func WithNumAccounts(n uint64) Opt { } } +func WithNumFundedAccounts(n uint64) Opt { + return func(setup *ChainSetup) { + setup.numFundedAccounts = n + } +} + func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { ctx := context.Background() setp := &ChainSetup{ @@ -431,7 +439,8 @@ func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { if !ok { return nil, errors.New("could not set big int") } - for _, acc := range accs { + for i := 0; i < len(accs); i++ { + acc := accs[i] transferTx, err := tokenBindings.TestWETH9Transactor.Transfer(accs[0].TxOpts, acc.TxOpts.From, seed) if err != nil { return nil, errors.Wrap(err, "could not approve account") @@ -481,6 +490,7 @@ func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { setp.Addrs = addresses setp.Backend = backend setp.RollupConfig = cfg + setp.StakeTokenAddress = stakeToken return setp, nil }