3.2 Bridge Pattern Analysis
The method used to authenticate the Canton intent on the public chain dictates the trust model and gas efficiency. Three primary patterns are supported, each with distinct trade-offs.
Pattern Comparison Overview
| Pattern | Trust Level | Speed | Gas Cost | Security Model |
|---|---|---|---|---|
| P1: Signed Intent Relay | High trust in Relayer | Fast | Low | Relayer + Canton sigs |
| P2: Proof Publication | Low trust (cryptographic) | Moderate | High | On-chain verification |
| P3: MPC-Signed Tx | Trust in MPC provider | Very Fast | Lowest | Standard custody |
Pattern 1: Signed Intent Relay
Overview
Canton signs the ExecutionIntent payload. The Relayer verifies this intent, packages it, and then co-signs the resulting public chain transaction.
Architecture
┌────────────┐
│ Canton │
│ (Signs │
│ Intent) │
└──────┬─────┘
│ cantonSig
│
┌──────▼─────┐
│ Relayer │
│ (Verifies │
│ + Signs │
│ TX) │
└──────┬─────┘
│ relayerSig
│
┌──────▼─────┐
│ Vault │
│ Contract │
│ (Verifies │
│ both) │
└────────────┘Vault Contract Implementation
contract VaultP1 {
address public cantonSigner;
address public authorizedRelayer;
uint256 public nonce;
function executeIntent(
bytes calldata intentPayload,
bytes calldata cantonSig,
bytes calldata /* proof - unused */
) external {
// 1. Verify caller is authorized Relayer
require(
msg.sender == authorizedRelayer,
"Unauthorized relayer"
);
// 2. Verify Canton signature
bytes32 intentHash = keccak256(intentPayload);
address signer = recoverSigner(intentHash, cantonSig);
require(signer == cantonSigner, "Invalid Canton signature");
// 3. Parse and execute intent
Intent memory intent = abi.decode(intentPayload, (Intent));
require(intent.nonce > nonce, "Invalid nonce");
require(block.timestamp <= intent.deadline, "Intent expired");
nonce = intent.nonce;
_executeActions(intent.actions);
}
}Advantages
- Low gas overhead: Only one signature verification
- High speed: Fast submission and confirmation
- Simple implementation: Minimal smart contract complexity
Disadvantages
- Relayer trust: High dependency on Relayer honesty
- Single point of failure: Relayer compromise = system compromise
- Equivocation risk: Malicious Relayer could submit conflicting transactions
Use Cases
- Internal deployments with trusted infrastructure
- Development and testing environments
- Low-value operations with acceptable risk
- High-frequency trading requiring minimal latency
Pattern 2: Proof Publication + On-chain Verification
Overview
This pattern enhances non-repudiation by making the Canton policy verifiable directly by the public smart contract.
Architecture
┌────────────┐
│ Canton │
│ (Signs │
│ Intent + │
│ Publishes │
│ PubKey) │
└──────┬─────┘
│
├──────────────────┐
│ cantonSig │ publicKey
│ │
┌──────▼─────┐ ┌───────▼────────┐
│ Relayer │ │ Key Registry │
│ (Submits │───▶│ Contract │
│ Intent) │ │ (Stores Keys) │
└──────┬─────┘ └────────────────┘
│ │
│ │ lookup
┌──────▼──────────────────▼─┐
│ Vault Contract │
│ (Verifies Canton sig │
│ against Registry) │
└────────────────────────────┘Key Registry Contract
contract CantonKeyRegistry {
mapping(bytes32 => address) public cantonPartyToKey;
address public admin;
event KeyRegistered(
bytes32 indexed partyId,
address publicKey
);
function registerKey(
bytes32 partyId,
address publicKey
) external {
require(msg.sender == admin, "Not authorized");
cantonPartyToKey[partyId] = publicKey;
emit KeyRegistered(partyId, publicKey);
}
}Vault Contract Implementation
contract VaultP2 {
ICantonKeyRegistry public keyRegistry;
bytes32 public authorizedPartyId;
uint256 public nonce;
function executeIntent(
bytes calldata intentPayload,
bytes calldata cantonSig,
bytes calldata proof
) external {
// 1. Recover signer from Canton signature
bytes32 intentHash = keccak256(intentPayload);
address recoveredSigner = recoverSigner(intentHash, cantonSig);
// 2. Verify against Canton Key Registry
address authorizedKey = keyRegistry.cantonPartyToKey(
authorizedPartyId
);
require(
recoveredSigner == authorizedKey,
"Invalid Canton signer"
);
// 3. Parse and execute intent
Intent memory intent = abi.decode(intentPayload, (Intent));
require(intent.nonce > nonce, "Invalid nonce");
require(block.timestamp <= intent.deadline, "Intent expired");
nonce = intent.nonce;
_executeActions(intent.actions);
}
}Canton Key Synchronization
The Relayer must synchronize Canton public keys to the registry:
async function syncCantonKeys(): Promise<void> {
// 1. Fetch authorized parties from Canton
const parties = await canton.getTopology();
// 2. Update on-chain registry
for (const party of parties) {
const currentKey = await keyRegistry.cantonPartyToKey(
party.partyId
);
if (currentKey !== party.publicKey) {
await keyRegistry.registerKey(
party.partyId,
party.publicKey
);
}
}
}Advantages
- Trustless verification: Vault contract trusts Canton directly
- No Relayer trust: Relayer is just a messenger
- Strong non-repudiation: Cryptographic proof on-chain
- Transparent: Anyone can verify intent authenticity
Disadvantages
- Higher gas cost: ECDSA recovery + registry lookup
- Key management complexity: Must sync Canton topology
- Registry dependency: Additional infrastructure component
Use Cases
- High-value operations requiring maximum security
- Multi-institution deployments
- Regulatory environments requiring proof
- Long-term archival and auditability
Pattern 3: MPC-Signed Transactions
Overview
For institutions using professional custody solutions, the Multichain Party Computation (MPC) model offers the fastest execution.
Architecture
┌────────────┐
│ Canton │
│ (Emits │
│ Intent) │
└──────┬─────┘
│ unsigned intent
│
┌──────▼─────┐
│ Relayer │
│ (Prepares │
│ Raw TX) │
└──────┬─────┘
│ unsigned tx
│
┌──────▼─────┐
│ MPC │
│ Signing │
│ Cluster │
└──────┬─────┘
│ signed tx
│
┌──────▼─────┐
│ Public │
│ Chain │
└────────────┘Relayer Integration
async function executeMPCPattern(
intent: ExecutionIntent
): Promise<TransactionReceipt> {
// 1. Prepare unsigned transaction
const unsignedTx = {
to: vaultAddress,
data: encodeSwapCall(intent.actions),
value: 0,
chainId: intent.targetChain,
nonce: await getChainNonce()
};
// 2. Send to MPC for policy check + signing
const signedTx = await mpcService.signTransaction({
transaction: unsignedTx,
cantonIntent: intent,
policyValidation: true
});
// 3. Broadcast to chain
return await provider.sendTransaction(signedTx);
}MPC Service Policy Enforcement
The MPC provider must implement internal policy checks:
// MPC Service Internal Logic
async function signTransaction(
request: SignRequest
): Promise<SignedTransaction> {
// 1. Verify Canton intent signature
const intentValid = await verifyCantonSignature(
request.cantonIntent
);
if (!intentValid) throw new Error('Invalid intent');
// 2. Check internal policy constraints
await enforceMPCPolicy(request.transaction, request.cantonIntent);
// 3. Multi-party signing ceremony
const signature = await mpcSign(
request.transaction,
this.vaultKeyShares
);
return {
...request.transaction,
signature
};
}
async function enforceMPCPolicy(
tx: Transaction,
intent: ExecutionIntent
): Promise<void> {
// Mirror Daml VaultPolicy constraints
const policy = await fetchPolicyFromCanton(intent.policyId);
// Check transaction value limits
if (tx.value > policy.maxTransactionValue) {
throw new Error('Exceeds max transaction value');
}
// Check destination whitelisting
if (!policy.allowedContracts.includes(tx.to)) {
throw new Error('Destination not whitelisted');
}
// Check daily limits
await checkDailyLimits(policy.dailyLimit);
}Vault Contract (Simplified)
contract VaultP3 {
// No special verification needed
// Standard custody model with EOA/multisig
function swap(
address tokenIn,
address tokenOut,
uint256 amountIn
) external {
// Only callable by the MPC-controlled vault address
require(msg.sender == vaultOwner, "Not authorized");
// Execute swap logic
_performSwap(tokenIn, tokenOut, amountIn);
}
}Advantages
- Fastest execution: No on-chain verification overhead
- Lowest gas cost: Standard transaction fees
- Professional custody: Industry-standard MPC providers
- Multichain consistency: Same address across chains
Disadvantages
- MPC trust: Must trust MPC provider's policy enforcement
- Policy drift risk: MPC and Canton policies must stay synchronized
- Vendor lock-in: Dependent on specific MPC provider
- Less transparent: Policy enforcement not visible on-chain
Use Cases
- High-frequency trading operations
- Multi-strategy institutional vaults
- Cross-chain operations requiring speed
- Large AUM requiring professional custody
Pattern Selection Guide
Decision Matrix
Speed Gas Trust Auditability
───── ─── ───── ────────────
Pattern 1 (Relay) ★★★★ ★★★★ ★★ ★★★
Pattern 2 (Proof) ★★★ ★★ ★★★★★ ★★★★★
Pattern 3 (MPC) ★★★★★ ★★★★★ ★★★ ★★★★Recommendation Flow
Start
│
├─ Need professional custody?
│ Yes → Pattern 3 (MPC)
│ No ↓
│
├─ Need maximum trustlessness?
│ Yes → Pattern 2 (Proof)
│ No ↓
│
├─ Internal deployment only?
│ Yes → Pattern 1 (Relay)
│ No → Pattern 2 (Proof)Hybrid Approach
Institutions can use different patterns for different operation types:
function selectPattern(intent: ExecutionIntent): BridgePattern {
if (intent.value > HIGH_VALUE_THRESHOLD) {
return 'ProofPublication'; // Pattern 2 for high-value
}
if (intent.frequency === 'high') {
return 'MPCSigned'; // Pattern 3 for high-frequency
}
return 'SignedIntentRelay'; // Pattern 1 for routine ops
}Implementation Checklist
Pattern 1 (Signed Intent Relay)
- [ ] Deploy Relayer with secure key management
- [ ] Whitelist Relayer address in vault contract
- [ ] Implement Canton signature verification
- [ ] Set up monitoring for Relayer equivocation
- [ ] Regular Relayer key rotation
Pattern 2 (Proof Publication)
- [ ] Deploy Canton Key Registry contract
- [ ] Sync Canton Topology to registry
- [ ] Implement on-chain ECDSA verification
- [ ] Set up automated key sync service
- [ ] Test key rotation procedures
Pattern 3 (MPC-Signed)
- [ ] Integrate with MPC provider API
- [ ] Mirror Canton policies in MPC service
- [ ] Implement policy sync mechanism
- [ ] Test MPC signing latency
- [ ] Set up MPC cluster monitoring
