Skip to content

Commit 80941c3

Browse files
committed
simulators/ethereum/engine: more withdrawals verifications
1 parent fa8d62c commit 80941c3

File tree

4 files changed

+187
-23
lines changed

4 files changed

+187
-23
lines changed

simulators/ethereum/engine/helper/payload.go

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,22 @@ import (
1515
)
1616

1717
type CustomPayloadData struct {
18-
ParentHash *common.Hash
19-
FeeRecipient *common.Address
20-
StateRoot *common.Hash
21-
ReceiptsRoot *common.Hash
22-
LogsBloom *[]byte
23-
PrevRandao *common.Hash
24-
Number *uint64
25-
GasLimit *uint64
26-
GasUsed *uint64
27-
Timestamp *uint64
28-
ExtraData *[]byte
29-
BaseFeePerGas *big.Int
30-
BlockHash *common.Hash
31-
Transactions *[][]byte
32-
Withdrawals types.Withdrawals
18+
ParentHash *common.Hash
19+
FeeRecipient *common.Address
20+
StateRoot *common.Hash
21+
ReceiptsRoot *common.Hash
22+
LogsBloom *[]byte
23+
PrevRandao *common.Hash
24+
Number *uint64
25+
GasLimit *uint64
26+
GasUsed *uint64
27+
Timestamp *uint64
28+
ExtraData *[]byte
29+
BaseFeePerGas *big.Int
30+
BlockHash *common.Hash
31+
Transactions *[][]byte
32+
Withdrawals types.Withdrawals
33+
RemoveWithdrawals bool
3334
}
3435

