diff --git a/simulators/ethereum/engine/libgno/gno.go b/simulators/ethereum/engine/libgno/gno.go index c0fea3682b..15818998ca 100644 --- a/simulators/ethereum/engine/libgno/gno.go +++ b/simulators/ethereum/engine/libgno/gno.go @@ -7,7 +7,9 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" ) const ( @@ -95,6 +97,39 @@ func TransferData(recipient common.Address, amount *big.Int) ([]byte, error) { return dataBytes, nil } +// GetWithdrawalsTransferEvents get all transfer events where: +// +// `From` = deposit contract address +// `To` = all addresses from addresses argument +// +// for specific block range and returns map { withdrawalAddress: transferAmount } +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) + if err != nil { + return nil, fmt.Errorf("can't bind token contract: %w", err) + } + eventsIterator, err := token.FilterTransfer( + &bind.FilterOpts{ + Start: fromBlock, + End: &toBlock, + }, + []common.Address{WithdrawalsContractAddress}, + addresses, + ) + if err != nil { + return nil, fmt.Errorf("can't filter transfers: %w", err) + } + + for eventsIterator.Next() { + addr := eventsIterator.Event.To.Hex() + amount := eventsIterator.Event.Value + + transfersList[addr] = amount + } + return transfersList, nil +} + // GetGNOTokenABI return the GNO token ABI. func GetGNOTokenABI() (*abi.ABI, error) { gnoTokenABI, err := abi.JSON(strings.NewReader(GNOTokenContractABI)) diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go index ce260068b7..ae3f26ce39 100644 --- a/simulators/ethereum/engine/suites/withdrawals/tests.go +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -91,18 +91,18 @@ var ( // List of all withdrawals tests var Tests = []test.SpecInterface{ - &WithdrawalsBaseSpec{ - Spec: test.Spec{ - Name: "Withdawals Fork on Block 1", - About: ` - Tests the withdrawals fork happening on block 1, Block 0 is for Aura. - `, - }, - WithdrawalsForkHeight: 1, //TODO - WithdrawalsBlockCount: 1, // Genesis is not a withdrawals block - WithdrawalsPerBlock: 16, - TimeIncrements: 5, - }, + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdawals Fork on Block 1", + // About: ` + // Tests the withdrawals fork happening on block 1, Block 0 is for Aura. + // `, + // }, + // WithdrawalsForkHeight: 1, //TODO + // WithdrawalsBlockCount: 1, // Genesis is not a withdrawals block + // WithdrawalsPerBlock: 16, + // TimeIncrements: 5, + // }, // //&WithdrawalsBaseSpec{ // Spec: test.Spec{ @@ -116,22 +116,21 @@ var Tests = []test.SpecInterface{ // WithdrawalsPerBlock: 16, //}, - // TODO: Fix this test - //&WithdrawalsBaseSpec{ - // Spec: test.Spec{ - // Name: "Withdrawals Fork on Block 5", - // About: ` - // Tests the transition to the withdrawals fork after a single block - // has happened. - // Block 1 is sent with invalid non-null withdrawals payload and - // client is expected to respond with the appropriate error. - // `, - // }, - // WithdrawalsForkHeight: 5, // Genesis and Block 1 are Pre-Withdrawals - // WithdrawalsBlockCount: 1, - // WithdrawalsPerBlock: 16, - // TimeIncrements: 5, - //}, + // &WithdrawalsBaseSpec{ + // Spec: test.Spec{ + // Name: "Withdrawals Fork on Block 5", + // About: ` + // Tests the transition to the withdrawals fork after a single block + // has happened. + // Block 1 is sent with invalid non-null withdrawals payload and + // client is expected to respond with the appropriate error. + // `, + // }, + // WithdrawalsForkHeight: 5, // Genesis and Block 1 are Pre-Withdrawals + // WithdrawalsBlockCount: 1, + // WithdrawalsPerBlock: 16, + // TimeIncrements: 5, + // }, // TODO: Fix this test. It's reverting the block, which is the expected behavior. //&WithdrawalsBaseSpec{ @@ -553,6 +552,7 @@ var Tests = []test.SpecInterface{ // ReOrgViaSync: true, // SidechainTimeIncrements: 1, //}, + //// TODO: REORG SYNC WHERE SYNCED BLOCKS HAVE WITHDRAWALS BEFORE TIME // //// EVM Tests (EIP-3651, EIP-3855, EIP-3860) @@ -567,6 +567,21 @@ var Tests = []test.SpecInterface{ // OverflowMaxInitcodeTxCountBeforeFork: 0, // OverflowMaxInitcodeTxCountAfterFork: 1, //}, + + &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, + }, + NumberOfClaims: 2, + }, } // Helper types to convert gwei into wei more easily @@ -577,9 +592,8 @@ func WeiAmount(w *types.Withdrawal) *big.Int { // Helper structure used to keep history of the amounts withdrawn to each test account. type WithdrawalsHistory map[uint64]types.Withdrawals -// Gets an account expected value for a given block, taking into account all -// withdrawals that credited the account. -func (wh WithdrawalsHistory) GetExpectedAccountBalance(account common.Address, block uint64) *big.Int { +// Gets an accumulated account balance range of blocks 0 --> given block +func (wh WithdrawalsHistory) GetExpectedAccumulatedBalance(account common.Address, block uint64) *big.Int { balance := big.NewInt(0) for b := uint64(0); b <= block; b++ { if withdrawals, ok := wh[b]; ok && withdrawals != nil { @@ -849,14 +863,22 @@ func (ws *WithdrawalsBaseSpec) sendPayloadTransactions(t *test.Env) { } } +func (ws *WithdrawalsBaseSpec) waitForShaghai(t *test.Env) { + if time.Now().Unix() < int64(*t.Genesis.Config().ShanghaiTime) { + tUnix := time.Unix(t.CLMock.ShanghaiTimestamp.Int64(), 0) + durationUntilFuture := time.Until(tUnix) + if durationUntilFuture > 0 { + time.Sleep(durationUntilFuture) + } + } +} + // Base test case execution procedure for withdrawals func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { // Create the withdrawals history object ws.WithdrawalsHistory = make(WithdrawalsHistory) shangaiTime := *t.Genesis.Config().ShanghaiTime t.CLMock.ShanghaiTimestamp = big.NewInt(0).SetUint64(shangaiTime) - // Create a time.Time value from the int64 timestamp - tUnix := time.Unix(t.CLMock.ShanghaiTimestamp.Int64(), 0) // Wait ttd t.CLMock.WaitForTTD() @@ -926,14 +948,7 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { }, }) - // Wait for shangai - if time.Now().Unix() < int64(*t.Genesis.Config().ShanghaiTime) { - tUnix = time.Unix(t.CLMock.ShanghaiTimestamp.Int64(), 0) - durationUntilFuture := time.Until(tUnix) - if durationUntilFuture > 0 { - time.Sleep(durationUntilFuture) - } - } + ws.waitForShaghai(t) var ( startAccount = ws.GetWithdrawalsStartAccount() @@ -997,8 +1012,9 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { 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.GetExpectedAccountBalance(addr, t.CLMock.LatestExecutedPayload.Number) + expectBalanceEqual := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number) if newLatestBalance.Cmp(expectBalanceEqual) != 0 { t.Fatalf( @@ -1045,17 +1061,52 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { 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 { - for _, addr := range ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(t.CLMock.LatestExecutedPayload.Number - 1) { + 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.GetExpectedAccountBalance(addr, t.CLMock.LatestExecutedPayload.Number-1) + expectBalanceEqual := ws.WithdrawalsHistory.GetExpectedAccumulatedBalance(addr, t.CLMock.LatestExecutedPayload.Number-1) if newLatestBalance.Cmp(expectBalanceEqual) != 0 { t.Fatalf( @@ -1064,6 +1115,7 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { ) } } + } }, }) @@ -1564,3 +1616,155 @@ func (ws *WithdrawalsReorgSpec) Execute(t *test.Env) { }, nil) r.ExpectPayloadStatus(test.Valid) } + +// Withdrawals execution layer spec: +type WithdrawalsExecutionLayerSpec struct { + *WithdrawalsBaseSpec +} + +func (ws *WithdrawalsExecutionLayerSpec) Execute(t *test.Env) { + // Create the withdrawals history object + ws.WithdrawalsHistory = make(WithdrawalsHistory) + shangaiTime := *t.Genesis.Config().ShanghaiTime + t.CLMock.ShanghaiTimestamp = big.NewInt(0).SetUint64(shangaiTime) + + // Wait ttd + t.CLMock.WaitForTTD() + + r := t.TestEngine.TestBlockByNumber(nil) + r.ExpectationDescription = ` + Requested "latest" block expecting genesis to contain + withdrawalRoot=nil, because genesis.timestamp < shanghaiTime + ` + r.ExpectWithdrawalsRoot(nil) + + // Produce any blocks necessary to reach withdrawals fork + t.CLMock.ProduceBlocks(int(ws.GetPreWithdrawalsBlockCount()), clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + if !ws.SkipBaseVerifications { + r := t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestHeader.Hash(), + }, + &beacon.PayloadAttributes{ + Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(), + Random: common.Hash{}, + SuggestedFeeRecipient: common.Address{}, + Withdrawals: nil, + }, + ) + r.ExpectationDescription = "Sent pre-shanghai Forkchoice ForkchoiceUpdatedV2 + null withdrawals, no error is expected" + r.ExpectNoError() + } + }, + }) + + ws.waitForShaghai(t) + + var ( + startAccount = ws.GetWithdrawalsStartAccount() + nextIndex = uint64(0) + ) + + // start client + client := getClient(t) + if client == nil { + t.Fatalf("Couldn't connect to client") + return + } + 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. + for i := 0; i < int(ws.WithdrawalsBlockCount); 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 + }, + }) + } + + // 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) + } + }}) + + 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()) + } + + 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()], + ) + } + } + } +}