Skip to content

Commit 8d7f0dc

Browse files
asimm241zone117x
authored andcommitted
feat: add implementation of rosetta construction/combine endpoint
1 parent 30df628 commit 8d7f0dc

File tree

3 files changed

+141
-3
lines changed

3 files changed

+141
-3
lines changed

src/api/rosetta-constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,21 @@ export const RosettaErrors: Record<string, RosettaError> = {
207207
message: 'Public key not available',
208208
retriable: false,
209209
},
210+
noSignatures: {
211+
code: 633,
212+
message: 'no signature found',
213+
retriable: false,
214+
},
215+
invalidSignature: {
216+
code: 634,
217+
message: 'Invalid Signature',
218+
retriable: false,
219+
},
220+
signatureNotVerified: {
221+
code: 635,
222+
message: 'Signature(s) not verified with this public key(s)',
223+
retriable: false,
224+
},
210225
};
211226

212227
// All request types, used to validate input.

src/api/routes/rosetta/construction.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,35 @@ import {
1515
RosettaConstructionPreprocessRequest,
1616
RosettaConstructionMetadataRequest,
1717
RosettaConstructionPayloadResponse,
18+
RosettaConstructionCombineRequest,
19+
RosettaConstructionCombineResponse,
1820
} from '@blockstack/stacks-blockchain-api-types';
1921
import {
22+
createMessageSignature,
23+
createTransactionAuthField,
2024
emptyMessageSignature,
2125
isSingleSig,
26+
MessageSignature,
2227
} from '@blockstack/stacks-transactions/lib/authorization';
2328
import { BufferReader } from '@blockstack/stacks-transactions/lib/bufferReader';
24-
import { deserializeTransaction } from '@blockstack/stacks-transactions/lib/transaction';
29+
import {
30+
deserializeTransaction,
31+
StacksTransaction,
32+
} from '@blockstack/stacks-transactions/lib/transaction';
2533
import {
2634
UnsignedTokenTransferOptions,
2735
makeUnsignedSTXTokenTransfer,
2836
} from '@blockstack/stacks-transactions';
2937
import * as express from 'express';
3038
import { StacksCoreRpcClient } from '../../../core-rpc/client';
3139
import { DataStore, DbBlock } from '../../../datastore/common';
32-
import { FoundOrNot, hexToBuffer, isValidC32Address, digestSha512_256 } from '../../../helpers';
40+
import {
41+
FoundOrNot,
42+
hexToBuffer,
43+
isValidC32Address,
44+
digestSha512_256,
45+
has0xPrefix,
46+
} from '../../../helpers';
3347
import { RosettaConstants, RosettaErrors } from '../../rosetta-constants';
3448
import {
3549
bitcoinAddressToSTXAddress,
@@ -43,6 +57,8 @@ import {
4357
rawTxToBaseTx,
4458
rawTxToStacksTransaction,
4559
GetStacksTestnetNetwork,
60+
makePresignHash,
61+
verifySignature,
4662
} from './../../../rosetta-helpers';
4763
import { makeRosettaError, rosettaValidateRequest, ValidSchema } from './../../rosetta-validate';
4864

@@ -405,7 +421,77 @@ export function createRosettaConstructionRouter(db: DataStore): RouterWithAsync
405421
});
406422

