@broxus/locklift-network
Version:
In-memory TVM-blockchain emulator for locklift
297 lines (261 loc) • 9.94 kB
text/typescript
import * as nt from "nekoton-wasm";
import { BlockchainConfig } from "nekoton-wasm";
import { Address, LT_COLLATOR } from "everscale-inpage-provider";
import { Heap } from "heap-js";
import _ from "lodash";
import { GIVER_ADDRESS_EVER_WALLET, GIVER_BOC_EVER_WALLET, ZERO_ADDRESS } from "./constants";
import { AccountFetcherCallback } from "../types";
import { TychoExecutor } from "@tychosdk/emulator";
import { beginCell, Cell, Dictionary, loadShardAccount, storeShardAccount } from "@ton/core";
import type { ExecutorEmulationResult } from "@ton/sandbox";
import { bocFromShardAccount, parseBlocks, shardAccountFromBoc } from "./utils";
const messageComparator = (a: nt.JsRawMessage, b: nt.JsRawMessage) => LT_COLLATOR.compare(a.lt || "0", b.lt || "0");
const emptyShardAccount = beginCell()
.store(
storeShardAccount({
account: null,
lastTransactionHash: 0n,
lastTransactionLt: 0n,
}),
)
.endCell()
.toBoc()
.toString("base64");
type ExecutorState = {
// accounts: { [id: string]: nt.FullContractState };
accounts: { [id: string]: string };
// txId -> tx
transactions: { [id: string]: nt.JsRawTransaction };
// txId -> trace
traces: { [id: string]: { parsed: nt.EngineTraceInfo[]; raw: string } };
// msgHash -> tx_id
msgToTransaction: { [msgHash: string]: string };
// address -> tx_ids
addrToTransactions: { [addr: string]: string[] };
messageQueue: Heap<nt.JsRawMessage>;
libs: Dictionary<bigint, Cell>;
};
interface LockliftTransport {
getBlockchainConfig(): Promise<BlockchainConfig>;
setExecutor(executor: LockliftExecutor): void;
}
export class LockliftExecutor {
private traceEnabled = false;
private state: ExecutorState = {} as ExecutorState;
private snapshots: { [id: string]: ExecutorState } = {};
private nonce = 0;
private blockchainConfig!: string;
private globalId: number | undefined;
private clock: nt.ClockWithOffset | undefined;
private tychoExecutor!: TychoExecutor;
blockchainLt = 1000n;
constructor(
private readonly transport: LockliftTransport,
private readonly accountFetcherCallback?: AccountFetcherCallback,
) {
this.createInitialBlockchainState();
transport.setExecutor(this);
}
private createInitialBlockchainState() {
this.state = {
accounts: {},
transactions: {},
msgToTransaction: {},
addrToTransactions: {},
traces: {},
messageQueue: new Heap<nt.JsRawMessage>(messageComparator),
libs: Dictionary.empty(Dictionary.Keys.BigInt(256), Dictionary.Values.Cell()),
};
// set this in order to pass standalone-client checks
this.state.accounts[ZERO_ADDRESS.toString()] = bocFromShardAccount({
account: null,
lastTransactionHash: 0n,
lastTransactionLt: 0n,
});
this.state.accounts[GIVER_ADDRESS_EVER_WALLET] = bocFromShardAccount(
shardAccountFromBoc(nt.makeFullAccountBoc(GIVER_BOC_EVER_WALLET), 0n, 999999999999999999999999999999n),
);
}
async initialize() {
const config = await this.transport.getBlockchainConfig();
this.blockchainConfig = Cell.fromBase64(config.boc).asSlice().loadRef().toBoc().toString("base64");
this.globalId = Number(config.globalId);
this.tychoExecutor = await TychoExecutor.create();
}
setClock(clock: nt.ClockWithOffset) {
if (this.clock !== undefined) throw new Error("Clock already set");
this.clock = clock;
}
_setAccount1(address: Address | string, boc: string) {
this.state.accounts[address.toString()] = boc;
}
_setLibrary(key: bigint, cell: Cell) {
this.state.libs.set(key, cell);
}
setAccount(address: Address | string, boc: string, type: "accountStuffBoc" | "fullAccountBoc") {
const newBoc = type === "accountStuffBoc" ? nt.makeFullAccountBoc(boc) : boc;
this.state.accounts[address.toString()] = bocFromShardAccount(shardAccountFromBoc(newBoc, 0n));
}
async _getAccount(address: Address | string): Promise<string | undefined> {
return this.state.accounts[address.toString()];
// ||
// this.accountFetcherCallback?.(address instanceof Address ? address : new Address(address))
// .then(({ boc, type }) => {
// if (!boc) throw new Error("Account not found");
// this.setAccount(address, boc, type);
// return this.state.accounts[address.toString()];
// })
// .catch(e => {
// console.error(`Failed to fetch account ${address.toString()}: ${e.trace}`);
// return undefined;
// })
}
enableTraces() {
this.traceEnabled = true;
console.log("VM traces enabled, note that performance is downgraded!");
}
disableTraces() {
this.traceEnabled = false;
}
getAccounts(): Record<string, nt.FullContractState> {
return Object.entries(this.state.accounts).reduce((acc, next) => {
const [address, account] = next;
const fullContractState = nt.parseShardAccountBoc(account);
if (!fullContractState) {
return acc;
}
acc[address] = fullContractState;
return acc;
}, {} as Record<string, nt.FullContractState>);
}
getTxTrace(txId: string): { parsed: nt.EngineTraceInfo[]; raw: string } | undefined {
return this.state.traces[txId];
}
private saveTransaction(tx: nt.JsRawTransaction, trace: { parsed: nt.EngineTraceInfo[]; raw: string }) {
this.state.transactions[tx.hash] = tx;
this.state.msgToTransaction[tx.inMessage.hash] = tx.hash;
this.state.addrToTransactions[tx.inMessage.dst as string] = [tx.hash].concat(
this.state.addrToTransactions[tx.inMessage.dst as string] || [],
);
this.state.traces[tx.hash] = trace;
}
getDstTransaction(msgHash: string): nt.JsRawTransaction | undefined {
return this.state.transactions[this.state.msgToTransaction[msgHash]];
}
getTransaction(id: string): nt.JsRawTransaction | undefined {
return this.state.transactions[id];
}
getTransactions(address: Address | string, fromLt: string, count: number): nt.JsRawTransaction[] {
const result: nt.JsRawTransaction[] = [];
for (const txId of this.state.addrToTransactions[address.toString()] || []) {
const rawTx = this.state.transactions[txId];
if (Number(rawTx.lt) > Number(fromLt)) continue;
result.push(rawTx);
if (result.length >= count) return result;
}
return result;
}
saveSnapshot(): number {
this.snapshots[this.nonce] = _.cloneDeep(this.state);
// postincrement!
return this.nonce++;
}
loadSnapshot(id: number) {
if (this.snapshots[id] === undefined) {
throw new Error(`Snapshot ${id} not found`);
}
this.state = this.snapshots[id];
}
clearSnapshots() {
this.snapshots = {};
}
resetBlockchainState() {
this.createInitialBlockchainState();
}
// process all msgs in queue
async processQueue() {
while (this.state.messageQueue.size() > 0) {
await this.processNextMsg();
}
}
// process msg with lowest lt in queue
async processNextMsg() {
const message = this.state.messageQueue.pop() as nt.JsRawMessage;
// everything is processed
if (!message) return;
const receiverAcc = (await this._getAccount(message.dst as string)) || emptyShardAccount;
const messageCell = Cell.fromBase64(message.boc);
const now = Math.floor(this.clock!.nowMs / 1000);
let trace: Array<nt.EngineTraceInfo> = [];
let res: ExecutorEmulationResult = await this.tychoExecutor.runTransaction({
config: this.blockchainConfig,
message: messageCell,
lt: this.blockchainLt,
shardAccount: receiverAcc,
now,
libs: this.state.libs.size > 0 ? beginCell().storeDictDirect(this.state.libs).endCell() : null,
debugEnabled: true,
randomSeed: null,
verbosity: this.traceEnabled ? "full_location_stack_verbose" : "short",
ignoreChksig: true,
});
if (!res.result.success) {
console.log("Error in executor: ", res.result.error);
return;
}
const decodedTx = nt.decodeRawTransaction(res.result.transaction);
if (decodedTx.description.aborted) {
// run 1 more time with trace on
res = await this.tychoExecutor.runTransaction({
config: this.blockchainConfig,
message: messageCell,
lt: this.blockchainLt,
shardAccount: receiverAcc,
now,
libs: this.state.libs.size > 0 ? beginCell().storeDictDirect(this.state.libs).endCell() : null,
debugEnabled: true,
randomSeed: null,
verbosity: "full_location_stack_verbose",
ignoreChksig: true,
});
}
if (res.result.success && res.result.vmLog) {
trace = parseBlocks(res.result.vmLog);
}
if (res.logs || res.debugLogs) {
console.log("Debug logs: ", res.debugLogs);
}
if (!res.result.success) {
console.log("Error in executor: ", res.result.error);
return;
}
this.blockchainLt += 1000n;
if (res.result.shardAccount) {
if (message.dst?.startsWith("-1")) {
const loadedShardAccount = loadShardAccount(Cell.fromBase64(res.result.shardAccount).asSlice()).account;
if (loadedShardAccount?.storage.state.type === "active") {
const libraries = loadedShardAccount.storage.state.state.libraries;
libraries?.keys().map(el => {
const lib = libraries.get(el);
if (lib && lib.public && lib.root) {
this._setLibrary(el, lib.root);
}
});
}
}
this._setAccount1(message.dst as string, res.result.shardAccount);
this.saveTransaction(decodedTx, {
parsed: trace,
raw: res.result.vmLog || "",
});
decodedTx.outMessages.map((msg: nt.JsRawMessage) => {
if (msg.msgType === "ExtOut") return; // event
this.enqueueMsg(msg);
});
}
}
// push new message to queue
enqueueMsg(message: nt.JsRawMessage) {
this.state.messageQueue.push(message);
}
}