Skip to content

Commit af28659

Browse files
committed
feat: try big endian patch for solana verifyTransacion
Ticket: WP-6284
1 parent 4ba2921 commit af28659

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

S390X_SOLANA_FIX_README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# s390x Solana Endianness Fix
2+
3+
## Problem
4+
5+
On s390x (big-endian) architecture, the Solana SDK incorrectly parses transaction amounts in `explainTransaction()`.
6+
7+
**Example:**
8+
9+
- Expected amount: 500000 lamports (0.0005 SOL)
10+
- Parsed amount: 2351168177045504000 (garbage value)
11+
12+
## Root Cause
13+
14+
Solana stores transaction data (including amounts) in little-endian format in Buffers. On big-endian architectures like s390x, Node.js Buffer methods (`readBigUInt64LE`, `readUInt32LE`, etc.) do not correctly swap bytes when reading little-endian data, causing incorrect values to be read from the transaction buffers.
15+
16+
The issue occurs in:
17+
18+
- `@bitgo-beta/sdk-coin-sol/src/sol.ts:255` - `explainTransaction()` method
19+
- Specifically when reading `instruction.params.amount` from the transaction buffer
20+
21+
## Solution
22+
23+
The `s390x-solana-endianness-fix.js` file monkey-patches Node.js Buffer prototype methods to correctly handle little-endian reads on big-endian systems by manually swapping bytes.
24+
25+
## Integration
26+
27+
### Method 1: Load at Application Startup (Recommended)
28+
29+
Edit `bin/advanced-wallet-manager` to load the fix before any other modules:
30+
31+
```javascript
32+
#!/usr/bin/env node
33+
34+
// Load s390x fix FIRST, before any other imports
35+
require('../s390x-solana-endianness-fix');
36+
37+
process.on('unhandledRejection', (reason, promise) => {
38+
console.error('----- Unhandled Rejection at -----');
39+
console.error(promise);
40+
console.error('----- Reason -----');
41+
console.error(reason);
42+
});
43+
44+
const { init } = require('../dist/src/app');
45+
46+
if (require.main === module) {
47+
init().catch((err) => {
48+
console.log(`Fatal error: ${err.message}`);
49+
console.log(err.stack);
50+
});
51+
}
52+
```
53+
54+
### Method 2: Load via NODE_OPTIONS
55+
56+
Set the environment variable to preload the fix:
57+
58+
```bash
59+
NODE_OPTIONS="--require ./s390x-solana-endianness-fix.js" npm start
60+
```
61+
62+
### Method 3: Load in Docker
63+
64+
Add to your `Dockerfile`:
65+
66+
```dockerfile
67+
# Copy the fix file
68+
COPY s390x-solana-endianness-fix.js /app/
69+
70+
# Set NODE_OPTIONS to preload it
71+
ENV NODE_OPTIONS="--require /app/s390x-solana-endianness-fix.js"
72+
```
73+
74+
## Verification
75+
76+
When the fix is loaded successfully, you should see this console output:
77+
78+
```
79+
🔧 Applying s390x Solana endianness fix...
80+
✅ s390x Solana endianness fix applied successfully
81+
Patched methods: readBigUInt64LE, readBigInt64LE, readUInt32LE, readInt32LE, readUInt16LE, readInt16LE
82+
```
83+
84+
On non-s390x architectures, you'll see:
85+
86+
```
87+
ℹ️ s390x Solana endianness fix: Not needed on x64
88+
```
89+
90+
## Testing
91+
92+
After applying the fix, test with a Solana transaction to verify amounts are parsed correctly:
93+
94+
```bash
95+
# The amount should now be parsed correctly as 500000 instead of 2351168177045504000
96+
```
97+
98+
## Technical Details
99+
100+
The fix patches these Buffer methods:
101+
102+
- `readBigUInt64LE` - Used for Solana amounts (u64/lamports)
103+
- `readBigInt64LE` - Used for signed 64-bit values
104+
- `readUInt32LE` - Used for 32-bit values in transaction data
105+
- `readInt32LE` - Used for signed 32-bit values
106+
- `readUInt16LE` - Used for 16-bit values
107+
- `readInt16LE` - Used for signed 16-bit values
108+
109+
Each patched method manually reads bytes in little-endian order and constructs the correct value, effectively reversing the byte order that the big-endian system would naturally use.

bin/advanced-wallet-manager

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#!/usr/bin/env node
22

