-
Notifications
You must be signed in to change notification settings - Fork 3.9k
op-supernode: Interop Message Validation #19051
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
84b62cc
op-acceptance-tests: Add InvalidMessageHalt test for supernode interop
axelKingsley c3dcd38
op-supernode: Add logsDB infrastructure for interop activity
axelKingsley 2e8b4e1
op-supernode: Refactor interop logdb and improve test coverage
axelKingsley 2e5b9ab
op-supernode: Implement interop message verification
axelKingsley 572d6d3
op-supernode: Consolidate interop unit tests
axelKingsley f9bb34d
remove unused code
axelKingsley 26b2988
validate timestamp gap using block time
axelKingsley d8ed837
implement message expiry time validation
axelKingsley cd5365f
simplify verifyExecutingMessage logic
axelKingsley 5d27cad
fix logdb timing check and track local safe in acceptance test
axelKingsley 309ed88
fix loadLogs to process blocks when DB is empty
axelKingsley d4300d9
simplify timestamp progression test baseline logic
axelKingsley cef5257
skip logsDB loading and verification at activation timestamp
axelKingsley eaf867e
fix logsDB to start at activation block instead of genesis
axelKingsley 0e10164
move invalid_message_halt_test to separate package to avoid test poll…
axelKingsley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
op-acceptance-tests/tests/supernode/interop/halt/init_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package halt | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
|
|
||
| "github.com/ethereum-optimism/optimism/op-devstack/presets" | ||
| ) | ||
|
|
||
| // TestMain creates an isolated two-L2 setup with a shared supernode that has interop enabled. | ||
| // This package tests invalid message scenarios that would pollute other tests if run on a shared devnet. | ||
| func TestMain(m *testing.M) { | ||
| _ = os.Setenv("DEVSTACK_L2CL_KIND", "supernode") | ||
| presets.DoMain(m, presets.WithTwoL2SupernodeInterop(0)) | ||
| } |
251 changes: 251 additions & 0 deletions
251
op-acceptance-tests/tests/supernode/interop/halt/invalid_message_halt_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| package halt | ||
|
|
||
| import ( | ||
| "context" | ||
| "math/rand" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/core/types" | ||
|
|
||
| "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" | ||
| "github.com/ethereum-optimism/optimism/op-devstack/devtest" | ||
| "github.com/ethereum-optimism/optimism/op-devstack/dsl" | ||
| "github.com/ethereum-optimism/optimism/op-devstack/presets" | ||
| "github.com/ethereum-optimism/optimism/op-service/bigs" | ||
| "github.com/ethereum-optimism/optimism/op-service/eth" | ||
| "github.com/ethereum-optimism/optimism/op-service/testutils" | ||
| "github.com/ethereum-optimism/optimism/op-service/txintent" | ||
| ) | ||
|
|
||
| // TestSupernodeInteropInvalidMessageHalt tests that: | ||
| // WHEN: an invalid Executing Message is included in a chain | ||
| // THEN: | ||
| // - Validity Never Advances to include the Invalid Block | ||
| // - Local Safety and Unsafety for both chains continue to advance | ||
| // | ||
| // This is a TDD test that starts a cycle to implement the Interop Activity's actual algorithm. | ||
| func TestSupernodeInteropInvalidMessageHalt(gt *testing.T) { | ||
| t := devtest.SerialT(gt) | ||
| sys := presets.NewTwoL2SupernodeInterop(t, 0) | ||
|
|
||
| ctx := t.Ctx() | ||
| snClient := sys.SuperNodeClient() | ||
|
|
||
| // Create funded EOAs on both chains | ||
| alice := sys.FunderA.NewFundedEOA(eth.OneEther) | ||
| bob := sys.FunderB.NewFundedEOA(eth.OneEther) | ||
|
|
||
| // Deploy event logger on chain A | ||
| eventLoggerA := alice.DeployEventLogger() | ||
|
|
||
| // Sync chains | ||
| sys.L2B.CatchUpTo(sys.L2A) | ||
| sys.L2A.CatchUpTo(sys.L2B) | ||
|
|
||
| rng := rand.New(rand.NewSource(12345)) | ||
|
|
||
| // Send an initiating message on chain A | ||
| initTrigger := randomInitTrigger(rng, eventLoggerA, 2, 10) | ||
| initTx, initReceipt := alice.SendInitMessage(initTrigger) | ||
|
|
||
| t.Logger().Info("initiating message sent on chain A", | ||
| "block", initReceipt.BlockNumber, | ||
| "hash", initReceipt.BlockHash, | ||
| ) | ||
|
|
||
| // Wait for chain B to catch up | ||
| sys.L2B.WaitForBlock() | ||
|
|
||
| // Record the verified timestamp before the invalid message | ||
| // We need to know what timestamp was verified before the invalid exec message | ||
| blockTime := sys.L2A.Escape().RollupConfig().BlockTime | ||
| genesisTime := sys.L2A.Escape().RollupConfig().Genesis.L2Time | ||
|
|
||
| // Wait for some timestamps to be verified first | ||
| targetTimestamp := genesisTime + blockTime*2 | ||
| t.Require().Eventually(func() bool { | ||
| resp, err := snClient.SuperRootAtTimestamp(ctx, targetTimestamp) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| t.Logger().Info("super root at timestamp", "timestamp", targetTimestamp, "data", resp.Data) | ||
| return resp.Data != nil | ||
| }, 60*time.Second, time.Second, "initial timestamps should be verified") | ||
|
|
||
| t.Logger().Info("initial verification confirmed", "timestamp", targetTimestamp) | ||
|
|
||
| // Send an INVALID executing message on chain B | ||
| // Modify the message identifier to make it invalid (wrong log index) | ||
| invalidExecReceipt := sendInvalidExecMessage(t, bob, initTx, 0) | ||
|
|
||
| invalidBlockNumber := bigs.Uint64Strict(invalidExecReceipt.BlockNumber) | ||
| invalidBlock := sys.L2ELB.BlockRefByHash(invalidExecReceipt.BlockHash) | ||
| invalidBlockTimestamp := invalidBlock.Time | ||
|
|
||
| t.Logger().Info("invalid executing message sent on chain B", | ||
| "block", invalidExecReceipt.BlockNumber, | ||
| "hash", invalidExecReceipt.BlockHash, | ||
| "timestamp", invalidBlockTimestamp, | ||
| ) | ||
|
|
||
| // Record the safety status before waiting | ||
| initialStatusA := sys.L2ACL.SyncStatus() | ||
| initialStatusB := sys.L2BCL.SyncStatus() | ||
|
|
||
| t.Logger().Info("initial safety status", | ||
| "chainA_local_safe", initialStatusA.LocalSafeL2.Number, | ||
| "chainA_unsafe", initialStatusA.UnsafeL2.Number, | ||
| "chainB_local_safe", initialStatusB.LocalSafeL2.Number, | ||
| "chainB_unsafe", initialStatusB.UnsafeL2.Number, | ||
| ) | ||
|
|
||
| // Now we verify the key behaviors over time: | ||
| // 1. Validity should NEVER advance to include the invalid block | ||
| // 2. Local Safety and Unsafety should continue to advance for both chains | ||
|
|
||
| observationDuration := 30 * time.Second | ||
| checkInterval := time.Second | ||
|
|
||
| start := time.Now() | ||
| var lastVerifiedTimestamp uint64 | ||
|
|
||
| for time.Since(start) < observationDuration { | ||
| time.Sleep(checkInterval) | ||
|
|
||
| // Check current safety status | ||
| statusA := sys.L2ACL.SyncStatus() | ||
| statusB := sys.L2BCL.SyncStatus() | ||
|
|
||
| // KEY ASSERTION 1: Validity should NOT advance past the invalid block's timestamp | ||
| // Check if the invalid block's timestamp has been verified (it should NOT be) | ||
| resp, err := snClient.SuperRootAtTimestamp(ctx, invalidBlockTimestamp) | ||
| t.Require().NoError(err, "SuperRootAtTimestamp should not error") | ||
|
|
||
| if resp.Data != nil { | ||
| t.Logger().Error("UNEXPECTED: invalid block timestamp was verified!", | ||
| "timestamp", invalidBlockTimestamp, | ||
| "invalid_block", invalidBlockNumber, | ||
| ) | ||
| t.FailNow() | ||
| } | ||
|
|
||
| // Track the last verified timestamp (for timestamps before the invalid block) | ||
| if invalidBlockTimestamp > blockTime { | ||
| checkTs := invalidBlockTimestamp - blockTime | ||
| checkResp, _ := snClient.SuperRootAtTimestamp(ctx, checkTs) | ||
| if checkResp.Data != nil { | ||
| lastVerifiedTimestamp = checkTs | ||
| } | ||
| } | ||
|
|
||
| t.Logger().Info("observation tick", | ||
| "elapsed", time.Since(start).Round(time.Second), | ||
| "chainA_local_safe", statusA.LocalSafeL2.Number, | ||
| "chainA_unsafe", statusA.UnsafeL2.Number, | ||
| "chainB_local_safe", statusB.LocalSafeL2.Number, | ||
| "chainB_unsafe", statusB.UnsafeL2.Number, | ||
| "last_verified_ts", lastVerifiedTimestamp, | ||
| "invalid_block_ts", invalidBlockTimestamp, | ||
| ) | ||
| } | ||
|
|
||
| // Final assertions after observation period | ||
|
|
||
| finalStatusA := sys.L2ACL.SyncStatus() | ||
| finalStatusB := sys.L2BCL.SyncStatus() | ||
|
|
||
| // ASSERTION: Local Safety should have advanced for both chains | ||
| t.Require().Greater(finalStatusA.LocalSafeL2.Number, initialStatusA.LocalSafeL2.Number, | ||
| "chain A local safe head should advance") | ||
| t.Require().Greater(finalStatusB.LocalSafeL2.Number, initialStatusB.LocalSafeL2.Number, | ||
| "chain B local safe head should advance") | ||
|
|
||
| // ASSERTION: Unsafety should have advanced for both chains | ||
| t.Require().Greater(finalStatusA.UnsafeL2.Number, initialStatusA.UnsafeL2.Number, | ||
| "chain A unsafe head should advance") | ||
| t.Require().Greater(finalStatusB.UnsafeL2.Number, initialStatusB.UnsafeL2.Number, | ||
| "chain B unsafe head should advance") | ||
|
|
||
| // ASSERTION: The invalid block's timestamp should still NOT be verified | ||
| finalResp, err := snClient.SuperRootAtTimestamp(ctx, invalidBlockTimestamp) | ||
| t.Require().NoError(err) | ||
| t.Require().Nil(finalResp.Data, | ||
| "invalid block timestamp should NEVER be verified") | ||
|
|
||
| t.Logger().Info("test complete: invalid message correctly halted validity advancement", | ||
| "final_chainA_local_safe", finalStatusA.LocalSafeL2.Number, | ||
| "final_chainA_unsafe", finalStatusA.UnsafeL2.Number, | ||
| "final_chainB_local_safe", finalStatusB.LocalSafeL2.Number, | ||
| "final_chainB_unsafe", finalStatusB.UnsafeL2.Number, | ||
| "invalid_block_timestamp", invalidBlockTimestamp, | ||
| "last_verified_timestamp", lastVerifiedTimestamp, | ||
| ) | ||
| } | ||
|
|
||
| // sendInvalidExecMessage sends an executing message with a modified (invalid) identifier. | ||
| // This makes the message invalid because it references a non-existent log index. | ||
| func sendInvalidExecMessage( | ||
| t devtest.T, | ||
| bob *dsl.EOA, | ||
| initIntent *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput], | ||
| eventIdx int, | ||
| ) *types.Receipt { | ||
| ctx := t.Ctx() | ||
|
|
||
| // Evaluate the init result to get the message entries | ||
| result, err := initIntent.Result.Eval(ctx) | ||
| t.Require().NoError(err, "failed to evaluate init result") | ||
| t.Require().Greater(len(result.Entries), eventIdx, "event index out of range") | ||
|
|
||
| // Get the message and modify it to be invalid | ||
| msg := result.Entries[eventIdx] | ||
|
|
||
| // Make the message invalid by setting an impossible log index | ||
| // This creates a message that claims to reference a log that doesn't exist | ||
| msg.Identifier.LogIndex = 9999 | ||
|
|
||
| // Create the exec trigger with the invalid message | ||
| execTrigger := &txintent.ExecTrigger{ | ||
| Executor: constants.CrossL2Inbox, | ||
| Msg: msg, | ||
| } | ||
|
|
||
| // Create the intent with the invalid trigger | ||
| tx := txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](bob.Plan()) | ||
| tx.Content.DependOn(&initIntent.Result) | ||
| tx.Content.Fn(func(ctx context.Context) (*txintent.ExecTrigger, error) { | ||
| return execTrigger, nil | ||
| }) | ||
|
|
||
| receipt, err := tx.PlannedTx.Included.Eval(ctx) | ||
| t.Require().NoError(err, "invalid exec msg receipt not found") | ||
| t.Logger().Info("invalid exec message included", "chain", bob.ChainID(), "block", receipt.BlockNumber) | ||
|
|
||
| return receipt | ||
| } | ||
|
|
||
| // randomInitTrigger creates a random init trigger for testing. | ||
| func randomInitTrigger(rng *rand.Rand, eventLoggerAddress common.Address, topicCount, dataLen int) *txintent.InitTrigger { | ||
| if topicCount > 4 { | ||
| topicCount = 4 // Max 4 topics in EVM logs | ||
| } | ||
| if topicCount < 1 { | ||
| topicCount = 1 | ||
| } | ||
| if dataLen < 1 { | ||
| dataLen = 1 | ||
| } | ||
|
|
||
| topics := make([][32]byte, topicCount) | ||
| for i := range topics { | ||
| copy(topics[i][:], testutils.RandomData(rng, 32)) | ||
| } | ||
|
|
||
| return &txintent.InitTrigger{ | ||
| Emitter: eventLoggerAddress, | ||
| Topics: topics, | ||
| OpaqueData: testutils.RandomData(rng, dataLen), | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.