Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simulators/ethereum/engine: Add test to re-org to mempool and back in #900

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 198 additions & 98 deletions simulators/ethereum/engine/suites/engine/reorg.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,19 @@ func (spec SidechainReOrgTest) Execute(t *test.Env) {
}

// Test performing a re-org that involves removing or modifying a transaction
type TransactionReOrgScenario string

const (
TransactionReOrgScenarioReOrgOut TransactionReOrgScenario = "Re-Org Out"
TransactionReOrgScenarioReOrgBackIn TransactionReOrgScenario = "Re-Org Back In"
TransactionReOrgScenarioReOrgDifferentBlock TransactionReOrgScenario = "Re-Org to Different Block"
TransactionReOrgScenarioNewPayloadOnRevert TransactionReOrgScenario = "New Payload on Revert Back"
)

type TransactionReOrgTest struct {
test.BaseSpec
ReorgOut bool
ReorgDifferentBlock bool
NewPayloadOnRevert bool
TransactionCount int
Scenario TransactionReOrgScenario
}

func (s TransactionReOrgTest) WithMainFork(fork config.Fork) test.Spec {
Expand All @@ -130,19 +138,11 @@ func (s TransactionReOrgTest) WithMainFork(fork config.Fork) test.Spec {
}

func (s TransactionReOrgTest) GetName() string {
reOrgType := "Out Of Block"
if s.ReorgDifferentBlock {
reOrgType = "To Different Block"
name := "Transaction Re-Org"
if s.Scenario != "" {
name = fmt.Sprintf("%s, %s", name, s.Scenario)
}
payloadOnRevertStatus := "False"
if s.NewPayloadOnRevert {
payloadOnRevertStatus = "True"
}
return fmt.Sprintf(
"Transaction ReOrg %s, NewPayloadOnRevert=%s",
reOrgType,
payloadOnRevertStatus,
)
return name
}

// Test transaction status after a forkchoiceUpdated re-orgs to an alternative hash where a transaction is not present
Expand All @@ -155,20 +155,48 @@ func (spec TransactionReOrgTest) Execute(t *test.Env) {

// Create transactions that modify the state in order to check after the reorg.
var (
txCount = 5
err error
txCount = spec.TransactionCount
sstoreContractAddr = common.HexToAddress("0000000000000000000000000000000000000317")
)

for i := 0; i < txCount; i++ {
var (
altPayload *typ.ExecutableData
tx typ.Transaction
if txCount == 0 {
// Default is to send 5 transactions
txCount = 5
}

// Send a transaction on each payload of the canonical chain
sendTransaction := func(i int) (typ.Transaction, error) {
data := common.LeftPadBytes([]byte{byte(i)}, 32)
t.Logf("transactionReorg, i=%v, data=%v\n", i, data)
return t.SendNextTransaction(
t.TestContext,
t.Engine,
&helper.BaseTransactionCreator{
Recipient: &sstoreContractAddr,
Amount: big0,
Payload: data,
TxType: t.TestTransactionType,
GasLimit: 75000,
ForkConfig: t.ForkConfig,
},
)

}

var (
altPayload *typ.ExecutableData
nextTx typ.Transaction
tx typ.Transaction
)

for i := 0; i < txCount; i++ {

// Generate two payloads, one with the transaction and the other one without it
t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{
OnPayloadAttributesGenerated: func() {
// At this point we have not broadcast the transaction.
if spec.ReorgOut {
if spec.Scenario == TransactionReOrgScenarioReOrgOut {
// Any payload we get should not contain any
payloadAttributes := t.CLMock.LatestPayloadAttributes
rand.Read(payloadAttributes.Random[:])
Expand All @@ -186,41 +214,33 @@ func (spec TransactionReOrgTest) Execute(t *test.Env) {
}
}

// At this point we can broadcast the transaction and it will be included in the next payload
// Data is the key where a `1` will be stored
data := common.LeftPadBytes([]byte{byte(i)}, 32)
t.Logf("transactionReorg, i=%v, data=%v\n", i, data)
var err error
tx, err = t.SendNextTransaction(
t.TestContext,
t.Engine,
&helper.BaseTransactionCreator{
Recipient: &sstoreContractAddr,
Amount: big0,
Payload: data,
TxType: t.TestTransactionType,
GasLimit: 75000,
ForkConfig: t.ForkConfig,
},
)
if err != nil {
t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err)
}
if spec.Scenario != TransactionReOrgScenarioReOrgBackIn {
// At this point we can broadcast the transaction and it will be included in the next payload
// Data is the key where a `1` will be stored
tx, err = sendTransaction(i)
if err != nil {
t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err)
}

// Get the receipt
ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout)
defer cancel()
receipt, _ := t.Eth.TransactionReceipt(ctx, tx.Hash())
if receipt != nil {
t.Fatalf("FAIL (%s): Receipt obtained before tx included in block: %v", t.TestName, receipt)
// Get the receipt
ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout)
defer cancel()
receipt, _ := t.Eth.TransactionReceipt(ctx, tx.Hash())
if receipt != nil {
t.Fatalf("FAIL (%s): Receipt obtained before tx included in block: %v", t.TestName, receipt)
}
}

},
OnGetPayload: func() {
// Check that indeed the payload contains the transaction
if !helper.TransactionInPayload(&t.CLMock.LatestPayloadBuilt, tx) {
t.Fatalf("FAIL (%s): Payload built does not contain the transaction: %v", t.TestName, t.CLMock.LatestPayloadBuilt)
if tx != nil {
if !helper.TransactionInPayload(&t.CLMock.LatestPayloadBuilt, tx) {
t.Fatalf("FAIL (%s): Payload built does not contain the transaction: %v", t.TestName, t.CLMock.LatestPayloadBuilt)
}
}
if spec.ReorgDifferentBlock {

if spec.Scenario == TransactionReOrgScenarioReOrgDifferentBlock || spec.Scenario == TransactionReOrgScenarioNewPayloadOnRevert {
// Create side payload with different hash
var err error
customizer := &helper.CustomPayloadData{
Expand All @@ -237,68 +257,148 @@ func (spec TransactionReOrgTest) Execute(t *test.Env) {
if altPayload.BlockHash == t.CLMock.LatestPayloadBuilt.BlockHash {
t.Fatalf("FAIL (%s): Incorrect hash for payloads: %v == %v", t.TestName, altPayload.BlockHash, t.CLMock.LatestPayloadBuilt.BlockHash)
}
} else if spec.Scenario == TransactionReOrgScenarioReOrgBackIn {
// At this point we broadcast the transaction and request a new payload from the client that must
// contain the transaction.
// Since we are re-orging out and back in on the next block, the verification of this transaction
// being included happens on the next block
nextTx, err = sendTransaction(i)
if err != nil {
t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err)
}

if i == 0 {
// We actually can only do this once because the transaction carries over and we cannot
// impede it from being included in the next payload
forkchoiceUpdated := t.CLMock.LatestForkchoice
payloadAttributes := t.CLMock.LatestPayloadAttributes
rand.Read(payloadAttributes.SuggestedFeeRecipient[:])
f := t.TestEngine.TestEngineForkchoiceUpdated(
&forkchoiceUpdated,
&payloadAttributes,
t.CLMock.LatestHeader.Time,
)
f.ExpectPayloadStatus(test.Valid)

// Wait a second for the client to prepare the payload with the included transaction

time.Sleep(t.CLMock.PayloadProductionClientDelay)

g := t.TestEngine.TestEngineGetPayload(f.Response.PayloadID, &t.CLMock.LatestPayloadAttributes)
g.ExpectNoError()

if !helper.TransactionInPayload(&g.Payload, nextTx) {
t.Fatalf("FAIL (%s): Payload built does not contain the transaction: %v", t.TestName, g.Payload)
}

// Send the new payload and forkchoiceUpdated to it
n := t.TestEngine.TestEngineNewPayload(&g.Payload)
n.ExpectStatus(test.Valid)

forkchoiceUpdated.HeadBlockHash = g.Payload.BlockHash

s := t.TestEngine.TestEngineForkchoiceUpdated(&forkchoiceUpdated, nil, g.Payload.Timestamp)
s.ExpectPayloadStatus(test.Valid)
}
}
},
OnNewPayloadBroadcast: func() {
// Get the receipt
ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout)
defer cancel()
receipt, _ := t.Eth.TransactionReceipt(ctx, tx.Hash())
if receipt != nil {
t.Fatalf("FAIL (%s): Receipt obtained before tx included in block (NewPayload): %v", t.TestName, receipt)
if tx != nil {
// Get the receipt
ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout)
defer cancel()
receipt, _ := t.Eth.TransactionReceipt(ctx, tx.Hash())
if receipt != nil {
t.Fatalf("FAIL (%s): Receipt obtained before tx included in block (NewPayload): %v", t.TestName, receipt)
}
}
},
OnForkchoiceBroadcast: func() {
// Transaction is now in the head of the canonical chain, re-org and verify it's removed
// Get the receipt
txt := t.TestEngine.TestTransactionReceipt(tx.Hash())
txt.ExpectBlockHash(t.CLMock.LatestForkchoice.HeadBlockHash)
if spec.Scenario != TransactionReOrgScenarioReOrgBackIn {
// Transaction is now in the head of the canonical chain, re-org and verify it's removed
// Get the receipt
txt := t.TestEngine.TestTransactionReceipt(tx.Hash())
txt.ExpectBlockHash(t.CLMock.LatestForkchoice.HeadBlockHash)

if altPayload.ParentHash != t.CLMock.LatestPayloadBuilt.ParentHash {
t.Fatalf("FAIL (%s): Incorrect parent hash for payloads: %v != %v", t.TestName, altPayload.ParentHash, t.CLMock.LatestPayloadBuilt.ParentHash)
}
if altPayload.BlockHash == t.CLMock.LatestPayloadBuilt.BlockHash {
t.Fatalf("FAIL (%s): Incorrect hash for payloads: %v == %v", t.TestName, altPayload.BlockHash, t.CLMock.LatestPayloadBuilt.BlockHash)
}
if altPayload.ParentHash != t.CLMock.LatestPayloadBuilt.ParentHash {
t.Fatalf("FAIL (%s): Incorrect parent hash for payloads: %v != %v", t.TestName, altPayload.ParentHash, t.CLMock.LatestPayloadBuilt.ParentHash)
}
if altPayload.BlockHash == t.CLMock.LatestPayloadBuilt.BlockHash {
t.Fatalf("FAIL (%s): Incorrect hash for payloads: %v == %v", t.TestName, altPayload.BlockHash, t.CLMock.LatestPayloadBuilt.BlockHash)
}

if altPayload == nil {
t.Fatalf("FAIL (%s): No payload to re-org to", t.TestName)
if altPayload == nil {
t.Fatalf("FAIL (%s): No payload to re-org to", t.TestName)
}
r := t.TestEngine.TestEngineNewPayload(altPayload)
r.ExpectStatus(test.Valid)
r.ExpectLatestValidHash(&altPayload.BlockHash)

s := t.TestEngine.TestEngineForkchoiceUpdated(&api.ForkchoiceStateV1{
HeadBlockHash: altPayload.BlockHash,
SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash,
FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash,
}, nil, altPayload.Timestamp)
s.ExpectPayloadStatus(test.Valid)

p := t.TestEngine.TestHeaderByNumber(Head)
p.ExpectHash(altPayload.BlockHash)

txt = t.TestEngine.TestTransactionReceipt(tx.Hash())
if spec.Scenario == TransactionReOrgScenarioReOrgOut {
if txt.Receipt != nil {
receiptJson, _ := json.MarshalIndent(txt.Receipt, "", " ")
t.Fatalf("FAIL (%s): Receipt was obtained when the tx had been re-org'd out: %s", t.TestName, receiptJson)
}
} else if spec.Scenario == TransactionReOrgScenarioReOrgDifferentBlock || spec.Scenario == TransactionReOrgScenarioNewPayloadOnRevert {
txt.ExpectBlockHash(altPayload.BlockHash)
}

// Re-org back
if spec.Scenario == TransactionReOrgScenarioNewPayloadOnRevert {
r = t.TestEngine.TestEngineNewPayload(&t.CLMock.LatestPayloadBuilt)
r.ExpectStatus(test.Valid)
r.ExpectLatestValidHash(&t.CLMock.LatestPayloadBuilt.BlockHash)
}
t.CLMock.BroadcastForkchoiceUpdated(&t.CLMock.LatestForkchoice, nil, 1)
}
r := t.TestEngine.TestEngineNewPayload(altPayload)
r.ExpectStatus(test.Valid)
r.ExpectLatestValidHash(&altPayload.BlockHash)

s := t.TestEngine.TestEngineForkchoiceUpdated(&api.ForkchoiceStateV1{
HeadBlockHash: altPayload.BlockHash,
SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash,
FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash,
}, nil, altPayload.Timestamp)
s.ExpectPayloadStatus(test.Valid)

p := t.TestEngine.TestHeaderByNumber(Head)
p.ExpectHash(altPayload.BlockHash)

txt = t.TestEngine.TestTransactionReceipt(tx.Hash())
if spec.ReorgOut {
if txt.Receipt != nil {
receiptJson, _ := json.MarshalIndent(txt.Receipt, "", " ")
t.Fatalf("FAIL (%s): Receipt was obtained when the tx had been re-org'd out: %s", t.TestName, receiptJson)

if tx != nil {
// Now it should be back with main payload
txt := t.TestEngine.TestTransactionReceipt(tx.Hash())
txt.ExpectBlockHash(t.CLMock.LatestForkchoice.HeadBlockHash)

if spec.Scenario != TransactionReOrgScenarioReOrgBackIn {
tx = nil
}
} else if spec.ReorgDifferentBlock {
txt.ExpectBlockHash(altPayload.BlockHash)
}

// Re-org back
if spec.NewPayloadOnRevert {
r = t.TestEngine.TestEngineNewPayload(&t.CLMock.LatestPayloadBuilt)
r.ExpectStatus(test.Valid)
r.ExpectLatestValidHash(&t.CLMock.LatestPayloadBuilt.BlockHash)
if spec.Scenario == TransactionReOrgScenarioReOrgBackIn && i > 0 {
// Reasoning: Most of the clients do not re-add blob transactions to the pool
// after a re-org, so we need to wait until the next tx is sent to actually
// verify.
tx = nextTx
}
t.CLMock.BroadcastForkchoiceUpdated(&t.CLMock.LatestForkchoice, nil, 1)

// Not it should be back with main payload
txt = t.TestEngine.TestTransactionReceipt(tx.Hash())
txt.ExpectBlockHash(t.CLMock.LatestForkchoice.HeadBlockHash)
},
})

}

if tx != nil {
// Produce one last block and verify that the block contains the transaction
t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{
OnForkchoiceBroadcast: func() {
if !helper.TransactionInPayload(&t.CLMock.LatestPayloadBuilt, tx) {
t.Fatalf("FAIL (%s): Payload built does not contain the transaction: %v", t.TestName, t.CLMock.LatestPayloadBuilt)
}
// Get the receipt
ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout)
defer cancel()
receipt, _ := t.Eth.TransactionReceipt(ctx, tx.Hash())
if receipt == nil {
t.Fatalf("FAIL (%s): Receipt not obtained after tx included in block: %v", t.TestName, receipt)
}
},
})

Expand Down
10 changes: 6 additions & 4 deletions simulators/ethereum/engine/suites/engine/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,16 @@ func init() {
// Re-org a transaction out of a block, or into a new block
Tests = append(Tests,
TransactionReOrgTest{
ReorgOut: true,
Scenario: TransactionReOrgScenarioReOrgOut,
},
TransactionReOrgTest{
ReorgDifferentBlock: true,
Scenario: TransactionReOrgScenarioReOrgDifferentBlock,
},
TransactionReOrgTest{
ReorgDifferentBlock: true,
NewPayloadOnRevert: true,
Scenario: TransactionReOrgScenarioNewPayloadOnRevert,
},
TransactionReOrgTest{
Scenario: TransactionReOrgScenarioReOrgBackIn,
},
)

Expand Down