feat: add cross-chain message bot#20408
feat: add cross-chain message bot#20408just-mitch wants to merge 1 commit intomerge-train/spartanfrom
Conversation
ebd9f4b to
7c7f4fe
Compare
There was a problem hiding this comment.
This surprised and confused me. It felt like a bug, so I made this writeup. Does anyone have thoughts? I don't want to leave this file in, and would rather fix/track the bug if we believe one exists.
There was a problem hiding this comment.
Make the simulator include pending L1-to-L2 messages
This is the correct option IMO. Simulation should be done in the same context as the next block that would be mined. We can do this in a separate PR though. We should also fix the e2e test for cross chain so we surface the issue.
|
|
||
| __pycache__ | ||
|
|
||
| *.local.md |
There was a problem hiding this comment.
I got tired of needing to avoid docs that I had created in the course of planning.
spalladino
left a comment
There was a problem hiding this comment.
Thanks for implementing this @just-mitch! I left some comments, including on the oddities.
yarn-project/bot/src/config.ts
Outdated
| /** Seed a new L1→L2 message every N ticks (crosschain mode). */ | ||
| l1ToL2SeedInterval: number; |
There was a problem hiding this comment.
Nit: what's a tick in this context?
Now that I've read the bot code I understand. Why do we want this config? Isn't it enough to just work with seedcount and always top-up until that value?
There was a problem hiding this comment.
Yep, big simplification coming. Good call.
| }, 120_000); | ||
| }); | ||
|
|
||
| describe('cross-chain-bot store', () => { |
There was a problem hiding this comment.
Nit: these should be unit tests for the store
There was a problem hiding this comment.
Make the simulator include pending L1-to-L2 messages
This is the correct option IMO. Simulation should be done in the same context as the next block that would be mined. We can do this in a separate PR though. We should also fix the e2e test for cross chain so we surface the issue.
yarn-project/bot/src/runner.ts
Outdated
| default: | ||
| throw new Error(`Unsupported bot mode: [${this.config.botMode}]`); |
There was a problem hiding this comment.
| default: | |
| throw new Error(`Unsupported bot mode: [${this.config.botMode}]`); | |
| default: | |
| const _exhaustive: never = this.config.botMode | |
| throw new Error(`Unsupported bot mode: [${this.config.botMode}]`); |
There was a problem hiding this comment.
Assuming config.botMode is an union type and not a plain string
| variable "BOT_CROSS_CHAIN_MNEMONIC_START_INDEX" { | ||
| description = "The cross-chain bot mnemonic start index" | ||
| type = string | ||
| default = "" | ||
| } | ||
|
|
||
| variable "BOT_CROSS_CHAIN_REPLICAS" { | ||
| description = "Number of cross-chain bot replicas to deploy (0 to disable)" | ||
| type = number | ||
| default = 0 | ||
| } | ||
|
|
||
| variable "BOT_CROSS_CHAIN_TX_INTERVAL_SECONDS" { | ||
| description = "Interval in seconds between cross-chain bot transactions" | ||
| type = number | ||
| default = 10 | ||
| } | ||
|
|
||
| variable "BOT_CROSS_CHAIN_FOLLOW_CHAIN" { | ||
| description = "Cross-chain bot follow-chain mode" | ||
| type = string | ||
| default = "PENDING" | ||
| } | ||
|
|
||
| variable "BOT_CROSS_CHAIN_L2_PRIVATE_KEY" { | ||
| description = "Private key for the cross-chain bot (hex string starting with 0x)" | ||
| type = string | ||
| nullable = true | ||
| default = null | ||
| } |
There was a problem hiding this comment.
Heads up we're missing a BOT_CROSS_CHAIN_PXE_SYNC_CHAIN_TIP (see #20392)
| * pendingMessages (Map) In-memory mirror of BotStore. | ||
| * Tracks L1->L2 messages that have been | ||
| * seeded on L1 but not yet consumed on L2. |
There was a problem hiding this comment.
Nit: I'd simplify and keep a single source of truth
There was a problem hiding this comment.
Had the same thought
| protected override async onTxMined(receipt: TxReceipt, logCtx: object): Promise<void> { | ||
| // Verify L2→L1 messages appeared in this tx's effects | ||
| if (receipt.blockNumber !== undefined) { | ||
| const block = await this.node.getBlock(receipt.blockNumber); |
There was a problem hiding this comment.
Nit: you can query node.getTxEffect directly
| // Message not found in any block — stale, remove it | ||
| this.pendingMessages.delete(msg.msgHash); | ||
| await this.store.deleteL1ToL2Message(msg.msgHash); | ||
| this.log.warn(`Removed stale L1→L2 message ${msg.msgHash}`); |
There was a problem hiding this comment.
I don't think isL1ToL2MessageReady throw if the message is stale, can you confirm?
There was a problem hiding this comment.
Hm, yep. I have it now just detecting based on timestamp, and log.warning. Not sure if we can do better there? I would also like to plumb some basic metrics through these bots so we can see their own understanding of state. That is, tracking how often messages become stale would be good to know.
| // Clean up consumed L1→L2 message from store | ||
| if (this.lastConsumedMsg) { | ||
| this.pendingMessages.delete(this.lastConsumedMsg.msgHash); | ||
| await this.store.deleteL1ToL2Message(this.lastConsumedMsg.msgHash); | ||
| this.l1ToL2Consumed++; | ||
| this.lastConsumedMsg = undefined; | ||
| } |
There was a problem hiding this comment.
I'm not sure the bot works with FOLLOW_CHAIN=NONE. If it's not instructed to wait for the tx to be mined, then onTxMined never gets called, and the pending l1 to l2 messages are never cleared.
I think this needs a few changes if we want to support that mode, since we could have more than one tx in-flight at any given time. We could have a collection of in-flight messages that we avoid picking from during getReadyL1ToL2Message. Or maybe the easiest solution is to just clean the message from the store as soon as we send the L2 tx that consumes it, regardless of it being successful or not.
There was a problem hiding this comment.
Hm. I think I'm just going to prevent the crosschain bot from using FOLLOW_CHAIN=NONE
| this.pendingMessages.size < this.config.l1ToL2SeedCount | ||
| ) { | ||
| try { | ||
| await this.seedNewL1ToL2Message(); |
There was a problem hiding this comment.
WDYT about not awaiting here, so we don't block the bot loop for at least one L1 slot until the message gets mined?
7c7f4fe to
e7d55bf
Compare
| const pendingMessages = await this.store.getUnconsumedL1ToL2Messages(); | ||
|
|
||
| // Send an L1→L2 message if we're below the threshold and not already seeding one | ||
| if (pendingMessages.length < this.config.l1ToL2SeedCount && !this.pendingSeedPromise) { |
There was a problem hiding this comment.
This will likely mean there is never more than 1 in-flight L1-> L2 message, but I'm fine with that for this PR. As noted elsewhere, I'd like to get some basic metrics up to track the backpressure before tuning message creation/consumption rates.
Adds a new bot mode that produces cross-chain L2-to-L1 messages, including terraform/spartan deployment config, e2e tests, and supporting infrastructure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e7d55bf to
2364885
Compare
Adds a new bot mode (
CROSS_CHAIN) that produces batch transactions of L2->L1 and L1->L2.