Skip to content

Commit 3086847

Browse files
Merge pull request #7646 from BitGo/WIN-8065
feat(sdk-coin-iota): add signature serialization for iota transaction
2 parents 2962195 + 8472e3c commit 3086847

File tree

4 files changed

+389
-8
lines changed

4 files changed

+389
-8
lines changed

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

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
IOTA_KEY_BYTES_LENGTH,
2424
IOTA_SIGNATURE_LENGTH,
2525
} from './constants';
26+
import utils from './utils';
2627

2728
export abstract class Transaction extends BaseTransaction {
2829
static EMPTY_PUBLIC_KEY = Buffer.alloc(IOTA_KEY_BYTES_LENGTH);
@@ -38,7 +39,9 @@ export abstract class Transaction extends BaseTransaction {
3839
private _gasSponsor?: string;
3940
private _sender: string;
4041
private _signature?: Signature;
42+
private _serializedSignature?: string;
4143
private _gasSponsorSignature?: Signature;
44+
private _serializedGasSponsorSignature?: string;
4245
private _txDataBytes?: Uint8Array<ArrayBufferLike>;
4346
private _isSimulateTx: boolean;
4447

@@ -47,12 +50,6 @@ export abstract class Transaction extends BaseTransaction {
4750
this._sender = '';
4851
this._rebuildRequired = false;
4952
this._isSimulateTx = true;
50-
this._signature = {
51-
publicKey: {
52-
pub: Transaction.EMPTY_PUBLIC_KEY.toString('hex'),
53-
},
54-
signature: Transaction.EMPTY_SIGNATURE,
55-
};
5653
}
5754

5855
get gasBudget(): number | undefined {
@@ -137,12 +134,10 @@ export abstract class Transaction extends BaseTransaction {
137134
}
138135

139136
addSignature(publicKey: PublicKey, signature: Buffer): void {
140-
this._signatures = [...this._signatures, signature.toString('hex')];
141137
this._signature = { publicKey, signature };
142138
}
143139

144140
addGasSponsorSignature(publicKey: PublicKey, signature: Buffer): void {
145-
this._signatures = [...this._signatures, signature.toString('hex')];
146141
this._gasSponsorSignature = { publicKey, signature };
147142
}
148143

@@ -154,6 +149,26 @@ export abstract class Transaction extends BaseTransaction {
154149
return this.gasBudget?.toString();
155150
}
156151

152+
get serializedGasSponsorSignature(): string | undefined {
153+
return this._serializedGasSponsorSignature;
154+
}
155+
156+
get serializedSignature(): string | undefined {
157+
return this._serializedSignature;
158+
}
159+
160+
serializeSignatures(): void {
161+
this._signatures = [];
162+
if (this._signature) {
163+
this._serializedSignature = this.serializeSignature(this._signature as Signature);
164+
this._signatures.push(this._serializedSignature);
165+
}
166+
if (this._gasSponsorSignature) {
167+
this._serializedGasSponsorSignature = this.serializeSignature(this._gasSponsorSignature as Signature);
168+
this._signatures.push(this._serializedGasSponsorSignature);
169+
}
170+
}
171+
157172
async toBroadcastFormat(): Promise<string> {
158173
const txDataBytes: Uint8Array<ArrayBufferLike> = await this.build();
159174
return toBase64(txDataBytes);
@@ -291,6 +306,7 @@ export abstract class Transaction extends BaseTransaction {
291306
this._txDataBytes = await this._iotaTransaction.build();
292307
this._rebuildRequired = false;
293308
}
309+
this.serializeSignatures();
294310
return this._txDataBytes;
295311
}
296312

@@ -306,6 +322,15 @@ export abstract class Transaction extends BaseTransaction {
306322
this._iotaTransaction.setSender(this.sender);
307323
}
308324

325+
private serializeSignature(signature: Signature): string {
326+
const pubKey = Buffer.from(signature.publicKey.pub, 'hex');
327+
const serialized_sig = new Uint8Array(1 + signature.signature.length + pubKey.length);
328+
serialized_sig.set([0x00]); //Hardcoding the signature scheme flag since we only support EDDSA for iota
329+
serialized_sig.set(signature.signature, 1);
330+
serialized_sig.set(pubKey, 1 + signature.signature.length);
331+
return toBase64(serialized_sig);
332+
}
333+
309334
private validateTxData(): void {
310335
this.validateTxDataImplementation();
311336
if (!this.sender || this.sender === '') {
@@ -329,5 +354,25 @@ export abstract class Transaction extends BaseTransaction {
329354
`Gas payment objects count (${this.gasPaymentObjects.length}) exceeds maximum allowed (${MAX_GAS_PAYMENT_OBJECTS})`
330355
);
331356
}
357+
358+
if (
359+
this._signature &&
360+
!(
361+
utils.isValidPublicKey(this._signature.publicKey.pub) &&
362+
utils.isValidSignature(toBase64(this._signature.signature))
363+
)
364+
) {
365+
throw new InvalidTransactionError('Invalid sender signature');
366+
}
367+
368+
if (
369+
this._gasSponsorSignature &&
370+
!(
371+
utils.isValidPublicKey(this._gasSponsorSignature.publicKey.pub) &&
372+
utils.isValidSignature(toBase64(this._gasSponsorSignature.signature))
373+
)
374+
) {
375+
throw new InvalidTransactionError('Invalid gas sponsor signature');
376+
}
332377
}
333378
}

modules/sdk-coin-iota/test/resources/iota.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,30 @@ export const generateObjects = (count: number): TransactionObjectInput[] => {
7878
digest: `digest${i}`,
7979
}));
8080
};
81+
82+
// Test signature data for signature serialization tests
83+
export const testSignature = {
84+
// 64-byte signature (hex string)
85+
signature: Buffer.from(
86+
'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2' +
87+
'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4',
88+
'hex'
89+
),
90+
// Public key (already defined in sender)
91+
publicKey: {
92+
pub: sender.publicKey,
93+
},
94+
};
95+
96+
export const testGasSponsorSignature = {
97+
// 64-byte signature (hex string)
98+
signature: Buffer.from(
99+
'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5' +
100+
'f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7',
101+
'hex'
102+
),
103+
// Public key (already defined in gasSponsor)
104+
publicKey: {
105+
pub: gasSponsor.publicKey,
106+
},
107+
};

