3.1 The Relayer/Bridge Layer Design
The Relayer (or Adapter) layer functions as the crucial secure boundary between the private Canton policy plane and the public execution environment. This component is an external application designed to transport verifiable action approvals (the signed Intents) securely to the public chains.
Architecture Overview
┌───────────────────────────────────────────┐
│ Canton Ledger API │
│ • ExecutionIntent contracts │
│ • Cryptographic signatures │
└─────────────────┬─────────────────────────┘
│
│ Monitor new intents
│
┌─────────────────▼─────────────────────────┐
│ Relayer Service │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 1. Intent Consumption │ │
│ │ 2. Signature Verification │ │
│ │ 3. Transaction Preparation │ │
│ │ 4. Custody Integration │ │
│ │ 5. Submission │ │
│ │ 6. State Reporting │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬─────────────────────────┘
│
┌─────────┴──────────┐
│ │
┌───────▼─────┐ ┌───────▼─────┐
│ Ethereum │ │ Solana │
│ Arbitrum │ │ Polygon │
│ Optimism │ │ Avalanche │
└─────────────┘ └─────────────┘Core Responsibilities
1. Intent Consumption
The Relayer continuously monitors the Canton Ledger API for new, signed ExecutionIntent contracts issued by the Policy Issuer Party.
// Pseudocode
async function monitorCantonIntents() {
const stream = cantonLedger.streamTransactions({
templateId: 'Vault.Policy:ExecutionIntent',
parties: [policyIssuerParty]
});
for await (const event of stream) {
if (event.type === 'created') {
await processIntent(event.payload);
}
}
}Features:
- Real-time event streaming
- Reliable delivery guarantees
- Idempotency handling
- Error recovery
2. Verification
The Relayer verifies the cryptographic signature (cantonSig) on the intentPayload.
async function verifyIntent(intent: ExecutionIntent): Promise<boolean> {
// 1. Recover public key from signature
const publicKey = recoverPublicKey(
intent.cantonSignature,
hash(intent.payload)
);
// 2. Verify against Canton Topology
const authorizedKeys = await canton.getAuthorizedKeys(
intent.issuer
);
// 3. Confirm authorization
return authorizedKeys.includes(publicKey);
}Verification includes:
- Confirming the signing party (Issuer) is recognized
- Validating authorization within Canton's Topology Management system
- Checking signature cryptographic validity
- Verifying intent hasn't expired
3. Transaction Preparation
The Relayer translates the generic, serialized payload (e.g., JSON) into a blockchain-specific transaction format.
function prepareTransaction(
intent: ExecutionIntent,
targetChain: Chain
): Transaction {
// Encode intent into chain-specific format
const calldata = encodeExecuteIntent(
intent.payload,
intent.cantonSignature,
intent.proof
);
return {
to: vaultContracts[targetChain],
data: calldata,
value: 0,
chainId: targetChain.id,
gasLimit: estimateGas(calldata)
};
}Chain-Specific Encoding:
- EVM: ABI-encoded calldata
- Solana: Transaction instruction format
- Cosmos: Protobuf messages
- Other: Custom encodings
4. Submission
The Relayer initiates the signature process (Pattern 1, 2, or 3) and broadcasts the finalized, signed transaction to the target public chain.
async function submitTransaction(
tx: Transaction,
pattern: BridgePattern
): Promise<TransactionReceipt> {
switch (pattern) {
case 'SignedIntentRelay':
// Pattern 1: Relayer co-signs
return await relayerWallet.sendTransaction(tx);
case 'ProofPublication':
// Pattern 2: Publish proof first
await publishProof(tx.proof);
return await relayerWallet.sendTransaction(tx);
case 'MPCSigned':
// Pattern 3: MPC signs directly
return await mpcService.signAndBroadcast(tx);
}
}5. State Reporting
After execution, the Relayer monitors the public chain for transaction receipts and execution events.
async function reportExecution(
intentId: string,
receipt: TransactionReceipt
): Promise<void> {
const executionReport = {
intentId,
txHash: receipt.transactionHash,
success: receipt.status === 1,
gasUsed: receipt.gasUsed,
blockNumber: receipt.blockNumber,
logs: parseExecutionLogs(receipt.logs)
};
// Report back to Canton
await cantonLedger.exercise({
templateId: 'Vault.Policy:ExecutionIntent',
contractId: intentId,
choice: 'RecordExecution',
argument: executionReport
});
}Reported Data:
- Transaction hash
- Success/failure status
- Gas usage
- Slippage metrics
- Error messages (if any)
High Availability Design
Relayer Cluster
Deploy multiple Relayer instances for fault tolerance:
┌─────────────────────────────────────────┐
│ Load Balancer │
└────┬──────────┬──────────┬─────────────┘
│ │ │
┌────▼────┐ ┌──▼─────┐ ┌──▼─────┐
│Relayer 1│ │Relayer 2│ │Relayer 3│
│ (Active)│ │(Standby)│ │(Standby)│
└─────────┘ └─────────┘ └─────────┘Features:
- Leader election (e.g., using Raft or etcd)
- Automatic failover
- Duplicate submission prevention
- State synchronization
Monitoring & Alerts
alerts:
- name: IntentProcessingDelay
condition: queue_depth > 10
action: page_on_call
- name: VerificationFailure
condition: invalid_signature_count > 0
action: alert_security_team
- name: ChainExecutionFailure
condition: failed_tx_rate > 0.05
action: alert_ops_team
- name: CantonDisconnection
condition: canton_connection_lost
action: immediate_escalationSecurity Considerations
Access Control
- Canton API Access: Secure credentials, rotate regularly
- MPC Integration: Hardware security module (HSM) protected
- Chain RPC: Rate limiting and DDoS protection
- Operational Logs: Encrypted at rest and in transit
Replay Protection
The Relayer must enforce:
- Intent expiration checking
- Nonce ordering verification
- Duplicate submission prevention
- Transaction idempotency
Error Handling
async function processIntentWithRetry(
intent: ExecutionIntent
): Promise<void> {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
// Verify intent is still valid
if (Date.now() > intent.deadline) {
throw new Error('Intent expired');
}
// Process intent
const tx = prepareTransaction(intent);
const receipt = await submitTransaction(tx);
await reportExecution(intent.id, receipt);
return; // Success
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
await reportFailure(intent.id, error);
throw error;
}
await sleep(exponentialBackoff(attempt));
}
}
}Performance Optimization
Batching
For high-throughput scenarios, batch multiple intents:
async function batchIntents(
intents: ExecutionIntent[]
): Promise<Transaction> {
// Combine multiple intents into single transaction
const batchCalldata = encodeMulticall(
intents.map(i => ({
target: vaultAddress,
callData: encodeExecuteIntent(i)
}))
);
return {
to: multicallContract,
data: batchCalldata
};
}Gas Optimization
- Dynamic gas price estimation
- EIP-1559 support with priority fees
- Transaction replacement (RBF) for stuck transactions
- Gas limit optimization based on action complexity
Parallel Execution
Process intents for different chains concurrently:
async function processIntents(
intents: ExecutionIntent[]
): Promise<void> {
// Group by target chain
const byChain = groupBy(intents, i => i.targetChain);
// Process each chain in parallel
await Promise.all(
Object.entries(byChain).map(([chain, chainIntents]) =>
processChainIntents(chain, chainIntents)
)
);
}Observability
Metrics
Track key performance indicators:
relayer_intents_processed_total
relayer_verification_failures_total
relayer_submission_latency_seconds
relayer_gas_used_total
relayer_chain_execution_success_rateTracing
Implement distributed tracing for end-to-end visibility:
Canton Intent Created
→ Relayer Received (span 1)
→ Verification (span 2)
→ Transaction Preparation (span 3)
→ MPC Signing (span 4)
→ Chain Submission (span 5)
→ Confirmation (span 6)
→ Canton Report (span 7)Logging
Structured logging for audit and debugging:
{
"timestamp": "2024-10-14T12:00:00Z",
"level": "INFO",
"component": "relayer",
"intentId": "intent-123",
"event": "intent_processed",
"chain": "ethereum",
"txHash": "0xabc...",
"gasUsed": 150000,
"duration_ms": 2500
}