Documentation Index
Fetch the complete documentation index at: https://mintlify.com/satsigner/satsigner/llms.txt
Use this file to discover all available pages before exploring further.
SatSigner provides comprehensive multi-signature wallet support with industry-standard PSBT (Partially Signed Bitcoin Transaction) workflows for secure, collaborative transaction signing.
Multi-Signature Overview
What is Multisig?
Multi-signature wallets require multiple private keys to authorize a transaction, providing:
- Enhanced Security - No single point of failure
- Shared Custody - Distributed key management
- Governance - Multi-party approval process
- Recovery Options - Backup keys prevent loss
Signature Schemes
Multisig uses M-of-N configurations:
// M = signatures required
// N = total keys
// Common configurations:
1-of-2 // Single sig with backup
2-of-2 // Dual approval
2-of-3 // Standard multisig (most popular)
3-of-5 // Organization treasury
5-of-7 // Large organization
Recommended Configurations:
| Use Case | Configuration | Reasoning |
|---|
| Personal with backup | 1-of-2 | Simple recovery |
| Joint account | 2-of-2 | Requires both parties |
| Family savings | 2-of-3 | Flexible + secure |
| Company treasury | 3-of-5 | Distributed control |
| Large organization | 5-of-9 | Byzantine fault tolerance |
Creating Multisig Wallets
Account Builder Configuration
type MultisigAccount = {
name: string
network: Network
policyType: 'multisig'
// Multisig configuration
keyCount: number // Total keys (N)
keysRequired: number // Signatures needed (M)
scriptVersion: 'P2WSH' | 'P2SH-P2WSH' | 'P2SH'
// Individual keys
keys: Key[]
}
Setting Up 2-of-3 Multisig
Step 1: Configure Multisig Parameters
const multisigSetup = {
name: "Company Treasury",
network: "bitcoin",
policyType: "multisig",
keyCount: 3,
keysRequired: 2,
scriptVersion: "P2WSH" // Native SegWit multisig
}
Step 2: Add Keys
Each key can be added via different methods:
// Key 1: Generate new seed
const key1 = {
index: 0,
name: "CFO Key",
creationType: "generateMnemonic",
mnemonicWordCount: 24,
scriptVersion: "P2WSH"
}
// Key 2: Import from hardware wallet
const key2 = {
index: 1,
name: "CEO Hardware Wallet",
creationType: "importExtendedPub",
extendedPublicKey: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
fingerprint: "a1b2c3d4",
scriptVersion: "P2WSH"
}
// Key 3: Import extended public key
const key3 = {
index: 2,
name: "Backup Key",
creationType: "importExtendedPub",
extendedPublicKey: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",
fingerprint: "e5f6g7h8",
scriptVersion: "P2WSH"
}
Step 3: Generate Descriptor
SatSigner automatically creates the output descriptor:
// Multisig descriptor format
const descriptor = `wsh(sortedmulti(2,
[a1b2c3d4/48h/0h/0h/2h]xpub.../0/*,
[e5f6g7h8/48h/0h/0h/2h]xpub.../0/*,
[12345678/48h/0h/0h/2h]xpub.../0/*
))`
// Key features:
// - wsh() = P2WSH wrapped
// - sortedmulti() = Lexicographically sorted keys
// - 2 = Signatures required
// - [fingerprint/path] = Key identification
// - 0/* = External addresses
// - 1/* = Change addresses (internal descriptor)
Script Types for Multisig
P2WSH (Recommended)
- Native SegWit multisig
- Lowest transaction fees
- Bech32 addresses (bc1…)
- Modern, efficient
P2SH-P2WSH
- Nested SegWit multisig
- Backward compatible
- Addresses start with 3…
- Moderate fees
P2SH (Legacy)
- Original multisig
- Addresses start with 3…
- Highest fees
- Maximum compatibility
Key Management Requirements
Critical Rules:
- Unique Fingerprints - Each key must be from different seed
// Validation check
const fingerprints = keys.map(k => k.fingerprint)
const uniqueFingerprints = new Set(fingerprints)
if (uniqueFingerprints.size !== fingerprints.length) {
throw new Error('Duplicate fingerprints detected')
}
- Unique Extended Public Keys - No key reuse
const xpubs = keys.map(k => k.extendedPublicKey)
const uniqueXpubs = new Set(xpubs)
if (uniqueXpubs.size !== xpubs.length) {
throw new Error('Duplicate extended public keys')
}
- Consistent Script Version - All keys same type
- Proper Derivation Paths - Follow BIP48 standard
PSBT Workflow
What is PSBT?
Partially Signed Bitcoin Transaction (BIP 174):
- Standard format for unsigned/partially signed transactions
- Contains all information needed for signing
- Enables coordination across multiple devices
- Privacy-preserving - no key material exchanged
PSBT Structure
type PSBT = {
// Global data
unsignedTx: Transaction,
version: number,
// Per-input data
inputs: {
witnessUtxo?: {
script: Buffer
value: number
},
witnessScript?: Buffer, // Multisig script
bip32Derivation?: {
pubkey: Buffer
fingerprint: Buffer
path: string
}[],
partialSig?: { // Signatures collected
pubkey: Buffer
signature: Buffer
}[]
}[],
// Per-output data
outputs: {
bip32Derivation?: {...}[]
}[]
}
Complete PSBT Flow
1. Create Transaction (Coordinator)
// Build transaction
const txBuilder = await new TxBuilder().create()
// Add UTXOs
await txBuilder.addUtxos(
selectedUtxos.map(u => ({ txid: u.txid, vout: u.vout }))
)
// Add recipients
for (const output of outputs) {
const scriptPubKey = await getScriptPubKeyFromAddress(
output.address,
network
)
await txBuilder.addRecipient(scriptPubKey, output.amount)
}
// Set fee and finish
await txBuilder.feeAbsolute(fee)
const txBuilderResult = await txBuilder.finish(wallet)
// Get unsigned PSBT
const unsignedPsbt = txBuilderResult.psbt.toBase64()
2. Share PSBT with Cosigners
Multiple sharing methods:
// QR Code (animated for large PSBTs)
exportPsbtAsQR(unsignedPsbt)
// File export
exportPsbtAsFile(unsignedPsbt, 'transaction.psbt')
// BBQr (Bitcoin Block Quick Response)
const bbqr = encodeBBQr(unsignedPsbt)
// Base64 text (small transactions)
shareAsText(unsignedPsbt)
3. Each Cosigner Signs
// Import PSBT
const psbtBase64 = importPsbtFromQR() // or file, clipboard, etc.
// Verify PSBT matches account
const match = await findMatchingAccount(psbtBase64, accounts)
if (!match) {
throw new Error('PSBT does not match any account')
}
// Extract transaction details
const txData = extractTransactionDataFromPSBT(
psbtBase64,
match.account
)
// Review transaction
console.log('Sending:', txData.outputs)
console.log('Fee:', txData.fee)
console.log('From account:', match.account.name)
// Sign with this cosigner's key
const signedPsbt = await signPSBTWithSeed(
psbtBase64,
mnemonic,
match.account.keys[0].scriptVersion
)
// Export signed PSBT
exportSignedPsbt(signedPsbt)
4. Combine Signatures
// Collect signed PSBTs from cosigners
const signedPsbts = [
signedPsbt1Base64, // From cosigner 1
signedPsbt2Base64, // From cosigner 2
signedPsbt3Base64 // From cosigner 3 (if applicable)
]
// Combine all signatures
const combinedPsbt = combinePsbts(signedPsbts)
// Verify threshold met
const validation = getSignedPSBTValidationInfo(combinedPsbt)
const signaturesCount = validation.signatures.length
if (signaturesCount >= account.keysRequired) {
console.log('✓ Threshold met:', signaturesCount, 'of', account.keysRequired)
} else {
console.log('Need more signatures:', signaturesCount, '/', account.keysRequired)
}
5. Finalize and Broadcast
// Extract final transaction
const psbt = Psbt.fromBase64(combinedPsbt)
const finalTx = psbt.extractTransaction()
// Broadcast to network
const txid = await blockchain.broadcast(finalTx)
console.log('Transaction broadcast:', txid)
PSBT Signing Process
Signing with Mnemonic
function signPSBTWithSeed(
psbtBase64: string,
seedWords: string,
scriptType: 'P2WSH' | 'P2SH' | 'P2SH-P2WSH'
): SigningResult {
const psbt = bitcoinjs.Psbt.fromBase64(psbtBase64)
// Derive key from mnemonic
const seed = bip39.mnemonicToSeedSync(seedWords.trim())
const root = bip32.fromSeed(seed)
const fingerprint = root.fingerprint.toString('hex')
// Find matching derivations in PSBT
const derivations = extractPSBTDerivations(psbtBase64)
const matchingDerivations = derivations.filter(
d => d.fingerprint === fingerprint
)
if (matchingDerivations.length === 0) {
throw new Error('No matching derivation for this seed')
}
// Sign each matching input
let signedInputs = 0
for (const derivation of matchingDerivations) {
const derivedKey = root.derivePath(derivation.path)
// Verify public key matches
if (derivedKey.publicKey.toString('hex') !== derivation.pubkey) {
continue
}
// Create signer
const signer = {
publicKey: derivedKey.publicKey,
sign: (hash: Buffer) => {
return ecc.sign(hash, derivedKey.privateKey)
}
}
// Sign input
try {
psbt.signInput(derivation.inputIndex, signer)
signedInputs++
} catch (error) {
console.error('Failed to sign input:', error)
}
}
return {
success: signedInputs > 0,
signedPSBT: psbt.toBase64(),
signedInputsCount: signedInputs
}
}
Signature Verification
function getSignedPSBTValidationInfo(signedPsbt: string) {
const psbt = bitcoinjs.Psbt.fromBase64(signedPsbt)
const validation = {
isValid: true,
errors: [],
warnings: [],
signatures: [],
inputs: []
}
// Check each input for signatures
psbt.data.inputs.forEach((input, inputIndex) => {
const inputInfo = {
index: inputIndex,
hasPartialSigs: false,
partialSigs: [],
hasWitnessUtxo: !!input.witnessUtxo,
hasWitnessScript: !!input.witnessScript
}
// Collect signatures
if (input.partialSig && input.partialSig.length > 0) {
inputInfo.hasPartialSigs = true
input.partialSig.forEach(sig => {
inputInfo.partialSigs.push({
pubkey: sig.pubkey.toString('hex'),
signature: sig.signature.toString('hex')
})
validation.signatures.push({
inputIndex,
pubkey: sig.pubkey.toString('hex'),
signature: sig.signature.toString('hex')
})
})
} else {
validation.warnings.push(
`Input ${inputIndex} has no signatures`
)
}
validation.inputs.push(inputInfo)
})
return validation
}
Account Matching
Finding Matching Account
async function findMatchingAccount(
psbtBase64: string,
accounts: Account[]
): Promise<AccountMatchResult | null> {
// Extract derivations from PSBT
const derivations = extractPSBTDerivations(psbtBase64)
if (derivations.length === 0) return null
const psbtFingerprints = [...new Set(
derivations.map(d => d.fingerprint)
)]
// Check each account
for (const account of accounts) {
if (account.keys.length === 0) continue
const accountFingerprints = []
const fingerprintToKeyIndex = new Map()
// Extract account key fingerprints
for (let i = 0; i < account.keys.length; i++) {
const key = account.keys[i]
const fingerprint = await extractKeyFingerprint(key)
if (fingerprint) {
accountFingerprints.push(fingerprint)
fingerprintToKeyIndex.set(fingerprint, i)
}
}
// Check if all PSBT fingerprints match account
const allMatch = psbtFingerprints.every(fp =>
accountFingerprints.includes(fp)
)
if (allMatch) {
const firstMatch = derivations.find(d =>
accountFingerprints.includes(d.fingerprint)
)
return {
account,
cosignerIndex: fingerprintToKeyIndex.get(
firstMatch.fingerprint
),
fingerprint: firstMatch.fingerprint,
derivationPath: firstMatch.derivationPath,
publicKey: firstMatch.publicKey
}
}
}
return null
}
Multi-Device Coordination
Coordinator Role
The coordinator device:
- Creates the wallet configuration
- Builds transactions
- Collects signatures from cosigners
- Combines and broadcasts final transaction
Cosigner Role
Each cosigner device:
- Imports wallet configuration
- Receives unsigned PSBTs
- Reviews transaction details
- Signs with their key
- Returns signed PSBT
Configuration Export/Import
Export Configuration:
const walletConfig = {
name: account.name,
network: account.network,
policyType: 'multisig',
keyCount: account.keyCount,
keysRequired: account.keysRequired,
scriptVersion: account.keys[0].scriptVersion,
descriptor: account.keys[0].secret.externalDescriptor
}
// Share as QR code
exportAsQR(JSON.stringify(walletConfig))
// Or as file
exportAsFile(walletConfig, 'multisig-config.json')
Import Configuration:
// Import from QR or file
const config = importWalletConfig()
// Create account from config
const account = createAccountFromConfig(config)
// Add user's key to the configuration
account.keys[userKeyIndex] = {
...config.keys[userKeyIndex],
secret: userMnemonic, // Or imported key
iv: randomIv()
}
Security Considerations
Key Distribution
Best Practices:
- Geographic Distribution - Keys in different physical locations
- Device Diversity - Different hardware/software
- Key Holder Diversity - Different trusted parties
- Secure Communication - Encrypted channels for PSBT sharing
- Verification Process - Multiple people verify transactions
PSBT Security
Always Verify:
// Check PSBT before signing
const checks = [
// 1. Matches your account
verifyAccountMatch(psbt, account),
// 2. UTXOs are yours
verifyUtxosOwnership(psbt, account.utxos),
// 3. Recipient addresses correct
verifyRecipients(psbt, expectedRecipients),
// 4. Amounts correct
verifyAmounts(psbt, expectedAmounts),
// 5. Fee reasonable
verifyFeeRange(psbt, minFee, maxFee),
// 6. No unexpected inputs
verifyNoExtraInputs(psbt, expectedInputs)
]
if (!checks.every(c => c === true)) {
throw new Error('PSBT validation failed')
}
Attack Vectors
Address Substitution:
- Attacker modifies recipient address
- Mitigation: Always verify addresses on multiple devices
Amount Manipulation:
- Attacker changes payment amounts
- Mitigation: Review all amounts before signing
Fee Attack:
- Excessive fees to drain funds
- Mitigation: Set maximum acceptable fee rate
PSBT Tampering:
- Modified PSBT between signers
- Mitigation: Verify original PSBT hash before signing
Advanced Features
Threshold Signatures
Optimize for different scenarios:
// Hot wallet with cold backup
1-of-2: Daily use key + Offline backup
// Shared custody
2-of-2: Both parties must approve
// Flexible with backup
2-of-3: Any 2 keys (protects against loss)
// Corporate governance
3-of-5: Requires majority approval
Descriptor Backup
Critical Information:
const backupData = {
// Wallet identification
name: account.name,
network: account.network,
created: account.createdAt,
// Policy
policyType: 'multisig',
keysRequired: 2,
keyCount: 3,
// Descriptor (most important!)
descriptor: account.keys[0].secret.externalDescriptor,
// Your key information
yourKeyIndex: 0,
yourFingerprint: account.keys[0].fingerprint,
// Recovery instructions
notes: "Key 1 stored in safe deposit box"
}
// Store securely
// - Encrypted cloud backup
// - Physical paper backup
// - Hardware wallet storage
Recovery Process:
- Gather descriptor and threshold keys
- Import descriptor into compatible wallet
- Restore each key from backup
- Reconstruct multisig wallet
- Sync with blockchain
Watch-Only Multisig
Monitor multisig wallet without signing ability:
// Import with descriptor only (no keys)
const watchOnlyMultisig = {
name: "Company Treasury (Watch-Only)",
policyType: "watchonly",
keys: [{
creationType: "importDescriptor",
externalDescriptor: descriptor,
fingerprint: undefined // No key access
}]
}
Use Cases:
- Accounting/audit
- Portfolio tracking
- Public transparency
- Coordinator node
Troubleshooting
PSBT Won’t Sign
Common Issues:
- Wrong account - PSBT fingerprint doesn’t match
- Incorrect script type - P2WSH vs P2SH mismatch
- Missing witness data - PSBT incomplete
- Already signed - This key already signed
Solutions:
// Verify fingerprint match
const psbtFps = extractPSBTDerivations(psbt)
.map(d => d.fingerprint)
const accountFps = account.keys.map(k => k.fingerprint)
const matches = psbtFps.filter(fp => accountFps.includes(fp))
console.log('Matching fingerprints:', matches)
Cannot Broadcast
Possible Causes:
- Insufficient signatures - Below threshold
- Invalid signatures - Signature verification failed
- Double spend - UTXOs already spent
- Fee too low - Below minimum relay fee
Check Before Broadcasting:
// Verify threshold
const sigCount = countSignatures(combinedPsbt)
if (sigCount < account.keysRequired) {
throw new Error(`Need ${account.keysRequired - sigCount} more signatures`)
}
// Verify signatures valid
const validation = validateSignedPSBT(combinedPsbt, account)
if (!validation) {
throw new Error('Invalid signatures detected')
}
Lost Cosigner Key
If a key is lost:
- Verify backup keys - Ensure you have threshold keys
- Sweep funds - Move to new multisig with remaining keys
- Create new wallet - Generate fresh multisig setup
- Update procedures - Improve key backup process
SatSigner’s multisig support provides enterprise-grade security with user-friendly PSBT workflows for safe, collaborative Bitcoin custody.