Multi-signature transactions allow multiple keys to sign the same transaction. This is useful for multi-sig accounts, escrow services, and collaborative operations.
Multi-Signature Basics
Hive accounts can have multiple keys with different weights for each authority level (owner, active, posting). A transaction requires enough signature weight to meet the threshold.
How Multi-Sig Works
- Authority Structure: Each account has owner, active, and posting authorities with configurable thresholds
- Key Weights: Each public key in an authority has a weight (default: 1)
- Threshold: Minimum total weight required for authorization
- Signatures: Transaction must collect enough signatures to meet or exceed threshold
For a standard account, the threshold is 1 and the single key has weight 1. Multi-sig accounts have thresholds greater than 1 or multiple keys.
Signing with Multiple Keys at Once
The simplest approach is to sign with all required keys in a single call:
import { Transaction, PrivateKey } from 'hive-tx'
// Create transaction
const tx = new Transaction()
await tx.addOperation('transfer', {
from: 'multisig-account',
to: 'alice',
amount: '10.000 HIVE',
memo: 'Multi-sig payment'
})
// Load multiple keys
const key1 = PrivateKey.from('5JdeC9P7Pbd1uGdFVEsJ41EkEnADbbHGq6p1BwFxm6txNBsQnsw')
const key2 = PrivateKey.from('5K...')
const key3 = PrivateKey.from('5H...')
// Sign with all keys at once
tx.sign([key1, key2, key3])
// Broadcast
const result = await tx.broadcast(true)
console.log('Transaction ID:', result.tx_id)
Pass an array of PrivateKey objects to .sign() to add multiple signatures at once.
Sequential Signing (Collecting Signatures)
For distributed multi-sig workflows, signatures can be collected sequentially:
Create the transaction
import { Transaction } from 'hive-tx'
const tx = new Transaction()
await tx.addOperation('transfer', {
from: 'multisig-account',
to: 'bob',
amount: '5.000 HIVE',
memo: 'Collaborative payment'
})
First signer signs
import { PrivateKey } from 'hive-tx'
const key1 = PrivateKey.from('5J...')
tx.sign(key1)
// Transaction now has 1 signature
console.log('Signatures:', tx.transaction?.signatures.length) // 1
Serialize and send to next signer
// Get the transaction object
const txData = tx.transaction
// Send to next signer (e.g., via API, file, etc.)
const serialized = JSON.stringify(txData)
Next signer adds their signature
// Receive transaction data
const txData = JSON.parse(serialized)
// Recreate transaction from data
const tx2 = new Transaction({ transaction: txData })
// Add second signature
const key2 = PrivateKey.from('5K...')
tx2.sign(key2)
console.log('Signatures:', tx2.transaction?.signatures.length) // 2
Broadcast when enough signatures collected
// After all required signatures are added
const result = await tx2.broadcast(true)
console.log('Multi-sig transaction confirmed:', result.tx_id)
Using addSignature Method
You can also add pre-computed signatures directly:
import { Transaction, PrivateKey } from 'hive-tx'
const tx = new Transaction()
await tx.addOperation('vote', {
voter: 'multisig-account',
author: 'alice',
permlink: 'post',
weight: 10000
})
// Sign externally and get signature string
const key = PrivateKey.from('5J...')
const { digest } = tx.digest()
const signature = key.sign(digest)
// Add signature directly (must be 130 character hex string)
tx.addSignature(signature.toString())
console.log('Signature added:', tx.transaction?.signatures.length)
The addSignature() method expects a 130-character hex signature string. Use this when working with external signing tools or hardware wallets.
Complete Multi-Sig Example
Here’s a complete example of a 2-of-3 multi-signature workflow:
import { Transaction, PrivateKey } from 'hive-tx'
// Scenario: 3 parties control a multi-sig account
// Threshold: 2 signatures required
class MultiSigCoordinator {
private tx?: Transaction
// Party 1: Initialize transaction
async initializeTransfer(from: string, to: string, amount: string) {
this.tx = new Transaction({ expiration: 300_000 }) // 5 min expiry
await this.tx.addOperation('transfer', {
from,
to,
amount,
memo: 'Multi-sig transfer'
})
return this.exportTransaction()
}
// Export for sharing with other signers
exportTransaction(): string {
if (!this.tx?.transaction) {
throw new Error('No transaction to export')
}
return JSON.stringify(this.tx.transaction)
}
// Import transaction from another party
importTransaction(txData: string) {
const parsed = JSON.parse(txData)
this.tx = new Transaction({ transaction: parsed })
}
// Sign with party's key
sign(privateKey: PrivateKey) {
if (!this.tx) {
throw new Error('No transaction loaded')
}
this.tx.sign(privateKey)
const sigCount = this.tx.transaction?.signatures.length || 0
console.log(`Signed. Total signatures: ${sigCount}`)
}
// Check if enough signatures
hasEnoughSignatures(required: number): boolean {
const count = this.tx?.transaction?.signatures.length || 0
return count >= required
}
// Broadcast when ready
async broadcast() {
if (!this.tx) {
throw new Error('No transaction to broadcast')
}
return await this.tx.broadcast(true)
}
}
// Usage:
// Party 1: Create and sign
const coordinator1 = new MultiSigCoordinator()
const txData1 = await coordinator1.initializeTransfer(
'multisig-account',
'alice',
'100.000 HIVE'
)
const party1Key = PrivateKey.from('5J...')
coordinator1.sign(party1Key)
// Send transaction to Party 2
const serialized = coordinator1.exportTransaction()
// Party 2: Import and sign
const coordinator2 = new MultiSigCoordinator()
coordinator2.importTransaction(serialized)
const party2Key = PrivateKey.from('5K...')
coordinator2.sign(party2Key)
// Check if enough signatures (2 required)
if (coordinator2.hasEnoughSignatures(2)) {
console.log('Threshold reached! Broadcasting...')
const result = await coordinator2.broadcast()
console.log('Success:', result.tx_id)
} else {
// Need Party 3's signature
const serialized2 = coordinator2.exportTransaction()
// Send to Party 3...
}
Multi-Sig Account Setup
To create a multi-sig account authority structure:
import { Transaction, PrivateKey, PublicKey } from 'hive-tx'
// Get public keys for the three parties
const pubkey1 = PublicKey.fromString('STM...')
const pubkey2 = PublicKey.fromString('STM...')
const pubkey3 = PublicKey.fromString('STM...')
// Update account with 2-of-3 multi-sig active authority
const tx = new Transaction()
await tx.addOperation('account_update', {
account: 'myaccount',
active: {
weight_threshold: 2, // Require 2 signatures
account_auths: [],
key_auths: [
[pubkey1, 1], // Key 1 has weight 1
[pubkey2, 1], // Key 2 has weight 1
[pubkey3, 1] // Key 3 has weight 1
]
},
memo_key: 'STM...', // unchanged
json_metadata: ''
})
// Sign with current owner key
const ownerKey = PrivateKey.from('5H...')
tx.sign(ownerKey)
await tx.broadcast(true)
console.log('Account updated to 2-of-3 multi-sig')
Always test multi-sig configurations on testnet first! Incorrect setup can lock you out of your account.
Weighted Multi-Sig
You can assign different weights to different keys:
import { Transaction } from 'hive-tx'
// Setup: CEO key has weight 3, CFO has weight 2, others have weight 1
// Threshold: 3 required
const tx = new Transaction()
await tx.addOperation('account_update', {
account: 'company-account',
active: {
weight_threshold: 3,
account_auths: [],
key_auths: [
['STM_CEO_PUBKEY', 3], // CEO can sign alone (3 >= 3)
['STM_CFO_PUBKEY', 2], // CFO + any other (2+1 >= 3)
['STM_MANAGER1_PUBKEY', 1], // Needs 3 managers or CFO+manager
['STM_MANAGER2_PUBKEY', 1],
['STM_MANAGER3_PUBKEY', 1]
]
},
memo_key: 'STM...',
json_metadata: ''
})
Transaction Expiration in Multi-Sig
For multi-sig workflows, consider longer expiration:
import { Transaction } from 'hive-tx'
// Give parties more time to sign
const tx = new Transaction({
expiration: 3_600_000 // 1 hour instead of default 60 seconds
})
await tx.addOperation('transfer', {
from: 'multisig-account',
to: 'alice',
amount: '50.000 HIVE',
memo: 'Pending multi-sig approval'
})
// First party signs
const key1 = PrivateKey.from('5J...')
tx.sign(key1)
// Serialize and send to others
const txData = JSON.stringify(tx.transaction)
// They have up to 1 hour to add signatures and broadcast
Maximum expiration is 24 hours (86,400,000 ms). Use longer expirations for multi-sig workflows that require coordination across time zones.
Verifying Signatures
Check how many signatures a transaction has:
import { Transaction } from 'hive-tx'
const tx = new Transaction({ transaction: txData })
const signatureCount = tx.transaction?.signatures.length || 0
console.log(`Transaction has ${signatureCount} signature(s)`)
if (signatureCount >= 2) {
console.log('Enough signatures for 2-of-3 multi-sig')
await tx.broadcast(true)
} else {
console.log(`Need ${2 - signatureCount} more signature(s)`)
}
Error Handling
import { Transaction, PrivateKey, RPCError } from 'hive-tx'
try {
const tx = new Transaction({ transaction: txData })
const key = PrivateKey.from('5J...')
tx.sign(key)
const result = await tx.broadcast(true)
console.log('Success:', result.tx_id)
} catch (error) {
if (error instanceof RPCError) {
if (error.message.includes('missing required')) {
console.error('Not enough signatures for required authority')
console.error('Add more signatures before broadcasting')
} else if (error.message.includes('expired')) {
console.error('Transaction expired - signatures took too long')
} else {
console.error('Blockchain error:', error.message)
}
} else {
console.error('Error:', error.message)
}
}
Common Multi-Sig Patterns
Escrow Service
// 3-party escrow: buyer, seller, arbitrator
// Threshold: 2 (any 2 can release funds)
const escrowAuthority = {
weight_threshold: 2,
account_auths: [],
key_auths: [
['STM_BUYER_KEY', 1],
['STM_SELLER_KEY', 1],
['STM_ARBITRATOR_KEY', 1]
]
}
Corporate Account
// Board approval: 3 of 5 directors required
const boardAuthority = {
weight_threshold: 3,
account_auths: [],
key_auths: [
['STM_DIRECTOR1_KEY', 1],
['STM_DIRECTOR2_KEY', 1],
['STM_DIRECTOR3_KEY', 1],
['STM_DIRECTOR4_KEY', 1],
['STM_DIRECTOR5_KEY', 1]
]
}
Recovery Account
// Cold storage + hot wallet
// Either cold storage alone OR 2 hot wallets
const recoveryAuthority = {
weight_threshold: 2,
account_auths: [],
key_auths: [
['STM_COLD_STORAGE_KEY', 2], // Can act alone
['STM_HOT_WALLET1_KEY', 1],
['STM_HOT_WALLET2_KEY', 1]
]
}
Next Steps