Skip to content

Conversation

@amateima
Copy link
Contributor

@amateima amateima commented Oct 16, 2025

This PR implements a new service that publishes CCTP (Cross-Chain Transfer Protocol) burn events to a GCP PubSub topic, enabling the finalization bot to process and finalize these transactions onchain.

Changes

Database Layer

  • New Entity: CctpFinalizerJob - Tracks burn events that have been successfully sent to the finalizer bot

Service Layer

  • New Service: CctpFinalizerService - Core service that runs on an interval
    • Queries burn events that haven't been published yet
    • Validates attestation time has passed based on chain-specific requirements
    • Filters out soft-deleted events (deleted due to re-orgs)
    • Publishes burn event info to PubSub topic
    • Creates CctpFinalizerJob records to track published events

Integration Layer

  • PubSubService: Helper class for publishing messages to GCP PubSub
    • Formats payload as base64-encoded JSON
    • Includes burn transaction hash and source chain ID
    • Validates against Avro schema in GCP
    • ⚠️ Note: Currently uses placeholder auth token for local testing. It needs to be updated with a proper SA

Configuration

  • New Environment Variables:
    • ENABLE_CCTP_FINALIZER - Feature flag to enable/disable the service
    • CCTP_FINALIZER_PUBSUB_TOPIC - GCP PubSub topic URL for publishing messages

@linear
Copy link

linear bot commented Oct 16, 2025

Comment on lines 149 to 200
await this.pubSubService.publishCctpFinalizerMessage(
transactionHash,
Number(chainId),
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if this succeeds but the Cloud Run service fails for some reason, we will still update the CctpFinalizerJob table below right? Will it still retry in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it won't retry. I will have a follow up PR which will check periodically for unfinalized old events

const qb = this.postgres
.createQueryBuilder(DepositForBurn, "burnEvent")
.leftJoinAndSelect("burnEvent.finalizerJob", "job")
.where("job.id IS NULL")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to understand - when would job.id be null? Won't it be auto-incremented when the insert happens below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, but we search for burn events that don't have a job associated yet. Therefore we need to execut a LEFT JOIN where job's id is NULL (i.e doesn't exist)

Comment on lines +72 to +78
indexingDelaySeconds:
getIndexingDelaySeconds(chainId, this.config) * 2,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexing CCTP events is much fast than SpokePool events via the SDK, so we can increase the delay a bit here

@amateima amateima force-pushed the amatei/acx-4576-send-burn-txns-to-finalization-bot branch from bb2a4aa to c894c13 Compare October 23, 2025 16:42
@amateima amateima requested a review from ashwinrava October 23, 2025 16:42
@amateima amateima marked this pull request as ready for review October 23, 2025 16:42
Copy link
Member

@ashwinrava ashwinrava left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Comment on lines 25 to 27
const topic = await this.pubSub.topic(
this.config.pubSubCctpFinalizerTopic,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.pubSub.topic() is not an async function right? Do we need the await?

Copy link
Contributor Author

@amateima amateima Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. edited

Comment on lines +86 to +98
/**
* @notice Returns the CCTP domain for a given chain ID. Throws if the chain ID is not a CCTP domain.
* @param chainId
* @returns CCTP Domain ID
*/
export function getCctpDomainForChainId(chainId: number): number {
const cctpDomain = PUBLIC_NETWORKS[chainId]?.cctpDomain;
if (!across.utils.isDefined(cctpDomain) || cctpDomain === CCTP_NO_DOMAIN) {
throw new Error(`No CCTP domain found for chainId: ${chainId}`);
}
return cctpDomain;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same function and fetchAttestationsForTxn is being used in the finalizer service too. Maybe we can consolidate in the SDK later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @ashwinrava. Nick just merged a PR with CCTP utils which is going to be integrated soon in the indexer 👍

const attestations = await fetchAttestationsForTxn(
getCctpDomainForChainId(Number(burnEvent.chainId)),
transactionHash,
true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we ever need to handle testnets?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean that getCctpDomainForChainId function doesn't handle testnets?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean true is being passed to the isMainnet field on line 147. In the finalizer service I call chainIsProd(sourceChainId) and then pass that value to fetchAttestationsForTxn. Just thinking if we need to do something similar here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants