Skip to content

Commit f5c4da7

Browse files
authored
feat: [CPU optimizations] use native rust module for decoding Clarity values, binary transaction blobs, post-condition binary blobs, Stacks addresses (#1094)
* feat: use native nodejs addon module for Stacks binary encoding and decoding (Clarity values, binary wire formats, addresses, etc) * refactor: move transaction, post-condition, and clarity value decoding from js to native addon * chore: fix rosetta pox contract-call parsing * chore: use npm registry for stacks-encoding-native-js * chore: fix unused export * chore: commit dev file to be converted to unit tests * refactor: use stacks-encoding-native lib for stacks-address operations * chore: bump stacks-encoding-native lib * chore: stacks-native-encoding version bump with fix for decoding multisig tx sender address * chore: stacks-native-encoding version bump with fix for decoding zero-length prefixed contract call args * test: introduce tests for on-btc-chain originating stx tx processing * chore: prettier ignore json payload sample files * chore: clear up type import syntax * test: tests for on-btc-chain originating stx-lock tx processing * feat: expand cpu profiling capabilties with distinct start and stop endpoints easier to use in scripts * chore: fix compilation error in /utils * feat: support for performing cpu profiling with the native addon * fix: avoid re-injecting sourcemap support when running with ts-node * chore: updates for latest stacks-native-encoding library function usage, only decode clarity value type signatures when needed * chore: decode only clarity value repr string rather than deep value in several areas * chore: fix package-lock.json * feat: commit load testing script with sampled endpoint list * chore: fix synthetic-stx-txs-tests * chore: update to latest stacks-encoding-native types (using hex strings rather than js buffers) * chore: bump stacks-encoding-native version * chore: fix regular integration tests * chore: fix bug with rosetta principal repr parsing * chore: bump stacks-encoding-native version * chore: add several TODO(perf) comments where buffers should be hex strings * chore: remove stacks-native-encoding debug code that have since been converted into unit tests * chore: remove debug code * fix: bug fix for reverse file stream bug if read chunk starts with line-ending * chore: wrap synthetic stx txs tests
1 parent 6b0c0b0 commit f5c4da7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4882
-2672
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ module.exports = {
3030

3131
// TODO: temporarily disable this until the express async handler is typed correctly
3232
'@typescript-eslint/no-misused-promises': 'off',
33+
'@typescript-eslint/no-unsafe-argument': 'off',
3334
},
3435
};

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ src/migrations/*
33
src/tests-rosetta/
44
src/tests-rosetta-cli/
55
src/tests-bns/
6+
src/tests/synthetic-tx-payloads/

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@
235235
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
236236
"cwd": "${workspaceFolder}/utils",
237237
"args": ["${workspaceFolder}/utils/src/chaintip-cache-control-test.ts"]
238-
}
238+
},
239239
],
240240
"compounds": [
241241
{

package-lock.json

Lines changed: 1340 additions & 1294 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
"start": "node ./lib/index.js",
2828
"lint": "npm run lint:eslint && npm run lint:prettier",
2929
"lint:unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=src/migrations/*",
30-
"lint:eslint": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe",
30+
"lint:eslint": "eslint . --ext .js,.jsx,.ts,.tsx -f unix",
3131
"lint:prettier": "prettier --check src/**/*.{ts,json}",
32-
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe --fix && prettier --write --check src/**/*.{ts,json}",
32+
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f unix --fix && prettier --write --check src/**/*.{ts,json}",
3333
"migrate": "node-pg-migrate -m src/migrations",
3434
"devenv:build": "docker-compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml -f docker/docker-compose.dev.bitcoind.yml -f docker/docker-compose.dev.rosetta-cli.yml build --no-cache",
3535
"devenv:deploy": "docker-compose -f docker/docker-compose.dev.postgres.yml -f docker/docker-compose.dev.stacks-blockchain.yml -f docker/docker-compose.dev.bitcoind.yml up",
@@ -139,9 +139,10 @@
139139
"socket.io": "4.4.0",
140140
"source-map-support": "0.5.21",
141141
"split2": "3.2.2",
142+
"stacks-encoding-native-js": "0.1.0-alpha.9",
142143
"strict-event-emitter-types": "2.0.0",
143144
"ts-unused-exports": "7.0.3",
144-
"typescript": "4.5.4",
145+
"typescript": "4.6.2",
145146
"uuid": "8.3.2",
146147
"winston": "3.3.3",
147148
"ws": "7.5.6",
@@ -155,15 +156,15 @@
155156
"@commitlint/cli": "9.1.2",
156157
"@commitlint/config-conventional": "10.0.0",
157158
"@stacks/eslint-config": "1.2.0",
158-
"@stacks/prettier-config": "0.0.7",
159+
"@stacks/prettier-config": "0.0.10",
159160
"@types/ajv": "1.0.0",
160161
"@types/bluebird": "3.5.36",
161162
"@types/bn.js": "4.11.6",
162163
"@types/cors": "2.8.12",
163164
"@types/dotenv-flow": "3.2.0",
164165
"@types/express": "4.17.13",
165166
"@types/is-ci": "3.0.0",
166-
"@types/jest": "27.0.3",
167+
"@types/jest": "27.4.1",
167168
"@types/node": "16.11.17",
168169
"@types/node-fetch": "2.5.12",
169170
"@types/pg": "7.14.11",
@@ -172,17 +173,17 @@
172173
"@types/split2": "2.1.6",
173174
"@types/supertest": "2.0.11",
174175
"@types/uuid": "7.0.5",
175-
"@typescript-eslint/eslint-plugin": "4.33.0",
176-
"@typescript-eslint/parser": "4.33.0",
176+
"@typescript-eslint/eslint-plugin": "5.15.0",
177+
"@typescript-eslint/parser": "5.15.0",
177178
"concurrently": "6.5.1",
178179
"docker-compose": "0.23.14",
179-
"eslint": "7.32.0",
180-
"eslint-config-prettier": "8.3.0",
181-
"eslint-plugin-prettier": "3.4.1",
180+
"eslint": "8.11.0",
181+
"eslint-config-prettier": "8.5.0",
182+
"eslint-plugin-prettier": "4.0.0",
182183
"eslint-plugin-tsdoc": "0.2.14",
183184
"husky": "4.3.8",
184185
"is-ci": "3.0.1",
185-
"jest": "27.4.5",
186+
"jest": "27.5.1",
186187
"nock": "13.2.1",
187188
"nodemon": "2.0.15",
188189
"pg-connection-string": "2.5.0",
@@ -191,8 +192,8 @@
191192
"rpc-websocket-client": "1.1.4",
192193
"socket.io-client": "4.4.0",
193194
"supertest": "4.0.2",
194-
"ts-jest": "27.1.2",
195-
"ts-node": "9.1.1",
195+
"ts-jest": "27.1.3",
196+
"ts-node": "10.7.0",
196197
"why-is-node-running": "2.2.0"
197198
},
198199
"optionalDependencies": {

src/api/controllers/db-controller.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import {
22
abiFunctionToString,
3-
BufferReader,
43
ClarityAbi,
54
ClarityAbiFunction,
6-
cvToString,
7-
deserializeCV,
85
getTypeString,
9-
serializeCV,
106
} from '@stacks/transactions';
7+
import {
8+
decodeClarityValueList,
9+
decodeClarityValueToRepr,
10+
decodeClarityValueToTypeName,
11+
decodePostConditions,
12+
} from 'stacks-encoding-native-js';
1113

1214
import {
1315
AbstractMempoolTransaction,
@@ -22,6 +24,7 @@ import {
2224
MempoolTransactionStatus,
2325
Microblock,
2426
PoisonMicroblockTransactionMetadata,
27+
PostCondition,
2528
RosettaBlock,
2629
RosettaParentBlockIdentifier,
2730
RosettaTransaction,
@@ -72,7 +75,6 @@ import {
7275
unixEpochToIso,
7376
EMPTY_HASH_256,
7477
} from '../../helpers';
75-
import { readClarityValueArray, readTransactionPostConditions } from '../../p2p/tx';
7678
import { serializePostCondition, serializePostConditionMode } from '../serializers/post-conditions';
7779
import { getOperations, parseTransactionMemo, processUnlockingEvents } from '../../rosetta-helpers';
7880

@@ -214,16 +216,18 @@ export function parseDbEvent(dbEvent: DbEvent): TransactionEvent {
214216
switch (dbEvent.event_type) {
215217
case DbEventTypeId.SmartContractLog: {
216218
const valueBuffer = dbEvent.value;
217-
const valueHex = bufferToHexPrefixString(valueBuffer);
218-
const valueRepr = cvToString(deserializeCV(valueBuffer));
219+
const parsedClarityValue = decodeClarityValueToRepr(valueBuffer);
219220
const event: TransactionEventSmartContractLog = {
220221
event_index: dbEvent.event_index,
221222
event_type: 'smart_contract_log',
222223
tx_id: dbEvent.tx_id,
223224
contract_log: {
224225
contract_id: dbEvent.contract_identifier,
225226
topic: dbEvent.topic,
226-
value: { hex: valueHex, repr: valueRepr },
227+
value: {
228+
hex: bufferToHexPrefixString(valueBuffer),
229+
repr: parsedClarityValue,
230+
},
227231
},
228232
};
229233
return event;
@@ -272,8 +276,7 @@ export function parseDbEvent(dbEvent: DbEvent): TransactionEvent {
272276
}
273277
case DbEventTypeId.NonFungibleTokenAsset: {
274278
const valueBuffer = dbEvent.value;
275-
const valueHex = bufferToHexPrefixString(valueBuffer);
276-
const valueRepr = cvToString(deserializeCV(valueBuffer));
279+
const parsedClarityValue = decodeClarityValueToRepr(valueBuffer);
277280
const event: TransactionEventNonFungibleAsset = {
278281
event_index: dbEvent.event_index,
279282
event_type: 'non_fungible_token_asset',
@@ -284,8 +287,8 @@ export function parseDbEvent(dbEvent: DbEvent): TransactionEvent {
284287
sender: dbEvent.sender || '',
285288
recipient: dbEvent.recipient || '',
286289
value: {
287-
hex: valueHex,
288-
repr: valueRepr,
290+
hex: bufferToHexPrefixString(valueBuffer),
291+
repr: parsedClarityValue,
289292
},
290293
},
291294
};
@@ -641,13 +644,10 @@ interface GetTxWithEventsArgs extends GetTxArgs {
641644
}
642645

643646
function parseDbBaseTx(dbTx: DbTx | DbMempoolTx): BaseTransaction {
644-
const postConditions =
645-
dbTx.post_conditions.byteLength > 2
646-
? readTransactionPostConditions(
647-
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
648-
).map(pc => serializePostCondition(pc))
649-
: [];
650-
647+
const decodedPostConditions = decodePostConditions(dbTx.post_conditions);
648+
const normalizedPostConditions = decodedPostConditions.post_conditions.map(pc =>
649+
serializePostCondition(pc)
650+
);
651651
const tx: BaseTransaction = {
652652
tx_id: dbTx.tx_id,
653653
nonce: dbTx.nonce,
@@ -656,8 +656,8 @@ function parseDbBaseTx(dbTx: DbTx | DbMempoolTx): BaseTransaction {
656656
sender_address: dbTx.sender_address,
657657
sponsored: dbTx.sponsored,
658658
sponsor_address: dbTx.sponsor_address,
659-
post_condition_mode: serializePostConditionMode(dbTx.post_conditions.readUInt8(0)),
660-
post_conditions: postConditions,
659+
post_condition_mode: serializePostConditionMode(decodedPostConditions.post_condition_mode),
660+
post_conditions: normalizedPostConditions,
661661
anchor_mode: getTxAnchorModeString(dbTx.anchor_mode),
662662
};
663663
return tx;
@@ -752,31 +752,30 @@ export function parseContractCallMetadata(tx: BaseTx): ContractCallTransactionMe
752752
throw new Error(`Could not find function name "${functionName}" in ABI for ${contractId}`);
753753
}
754754
}
755+
756+
const functionArgs = tx.contract_call_function_args
757+
? decodeClarityValueList(tx.contract_call_function_args).map((c, fnArgIndex) => {
758+
const functionArgAbi = functionAbi
759+
? functionAbi.args[fnArgIndex++]
760+
: { name: '', type: undefined };
761+
return {
762+
hex: c.hex,
763+
repr: c.repr,
764+
name: functionArgAbi.name,
765+
type: functionArgAbi.type
766+
? getTypeString(functionArgAbi.type)
767+
: decodeClarityValueToTypeName(c.hex),
768+
};
769+
})
770+
: undefined;
771+
755772
const metadata: ContractCallTransactionMetadata = {
756773
tx_type: 'contract_call',
757774
contract_call: {
758775
contract_id: contractId,
759776
function_name: functionName,
760777
function_signature: functionAbi ? abiFunctionToString(functionAbi) : '',
761-
function_args: tx.contract_call_function_args
762-
? readClarityValueArray(tx.contract_call_function_args).map((c, fnArgIndex) => {
763-
const functionArgAbi = functionAbi
764-
? functionAbi.args[fnArgIndex++]
765-
: { name: '', type: undefined };
766-
return {
767-
hex: bufferToHexPrefixString(serializeCV(c)),
768-
repr: cvToString(c),
769-
name: functionArgAbi.name,
770-
// TODO: This stacks.js function throws when given an empty `list` clarity value.
771-
// This is only used to provide function signature type information if the contract
772-
// ABI is unavailable, which should only happen during rare re-org situations.
773-
// Typically this will be filled in with more accurate type data in a later step before
774-
// being sent to client.
775-
// type: getCVTypeString(c),
776-
type: functionArgAbi.type ? getTypeString(functionArgAbi.type) : '',
777-
};
778-
})
779-
: undefined,
778+
function_args: functionArgs,
780779
},
781780
};
782781
return metadata;
@@ -799,7 +798,7 @@ function parseDbAbstractTx(dbTx: DbTx, baseTx: BaseTransaction): AbstractTransac
799798
tx_status: getTxStatusString(dbTx.status) as TransactionStatus,
800799
tx_result: {
801800
hex: dbTx.raw_result,
802-
repr: cvToString(deserializeCV(hexToBuffer(dbTx.raw_result))),
801+
repr: decodeClarityValueToRepr(dbTx.raw_result),
803802
},
804803
microblock_hash: dbTx.microblock_hash,
805804
microblock_sequence: dbTx.microblock_sequence,

src/api/routes/address.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ import {
3939
AddressTransactionsWithTransfersListResponse,
4040
AddressNonces,
4141
} from '@stacks/stacks-blockchain-api-types';
42-
import { ChainID, cvToString, deserializeCV } from '@stacks/transactions';
42+
import { ChainID } from '@stacks/transactions';
43+
import { decodeClarityValueToRepr } from 'stacks-encoding-native-js';
4344
import { validate } from '../validate';
4445
import { NextFunction, Request, Response } from 'express';
4546
import {
@@ -354,13 +355,12 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): express.Ro
354355
recipient: transfer.recipient,
355356
})),
356357
nft_transfers: entry.nft_transfers.map(transfer => {
357-
const valueHex = bufferToHexPrefixString(transfer.value);
358-
const valueRepr = cvToString(deserializeCV(transfer.value));
358+
const parsedClarityValue = decodeClarityValueToRepr(transfer.value);
359359
const nftTransfer = {
360360
asset_identifier: transfer.asset_identifier,
361361
value: {
362-
hex: valueHex,
363-
repr: valueRepr,
362+
hex: bufferToHexPrefixString(transfer.value),
363+
repr: parsedClarityValue,
364364
},
365365
sender: transfer.sender,
366366
recipient: transfer.recipient,
@@ -497,17 +497,21 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): express.Ro
497497
blockHeight,
498498
includeUnanchored,
499499
});
500-
const nft_events = response.results.map(row => ({
501-
sender: row.sender,
502-
recipient: row.recipient,
503-
asset_identifier: row.asset_identifier,
504-
value: {
505-
hex: bufferToHexPrefixString(row.value),
506-
repr: cvToString(deserializeCV(row.value)),
507-
},
508-
tx_id: bufferToHexPrefixString(row.tx_id),
509-
block_height: row.block_height,
510-
}));
500+
const nft_events = response.results.map(row => {
501+
const parsedClarityValue = decodeClarityValueToRepr(row.value);
502+
const r = {
503+
sender: row.sender,
504+
recipient: row.recipient,
505+
asset_identifier: row.asset_identifier,
506+
value: {
507+
hex: bufferToHexPrefixString(row.value),
508+
repr: parsedClarityValue,
509+
},
510+
tx_id: bufferToHexPrefixString(row.tx_id),
511+
block_height: row.block_height,
512+
};
513+
return r;
514+
});
511515
const nftListResponse: AddressNftListResponse = {
512516
nft_events: nft_events,
513517
total: response.total,

src/api/routes/debug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as express from 'express';
22
import * as BN from 'bn.js';
33
import * as btc from 'bitcoinjs-lib';
4-
import * as c32check from 'c32check';
4+
import { stacksToBitcoinAddress } from 'stacks-encoding-native-js';
55
import * as bodyParser from 'body-parser';
66
import { asyncHandler } from '../async-handler';
77
import { htmlEscape } from 'escape-goat';
@@ -581,7 +581,7 @@ export function createDebugRouter(db: DataStore): express.Router {
581581
);
582582
}
583583
const [contractAddress, contractName] = poxInfo.contract_id.split('.');
584-
const btcAddr = c32check.c32ToB58(sender.stacksAddress);
584+
const btcAddr = stacksToBitcoinAddress(sender.stacksAddress);
585585
const { hashMode, data } = convertBTCAddress(btcAddr);
586586
const cycles = 3;
587587
const txOptions: SignedContractCallOptions = {

src/api/routes/rosetta/construction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ import {
6161
RosettaOperationType,
6262
} from '../../rosetta-constants';
6363
import {
64-
bitcoinAddressToSTXAddress,
6564
getOperations,
6665
getOptionsFromOperations,
6766
getSigners,
@@ -77,6 +76,7 @@ import {
7776
parseTransactionMemo,
7877
} from './../../../rosetta-helpers';
7978
import { makeRosettaError, rosettaValidateRequest, ValidSchema } from './../../rosetta-validate';
79+
import { bitcoinToStacksAddress } from 'stacks-encoding-native-js';
8080

8181
export function createRosettaConstructionRouter(db: DataStore, chainId: ChainID): express.Router {
8282
const router = express.Router();
@@ -109,7 +109,7 @@ export function createRosettaConstructionRouter(db: DataStore, chainId: ChainID)
109109
res.status(400).json(RosettaErrors[RosettaErrorsTypes.invalidPublicKey]);
110110
return;
111111
}
112-
const stxAddress = bitcoinAddressToSTXAddress(btcAddress);
112+
const stxAddress = bitcoinToStacksAddress(btcAddress);
113113

114114
const accountIdentifier: RosettaAccountIdentifier = {
115115
address: stxAddress,

0 commit comments

Comments
 (0)