@@ -200,5 +200,190 @@ describe('Flrp Export In P Tx Builder', () => {
200200 rawTx . should . equal ( signedExportHex ) ;
201201 tx . id . should . equal ( 'ka8at5CinmpUc6QMVr33dyUJi156LKMdodrJM59kS6EWr3vHg' ) ;
202202 } ) ;
203+
204+ it ( 'should FAIL with unsorted UTXO addresses - demonstrates AddressMap mismatch issue for export in P-chain tx' , async ( ) => {
205+ // This test uses UTXO addresses in UNSORTED order to demonstrate the issue.
206+ // With unsorted addresses, the current implementation will create AddressMaps incorrectly
207+ // because it uses sorted addresses, not UTXO address order.
208+ //
209+ // Expected: AddressMap should map addresses to signature slots based on UTXO order (sigIndicies)
210+ // Current (WRONG): AddressMap uses sorted addresses with sequential slots
211+ //
212+ // This test WILL FAIL with current implementation because AddressMaps don't match sigIndicies
213+
214+ // UTXO addresses in UNSORTED order (different from sorted)
215+ // Sorted would be: [0x12cb... (smallest), 0xa6e0... (middle), 0xc386... (largest)]
216+ // Unsorted: [0xc386... (largest), 0x12cb... (smallest), 0xa6e0... (middle)]
217+ const unsortedUtxoAddresses = [
218+ '0xc386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3' , // Largest (would be index 2 if sorted)
219+ '0x12cb32eaf92553064db98d271b56cba079ec78f5' , // Smallest (would be index 0 if sorted)
220+ '0xa6e0c1abd0132f70efb77e2274637ff336a29a57' , // Middle (would be index 1 if sorted)
221+ ] ;
222+
223+ // Corresponding P-chain addresses (in same order as UTXO)
224+ const pAddresses = [
225+ 'P-costwo15msvr27szvhhpmah0c38gcml7vm29xjh7tcek8' , // Maps to 0xc386... (UTXO index 0)
226+ 'P-costwo1zt9n96hey4fsvnde35n3k4kt5pu7c784dzewzd' , // Maps to 0x12cb... (UTXO index 1)
227+ 'P-costwo1cwrdtrgf4xh80ncu7palrjw7gn4mpj0n4dxghh' , // Maps to 0xa6e0... (UTXO index 2)
228+ ] ;
229+
230+ // Create UTXO with UNSORTED addresses
231+ // Amount must cover export amount + fee
232+ const exportAmount = '50000000' ;
233+ const fee = '1261000' ;
234+ const utxoAmount = ( BigInt ( exportAmount ) + BigInt ( fee ) ) . toString ( ) ; // amount + fee
235+
236+ const utxo : DecodedUtxoObj = {
237+ outputID : 0 ,
238+ amount : utxoAmount ,
239+ txid : 'zstyYq5riDKYDSR3fUYKKkuXKJ1aJCe8WNrXKqEBJD4CGwzFw' ,
240+ outputidx : '0' ,
241+ addresses : unsortedUtxoAddresses , // UNSORTED order
242+ threshold : 2 ,
243+ } ;
244+
245+ // Build transaction
246+ const txBuilder = factory
247+ . getExportInPBuilder ( )
248+ . threshold ( 2 )
249+ . locktime ( 0 )
250+ . fromPubKey ( pAddresses )
251+ . externalChainId ( testData . sourceChainId )
252+ . amount ( exportAmount )
253+ . fee ( fee )
254+ . utxos ( [ utxo ] ) ;
255+
256+ // Build unsigned transaction
257+ const unsignedTx = await txBuilder . build ( ) ;
258+ const unsignedHex = unsignedTx . toBroadcastFormat ( ) ;
259+
260+ // Parse it back to inspect AddressMaps and sigIndicies
261+ const parsedBuilder = factory . from ( unsignedHex ) ;
262+ const parsedTx = await parsedBuilder . build ( ) ;
263+ const flareTx = ( parsedTx as any ) . _flareTransaction ;
264+
265+ // Get the input to check sigIndicies
266+ const exportTx = flareTx . tx as any ;
267+ const input = exportTx . baseTx . inputs [ 0 ] ;
268+ const transferInput = input . input ;
269+ const sigIndicies = transferInput . sigIndicies ( ) ;
270+
271+ // sigIndicies tells us: sigIndicies[slotIndex] = utxoAddressIndex
272+ // For threshold=2, we need signatures for first 2 addresses in UTXO order
273+ // UTXO order: [0xc386... (index 0), 0x12cb... (index 1), 0xa6e0... (index 2)]
274+ // So sigIndicies should be [0, 1] meaning: slot 0 = UTXO index 0, slot 1 = UTXO index 1
275+
276+ // Verify sigIndicies are [0, 1] (first 2 addresses in UTXO order, NOT sorted order)
277+ sigIndicies . length . should . equal ( 2 ) ;
278+ sigIndicies [ 0 ] . should . equal ( 0 , 'First signature slot should be UTXO address index 0 (0xc386...)' ) ;
279+ sigIndicies [ 1 ] . should . equal ( 1 , 'Second signature slot should be UTXO address index 1 (0x12cb...)' ) ;
280+
281+ // The critical test: Verify that signature slots have embedded addresses based on UTXO order
282+ // With unsorted UTXO addresses, this will FAIL if AddressMaps don't match UTXO order
283+ //
284+ // sigIndicies tells us: sigIndicies[slotIndex] = utxoAddressIndex
285+ // For threshold=2, we need signatures for first 2 addresses in UTXO order
286+ // UTXO order: [0xc386... (index 0), 0x12cb... (index 1), 0xa6e0... (index 2)]
287+ // So sigIndicies should be [0, 1] meaning: slot 0 = UTXO index 0, slot 1 = UTXO index 1
288+
289+ // Parse the credential to see which slots have which embedded addresses
290+ const credential = flareTx . credentials [ 0 ] ;
291+ const signatures = credential . getSignatures ( ) ;
292+
293+ // Helper function to check if signature has embedded address (same logic as transaction.ts)
294+ const testUtils2 = require ( '../../../src/lib/utils' ) . default ;
295+ const isEmptySignature = ( signature : string ) : boolean => {
296+ return ! ! signature && testUtils2 . removeHexPrefix ( signature ) . startsWith ( '0' . repeat ( 90 ) ) ;
297+ } ;
298+
299+ const hasEmbeddedAddress = ( signature : string ) : boolean => {
300+ if ( ! isEmptySignature ( signature ) ) return false ;
301+ const cleanSig = testUtils2 . removeHexPrefix ( signature ) ;
302+ if ( cleanSig . length < 130 ) return false ;
303+ const embeddedPart = cleanSig . substring ( 90 , 130 ) ;
304+ // Check if embedded part is not all zeros
305+ return embeddedPart !== '0' . repeat ( 40 ) ;
306+ } ;
307+
308+ // Extract embedded addresses from signature slots
309+ const embeddedAddresses : string [ ] = [ ] ;
310+
311+ signatures . forEach ( ( sig : string , slotIndex : number ) => {
312+ if ( hasEmbeddedAddress ( sig ) ) {
313+ // Extract embedded address (after position 90, 40 chars = 20 bytes)
314+ const cleanSig = testUtils2 . removeHexPrefix ( sig ) ;
315+ const embeddedAddr = cleanSig . substring ( 90 , 130 ) . toLowerCase ( ) ;
316+ embeddedAddresses [ slotIndex ] = '0x' + embeddedAddr ;
317+ }
318+ } ) ;
319+
320+ // Verify: Credentials only embed ONE address (user/recovery), not both
321+ // The embedded address should be based on addressesIndex logic, not sorted order
322+ //
323+ // Compute addressesIndex to determine expected signature order
324+ const utxoAddressBytes = unsortedUtxoAddresses . map ( ( addr ) => testUtils2 . parseAddress ( addr ) ) ;
325+ const pAddressBytes = pAddresses . map ( ( addr ) => testUtils2 . parseAddress ( addr ) ) ;
326+
327+ const addressesIndex : number [ ] = [ ] ;
328+ pAddressBytes . forEach ( ( pAddr ) => {
329+ const utxoIndex = utxoAddressBytes . findIndex (
330+ ( uAddr ) => Buffer . compare ( Buffer . from ( uAddr ) , Buffer . from ( pAddr ) ) === 0
331+ ) ;
332+ addressesIndex . push ( utxoIndex ) ;
333+ } ) ;
334+
335+ // firstIndex = 0 (user), bitgoIndex = 1
336+ const firstIndex = 0 ;
337+ const bitgoIndex = 1 ;
338+
339+ // Determine expected signature order based on addressesIndex
340+ const userComesFirst = addressesIndex [ bitgoIndex ] > addressesIndex [ firstIndex ] ;
341+
342+ // Expected credential structure:
343+ // - If user comes first: [userAddress, zeros]
344+ // - If bitgo comes first: [zeros, userAddress]
345+ const userAddressHex = Buffer . from ( pAddressBytes [ firstIndex ] ) . toString ( 'hex' ) . toLowerCase ( ) ;
346+ const expectedUserAddr = '0x' + userAddressHex ;
347+
348+ if ( userComesFirst ) {
349+ // Expected: [userAddress, zeros]
350+ // Slot 0 should have user address (pAddr0 = 0xc386... = UTXO index 0)
351+ if ( embeddedAddresses [ 0 ] ) {
352+ embeddedAddresses [ 0 ]
353+ . toLowerCase ( )
354+ . should . equal (
355+ expectedUserAddr ,
356+ `Slot 0 should have user address (${ expectedUserAddr } ) because user comes first in UTXO order`
357+ ) ;
358+ } else {
359+ throw new Error ( `Slot 0 should have embedded user address, but is empty` ) ;
360+ }
361+ // Slot 1 should be zeros (no embedded address)
362+ if ( embeddedAddresses [ 1 ] ) {
363+ throw new Error ( `Slot 1 should be zeros, but has embedded address: ${ embeddedAddresses [ 1 ] } ` ) ;
364+ }
365+ } else {
366+ // Expected: [zeros, userAddress]
367+ // Slot 0 should be zeros
368+ if ( embeddedAddresses [ 0 ] ) {
369+ throw new Error ( `Slot 0 should be zeros, but has embedded address: ${ embeddedAddresses [ 0 ] } ` ) ;
370+ }
371+ // Slot 1 should have user address
372+ if ( embeddedAddresses [ 1 ] ) {
373+ embeddedAddresses [ 1 ]
374+ . toLowerCase ( )
375+ . should . equal (
376+ expectedUserAddr ,
377+ `Slot 1 should have user address (${ expectedUserAddr } ) because bitgo comes first in UTXO order`
378+ ) ;
379+ } else {
380+ throw new Error ( `Slot 1 should have embedded user address, but is empty` ) ;
381+ }
382+ }
383+
384+ // The key verification: AddressMaps should match the credential order
385+ // With the fix, AddressMaps are created using the same addressesIndex logic as credentials
386+ // This ensures signing works correctly even with unsorted UTXO addresses
387+ } ) ;
203388 } ) ;
204389} ) ;
0 commit comments