@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
543 lines • 23.6 kB
JavaScript
"use strict";
// Copyright 2017-2025 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
Object.defineProperty(exports, "__esModule", { value: true });
exports.Trace = void 0;
const util_1 = require("@polkadot/util");
const http_errors_1 = require("http-errors");
const util_2 = require("../../../types/util");
const types_1 = require("./types");
const EMPTY_KEY_INFO = Object.freeze({
error: 'key not found',
});
/**
* Known spans that we actively look for in the operation creation logic.
*/
const SPANS = Object.freeze({
executeBlock: {
name: 'execute_block',
target: 'frame_executive',
},
applyExtrinsic: {
name: 'apply_extrinsic',
target: 'frame_executive',
},
});
/**
* Corresponds to Substrate `well_known_keys` module.
* ref: https://github.com/paritytech/substrate/blob/c93ef27486e5f14696e5b6d36edafea7936edbc8/primitives/storage/src/lib.rs#L157
*
* Note: not all keys are used, just here for completeness.
*/
const WELL_KNOWN_KEYS = Object.freeze({
'3a636f6465': {
special: ':code',
},
'3a686561707061676573': {
special: ':heappages',
},
'3a65787472696e7369635f696e646578': {
special: ':extrinsic_index',
},
'3a6368616e6765735f74726965': {
special: ':changes_trie',
},
'3a6368696c645f73746f726167653a': {
special: ':child_storage',
},
});
/**
* Class for processing traces from the `state_traceBlock` RPC endpoint.
*
* Assumptions:
* - Spans do not start in sorted order.
* - Events start in sorted order.
*
* For a conceptual overview on traces from block excution and `Operation` construction
* consult [this diagram.](https://docs.google.com/drawings/d/1vZoJo9jaXlz0LmrdTOgHck9_1LsfuQPRmTr-5g1tOis/edit?usp=sharing)
*/
class Trace {
constructor(api,
/**
* `state_traceBlock` RPC response `result`.
*/
traceBlock,
/**
* Type registry corresponding to the exact runtime the traces are from.
*/
registry) {
this.traceBlock = traceBlock;
this.registry = registry;
if (!traceBlock.spans.length) {
throw new http_errors_1.InternalServerError('No spans found. This runtime is likely not supported with tracing.');
}
this.keyNames = Trace.getKeyNames(api);
}
// In the future this will need a more chain specific solution that creates
// keys directly from metadata. https://github.com/paritytech/substrate-api-sidecar/issues/528
/**
* Get all the storage key names based on the ones built into `api`.
*
* @param api ApiPromise
*/
static getKeyNames(api) {
const moduleKeys = Object.keys(api.query).reduce((acc, mod) => {
Object.keys(api.query[mod]).forEach((item) => {
const queryObj = api.query[mod][item];
let key;
try {
// `slice(2)` is to slice off '0x' prefix
key = queryObj.key().slice(2);
}
catch {
key = queryObj.keyPrefix().slice(2);
}
acc[key.toString()] = {
module: mod,
item,
};
});
return acc;
}, {});
return { ...moduleKeys, ...WELL_KNOWN_KEYS };
}
/**
* Extract extrinsic indexes from all `:extrinsic_index` storage item read events.
*
* Note: Expects the given `spanIds` are from an apply extrinsic action group because
* apply extrinsic action groups should contain read events to `:extrinsic_index`.
*
* @param spanIds List of spanIds for an apply extrinsic action group
* @param extrinsicIndexBySpanId Map of spanId => extrinsicIndex
*/
static extractExtrinsicIndex(spanIds, extrinsicIndexBySpanId) {
// There are typically multiple reads of extrinsic index when applying an extrinsic,
// so we check that all those reads are the same number.
return spanIds.reduce((uniqExtIdx, id) => {
const extIdxToCheck = extrinsicIndexBySpanId.get(id);
if (uniqExtIdx === undefined && extIdxToCheck) {
uniqExtIdx = extIdxToCheck;
}
else if (extIdxToCheck && !(uniqExtIdx === null || uniqExtIdx === void 0 ? void 0 : uniqExtIdx.eq(extIdxToCheck))) {
// Since we assume the `spanIds` are for spans from an apply extrinsic action group,
// we assume there is always an `:extrinsic_index` event
throw new http_errors_1.InternalServerError('Expected at least one extrinsic index');
}
return uniqExtIdx;
}, undefined);
}
/**
* Find the Ids of all the spans which are descendant of the span `root`.
*
* @param root span which we want all the descendants of
* @param spansById map of span id => `SpanWithChildren`
*/
static findDescendants(root, spansById) {
var _a, _b;
// Note: traversal order doesn't matter here
const stack = (_a = spansById.get(root)) === null || _a === void 0 ? void 0 : _a.children;
if (!stack) {
return [];
}
const descendants = [];
while (stack.length) {
const curId = stack.pop();
if (!curId) {
break;
}
descendants.push(curId);
const curSpanChildren = (_b = spansById.get(curId)) === null || _b === void 0 ? void 0 : _b.children;
if (curSpanChildren) {
stack.push(...curSpanChildren);
}
}
return descendants;
}
/**
* Parses spans by
* 1) creating a Map of span's `id` to the the span with its children
* 2) creating an array with the same spans
*
* @param spans spans to parse
*/
static parseSpans(spans) {
const spansById = new Map();
const parsedSpans = [];
let executeBlockSpanId;
// This loop does 3 things: 1) build `spansById` 2) build `parseSpans` 3) finds
// `executeBlockSpanId`
for (const span of spans) {
const spanWithChildren = { ...span, children: [] };
if (span.name === SPANS.executeBlock.name && span.target === SPANS.executeBlock.target) {
if (executeBlockSpanId !== undefined) {
throw new http_errors_1.InternalServerError('More than 1 execute block span found when only 1 was expected');
}
executeBlockSpanId = span.id;
}
parsedSpans.push(spanWithChildren);
spansById.set(span.id, spanWithChildren);
}
if (executeBlockSpanId === undefined) {
throw new http_errors_1.InternalServerError('execute_block span could not be found');
}
// Go through each span, and add its `id` to the `children` array of its
// parent span, creating a tree of spans starting with `executeBlock` span as
// the root.
for (const span of spans) {
if (!span.parentId) {
continue;
}
const maybeParentSpan = spansById.get(span.parentId);
if (!maybeParentSpan) {
throw new http_errors_1.InternalServerError('Expected spans parent to exist in spansById');
}
maybeParentSpan.children.push(span.id);
}
return { spansById, parsedSpans, executeBlockSpanId };
}
/**
* Derive the action groups and operations from the block trace.
*/
actionsAndOps() {
var _a;
const { spansById, parsedSpans: spans, executeBlockSpanId } = Trace.parseSpans(this.traceBlock.spans);
const { eventsByParentId, extrinsicIndexBySpanId } = this.parseEvents(spansById);
const actions = [];
// Create a list of action groups (`actions`) and a list of all `Operations`.
for (const primary of spans) {
if (primary.parentId !== executeBlockSpanId) {
// Only use primary spans (spans where parentId === execute_block). All other
// spans are either the executeBlockSpan or descendant of a primary span.
continue;
}
// Gather the secondary spans and their events. Events are useful for
// creating Operations. Spans are useful for attributing events to a
// hook or extrinsic.
// Keep in mind events are storage Get/Put events. The spans are used to
// attribute events to some action in the block execution pipeline.
const secondarySpansIds = Trace.findDescendants(primary.id, spansById);
const secondarySpansEvents = [];
const secondarySpans = [];
secondarySpansIds.forEach((id) => {
const events = eventsByParentId.get(id);
const span = spansById.get(id);
events && secondarySpansEvents.push(...events);
span && secondarySpans.push(span);
});
let phase, extrinsicIndex;
if (primary.name === SPANS.applyExtrinsic.name) {
// This is an action group for an extrinsic application
extrinsicIndex = Trace.extractExtrinsicIndex(
// Pass in all the relevant spanIds of this action group
secondarySpansIds.concat(primary.id), extrinsicIndexBySpanId);
phase = types_1.Phase.ApplyExtrinsic;
}
else {
// This is likely an action group for a pallet hook or some initial/final
// block execution checks (i.e one of `initBlock`, `initialChecks`,
// `onFinalize`, or `finalChecks`).
// Note: `onInitialize` spans belong to the `initBlock` action group, so
// we give that action group the phase `onInitialize`.
phase = secondarySpans[0]
? // This case catches `onInitialize` spans since they are the secondary
// spans of `initBlock`
(0, util_1.stringCamelCase)((_a = secondarySpans[0]) === null || _a === void 0 ? void 0 : _a.name)
: // This case catches `onFinalize` since `onFinalize` spans are
// identified by the priamry spans name.
(0, util_1.stringCamelCase)(primary.name);
}
const primarySpanEvents = eventsByParentId.get(primary.id);
const events = (primarySpanEvents === null || primarySpanEvents === void 0 ? void 0 : primarySpanEvents.concat(secondarySpansEvents)) || [];
// Modify the original block trace events in place, making each event a
// `ParsedActionEvent`.
events.forEach((e) => {
e.phase = {
variant: phase,
extrinsicIndex: extrinsicIndex,
};
e.primarySpanId = {
id: primary.id,
name: primary.name,
target: primary.target,
};
});
// Primary span, and all associated descendant spans and events
// representing an action group.
actions.push({
extrinsicIndex,
phase,
primarySpan: primary,
secondarySpans,
});
}
const accountEventsByAddress = this.accountEventsByAddress(this.traceBlock.events);
// Note: if operations need to be in order of the actual storage ops they need to be sorted
// again by `eventIndex`. We skip that here for performance.
const operations = this.deriveOperations(accountEventsByAddress);
return {
actions,
operations,
};
}
/**
* Extract an extrinsic index from an `:extrinsic_index` event. If it is not an
* `:extrinsic_index` event returns `null`.
*
* @param event a parsed event
*/
maybeExtractIndex(event) {
var _a, _b;
if (!(((_a = event.storagePath) === null || _a === void 0 ? void 0 : _a.special) === ':extrinsic_index' && event.data.stringValues.method == 'Get')) {
// Note: there are many read and writes of `:extrinsic_index` per extrinsic
// so take care when modifying this logic.
// Note: we only consider `Get` ops; `Put` ops are prep for next extrinsic.
return null;
}
const { value_encoded, result_encoded } = event.data.stringValues;
const indexEncodedOption = value_encoded !== null && value_encoded !== void 0 ? value_encoded : result_encoded;
if (!(typeof indexEncodedOption == 'string')) {
throw new http_errors_1.InternalServerError('Expected there to be an encoded extrinsic index for extrinsic index event');
}
const scale = (_b = indexEncodedOption
.slice(2) // Slice of leading `Option` bits beacuse they cause trouble
.match(/.{1,2}/g) // Reverse the order of the bytes for correct endian-ness
) === null || _b === void 0 ? void 0 : _b.reverse().join('');
const hex = `0x${scale || ''}`;
return this.registry.createType('u32', hex).toBn();
}
/**
* Parse events by
* 1) Adding parent span info, event index and the storage item name to each event.
* Also adds the extrinsic index as an integer if there is an `:extrinsic_index` event.
* 2) Create a map of span id => array of children events
* 3) Create a map of span id => to extrinisic index
*
* @param spansById map of span id => to span with its children for all spans
* from tracing the block.
*/
parseEvents(spansById) {
var _a;
const extrinsicIndexBySpanId = new Map();
const eventsByParentId = new Map();
for (const [idx, event] of this.traceBlock.events.entries()) {
const { key } = event.data.stringValues;
const keyPrefix = key === null || key === void 0 ? void 0 : key.slice(0, 64);
const storagePath = this.getStoragePathFromKey(keyPrefix);
const p = spansById.get(event.parentId);
const parentSpanId = p && {
name: p.name,
target: p.target,
id: p.id,
};
// Make this a `ParsedEvent` by mutating it in place and
event.parentSpanId = parentSpanId;
event.eventIndex = idx;
event.storagePath = storagePath;
const maybeId = eventsByParentId.get(event.parentId);
if (!maybeId) {
eventsByParentId.set(event.parentId, [event]);
}
else {
(_a = eventsByParentId.get(event.parentId)) === null || _a === void 0 ? void 0 : _a.push(event);
}
const extrinsicIndexOpt = this.maybeExtractIndex(event);
if ((0, util_2.isSome)(extrinsicIndexOpt)) {
// Its ok to overwrite here, we just want thee last `:extrinsic_index` `Get`
// in this span.
extrinsicIndexBySpanId.set(event.parentId, extrinsicIndexOpt);
}
}
return { eventsByParentId, extrinsicIndexBySpanId };
}
/**
* Create a mapping address => Account events for that address based on an
* array of all the events.
*
* This mapping is useful because we create operations based on consecutive
* gets/puts to a single accounts balance data. So later on we can just take all
* the account events from a single address and create `Operation`s from those.
*
* Note: Assumes passed in events are already sorted.
*
* @param events events with phase info
*/
accountEventsByAddress(events) {
return events.reduce((acc, cur) => {
if (!(cur.storagePath.module == 'system' && cur.storagePath.item == 'account')) {
// filter out non system::Account events
return acc;
}
const asAccount = this.toAccountEvent(cur);
const address = asAccount.address.toString();
const maybeAccountEvents = acc.get(address);
if (!maybeAccountEvents) {
acc.set(address, [asAccount]);
}
else {
maybeAccountEvents.push(asAccount);
}
return acc;
}, new Map());
}
/**
* Convert a `ParsedActionEvent` to a `ParsedAccountEvent` by adding the decoded `accountInfo`
* and `address`.
*
* Notes: will throw if event does not have `system` `account` `storagePath`
*
* @param event ParsedActionEvent
*/
toAccountEvent(event) {
var _a;
const { storagePath } = event;
if (!(storagePath.module == 'system' && storagePath.item == 'account')) {
throw new http_errors_1.InternalServerError('Event did not have system::account key prefix as expected');
}
// storage key = h(system) + h(account) + h(address) + address
// ^^^^^^^^^^^^^^^^^^^^storage item prefix
// Since this is a transparent key with the address at the end we can pull out
// the address from the key. Here we slice off the storage key + account hash
// (which is equal to 96 bytes).
const addressRaw = (_a = event.data.stringValues.key) === null || _a === void 0 ? void 0 : _a.slice(96);
if (!(typeof addressRaw === 'string')) {
throw new http_errors_1.InternalServerError('Expect encoded address in system::account event storage key');
}
const address = this.registry.createType('Address', `0x${addressRaw}`);
const { value_encoded, result_encoded } = event.data.stringValues;
const accountInfoEncoded = value_encoded !== null && value_encoded !== void 0 ? value_encoded : result_encoded;
if (!(typeof accountInfoEncoded === 'string')) {
throw new http_errors_1.InternalServerError('Expect accountInfoEncoded to always be a string in system::Account event');
}
const accountInfo = this.registry.createType('AccountInfo', `0x${accountInfoEncoded.slice(2)}`);
// Mutate the event to make it a `ParsedAccountEvent`
event.accountInfo = accountInfo;
event.address = address;
return event;
}
/**
* Derive `Operation`s based on consecutive Get/Put events to an addresses `accountInfo`
* storage item.
*
* `Operation`s are created by checking for deltas in each field of `accountInfo` based
* on the previous event vs the current event.
*
* @param accountEventsByAddress map of address => events of that addresses `accountInfo`
* events.
*/
deriveOperations(accountEventsByAddress) {
const ops = [];
for (const events of accountEventsByAddress.values()) {
// IMPORTANT NOTE: we assume the events are in order so we know the deltas are
// correct between previous event <=> current event.
for (const [i, e] of events.entries()) {
if (i === 0) {
// Skip the first event; we always compare previous event <=> current event.
continue;
}
// Previous accountInfo.data
const prev = events[i - 1].accountInfo.data;
// Current accountInfo.data
const cur = e.accountInfo.data;
const free = cur.free.sub(prev.free);
const reserved = cur.reserved.sub(prev.reserved);
const miscFrozen = cur.miscFrozen.sub(prev.miscFrozen);
const feeFrozen = cur.feeFrozen.sub(prev.feeFrozen);
// Information that will go into any operation we create.
const baseInfo = {
phase: e.phase,
parentSpanId: e.parentSpanId,
primarySpanId: e.primarySpanId,
eventIndex: e.eventIndex,
address: e.address,
};
const currency = {
// For now we assume everything is always in the same token
symbol: this.registry.chainTokens[0],
};
// Base information for all `storage` values
const systemAccountData = {
pallet: 'system',
item: 'Account',
field1: 'data',
};
if (!free.eqn(0)) {
ops.push({
...baseInfo,
storage: {
...systemAccountData,
field2: 'free',
},
amount: {
value: free,
currency,
},
});
}
if (!reserved.eqn(0)) {
ops.push({
...baseInfo,
storage: {
...systemAccountData,
field2: 'reserved',
},
amount: {
value: reserved,
currency,
},
});
}
if (!miscFrozen.eqn(0)) {
ops.push({
...baseInfo,
storage: {
...systemAccountData,
field2: 'miscFrozen',
},
amount: {
value: miscFrozen,
currency,
},
});
}
if (!feeFrozen.eqn(0)) {
ops.push({
...baseInfo,
storage: {
...systemAccountData,
field2: 'feeFrozen',
},
amount: {
value: feeFrozen,
currency,
},
});
}
}
}
return ops;
}
/**
* Extract the storage item name based on the key prefix.
*
* @param key hex encoded storage key
* @returns KeyInfo
*/
getStoragePathFromKey(key) {
return (key && this.keyNames[key === null || key === void 0 ? void 0 : key.slice(0, 64)]) || EMPTY_KEY_INFO;
}
}
exports.Trace = Trace;
//# sourceMappingURL=Trace.js.map