Skip to content

Commit

Permalink
feat: add implementation of rosetta construction/combine endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
asimm241 authored and zone117x committed Oct 12, 2020
1 parent 30df628 commit 8d7f0dc
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 3 deletions.
15 changes: 15 additions & 0 deletions src/api/rosetta-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,21 @@ export const RosettaErrors: Record<string, RosettaError> = {
message: 'Public key not available',
retriable: false,
},
noSignatures: {
code: 633,
message: 'no signature found',
retriable: false,
},
invalidSignature: {
code: 634,
message: 'Invalid Signature',
retriable: false,
},
signatureNotVerified: {
code: 635,
message: 'Signature(s) not verified with this public key(s)',
retriable: false,
},
};

// All request types, used to validate input.
Expand Down
92 changes: 89 additions & 3 deletions src/api/routes/rosetta/construction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,35 @@ import {
RosettaConstructionPreprocessRequest,
RosettaConstructionMetadataRequest,
RosettaConstructionPayloadResponse,
RosettaConstructionCombineRequest,
RosettaConstructionCombineResponse,
} from '@blockstack/stacks-blockchain-api-types';
import {
createMessageSignature,
createTransactionAuthField,
emptyMessageSignature,
isSingleSig,
MessageSignature,
} from '@blockstack/stacks-transactions/lib/authorization';
import { BufferReader } from '@blockstack/stacks-transactions/lib/bufferReader';
import { deserializeTransaction } from '@blockstack/stacks-transactions/lib/transaction';
import {
deserializeTransaction,
StacksTransaction,
} from '@blockstack/stacks-transactions/lib/transaction';
import {
UnsignedTokenTransferOptions,
makeUnsignedSTXTokenTransfer,
} from '@blockstack/stacks-transactions';
import * as express from 'express';
import { StacksCoreRpcClient } from '../../../core-rpc/client';
import { DataStore, DbBlock } from '../../../datastore/common';
import { FoundOrNot, hexToBuffer, isValidC32Address, digestSha512_256 } from '../../../helpers';
import {
FoundOrNot,
hexToBuffer,
isValidC32Address,
digestSha512_256,
has0xPrefix,
} from '../../../helpers';
import { RosettaConstants, RosettaErrors } from '../../rosetta-constants';
import {
bitcoinAddressToSTXAddress,
Expand All @@ -43,6 +57,8 @@ import {
rawTxToBaseTx,
rawTxToStacksTransaction,
GetStacksTestnetNetwork,
makePresignHash,
verifySignature,
} from './../../../rosetta-helpers';
import { makeRosettaError, rosettaValidateRequest, ValidSchema } from './../../rosetta-validate';

Expand Down Expand Up @@ -405,7 +421,77 @@ export function createRosettaConstructionRouter(db: DataStore): RouterWithAsync
});

//construction/combine endpoint
router.postAsync('combine', async (req, res) => {});
router.postAsync('/combine', async (req, res) => {
const valid: ValidSchema = await rosettaValidateRequest(req.originalUrl, req.body);
if (!valid.valid) {
res.status(400).json(makeRosettaError(valid));
return;
}

const combineRequest: RosettaConstructionCombineRequest = req.body;
const signatures = combineRequest.signatures;

if (has0xPrefix(combineRequest.unsigned_transaction)) {
res.status(400).json(RosettaErrors.invalidTransactionString);
return;
}

if (signatures.length === 0) {
res.status(400).json(RosettaErrors.noSignatures);
return;
}

let unsigned_transaction_buffer: Buffer;
let transaction: StacksTransaction;

try {
unsigned_transaction_buffer = hexToBuffer('0x' + combineRequest.unsigned_transaction);
transaction = deserializeTransaction(BufferReader.fromBuffer(unsigned_transaction_buffer));
} catch (e) {
res.status(400).json(RosettaErrors.invalidTransactionString);
return;
}

for (const signature of signatures) {
if (signature.public_key.curve_type !== 'secp256k1') {
res.status(400).json(RosettaErrors.invalidCurveType);
return;
}
const preSignHash = makePresignHash(transaction);
if (!preSignHash) {
res.status(400).json(RosettaErrors.invalidTransactionString);
return;
}

let newSignature: MessageSignature;

try {
newSignature = createMessageSignature(signature.signing_payload.hex_bytes);
} catch (error) {
res.status(400).json(RosettaErrors.invalidSignature);
return;
}

if (!verifySignature(preSignHash, signature.public_key.hex_bytes, newSignature)) {
res.status(400).json(RosettaErrors.signatureNotVerified);
}

if (transaction.auth.spendingCondition && isSingleSig(transaction.auth.spendingCondition)) {
transaction.auth.spendingCondition.signature = newSignature;
} else {
const authField = createTransactionAuthField(newSignature);
transaction.auth.spendingCondition?.fields.push(authField);
}
}

const serializedTx = transaction.serialize().toString('hex');

const combineResponse: RosettaConstructionCombineResponse = {
signed_transaction: serializedTx,
};

res.status(200).json(combineResponse);
});

return router;
}
37 changes: 37 additions & 0 deletions src/rosetta-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ import {
import {
emptyMessageSignature,
isSingleSig,
createMessageSignature,
makeSigHashPreSign,
MessageSignature,
} from '@blockstack/stacks-transactions/lib/authorization';
import { BufferReader } from '@blockstack/stacks-transactions/lib/bufferReader';
import {
deserializeTransaction,
StacksTransaction,
} from '@blockstack/stacks-transactions/lib/transaction';

import { parseRecoverableSignature } from '@blockstack/stacks-transactions';
import { ec as EC } from 'elliptic';

import { txidFromData } from '@blockstack/stacks-transactions/lib/utils';
import * as btc from 'bitcoinjs-lib';
import * as c32check from 'c32check';
Expand Down Expand Up @@ -415,3 +422,33 @@ export function GetStacksTestnetNetwork() {
stacksNetwork.coreApiUrl = `http://${getCoreNodeEndpoint()}`;
return stacksNetwork;
}

export function verifySignature(
message: string,
publicAddress: string,
signature: MessageSignature
): boolean {
const { r, s } = parseRecoverableSignature(signature.data);

try {
const ec = new EC('secp256k1');
const publicKeyPair = ec.keyFromPublic(publicAddress, 'hex'); // use the accessible public key to verify the signature
const isVerified = publicKeyPair.verify(message, { r, s });
return isVerified;
} catch (error) {
return false;
}
}

export function makePresignHash(transaction: StacksTransaction): string | undefined {
if (!transaction.auth.authType || !transaction.auth.spendingCondition?.nonce) {
return undefined;
}

return makeSigHashPreSign(
transaction.verifyBegin(),
transaction.auth.authType,
transaction.auth.spendingCondition?.fee,
transaction.auth.spendingCondition?.nonce
);
}

0 comments on commit 8d7f0dc

Please sign in to comment.