Skip to content

Commit 639a077

Browse files
authored
Merge pull request #7796 from BitGo/WIN-8445
feat(sdk-coin-flrp): implement dynamic credential ordering
2 parents e7a5a20 + 143830f commit 639a077

File tree

10 files changed

+267
-56
lines changed

10 files changed

+267
-56
lines changed

modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,57 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
114114
const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
115115

116116
// When credentials were extracted, use them directly to preserve existing signatures
117-
// Otherwise, create empty credentials with embedded addresses for slot identification
117+
// Otherwise, create empty credentials with dynamic ordering based on addressesIndex
118+
// Match avaxp behavior: order depends on UTXO address positions
118119
const txCredentials =
119120
credentials.length > 0
120121
? credentials
121-
: exportTx.baseTx.inputs.map((input) => {
122+
: exportTx.baseTx.inputs.map((input, inputIdx) => {
122123
const transferInput = input.input as TransferInput;
123124
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;
124-
// Create empty signatures with embedded addresses for slot identification
125-
const sigSlots: ReturnType<typeof utils.createEmptySigWithAddress>[] = [];
126-
for (let i = 0; i < inputThreshold; i++) {
127-
const addrHex = Buffer.from(sortedAddresses[i]).toString('hex');
128-
sigSlots.push(utils.createEmptySigWithAddress(addrHex));
125+
126+
// Get UTXO for this input to determine addressesIndex
127+
const utxo = this.transaction._utxos[inputIdx];
128+
129+
// either user (0) or recovery (2)
130+
const firstIndex = this.recoverSigner ? 2 : 0;
131+
const bitgoIndex = 1;
132+
133+
// If UTXO has addresses, compute dynamic ordering
134+
if (utxo && utxo.addresses && utxo.addresses.length > 0) {
135+
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
136+
const addressesIndex = this.transaction._fromAddresses.map((a) =>
137+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
138+
);
139+
140+
// Dynamic ordering based on addressesIndex
141+
let sigSlots: ReturnType<typeof utils.createNewSig>[];
142+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
143+
// Bitgo comes first: [zeros, userAddress]
144+
sigSlots = [
145+
utils.createNewSig(''),
146+
utils.createEmptySigWithAddress(
147+
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
148+
),
149+
];
150+
} else {
151+
// User comes first: [userAddress, zeros]
152+
sigSlots = [
153+
utils.createEmptySigWithAddress(
154+
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
155+
),
156+
utils.createNewSig(''),
157+
];
158+
}
159+
return new Credential(sigSlots);
160+
} else {
161+
// Fallback: use all zeros if no UTXO addresses available
162+
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
163+
for (let i = 0; i < inputThreshold; i++) {
164+
sigSlots.push(utils.createNewSig(''));
165+
}
166+
return new Credential(sigSlots);
129167
}
130-
return new Credential(sigSlots);
131168
});
132169

133170
// Create address maps for signing - one per input/credential
@@ -277,14 +314,43 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
277314

278315
inputs.push(transferableInput);
279316

280-
// Create credential with empty signatures that have embedded addresses for slot identification
281-
// This allows the signing logic to determine which slot belongs to which address
282-
const sortedAddrs = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
283-
const emptySignatures = sigIndices.map((idx) => {
284-
const addrHex = Buffer.from(sortedAddrs[idx]).toString('hex');
285-
return utils.createEmptySigWithAddress(addrHex);
286-
});
287-
credentials.push(new Credential(emptySignatures));
317+
// Create credential with empty signatures for slot identification
318+
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
319+
const hasAddresses =
320+
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;
321+
322+
if (!hasAddresses) {
323+
// If addresses not available, use all zeros
324+
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
325+
credentials.push(new Credential(emptySignatures));
326+
} else {
327+
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
328+
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
329+
const addressesIndex = this.transaction._fromAddresses.map((a) =>
330+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
331+
);
332+
333+
// either user (0) or recovery (2)
334+
const firstIndex = this.recoverSigner ? 2 : 0;
335+
const bitgoIndex = 1;
336+
337+
// Dynamic ordering based on addressesIndex
338+
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
339+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
340+
// Bitgo comes first in signature order: [zeros, userAddress]
341+
emptySignatures = [
342+
utils.createNewSig(''),
343+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
344+
];
345+
} else {
346+
// User comes first in signature order: [userAddress, zeros]
347+
emptySignatures = [
348+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
349+
utils.createNewSig(''),
350+
];
351+
}
352+
credentials.push(new Credential(emptySignatures));
353+
}
288354
}
289355

