Multi-Signing - Shared Control | XRPL Development 101 | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
intermediate55 min

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 2

You set up a 2-of-3 multi-sig and disable the master key. One signer loses their key permanently. What can you do?

  • 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

1

Quorum is the threshold

: Total weight needed to authorize. Plan combinations carefully.

2

Signers are sorted alphabetically

: When combining signatures, order by address. XRPL requires this.

3

Multi-sig fees are higher

: Fee = base fee × (1 + number of signers). Budget accordingly.

4

Disable master key carefully

: Only after verifying signer list works. Can be re-enabled via multi-sig.

5

Test before production

: Always verify your multi-sig setup works before putting real value at risk. ---