diff --git a/modules/sdk-coin-canton/README.md b/modules/sdk-coin-canton/README.md index 97f7d3c6af..b995b07c2b 100644 --- a/modules/sdk-coin-canton/README.md +++ b/modules/sdk-coin-canton/README.md @@ -23,6 +23,22 @@ const sdk = new BitGoAPI(); sdk.register('canton', Canton.createInstance); ``` +## Documentation + +Canton is a privacy-enabled blockchain with unique requirements: + +- **[Wallet Initialization Guide](./docs/WALLET_INITIALIZATION.md)** - Comprehensive guide to Canton wallet initialization transactions +- **[Quick Reference](./docs/WALLET_INITIALIZATION_QUICK_REF.md)** - Quick reference for wallet initialization +- **[Flow Diagram](./docs/WALLET_INITIALIZATION_FLOW.md)** - Visual flow diagram of the initialization process + +### Key Features + +- **Party-based Model**: Wallets are registered as "parties" on the Canton network +- **Wallet Initialization Required**: All wallets must complete an initialization transaction before use +- **EdDSA Signatures**: Uses Ed25519 curve for cryptographic operations +- **Multi-signature Support**: Threshold signatures with confirming and observing participants +- **Memo-based Addressing**: Address format: `partyHint::fingerprint?memoId=index` + ## Development Most of the coin implementations are derived from `@bitgo/sdk-core`, `@bitgo/statics`, and coin specific packages. These implementations are used to interact with the BitGo API and BitGo platform services. diff --git a/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION.md b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION.md new file mode 100644 index 0000000000..3f6c872230 --- /dev/null +++ b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION.md @@ -0,0 +1,519 @@ +# Canton Wallet Initialization Transaction + +## Overview + +Canton is a privacy-enabled blockchain that requires a special **wallet initialization transaction** before a wallet can send or receive assets. This document explains the Canton wallet initialization process in the BitGoJS SDK. + +## Why Is Wallet Initialization Required? + +Unlike traditional blockchains where addresses are derived directly from public keys, Canton uses a **party-based model** where: + +1. **Party Registration**: Each wallet must be registered as a "party" on the Canton network's topology +2. **Multi-signature Setup**: The wallet is configured with threshold signatures using EdDSA (Edwards-curve Digital Signature Algorithm) +3. **Participant Configuration**: The wallet defines confirming and observing participants with specific roles + +The `requiresWalletInitializationTransaction()` method in `canton.ts` returns `true`, indicating this special transaction is mandatory before any transfers can occur. + +## Transaction Components + +### 1. WalletInitRequest + +The request object contains the following fields: + +```typescript +interface WalletInitRequest { + partyHint: string; // 5-character max identifier for the party + publicKey: IPublicKey; // EdDSA public key in specific format + localParticipantObservationOnly: boolean; // Whether local participant only observes + otherConfirmingParticipantUids: string[]; // UIDs of other confirming participants + confirmationThreshold: number; // Number of confirmations required (min: 1) + observingParticipantUids: string[]; // UIDs of observing participants +} +``` + +**Key Fields Explained:** + +- **partyHint**: A short, human-readable identifier (max 5 characters) that becomes part of the party's address +- **publicKey**: Contains: + - `format`: "CRYPTO_KEY_FORMAT_RAW" + - `keyData`: Base64-encoded EdDSA public key + - `keySpec`: "SIGNING_KEY_SPEC_EC_CURVE25519" +- **confirmationThreshold**: Minimum number of participants required to confirm transactions (for multi-sig) +- **otherConfirmingParticipantUids**: List of participant unique identifiers that can confirm transactions +- **observingParticipantUids**: List of participants that can only observe, not confirm + +### 2. PreparedParty + +After the request is sent to Canton, the network returns a `PreparedParty` object: + +```typescript +interface PreparedParty { + partyId: string; // Full party identifier: "partyHint::fingerprint" + publicKeyFingerprint: string; // SHA-256 hash of the public key + topologyTransactions: string[]; // Base64-encoded topology transactions + multiHash: string; // Combined hash of all topology transactions + shouldIncludeTxnType?: boolean; // Optional flag for transaction type inclusion +} +``` + +**Key Fields Explained:** + +- **partyId**: The complete party identifier in format `partyHint::fingerprint`, where fingerprint is derived from the public key +- **publicKeyFingerprint**: SHA-256 hash computed using Canton's specific hashing scheme (purpose 12) +- **topologyTransactions**: Array of Canton topology transactions that register the party on the network +- **multiHash**: A composite hash computed from all topology transactions, used for signing and validation + +### 3. WalletInitTransaction + +The transaction object encapsulates the wallet initialization: + +```typescript +class WalletInitTransaction extends BaseTransaction { + private _preparedParty: PreparedParty; + + // Returns the data to be signed (the multiHash) + get signablePayload(): Buffer { + return Buffer.from(this._preparedParty.multiHash, 'base64'); + } + + // Converts to broadcast-ready format + toBroadcastFormat(): string { + // Returns base64-encoded JSON with: + // - preparedParty + // - onboardingTransactions (topology transactions) + // - multiHashSignatures (signatures from all signers) + } +} +``` + +## Wallet Initialization Flow + +### Step 1: Build the Wallet Init Request + +```typescript +import { WalletInitBuilder } from '@bitgo/sdk-coin-canton'; +import { coins } from '@bitgo/statics'; + +const builder = new WalletInitBuilder(coins.get('canton')); + +builder + .publicKey('zs4J2IrVpfYNHN0bR7EHS0Fb3rETUyyu2L2QwxucPjg=') // Base64 EdDSA public key + .partyHint('alice') // 5-char max identifier + .confirmationThreshold(2) // Require 2 confirmations + .otherConfirmingParticipantUid('participant-uid-1') // Add confirming participant + .otherConfirmingParticipantUid('participant-uid-2') // Add another confirming participant + .observingParticipantUid('observer-uid-1'); // Add observer + +const walletInitRequest = builder.toRequestObject(); +``` + +### Step 2: Submit Request to Canton Network + +The `walletInitRequest` is sent to the Canton network API, which: +1. Validates the public key and party hint +2. Generates topology transactions for party registration +3. Computes the fingerprint from the public key +4. Creates the full partyId: `partyHint::fingerprint` +5. Computes a multiHash of all topology transactions +6. Returns a `PreparedParty` object + +### Step 3: Sign the Transaction + +```typescript +// The PreparedParty is received from Canton network +const preparedParty = { + partyId: 'alice::1220389e648074c708ead527fd...', + publicKeyFingerprint: '1220389e648074c708ead527fd...', + topologyTransactions: ['base64-tx-1', 'base64-tx-2', ...], + multiHash: 'base64-multihash' +}; + +// Set the prepared party on the builder's transaction +builder.transaction = preparedParty; + +// The signablePayload is the multiHash +const payload = builder.transaction.signablePayload; + +// Sign with EdDSA private key +const signature = eddsaSign(payload, privateKey); + +// Add signature to transaction +builder.addSignature(publicKey, signature); +``` + +### Step 4: Validate the Transaction + +```typescript +// Validate that topology transactions match the multiHash +builder.validateRawTransaction(preparedParty.topologyTransactions); + +// Validate the complete transaction +const walletInitTxn = builder.transaction; +builder.validateTransaction(walletInitTxn); +``` + +The validation computes a local hash from the topology transactions and compares it to the `multiHash`: + +```typescript +// Hash computation algorithm +function computeHashFromCreatePartyResponse(topologyTransactions: string[]): string { + // 1. Convert each transaction from base64 to buffer + const txBuffers = topologyTransactions.map(tx => Buffer.from(tx, 'base64')); + + // 2. Hash each transaction with purpose 11 (returns hex strings with '1220' prefix) + const rawHashes = txBuffers.map(tx => computeSha256CantonHash(11, tx)); + // Each hash is a 68-char hex string: '1220' + SHA-256 hash (64 chars) + + // 3. Combine hashes with length prefixes + const combinedHashes = computeMultiHashForTopology(rawHashes); + // This sorts hashes, prefixes each with its length, and adds a count prefix + + // 4. Hash the combined buffer with purpose 55 + const computedHash = computeSha256CantonHash(55, combinedHashes); + + // 5. Convert final hex hash to base64 + return Buffer.from(computedHash, 'hex').toString('base64'); +} + +// Canton hash function: prefixes data with purpose, hashes with SHA-256, adds multihash prefix +function computeSha256CantonHash(purpose: number, bytes: Buffer): string { + const hashInput = prefixedInt(purpose, bytes); // 4-byte big-endian purpose + data + const hash = crypto.createHash('sha256').update(hashInput).digest(); + const multiprefix = Buffer.from([0x12, 0x20]); // SHA-256 multihash indicator + return Buffer.concat([multiprefix, hash]).toString('hex'); +} + +// Combines multiple hashes with sorting and length prefixing +function computeMultiHashForTopology(hashes: string[]): Buffer { + // 1. Convert hex strings to buffers and sort lexicographically + const sortedHashes = hashes + .map(hex => Buffer.from(hex, 'hex')) + .sort((a, b) => a.toString('hex').localeCompare(b.toString('hex'))); + + // 2. Build combined buffer: count + (length + hash) for each hash + const numHashesBytes = encodeInt32(sortedHashes.length); + const parts: Buffer[] = [numHashesBytes]; + + for (const h of sortedHashes) { + const lengthBytes = encodeInt32(h.length); + parts.push(lengthBytes, h); + } + + return Buffer.concat(parts); +} +``` + +### Step 5: Broadcast the Transaction + +```typescript +// Convert to broadcast format +const broadcastData = walletInitTxn.toBroadcastFormat(); + +// This creates a base64-encoded JSON structure: +{ + preparedParty: { ... }, + onboardingTransactions: [ + { transaction: 'base64-tx-1' }, + { transaction: 'base64-tx-2' }, + ... + ], + multiHashSignatures: [ + { + format: 'SIGNATURE_FORMAT_RAW', + signature: 'base64-signature', + signedBy: 'publicKeyFingerprint', + signingAlgorithmSpec: 'SIGNING_ALGORITHM_SPEC_ED25519' + } + ] +} + +// Submit to Canton network for processing +``` + +## Address Format + +After successful wallet initialization, the Canton address format is: + +``` +partyHint::fingerprint?memoId=index +``` + +**Example:** +``` +alice::1220389e648074c708ead527fd8c7b5e92e29c27ad70a9f08931f3f8e3a4c23cb841?memoId=0 +``` + +Where: +- `alice` = partyHint (user-friendly identifier) +- `1220389e648074c708ead527fd8c7b5e92e29c27ad70a9f08931f3f8e3a4c23cb841` = fingerprint (68-char hex derived from public key) +- `?memoId=0` = optional memo identifier for sub-accounts + +## Multi-Signature Configuration + +Canton supports threshold signatures where multiple participants must confirm transactions: + +```typescript +// Example: 2-of-3 multisig wallet +builder + .publicKey(derivedPublicKey) + .partyHint('vault') + .confirmationThreshold(2) // Require 2 signatures + .otherConfirmingParticipantUid('signer-1') // First co-signer + .otherConfirmingParticipantUid('signer-2') // Second co-signer + .otherConfirmingParticipantUid('signer-3'); // Third co-signer +``` + +**Participant Types:** + +1. **Confirming Participants**: Can sign and approve transactions + - Must reach the `confirmationThreshold` to execute transactions + - Configured via `otherConfirmingParticipantUids` + +2. **Observing Participants**: Can only view transactions + - Cannot sign or approve + - Useful for auditing and compliance + - Configured via `observingParticipantUids` + +3. **Local Participant**: The wallet owner + - Can be set to observation-only mode via `localParticipantObservationOnly(true)` + +## Security Considerations + +### 1. Hash Validation + +Always validate that the `multiHash` matches the `topologyTransactions`: + +```typescript +const localHash = utils.computeHashFromCreatePartyResponse( + preparedParty.topologyTransactions +); + +if (localHash !== preparedParty.multiHash) { + throw new Error('Invalid transaction: hash mismatch'); +} +``` + +This prevents man-in-the-middle attacks where topology transactions could be altered. + +### 2. Public Key Format + +The public key must be: +- EdDSA (Ed25519 curve) +- Base64-encoded +- 32 bytes in length + +Validation: + +```typescript +// Validates using isValidEd25519PublicKey from @bitgo/sdk-core +utils.isValidPublicKey(publicKeyBase64); +``` + +### 3. Signature Algorithm + +Canton uses EdDSA with the following specifications: +- **Algorithm**: EdDSA (Edwards-curve Digital Signature Algorithm) +- **Curve**: Ed25519 +- **Hash Function**: SHA-512 (internally by EdDSA) +- **Format**: Raw signature bytes, base64-encoded + +## Transaction Explanation + +When explaining a wallet initialization transaction: + +```typescript +const explanation = walletInitTxn.explainTransaction(); + +// Returns: +{ + id: 'multiHashValue', // Transaction ID (the multiHash) + type: 'WalletInitialization', // Transaction type + outputs: [], // No outputs for init + outputAmount: '0', // No value transfer + changeOutputs: [], // No change + changeAmount: '0', // No change amount + fee: { fee: '0' } // No fee for initialization +} +``` + +Wallet initialization transactions: +- Have no monetary value transfer +- Have no fees (network registration is typically free or handled separately) +- Are one-time operations per wallet + +## Common Errors and Solutions + +### Error: "partyHint cannot be empty" + +**Cause**: The party hint was not provided or is an empty string. + +**Solution**: +```typescript +builder.partyHint('alice'); // Provide a non-empty string (max 5 chars) +``` + +### Error: "partyHint must be less than 6 characters long" + +**Cause**: The party hint exceeds 5 characters. + +**Solution**: +```typescript +builder.partyHint('alice'); // Use 5 or fewer characters +``` + +### Error: "Invalid publicKey" + +**Cause**: Public key is missing required fields or is invalid. + +**Solution**: +```typescript +// Ensure public key is base64-encoded Ed25519 key +const validPublicKey = 'zs4J2IrVpfYNHN0bR7EHS0Fb3rETUyyu2L2QwxucPjg='; +builder.publicKey(validPublicKey); +``` + +### Error: "invalid raw transaction, hash not matching" + +**Cause**: The topology transactions don't match the provided multiHash. + +**Solution**: This indicates the prepared party data was corrupted or tampered with. Re-request the prepared party from the Canton network. + +### Error: "confirmationThreshold must be a positive integer" + +**Cause**: Threshold is 0, negative, or not an integer. + +**Solution**: +```typescript +builder.confirmationThreshold(1); // Use a positive integer +``` + +## Testing + +### Unit Test Example + +```typescript +import { WalletInitBuilder, WalletInitTransaction } from '@bitgo/sdk-coin-canton'; +import { coins } from '@bitgo/statics'; + +describe('Wallet Initialization', () => { + it('should create valid wallet init request', () => { + const builder = new WalletInitBuilder(coins.get('canton')); + + builder + .publicKey('zs4J2IrVpfYNHN0bR7EHS0Fb3rETUyyu2L2QwxucPjg=') + .partyHint('alice'); + + const request = builder.toRequestObject(); + + expect(request.partyHint).toBe('alice'); + expect(request.publicKey.keyData).toBe('zs4J2IrVpfYNHN0bR7EHS0Fb3rETUyyu2L2QwxucPjg='); + expect(request.confirmationThreshold).toBe(1); + expect(request.localParticipantObservationOnly).toBe(false); + }); + + it('should validate topology transactions', () => { + const builder = new WalletInitBuilder(coins.get('canton')); + + // Set prepared party from network response + builder.transaction = preparedPartyFromNetwork; + + // Validate - should not throw + builder.validateRawTransaction(preparedPartyFromNetwork.topologyTransactions); + }); +}); +``` + +## Integration with BitGo Platform + +When using the BitGo platform API: + +1. **Create Wallet Request**: Include wallet initialization as part of wallet creation +2. **TSS Key Generation**: BitGo handles EdDSA key generation for TSS wallets +3. **Party Registration**: BitGo submits the wallet init transaction to Canton +4. **Address Generation**: After initialization, addresses are generated using the partyId + +Example workflow with BitGo SDK: + +```typescript +const bitgo = new BitGoAPI({ env: 'test' }); +await bitgo.authenticate({ ... }); + +const canton = bitgo.coin('canton'); + +// Create wallet - includes automatic wallet initialization +const wallet = await canton.wallets().generateWallet({ + label: 'My Canton Wallet', + passphrase: 'secure-passphrase', + // BitGo handles the wallet initialization transaction internally +}); + +// Wallet is now ready to send and receive Canton assets +``` + +## Technical Deep Dive + +### Canton Hashing Scheme + +Canton uses a purpose-based hashing scheme where different hash purposes serve different roles: + +- **Purpose 11**: Hash individual topology transactions +- **Purpose 12**: Hash public keys to create fingerprints +- **Purpose 55**: Hash combined topology transaction hashes + +The hash format is: + +``` +hash = '1220' + SHA-256(purpose_as_4_bytes_big_endian || data) +``` + +Where: +- `purpose` is a 4-byte big-endian integer prefix indicating the hash purpose +- `data` is the content being hashed +- `||` denotes concatenation +- Result is prefixed with `1220` (multihash format indicating SHA-256) +- Final hash is a 68-character hex string: `1220` (4 chars) + SHA-256 (64 chars) + +**Multihash Prefix `1220`:** +- `12` = SHA-256 hash function indicator +- `20` = 32 bytes (hex representation of length) + +### Topology Transactions + +Topology transactions are Canton's mechanism for updating network state: + +1. **Party Addition**: Registers a new party with public key +2. **Namespace Delegation**: Grants permissions to the party +3. **Owner to Key Mapping**: Maps the party to its signing key +4. **Participant State**: Updates participant configuration + +All these transactions are bundled together in the `topologyTransactions` array and must be submitted atomically. + +## Summary + +The Canton wallet initialization transaction is a critical first step that: + +1. **Registers** the wallet as a party on the Canton network +2. **Establishes** the cryptographic identity (EdDSA public key) +3. **Configures** multi-signature and participant settings +4. **Creates** a unique partyId for addressing + +Without this initialization, the wallet cannot participate in transfers or other Canton operations. The process involves careful validation of topology transactions through hash verification and proper signature handling using EdDSA. + +## Related Documentation + +- Canton Transfer Transactions - How to send and receive assets after initialization +- Canton Token Enablement - Enabling tokens for transfers +- Canton Address Format - Understanding Canton addresses +- [EdDSA Signatures](https://ed25519.cr.yp.to/) - EdDSA signature algorithm specification + +## References + +- **Source Files**: + - `modules/sdk-coin-canton/src/lib/walletInitBuilder.ts` - Builder implementation + - `modules/sdk-coin-canton/src/lib/walletInitialization/walletInitTransaction.ts` - Transaction class + - `modules/sdk-coin-canton/src/canton.ts` - Main Canton coin class + - `modules/sdk-coin-canton/src/lib/utils.ts` - Utility functions including hash computation + +- **Test Files**: + - `modules/sdk-coin-canton/test/unit/builder/walletInit/walletInitBuilder.ts` - Unit tests + - `modules/sdk-coin-canton/test/integration/canton.integration.ts` - Integration tests diff --git a/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_FLOW.md b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_FLOW.md new file mode 100644 index 0000000000..40537ba45e --- /dev/null +++ b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_FLOW.md @@ -0,0 +1,247 @@ +# Canton Wallet Initialization Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Canton Wallet Initialization Process │ +└─────────────────────────────────────────────────────────────────────────────┘ + +Step 1: Build Request +┌──────────────────┐ +│ WalletInitBuilder│ +│ │ .publicKey("base64-ed25519-key") +│ - publicKey │ .partyHint("alice") +│ - partyHint │ .confirmationThreshold(1) +│ - threshold │ .otherConfirmingParticipantUid("participant-1") +│ - participants │ +└────────┬─────────┘ + │ toRequestObject() + ▼ +┌────────────────────────────────────────────┐ +│ WalletInitRequest (JSON) │ +├────────────────────────────────────────────┤ +│ { │ +│ partyHint: "alice", │ +│ publicKey: { │ +│ format: "CRYPTO_KEY_FORMAT_RAW", │ +│ keyData: "base64-key", │ +│ keySpec: "SIGNING_KEY_SPEC_EC_CURVE25519"│ +│ }, │ +│ confirmationThreshold: 1, │ +│ otherConfirmingParticipantUids: [...], │ +│ observingParticipantUids: [...], │ +│ localParticipantObservationOnly: false │ +│ } │ +└────────────────┬───────────────────────────┘ + │ + │ HTTP POST to Canton Network API + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Canton Network Processing │ +├─────────────────────────────────────────────────────────────────────┤ +│ 1. Validate public key and party hint │ +│ 2. Compute fingerprint from public key │ +│ - fingerprint = sha256Canton(12, signingPublicKey) │ +│ 3. Create partyId = "alice::1220..." (partyHint::fingerprint) │ +│ 4. Generate topology transactions: │ +│ - Party addition │ +│ - Namespace delegation │ +│ - Owner to key mapping │ +│ - Participant state updates │ +│ 5. Compute multiHash from topology transactions │ +│ - Hash each txn with purpose 11 │ +│ - Sort and combine with length prefixes │ +│ - Hash combined buffer with purpose 55 │ +└────────────────┬────────────────────────────────────────────────────┘ + │ + │ Returns PreparedParty + ▼ + +Step 2: Receive PreparedParty +┌────────────────────────────────────────────┐ +│ PreparedParty (JSON) │ +├────────────────────────────────────────────┤ +│ { │ +│ partyId: "alice::1220389e648074c7...", │ +│ publicKeyFingerprint: "1220389e...", │ +│ topologyTransactions: [ │ +│ "base64-encoded-tx-1", │ +│ "base64-encoded-tx-2", │ +│ "base64-encoded-tx-3" │ +│ ], │ +│ multiHash: "base64-multihash" │ +│ } │ +└────────────────┬───────────────────────────┘ + │ + │ builder.transaction = preparedParty + ▼ + +Step 3: Validate and Sign +┌────────────────────────────────────────────┐ +│ Validation Process │ +├────────────────────────────────────────────┤ +│ 1. Decode topology transactions │ +│ 2. Compute local multiHash: │ +│ ┌─────────────────────────────────┐ │ +│ │ For each topology transaction: │ │ +│ │ hash = sha256Canton(11, tx) │ │ +│ │ → "1220" + 64-char-hex │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ Sort hashes lexicographically │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ Combine with length prefixes: │ │ +│ │ count(4B) + [len(4B)+hash]... │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ Hash combined buffer: │ │ +│ │ sha256Canton(55, combined) │ │ +│ │ → Convert hex to base64 │ │ +│ └─────────────────────────────────┘ │ +│ 3. Compare: localHash === multiHash ✓ │ +└────────────────┬───────────────────────────┘ + │ + │ Validation passed + ▼ +┌────────────────────────────────────────────┐ +│ Signing Process │ +├────────────────────────────────────────────┤ +│ signablePayload = multiHash (base64) │ +│ │ │ +│ ▼ │ +│ signature = eddsaSign( │ +│ Buffer.from(multiHash, 'base64'), │ +│ privateKey │ +│ ) │ +│ │ │ +│ ▼ │ +│ builder.addSignature(publicKey, signature)│ +└────────────────┬───────────────────────────┘ + │ + ▼ + +Step 4: Broadcast Transaction +┌────────────────────────────────────────────────────────────────┐ +│ toBroadcastFormat() │ +├────────────────────────────────────────────────────────────────┤ +│ Creates WalletInitBroadcastData (JSON): │ +│ { │ +│ preparedParty: { ... }, │ +│ onboardingTransactions: [ │ +│ { transaction: "base64-topology-tx-1" }, │ +│ { transaction: "base64-topology-tx-2" }, │ +│ { transaction: "base64-topology-tx-3" } │ +│ ], │ +│ multiHashSignatures: [ │ +│ { │ +│ format: "SIGNATURE_FORMAT_RAW", │ +│ signature: "base64-signature", │ +│ signedBy: "publicKeyFingerprint", │ +│ signingAlgorithmSpec: "SIGNING_ALGORITHM_SPEC_ED25519" │ +│ } │ +│ ] │ +│ } │ +│ │ +│ → Encoded as base64 for transmission │ +└────────────────┬───────────────────────────────────────────────┘ + │ + │ HTTP POST to Canton Network + ▼ +┌────────────────────────────────────────────┐ +│ Canton Network - Process Onboarding │ +├────────────────────────────────────────────┤ +│ 1. Verify multiHash signatures │ +│ 2. Apply topology transactions atomically │ +│ 3. Register party in network state │ +│ 4. Party becomes active and addressable │ +└────────────────┬───────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────┐ +│ ✓ Wallet Initialized! │ +├────────────────────────────────────────────┤ +│ partyId: alice::1220389e648074c7... │ +│ Address: alice::1220...?memoId=0 │ +│ Status: Ready for transfers │ +└────────────────────────────────────────────┘ +``` + +## Key Takeaways + +### Hash Computation Flow +``` +Topology Txns (base64) + │ + ├─► Decode to Buffer + │ + ├─► Hash each with purpose 11 → Hex strings with '1220' prefix + │ (68 chars each) + ├─► Sort lexicographically + │ + ├─► Combine with length prefixes: + │ [numHashes(4B)] + [length(4B) + hash(34B)] + ... + │ + ├─► Hash combined with purpose 55 → Hex string with '1220' prefix + │ + └─► Convert to base64 → multiHash +``` + +### Canton Hash Function +``` +sha256Canton(purpose, data): + 1. Create prefix: 4-byte big-endian integer (purpose) + 2. Concatenate: prefix + data + 3. SHA-256 hash the result + 4. Prepend multihash prefix: 0x1220 (SHA-256 indicator) + 5. Return as hex string (68 characters) +``` + +### Multi-Signature Workflow +``` +For 2-of-3 multisig: + +Signer 1 Signer 2 Signer 3 + │ │ │ + ├─► Sign multiHash │ │ + │ │ │ + │ ◄──────────── Collect signature 1 ──────────►│ + │ │ │ + │ ├─► Sign multiHash │ + │ │ │ + │ ◄──────────── Collect signature 2 ──────────►│ + │ │ │ + └────────────────────────┴────────────────────────┘ + │ + ▼ + Broadcast with both signatures + (Threshold = 2 met ✓) +``` + +## Address Components + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Canton Address Format │ +├──────────────────────────────────────────────────────────────────┤ +│ │ +│ alice::1220389e648074c708ead527fd8c7b5e92e29c27ad70a9...?memoId=0│ +│ └───┘ └──────────────────────────────────────────────┘ └────┘│ +│ partyHint publicKeyFingerprint memoId │ +│ (max 5 chars) (68-char hex: 1220 + SHA-256) (optional)│ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## Transaction Types After Initialization + +Once the wallet is initialized, it can perform: + +1. **Transfer** - Send assets to another party +2. **TransferAccept** - Accept incoming transfers +3. **TransferReject** - Reject incoming transfers +4. **TransferAcknowledge** - Acknowledge completed transfers +5. **OneStepPreApproval** - Enable tokens for use +6. **TransferOfferWithdrawn** - Withdraw transfer offers + +All of these require the wallet to be initialized first! diff --git a/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_QUICK_REF.md b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_QUICK_REF.md new file mode 100644 index 0000000000..c0fd3e5c2a --- /dev/null +++ b/modules/sdk-coin-canton/docs/WALLET_INITIALIZATION_QUICK_REF.md @@ -0,0 +1,228 @@ +# Canton Wallet Initialization - Quick Reference + +## TL;DR + +Canton wallets **must** complete a wallet initialization transaction before they can send or receive assets. This one-time transaction registers the wallet as a "party" on the Canton network. + +## Quick Start + +```typescript +import { WalletInitBuilder } from '@bitgo/sdk-coin-canton'; +import { coins } from '@bitgo/statics'; + +// 1. Create the builder +const builder = new WalletInitBuilder(coins.get('canton')); + +// 2. Configure the wallet +builder + .publicKey('your-base64-eddsa-public-key') + .partyHint('alice'); // 5 chars max + +// 3. Build the request +const request = builder.toRequestObject(); + +// 4. Send request to Canton network (returns PreparedParty) +// 5. Set the prepared party on transaction +builder.transaction = preparedParty; + +// 6. Sign the transaction +builder.addSignature(publicKey, signature); + +// 7. Broadcast +const broadcastData = builder.transaction.toBroadcastFormat(); +``` + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| **partyHint** | Short identifier (max 5 chars) for the wallet, e.g., "alice" | +| **partyId** | Full identifier: `partyHint::fingerprint`, e.g., "alice::1220389e..." | +| **fingerprint** | 68-char hex derived from public key using SHA-256 | +| **topologyTransactions** | Array of transactions that register the party on Canton | +| **multiHash** | Combined hash of all topology transactions (what you sign) | +| **EdDSA** | Signature algorithm (Ed25519 curve) | + +## Request Fields + +```typescript +{ + partyHint: string, // Required: max 5 chars + publicKey: { + format: "CRYPTO_KEY_FORMAT_RAW", // Fixed value + keyData: string, // Base64 Ed25519 public key + keySpec: "SIGNING_KEY_SPEC_EC_CURVE25519" // Fixed value + }, + confirmationThreshold: number, // Default: 1 (for multisig) + otherConfirmingParticipantUids: string[], // Co-signers for multisig + observingParticipantUids: string[], // Read-only participants + localParticipantObservationOnly: boolean // Default: false +} +``` + +## Response (PreparedParty) + +```typescript +{ + partyId: string, // "partyHint::fingerprint" + publicKeyFingerprint: string, // Derived from public key + topologyTransactions: string[], // Base64-encoded transactions + multiHash: string // Hash of all topology txns +} +``` + +## Signing + +```typescript +// The signablePayload is the multiHash +const payload = builder.transaction.signablePayload; + +// Sign with EdDSA (Ed25519) +const signature = eddsaSign(payload, privateKey); + +// Add to transaction +builder.addSignature(publicKey, signature); +``` + +## Validation + +```typescript +// Validates topology transactions match multiHash +builder.validateRawTransaction(preparedParty.topologyTransactions); + +// Validates complete transaction +builder.validateTransaction(walletInitTxn); +``` + +## Broadcast Format + +```typescript +{ + preparedParty: PreparedParty, + onboardingTransactions: [ + { transaction: "base64-topology-tx-1" }, + { transaction: "base64-topology-tx-2" } + ], + multiHashSignatures: [ + { + format: "SIGNATURE_FORMAT_RAW", + signature: "base64-signature", + signedBy: "fingerprint", + signingAlgorithmSpec: "SIGNING_ALGORITHM_SPEC_ED25519" + } + ] +} +``` + +## Multi-Signature Setup + +```typescript +builder + .publicKey(publicKey) + .partyHint('vault') + .confirmationThreshold(2) // 2-of-3 multisig + .otherConfirmingParticipantUid('signer-1') + .otherConfirmingParticipantUid('signer-2') + .otherConfirmingParticipantUid('signer-3'); +``` + +## Common Validations + +| Check | Rule | +|-------|------| +| partyHint | Non-empty, max 5 characters | +| publicKey | Valid base64 Ed25519 key (32 bytes) | +| confirmationThreshold | Positive integer ≥ 1 | +| multiHash | Must match computed hash of topology transactions | + +## Hash Computation + +```typescript +// Step 1: Hash each topology transaction with purpose 11 (returns hex with '1220' prefix) +rawHashes = topologyTxns.map(tx => sha256Canton(11, tx)) + +// Step 2: Sort hashes and combine with length prefixes +// Format: numHashes(4 bytes) + [length(4 bytes) + hash]... +combinedHashes = sortAndCombineWithLengthPrefix(rawHashes) + +// Step 3: Hash the combined buffer with purpose 55 +multiHash = sha256Canton(55, combinedHashes) + +// Step 4: Convert hex to base64 +multiHash_base64 = Buffer.from(multiHash, 'hex').toString('base64') +``` + +**Canton Hash Function:** +``` +hash = '1220' + SHA-256(purpose_4bytes_BE || data) +``` +- Purpose is 4-byte big-endian integer +- `1220` is multihash prefix (12=SHA-256, 20=32 bytes) + +## Canton Address Format + +After initialization, addresses follow this format: + +``` +partyHint::fingerprint?memoId=index +``` + +Example: +``` +alice::1220389e648074c708ead527fd8c7b5e92e29c27ad70a9f08931f3f8e3a4c23cb841?memoId=0 +``` + +## Transaction Properties + +- **Type**: `TransactionType.WalletInitialization` +- **Value**: 0 (no monetary transfer) +- **Fee**: 0 (registration is free) +- **One-time**: Only needed once per wallet + +## Common Errors + +| Error | Fix | +|-------|-----| +| "partyHint cannot be empty" | Provide non-empty string: `.partyHint('alice')` | +| "partyHint must be less than 6 characters long" | Use ≤5 characters | +| "Invalid publicKey" | Use valid base64 Ed25519 key | +| "invalid raw transaction, hash not matching" | Re-request PreparedParty from network | +| "confirmationThreshold must be a positive integer" | Use integer ≥ 1 | + +## Builder Methods + +```typescript +builder.publicKey(key: string) // Set public key +builder.partyHint(hint: string) // Set party hint +builder.confirmationThreshold(n: number) // Set multisig threshold +builder.otherConfirmingParticipantUid(uid: string) // Add confirming participant +builder.observingParticipantUid(uid: string) // Add observer +builder.localParticipantObservationOnly(flag: boolean) // Set local as observer +builder.toRequestObject() // Build request +builder.validateRawTransaction(txns: string[]) // Validate topology txns +builder.validateTransaction(txn: WalletInitTransaction) // Validate complete txn +builder.addSignature(pubKey, signature) // Add signature +``` + +## Testing Example + +```typescript +import { WalletInitBuilder } from '@bitgo/sdk-coin-canton'; +import { coins } from '@bitgo/statics'; + +const builder = new WalletInitBuilder(coins.get('tcanton')); +builder + .publicKey('zs4J2IrVpfYNHN0bR7EHS0Fb3rETUyyu2L2QwxucPjg=') + .partyHint('alice'); + +const request = builder.toRequestObject(); +// request.partyHint === 'alice' +// request.confirmationThreshold === 1 +// request.localParticipantObservationOnly === false +``` + +## See Also + +- [Full Documentation](./WALLET_INITIALIZATION.md) - Complete guide with examples +- [Canton Transfer Transactions](./TRANSFERS.md) - Sending assets after initialization +- Source: `modules/sdk-coin-canton/src/lib/walletInitBuilder.ts`