Skip to content

Commit

Permalink
Add E2E Test to Prove Honest Validator Can Be a Delegated Staker (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
rauljordan authored Feb 6, 2025
1 parent f708331 commit 0cac9c6
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 1 deletion.
274 changes: 274 additions & 0 deletions testing/endtoend/e2e_delegated_staking_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
2 changes: 2 additions & 0 deletions testing/endtoend/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -46,6 +47,7 @@ func setupAssertionChain(
txOpts,
backend,
solimpl.NewChainBackendTransactor(backend),
opts...,
)
require.NoError(t, err)
return chain
Expand Down
12 changes: 11 additions & 1 deletion testing/setup/rollup_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}

Expand Down

0 comments on commit 0cac9c6

Please sign in to comment.