diff --git a/.gitignore b/.gitignore index 71fa6671ee..1f2ef94e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ workspace .idea/ # build output /hive + +hiveview diff --git a/simulators/ethereum/engine/debug.Dockerfile b/simulators/ethereum/engine/debug.Dockerfile index 48b767afd3..3556b04363 100644 --- a/simulators/ethereum/engine/debug.Dockerfile +++ b/simulators/ethereum/engine/debug.Dockerfile @@ -19,7 +19,7 @@ ENV GO111MODULE=on RUN go install github.com/go-delve/delve/cmd/dlv@latest ADD . $GOPATH/src/github.com/gnosischain/hive/simulators/ethereum/engine WORKDIR $GOPATH/src/github.com/gnosischain/hive/simulators/ethereum/engine -RUN go build -gcflags="all=-N -l" -v . +RUN go build -gcflags="all=-N -l" -v . # Build the simulator run container. FROM alpine:latest diff --git a/simulators/ethereum/engine/libgno/gno.go b/simulators/ethereum/engine/libgno/gno.go index 15818998ca..9162e7cbf3 100644 --- a/simulators/ethereum/engine/libgno/gno.go +++ b/simulators/ethereum/engine/libgno/gno.go @@ -102,7 +102,7 @@ func TransferData(recipient common.Address, amount *big.Int) ([]byte, error) { // `From` = deposit contract address // `To` = all addresses from addresses argument // -// for specific block range and returns map { withdrawalAddress: transferAmount } +// for specific block range and returns accumulated map { withdrawalAddress: Sum(transferAmounts) } func GetWithdrawalsTransferEvents(client *ethclient.Client, addresses []common.Address, fromBlock, toBlock uint64) (map[string]*big.Int, error) { transfersList := make(map[string]*big.Int, 0) token, err := NewGnoToken(GNOTokenAddress, client) @@ -122,14 +122,27 @@ func GetWithdrawalsTransferEvents(client *ethclient.Client, addresses []common.A } for eventsIterator.Next() { + addr := eventsIterator.Event.To.Hex() amount := eventsIterator.Event.Value - - transfersList[addr] = amount + if _, ok := transfersList[addr]; ok { + transfersList[addr].Add(transfersList[addr], amount) + } else { + transfersList[addr] = amount + } } return transfersList, nil } +func GetBalanceOf(client *ethclient.Client, account common.Address, block *big.Int) (*big.Int, error) { + opts := &bind.CallOpts{Pending: false, BlockNumber: block} + token, err := NewGnoToken(GNOTokenAddress, client) + if err != nil { + return nil, fmt.Errorf("can't bind token contract: %w", err) + } + return token.BalanceOf(opts, account) +} + // GetGNOTokenABI return the GNO token ABI. func GetGNOTokenABI() (*abi.ABI, error) { gnoTokenABI, err := abi.JSON(strings.NewReader(GNOTokenContractABI)) @@ -147,3 +160,9 @@ func GetWithdrawalsABI() (*abi.ABI, error) { } return &withdrawalsABI, nil } + +// UnwrapToGNO takes mGNO wrapped token amount and returns GNO (32 mGNO == 1 GNO) amount +// see https://docs.gnosischain.com/node/faq/#what-is-mgno for rationale +func UnwrapToGNO(amount *big.Int) *big.Int { + return amount.Div(amount, big.NewInt(32)) +} diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go index 7d9e4de7e7..5777d2a36a 100644 --- a/simulators/ethereum/engine/suites/withdrawals/tests.go +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -103,18 +103,18 @@ var Tests = []test.SpecInterface{ // WithdrawalsPerBlock: 16, // TimeIncrements: 5, // }, - // - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdrawals Fork on Block 1", - // About: ` - // Tests the withdrawals fork happening directly after genesis. - // `, - // }, - // WithdrawalsForkHeight: 1, // Only Genesis is Pre-Withdrawals - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 16, - //}, + + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdrawals Fork on Block 1", + // About: ` + // Tests the withdrawals fork happening directly after genesis. + // `, + // }, + // WithdrawalsForkHeight: 1, // Only Genesis is Pre-Withdrawals + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 16, + // }, // &WithdrawalsBaseSpec{ // Spec: test.Spec{ @@ -132,109 +132,109 @@ var Tests = []test.SpecInterface{ // TimeIncrements: 5, // }, - // TODO: Fix this test. It's reverting the block, which is the expected behavior. - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdrawals Fork on Block 3", - // About: ` - // Tests the transition to the withdrawals fork after two blocks - // have happened. - // Block 2 is sent with invalid non-null withdrawals payload and - // client is expected to respond with the appropriate error. - // `, - // }, - // WithdrawalsForkHeight: 3, // Genesis, Block 1 and 2 are Pre-Withdrawals - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 16, - // TimeIncrements: 5, - //}, - // - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdraw to a single account", - // About: ` - // Make multiple withdrawals to a single account. - // `, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 64, - // WithdrawableAccountCount: 1, - //}, - // - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdraw to two accounts", - // About: ` - // Make multiple withdrawals to two different accounts, repeated in - // round-robin. - // Reasoning: There might be a difference in implementation when an - // account appears multiple times in the withdrawals list but the list - // is not in ordered sequence. - // `, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 64, - // WithdrawableAccountCount: 2, - //}, + // // TODO: Fix this test. It's reverting the block, which is the expected behavior. + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdrawals Fork on Block 3", + // About: ` + // Tests the transition to the withdrawals fork after two blocks + // have happened. + // Block 2 is sent with invalid non-null withdrawals payload and + // client is expected to respond with the appropriate error. + // `, + // }, + // WithdrawalsForkHeight: 3, // Genesis, Block 1 and 2 are Pre-Withdrawals + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 16, + // TimeIncrements: 5, + // }, - // TODO: Fix this test, it's reverting the block, which is the expected behavior. - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdraw many accounts", - // About: ` - // Make multiple withdrawals to 1024 different accounts. - // Execute many blocks this way. - // `, - // TimeoutSeconds: 240, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 4, - // WithdrawalsPerBlock: 1024, - // WithdrawableAccountCount: 1024, - //}, + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdraw to a single account", + // About: ` + // Make multiple withdrawals to a single account. + // `, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 64, + // WithdrawableAccountCount: 1, + // }, - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdraw zero amount", - // About: ` - // Make multiple withdrawals where the amount withdrawn is 0. - // `, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 64, - // WithdrawableAccountCount: 2, - // WithdrawAmounts: []uint64{ - // 0, - // 1, - // }, - //}, - // - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Empty Withdrawals", - // About: ` - // Produce withdrawals block with zero withdrawals. - // `, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 0, - //}, - // - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Corrupted Block Hash Payload (INVALID)", - // About: ` - // Send a valid payload with a corrupted hash using engine_newPayloadV2. - // `, - // }, - // WithdrawalsForkHeight: 1, - // WithdrawalsBlockCount: 1, - // TestCorrupedHashPayloads: true, - //}, + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdraw to two accounts", + // About: ` + // Make multiple withdrawals to two different accounts, repeated in + // round-robin. + // Reasoning: There might be a difference in implementation when an + // account appears multiple times in the withdrawals list but the list + // is not in ordered sequence. + // `, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 64, + // WithdrawableAccountCount: 2, + // }, + + // // TODO: Fix this test, it's reverting the block, which is the expected behavior. + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdraw many accounts", + // About: ` + // Make multiple withdrawals to 1024 different accounts. + // Execute many blocks this way. + // `, + // TimeoutSeconds: 240, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 4, + // WithdrawalsPerBlock: 1024, + // WithdrawableAccountCount: 1024, + // }, + + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdraw zero amount", + // About: ` + // Make multiple withdrawals where the amount withdrawn is 0. + // `, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 64, + // WithdrawableAccountCount: 2, + // WithdrawAmounts: []uint64{ + // 0, + // 1, + // }, + // }, + + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Empty Withdrawals", + // About: ` + // Produce withdrawals block with zero withdrawals. + // `, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 0, + // }, + + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Corrupted Block Hash Payload (INVALID)", + // About: ` + // Send a valid payload with a corrupted hash using engine_newPayloadV2. + // `, + // }, + // WithdrawalsForkHeight: 1, + // WithdrawalsBlockCount: 1, + // TestCorrupedHashPayloads: true, + // }, // Block value tests //&BlockValueSpec{ @@ -568,6 +568,35 @@ var Tests = []test.SpecInterface{ // OverflowMaxInitcodeTxCountAfterFork: 1, //}, + //// Execution layer withdrawals spec + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 1, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 16, + TimeIncrements: 5, + }, + ClaimBlocksCount: 1, + }, + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 2", + About: ` + `, + }, + WithdrawalsForkHeight: 1, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + TimeIncrements: 5, + }, + ClaimBlocksCount: 2, + }, &WithdrawalsExecutionLayerSpec{ WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ Spec: test.Spec{ @@ -580,6 +609,78 @@ var Tests = []test.SpecInterface{ WithdrawalsPerBlock: 16, TimeIncrements: 5, }, + ClaimBlocksCount: 2, + }, + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 2, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + TimeIncrements: 5, + }, + ClaimBlocksCount: 5, + }, + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 1, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + TimeIncrements: 5, + }, + ClaimBlocksCount: 2, + }, + + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 5, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 32, + TimeIncrements: 5, + }, + ClaimBlocksCount: 2, + }, + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 2, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 10, + WithdrawalsPerBlock: 256, + TimeIncrements: 5, + }, + ClaimBlocksCount: 2, + }, + &WithdrawalsExecutionLayerSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 5", + About: ` + `, + }, + WithdrawalsForkHeight: 1, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 3, + WithdrawalsPerBlock: 1024, + TimeIncrements: 5, + }, + ClaimBlocksCount: 4, }, } @@ -606,6 +707,21 @@ func (wh WithdrawalsHistory) GetExpectedAccumulatedBalance(account common.Addres return balance } +// Gets an accumulated account balance to a range fromBlock --> toBlock +func (wh WithdrawalsHistory) GetExpectedAccumulatedBalanceDelta(account common.Address, fromBlock, toBlock uint64) *big.Int { + balance := big.NewInt(0) + for b := fromBlock; b <= toBlock; b++ { + if withdrawals, ok := wh[b]; ok && withdrawals != nil { + for _, withdrawal := range withdrawals { + if withdrawal.Address == account { + balance.Add(balance, WeiAmount(withdrawal)) + } + } + } + } + return balance +} + // Get a list of all addresses that were credited by withdrawals on a given block. func (wh WithdrawalsHistory) GetAddressesWithdrawnOnBlock(block uint64) []common.Address { addressMap := make(map[common.Address]bool) @@ -778,9 +894,9 @@ func (ws *WithdrawalsBaseSpec) ConfigureCLMock(cl *clmock.CLMocker) { func (ws *WithdrawalsBaseSpec) GetPreWithdrawalsBlockCount() uint64 { if ws.WithdrawalsForkHeight == 0 { return 0 - } else { - return ws.WithdrawalsForkHeight - 1 } + return ws.WithdrawalsForkHeight - 1 + } // Number of payloads to be produced (pre and post withdrawals) during the entire test @@ -962,13 +1078,9 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { } defer client.Close() - // Produce requested post-shanghai blocks - // (At least 3 block will be produced after this procedure ends). - // Since we implemented a pull strategy for withdrawals, we needed to - // process 3 blocks: + // Produce requested post-shanghai blocks: // 1. Send some withdrawals and transactions - // 2. Check withdrawable amount, claim tokens and check balance is as expected - // 3. + // 2. Check that contract withdrawable amount matches the expectations for i := 0; i < int(ws.WithdrawalsBlockCount); i++ { t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { @@ -1006,118 +1118,28 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { OnForkchoiceBroadcast: func() { if !ws.SkipBaseVerifications { for _, addr := range ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(t.CLMock.LatestExecutedPayload.Number) { - //Test balance at `latest`, which should have the withdrawal applied. - latestBalance, err := getWithdrawableAmount(client, addr, big.NewInt(int64(t.CLMock.LatestExecutedPayload.Number))) + // Test balance at `latest`, which should have the withdrawal applied. + withdrawableAmount, err := getWithdrawableAmount(client, addr, big.NewInt(int64(t.CLMock.LatestExecutedPayload.Number))) if err != nil { t.Fatalf("FAIL (%s): Error trying to get balance of token: %v, address: %v", t.TestName, err, addr.Hex()) } - // TODO: - newLatestBalance := latestBalance.Mul(latestBalance, big.NewInt(32)) - expectBalanceEqual := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number) - if newLatestBalance.Cmp(expectBalanceEqual) != 0 { + expectBalanceMGNO := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number) + expectBalance := libgno.UnwrapToGNO(expectBalanceMGNO) + + if withdrawableAmount.Cmp(expectBalance) != 0 { t.Fatalf( "FAIL (%s): Incorrect balance on account %s after withdrawals applied: want=%d, got=%d", t.TestName, addr, - expectBalanceEqual, - latestBalance, + expectBalance, + withdrawableAmount, ) } } } }, }) - - t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ - - OnPayloadProducerSelected: func() { - // Get ExecuteWithdrawalsClaims - addresses := make([]common.Address, 0) - for _, w := range ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber-1] { - addresses = append(addresses, w.Address) - } - // Send claim transaction - claims, err := libgno.ClaimWithdrawalsData(addresses) - if err != nil { - t.Fatalf("FAIL (%s): Cant create claimWithdrawals transaction payload: %v", t.TestName, err) - } - _, err = helper.SendNextTransactionWithAccount( - t.TestContext, - t.CLMock.NextBlockProducer, - &helper.BaseTransactionCreator{ - Recipient: &libgno.WithdrawalsContractAddress, - Amount: common.Big0, - Payload: claims, - PrivateKey: globals.GnoVaultVaultKey, - TxType: t.TestTransactionType, - GasLimit: t.Genesis.GasLimit(), - ChainID: t.Genesis.Config().ChainID, - }, - globals.GnoVaultAccountAddress, - ) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to send claim transaction: %v", t.TestName, err) - } - }, - OnGetPayload: func() { - if !ws.SkipBaseVerifications { - block := t.CLMock.LatestExecutedPayload.Number - addresses := ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(block - 1) - transfersMap, err := libgno.GetWithdrawalsTransferEvents(client, addresses, block, block) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to get claims transfer events: %w", t.TestName, err) - } - - for _, addr := range addresses { - //Test balance at `latest`, which should have the withdrawal applied. - latestBalance, err := getBalanceOfToken(client, addr, big.NewInt(int64(t.CLMock.LatestExecutedPayload.Number))) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to get balance of token: %v, address: %v", t.TestName, err, addr.Hex()) - } - eventValue := transfersMap[addr.Hex()] - if eventValue == nil { - t.Fatalf( - "FAIL (%s): No value withdrawal transfer value presented in events list for address: %v", - t.TestName, addr.Hex(), - ) - } - // check value from event equal balance - if latestBalance.Cmp(eventValue) != 0 { - t.Fatalf( - "FAIL (%s): Transfer event value is not equal latest balance for address %s: want=%d (actual), got=%d (from event)", - t.TestName, addr.Hex(), latestBalance, transfersMap[addr.Hex()], - ) - } - } - } - }, - OnForkchoiceBroadcast: func() { - if !ws.SkipBaseVerifications { - block := t.CLMock.LatestExecutedPayload.Number - 1 - addresses := ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(block) - for _, addr := range addresses { - //Test balance at `latest`, which should have the withdrawal applied. - latestBalance, err := getBalanceOfToken(client, addr, big.NewInt(int64(t.CLMock.LatestExecutedPayload.Number))) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to get balance of token: %v, address: %v", t.TestName, err, addr.Hex()) - } - - // TODO: - newLatestBalance := latestBalance.Mul(latestBalance, big.NewInt(32)) - expectBalanceEqual := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number-1) - - if newLatestBalance.Cmp(expectBalanceEqual) != 0 { - t.Fatalf( - "FAIL (%s): Incorrect balance on account %s after withdrawals applied: want=%d, got=%d", - t.TestName, addr, expectBalanceEqual, latestBalance, - ) - } - } - - } - }, - }) } // Iterate over balance history of withdrawn accounts using RPC and @@ -1161,25 +1183,23 @@ func getClient(t *test.Env) *ethclient.Client { return client } -func getBalanceOfToken(client *ethclient.Client, account common.Address, block *big.Int) (*big.Int, error) { - // get GnoTokenABI - tokenABI, err := libgno.GetGNOTokenABI() +// getBalanceChangeDelta calls balanceOf for fromBlock and toBlock heights and returns balance delta +// i.e. how much balance changed from block X to block Y +func getBalanceChangeDelta(client *ethclient.Client, account common.Address, fromBlock, toBlock *big.Int) (*big.Int, error) { + fromBalance, err := libgno.GetBalanceOf(client, account, fromBlock) if err != nil { return nil, err } - var result []interface{} - opts := &bind.CallOpts{Pending: false, BlockNumber: block} - - //Call the balanceOf function - contract := bind.NewBoundContract(libgno.GNOTokenAddress, *tokenABI, client, client, client) - err = contract.Call(opts, &result, "balanceOf", account) + toBalance, err := libgno.GetBalanceOf(client, account, toBlock) if err != nil { return nil, err } - if len(result) != 1 { - return nil, fmt.Errorf("unexpected result length: %d", len(result)) + diff := big.NewInt(0).Sub(toBalance, fromBalance) + // if diff is positive return it, othervise return -(diff) + if toBalance.Cmp(fromBalance) == 1 { + return diff, nil } - return result[0].(*big.Int), nil + return diff.Neg(diff), nil } // getWithdrawableAmount returns withdrawableAmount for specific address from deposit contract @@ -1619,6 +1639,97 @@ func (ws *WithdrawalsReorgSpec) Execute(t *test.Env) { // Withdrawals execution layer spec: type WithdrawalsExecutionLayerSpec struct { *WithdrawalsBaseSpec + ClaimBlocksCount int +} + +func (ws *WithdrawalsExecutionLayerSpec) claimBlocksCount() int { + if ws.ClaimBlocksCount == 0 { + return 1 + } + return ws.ClaimBlocksCount +} + +// ClaimWithdrawals sends claimWithdrawals() call to deposit contract with list of addresses +// from withdrawals history +func (ws *WithdrawalsBaseSpec) ClaimWithdrawals(t *test.Env) { + // Get ExecuteWithdrawalsClaims + addresses := make([]common.Address, 0) + for _, w := range ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber-1] { + addresses = append(addresses, w.Address) + } + // Send claim transaction + claims, err := libgno.ClaimWithdrawalsData(addresses) + if err != nil { + t.Fatalf("FAIL (%s): Cant create claimWithdrawals transaction payload: %v", t.TestName, err) + } + _, err = helper.SendNextTransactionWithAccount( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &libgno.WithdrawalsContractAddress, + Amount: common.Big0, + Payload: claims, + PrivateKey: globals.GnoVaultVaultKey, + TxType: t.TestTransactionType, + GasLimit: t.Genesis.GasLimit(), + ChainID: t.Genesis.Config().ChainID, + }, + globals.GnoVaultAccountAddress, + ) + if err != nil { + t.Fatalf("FAIL (%s): Error trying to send claim transaction: %v", t.TestName, err) + } +} + +// VerifyClaimsExecution verifies that: +// +// sum(withdrawals) == ERC20 balance delta == sum(transfer events values) +// +// for provided block range +func (ws *WithdrawalsExecutionLayerSpec) VerifyClaimsExecution( + t *test.Env, client *ethclient.Client, fromBlock, toBlock uint64, +) { + addresses := ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(toBlock - 1) + transfersMap, err := libgno.GetWithdrawalsTransferEvents(client, addresses, fromBlock, toBlock) + if err != nil { + t.Fatalf("FAIL (%s): Error trying to get claims transfer events: %w", t.TestName, err) + } + if len(addresses) == 0 { + t.Fatalf("FAIL (%s): No withdrawal addresses found: %w", t.TestName) + } + for _, addr := range addresses { + balanceDelta, err := getBalanceChangeDelta(client, addr, big.NewInt(int64(fromBlock)), big.NewInt(int64(toBlock))) + if err != nil { + t.Fatalf("FAIL (%s): Error trying to get balance delta of token: %v, address: %v, from block %d to block %d", t.TestName, err, addr.Hex(), fromBlock, toBlock) + } + + withdrawalsAccumulatedDeltaMGNO := ws.WithdrawalsHistory.GetExpectedAccumulatedBalanceDelta(addr, fromBlock, toBlock) + withdrawalsDeltaGNO := libgno.UnwrapToGNO(withdrawalsAccumulatedDeltaMGNO) + + // check that account balance == expected balance from withdrawals history + if balanceDelta.Cmp(withdrawalsDeltaGNO) != 0 { + t.Fatalf( + "FAIL (%s): Incorrect balance on account %s after withdrawals applied: want=%d, got=%d", + t.TestName, addr, balanceDelta, withdrawalsDeltaGNO, + ) + } + // if block range is >1 there will be the trasfered sum from all transfer events + // for specific address in block range + eventValue := transfersMap[addr.Hex()] + if eventValue == nil { + t.Fatalf( + "FAIL (%s): No withdrawal transfer value presented in events list for address: %v", + t.TestName, addr.Hex(), + ) + } + // check that account balance == value from transfer event + if balanceDelta.Cmp(eventValue) != 0 { + t.Fatalf( + "FAIL (%s): Transfer event value is not equal latest balance for address %s: want=%d (actual), got=%d (from event)", + t.TestName, addr.Hex(), balanceDelta, transfersMap[addr.Hex()], + ) + } + } } func (ws *WithdrawalsExecutionLayerSpec) Execute(t *test.Env) { @@ -1673,15 +1784,13 @@ func (ws *WithdrawalsExecutionLayerSpec) Execute(t *test.Env) { } defer client.Close() - // Produce requested post-shanghai blocks - // (At least 3 block will be produced after this procedure ends). - // Since we implemented a pull strategy for withdrawals, we needed to - // process 3 blocks: - // 1. Send some withdrawals and transactions - // 2. Check withdrawable amount, claim tokens and check balance is as expected - // 3. + // Produce requested post-shanghai blocks: + // 1. Produce WithdrawalsBlockCount number of blocks with withdrawals + // 2. Claim accumulated withdrawals and verify claims execution and balances + // 3. Iteratively produce block pairs (first with withdrawals and second with claim Tx) + // and verify every claim execution for i := 0; i < int(ws.WithdrawalsBlockCount); i++ { - t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ + t.CLMock.ProduceBlocks(int(ws.WithdrawalsBlockCount), clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { // Send some withdrawals t.CLMock.NextWithdrawals, nextIndex = ws.GenerateWithdrawalsForBlock(nextIndex, startAccount) @@ -1690,80 +1799,37 @@ func (ws *WithdrawalsExecutionLayerSpec) Execute(t *test.Env) { }) } - // for i := 0; i < ws.numberOfClaims(); i++ { - // claim accumulated withdrawals on that block t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ - OnPayloadProducerSelected: func() { - // Get ExecuteWithdrawalsClaims - addresses := make([]common.Address, 0) - for _, w := range ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber-1] { - addresses = append(addresses, w.Address) - } - // Send claim transaction - claims, err := libgno.ClaimWithdrawalsData(addresses) - if err != nil { - t.Fatalf("FAIL (%s): Cant create claimWithdrawals transaction payload: %v", t.TestName, err) - } - _, err = helper.SendNextTransactionWithAccount( - t.TestContext, - t.CLMock.NextBlockProducer, - &helper.BaseTransactionCreator{ - Recipient: &libgno.WithdrawalsContractAddress, - Amount: common.Big0, - Payload: claims, - PrivateKey: globals.GnoVaultVaultKey, - TxType: t.TestTransactionType, - GasLimit: t.Genesis.GasLimit(), - ChainID: t.Genesis.Config().ChainID, - }, - globals.GnoVaultAccountAddress, - ) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to send claim transaction: %v", t.TestName, err) - } - }}) + ws.ClaimWithdrawals(t) + }, + OnForkchoiceBroadcast: func() { + ws.VerifyClaimsExecution(t, client, ws.WithdrawalsForkHeight, t.CLMock.LatestExecutedPayload.Number) + }, + }) - if !ws.SkipBaseVerifications { - block := t.CLMock.LatestExecutedPayload.Number - addresses := ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(block - 1) - transfersMap, err := libgno.GetWithdrawalsTransferEvents(client, addresses, block, block) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to get claims transfer events: %w", t.TestName, err) - } - if len(addresses) == 0 { - t.Fatalf("FAIL (%s): No withdrawal addresses found: %w", t.TestName) - } - for _, addr := range addresses { - //Test balance at `latest`, which should have the withdrawal applied. - latestBalance, err := getBalanceOfToken(client, addr, big.NewInt(int64(t.CLMock.LatestExecutedPayload.Number))) - if err != nil { - t.Fatalf("FAIL (%s): Error trying to get balance of token: %v, address: %v", t.TestName, err, addr.Hex()) - } + // performs multiple iterations of withdrawals generation and claiming + if !ws.SkipBaseVerifications && ws.claimBlocksCount() > 1 { + for i := 1; i <= ws.claimBlocksCount(); i++ { + t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + // Send some withdrawals + t.CLMock.NextWithdrawals, nextIndex = ws.GenerateWithdrawalsForBlock(nextIndex, startAccount) + ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals + }, + }) + t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + ws.ClaimWithdrawals(t) + }, + OnForkchoiceBroadcast: func() { + ws.VerifyClaimsExecution(t, client, t.CLMock.LatestExecutedPayload.Number-1, t.CLMock.LatestExecutedPayload.Number) + }, + }) - withdrawalsAccumulated := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number-1) - withdrawalsAccumulated.Div(withdrawalsAccumulated, big.NewInt(32)) - // check that account balance == expected balance from withdrawals history - if latestBalance.Cmp(withdrawalsAccumulated) != 0 { - t.Fatalf( - "FAIL (%s): Incorrect balance on account %s after withdrawals applied: want=%d, got=%d", - t.TestName, addr, latestBalance, withdrawalsAccumulated, - ) - } - eventValue := transfersMap[addr.Hex()] - if eventValue == nil { - t.Fatalf( - "FAIL (%s): No withdrawal transfer value presented in events list for address: %v", - t.TestName, addr.Hex(), - ) - } - // check that account balance == value from transfer event - if latestBalance.Cmp(eventValue) != 0 { - t.Fatalf( - "FAIL (%s): Transfer event value is not equal latest balance for address %s: want=%d (actual), got=%d (from event)", - t.TestName, addr.Hex(), latestBalance, transfersMap[addr.Hex()], - ) - } } } + if !ws.SkipBaseVerifications { + ws.VerifyClaimsExecution(t, client, ws.WithdrawalsForkHeight, t.CLMock.LatestExecutedPayload.Number) + } }