Skip to content

Commit 89cbdbb

Browse files
committed
refactor(sdk-coin-flrp): simplify signature recovery and verification methods
Ticket: WIN-8452
1 parent 85ccc96 commit 89cbdbb

File tree

6 files changed

+80
-38
lines changed

6 files changed

+80
-38
lines changed

modules/sdk-coin-flr/src/flr.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
TransactionExplanation,
2727
Entry,
2828
} from '@bitgo/sdk-core';
29-
import { BaseCoin as StaticsBaseCoin, coins, FlareNetwork } from '@bitgo/statics';
29+
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
3030
import {
3131
AbstractEthLikeNewCoins,
3232
optionalDeps,
@@ -157,10 +157,7 @@ export class Flr extends AbstractEthLikeNewCoins {
157157
const tx = await txBuilder.build();
158158
const payload = tx.signablePayload;
159159
const signatures = tx.signature.map((s) => Buffer.from(FlrPLib.Utils.removeHexPrefix(s), 'hex'));
160-
const network = _.get(tx, '_network');
161-
const recoverPubkey = signatures.map((s) =>
162-
FlrPLib.Utils.recoverySignature(network as unknown as FlareNetwork, payload, s)
163-
);
160+
const recoverPubkey = signatures.map((s) => FlrPLib.Utils.recoverySignature(payload, s));
164161
const expectedSenders = recoverPubkey.map((r) => pubToAddress(r, true));
165162
const senders = tx.inputs.map((i) => FlrPLib.Utils.parseAddress(i.address));
166163
return expectedSenders.every((e) => senders.some((sender) => e.equals(sender)));

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,8 @@ export class Flrp extends BaseCoin {
323323
}
324324
}
325325

326-
recoverySignature(message: Buffer, signature: Buffer): Buffer {
327-
return FlrpLib.Utils.recoverySignature(this._staticsCoin.network as FlareNetwork, message, signature);
326+
recoverySignature(messageHash: Buffer, signature: Buffer): Buffer {
327+
return FlrpLib.Utils.recoverySignature(messageHash, signature);
328328
}
329329

330330
async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ export class Transaction extends BaseTransaction {
237237
// avaxp P-chain: transaction.ts uses addChecksum() explicitly
238238
// avaxp C-chain: deprecatedTransaction.ts uses Tx.toStringHex() which internally adds checksum
239239
const rawTx = FlareUtils.bufferToHex(utils.addChecksum(signedTxBytes));
240-
console.log('rawTx in toBroadcastFormat:', rawTx);
241240
return rawTx;
242241
}
243242

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,13 @@ export class Utils implements BaseUtils {
151151

152152
/**
153153
* Verifies a signature
154+
* @param messageHash - The SHA256 hash of the message (e.g., signablePayload)
155+
* @param signature - The 64-byte signature (without recovery parameter)
156+
* @param publicKey - The public key to verify against
157+
* @returns true if signature is valid
154158
*/
155-
verifySignature(network: FlareNetwork, message: Buffer, signature: Buffer, publicKey: Buffer): boolean {
159+
verifySignature(messageHash: Buffer, signature: Buffer, publicKey: Buffer): boolean {
156160
try {
157-
const messageHash = this.sha256(message);
158161
return ecc.verify(messageHash, publicKey, signature);
159162
} catch (e) {
160163
return false;
@@ -362,17 +365,13 @@ export class Utils implements BaseUtils {
362365
}
363366

364367
/**
365-
* FlareJS wrapper to recover signature
366-
* @param network
367-
* @param message
368-
* @param signature
368+
* Recover public key from signature
369+
* @param messageHash - The SHA256 hash of the message (e.g., signablePayload)
370+
* @param signature - 65-byte signature (64 bytes signature + 1 byte recovery parameter)
369371
* @return recovered public key
370372
*/
371-
recoverySignature(network: FlareNetwork, message: Buffer, signature: Buffer): Buffer {
373+
recoverySignature(messageHash: Buffer, signature: Buffer): Buffer {
372374
try {
373-
// Hash the message first - must match the hash used in signing
374-
const messageHash = createHash('sha256').update(message).digest();
375-
376375
// Extract recovery parameter and signature
377376
if (signature.length !== 65) {
378377
throw new Error('Invalid signature length - expected 65 bytes (64 bytes signature + 1 byte recovery)');
@@ -382,6 +381,7 @@ export class Utils implements BaseUtils {
382381
const sigOnly = signature.slice(0, 64);
383382

384383
// Recover public key using the provided recovery parameter
384+
// messageHash should already be the SHA256 hash (signablePayload)
385385
const recovered = ecc.recoverPublicKey(messageHash, sigOnly, recoveryParam, true);
386386
if (!recovered) {
387387
throw new Error('Failed to recover public key');

modules/sdk-coin-flrp/test/unit/flrp.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,7 @@ describe('Flrp test cases', function () {
186186
const signature = await basecoin.signMessage(keys, messageToSign.toString('hex'));
187187

188188
const verify = FlrpLib.Utils.verifySignature(
189-
basecoin._staticsCoin.network,
190-
messageToSign,
189+
FlrpLib.Utils.sha256(messageToSign),
191190
signature.slice(0, 64), // Remove recovery byte for verification
192191
Buffer.from(pubKey, 'hex')
193192
);
@@ -551,12 +550,9 @@ describe('Flrp test cases', function () {
551550
it('should recover signature from signed message', async () => {
552551
const message = Buffer.from(SEED_ACCOUNT.message, 'utf8');
553552
const privateKey = Buffer.from(SEED_ACCOUNT.privateKey, 'hex');
554-
555-
// Create signature
556553
const signature = FlrpLib.Utils.createSignature(basecoin._staticsCoin.network, message, privateKey);
557-
558-
// Recover public key from signature
559-
const recoveredPubKey = basecoin.recoverySignature(message, signature);
554+
const messageHash = FlrpLib.Utils.sha256(message);
555+
const recoveredPubKey = basecoin.recoverySignature(messageHash, signature);
560556

561557
recoveredPubKey.should.be.instanceOf(Buffer);
562558
recoveredPubKey.length.should.equal(33);

modules/sdk-coin-flrp/test/unit/lib/utils.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IMPORT_IN_P } from '../../resources/transactionData/importInP';
1515
import { EXPORT_IN_P } from '../../resources/transactionData/exportInP';
1616
import { IMPORT_IN_C } from '../../resources/transactionData/importInC';
1717
import { TransactionBuilderFactory, Transaction } from '../../../src/lib';
18+
import { secp256k1, Address } from '@flarenetwork/flarejs';
1819

1920
describe('Utils', function () {
2021
let utils: Utils;
@@ -206,7 +207,8 @@ describe('Utils', function () {
206207
const signature = utils.createSignature(network, message, privateKey);
207208
const sigOnly = signature.slice(0, 64);
208209

209-
const isValid = utils.verifySignature(network, message, sigOnly, publicKey);
210+
const messageHash = utils.sha256(message);
211+
const isValid = utils.verifySignature(messageHash, sigOnly, publicKey);
210212
assert.strictEqual(isValid, true);
211213
});
212214

@@ -215,7 +217,8 @@ describe('Utils', function () {
215217
const publicKey = Buffer.from(SEED_ACCOUNT.publicKey, 'hex');
216218
const invalidSignature = Buffer.alloc(64);
217219

218-
const isValid = utils.verifySignature(network, message, invalidSignature, publicKey);
220+
const messageHash = utils.sha256(message);
221+
const isValid = utils.verifySignature(messageHash, invalidSignature, publicKey);
219222
assert.strictEqual(isValid, false);
220223
});
221224

@@ -480,11 +483,12 @@ describe('Utils', function () {
480483
const message = Buffer.from(SEED_ACCOUNT.message, 'utf8');
481484
const privateKey = Buffer.from(SEED_ACCOUNT.privateKey, 'hex');
482485

483-
// Create signature using the same private key
486+
// Create signature using the same private key (createSignature hashes the message internally)
484487
const signature = utils.createSignature(network, message, privateKey);
485488

486-
// Recover public key
487-
const recoveredPubKey = utils.recoverySignature(network, message, signature);
489+
// Recover public key - pass the hashed message since recoverySignature expects pre-hashed
490+
const messageHash = utils.sha256(message);
491+
const recoveredPubKey = utils.recoverySignature(messageHash, signature);
488492

489493
assert.ok(recoveredPubKey instanceof Buffer);
490494
assert.strictEqual(recoveredPubKey.length, 33); // Should be compressed public key (33 bytes)
@@ -495,8 +499,9 @@ describe('Utils', function () {
495499
const privateKey = Buffer.from(SEED_ACCOUNT.privateKey, 'hex');
496500
const signature = utils.createSignature(network, message, privateKey);
497501

498-
const pubKey1 = utils.recoverySignature(network, message, signature);
499-
const pubKey2 = utils.recoverySignature(network, message, signature);
502+
const messageHash = utils.sha256(message);
503+
const pubKey1 = utils.recoverySignature(messageHash, signature);
504+
const pubKey2 = utils.recoverySignature(messageHash, signature);
500505

501506
assert.deepStrictEqual(pubKey1, pubKey2);
502507
});
@@ -510,7 +515,8 @@ describe('Utils', function () {
510515

511516
// Create signature and recover public key
512517
const signature = utils.createSignature(network, message, privateKey);
513-
const recoveredPubKey = utils.recoverySignature(network, message, signature);
518+
const messageHash = utils.sha256(message);
519+
const recoveredPubKey = utils.recoverySignature(messageHash, signature);
514520

515521
// Convert both to hex strings for comparison
516522
assert.strictEqual(recoveredPubKey.toString('hex'), originalPubKey.toString('hex'));
@@ -523,23 +529,67 @@ describe('Utils', function () {
523529
const originalPubKey = Buffer.from(ecc.pointFromScalar(privateKey, true) as Uint8Array);
524530

525531
const signature = utils.createSignature(network, message, privateKey);
526-
const recoveredPubKey = utils.recoverySignature(network, message, signature);
532+
const messageHash = utils.sha256(message);
533+
const recoveredPubKey = utils.recoverySignature(messageHash, signature);
527534

528535
assert.strictEqual(recoveredPubKey.toString('hex'), originalPubKey.toString('hex'));
529536
});
530537

531538
it('should throw error for invalid signature length', function () {
532-
const message = Buffer.from(SEED_ACCOUNT.message, 'utf8');
539+
const messageHash = utils.sha256(Buffer.from(SEED_ACCOUNT.message, 'utf8'));
533540
const invalidSignature = Buffer.from(INVALID_SHORT_KEYPAIR_KEY, 'hex');
534541

535-
assert.throws(() => utils.recoverySignature(network, message, invalidSignature), /Failed to recover signature/);
542+
assert.throws(() => utils.recoverySignature(messageHash, invalidSignature), /Failed to recover signature/);
536543
});
537544

538545
it('should throw error for signature with invalid recovery parameter', function () {
539-
const message = Buffer.from(SEED_ACCOUNT.message, 'utf8');
546+
const messageHash = utils.sha256(Buffer.from(SEED_ACCOUNT.message, 'utf8'));
540547
const signature = Buffer.alloc(65); // Valid length but all zeros - invalid signature
541548

542-
assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/);
549+
assert.throws(() => utils.recoverySignature(messageHash, signature), /Failed to recover signature/);
550+
});
551+
552+
it('should recover signature and verify sender address from signed C-chain Export tx', async function () {
553+
// Transaction from actual build response - C-chain Export tx
554+
const tx =
555+
'0x0000000000010000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000012a96025ad506b9fbb9023fbdc1665c7f7d7c923f000000000605236658734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000006052340000000000000000000000002000000037fa8c7e0c8ad9f09f9179b42b77e94a487c3df758d4ba538f772333ca7bf3668a2fe36648438c79d9b6b77b56effb860eaa430e0e30c4e392f59cd08000000010000000900000001750076e67d9720283a71c6e7a9a88ff662608fefdd3f316f1211957ca1873eee3ee4a74b468bda66176a3e5d3ab54d43a8c0be12348f251a3093c16d9db00cd001c31e9c15';
556+
const expectedSenderAddress = '0x2a96025ad506b9fbb9023fbdc1665c7f7d7c923f';
557+
558+
const factory = new TransactionBuilderFactory(coins.get('tflrp'));
559+
const txn = (await factory.from(tx).build()) as Transaction;
560+
const signablePayload = txn.signablePayload;
561+
const signatures = txn.signature;
562+
const sig = Buffer.from(utils.removeHexPrefix(signatures[0]), 'hex');
563+
564+
// Recover public key from signature (signablePayload is already SHA256 hashed)
565+
const recoveredPubKey = utils.recoverySignature(signablePayload, sig);
566+
567+
// Get the sender address from the transaction inputs
568+
const txInputs = txn.inputs;
569+
const senderAddressFromTx = txInputs[0].address.toLowerCase();
570+
571+
// Verify sender address matches expected
572+
assert.strictEqual(
573+
senderAddressFromTx,
574+
expectedSenderAddress.toLowerCase(),
575+
'Transaction sender address does not match expected'
576+
);
577+
578+
// Derive address from recovered public key
579+
const derivedEvmAddress =
580+
'0x' + Buffer.from(new Address(secp256k1.publicKeyToEthAddress(recoveredPubKey)).toBytes()).toString('hex');
581+
582+
// Verify the recovered public key matches the sender
583+
assert.strictEqual(
584+
derivedEvmAddress.toLowerCase(),
585+
senderAddressFromTx,
586+
'Recovered public key does not match sender address'
587+
);
588+
589+
// Also verify signature validity
590+
const sigOnly = sig.slice(0, 64);
591+
const isValid = utils.verifySignature(signablePayload, sigOnly, recoveredPubKey);
592+
assert.strictEqual(isValid, true, 'Signature verification failed');
543593
});
544594
});
545595

0 commit comments

Comments
 (0)