Multisig Program Signing (Delegated Multisig Logic Signatures) with KMD
Description
Section titled “Description”This example demonstrates how to sign programs with multisig using the KMD signMultisigProgram() method. It shows:
- Creating a 2-of-3 multisig account
- Creating a simple TEAL program (logic signature)
- Compiling the TEAL program using algod tealCompile
- Signing the program with the first participant (partial signature)
- Signing the program with the second participant (completing the multisig)
- Understanding delegated multisig logic signatures What is Multisig Program Signing? Multisig program signing creates a “delegated multisig logic signature”. This combines two powerful concepts: 1. Multisig: Requiring multiple parties to approve 2. Delegated Logic Signatures: Authorizing a program to act on behalf of an account With a delegated multisig lsig:
- The multisig account authorizes a program to sign transactions
- Multiple parties must sign the program (meeting the threshold)
- Once signed, the program can authorize transactions within its logic
- No further interaction needed from the multisig participants Use cases for delegated multisig logic signatures:
- Multi-party controlled recurring payments
- Joint account automation (e.g., business partners authorizing limit)
- Escrow with automated release conditions
- DAO treasury with programmatic spending rules
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start) - Covered operations:
- signMultisigProgram() - Sign a compiled TEAL program with a multisig participant
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example kmd_client/13-multisig-program-signing.ts/** * Example: Multisig Program Signing (Delegated Multisig Logic Signatures) with KMD * * This example demonstrates how to sign programs with multisig using the KMD * `signMultisigProgram()` method. It shows: * - Creating a 2-of-3 multisig account * - Creating a simple TEAL program (logic signature) * - Compiling the TEAL program using algod tealCompile * - Signing the program with the first participant (partial signature) * - Signing the program with the second participant (completing the multisig) * - Understanding delegated multisig logic signatures * * What is Multisig Program Signing? * Multisig program signing creates a "delegated multisig logic signature". * This combines two powerful concepts: * 1. Multisig: Requiring multiple parties to approve * 2. Delegated Logic Signatures: Authorizing a program to act on behalf of an account * * With a delegated multisig lsig: * - The multisig account authorizes a program to sign transactions * - Multiple parties must sign the program (meeting the threshold) * - Once signed, the program can authorize transactions within its logic * - No further interaction needed from the multisig participants * * Use cases for delegated multisig logic signatures: * - Multi-party controlled recurring payments * - Joint account automation (e.g., business partners authorizing limit) * - Escrow with automated release conditions * - DAO treasury with programmatic spending rules * * Prerequisites: * - LocalNet running (via `algokit localnet start`) * * Covered operations: * - signMultisigProgram() - Sign a compiled TEAL program with a multisig participant */
import { Address, encodeAddress } from '@algorandfoundation/algokit-utils';import type { MultisigSig } from '@algorandfoundation/algokit-utils/kmd-client';import { LogicSig, LogicSigAccount, type MultisigSignature,} from '@algorandfoundation/algokit-utils/transact';import { cleanupTestWallet, createAlgodClient, createKmdClient, createTestWallet, loadTealSource, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js';// Use algorand-msgpack for decoding the multisig responseimport { IntMode, decode as msgpackDecode } from 'algorand-msgpack';
/** * Format a byte array for display, showing first and last few bytes * * @param bytes - The bytes to format * @param showFirst - Number of bytes to show at the start (default: 8) * @param showLast - Number of bytes to show at the end (default: 8) * @returns Formatted hex string with ellipsis if truncated */function formatBytesForDisplay(bytes: Uint8Array, showFirst = 8, showLast = 8): string { const hex = Buffer.from(bytes).toString('hex'); if (bytes.length <= showFirst + showLast) { return hex; } const firstBytes = hex.slice(0, showFirst * 2); const lastBytes = hex.slice(-(showLast * 2)); return `${firstBytes}...${lastBytes}`;}
/** * Decode the KMD multisig response bytes into a MultisigSig structure. * The KMD API returns msgpack-encoded MultisigSig with wire keys: * - 'subsig' -> subsignatures array * - 'thr' -> threshold * - 'v' -> version * Each subsig has: * - 'pk' -> publicKey * - 's' -> signature (optional) */function decodeKmdMultisigResponse(multisigBytes: Uint8Array): MultisigSig { const decoded = msgpackDecode(multisigBytes, { intMode: IntMode.AS_ENCODED, useMap: true, rawBinaryStringKeys: true, rawBinaryStringValues: true, }) as Map<Uint8Array, unknown>;
// Helper to find value by string key in Map with Uint8Array keys const findByKey = (map: Map<Uint8Array, unknown>, key: string): unknown => { const keyBytes = new TextEncoder().encode(key); for (const [k, v] of map) { if (k.length === keyBytes.length && k.every((b, i) => b === keyBytes[i])) { return v; } } return undefined; };
const subsigArray = findByKey(decoded, 'subsig') as Array<Map<Uint8Array, unknown>>; const threshold = findByKey(decoded, 'thr') as number; const version = findByKey(decoded, 'v') as number;
const subsignatures = subsigArray.map(subsigMap => { const publicKey = findByKey(subsigMap, 'pk') as Uint8Array; const signature = findByKey(subsigMap, 's') as Uint8Array | undefined; return { publicKey, signature }; });
return { subsignatures, threshold, version };}
/** * Convert a KMD MultisigSig to the transact MultisigSignature format */function kmdMultisigToTransactMultisig(kmdMsig: MultisigSig): MultisigSignature { return { version: kmdMsig.version, threshold: kmdMsig.threshold, subsigs: kmdMsig.subsignatures.map(subsig => ({ publicKey: subsig.publicKey, sig: subsig.signature, })), };}
/** * Count the number of signatures in a KMD MultisigSig */function countSignatures(msig: MultisigSig): number { return msig.subsignatures.filter(subsig => subsig.signature !== undefined).length;}
async function main() { printHeader('KMD Multisig Program Signing Example');
const kmd = createKmdClient(); const algod = createAlgodClient(); let walletHandleToken = ''; const walletPassword = 'test-password';
try { // ========================================================================= // Step 1: Create a Test Wallet // ========================================================================= printStep(1, 'Creating a test wallet for multisig program signing');
const testWallet = await createTestWallet(kmd, walletPassword); walletHandleToken = testWallet.walletHandleToken;
printSuccess(`Test wallet created: ${testWallet.walletName}`); printInfo(`Wallet ID: ${testWallet.walletId}`);
// ========================================================================= // Step 2: Generate 3 Keys for Multisig Participants // ========================================================================= printStep(2, 'Generating 3 keys to use as multisig participants');
const participantAddresses: Address[] = []; const publicKeys: Uint8Array[] = []; const numParticipants = 3;
for (let i = 1; i <= numParticipants; i++) { const result = await kmd.generateKey({ walletHandleToken }); participantAddresses.push(result.address); publicKeys.push(result.address.publicKey); printInfo(`Participant ${i}: ${shortenAddress(result.address.toString())}`); }
printSuccess(`Generated ${numParticipants} participant keys`);
// ========================================================================= // Step 3: Create a 2-of-3 Multisig Account // ========================================================================= printStep(3, 'Creating a 2-of-3 multisig account');
const threshold = 2; // Minimum signatures required const multisigVersion = 1; // Multisig format version
const multisigResult = await kmd.importMultisig({ walletHandleToken, publicKeys, threshold, multisigVersion, });
const multisigAddress = multisigResult.address;
printSuccess('Multisig account created!'); printInfo(`Multisig Address: ${multisigAddress}`); printInfo(`Threshold: ${threshold}-of-${numParticipants}`); printInfo(''); printInfo('This multisig address will be used as the delegator for the logic signature.');
// ========================================================================= // Step 4: Create a Simple TEAL Program // ========================================================================= printStep(4, 'Creating a simple TEAL program');
// Load TEAL logic signature from shared artifacts // This program approves payment transactions up to 1 ALGO // In production, you'd have more sophisticated logic const tealSource = loadTealSource('delegated-payment-limit.teal');
printInfo('TEAL Program Source:'); printInfo(''); tealSource.split('\n').forEach(line => { printInfo(` ${line}`); }); printInfo(''); printInfo('This program approves payment transactions up to 1 ALGO.'); printInfo('When signed by a multisig account, it creates a "delegated multisig lsig".');
// ========================================================================= // Step 5: Compile the TEAL Program // ========================================================================= printStep(5, 'Compiling the TEAL program using algod tealCompile');
const compileResult = await algod.tealCompile(tealSource);
// Decode base64 result to Uint8Array const programBytes = new Uint8Array(Buffer.from(compileResult.result, 'base64'));
printSuccess('TEAL program compiled successfully!'); printInfo(''); printInfo('Compilation Result:'); printInfo(` Hash (program address): ${compileResult.hash}`); printInfo(` Compiled size: ${programBytes.length} bytes`); printInfo(` Compiled bytes: ${formatBytesForDisplay(programBytes)}`);
// ========================================================================= // Step 6: Sign with the First Participant (Partial Signature) // ========================================================================= printStep(6, 'Signing the program with the first participant');
printInfo(`First signer: ${shortenAddress(participantAddresses[0].toString())}`); printInfo('');
const firstSignResult = await kmd.signMultisigProgram({ walletHandleToken, address: multisigAddress, program: programBytes, publicKey: publicKeys[0], walletPassword, });
printSuccess('First signature obtained!'); printInfo(''); printInfo('SignProgramMultisigResponse fields:'); printInfo(` multisig: Uint8Array (${firstSignResult.multisig.length} bytes)`); printInfo('');
// Decode and display the partial multisig signature const partialKmdMultisig = decodeKmdMultisigResponse(firstSignResult.multisig); const sigCount1 = countSignatures(partialKmdMultisig);
printInfo('Partial Multisig Signature:'); printInfo(` version: ${partialKmdMultisig.version}`); printInfo(` threshold: ${partialKmdMultisig.threshold}`); printInfo(` subsigs: ${partialKmdMultisig.subsignatures.length} participants`); printInfo(` Signatures collected: ${sigCount1} of ${threshold} required`); printInfo(''); printInfo('Subsignature details:'); partialKmdMultisig.subsignatures.forEach((subsig, i) => { const hasSig = subsig.signature !== undefined; const status = hasSig ? '✓ SIGNED' : '○ pending'; const addr = encodeAddress(subsig.publicKey); printInfo(` ${i + 1}. ${shortenAddress(addr)} - ${status}`); });
printInfo(''); printInfo('Note: With only 1 signature, the multisig lsig is not yet valid.'); printInfo(` We need ${threshold} signatures (${threshold - sigCount1} more required).`);
// ========================================================================= // Step 7: Sign with the Second Participant (Complete the Multisig) // ========================================================================= printStep(7, 'Signing the program with the second participant');
printInfo(`Second signer: ${shortenAddress(participantAddresses[1].toString())}`); printInfo(''); printInfo('Passing the partial multisig from Step 6 to collect the second signature...'); printInfo('');
const secondSignResult = await kmd.signMultisigProgram({ walletHandleToken, address: multisigAddress, program: programBytes, publicKey: publicKeys[1], walletPassword, partialMultisig: partialKmdMultisig, // Pass the partial signature from first signer });
printSuccess('Second signature obtained!'); printInfo('');
// Decode and display the completed multisig signature const completedKmdMultisig = decodeKmdMultisigResponse(secondSignResult.multisig); const sigCount2 = countSignatures(completedKmdMultisig);
printInfo('Completed Multisig Signature:'); printInfo(` version: ${completedKmdMultisig.version}`); printInfo(` threshold: ${completedKmdMultisig.threshold}`); printInfo(` Signatures collected: ${sigCount2} of ${threshold} required`); printInfo(''); printInfo('Subsignature details:'); completedKmdMultisig.subsignatures.forEach((subsig, i) => { const hasSig = subsig.signature !== undefined; const status = hasSig ? '✓ SIGNED' : '○ pending'; const addr = encodeAddress(subsig.publicKey); printInfo(` ${i + 1}. ${shortenAddress(addr)} - ${status}`); });
printInfo(''); printSuccess(`Threshold met! ${sigCount2} >= ${threshold} signatures collected.`); printInfo('The delegated multisig logic signature is now fully authorized.');
// ========================================================================= // Step 8: Create a LogicSigAccount with the Multisig Signature // ========================================================================= printStep(8, 'Creating a LogicSigAccount with the multisig signature');
printInfo('A delegated multisig LogicSigAccount combines:'); printInfo(' 1. The compiled program (logic)'); printInfo(' 2. The multisig signature (delegation proof from multiple parties)'); printInfo(' 3. The multisig delegator address'); printInfo('');
// Get the program address (hash of "Program" + logic) const nonDelegatedLsig = new LogicSig(programBytes); const programAddress = nonDelegatedLsig.address();
// Create a LogicSigAccount for delegation const lsigAccount = new LogicSigAccount(programBytes, [], multisigAddress);
// Convert KMD MultisigSig to transact's MultisigSignature format const completedMultisig = kmdMultisigToTransactMultisig(completedKmdMultisig); lsigAccount.msig = completedMultisig;
printSuccess('LogicSigAccount created with multisig signature!'); printInfo(''); printInfo('LogicSigAccount properties:'); printInfo(` Program address: ${shortenAddress(programAddress.toString())}`); printInfo(` Delegator address: ${shortenAddress(multisigAddress.toString())} (multisig)`); printInfo(` Has msig: ${lsigAccount.msig !== undefined}`); printInfo(` Logic size: ${lsigAccount.logic.length} bytes`); printInfo('');
// Show the distinction between program address and delegator printInfo('Important distinction:'); printInfo(` - Program address: ${shortenAddress(programAddress.toString())}`); printInfo(' Hash of the logic - this is the "contract account" in non-delegated mode'); printInfo(''); printInfo(` - lsigAccount.addr: ${shortenAddress(lsigAccount.addr.toString())}`); printInfo(' This is the DELEGATOR - the multisig account authorizing the program'); printInfo(''); printInfo('When using this delegated multisig lsig, transactions will be authorized'); printInfo( `as if signed by the multisig account ${shortenAddress(multisigAddress.toString())}.`, );
// ========================================================================= // Step 9: Explain How Delegated Multisig Logic Signatures Work // ========================================================================= printStep(9, 'Understanding delegated multisig logic signatures');
printInfo('Delegated Multisig Logic Signatures combine two concepts:'); printInfo(''); printInfo('1. MULTISIG AUTHORIZATION:'); printInfo(' - Multiple parties (2-of-3 in this example) must approve'); printInfo(' - Each party signs the program bytes with their key'); printInfo(' - Signatures are collected via partialMultisig parameter'); printInfo(' - Once threshold is met, the authorization is complete'); printInfo(''); printInfo('2. DELEGATED LOGIC SIGNATURE:'); printInfo(' - The program defines the rules for transactions'); printInfo(' - The multisig signature authorizes the program'); printInfo(' - Anyone with the lsig can submit transactions (if program approves)'); printInfo(' - No further interaction from the multisig signers needed'); printInfo(''); printInfo('Key differences from regular multisig transactions:'); printInfo(' - Multisig Txn: Signers approve EACH transaction'); printInfo(' - Multisig Lsig: Signers approve the PROGRAM once, then'); printInfo(' the program approves transactions automatically'); printInfo(''); printInfo('Example workflow for using the delegated multisig lsig:'); printInfo(''); printInfo(' 1. Create a Transaction with sender = multisig address'); printInfo(''); printInfo(' 2. Use the LogicSigAccount.signer to sign:'); printInfo(''); printInfo(' const signedTxns = await lsigAccount.signer([txn], [0])'); printInfo(''); printInfo(' 3. The signed transaction includes:'); printInfo(' - lsig.logic: The compiled TEAL program'); printInfo(' - lsig.msig: The multisig delegation signature'); printInfo(''); printInfo(' 4. When submitted, the network validates:'); printInfo(' - The multisig signature is valid (threshold met)'); printInfo(' - The program logic approves the transaction');
// ========================================================================= // Cleanup // ========================================================================= printStep(10, 'Cleaning up test wallet');
await cleanupTestWallet(kmd, walletHandleToken); walletHandleToken = ''; // Mark as cleaned up
printSuccess('Test wallet handle released');
// ========================================================================= // Summary // ========================================================================= printHeader('Summary'); printInfo('This example demonstrated multisig program signing with KMD:'); printInfo(''); printInfo(' signMultisigProgram() - Sign a TEAL program with multisig'); printInfo(' Parameters:'); printInfo(' - walletHandleToken: The wallet session token'); printInfo(' - address: The multisig account address (delegator)'); printInfo(' - program: The compiled TEAL program bytes'); printInfo(' - publicKey: The public key of the signer (must be in wallet)'); printInfo(' - walletPassword: The wallet password'); printInfo(' - partialMultisig: (optional) Existing partial signature to add to'); printInfo(' Returns:'); printInfo(' - multisig: Uint8Array (msgpack-encoded MultisigSig)'); printInfo(''); printInfo('Multisig program signing workflow:'); printInfo(' 1. Create a multisig account with importMultisig()'); printInfo(' 2. Write a TEAL program with the desired authorization logic'); printInfo(' 3. Compile the program using algod.tealCompile()'); printInfo(' 4. Sign with first participant using signMultisigProgram()'); printInfo(' 5. Sign with additional participants, passing partialMultisig'); printInfo(' 6. Once threshold is met, create LogicSigAccount with msig'); printInfo(' 7. Use LogicSigAccount.signer to sign authorized transactions'); printInfo(''); printInfo('Key points:'); printInfo(' - Each participant signs the PROGRAM (not transactions)'); printInfo(' - The partialMultisig parameter chains signatures together'); printInfo(' - The response contains msgpack-encoded MultisigSignature'); printInfo(' - Unlike signMultisigTransaction, program signing is done ONCE'); printInfo(' - The resulting lsig can sign unlimited transactions (per program logic)'); printInfo(''); printInfo('Security considerations:'); printInfo(' - Write program logic carefully - it controls your multisig account!'); printInfo(' - Multiple parties review and sign the program code'); printInfo(' - Consider time bounds, amount limits, and recipient restrictions'); printInfo(' - The delegated lsig grants ongoing authorization until program expires'); printInfo(''); printInfo('Note: The test wallet remains in KMD (wallets cannot be deleted via API).'); } catch (error) { printError(`Error: ${error instanceof Error ? error.message : String(error)}`); printInfo(''); printInfo('Troubleshooting:'); printInfo(' - Ensure LocalNet is running: algokit localnet start'); printInfo(' - If LocalNet issues occur: algokit localnet reset'); printInfo(' - Check that KMD is accessible on port 4002'); printInfo(' - Check that Algod is accessible on port 4001');
// Cleanup on error if (walletHandleToken) { await cleanupTestWallet(kmd, walletHandleToken); }
process.exit(1); }}
main().catch(error => { console.error('Fatal error:', error); process.exit(1);});Other examples in KMD Client
Section titled “Other examples in KMD Client”- KMD Version Information
- Wallet Creation and Listing
- Wallet Session Management
- Key Generation
- Key Import and Export
- Key Listing and Deletion
- Master Key Export
- Multisig Account Setup
- Multisig Account Management
- Transaction Signing with KMD
- Multisig Transaction Signing with KMD
- Program Signing (Delegated Logic Signatures) with KMD
- Multisig Program Signing (Delegated Multisig Logic Signatures) with KMD