modules/sdk-coin-iota/test/unit/transactionBuilder/transferBuilder.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,157 @@ describe('Iota Transfer Builder', () => {
306306
should(() => factory.from('invalidRawTransaction')).throwError();
307307
});
308308
});
309+
310+
describe('Transaction Signing', () => {
311+
it('should build transaction with sender signature', async function () {
312+
const txBuilder = factory.getTransferBuilder();
313+
txBuilder.sender(testData.sender.address);
314+
txBuilder.recipients(testData.recipients);
315+
txBuilder.paymentObjects(testData.paymentObjects);
316+
txBuilder.gasData(testData.gasData);
317+
318+
const tx = (await txBuilder.build()) as TransferTransaction;
319+
320+
// Add signature
321+
tx.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
322+
323+
// Rebuild to trigger serialization
324+
await tx.build();
325+
326+
should.exist(tx.serializedSignature);
327+
should.equal(tx.serializedSignature!.length > 0, true);
328+
});
329+
330+
it('should build transaction with gas sponsor signature', async function () {
331+
const txBuilder = factory.getTransferBuilder();
332+
txBuilder.sender(testData.sender.address);
333+
txBuilder.recipients(testData.recipients);
334+
txBuilder.paymentObjects(testData.paymentObjects);
335+
txBuilder.gasData(testData.gasData);
336+
txBuilder.gasSponsor(testData.gasSponsor.address);
337+
338+
const tx = (await txBuilder.build()) as TransferTransaction;
339+
340+
// Add gas sponsor signature
341+
tx.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, testData.testGasSponsorSignature.signature);
342+
343+
// Rebuild to trigger serialization
344+
await tx.build();
345+
346+
should.exist(tx.serializedGasSponsorSignature);
347+
should.equal(tx.serializedGasSponsorSignature!.length > 0, true);
348+
});
349+
350+
it('should build transaction with both sender and gas sponsor signatures', async function () {
351+
const txBuilder = factory.getTransferBuilder();
352+
txBuilder.sender(testData.sender.address);
353+
txBuilder.recipients(testData.recipients);
354+
txBuilder.paymentObjects(testData.paymentObjects);
355+
txBuilder.gasData(testData.gasData);
356+
txBuilder.gasSponsor(testData.gasSponsor.address);
357+
358+
const tx = (await txBuilder.build()) as TransferTransaction;
359+
360+
// Add both signatures
361+
tx.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
362+
tx.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, testData.testGasSponsorSignature.signature);
363+
364+
// Rebuild to trigger serialization
365+
await tx.build();
366+
367+
should.exist(tx.serializedSignature);
368+
should.exist(tx.serializedGasSponsorSignature);
369+
tx.signature.length.should.equal(2);
370+
});
371+
372+
it('should add signature through builder and serialize correctly', async function () {
373+
const txBuilder = factory.getTransferBuilder();
374+
txBuilder.sender(testData.sender.address);
375+
txBuilder.recipients(testData.recipients);
376+
txBuilder.paymentObjects(testData.paymentObjects);
377+
txBuilder.gasData(testData.gasData);
378+
379+
// Add signature through builder
380+
txBuilder.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
381+
382+
const tx = (await txBuilder.build()) as TransferTransaction;
383+
384+
should.exist(tx.serializedSignature);
385+
should.equal(typeof tx.serializedSignature, 'string');
386+
// Verify signature array is populated
387+
tx.signature.length.should.equal(1);
388+
});
389+
390+
it('should add gas sponsor signature through builder and serialize correctly', async function () {
391+
const txBuilder = factory.getTransferBuilder();
392+
txBuilder.sender(testData.sender.address);
393+
txBuilder.recipients(testData.recipients);
394+
txBuilder.paymentObjects(testData.paymentObjects);
395+
txBuilder.gasData(testData.gasData);
396+
txBuilder.gasSponsor(testData.gasSponsor.address);
397+
398+
// Add gas sponsor signature through builder
399+
txBuilder.addGasSponsorSignature(
400+
testData.testGasSponsorSignature.publicKey,
401+
testData.testGasSponsorSignature.signature
402+
);
403+
404+
const tx = (await txBuilder.build()) as TransferTransaction;
405+
406+
should.exist(tx.serializedGasSponsorSignature);
407+
should.equal(typeof tx.serializedGasSponsorSignature, 'string');
408+
// Verify signature array is populated
409+
tx.signature.length.should.equal(1);
410+
});
411+
412+
it('should serialize signatures in correct order', async function () {
413+
const txBuilder = factory.getTransferBuilder();
414+
txBuilder.sender(testData.sender.address);
415+
txBuilder.recipients(testData.recipients);
416+
txBuilder.paymentObjects(testData.paymentObjects);
417+
txBuilder.gasData(testData.gasData);
418+
txBuilder.gasSponsor(testData.gasSponsor.address);
419+
420+
// Add signatures through builder
421+
txBuilder.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
422+
txBuilder.addGasSponsorSignature(
423+
testData.testGasSponsorSignature.publicKey,
424+
testData.testGasSponsorSignature.signature
425+
);
426+
427+
const tx = (await txBuilder.build()) as TransferTransaction;
428+
429+
// Verify signatures are in correct order: sender first, gas sponsor second
430+
tx.signature.length.should.equal(2);
431+
tx.signature[0].should.equal(tx.serializedSignature);
432+
tx.signature[1].should.equal(tx.serializedGasSponsorSignature);
433+
});
434+
435+
it('should fail to add invalid sender signature via builder', function () {
436+
const txBuilder = factory.getTransferBuilder();
437+
txBuilder.sender(testData.sender.address);
438+
txBuilder.recipients(testData.recipients);
439+
txBuilder.paymentObjects(testData.paymentObjects);
440+
txBuilder.gasData(testData.gasData);
441+
442+
// Builder should validate and throw when adding invalid signature
443+
should(() => txBuilder.addSignature({ pub: 'tooshort' }, testData.testSignature.signature)).throwError(
444+
'Invalid transaction signature'
445+
);
446+
});
447+
448+
it('should fail to add invalid gas sponsor signature via builder', function () {
449+
const txBuilder = factory.getTransferBuilder();
450+
txBuilder.sender(testData.sender.address);
451+
txBuilder.recipients(testData.recipients);
452+
txBuilder.paymentObjects(testData.paymentObjects);
453+
txBuilder.gasData(testData.gasData);
454+
txBuilder.gasSponsor(testData.gasSponsor.address);
455+
456+
// Builder should validate and throw when adding invalid signature
457+
should(() =>
458+
txBuilder.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, Buffer.from('invalid'))
459+
).throwError('Invalid transaction signature');
460+
});
461+
});
309462
});

0 commit comments

Comments
 (0)