290356
// Create change output if there is remaining amount after export and fee

modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,12 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
9393
const addressMaps = new FlareUtils.AddressMaps([addressMap]);
9494

9595
// When credentials were extracted, use them directly to preserve existing signatures
96+
// For initBuilder, _fromAddresses may not be set yet, so use all zeros for credential slots
9697
let txCredentials: Credential[];
9798
if (credentials.length > 0) {
9899
txCredentials = credentials;
99100
} else {
100-
// Create empty credential with threshold number of signature slots
101+
// Create empty credential with threshold number of signature slots (all zeros)
101102
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
102103
for (let i = 0; i < inputThreshold; i++) {
103104
emptySignatures.push(utils.createNewSig(''));
@@ -227,10 +228,43 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
227228

228229
inputs.push(transferableInput);
229230

230-
// Create empty credential for each input with threshold signers
231-
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
232-
const credential = new Credential(emptySignatures);
233-
credentials.push(credential);
231+
// Create credential with empty signatures for slot identification
232+
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
233+
const hasAddresses =
234+
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;
235+
236+
if (!hasAddresses) {
237+
// If addresses not available, use all zeros
238+
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
239+
credentials.push(new Credential(emptySignatures));
240+
} else {
241+
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
242+
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
243+
const addressesIndex = this.transaction._fromAddresses.map((a) =>
244+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
245+
);
246+
247+
// either user (0) or recovery (2)
248+
const firstIndex = this.recoverSigner ? 2 : 0;
249+
const bitgoIndex = 1;
250+
251+
// Dynamic ordering based on addressesIndex
252+
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
253+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
254+
// Bitgo comes first in signature order: [zeros, userAddress]
255+
emptySignatures = [
256+
utils.createNewSig(''),
257+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
258+
];
259+
} else {
260+
// User comes first in signature order: [userAddress, zeros]
261+
emptySignatures = [
262+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
263+
utils.createNewSig(''),
264+
];
265+
}
266+
credentials.push(new Credential(emptySignatures));
267+
}
234268
});
235269

236270
return {

modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,51 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
9696
const addressMaps = sortedAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]]));
9797

9898
// When credentials were extracted, use them directly to preserve existing signatures
99+
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
99100
const txCredentials =
100101
credentials.length > 0
101102
? credentials
102-
: [new Credential(sortedAddresses.slice(0, this.transaction._threshold).map(() => utils.createNewSig('')))];
103+
: this.transaction._utxos.map((utxo) => {
104+
// either user (0) or recovery (2)
105+
const firstIndex = this.recoverSigner ? 2 : 0;
106+
const bitgoIndex = 1;
107+
108+
// If UTXO has addresses, compute dynamic ordering
109+
if (utxo && utxo.addresses && utxo.addresses.length > 0) {
110+
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
111+
const addressesIndex = this.transaction._fromAddresses.map((a) =>
112+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
113+
);
114+
115+
// Dynamic ordering based on addressesIndex
116+
let sigSlots: ReturnType<typeof utils.createNewSig>[];
117+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
118+
// Bitgo comes first: [zeros, userAddress]
119+
sigSlots = [
120+
utils.createNewSig(''),
121+
utils.createEmptySigWithAddress(
122+
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
123+
),
124+
];
125+
} else {
126+
// User comes first: [userAddress, zeros]
127+
sigSlots = [
128+
utils.createEmptySigWithAddress(
129+
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
130+
),
131+
utils.createNewSig(''),
132+
];
133+
}
134+
return new Credential(sigSlots);
135+
} else {
136+
// Fallback: use all zeros if no UTXO addresses available
137+
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
138+
for (let i = 0; i < this.transaction._threshold; i++) {
139+
sigSlots.push(utils.createNewSig(''));
140+
}
141+
return new Credential(sigSlots);
142+
}
143+
});
103144

