Skip to content
Open
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
43 changes: 38 additions & 5 deletions packages/fx-tunnel-relayer/src/Relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,29 @@ export class Relayer {
// For any found events, grab its block number and check whether it has been checkpointed yet to the
// CheckpointManager on Ethereum.
if (messageSentEvents.length > 0) {
// A single Polygon transaction can contain multiple MessageSent events, e.g. two price requests bridged
// atomically. matic.js builds the exit proof for the i-th log matching the MessageSent signature within the
// transaction (defaulting to the first one), so each event must be proven at its own index or every event in
// the transaction would produce a proof for the first message and the rest would never be relayed.
const messageSentLogIndicesByTxHash: { [transactionHash: string]: number[] } = {};
for (const e of messageSentEvents) {
await this._relayMessage(e);
if (messageSentLogIndicesByTxHash[e.transactionHash] === undefined) {
const receipt = await this.web3.eth.getTransactionReceipt(e.transactionHash);
messageSentLogIndicesByTxHash[e.transactionHash] = receipt.logs
.filter((log) => log.topics.length > 0 && log.topics[0].toLowerCase() === POLYGON_MESSAGE_SENT_EVENT_SIG)
.map((log) => log.logIndex);
}
const messageIndex = messageSentLogIndicesByTxHash[e.transactionHash].indexOf(e.logIndex);
if (messageIndex === -1) {
this.logger.error({
at: "Relayer#relayMessage",
message: "Could not find MessageSent event's log index in its transaction receipt 📛",
transactionHash: e.transactionHash,
logIndex: e.logIndex,
});
continue;
}
await this._relayMessage(e, messageIndex);
}
} else {
this.logger.debug({
Expand All @@ -68,8 +89,9 @@ export class Relayer {

// First check if the transaction hash corresponding to the MessageSent event has been checkpointed to Ethereum
// Mainnet yet. If it has, then derive a Polygon-specific proof for it and execute OracleRootTunnel.receiveMessage
// by passing in the proof as input.
async _relayMessage(messageEvent: EventData): Promise<void> {
// by passing in the proof as input. `messageIndex` is the position of this event among all logs matching the
// MessageSent signature in the transaction, used to prove the correct log when a transaction contains several.
async _relayMessage(messageEvent: EventData, messageIndex: number): Promise<void> {
const transactionHash = messageEvent.transactionHash;
const blockNumber = messageEvent.blockNumber;

Expand All @@ -91,6 +113,7 @@ export class Relayer {
message: "Deriving proof for transaction that emitted MessageSent",
transactionHash: transactionHash,
blockNumber,
messageIndex,
});

let chainBlockInfo; // Only used for debugging purposes upon error.
Expand All @@ -102,7 +125,8 @@ export class Relayer {
proof = await this.maticPosClient.exitUtil.buildPayloadForExit(
transactionHash,
POLYGON_MESSAGE_SENT_EVENT_SIG, // SEND_MESSAGE_EVENT_SIG, do not change
false
false,
messageIndex
);
if (!proof) throw new Error("Proof construction succeeded but returned undefined");
} catch (error) {
Expand Down Expand Up @@ -135,10 +159,19 @@ export class Relayer {
message: "Submitted relay proof!🕴🏼",
tx: transactionHash,
messageEvent,
messageIndex,
});
} catch (error) {
// If the proof was already submitted, then don't emit an error level log.
if ((error as Error)?.message.includes("EXIT_ALREADY_PROCESSED")) return;
if ((error as Error)?.message.includes("EXIT_ALREADY_PROCESSED")) {
this.logger.debug({
at: "Relayer#relayMessage",
message: "Exit proof already processed by root tunnel, skipping",
transactionHash,
messageIndex,
});
return;
}
this.logger.error({
at: "Relayer#relayMessage",
message: "Failed to submit proof to root tunnel🚨",
Expand Down
55 changes: 54 additions & 1 deletion packages/fx-tunnel-relayer/test/Relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { getContract, deployments, web3 } = require("hardhat");

const { utf8ToHex } = web3.utils;
const Finder = getContract("Finder");
const Multicall = getContract("Multicall3");
const OracleChildTunnel = getContract("OracleChildTunnel");
const Registry = getContract("Registry");
const OracleRootTunnel = getContract("OracleRootTunnelMock");
Expand All @@ -20,7 +21,7 @@ const FxChild = getContract("FxChildMock");
const FxRoot = getContract("FxRootMock");

// This function should return a bytes string.
type customPayloadFn = () => Promise<string>;
type customPayloadFn = (burnTxHash?: string, logEventSig?: string, isFast?: boolean, index?: number) => Promise<string>;
interface MaticPosClient {
exitUtil: {
buildPayloadForExit: customPayloadFn;
Expand Down Expand Up @@ -141,6 +142,58 @@ describe("Relayer unit tests", function () {
assert.equal(nonDebugEvents.length, 1);
assert.isTrue(lastSpyLogIncludes(spy, "Submitted relay proof"));
});
it("relays all messages when a transaction contains multiple MessageSent events", async function () {
// Bundle two price requests into a single transaction via a registered multicall contract so that one
// transaction emits two MessageSent events.
const multicall = await Multicall.new().send({ from: owner });
await registry.methods.registerContract([], multicall.options.address).send({ from: owner });
await multicall.methods
.aggregate([
[
oracleChild.options.address,
oracleChild.methods.requestPrice(testIdentifier, testTimestamp, utf8ToHex("request:1")).encodeABI(),
],
[
oracleChild.options.address,
oracleChild.methods.requestPrice(testIdentifier, testTimestamp, utf8ToHex("request:2")).encodeABI(),
],
])
.send({ from: owner });
const eventsEmitted = await oracleChild.getPastEvents("MessageSent", { fromBlock: 0 });
assert.equal(eventsEmitted.length, 2);
assert.equal(eventsEmitted[0].transactionHash, eventsEmitted[1].transactionHash);

// Record the message index passed to the proof builder for each relayed message: each message must be proven
// at its own position among the transaction's MessageSent logs, not just the first one.
const provenIndices: (number | undefined)[] = [];
const _maticPosClient: MaticPosClient = {
exitUtil: {
buildPayloadForExit: async (_burnTxHash?: string, _logEventSig?: string, _isFast?: boolean, index?: number) => {
provenIndices.push(index);
return utf8ToHex("Test proof");
},
isCheckPointed: async () => new Promise((resolve) => resolve(true)),
getChainBlockInfo: async () => new Promise((resolve) => resolve({ lastChildBlock: 0, txBlockNumber: 0 })),
},
};
const _relayer: any = new Relayer(
spyLogger,
owner,
gasEstimator,
_maticPosClient,
oracleChild,
oracleRoot,
web3,
0,
100
);
await _relayer.fetchAndRelayMessages();

assert.deepEqual(provenIndices, [0, 1]);
const infoEvents = spy.getCalls().filter((log: any) => log.lastArg.level === "info");
assert.equal(infoEvents.length, 2);
assert.isTrue(lastSpyLogIncludes(spy, "Submitted relay proof"));
});
it("ignores events older than earliest polygon block to query", async function () {
const txn = await oracleChild.methods
.requestPrice(testIdentifier, testTimestamp, testAncillaryData)
Expand Down