3536
// Construct a customized payload by taking an existing payload as base and mixing it CustomPayloadData
@@ -100,7 +101,9 @@ func CustomizePayload(basePayload *api.ExecutableData, customData *CustomPayload
100101
if customData.BaseFeePerGas != nil {
101102
customPayloadHeader.BaseFee = customData.BaseFeePerGas
102103
}
103-
if customData.Withdrawals != nil {
104+
if customData.RemoveWithdrawals {
105+
customPayloadHeader.WithdrawalsHash = nil
106+
} else if customData.Withdrawals != nil {
104107
h := types.DeriveSha(customData.Withdrawals, trie.NewStackTrie(nil))
105108
customPayloadHeader.WithdrawalsHash = &h
106109
} else if basePayload.Withdrawals != nil {
@@ -125,7 +128,9 @@ func CustomizePayload(basePayload *api.ExecutableData, customData *CustomPayload
125128
BlockHash: customPayloadHeader.Hash(),
126129
Transactions: txs,
127130
}
128-
if customData.Withdrawals != nil {
131+
if customData.RemoveWithdrawals {
132+
result.Withdrawals = nil
133+
} else if customData.Withdrawals != nil {
129134
result.Withdrawals = customData.Withdrawals
130135
} else if basePayload.Withdrawals != nil {
131136
result.Withdrawals = basePayload.Withdrawals

simulators/ethereum/engine/suites/withdrawals/README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@ This test suite verifies behavior of the Engine API on each client after the Sha
66
## Shanghai Fork
77
- Genesis Shanghai: Tests the withdrawals fork happening since genesis (e.g. on a testnet).
88
- Shanghai Fork on Block 1: Tests the withdrawals fork happening directly after genesis.
9-
- Shanghai Fork on Block 2: 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.
10-
- Shanghai Fork on Block 3: Tests the transition to the withdrawals fork after two blocks have happened. Block 2 is sent with invalid non-null withdrawals payload (both in `engine_newPayloadV2` and the attributes of `engine_forkchoiceUpdatedV2`) and client is expected to respond with the appropriate error.
9+
- Shanghai Fork on Block 2: Tests the transition to the withdrawals fork after a single block has happened. Block 1 is used to send invalid non-null withdrawals payload.
10+
- Shanghai Fork on Block 3: Tests the transition to the withdrawals fork after two blocks have happened. Block 1 and 2 are used to send invalid non-null withdrawals payload.
11+
12+
All test cases contain following verifications:
13+
- Verify client responds with error -32602 on the following scenarios:
14+
- Send `ExecutionPayloadV2` using a custom valid (correct BlockHash) execution payload that includes an empty array for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_newPayloadV2`
15+
- Send `PayloadAttributesV2` using an empty array for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_forkchoiceUpdatedV2`
16+
- Send `ExecutionPayloadV1` using a custom valid (correct BlockHash) execution payload that includes `null` as withdrawals on `timestamp >= SHANGHAI_TIMESTAMP` using `engine_newPayloadV2`
17+
- Send `PayloadAttributesV2` using `null` for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_forkchoiceUpdatedV2`
18+
- Use `engine_forkchoiceUpdatedV2` and `engine_newPayloadV2` to send pre-Shanghai payloads/payload attributes, and verify method call succeeds.
1119

1220
## Withdrawals
1321
- Withdraw to a single account: Make multiple withdrawals to a single account.
@@ -16,6 +24,11 @@ This test suite verifies behavior of the Engine API on each client after the Sha
1624
- Withdraw zero amount: Make multiple withdrawals where the amount withdrawn is 0.
1725
- Empty Withdrawals: Produce withdrawals block with zero withdrawals.
1826

27+
All test cases contain the following verifications:
28+
- Verify all withdrawal amounts (sent in Gwei) correctly translate to a wei balance increase in the execution client (after the payload has been set as head using `engine_forkchoiceUpdatedV2`).
29+
- Verify using `eth_getBalance` that the balance of all withdrawn accounts match the expected amount on each block of the chain and `latest`.
30+
- Payload returned by `engine_getPayloadV2` contains the same list of withdrawals as were passed by `PayloadAttributesV2` in the `engine_forkchoiceUpdatedV2` method call.
31+
1932
## Sync to Shanghai
2033
- Sync after 2 blocks - Shanghai on Block 1 - Single Withdrawal Account - No Transactions:
2134
- Spawn a first client
@@ -151,4 +164,13 @@ This test suite verifies behavior of the Engine API on each client after the Sha
151164
- Verify that `TxB` returns error on `eth_sendRawTransaction` and also should be absent from the transaction pool of the client
152165
- Request a new payload from the client and verify that the payload built only includes `TxA`, and `TxB` is not included, nor the contract it could create is present in the `stateRoot`.
153166
- Create a modified payload where `TxA` is replaced by `TxB` and send using `engine_newPayloadV2`
154-
- Verify that `engine_newPayloadV2` returns `INVALID` nad `latestValidHash` points to the latest valid payload in the canonical chain.
167+
- Verify that `engine_newPayloadV2` returns `INVALID` nad `latestValidHash` points to the latest valid payload in the canonical chain.
168+
169+
## Block Value Tests
170+
- Block Value on GetPayloadV2 Post-Shanghai
171+
- Create a Shanghai chain where the fork transition happens at block 1
172+
- Send transactions, submit forkchoice and get payload built
173+
- Verify transactions were included in payload created
174+
- Set forkchoice head to the new payload
175+
- Calculate the block value by requesting the transaction receipts
176+
- Verify that the `blockValue` returned by `engine_getPayloadV2` matches the expected calculated value

simulators/ethereum/engine/suites/withdrawals/tests.go

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ var Tests = []test.SpecInterface{
162162
WithdrawalsPerBlock: 0,
163163
},
164164

165+
// Block value tests
166+
&BlockValueSpec{
167+
WithdrawalsBaseSpec: &WithdrawalsBaseSpec{
168+
Spec: test.Spec{
169+
Name: "GetPayloadV2 Block Value",
170+
About: `
171+
Verify the block value returned in GetPayloadV2.
172+
`,
173+
},
174+
WithdrawalsForkHeight: 1,
175+
WithdrawalsBlockCount: 1,
176+
},
177+
},
178+
165179
// Sync Tests
166180
&WithdrawalsSyncSpec{
167181
WithdrawalsBaseSpec: &WithdrawalsBaseSpec{
@@ -739,14 +753,30 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) {
739753
HeadBlockHash: t.CLMock.LatestHeader.Hash(),
740754
},
741755
&beacon.PayloadAttributes{
742-
Timestamp: t.CLMock.LatestHeader.Time + 1,
756+
Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(),
743757
Random: common.Hash{},
744758
SuggestedFeeRecipient: common.Address{},
745759
Withdrawals: make(types.Withdrawals, 0),
746760
},
747761
)
748-
r.ExpectationDescription = "Sent pre-shanghai fcu using EngineForkchoiceUpdatedV2+Withdrawals, error is expected"
762+
r.ExpectationDescription = "Sent pre-shanghai Forkchoice using ForkchoiceUpdatedV2 + Withdrawals, error is expected"
749763
r.ExpectErrorCode(InvalidParamsError)
764+
765+
// Send a valid Pre-Shanghai request using ForkchoiceUpdatedV2
766+
// (CLMock uses V1 by default)
767+
r = t.TestEngine.TestEngineForkchoiceUpdatedV2(
768+
&beacon.ForkchoiceStateV1{
769+
HeadBlockHash: t.CLMock.LatestHeader.Hash(),
770+
},
771+
&beacon.PayloadAttributes{
772+
Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(),
773+
Random: common.Hash{},
774+
SuggestedFeeRecipient: common.Address{},
775+
Withdrawals: nil,
776+
},
777+
)
778+
r.ExpectationDescription = "Sent pre-shanghai Forkchoice ForkchoiceUpdatedV2 + null withdrawals, no error is expected"
779+
r.ExpectNoError()
750780
}
751781
},
752782
OnGetPayload: func() {
@@ -763,6 +793,11 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) {
763793
r := t.TestEngine.TestEngineNewPayloadV2(payloadPlusWithdrawals)
764794
r.ExpectationDescription = "Sent pre-shanghai payload using NewPayloadV2+Withdrawals, error is expected"
765795
r.ExpectErrorCode(InvalidParamsError)
796+
797+
// Send valid ExecutionPayloadV1 using engine_newPayloadV2
798+
r = t.TestEngine.TestEngineNewPayloadV2(&t.CLMock.LatestPayloadBuilt)
799+
r.ExpectationDescription = "Sent pre-shanghai payload using NewPayloadV2, no error is expected"
800+
r.ExpectStatus(test.Valid)
766801
}
767802
},
768803
OnNewPayloadBroadcast: func() {
@@ -790,6 +825,25 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) {
790825

791826
t.CLMock.ProduceBlocks(int(ws.WithdrawalsBlockCount), clmock.BlockProcessCallbacks{
792827
OnPayloadProducerSelected: func() {
828+
829+
if !ws.SkipBaseVerifications {
830+
// Try to send a PayloadAttributesV1 with null withdrawals after
831+
// Shanghai
832+
r := t.TestEngine.TestEngineForkchoiceUpdatedV2(
833+
&beacon.ForkchoiceStateV1{
834+
HeadBlockHash: t.CLMock.LatestHeader.Hash(),
835+
},
836+
&beacon.PayloadAttributes{
837+
Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(),
838+
Random: common.Hash{},
839+
SuggestedFeeRecipient: common.Address{},
840+
Withdrawals: nil,
841+
},
842+
)
843+
r.ExpectationDescription = "Sent shanghai fcu using PayloadAttributesV1, error is expected"
844+
r.ExpectErrorCode(InvalidParamsError)
845+
}
846+
793847
// Send some withdrawals
794848
t.CLMock.NextWithdrawals, nextIndex = ws.GenerateWithdrawalsForBlock(nextIndex, startAccount)
795849
ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals
@@ -812,7 +866,38 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) {
812866
}
813867
},
814868
OnGetPayload: func() {
815-
// TODO: Send new payload with `withdrawals=null` and expect error
869+
if !ws.SkipBaseVerifications {
870+
// Send invalid `ExecutionPayloadV1` by replacing withdrawals list
871+
// with null, and client must respond with `InvalidParamsError`.
872+
// Note that StateRoot is also incorrect but null withdrawals should
873+
// be checked first instead of responding `INVALID`
874+
nilWithdrawalsPayload, err := helper.CustomizePayload(&t.CLMock.LatestPayloadBuilt, &helper.CustomPayloadData{
875+
RemoveWithdrawals: true,
876+
})
877+
if err != nil {
878+
t.Fatalf("Unable to append withdrawals: %v", err)
879+
}
880+
r := t.TestEngine.TestEngineNewPayloadV2(nilWithdrawalsPayload)
881+
r.ExpectationDescription = "Sent shanghai payload using ExecutionPayloadV1, error is expected"
882+
r.ExpectErrorCode(InvalidParamsError)
883+
884+
// Verify the list of withdrawals returned on the payload built
885+
// completely matches the list provided in the
886+
// engine_forkchoiceUpdatedV2 method call
887+
if sentList, ok := ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber]; !ok {
888+
panic("withdrawals sent list was not saved")
889+
} else {
890+
if len(sentList) != len(t.CLMock.LatestPayloadBuilt.Withdrawals) {
891+
t.Fatalf("FAIL (%s): Incorrect list of withdrawals on built payload: want=%d, got=%d", t.TestName, len(sentList), len(t.CLMock.LatestPayloadBuilt.Withdrawals))
892+
}
893+
for i := 0; i < len(sentList); i++ {
894+
if err := test.CompareWithdrawals(sentList[i], t.CLMock.LatestPayloadBuilt.Withdrawals[i]); err != nil {
895+
t.Fatalf("FAIL (%s): Incorrect withdrawal on index %d: %v", t.TestName, i, err)
896+
}
897+
}
898+
899+
}
900+
}
816901
},
817902
OnNewPayloadBroadcast: func() {
818903
// Check withdrawal addresses and verify withdrawal balances
@@ -1390,3 +1475,37 @@ func (s *MaxInitcodeSizeSpec) Execute(t *test.Env) {
13901475
},
13911476
})
13921477
}
1478+
1479+
type BlockValueSpec struct {
1480+
*WithdrawalsBaseSpec
1481+
}
1482+
1483+
func (s *BlockValueSpec) Execute(t *test.Env) {
1484+
s.WithdrawalsBaseSpec.SkipBaseVerifications = true
1485+
s.WithdrawalsBaseSpec.Execute(t)
1486+
1487+
// Get the latest block and the transactions included
1488+
b := t.TestEngine.TestBlockByNumber(nil)
1489+
b.ExpectNoError()
1490+
1491+
totalValue := new(big.Int)
1492+
txs := b.Block.Transactions()
1493+
if len(txs) == 0 {
1494+
t.Fatalf("FAIL (%s): No transactions included in latest block", t.TestName)
1495+
}
1496+
for _, tx := range txs {
1497+
r := t.TestEngine.TestTransactionReceipt(tx.Hash())
1498+
r.ExpectNoError()
1499+
1500+
receipt := r.Receipt
1501+
1502+
gasUsed := new(big.Int).SetUint64(receipt.GasUsed)
1503+
txTip, _ := tx.EffectiveGasTip(b.Block.Header().BaseFee)
1504+
txTip.Mul(txTip, gasUsed)
1505+
totalValue.Add(totalValue, txTip)
1506+
}
1507+
1508+
if totalValue.Cmp(t.CLMock.LatestBlockValue) != 0 {
1509+
t.Fatalf("FAIL (%s): Unexpected block value returned on GetPayloadV2: want=%d, got=%d", t.TestName, totalValue, t.CLMock.LatestBlockValue)
1510+
}
1511+
}

simulators/ethereum/engine/test/expect.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package test
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
@@ -390,6 +391,23 @@ func (exp *GetPayloadResponseExpectObject) ExpectTimestamp(expectedTimestamp uin
390391
}
391392
}
392393

394+
func CompareWithdrawals(want *types.Withdrawal, got *types.Withdrawal) error {
395+
if want == nil && got != nil || want != nil && got == nil {
396+
return fmt.Errorf("want=%v, got=%v", want, got)
397+
}
398+
if want != nil {
399+
if want.Amount != got.Amount ||
400+
!bytes.Equal(want.Address[:], got.Address[:]) ||
401+
want.Index != got.Index ||
402+
want.Validator != got.Validator {
403+
wantStr, _ := json.MarshalIndent(want, "", " ")
404+
gotStr, _ := json.MarshalIndent(got, "", " ")
405+
return fmt.Errorf("want=%v, got=%v", wantStr, gotStr)
406+
}
407+
}
408+
return nil
409+
}
410+
393411
// BlockNumber
394412
type BlockNumberResponseExpectObject struct {
395413
*ExpectEnv

0 commit comments

Comments
 (0)