Skip to content
LogoLogo

Multisig with PST

Spend from a 2-of-3 multisig where each co-signer keeps their own key, using the PST format to pass the transaction around.

Flow

coordinator: createPst(tx, spentOutputs)  ──▶  signer A: signPst  ──┐
                                           ──▶  signer B: signPst  ──┴──▶ combinePst ──▶ finalizePst ──▶ sendTransaction
  1. Address. multisigLock(2, [pkA, pkB, pkC]) gives a MULTISIG_HASH lock and an rarrk1z… address. Keys may mix ML-DSA-65 and ML-DSA-87.
  2. Create. The coordinator builds the unsigned transaction and wraps it with the outputs it spends and the multisig policy as redeem data.
  3. Sign. Each co-signer decodes the PST, checks the amounts leaving, the change and the fee (all trustworthy from the PST alone), and signs. Signing clears the flags, so the transaction can no longer change shape.
  4. Combine and finalize. The combiner verifies and merges signatures; the finalizer builds the witness [policy, slots…] from the first 2 signatures in key order and returns the signed transaction.

PSTs travel as base64 text, so they work over any channel, including air-gapped devices.

/**
 * 2-of-3 multisig with a PST: fund a shared vault, then two co-signers sign independently.
 *
 *   ARREKTA_URL=http://127.0.0.1:28332 ARREKTA_ADMIN_TOKEN=... npx tsx examples/multisig-pst.ts
 */
import {
  addPstOrigins,
  addPstRedeem,
  combinePst,
  createClient,
  createMultisigPolicy,
  createPst,
  createTransaction,
  encodeAddress,
  equalLocks,
  finalizePst,
  fromHex,
  generateSeed,
  getChain,
  getCoins,
  isPstComplete,
  keychainFromSeed,
  mine,
  multisigLock,
  pstFromBase64,
  pstToBase64,
  sendPayment,
  sendTransaction,
  signPstWithSeed,
  txid,
} from 'arrekta-sdk';
 
const URL = process.env.ARREKTA_URL ?? 'http://127.0.0.1:28332';
const ADMIN = process.env.ARREKTA_ADMIN_TOKEN;
const SEED = fromHex(process.env.ARREKTA_SEED ?? 'a1'.repeat(32));
 
// 1. Connect; a funder and three co-signers.
const client = createClient({ url: URL, adminToken: ADMIN });
const { network } = await getChain(client);
const funder = keychainFromSeed(SEED, { network });
const seeds = [generateSeed(), generateSeed(), generateSeed()];
const cosigners = seeds.map((s) => keychainFromSeed(s, { network }));
if ((await getCoins(client, { wallet: funder })).length === 0) await mine(client, funder.address(0), 101);
 
// 2. The 2-of-3 policy and its address.
const policy = createMultisigPolicy(2, cosigners.map((kc) => kc.key(0)));
const vaultLock = multisigLock(policy);
const vault = encodeAddress(vaultLock, network);
console.log(`vault ${vault}`);
 
// 3. Fund the vault with 4 ARRK and confirm.
const fund = await sendPayment(client, { wallet: funder, to: vault, amount: '4' });
await mine(client, funder.address(0), 1);
const vout = fund.tx.outputs.findIndex((o) => equalLocks(o.lock, vaultLock));
const coin = fund.tx.outputs[vout]!;
 
// 4. The coordinator creates a PST paying 3.9998 ARRK back to the funder.
const tx = createTransaction({
  inputs: [{ prevout: { txid: txid(fund.tx), vout }, sequence: 0xffffffff }],
  outputs: [{ asset: coin.asset, amount: coin.amount - 20_000n, lock: funder.lock(1) }],
});
const unsigned = addPstOrigins(addPstRedeem(createPst(tx, [coin], network), [policy]), cosigners.map((kc) => kc.origin(0)));
const shared = pstToBase64(unsigned); // base64, sent to each co-signer
 
// 5. Co-signers 1 and 3 sign independently.
const signedA = pstToBase64(signPstWithSeed(pstFromBase64(shared), seeds[0]!));
const signedC = pstToBase64(signPstWithSeed(pstFromBase64(shared), seeds[2]!));
 
// 6. Combine, finalize and broadcast.
const combined = combinePst(pstFromBase64(signedA), pstFromBase64(signedC));
console.log('complete:', isPstComplete(combined));
const final = finalizePst(combined);
console.log('sent', (await sendTransaction(client, { tx: final })).txid);
await mine(client, funder.address(0), 1);

Source: sdk/typescript/examples/multisig-pst.ts (updated 2026-11-02) · sdk/python/examples/multisig_pst.py (updated 2026-11-02). Runnable against a regtest node; CI runs every example.

Production

The wallet service runs this flow for teams: co-signers register keys in signed batches, a policy engine (limits, allowlists, approvals) gates every transfer, and the service never holds private keys.

Decoder errors (pst-redeem-mismatch, pst-flags, pst-foreign-key, …) are listed in the error code reference.