UNPKG

@broxus/locklift-network

Version:

In-memory TVM-blockchain emulator for locklift

297 lines (261 loc) 9.94 kB
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); } }