3+
// Load s390x Solana endianness fix FIRST, before any other imports
4+
// This patches Buffer methods to correctly handle little-endian data on big-endian architectures
5+
require('../s390x-solana-endianness-fix');
6+
37
// TODO: Remove this unhandledRejection hook once BG-49996 is implemented.
48
process.on('unhandledRejection', (reason, promise) => {
59
console.error('----- Unhandled Rejection at -----');

s390x-solana-endianness-fix.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Solana s390x Big-Endian Fix
3+
*
4+
* Problem:
5+
* On s390x (big-endian) architecture, the Solana SDK incorrectly parses transaction amounts.
6+
* When parsing Solana transactions in explainTransaction(), the amount field reads garbage values
7+
* instead of the correct lamport amounts (e.g., reads 2351168177045504000 instead of 500000).
8+
*
9+
* Root Cause:
10+
* Solana stores transaction data (including amounts) in little-endian format in Buffers.
11+
* On big-endian architectures like s390x, Node.js Buffer methods (readBigUInt64LE, readUInt32LE, etc.)
12+
* do not correctly swap bytes when reading little-endian data.
13+
*
14+
* Solution:
15+
* This patch monkey-patches Buffer.prototype methods to correctly handle little-endian reads
16+
* on big-endian systems by manually swapping bytes in the correct order.
17+
*
18+
* Usage:
19+
* Load this file BEFORE initializing the Solana SDK:
20+
* require('./s390x-solana-endianness-fix');
21+
*/
22+
23+
// Only apply patch on s390x architecture
24+
if (process.arch === 's390x') {
25+
console.log('🔧 Applying s390x Solana endianness fix...');
26+
27+
// Store original methods
28+
const originalReadBigUInt64LE = Buffer.prototype.readBigUInt64LE;
29+
const originalReadBigInt64LE = Buffer.prototype.readBigInt64LE;
30+
const originalReadUInt32LE = Buffer.prototype.readUInt32LE;
31+
const originalReadInt32LE = Buffer.prototype.readInt32LE;
32+
const originalReadUInt16LE = Buffer.prototype.readUInt16LE;
33+
const originalReadInt16LE = Buffer.prototype.readInt16LE;
34+
35+
/**
36+
* Manually read little-endian BigUInt64 by swapping bytes
37+
* Solana amounts are stored as u64 (8-byte unsigned integers)
38+
*/
39+
function readBigUInt64LEFixed(offset = 0) {
40+
// Read 8 bytes and construct BigInt in little-endian order
41+
let result = 0n;
42+
for (let i = 7; i >= 0; i--) {
43+
result = (result << 8n) | BigInt(this[offset + i]);
44+
}
45+
return result;
46+
}
47+
48+
/**
49+
* Manually read little-endian BigInt64 by swapping bytes
50+
*/
51+
function readBigInt64LEFixed(offset = 0) {
52+
let result = 0n;
53+
for (let i = 7; i >= 0; i--) {
54+
result = (result << 8n) | BigInt(this[offset + i]);
55+
}
56+
// Handle two's complement for signed values
57+
if (result >= 0x8000000000000000n) {
58+
result = result - 0x10000000000000000n;
59+
}
60+
return result;
61+
}
62+
63+
/**
64+
* Manually read little-endian UInt32 by swapping bytes
65+
*/
66+
function readUInt32LEFixed(offset = 0) {
67+
return (
68+
this[offset] |
69+
(this[offset + 1] << 8) |
70+
(this[offset + 2] << 16) |
71+
(this[offset + 3] * 0x1000000) // Use multiplication to avoid signed int issues
72+
);
73+
}
74+
75+
/**
76+
* Manually read little-endian Int32 by swapping bytes
77+
*/
78+
function readInt32LEFixed(offset = 0) {
79+
const value =
80+
this[offset] | (this[offset + 1] << 8) | (this[offset + 2] << 16) | (this[offset + 3] << 24);
81+
return value;
82+
}
83+
84+
/**
85+
* Manually read little-endian UInt16 by swapping bytes
86+
*/
87+
function readUInt16LEFixed(offset = 0) {
88+
return this[offset] | (this[offset + 1] << 8);
89+
}
90+
91+
/**
92+
* Manually read little-endian Int16 by swapping bytes
93+
*/
94+
function readInt16LEFixed(offset = 0) {
95+
const value = this[offset] | (this[offset + 1] << 8);
96+
return value & 0x8000 ? value | 0xffff0000 : value;
97+
}
98+
99+
// Apply patches
100+
Buffer.prototype.readBigUInt64LE = function (offset = 0) {
101+
try {
102+
return readBigUInt64LEFixed.call(this, offset);
103+
} catch (err) {
104+
console.error('s390x readBigUInt64LE error:', err);
105+
return originalReadBigUInt64LE.call(this, offset);
106+
}
107+
};
108+
109+
Buffer.prototype.readBigInt64LE = function (offset = 0) {
110+
try {
111+
return readBigInt64LEFixed.call(this, offset);
112+
} catch (err) {
113+
console.error('s390x readBigInt64LE error:', err);
114+
return originalReadBigInt64LE.call(this, offset);
115+
}
116+
};
117+
118+
Buffer.prototype.readUInt32LE = function (offset = 0) {
119+
try {
120+
return readUInt32LEFixed.call(this, offset);
121+
} catch (err) {
122+
console.error('s390x readUInt32LE error:', err);
123+
return originalReadUInt32LE.call(this, offset);
124+
}
125+
};
126+
127+
Buffer.prototype.readInt32LE = function (offset = 0) {
128+
try {
129+
return readInt32LEFixed.call(this, offset);
130+
} catch (err) {
131+
console.error('s390x readInt32LE error:', err);
132+
return originalReadInt32LE.call(this, offset);
133+
}
134+
};
135+
136+
Buffer.prototype.readUInt16LE = function (offset = 0) {
137+
try {
138+
return readUInt16LEFixed.call(this, offset);
139+
} catch (err) {
140+
console.error('s390x readUInt16LE error:', err);
141+
return originalReadUInt16LE.call(this, offset);
142+
}
143+
};
144+
145+
Buffer.prototype.readInt16LE = function (offset = 0) {
146+
try {
147+
return readInt16LEFixed.call(this, offset);
148+
} catch (err) {
149+
console.error('s390x readInt16LE error:', err);
150+
return originalReadInt16LE.call(this, offset);
151+
}
152+
};
153+
154+
console.log('✅ s390x Solana endianness fix applied successfully');
155+
console.log(
156+
' Patched methods: readBigUInt64LE, readBigInt64LE, readUInt32LE, readInt32LE, readUInt16LE, readInt16LE',
157+
);
158+
} else {
159+
console.log('ℹ️ s390x Solana endianness fix: Not needed on', process.arch);
160+
}
161+
162+
module.exports = {};

0 commit comments

Comments
 (0)