智能合约方法在Remix运行正常但Ganache中触发回退如何解决
概述
我正在完成区块链开发者课程的项目,项目必须使用Ganache测试,因为课程评审团队将使用Ganache核验成果。
我当前开发的智能合约包含两个独立合约:一个负责数据存储,另一个负责应用层用户交互。用户调用registerOracle方法时,合约要求支付1 ether以生成1个oracle,该oracle将拥有1个ID和3个索引。
我在Remix上测试合约一切正常,但在Ganache中测试oracle生成功能时触发回退错误。
项目地址
项目文件存放在Github仓库中,同时上传了Remix测试操作流程、Ganache报错信息的相关截图。
代码体量较大,我将在下方贴出报错相关的核心代码,完整代码可查看仓库内容。
TestingEnv.js 文件
const Web3 = require('web3'); const fs = require('fs').promises; async function main () { /** * 以下部分代码逻辑无问题可忽略 */ const web3 = new Web3(new Web3.providers.WebsocketProvider("HTTP://127.0.0.1:8545")); const appABI = (JSON.parse(await fs.readFile("./bin/appAbi.json", "utf8"))).abi; const appBYTECODE = (JSON.parse(await fs.readFile("./bin/appByteCode.json", "utf8"))).bytecode; const dataABI = (JSON.parse(await fs.readFile("./bin/dataAbi.json", "utf8"))).abi; const dataBYTECODE = (JSON.parse(await fs.readFile("./bin/dataByteCode.json", "utf8"))).bytecode; const owner = (await web3.eth.getAccounts())[0]; const appContract = await new web3.eth.Contract(appABI).deploy({data: '0x' + appBYTECODE}) .send({from: owner, gasLimit: 6000000, gasPrice: web3.utils.toWei('5', 'gwei')}); const dataContract = await new web3.eth.Contract(dataABI).deploy({data: '0x' + dataBYTECODE}) .send({from: owner, gasLimit: 6000000, gasPrice: web3.utils.toWei('5', 'gwei')}); const appAddress = appContract._address; const dataAddress = dataContract._address; await appContract.methods.registerDataContract(dataAddress).send({from: owner}); await dataContract.methods.registerApplicationContract(appAddress).send({from: owner}); /** * 可忽略代码部分结束 */ /** * 回退错误触发位置 */ const oracleFee = await web3.utils.toWei("1", "ether"); const server = (await web3.eth.getAccounts())[2]; // 我需要注册20个oracle for (var i=0; i<20; i++) { await appContract.methods.registerOracle().send({from: server, value: oracleFee}); await new Promise(resolve => setTimeout(resolve, 2000)); }; const totalOracles = await appContract.methods.getTotalOracles().call({from: server}); console.log(`Total Numbers of Oracles Registered: ${totalOracles}`); for (var i = 0; i<totalOracles; i++) { var indexes = await appContract.methods.getOraceIndexes(i).call({from: server}); if (i >= 0 && i <= 9) { console.log(`Oracle 0${i} indexes: ${indexes._index1.toString()} ${indexes._index2.toString()} ${indexes._index2.toString()}`); } else { console.log(`Oracle ${i} indexes: ${indexes._index1.toString()} ${indexes._index2.toString()} ${indexes._index2.toString()}`); }; }; }; main();
FlightSuretyApp.sol 合约代码
pragma solidity ^0.4.24; interface iFlightSuretyData { function registerOracle(address server, uint8 index1, uint8 index2, uint8 index3) external payable; } import "./SafeMath.sol"; contract FlightSuretyApp { using SafeMath for uint256; bool operationalStatus; address owner; iFlightSuretyData FlightSecuretyData; constructor() public { operationalStatus = true; owner = msg.sender; } modifier requireOwner() { require(msg.sender == owner, "Owner is required"); _; } modifier requireOperational() { require(operationalStatus == true, "Contract is not operational"); _; } function registerDataContract(address dataContract) external requireOwner { FlightSecuretyData = iFlightSuretyData(dataContract); } uint256 constant ORACLE_REGISTRATION_FEE = 1 ether; modifier requireOracleRegistrationFee() { require(msg.value == ORACLE_REGISTRATION_FEE, "Oracle Registration Cost 1 ether"); _; } function registerOracle() external payable requireOperational requireOracleRegistrationFee { (uint8 index1, uint8 index2, uint8 index3) = indexesThrown(); FlightSecuretyData.registerOracle.value(msg.value)(msg.sender, index1, index2, index3); } function indexesThrown() private view returns(uint8 _index1, uint8 _index2, uint8 _index3) { uint8 index1 = generateIndex1(); uint8 index2 = generateIndex2(index1); uint8 index3 = generateIndex3(index1, index2); return (index1, index2, index3); } function generateIndex1() private view returns(uint8 _index) { uint256 mod = 10; uint256 time = block.timestamp; uint256 difficulty = block.difficulty; uint8 value = uint8(SafeMath.mod(uint256(keccak256(abi.encodePacked(time, difficulty, msg.sender))), mod)); return value; } function generateIndex2(uint8 _index1) private view returns(uint8 _index) { uint256 mod = 10; uint256 time = block.timestamp; uint256 difficulty = block.difficulty; uint8 value = uint8(SafeMath.mod(uint256(keccak256(abi.encodePacked(time, difficulty, msg.sender))), mod)); while(value == _index1) { time = SafeMath.add(time, 500); difficulty = SafeMath.add(difficulty, 700); value = uint8(SafeMath.mod(uint256(keccak256(abi.encodePacked(time, difficulty, msg.sender))), mod)); } return value; } function generateIndex3(uint8 _index1, uint8 _index2) private view returns(uint8 _index) { uint256 mod = 10; uint256 time = block.timestamp; uint256 difficulty = block.difficulty; uint8 value = uint8(SafeMath.mod(uint256(keccak256(abi.encodePacked(time, difficulty, msg.sender))), mod)); while((value == _index1) || (value == _index2)) { time = SafeMath.add(time, 500); difficulty = SafeMath.add(difficulty, 700); value = uint8(SafeMath.mod(uint256(keccak256(abi.encodePacked(time, difficulty, msg.sender))), mod)); } return value; } }
FlightSuretyData.sol 合约代码
import "./SafeMath.sol"; contract FlightSuretyData { using SafeMath for uint256; bool operationalStatus; address owner; address appContract; constructor() public { operationalStatus = true; owner = msg.sender; airlines[owner].registrationStatus = true; } modifier requireOwner() { require(msg.sender == owner, "Require contract owner"); _; } modifier requireApplication() { require(msg.sender == appContract, "Require application"); _; } modifier requireOperational() { require(operationalStatus == true, "Contract is not operational"); _; } function registerApplicationContract(address application) external requireOwner requireOperational { appContract = application; } struct oracle { uint8 index1; uint8 index2; uint8 index3; mapping(bytes32 => bool) voteState; } struct oracleServer { mapping(uint256 => oracle) oracles; uint256 numberOfOracles; } mapping(address => oracleServer) private oracleServers; function registerOracle(address server, uint8 index1, uint8 index2, uint8 index3) external payable requireOperational requireApplication { uint256 counter = oracleServers[server].numberOfOracles; oracleServers[server].oracles[counter].index1 = index1; oracleServers[server].oracles[counter].index2 = index2; oracleServers[server].oracles[counter].index3 = index3; counter = SafeMath.add(counter, 1); oracleServers[server].numberOfOracles = counter; } }
项目运行方式
安装依赖后直接运行TestingEnv.js文件即可复现问题。
运行环境配置
- Windows 10 Home 21H1版本
- Ganache 2.5.4版本(使用Ganache官方桌面客户端,非依赖包版本)
- Node 11.0.0版本
- Solidity 0.4.24版本
Remix运行正常证明

Ganache报错证明

问题咨询
我已测试合约其他功能均运行正常,仅oracle注册功能触发报错导致项目进度停滞,请问该问题如何排查解决?我遗漏了哪些配置或逻辑?
问题排查与解决方法
1. 核心触发原因:Ganache出块规则差异
Remix测试环境是每笔交易单独生成一个区块,而你本地Ganache默认启用自动挖矿,你代码中每次注册oracle后添加的2秒延迟不会触发新块生成,导致20次循环调用的block.timestamp、block.difficulty完全一致,加上调用地址固定为同一个server,所以第一次生成三个不同索引后,后续调用时generateIndex2、generateIndex3的while循环会始终生成和第一个索引重复的值,进入死循环耗尽gas触发回退。
修复方案:要么修改索引生成逻辑,新增随调用次数递增的随机种子参数,不再完全依赖区块变量;要么在Ganache设置中将自动挖矿改为固定1秒间隔出块;要么去掉代码中的2秒延迟,让多笔交易尽可能打包到不同区块。
2. 隐藏问题1:Data合约无法接收ETH
你在调用FlightSecuretyData.registerOracle时向Data合约转入了1 ETH,但FlightSuretyData没有实现payable fallback函数,Ganache的严格校验模式会直接拒绝合约收款操作,触发回退。
修复方案:在FlightSuretyData合约中添加fallback函数:
function() external payable {}
或者调整逻辑,将注册费直接存在App合约中,不向Data合约转账。
3. 隐藏问题2:索引生成逻辑低概率冲突
当前索引生成逻辑是从0-9中取3个不重复的值,当同一个地址注册超过12次时,所有索引组合已经被穷尽,必然进入死循环,建议增加随机种子维度,或者允许同一地址重复使用索引组合。
内容的提问来源于stack exchange,提问作者Jask_Skull

