Skip to content

Fix(sdk-coin-xrp): Update XRP signers sorting to use numeric comparison #6130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions modules/sdk-coin-xrp/src/ripple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,22 @@
import * as rippleKeypairs from 'ripple-keypairs';
import * as xrpl from 'xrpl';
import { ECPair } from '@bitgo/secp256k1';
import BigNumber from 'bignumber.js';

import * as binary from 'ripple-binary-codec';

/**
* Convert an XRP address to a BigNumber for numeric comparison.
* This is needed for proper sorting of signers as required by the XRP protocol.
*
* @param address - The XRP address to convert
* @returns BigNumber representation of the address
*/
function addressToBigNumber(address: string): BigNumber {
const hex = Buffer.from(xrpl.decodeAccountID(address)).toString('hex');
return new BigNumber(hex, 16);
}

function computeSignature(tx, privateKey, signAs) {
const signingData = signAs ? binary.encodeForMultisigning(tx, signAs) : binary.encodeForSigning(tx);
return rippleKeypairs.sign(signingData, privateKey);
Expand Down Expand Up @@ -53,9 +66,17 @@ const signWithPrivateKey = function (txHex, privateKey, options) {
TxnSignature: computeSignature(tx, privateKey, options.signAs),
};
// Ordering of private key signing matters, or the Ripple fullnode will throw an 'Unsorted Signers array' error.
// Additional signers must be added to the front of the signers array list.
if (tx.TxnSignature || tx.Signers) {
tx.Signers.unshift({ Signer: signer });
// XRP requires Signers array to be sorted based on numeric value of signer addresses (lowest first)
if (tx.Signers) {
// Add the current signer
tx.Signers.push({ Signer: signer });

// Sort the Signers array by numeric value of Account (address) to ensure proper ordering
tx.Signers.sort((a, b) => {
const addressBN1 = addressToBigNumber(a.Signer.Account);
const addressBN2 = addressToBigNumber(b.Signer.Account);
return addressBN1.comparedTo(addressBN2);
});
Comment on lines +75 to +79
Copy link
Preview

Copilot AI May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comparator decodes and converts each address to a BigNumber on every comparison. For larger signer arrays this could be optimized by computing each numeric value once before sorting and reusing them.

Suggested change
tx.Signers.sort((a, b) => {
const addressBN1 = addressToBigNumber(a.Signer.Account);
const addressBN2 = addressToBigNumber(b.Signer.Account);
return addressBN1.comparedTo(addressBN2);
});
// Precompute BigNumber values for each signer address
tx.Signers = tx.Signers.map(signer => ({
...signer,
_addressBN: addressToBigNumber(signer.Signer.Account),
}));
// Sort the Signers array using the precomputed BigNumber values
tx.Signers.sort((a, b) => a._addressBN.comparedTo(b._addressBN));
// Remove the temporary _addressBN property after sorting
tx.Signers = tx.Signers.map(({ _addressBN, ...signer }) => signer);

Copilot uses AI. Check for mistakes.

} else {
tx.Signers = [{ Signer: signer }];
}
Expand Down
53 changes: 53 additions & 0 deletions modules/sdk-coin-xrp/test/unit/xrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import assert from 'assert';
import * as rippleBinaryCodec from 'ripple-binary-codec';
import sinon from 'sinon';
import * as testData from '../resources/xrp';
import { SIGNER_USER, SIGNER_BACKUP, SIGNER_BITGO } from '../resources/xrp';
import * as _ from 'lodash';
import { XrpToken } from '../../src';
import * as xrpl from 'xrpl';
Expand Down Expand Up @@ -224,6 +225,58 @@ describe('XRP:', function () {
coSignedHexTransaction.id.should.equal(coSignedJsonTransaction.id);
});

it('should correctly sort signers by numeric value of addresses', function () {
// Use the test signers from the test resources
// These are known valid key pairs where the private key corresponds to the address
const signers = [SIGNER_USER, SIGNER_BACKUP, SIGNER_BITGO];

// Unsigned transaction
const unsignedTxHex =
'120000228000000024000000072E00000000201B0018D07161400000000003DE2968400000000000002D8114726D0D8A26568D5D9680AC80577C912236717191831449EE221CCACC4DD2BF8862B22B0960A84FC771D9';

// Sign with first signer
let signedTx = ripple.signWithPrivateKey(unsignedTxHex, signers[0].prv, {
signAs: signers[0].address,
});

// Sign with second signer
signedTx = ripple.signWithPrivateKey(signedTx.signedTransaction, signers[1].prv, {
signAs: signers[1].address,
});

// Sign with third signer
signedTx = ripple.signWithPrivateKey(signedTx.signedTransaction, signers[2].prv, {
signAs: signers[2].address,
});

// Decode the signed transaction
const decodedTx = rippleBinaryCodec.decode(signedTx.signedTransaction);

// Verify that the Signers array exists and has the correct length
assert(Array.isArray(decodedTx.Signers));
(decodedTx.Signers as Array<any>).length.should.equal(3);

// Extract the addresses from the Signers array
const signerAddresses = (decodedTx.Signers as Array<any>).map((signer) => signer.Signer.Account);

// Convert addresses to BigNumber for numeric comparison
const addressToBigNumber = (address) => {
const hex = Buffer.from(xrpl.decodeAccountID(address)).toString('hex');
return BigInt('0x' + hex);
};

// Convert the addresses to BigNumber values
const signerValues = signerAddresses.map(addressToBigNumber);

// Verify that the Signers array is sorted in ascending order by numeric value
for (let i = 0; i < signerValues.length - 1; i++) {
assert(
signerValues[i] < signerValues[i + 1],
`Signers not properly sorted: ${signerValues[i]} should be less than ${signerValues[i + 1]}`
);
}
});

it('Should be unable to explain bogus XRP transaction', async function () {
await basecoin
.explainTransaction({ txHex: 'abcdefgH' })
Expand Down