104145
const unsignedTx = new UnsignedTx(importTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
105146

@@ -221,9 +262,43 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
221262

222263
inputs.push(transferableInput);
223264

224-
// Create credential with empty signatures for threshold signers
225-
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
226-
credentials.push(new Credential(emptySignatures));
265+
// Create credential with empty signatures for slot identification
266+
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
267+
const hasAddresses =
268+
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;
269+
270+
if (!hasAddresses) {
271+
// If addresses not available, use all zeros
272+
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
273+
credentials.push(new Credential(emptySignatures));
274+
} else {
275+
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
276+
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
277+
const addressesIndex = this.transaction._fromAddresses.map((a) =>
278+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
279+
);
280+
281+
// either user (0) or recovery (2)
282+
const firstIndex = this.recoverSigner ? 2 : 0;
283+
const bitgoIndex = 1;
284+
285+
// Dynamic ordering based on addressesIndex
286+
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
287+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
288+
// Bitgo comes first in signature order: [zeros, userAddress]
289+
emptySignatures = [
290+
utils.createNewSig(''),
291+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
292+
];
293+
} else {
294+
// User comes first in signature order: [userAddress, zeros]
295+
emptySignatures = [
296+
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
297+
utils.createNewSig(''),
298+
];
299+
}
300+
credentials.push(new Credential(emptySignatures));
301+
}
227302
});
228303

229304
return {

modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,42 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
9797
Object.assign(transferableInput, { input });
9898
inputs.push(transferableInput);
9999

100-
// Create empty credential for each input
101-
const emptySignatures = sender.map(() => utils.createNewSig(''));
102-
credentials.push(new Credential(emptySignatures));
100+
// Create credential with empty signatures for slot identification
101+
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
102+
const hasAddresses = sender && sender.length >= (this.transaction as Transaction)._threshold;
103+
104+
if (!hasAddresses) {
105+
// If addresses not available, use all zeros
106+
const emptySignatures = sender.map(() => utils.createNewSig(''));
107+
credentials.push(new Credential(emptySignatures));
108+
} else {
109+
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
110+
const utxoAddresses = utxo.addresses.map((a: string) => utils.parseAddress(a));
111+
const addressesIndex = sender.map((a) =>
112+
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
113+
);
114+
115+
// either user (0) or recovery (2)
116+
const firstIndex = this.recoverSigner ? 2 : 0;
117+
const bitgoIndex = 1;
118+
119+
// Dynamic ordering based on addressesIndex
120+
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
121+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
122+
// Bitgo comes first in signature order: [zeros, userAddress]
123+
emptySignatures = [
124+
utils.createNewSig(''),
125+
utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')),
126+
];
127+
} else {
128+
// User comes first in signature order: [userAddress, zeros]
129+
emptySignatures = [
130+
utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')),
131+
utils.createNewSig(''),
132+
];
133+
}
134+
credentials.push(new Credential(emptySignatures));
135+
}
103136
});
104137

105138
// Create output if there is change

modules/sdk-coin-flrp/src/lib/transaction.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,14 @@ export class Transaction extends BaseTransaction {
231231
return FlareUtils.bufferToHex(this._rawSignedBytes);
232232
}
233233
const unsignedTx = this._flareTransaction as UnsignedTx;
234-
// For signed transactions, return the full signed tx with credentials
235-
// Check signature.length for robustness
236-
if (this.signature.length > 0) {
237-
return FlareUtils.bufferToHex(unsignedTx.getSignedTx().toBytes());
238-
}
239-
// For unsigned transactions, return just the transaction bytes
240-
return FlareUtils.bufferToHex(unsignedTx.toBytes());
234+
const signedTxBytes = unsignedTx.getSignedTx().toBytes();
235+
236+
// Both P-chain and C-chain transactions include checksum (matching avaxp behavior)
237+
// avaxp P-chain: transaction.ts uses addChecksum() explicitly
238+
// avaxp C-chain: deprecatedTransaction.ts uses Tx.toStringHex() which internally adds checksum
239+
const rawTx = FlareUtils.bufferToHex(utils.addChecksum(signedTxBytes));
240+
console.log('rawTx in toBroadcastFormat:', rawTx);
241+
return rawTx;
241242
}
242243

243244
toJson(): TxData {

modules/sdk-coin-flrp/src/lib/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,12 @@ export class Utils implements BaseUtils {
308308

309309
/**
310310
* Adds a checksum to a Buffer and returns the concatenated result
311+
* Uses last 4 bytes of SHA256 hash as checksum (matching avaxp behavior)
311312
*/
312-
private addChecksum(buff: Buffer): Buffer {
313-
const hashSlice = createHash('sha256').update(buff).digest().slice(28);
314-
return Buffer.concat([buff, hashSlice]);
313+
public addChecksum(buff: Buffer | Uint8Array): Uint8Array {
314+
const buffer = Buffer.from(buff);
315+
const hashSlice = createHash('sha256').update(buffer).digest().slice(28);
316+
return new Uint8Array(Buffer.concat([buffer, hashSlice]));
315317
}
316318

317319
// In utils.ts, add this method to the Utils class:

0 commit comments

Comments
 (0)