Skip to content

Commit f1c06bc

Browse files
committed
feat(sdk-coin-sol): big endian support for verifyTransaction
TICKET: WP-6284
1 parent 8361510 commit f1c06bc

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
@@ -178,6 +178,41 @@ export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecover
178178
const HEX_REGEX = /^[0-9a-fA-F]+$/;
179179
const BLIND_SIGNING_TX_TYPES_TO_CHECK = { enabletoken: 'AssociatedTokenAccountInitialization' };
180180

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

@@ -371,8 +406,12 @@ export class Sol extends BaseCoin {
371406
const recipientFromTx = filteredOutputs[index]; // This address should be an ATA
372407

373408
// Compare the BigNumber values because amount is (string | number)
374-
const userAmount = new BigNumber(recipientFromUser.amount);
375-
const txAmount = new BigNumber(recipientFromTx.amount);
409+
// Apply s390x endianness fix if needed
410+
const userAmountStr = String(recipientFromUser.amount);
411+
const txAmountStr = getAmountBasedOnEndianness(recipientFromTx.amount);
412+
413+
const userAmount = new BigNumber(userAmountStr);
414+
const txAmount = new BigNumber(txAmountStr);
376415
if (!userAmount.isEqualTo(txAmount)) {
377416
return false;
378417
}
@@ -459,10 +498,13 @@ export class Sol extends BaseCoin {
459498
const explainedTxTotal: Record<string, BigNumber> = {};
460499

461500
for (const output of explainedTx.outputs) {
501+
// Apply s390x endianness fix to output amounts before summing
502+
const outputAmountStr = getAmountBasedOnEndianness(output.amount);
503+
462504
// total output amount based on each token
463505
const assetName = output.tokenName || this.getChain();
464506
const amount = explainedTxTotal[assetName] || new BigNumber(0);
465-
explainedTxTotal[assetName] = amount.plus(output.amount);
507+
explainedTxTotal[assetName] = amount.plus(outputAmountStr);
466508
}
467509

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