如何获取NodeJS VM执行后的状态字节码?JavaScript智能合约实验【更新版】
如何从Node.js VM实例中获取包含新状态的字节码?
我明白你现在在尝试用Node.js的VM模块模拟以太坊智能合约的运行流程,核心诉求是——执行完修改状态的调用后,怎么把包含新状态的合约导出成字节码,传给下一个VM实例继续用。先给你理清楚问题所在,再给两个可行的解决方案:
先说说当前代码的核心问题
你现在的代码里,compileCode生成的字节码本质是JS代码的编译缓存,它只保存代码本身的编译结果,完全不包含运行时的状态(比如你设置的name变量)。而runBytecode每次调用都是在同一个全局上下文里执行,状态确实会被修改,但这些状态是存在内存的上下文里,根本不在字节码里,所以你没法从原来的bytecode变量里拿到更新后的状态。
两种解决方案,推荐第一种(更贴合以太坊逻辑)
方案1:状态与代码分离,序列化状态传递
这和以太坊的设计思路一致:合约代码是固定不变的,状态单独存储,每次调用合约是用代码去操作状态。我们可以把合约逻辑封装成对象方法,状态存在上下文对象里,执行后把状态序列化,传给下一个VM实例时再恢复。
完整代码实现:
const vm = require('vm'); const v8 = require('v8'); v8.setFlagsFromString('--no-lazy'); if (Number.parseInt(process.versions.node.split('.')[0], 10) >= 12) { v8.setFlagsFromString('--no-flush-bytecode'); } // 编译合约逻辑(仅代码,不含状态) const compileContract = function (contractCode) { if (typeof contractCode !== 'string') { throw new Error(`contractCode must be string. ${typeof contractCode} was given.`) } let script = new vm.Script(contractCode) return script.createCachedData(); } // 加载合约到新上下文,注入初始状态,返回带合约方法的上下文 const loadContract = function (bytecodeBuffer, initialState = {}) { if (!Buffer.isBuffer(bytecodeBuffer)) { throw new Error(`bytecodeBuffer must be a buffer object.`); } let length = readSourceHash(bytecodeBuffer); let dummyCode = ""; if (length > 1) { dummyCode = '"' + "\u200b".repeat(length - 2) + '"'; } // 创建独立上下文,避免污染全局 const context = vm.createContext({...initialState}); let script = new vm.Script(dummyCode, { cachedData: bytecodeBuffer }); if (script.cachedDataRejected) { throw new Error('Invalid or incompatible cached data (cachedDataRejected)'); } script.runInContext(context); return context; } // 执行调用代码,传入当前上下文,返回更新后的上下文 const executeCall = function (callCode, currentContext) { const context = vm.createContext({...currentContext}); const callScript = new vm.Script(callCode); callScript.runInContext(context); return context; } const readSourceHash = function (bytecodeBuffer) { if (!Buffer.isBuffer(bytecodeBuffer)) { throw new Error(`bytecodeBuffer must be a buffer object.`) } return bytecodeBuffer.slice(8, 12).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0) } // 合约逻辑:用this来维护状态,和代码逻辑分离 const smartContractCode = ` this.setName = function(_name){ this.names = this.names || []; this.names.push(_name); console.log('Added name:', _name); } this.getName = function(){ console.log('Current names:', this.names || []); return this.names || []; } ` // 流程演示 // 1. 编译合约逻辑成字节码(只做一次,因为代码不变) const contractBytecode = compileContract(smartContractCode); // 2. 初始化上下文,加载合约 let currentContext = loadContract(contractBytecode); // 3. 执行两次添加名称的调用,更新状态 currentContext = executeCall("setName('Satoshi')", currentContext); currentContext = executeCall("setName('Vitalik')", currentContext); // 4. 验证当前状态 executeCall("getName()", currentContext); // 5. 序列化状态,传给下一个VM实例 const serializedState = JSON.stringify(currentContext); // 模拟新VM实例加载 const newInitialState = JSON.parse(serializedState); const newContext = loadContract(contractBytecode, newInitialState); executeCall("getName()", newContext); // 会输出 ['Satoshi', 'Vitalik']
方案2:把状态嵌入代码重新编译(不推荐)
如果你一定要把状态包含在字节码里,可以每次执行后把当前状态和合约代码拼接成新的代码,重新编译成字节码。但这种方式会让字节码越来越大,而且状态硬编码后不够灵活,只适合简单场景。
示例片段:
// 生成包含当前状态的新合约代码 const getContractWithState = (originalCode, state) => { // 把状态变量定义放在代码最前面 const stateDefinitions = Object.entries(state) .map(([key, value]) => `let ${key} = ${JSON.stringify(value)};`) .join('\n'); return `${stateDefinitions}\n${originalCode}`; } // 执行调用后,生成带状态的新代码并重新编译 const newContractCode = getContractWithState(smartContract, currentContext); const newBytecode = compileCode(newContractCode); // 下一个VM实例运行这个新字节码就能拿到状态 runBytecode(newBytecode); runBytecode(compileCode("getName()")); // 能获取到之前设置的名称
最后再强调下关键逻辑
Node.js VM的cachedData(也就是你生成的bytecode)只存储代码的编译结果,和运行时状态完全无关。状态是保存在执行上下文(context)里的,这和以太坊的模式一致:合约字节码不可变,状态单独存储,调用合约只是用字节码去修改状态,而不是修改字节码本身。所以方案1才是更合理的选择。
内容的提问来源于stack exchange,提问作者MadeInDreams
相关产品推荐
相关产品推荐

