diff --git a/assertions/BUILD.bazel b/assertions/BUILD.bazel index 5d8117be65..4d79bb0fca 100644 --- a/assertions/BUILD.bazel +++ b/assertions/BUILD.bazel @@ -36,7 +36,7 @@ go_library( go_test( name = "assertions_test", - size = "small", + size = "medium", srcs = [ "manager_test.go", "poster_test.go", @@ -52,6 +52,7 @@ go_test( "//solgen/go/bridgegen", "//solgen/go/mocksgen", "//solgen/go/rollupgen", + "//solgen/go/testgen", "//testing", "//testing/casttest", "//testing/mocks/state-provider", @@ -59,6 +60,8 @@ go_test( "@com_github_ethereum_go_ethereum//accounts/abi", "@com_github_ethereum_go_ethereum//accounts/abi/bind", "@com_github_ethereum_go_ethereum//common", + "@com_github_ethereum_go_ethereum//common/hexutil", + "@com_github_ethereum_go_ethereum//core/types", "@com_github_stretchr_testify//require", ], ) diff --git a/assertions/manager_test.go b/assertions/manager_test.go index cb093dd466..bdf61307a5 100644 --- a/assertions/manager_test.go +++ b/assertions/manager_test.go @@ -344,6 +344,7 @@ func TestFastConfirmation(t *testing.T) { ctx := context.Background() testData, err := setup.ChainsWithEdgeChallengeManager( setup.WithMockOneStepProver(), + setup.WithAutoDeposit(), setup.WithChallengeTestingOpts( challenge_testing.WithLayerZeroHeights(&protocol.LayerZeroHeights{ BlockChallengeHeight: 64, @@ -417,6 +418,7 @@ func TestFastConfirmationWithSafe(t *testing.T) { ctx := context.Background() testData, err := setup.ChainsWithEdgeChallengeManager( // setup.WithMockBridge(), + setup.WithAutoDeposit(), setup.WithMockOneStepProver(), setup.WithChallengeTestingOpts( challenge_testing.WithLayerZeroHeights(&protocol.LayerZeroHeights{ diff --git a/assertions/poster_test.go b/assertions/poster_test.go index da7ebe74a0..144497ed19 100644 --- a/assertions/poster_test.go +++ b/assertions/poster_test.go @@ -6,28 +6,135 @@ package assertions_test 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" + "github.com/ethereum/go-ethereum/common/hexutil" + gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/offchainlabs/bold/assertions" protocol "github.com/offchainlabs/bold/chain-abstraction" cm "github.com/offchainlabs/bold/challenge-manager" "github.com/offchainlabs/bold/challenge-manager/types" + "github.com/offchainlabs/bold/solgen/go/bridgegen" "github.com/offchainlabs/bold/solgen/go/mocksgen" + "github.com/offchainlabs/bold/solgen/go/rollupgen" + "github.com/offchainlabs/bold/solgen/go/testgen" challenge_testing "github.com/offchainlabs/bold/testing" statemanager "github.com/offchainlabs/bold/testing/mocks/state-provider" "github.com/offchainlabs/bold/testing/setup" ) func TestPostAssertion(t *testing.T) { - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + chainSetup, chalManager, assertionManager, stateManager := setupAssertionPosting(t) + aliceChain := chainSetup.Chains[0] + + // Force the posting of a new sequencer batch. + forceSequencerMessageBatchPosting( + t, ctx, chainSetup.Accounts[0].TxOpts, chainSetup.Addrs.SequencerInbox, chainSetup.Backend, + ) + + // We have enabled auto-deposits for this test, so we expect that before starting the challenge manager, + // Alice has an ERC20 deposit token balance of 0. After starting, we should expect a non-zero balance. + rollup, err := rollupgen.NewRollupUserLogic(chainSetup.Addrs.Rollup, chainSetup.Backend) + require.NoError(t, err) + requiredStake, err := rollup.BaseStake(&bind.CallOpts{}) + require.NoError(t, err) + stakeTokenAddr, err := rollup.StakeToken(&bind.CallOpts{}) + require.NoError(t, err) + + erc20, err := testgen.NewERC20Token(stakeTokenAddr, chainSetup.Backend) + require.NoError(t, err) + balance, err := erc20.BalanceOf(&bind.CallOpts{}, aliceChain.StakerAddress()) + require.NoError(t, err) + require.True(t, big.NewInt(0).Cmp(balance) == 0) + + chalManager.Start(ctx) + + // Wait a little for the chain watcher to be ready. + time.Sleep(time.Second) + + preState, err := stateManager.ExecutionStateAfterPreviousState(ctx, 0, protocol.GoGlobalState{}) + require.NoError(t, err) + postState, err := stateManager.ExecutionStateAfterPreviousState(ctx, 1, preState.GlobalState) + require.NoError(t, err) + nextState, err := stateManager.ExecutionStateAfterPreviousState(ctx, 2, postState.GlobalState) + require.NoError(t, err) + + // Expect a non-zero balance equal to the required stake after the challenge manager auto-deposited. + balance, err = erc20.BalanceOf(&bind.CallOpts{}, aliceChain.StakerAddress()) + require.NoError(t, err) + require.True(t, requiredStake.Cmp(balance) == 0) + + // Verify that alice can post an assertion correctly. + posted, err := assertionManager.PostAssertion(ctx) + require.NoError(t, err) + require.Equal(t, true, posted.IsSome()) + + creationInfo, err := aliceChain.ReadAssertionCreationInfo(ctx, posted.Unwrap().Id()) + require.NoError(t, err) + require.Equal(t, postState, protocol.GoExecutionStateFromSolidity(creationInfo.AfterState)) + + // Wait a little and advance the chain to allow the next assertion to be posted. + time.Sleep(time.Second * 5) + chainSetup.Backend.Commit() + + // Expect a zero ERC20 balance after the first staked assertion was posted. + balance, err = erc20.BalanceOf(&bind.CallOpts{}, aliceChain.StakerAddress()) + require.NoError(t, err) + require.True(t, big.NewInt(0).Cmp(balance) == 0) + + posted, err = assertionManager.PostAssertion(ctx) + require.NoError(t, err) + require.Equal(t, true, posted.IsSome()) + + creationInfo, err = aliceChain.ReadAssertionCreationInfo(ctx, posted.Unwrap().Id()) + require.NoError(t, err) + require.Equal(t, nextState, protocol.GoExecutionStateFromSolidity(creationInfo.AfterState)) + + // Continue to expect a zero ERC20 balance after the second assertion was posted, as no new + // stake was expected for the validator. + time.Sleep(time.Second * 5) + chainSetup.Backend.Commit() + + balance, err = erc20.BalanceOf(&bind.CallOpts{}, chainSetup.Accounts[0].TxOpts.From) + require.NoError(t, err) + require.True(t, big.NewInt(0).Cmp(balance) == 0) + + // We then filter all the transactions to the staken address from the validator and expect + // there was only a single deposit event (a transfer event with from set to 0x0). + it, err := erc20.FilterTransfer( + &bind.FilterOpts{ + Start: 0, + End: nil, + }, + []common.Address{{}}, + []common.Address{aliceChain.StakerAddress()}, + ) + require.NoError(t, err) + defer func() { + if err = it.Close(); err != nil { + t.Error(err) + } + }() + totalTransfers := 0 + for it.Next() { + totalTransfers++ + } + require.Equal(t, 1, totalTransfers, "Expected only one deposit event by the staker") +} + +func setupAssertionPosting(t *testing.T) (*setup.ChainSetup, *cm.Manager, *assertions.Manager, *statemanager.L2StateBackend) { setup, err := setup.ChainsWithEdgeChallengeManager( - // setup.WithMockBridge(), setup.WithMockOneStepProver(), + setup.WithAutoDeposit(), setup.WithChallengeTestingOpts( challenge_testing.WithLayerZeroHeights(&protocol.LayerZeroHeights{ BlockChallengeHeight: 64, @@ -77,19 +184,29 @@ func TestPostAssertion(t *testing.T) { cm.OverrideAssertionManager(assertionManager), ) require.NoError(t, err) - chalManager.Start(ctx) + return setup, chalManager, assertionManager, stateManager - preState, err := stateManager.ExecutionStateAfterPreviousState(ctx, 0, protocol.GoGlobalState{}) - require.NoError(t, err) - postState, err := stateManager.ExecutionStateAfterPreviousState(ctx, 1, preState.GlobalState) - require.NoError(t, err) - - time.Sleep(time.Second) +} - posted, err := assertionManager.PostAssertion(ctx) +func forceSequencerMessageBatchPosting( + t *testing.T, + ctx context.Context, + sequencerOpts *bind.TransactOpts, + seqInboxAddr common.Address, + backend *setup.SimulatedBackendWrapper, +) { + batchCompressedBytes := hexutil.MustDecode("0x94643ec208c5558027fa768281f28aa273f01537942cd58cdd9c17e97e30281f") + message := append([]byte{0}, batchCompressedBytes...) + seqNum := new(big.Int).Lsh(common.Big1, 256) + seqNum.Sub(seqNum, common.Big1) + seqInbox, err := bridgegen.NewSequencerInbox(seqInboxAddr, backend) require.NoError(t, err) - require.Equal(t, true, posted.IsSome()) - creationInfo, err := aliceChain.ReadAssertionCreationInfo(ctx, posted.Unwrap().Id()) + tx, err := seqInbox.AddSequencerL2BatchFromOrigin8f111f3c( + sequencerOpts, seqNum, message, big.NewInt(1), common.Address{}, big.NewInt(0), big.NewInt(0), + ) require.NoError(t, err) - require.Equal(t, postState, protocol.GoExecutionStateFromSolidity(creationInfo.AfterState)) + require.NoError(t, challenge_testing.WaitForTx(ctx, backend, tx)) + receipt, err := backend.TransactionReceipt(ctx, tx.Hash()) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) } diff --git a/chain-abstraction/sol-implementation/assertion_chain.go b/chain-abstraction/sol-implementation/assertion_chain.go index 91f3d01246..fb4b4c4dc2 100644 --- a/chain-abstraction/sol-implementation/assertion_chain.go +++ b/chain-abstraction/sol-implementation/assertion_chain.go @@ -630,8 +630,14 @@ func (a *AssertionChain) createAndStakeOnAssertion( return nil, errors.Wrapf(err, "could not fetch assertion with computed hash %#x", computedHash) default: } - if err = a.autoDepositFunds(ctx, parentAssertionCreationInfo.RequiredStake); err != nil { - return nil, errors.Wrapf(err, "could not auto-deposit funds for assertion creation") + staked, err := a.IsStaked(ctx) + if err != nil { + return nil, err + } + if !staked { + if err = a.autoDepositFunds(ctx, parentAssertionCreationInfo.RequiredStake); err != nil { + return nil, errors.Wrapf(err, "could not auto-deposit funds for assertion creation") + } } receipt, err := a.transact(ctx, a.backend, func(opts *bind.TransactOpts) (*types.Transaction, error) { return stakeFn( diff --git a/testing/setup/rollup_stack.go b/testing/setup/rollup_stack.go index 3ab89fef38..6401cef289 100644 --- a/testing/setup/rollup_stack.go +++ b/testing/setup/rollup_stack.go @@ -173,6 +173,7 @@ type ChainSetup struct { numAccountsToGen uint64 numFundedAccounts uint64 minimumAssertionPeriod int64 + autoDeposit bool challengeTestingOpts []challenge_testing.Opt StateManagerOpts []statemanager.Opt StakeTokenAddress common.Address @@ -236,10 +237,17 @@ func WithNumFundedAccounts(n uint64) Opt { } } +func WithAutoDeposit() Opt { + return func(setup *ChainSetup) { + setup.autoDeposit = true + } +} + func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { ctx := context.Background() setp := &ChainSetup{ numAccountsToGen: 4, + autoDeposit: false, } for _, o := range opts { o(setp) @@ -274,22 +282,24 @@ func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { if !ok { return nil, errors.New("could not set value") } - accs[0].TxOpts.Value = value - mintTx, err := tokenBindings.Deposit(accs[0].TxOpts) - if err != nil { - return nil, err - } - if waitErr := challenge_testing.WaitForTx(ctx, backend, mintTx); waitErr != nil { - return nil, errors.Wrap(waitErr, "errored waiting for transaction") - } - receipt, err = backend.TransactionReceipt(ctx, mintTx.Hash()) - if err != nil { - return nil, err - } - if receipt.Status != types.ReceiptStatusSuccessful { - return nil, errors.New("receipt not successful") + if !setp.autoDeposit { + accs[0].TxOpts.Value = value + mintTx, err3 := tokenBindings.Deposit(accs[0].TxOpts) + if err3 != nil { + return nil, err3 + } + if waitErr := challenge_testing.WaitForTx(ctx, backend, mintTx); waitErr != nil { + return nil, errors.Wrap(waitErr, "errored waiting for transaction") + } + receipt, err = backend.TransactionReceipt(ctx, mintTx.Hash()) + if err != nil { + return nil, err + } + if receipt.Status != types.ReceiptStatusSuccessful { + return nil, errors.New("receipt not successful") + } + accs[0].TxOpts.Value = big.NewInt(0) } - accs[0].TxOpts.Value = big.NewInt(0) prod := false wasmModuleRoot := common.Hash{} @@ -445,49 +455,51 @@ func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { if !ok { return nil, errors.New("could not set big int") } - for i := 0; i < len(accs); i++ { - acc := accs[i] - transferTx, err := tokenBindings.Transfer(accs[0].TxOpts, acc.TxOpts.From, seed) - if err != nil { - return nil, errors.Wrap(err, "could not approve account") - } - if waitErr := challenge_testing.WaitForTx(ctx, backend, transferTx); waitErr != nil { - return nil, errors.Wrap(waitErr, "errored waiting for transfer transaction") - } - receipt, err := backend.TransactionReceipt(ctx, transferTx.Hash()) - if err != nil { - return nil, errors.Wrap(err, "could not get tx receipt") - } - if receipt.Status != types.ReceiptStatusSuccessful { - return nil, errors.New("receipt not successful") - } - approveTx, err := tokenBindings.Approve(acc.TxOpts, addresses.Rollup, value) - if err != nil { - return nil, errors.Wrap(err, "could not approve account") - } - if waitErr := challenge_testing.WaitForTx(ctx, backend, approveTx); waitErr != nil { - return nil, errors.Wrap(waitErr, "errored waiting for approval transaction") - } - receipt, err = backend.TransactionReceipt(ctx, approveTx.Hash()) - if err != nil { - return nil, errors.Wrap(err, "could not get tx receipt") - } - if receipt.Status != types.ReceiptStatusSuccessful { - return nil, errors.New("receipt not successful") - } - approveTx, err = tokenBindings.Approve(acc.TxOpts, chalManagerAddr, value) - if err != nil { - return nil, errors.Wrap(err, "could not approve account") - } - if waitErr := challenge_testing.WaitForTx(ctx, backend, approveTx); waitErr != nil { - return nil, errors.Wrap(waitErr, "errored waiting for approval transaction") - } - receipt, err = backend.TransactionReceipt(ctx, approveTx.Hash()) - if err != nil { - return nil, errors.Wrap(err, "could not get tx receipt") - } - if receipt.Status != types.ReceiptStatusSuccessful { - return nil, errors.New("receipt not successful") + if !setp.autoDeposit { + for i := 0; i < len(accs); i++ { + acc := accs[i] + transferTx, err := tokenBindings.Transfer(accs[0].TxOpts, acc.TxOpts.From, seed) + if err != nil { + return nil, errors.Wrap(err, "could not approve account") + } + if waitErr := challenge_testing.WaitForTx(ctx, backend, transferTx); waitErr != nil { + return nil, errors.Wrap(waitErr, "errored waiting for transfer transaction") + } + receipt, err := backend.TransactionReceipt(ctx, transferTx.Hash()) + if err != nil { + return nil, errors.Wrap(err, "could not get tx receipt") + } + if receipt.Status != types.ReceiptStatusSuccessful { + return nil, errors.New("receipt not successful") + } + approveTx, err := tokenBindings.Approve(acc.TxOpts, addresses.Rollup, value) + if err != nil { + return nil, errors.Wrap(err, "could not approve account") + } + if waitErr := challenge_testing.WaitForTx(ctx, backend, approveTx); waitErr != nil { + return nil, errors.Wrap(waitErr, "errored waiting for approval transaction") + } + receipt, err = backend.TransactionReceipt(ctx, approveTx.Hash()) + if err != nil { + return nil, errors.Wrap(err, "could not get tx receipt") + } + if receipt.Status != types.ReceiptStatusSuccessful { + return nil, errors.New("receipt not successful") + } + approveTx, err = tokenBindings.Approve(acc.TxOpts, chalManagerAddr, value) + if err != nil { + return nil, errors.Wrap(err, "could not approve account") + } + if waitErr := challenge_testing.WaitForTx(ctx, backend, approveTx); waitErr != nil { + return nil, errors.Wrap(waitErr, "errored waiting for approval transaction") + } + receipt, err = backend.TransactionReceipt(ctx, approveTx.Hash()) + if err != nil { + return nil, errors.Wrap(err, "could not get tx receipt") + } + if receipt.Status != types.ReceiptStatusSuccessful { + return nil, errors.New("receipt not successful") + } } }