Skip to main content
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

  1. Authority Structure: Each account has owner, active, and posting authorities with configurable thresholds
  2. Key Weights: Each public key in an authority has a weight (default: 1)
  3. Threshold: Minimum total weight required for authorization
  4. 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:
1

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'
})
2

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
3

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)
4

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
5

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

Build docs developers (and LLMs) love