Multi-Signing - Shared Control
Learning Objectives
Configure signer lists with weighted signers and quorum requirements
Build multi-signed transactions by collecting and combining signatures
Implement common patterns like 2-of-3 and weighted voting
Manage signer list lifecycle including updates and removal
Understand security trade-offs of different multi-sig configurations
Single-key accounts have a single point of failure: lose the key, lose the funds. Compromise the key, lose the funds. No oversight, no approval workflows.
Multi-signing solves this:
Traditional single-key:
Private Key → Signs Transaction → Executed
Multi-signing:
Signer A signs →
Signer B signs → Quorum reached → Executed
Signer C signs (optional) →
- Corporate treasury (CFO + CEO approval required)
- Secure cold storage (distribute keys geographically)
- Escrow services (2-of-3 between buyer, seller, arbiter)
- DAOs and governance (weighted voting)
- Who can sign (signer addresses)
- How much each signature counts (weights)
- How much weight is needed (quorum)
// SignerList structure
{
SignerQuorum: 3, // Total weight needed to authorize
SignerEntries: [
{ Account: "rAlice...", SignerWeight: 2 }, // Alice's sig = 2 weight
{ Account: "rBob...", SignerWeight: 1 }, // Bob's sig = 1 weight
{ Account: "rCarol...", SignerWeight: 1 } // Carol's sig = 1 weight
]
}
// Who can authorize?
// - Alice alone (weight 2) - NO (need 3)
// - Alice + Bob (weight 3) - YES
// - Alice + Carol (weight 3) - YES
// - Bob + Carol (weight 2) - NO (need 3)
// - All three (weight 4) - YES
```
// 2-of-3 (equal weights)
const twoOfThree = {
SignerQuorum: 2,
SignerEntries: [
{ Account: "rSigner1...", SignerWeight: 1 },
{ Account: "rSigner2...", SignerWeight: 1 },
{ Account: "rSigner3...", SignerWeight: 1 }
]
};
// 2-of-2 (both required)
const twoOfTwo = {
SignerQuorum: 2,
SignerEntries: [
{ Account: "rSigner1...", SignerWeight: 1 },
{ Account: "rSigner2...", SignerWeight: 1 }
]
};
// Weighted: CEO veto power
const ceoVeto = {
SignerQuorum: 3,
SignerEntries: [
{ Account: "rCEO...", SignerWeight: 3 }, // CEO alone can authorize
{ Account: "rCFO...", SignerWeight: 2 }, // CFO + CTO together can authorize
{ Account: "rCTO...", SignerWeight: 2 } // Or CFO + CEO, CTO + CEO
]
};
// 3-of-5 for high security
const threeOfFive = {
SignerQuorum: 3,
SignerEntries: [
{ Account: "rKey1...", SignerWeight: 1 },
{ Account: "rKey2...", SignerWeight: 1 },
{ Account: "rKey3...", SignerWeight: 1 },
{ Account: "rKey4...", SignerWeight: 1 },
{ Account: "rKey5...", SignerWeight: 1 }
]
};
```
Each signer in the list increases the owner reserve:
// Reserve calculation
const baseReserve = 10; // XRP
const ownerReserve = 2; // XRP per owned object
// SignerList is one object + entries
// Reserve = (signerCount + 1) * ownerReserve
// A 3-signer list: (3 + 1) * 2 = 8 XRP additional reserve
function calculateSignerListReserve(signerCount) {
return (signerCount + 1) * 2; // XRP
}
// Examples:
// 2-of-2: 6 XRP reserve
// 2-of-3: 8 XRP reserve
// 3-of-5: 12 XRP reserve
// src/multisig/create-signer-list.js
const xrpl = require('xrpl');
async function createSignerList(
accountWallet, // Current single-sig wallet
signers, // Array of { address, weight }
quorum
) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
// Validate inputs
const totalWeight = signers.reduce((sum, s) => sum + s.weight, 0);
if (quorum > totalWeight) {
throw new Error(Quorum ${quorum} exceeds total weight ${totalWeight});
}
if (signers.length < 1 || signers.length > 32) {
throw new Error('Signer count must be 1-32');
}
// Build SignerListSet transaction
const signerListSet = {
TransactionType: 'SignerListSet',
Account: accountWallet.address,
SignerQuorum: quorum,
SignerEntries: signers.map(s => ({
SignerEntry: {
Account: s.address,
SignerWeight: s.weight
}
}))
};
console.log('Setting up multi-sig:');
console.log( Quorum: ${quorum});
console.log(' Signers:');
signers.forEach(s => console.log( ${s.address}: weight ${s.weight}));
const prepared = await client.autofill(signerListSet);
const signed = accountWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
if (result.result.meta.TransactionResult === 'tesSUCCESS') {
console.log('✓ Signer list created');
return { success: true, hash: signed.hash };
}
return {
success: false,
resultCode: result.result.meta.TransactionResult
};
} finally {
await client.disconnect();
}
}
// Example: Set up 2-of-3 multi-sig
async function setupTwoOfThree(accountWallet, signer1, signer2, signer3) {
return createSignerList(
accountWallet,
[
{ address: signer1, weight: 1 },
{ address: signer2, weight: 1 },
{ address: signer3, weight: 1 }
],
2 // Quorum of 2
);
}
module.exports = { createSignerList, setupTwoOfThree };
```
For true multi-sig security, disable the master key:
async function disableMasterKey(accountWallet) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
// WARNING: Ensure signer list is set up first!
// Disabling master key without signers = locked account
const accountSet = {
TransactionType: 'AccountSet',
Account: accountWallet.address,
SetFlag: xrpl.AccountSetAsfFlags.asfDisableMaster // 4
};
console.log('⚠️ Disabling master key - ensure signer list is active!');
const prepared = await client.autofill(accountSet);
const signed = accountWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
if (result.result.meta.TransactionResult === 'tesSUCCESS') {
console.log('✓ Master key disabled');
console.log('Account now requires multi-sig for all transactions');
}
return result;
} finally {
await client.disconnect();
}
}
// Re-enable master key (requires multi-sig transaction)
async function enableMasterKey(multisigAccountAddress, signerWallets, quorum) {
// This must be submitted as multi-signed transaction
const accountSet = {
TransactionType: 'AccountSet',
Account: multisigAccountAddress,
ClearFlag: xrpl.AccountSetAsfFlags.asfDisableMaster // 4
};
return submitMultiSigned(accountSet, signerWallets, quorum);
}
1. Build transaction (without signing)
2. Distribute to signers
3. Each signer signs independently
4. Collect signatures
5. Combine into multi-signed transaction
6. Submit to network// src/multisig/prepare-transaction.js
const xrpl = require('xrpl');
async function prepareForMultiSign(transaction, accountAddress) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
// Get account info for sequence
const accountInfo = await client.request({
command: 'account_info',
account: accountAddress
});
// Get current fee
const serverInfo = await client.request({
command: 'server_info'
});
// Multi-sig transactions need higher fees
// Base fee * (1 + number of signers)
const signerCount = await getSignerCount(client, accountAddress);
const baseFee = Number(serverInfo.result.info.validated_ledger.base_fee_xrp) * 1_000_000;
const multisigFee = Math.ceil(baseFee * (1 + signerCount));
// Prepare transaction
const prepared = {
...transaction,
Account: accountAddress,
Sequence: accountInfo.result.account_data.Sequence,
Fee: multisigFee.toString(),
SigningPubKey: '' // Empty for multi-sig
};
// Get LastLedgerSequence
const currentLedger = serverInfo.result.info.validated_ledger.seq;
prepared.LastLedgerSequence = currentLedger + 20; // ~1 minute
return prepared;
} finally {
await client.disconnect();
}
}
async function getSignerCount(client, accountAddress) {
const accountObjects = await client.request({
command: 'account_objects',
account: accountAddress,
type: 'signer_list'
});
const signerList = accountObjects.result.account_objects[0];
return signerList ? signerList.SignerEntries.length : 0;
}
module.exports = { prepareForMultiSign };
```
// src/multisig/sign-as-signer.js
const xrpl = require('xrpl');
function signForMultiSig(preparedTransaction, signerWallet) {
// Create a copy to avoid modifying original
const txToSign = { ...preparedTransaction };
// Sign the transaction
const signature = signerWallet.sign(txToSign, true); // true = multisign
return {
signerAddress: signerWallet.address,
signature: signature.tx_blob,
txnSignature: extractTxnSignature(signature.tx_blob),
signingPubKey: signerWallet.publicKey
};
}
function extractTxnSignature(signedBlob) {
// In practice, use xrpl.js decode to extract TxnSignature
const decoded = xrpl.decode(signedBlob);
return decoded.TxnSignature;
}
// Alternative using xrpl.js authorizeChannel-style signing
function signTransaction(preparedTx, signerWallet) {
// xrpl.js provides multisign method
return signerWallet.sign(preparedTx, true); // multisign = true
}
module.exports = { signForMultiSig };
```
// src/multisig/combine-signatures.js
const xrpl = require('xrpl');
function combineSignatures(preparedTransaction, signerSignatures) {
// Sort signers by address (XRPL requirement)
const sortedSigners = signerSignatures.sort((a, b) =>
a.signerAddress.localeCompare(b.signerAddress)
);
// Build Signers array
const signersArray = sortedSigners.map(sig => ({
Signer: {
Account: sig.signerAddress,
TxnSignature: sig.txnSignature,
SigningPubKey: sig.signingPubKey
}
}));
// Create multi-signed transaction
const multiSigned = {
...preparedTransaction,
Signers: signersArray
};
return xrpl.encode(multiSigned);
}
// Complete multi-sign workflow
async function submitMultiSigned(transaction, signerWallets) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
// 1. Prepare transaction
const prepared = await prepareForMultiSign(transaction, transaction.Account);
// 2. Collect signatures from each signer
const signatures = signerWallets.map(wallet =>
signForMultiSig(prepared, wallet)
);
// 3. Combine signatures
const multiSignedBlob = combineSignatures(prepared, signatures);
// 4. Submit
const result = await client.submitAndWait(multiSignedBlob);
return {
success: result.result.meta.TransactionResult === 'tesSUCCESS',
hash: result.result.hash,
resultCode: result.result.meta.TransactionResult
};
} finally {
await client.disconnect();
}
}
module.exports = { combineSignatures, submitMultiSigned };
```
// src/multisig/complete-example.js
const xrpl = require('xrpl');
async function multiSigDemo() {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
console.log('=== Multi-Sig Demo ===\n');
// 1. Create main account and signers
const mainAccount = xrpl.Wallet.generate();
const signer1 = xrpl.Wallet.generate();
const signer2 = xrpl.Wallet.generate();
const signer3 = xrpl.Wallet.generate();
const recipient = xrpl.Wallet.generate();
// Fund accounts
console.log('Funding accounts...');
await client.fundWallet(mainAccount);
await client.fundWallet(signer1);
await client.fundWallet(signer2);
await client.fundWallet(signer3);
await client.fundWallet(recipient);
console.log('Main account:', mainAccount.address);
console.log('Signer 1:', signer1.address);
console.log('Signer 2:', signer2.address);
console.log('Signer 3:', signer3.address);
// 2. Set up 2-of-3 multi-sig
console.log('\nSetting up 2-of-3 multi-sig...');
const signerListSet = {
TransactionType: 'SignerListSet',
Account: mainAccount.address,
SignerQuorum: 2,
SignerEntries: [
{ SignerEntry: { Account: signer1.address, SignerWeight: 1 } },
{ SignerEntry: { Account: signer2.address, SignerWeight: 1 } },
{ SignerEntry: { Account: signer3.address, SignerWeight: 1 } }
]
};
const prepared1 = await client.autofill(signerListSet);
const signed1 = mainAccount.sign(prepared1);
await client.submitAndWait(signed1.tx_blob);
console.log('✓ Signer list created');
// 3. Optional: Disable master key
// (Skipped for demo - would lock account if signers unavailable)
// 4. Create a payment that requires multi-sig
console.log('\nCreating multi-signed payment...');
const payment = {
TransactionType: 'Payment',
Account: mainAccount.address,
Destination: recipient.address,
Amount: xrpl.xrpToDrops('10')
};
// 5. Prepare for multi-sign
const accountInfo = await client.request({
command: 'account_info',
account: mainAccount.address
});
const prepared2 = {
...payment,
Sequence: accountInfo.result.account_data.Sequence,
Fee: '36', // Base fee * (1 + 3 signers)
SigningPubKey: '',
LastLedgerSequence: accountInfo.result.ledger_index + 20
};
// 6. Collect signatures (signer1 and signer2)
console.log('Collecting signatures from signer1 and signer2...');
const sig1 = signer1.sign(prepared2, true);
const sig2 = signer2.sign(prepared2, true);
// 7. Decode to get signature components
const decoded1 = xrpl.decode(sig1.tx_blob);
const decoded2 = xrpl.decode(sig2.tx_blob);
// 8. Combine into multi-signed transaction
const multiSigned = {
...prepared2,
Signers: [
{
Signer: {
Account: signer1.address,
TxnSignature: decoded1.TxnSignature,
SigningPubKey: signer1.publicKey
}
},
{
Signer: {
Account: signer2.address,
TxnSignature: decoded2.TxnSignature,
SigningPubKey: signer2.publicKey
}
}
].sort((a, b) => a.Signer.Account.localeCompare(b.Signer.Account))
};
// 9. Submit multi-signed transaction
console.log('Submitting multi-signed transaction...');
const result = await client.submitAndWait(xrpl.encode(multiSigned));
console.log('Result:', result.result.meta.TransactionResult);
// 10. Verify payment
const recipientInfo = await client.request({
command: 'account_info',
account: recipient.address
});
console.log('\n✓ Multi-sig payment successful!');
console.log('Recipient balance:', xrpl.dropsToXrp(recipientInfo.result.account_data.Balance), 'XRP');
await client.disconnect();
}
multiSigDemo().catch(console.error);
```
// src/multisig/query-signers.js
const xrpl = require('xrpl');
async function getSignerList(accountAddress) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();
try {
const response = await client.request({
command: 'account_objects',
account: accountAddress,
type: 'signer_list'
});
if (response.result.account_objects.length === 0) {
return { hasSignerList: false };
}
const signerList = response.result.account_objects[0];
return {
hasSignerList: true,
quorum: signerList.SignerQuorum,
signers: signerList.SignerEntries.map(entry => ({
address: entry.SignerEntry.Account,
weight: entry.SignerEntry.SignerWeight
})),
reserveImpact: (signerList.SignerEntries.length + 1) * 2
};
} finally {
await client.disconnect();
}
}
async function displaySignerList(accountAddress) {
const info = await getSignerList(accountAddress);
console.log(\nSigner List for ${accountAddress}:);
if (!info.hasSignerList) {
console.log(' No signer list configured');
return;
}
console.log( Quorum: ${info.quorum});
console.log(' Signers:');
let totalWeight = 0;
for (const signer of info.signers) {
console.log( ${signer.address}: weight ${signer.weight});
totalWeight += signer.weight;
}
console.log( Total weight: ${totalWeight});
console.log( Reserve impact: ${info.reserveImpact} XRP);
}
module.exports = { getSignerList, displaySignerList };
```
async function updateSignerList(multisigAccount, newSigners, newQuorum, signerWallets) {
// Build SignerListSet with new configuration
const signerListSet = {
TransactionType: 'SignerListSet',
Account: multisigAccount,
SignerQuorum: newQuorum,
SignerEntries: newSigners.map(s => ({
SignerEntry: {
Account: s.address,
SignerWeight: s.weight
}
}))
};
// Submit as multi-signed transaction (requires current signers)
return submitMultiSigned(signerListSet, signerWallets);
}
// Remove signer list (convert back to single-sig)
async function removeSignerList(multisigAccount, signerWallets) {
const signerListSet = {
TransactionType: 'SignerListSet',
Account: multisigAccount,
SignerQuorum: 0,
SignerEntries: [] // Empty list removes it
};
return submitMultiSigned(signerListSet, signerWallets);
}
```
// Geographic distribution for cold storage
const secureCustodySetup = {
// 3-of-5 with keys in different locations
quorum: 3,
signers: [
{ address: 'rKey_NYC', weight: 1, location: 'New York vault' },
{ address: 'rKey_LON', weight: 1, location: 'London vault' },
{ address: 'rKey_SIN', weight: 1, location: 'Singapore vault' },
{ address: 'rKey_ZUR', weight: 1, location: 'Zurich vault' },
{ address: 'rKey_TKY', weight: 1, location: 'Tokyo vault' }
]
};
// Even if 2 locations are compromised, funds are safe
// Any 3 locations can authorize transactions
```
// Different roles with different weights
const corporateSetup = {
quorum: 100,
signers: [
// Board can act alone (3 of 5 board = 60, need CFO or CEO)
{ address: 'rBoard1', weight: 20, role: 'Board Member 1' },
{ address: 'rBoard2', weight: 20, role: 'Board Member 2' },
{ address: 'rBoard3', weight: 20, role: 'Board Member 3' },
{ address: 'rBoard4', weight: 20, role: 'Board Member 4' },
{ address: 'rBoard5', weight: 20, role: 'Board Member 5' },
// Executive team
{ address: 'rCEO', weight: 50, role: 'CEO' },
{ address: 'rCFO', weight: 40, role: 'CFO' },
{ address: 'rCTO', weight: 30, role: 'CTO' }
]
};
// Possible authorization combinations:
// - CEO + CFO (90... need 10 more) + any board member (20) = 110 ✓
// - CEO (50) + 3 board members (60) = 110 ✓
// - CFO + CTO (70) + 2 board members (40) = 110 ✓
```
Multi-signing is a powerful security tool when properly implemented. The complexity is in operations—coordinating signers, managing keys across locations, handling unavailability. Technical implementation is straightforward; organizational implementation requires careful planning.
Assignment: Build a complete multi-signature wallet system.
Requirements:
Create signer lists with configurable weights/quorum
Query and display current signer configuration
Update signer list (add/remove/change weights)
Calculate and display reserve impact
Prepare transactions for multi-signing
Calculate appropriate fees
Generate signable transaction representations
Sign transactions as individual signers
Validate signatures
Track which signers have signed
Combine signatures when quorum reached
Submit multi-signed transactions
Handle submission errors
Verify transaction success
Signer list management works correctly (25%)
Transaction preparation accurate (25%)
Signature collection handles all cases (25%)
Submission and verification complete (25%)
Time investment: 4 hours
Value: Production-ready multi-sig infrastructure for secure account management
Knowledge Check
Question 1 of 2You set up a 2-of-3 multi-sig and disable the master key. One signer loses their key permanently. What can you do?
- Multi-signing: https://xrpl.org/multi-signing.html
- SignerListSet: https://xrpl.org/signerlistset.html
- Multi-sign tutorial: https://xrpl.org/send-a-multi-signed-transaction.html
- Key management best practices
- HSM integration patterns
- Custody solutions
For Next Lesson:
You now understand multi-signing for secure account control. Lesson 13 covers real-time data with WebSocket subscriptions—essential for building responsive applications.
End of Lesson 12
Total words: ~4,900
Estimated completion time: 55 minutes reading + 4 hours for deliverable
Key Takeaways
Quorum is the threshold
: Total weight needed to authorize. Plan combinations carefully.
Signers are sorted alphabetically
: When combining signatures, order by address. XRPL requires this.
Multi-sig fees are higher
: Fee = base fee × (1 + number of signers). Budget accordingly.
Disable master key carefully
: Only after verifying signer list works. Can be re-enabled via multi-sig.
Test before production
: Always verify your multi-sig setup works before putting real value at risk. ---