Skip to content

Commit 5865baf

Browse files
authored
Merge pull request #7265 from BitGo/WP-6284-solana-bigendian-fix
feat(sdk-coin-sol): big endian support for verifyTransaction
2 parents ce8849d + f1c06bc commit 5865baf

File tree

2 files changed

+133
-4
lines changed

2 files changed

+133
-4
lines changed

modules/sdk-coin-sol/src/sol.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,41 @@ export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecover
179179
const HEX_REGEX = /^[0-9a-fA-F]+$/;
180180
const BLIND_SIGNING_TX_TYPES_TO_CHECK = { enabletoken: 'AssociatedTokenAccountInitialization' };
181181

182+
/**
183+
* Get amount string corrected for architecture-specific endianness issues.
184+
*
185+
* On s390x (big-endian) architecture, the Solana transaction parser (via @solana/web3.js)
186+
* incorrectly reads little-endian u64 amounts as big-endian, resulting in corrupted values.
187+
*
188+
* This function corrects all amounts on s390x by swapping byte order to undo
189+
* the incorrect byte order that happened during transaction parsing.
190+
*
191+
* @param amount - The amount to check and potentially fix
192+
* @returns The corrected amount as a string
193+
*/
194+
export function getAmountBasedOnEndianness(amount: string | number): string {
195+
const amountStr = String(amount);
196+
197+
// Only s390x architecture has this endianness issue
198+
const isS390x = process.arch === 's390x';
199+
if (!isS390x) {
200+
return amountStr;
201+
}
202+
203+
try {
204+
const amountBN = BigInt(amountStr);
205+
// On s390x, the parser ALWAYS reads u64 as big-endian when it's actually little-endian
206+
// So we ALWAYS need to swap bytes to get the correct value
207+
const buf = Buffer.alloc(8);
208+
buf.writeBigUInt64BE(amountBN, 0);
209+
const fixed = buf.readBigUInt64LE(0);
210+
return fixed.toString();
211+
} catch (e) {
212+
// If conversion fails, return original value
213+
return amountStr;
214+
}
215+
}
216+
182217
export class Sol extends BaseCoin {
183218
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
184219

@@ -449,8 +484,12 @@ export class Sol extends BaseCoin {
449484
const recipientFromTx = filteredOutputs[index]; // This address should be an ATA
450485

451486
// Compare the BigNumber values because amount is (string | number)
452-
const userAmount = new BigNumber(recipientFromUser.amount);
453-
const txAmount = new BigNumber(recipientFromTx.amount);
487+
// Apply s390x endianness fix if needed
488+
const userAmountStr = String(recipientFromUser.amount);
489+
const txAmountStr = getAmountBasedOnEndianness(recipientFromTx.amount);
490+
491+
const userAmount = new BigNumber(userAmountStr);
492+
const txAmount = new BigNumber(txAmountStr);
454493
if (!userAmount.isEqualTo(txAmount)) {
455494
return false;
456495
}
@@ -537,10 +576,13 @@ export class Sol extends BaseCoin {
537576
const explainedTxTotal: Record<string, BigNumber> = {};
538577

539578
for (const output of explainedTx.outputs) {
579+
// Apply s390x endianness fix to output amounts before summing
580+
const outputAmountStr = getAmountBasedOnEndianness(output.amount);
581+
540582
// total output amount based on each token
541583
const assetName = output.tokenName || this.getChain();
542584
const amount = explainedTxTotal[assetName] || new BigNumber(0);
543-
explainedTxTotal[assetName] = amount.plus(output.amount);
585+
explainedTxTotal[assetName] = amount.plus(outputAmountStr);
544586
}
545587

546588
if (!_.isEqual(explainedTxTotal, totalAmount)) {

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '@bitgo/sdk-core';
2525
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
2626
import { coins } from '@bitgo/statics';
27-
import { KeyPair, Sol, SolVerifyTransactionOptions, Tsol } from '../../src';
27+
import { getAmountBasedOnEndianness, KeyPair, Sol, SolVerifyTransactionOptions, Tsol } from '../../src';
2828
import { Transaction } from '../../src/lib';
2929
import { AtaInit, InstructionParams, TokenTransfer } from '../../src/lib/iface';
3030
import { getAssociatedTokenAccountAddress } from '../../src/lib/utils';
@@ -601,6 +601,93 @@ describe('SOL:', function () {
601601
});
602602
});
603603

604+
describe('getAmountBasedOnEndianness', () => {
605+
let originalArch: string;
606+
607+
beforeEach(() => {
608+
originalArch = process.arch;
609+
});
610+
611+
afterEach(() => {
612+
Object.defineProperty(process, 'arch', {
613+
value: originalArch,
614+
writable: true,
615+
configurable: true,
616+
});
617+
});
618+
619+
it('should return amount unchanged on non-s390x architectures', function () {
620+
Object.defineProperty(process, 'arch', {
621+
value: 'x64',
622+
writable: true,
623+
configurable: true,
624+
});
625+
626+
getAmountBasedOnEndianness('300000').should.equal('300000');
627+
getAmountBasedOnEndianness('10000').should.equal('10000');
628+
getAmountBasedOnEndianness('504403158265495552').should.equal('504403158265495552');
629+
});
630+
631+
it('should byte-swap small amounts on s390x (small becomes huge)', function () {
632+
Object.defineProperty(process, 'arch', {
633+
value: 's390x',
634+
writable: true,
635+
configurable: true,
636+
});
637+
638+
// Small amount 10,000 (0x2710) swaps to 1,163,899,028,698,562,560
639+
getAmountBasedOnEndianness('10000').should.equal('1163899028698562560');
640+
});
641+
642+
it('should byte-swap large amounts on s390x (large becomes tiny)', function () {
643+
Object.defineProperty(process, 'arch', {
644+
value: 's390x',
645+
writable: true,
646+
configurable: true,
647+
});
648+
649+
// Large amount 504,403,158,265,495,552 (0x0700000000000000) swaps to 7
650+
getAmountBasedOnEndianness('504403158265495552').should.equal('7');
651+
});
652+
653+
it('should handle numeric input', function () {
654+
Object.defineProperty(process, 'arch', {
655+
value: 's390x',
656+
writable: true,
657+
configurable: true,
658+
});
659+
660+
// Should work with numbers, not just strings
661+
getAmountBasedOnEndianness(10000).should.equal('1163899028698562560');
662+
});
663+
664+
it('should handle standard transaction amounts on s390x', function () {
665+
Object.defineProperty(process, 'arch', {
666+
value: 's390x',
667+
writable: true,
668+
configurable: true,
669+
});
670+
671+
// Standard amount 300,000 (0x493E0) swaps to large value
672+
const result = getAmountBasedOnEndianness('300000');
673+
// Verify it's different (swapped)
674+
result.should.not.equal('300000');
675+
// Verify swapping back gives original
676+
getAmountBasedOnEndianness(result).should.equal('300000');
677+
});
678+
679+
it('should handle invalid input gracefully', function () {
680+
Object.defineProperty(process, 'arch', {
681+
value: 's390x',
682+
writable: true,
683+
configurable: true,
684+
});
685+
686+
// Invalid BigInt input should return original string
687+
getAmountBasedOnEndianness('not-a-number').should.equal('not-a-number');
688+
});
689+
});
690+
604691
it('should accept valid address', function () {
605692
goodAddresses.forEach((addr) => {
606693
basecoin.isValidAddress(addr).should.equal(true);

0 commit comments

Comments
 (0)