Skip to content

Commit 8db2cb8

Browse files
committed
fix: use decimal string encoding for PDA seeds (not binary)
Found from CCTP source code (get_nonce_pda.rs): Seeds use decimal strings, not binary encoding! - domain: '4' (string), not [0x04,0x00,0x00,0x00] (binary u32 LE) - firstNonce: '288001' (string), not 8-byte LE - delimiter: empty string for domains < 11 Source: https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/get_nonce_pda.rs
1 parent 88d8d31 commit 8db2cb8

1 file changed

Lines changed: 43 additions & 25 deletions

File tree

public/app.js

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,13 @@ function debugPdaDerivation() {
243243
const messageTransmitterProgramId = new PublicKey('CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd');
244244

245245
// Known working values from successful transaction (nonce 288574)
246-
// Key finding: on-chain data shows firstNonce = 288001, NOT 288000!
247-
// CCTP uses 1-indexed buckets: firstNonce = floor((nonce-1)/6400)*6400 + 1
248246
const knownUsedNonces = '3ewgRKdMT8WjPjExuVuZ9gZ7qDYwquefpwtL1SUkLCxf';
249247
const sourceDomain = 4; // Noble
248+
const nonce = 288574n;
249+
250+
// Correct formula: firstNonce = floor((nonce-1)/6400)*6400 + 1
251+
const NONCES_PER_ACCOUNT = 6400n;
252+
const firstNonce = ((nonce - 1n) / NONCES_PER_ACCOUNT) * NONCES_PER_ACCOUNT + 1n;
250253

251254
const results = [];
252255

@@ -267,30 +270,35 @@ function debugPdaDerivation() {
267270
console.log('=== PDA Derivation Investigation ===');
268271
console.log('Known working PDA:', knownUsedNonces);
269272
console.log('Program:', messageTransmitterProgramId.toString());
273+
console.log('Nonce:', nonce.toString(), '→ firstNonce:', firstNonce.toString());
270274
console.log('');
271275

272-
// Standard LE encoding - with CORRECTED first nonce (288001 not 288000)
273-
const seed1 = bytesFromString('used_nonces');
274-
const domainLE = u32ToBytesLE(sourceDomain);
275-
276-
// The BUG was here: using 288000 instead of 288001
277-
// CCTP buckets are 1-indexed: firstNonce = floor((nonce-1)/6400)*6400 + 1
278-
const wrongNonce = 288000n; // What we were using
279-
const correctNonce = 288001n; // What CCTP actually uses (from on-chain data)
280-
281276
console.log('=== KEY FINDING ===');
282-
console.log('On-chain firstNonce from account data: 288001');
283-
console.log('What we were calculating: 288000');
284-
console.log('CCTP uses 1-indexed buckets!');
285-
console.log('Formula: firstNonce = floor((nonce-1)/6400)*6400 + 1');
277+
console.log('From CCTP source code (get_nonce_pda.rs):');
278+
console.log('Seeds use DECIMAL STRINGS, not binary encoding!');
279+
console.log(' - domain "4" (not 0x04000000)');
280+
console.log(' - firstNonce "288001" (not 0x0165040000000000)');
281+
console.log(' - delimiter "" for domains < 11');
286282
console.log('');
287283

288-
console.log('Testing with WRONG nonce (288000):');
289-
tryDerivation('WRONG: [used_nonces, domain_le, 288000_le]', [seed1, domainLE, u64ToBytesLE(wrongNonce)]);
284+
// WRONG: Binary encoding (what we were doing)
285+
console.log('Testing WRONG approach (binary encoding):');
286+
tryDerivation('WRONG: binary LE', [
287+
bytesFromString('used_nonces'),
288+
u32ToBytesLE(sourceDomain),
289+
u64ToBytesLE(firstNonce)
290+
]);
290291

291292
console.log('');
292-
console.log('Testing with CORRECT nonce (288001):');
293-
tryDerivation('CORRECT: [used_nonces, domain_le, 288001_le]', [seed1, domainLE, u64ToBytesLE(correctNonce)]);
293+
console.log('Testing CORRECT approach (string encoding):');
294+
// CORRECT: String encoding (from CCTP source)
295+
const delimiter = sourceDomain < 11 ? '' : '-';
296+
tryDerivation('CORRECT: strings', [
297+
bytesFromString('used_nonces'),
298+
bytesFromString(sourceDomain.toString()), // "4"
299+
bytesFromString(delimiter), // ""
300+
bytesFromString(firstNonce.toString()) // "288001"
301+
]);
294302

295303
console.log('');
296304
console.log('=== Summary ===');
@@ -823,19 +831,29 @@ async function relayToSolana() {
823831
const tokenMessengerMinterEventAuthority = new PublicKey('CNfZLeeL4RUxwfPnjA3tLiQt4y43jp4V7bMpga673jf9');
824832

825833
// ============ DYNAMIC: UsedNonces depends on nonce bucket ============
826-
// CCTP uses 1-indexed buckets! Formula: firstNonce = floor((nonce-1)/6400)*6400 + 1
827-
// This was discovered by inspecting on-chain account data which showed firstNonce=288001
834+
// CCTP uses 1-indexed buckets: firstNonce = floor((nonce-1)/6400)*6400 + 1
835+
// CRITICAL: Seeds use DECIMAL STRINGS, not binary encoding!
836+
// Source: https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/get_nonce_pda.rs
828837
const NONCES_PER_ACCOUNT = 6400n;
829838
const firstNonce = ((nonceValue - 1n) / NONCES_PER_ACCOUNT) * NONCES_PER_ACCOUNT + 1n;
830839

831-
// Derive usedNonces PDA dynamically (now works correctly with 1-indexed buckets!)
832-
const sourceDomainBuffer = u32ToBytesLE(sourceDomain);
833-
const firstNonceBuffer = u64ToBytesLE(firstNonce);
840+
// Seeds: ["used_nonces", domain_as_string, delimiter, firstNonce_as_string]
841+
// For domains < 11, delimiter is empty
842+
const domainString = sourceDomain.toString(); // "4" not binary bytes
843+
const firstNonceString = firstNonce.toString(); // "288001" not binary bytes
844+
const delimiter = sourceDomain < 11 ? '' : '-';
845+
834846
const [usedNonces] = PublicKey.findProgramAddressSync(
835-
[bytesFromString('used_nonces'), sourceDomainBuffer, firstNonceBuffer],
847+
[
848+
bytesFromString('used_nonces'),
849+
bytesFromString(domainString),
850+
bytesFromString(delimiter),
851+
bytesFromString(firstNonceString)
852+
],
836853
messageTransmitterProgramId
837854
);
838855
log(`Nonce ${nonceValue} → firstNonce bucket ${firstNonce}`, 'info');
856+
log(`usedNonces seeds: ["used_nonces", "${domainString}", "${delimiter}", "${firstNonceString}"]`, 'info');
839857
log(`usedNonces (derived): ${usedNonces.toString()}`, 'info');
840858
log(`authorityPda: ${authorityPda.toString()}`, 'info');
841859

0 commit comments

Comments
 (0)