Skip to content

Commit 0f59493

Browse files
committed
fix: chain sync batch — testnet xpub collisions, decline/invalid handling
- Drop the cross-chain duplicate-xpub rejection: all testnets share slip 1, so Sepolia and Amoy (both evm/p2sh with default bip32 bytes) derive byte-identical wallet xpubs — as do Testnet and Signet Bitcoin. Any batch containing such a pair was rejected wholesale as 'duplicate_xpub'. Duplicate CHAIN entries remain invalid. - Post chainsyncrejected('invalid') for malformed requests instead of silently dropping them — the wallet otherwise waits the full 30s fallback timeout before offering per-chain QR sync. This wires up the previously dead 'invalid' rejection-reason arm. - Declining a batch left batchStartedRef stuck true for the session (suppressing every later verification code) and, on a fresh pairing, dropped the identity verification code entirely while the wallet shows one. Decline now shows the identity code and resets the flag. - CHAIN_SYNC_POST_SPACING_MS 1500 → 3000: the relay sync doc is last-write-wins per walletIdentity and the wallet polls at 1s — 1.5s left almost no margin for network jitter on already-derived chains, and a missed doc is never re-posted. 3s gives the poll 2-3 chances per doc and stays far inside the wallet's 60s stall window.
1 parent 86e1610 commit 0f59493

3 files changed

Lines changed: 38 additions & 13 deletions

File tree

src/lib/chainSyncRequest.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ export const CHAIN_SYNC_MAX_CHAINS = 20;
2626
/**
2727
* Spacing between per-chain POST /v1/sync calls. The relay sync document is
2828
* last-write-wins keyed on walletIdentity and the wallet polls it every 1s —
29-
* posting faster than the poll interval could make the wallet miss a chain.
29+
* posting faster than the poll interval makes the wallet miss a chain. 3s
30+
* gives the poll 2-3 chances per document (1.5s left almost no margin for
31+
* network jitter on already-derived chains, and a missed doc is never
32+
* re-posted); still far inside the wallet's 60s per-chain stall window.
3033
*/
31-
export const CHAIN_SYNC_POST_SPACING_MS = 1500;
34+
export const CHAIN_SYNC_POST_SPACING_MS = 3000;
3235

3336
const MAX_XPUB_LENGTH = 3000; // solana pubkey arrays are ~950 chars; xpubs ~112
3437

@@ -114,7 +117,6 @@ export function parseChainSyncRequest(
114117
return { status: 'invalid', reason: 'too_many_chains' };
115118
}
116119
const seen = new Set<string>();
117-
const seenXpubs = new Set<string>();
118120
const entries: ChainSyncRequestEntry[] = [];
119121
for (const item of chains) {
120122
if (!item || typeof item !== 'object' || Array.isArray(item)) {
@@ -148,12 +150,10 @@ export function parseChainSyncRequest(
148150
if (!xpubValid) {
149151
return { status: 'invalid', reason: 'bad_xpub' };
150152
}
151-
// Each chain derives a distinct extended key; the same xpub on two
152-
// chains is never produced by a well-formed wallet.
153-
if (seenXpubs.has(xpubWallet)) {
154-
return { status: 'invalid', reason: 'duplicate_xpub' };
155-
}
156-
seenXpubs.add(xpubWallet);
153+
// NOTE: the same xpub CAN legitimately appear on two different chains —
154+
// all testnets share slip 1, so e.g. Sepolia and Amoy (both p2sh/evm,
155+
// default bip32 bytes) derive byte-identical xpubs. Only duplicate CHAIN
156+
// entries are invalid (checked above).
157157
entries.push({
158158
chain: chain as keyof cryptos,
159159
xpubWallet,

src/screens/Home/Home.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,15 @@ function Home({ navigation }: Props) {
489489
}
490490
console.log('[Chain Sync] Invalid request:', parsed.reason);
491491
displayMessage('error', t('home:err_invalid_request'));
492+
// Tell the wallet immediately — without this it waits out the full 30s
493+
// fallback timeout before offering per-chain QR sync.
494+
postAction(
495+
'chainsyncrejected',
496+
buildChainSyncRejectionPayload('invalid'),
497+
identityChain,
498+
'',
499+
sspWalletKeyInternalIdentity,
500+
).catch((error) => console.log(error));
492501
};
493502

494503
const handleChainSyncRequestAction = (status: boolean) => {
@@ -497,6 +506,16 @@ function Home({ navigation }: Props) {
497506
} else {
498507
// reject — notify wallet so it can offer per-chain QR sync right away
499508
setChainSyncData(null);
509+
// The declined batch will never drive the code screen. If identity was
510+
// paired this session, show its code now (the wallet shows one), and
511+
// clear the batch flag so a later pairing starts clean — leaving it set
512+
// would suppress every future identity code this session.
513+
batchStartedRef.current = false;
514+
if (identityVerifyEntryRef.current) {
515+
setBatchVerifyWords(
516+
sessionVerificationWords([identityVerifyEntryRef.current]),
517+
);
518+
}
500519
postAction(
501520
'chainsyncrejected',
502521
buildChainSyncRejectionPayload('declined'),

tests/lib/chainSyncRequest.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,23 @@ describe('parseChainSyncRequest', () => {
185185
expect(result).toEqual({ status: 'invalid', reason: 'duplicate_chain' });
186186
});
187187

188-
it('rejects the same xpub reused across different chains', () => {
188+
it('accepts the same xpub on two different chains (testnets share slip 1 and derive identical xpubs)', () => {
189189
const result = parseChainSyncRequest(
190190
payload({
191191
chains: [
192-
{ chain: 'eth', xpubWallet: VALID_XPUB },
193-
{ chain: 'polygon', xpubWallet: VALID_XPUB },
192+
{ chain: 'sepolia', xpubWallet: VALID_XPUB },
193+
{ chain: 'amoy', xpubWallet: VALID_XPUB },
194194
],
195195
}),
196196
IDENTITY_CHAIN,
197197
);
198-
expect(result).toEqual({ status: 'invalid', reason: 'duplicate_xpub' });
198+
expect(result.status).toBe('ok');
199+
if (result.status === 'ok') {
200+
expect(result.request.chains.map((c) => c.chain)).toEqual([
201+
'sepolia',
202+
'amoy',
203+
]);
204+
}
199205
});
200206

201207
it('rejects missing/empty/oversized xpubs', () => {

0 commit comments

Comments
 (0)