407423
//construction/combine endpoint
408-
router.postAsync('combine', async (req, res) => {});
424+
router.postAsync('/combine', async (req, res) => {
425+
const valid: ValidSchema = await rosettaValidateRequest(req.originalUrl, req.body);
426+
if (!valid.valid) {
427+
res.status(400).json(makeRosettaError(valid));
428+
return;
429+
}
430+
431+
const combineRequest: RosettaConstructionCombineRequest = req.body;
432+
const signatures = combineRequest.signatures;
433+
434+
if (has0xPrefix(combineRequest.unsigned_transaction)) {
435+
res.status(400).json(RosettaErrors.invalidTransactionString);
436+
return;
437+
}
438+
439+
if (signatures.length === 0) {
440+
res.status(400).json(RosettaErrors.noSignatures);
441+
return;
442+
}
443+
444+
let unsigned_transaction_buffer: Buffer;
445+
let transaction: StacksTransaction;
446+
447+
try {
448+
unsigned_transaction_buffer = hexToBuffer('0x' + combineRequest.unsigned_transaction);
449+
transaction = deserializeTransaction(BufferReader.fromBuffer(unsigned_transaction_buffer));
450+
} catch (e) {
451+
res.status(400).json(RosettaErrors.invalidTransactionString);
452+
return;
453+
}
454+
455+
for (const signature of signatures) {
456+
if (signature.public_key.curve_type !== 'secp256k1') {
457+
res.status(400).json(RosettaErrors.invalidCurveType);
458+
return;
459+
}
460+
const preSignHash = makePresignHash(transaction);
461+
if (!preSignHash) {
462+
res.status(400).json(RosettaErrors.invalidTransactionString);
463+
return;
464+
}
465+
466+
let newSignature: MessageSignature;
467+
468+
try {
469+
newSignature = createMessageSignature(signature.signing_payload.hex_bytes);
470+
} catch (error) {
471+
res.status(400).json(RosettaErrors.invalidSignature);
472+
return;
473+
}
474+
475+
if (!verifySignature(preSignHash, signature.public_key.hex_bytes, newSignature)) {
476+
res.status(400).json(RosettaErrors.signatureNotVerified);
477+
}
478+
479+
if (transaction.auth.spendingCondition && isSingleSig(transaction.auth.spendingCondition)) {
480+
transaction.auth.spendingCondition.signature = newSignature;
481+
} else {
482+
const authField = createTransactionAuthField(newSignature);
483+
transaction.auth.spendingCondition?.fields.push(authField);
484+
}
485+
}
486+
487+
const serializedTx = transaction.serialize().toString('hex');
488+
489+
const combineResponse: RosettaConstructionCombineResponse = {
490+
signed_transaction: serializedTx,
491+
};
492+
493+
res.status(200).json(combineResponse);
494+
});
409495

410496
return router;
411497
}

src/rosetta-helpers.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@ import {
1414
import {
1515
emptyMessageSignature,
1616
isSingleSig,
17+
createMessageSignature,
18+
makeSigHashPreSign,
19+
MessageSignature,
1720
} from '@blockstack/stacks-transactions/lib/authorization';
1821
import { BufferReader } from '@blockstack/stacks-transactions/lib/bufferReader';
1922
import {
2023
deserializeTransaction,
2124
StacksTransaction,
2225
} from '@blockstack/stacks-transactions/lib/transaction';
26+
27+
import { parseRecoverableSignature } from '@blockstack/stacks-transactions';
28+
import { ec as EC } from 'elliptic';
29+
2330
import { txidFromData } from '@blockstack/stacks-transactions/lib/utils';
2431
import * as btc from 'bitcoinjs-lib';
2532
import * as c32check from 'c32check';
@@ -415,3 +422,33 @@ export function GetStacksTestnetNetwork() {
415422
stacksNetwork.coreApiUrl = `http://${getCoreNodeEndpoint()}`;
416423
return stacksNetwork;
417424
}
425+
426+
export function verifySignature(
427+
message: string,
428+
publicAddress: string,
429+
signature: MessageSignature
430+
): boolean {
431+
const { r, s } = parseRecoverableSignature(signature.data);
432+
433+
try {
434+
const ec = new EC('secp256k1');
435+
const publicKeyPair = ec.keyFromPublic(publicAddress, 'hex'); // use the accessible public key to verify the signature
436+
const isVerified = publicKeyPair.verify(message, { r, s });
437+
return isVerified;
438+
} catch (error) {
439+
return false;
440+
}
441+
}
442+
443+
export function makePresignHash(transaction: StacksTransaction): string | undefined {
444+
if (!transaction.auth.authType || !transaction.auth.spendingCondition?.nonce) {
445+
return undefined;
446+
}
447+
448+
return makeSigHashPreSign(
449+
transaction.verifyBegin(),
450+
transaction.auth.authType,
451+
transaction.auth.spendingCondition?.fee,
452+
transaction.auth.spendingCondition?.nonce
453+
);
454+
}

0 commit comments

Comments
 (0)