diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 1b0e8a6637f..37ffa0384e0 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -54,6 +54,7 @@ import org.tron.core.vm.program.ProgramPrecompile; import org.tron.core.vm.program.invoke.ProgramInvoke; import org.tron.core.vm.program.invoke.ProgramInvokeFactory; +import org.tron.core.vm.program.listener.SimulationTracer; import org.tron.core.vm.repository.Repository; import org.tron.core.vm.repository.RepositoryImpl; import org.tron.core.vm.utils.MUtil; @@ -95,6 +96,12 @@ public class VMActuator implements Actuator2 { @Setter private boolean enableEventListener; + @Setter + private Repository injectedRootRepository; + + @Setter + private SimulationTracer simulationTracer; + private LogInfoTriggerParser logInfoTriggerParser; public VMActuator(boolean isConstantCall) { @@ -138,7 +145,9 @@ public void validate(Object object) throws ContractValidateException { //Route Type ContractType contractType = this.trx.getRawData().getContract(0).getType(); //Prepare Repository - rootRepository = RepositoryImpl.createRoot(context.getStoreFactory()); + rootRepository = injectedRootRepository != null + ? injectedRootRepository + : RepositoryImpl.createRoot(context.getStoreFactory()); enableEventListener = context.isEventPluginLoaded(); @@ -414,6 +423,7 @@ private void create() if (VMConfig.allowTvmCompatibleEvm()) { this.program.setContractVersion(1); } + this.program.setSimulationTracer(simulationTracer); byte[] txId = TransactionUtil.getTransactionId(trx).getBytes(); this.program.setRootTransactionId(txId); if (enableEventListener && isCheckTransaction()) { @@ -437,10 +447,18 @@ private void create() // transfer from callerAddress to contractAddress according to callValue if (callValue > 0) { MUtil.transfer(rootRepository, callerAddress, contractAddress, callValue); + if (simulationTracer != null) { + simulationTracer.onTransfer(stripTronPrefix(callerAddress), + stripTronPrefix(contractAddress), callValue); + } } if (VMConfig.allowTvmTransferTrc10() && tokenValue > 0) { MUtil.transferToken(rootRepository, callerAddress, contractAddress, String.valueOf(tokenId), tokenValue); + if (simulationTracer != null) { + simulationTracer.onTokenTransfer(stripTronPrefix(callerAddress), + stripTronPrefix(contractAddress), tokenId, tokenValue); + } } } @@ -529,6 +547,7 @@ private void call() if (VMConfig.allowTvmCompatibleEvm()) { this.program.setContractVersion(deployedContract.getContractVersion()); } + this.program.setSimulationTracer(simulationTracer); byte[] txId = TransactionUtil.getTransactionId(trx).getBytes(); this.program.setRootTransactionId(txId); @@ -543,14 +562,31 @@ private void call() if (callValue > 0) { MUtil.transfer(rootRepository, callerAddress, contractAddress, callValue); + if (simulationTracer != null) { + simulationTracer.onTransfer(stripTronPrefix(callerAddress), + stripTronPrefix(contractAddress), callValue); + } } if (VMConfig.allowTvmTransferTrc10() && tokenValue > 0) { MUtil.transferToken(rootRepository, callerAddress, contractAddress, String.valueOf(tokenId), tokenValue); + if (simulationTracer != null) { + simulationTracer.onTokenTransfer(stripTronPrefix(callerAddress), + stripTronPrefix(contractAddress), tokenId, tokenValue); + } } } + private static byte[] stripTronPrefix(byte[] tronAddress) { + if (tronAddress == null || tronAddress.length != 21) { + return tronAddress; + } + byte[] evm = new byte[20]; + System.arraycopy(tronAddress, 1, evm, 0, 20); + return evm; + } + public long getAccountEnergyLimitWithFixRatio(AccountCapsule account, long feeLimit, long callValue) { diff --git a/actuator/src/main/java/org/tron/core/vm/OperationActions.java b/actuator/src/main/java/org/tron/core/vm/OperationActions.java index 88c3c55899e..2a1993dd3db 100644 --- a/actuator/src/main/java/org/tron/core/vm/OperationActions.java +++ b/actuator/src/main/java/org/tron/core/vm/OperationActions.java @@ -754,6 +754,9 @@ public static void logAction(Program program) { new LogInfo(address.getLast20Bytes(), topics, data); program.getResult().addLogInfo(logInfo); + if (program.getSimulationTracer() != null) { + program.getSimulationTracer().onLog(logInfo); + } program.step(); } diff --git a/actuator/src/main/java/org/tron/core/vm/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 80d972041dc..5d7409cbacb 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Program.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Program.java @@ -91,6 +91,7 @@ import org.tron.core.vm.program.listener.CompositeProgramListener; import org.tron.core.vm.program.listener.ProgramListenerAware; import org.tron.core.vm.program.listener.ProgramStorageChangeListener; +import org.tron.core.vm.program.listener.SimulationTracer; import org.tron.core.vm.repository.Key; import org.tron.core.vm.repository.Repository; import org.tron.core.vm.trace.ProgramTrace; @@ -146,6 +147,9 @@ public class Program { @Getter @Setter private long callPenaltyEnergy; + @Getter + @Setter + private SimulationTracer simulationTracer; public Program(byte[] ops, byte[] codeAddress, ProgramInvoke programInvoke, InternalTransaction internalTransaction) { @@ -477,14 +481,17 @@ public void suicide(DataWord obtainerAddress) { byte[] blackHoleAddress = getContractState().getBlackHoleAddress(); if (VMConfig.allowTvmTransferTrc10()) { getContractState().addBalance(blackHoleAddress, balance); - MUtil.transferAllToken(getContractState(), owner, blackHoleAddress); + transferAllTokenWithTrace(owner, blackHoleAddress); } } else { createAccountIfNotExist(getContractState(), obtainer); try { MUtil.transfer(getContractState(), owner, obtainer, balance); + if (simulationTracer != null && balance > 0) { + simulationTracer.onTransfer(stripTronPrefix(owner), stripTronPrefix(obtainer), balance); + } if (VMConfig.allowTvmTransferTrc10()) { - MUtil.transferAllToken(getContractState(), owner, obtainer); + transferAllTokenWithTrace(owner, obtainer); } } catch (ContractValidateException e) { if (VMConfig.allowTvmConstantinople()) { @@ -515,6 +522,40 @@ public void suicide(DataWord obtainerAddress) { getResult().addDeleteAccount(this.getContractAddress()); } + private static byte[] stripTronPrefix(byte[] tronAddress) { + if (tronAddress == null || tronAddress.length != 21) { + return tronAddress; + } + byte[] evm = new byte[20]; + System.arraycopy(tronAddress, 1, evm, 0, 20); + return evm; + } + + /** + * Snapshot the owner's TRC-10 asset map, perform the sweep, then emit one + * {@code onTokenTransfer} per non-zero entry — used for SELFDESTRUCT. + * {@link MUtil#transferAllToken} mutates {@code owner.assetMapV2} in place, + * so the snapshot has to happen first; emitting after the sweep keeps the + * "log after real state change succeeds" invariant the other hooks follow. + */ + private void transferAllTokenWithTrace(byte[] owner, byte[] dest) { + if (simulationTracer == null) { + MUtil.transferAllToken(getContractState(), owner, dest); + return; + } + java.util.Map snapshot = + new java.util.HashMap<>(getContractState().getAccount(owner).getAssetMapV2()); + MUtil.transferAllToken(getContractState(), owner, dest); + byte[] fromEvm = stripTronPrefix(owner); + byte[] toEvm = stripTronPrefix(dest); + for (java.util.Map.Entry e : snapshot.entrySet()) { + if (e.getValue() != null && e.getValue() > 0) { + simulationTracer.onTokenTransfer(fromEvm, toEvm, + Long.parseLong(e.getKey()), e.getValue()); + } + } + } + public void suicide2(DataWord obtainerAddress) { byte[] owner = getContextAddress(); @@ -555,8 +596,11 @@ public void suicide2(DataWord obtainerAddress) { createAccountIfNotExist(getContractState(), obtainer); try { MUtil.transfer(getContractState(), owner, obtainer, balance); + if (simulationTracer != null && balance > 0) { + simulationTracer.onTransfer(stripTronPrefix(owner), stripTronPrefix(obtainer), balance); + } if (VMConfig.allowTvmTransferTrc10()) { - MUtil.transferAllToken(getContractState(), owner, obtainer); + transferAllTokenWithTrace(owner, obtainer); } } catch (ContractValidateException e) { if (VMConfig.allowTvmConstantinople()) { @@ -872,6 +916,10 @@ private void createContractImpl(DataWord value, byte[] programCode, byte[] newAd deposit.addBalance(newAddress, oldBalance); } + if (simulationTracer != null) { + simulationTracer.enterFrame(); + } + // [4] TRANSFER THE BALANCE long newBalance = 0L; if (!byTestingSuite() && endowment > 0) { @@ -883,6 +931,10 @@ private void createContractImpl(DataWord value, byte[] programCode, byte[] newAd } deposit.addBalance(senderAddress, -endowment); newBalance = deposit.addBalance(newAddress, endowment); + if (simulationTracer != null) { + simulationTracer.onTransfer(stripTronPrefix(senderAddress), + stripTronPrefix(newAddress), endowment); + } } // actual energy subtract @@ -914,6 +966,7 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), if (VMConfig.allowTvmCompatibleEvm()) { program.setContractVersion(getContractVersion()); } + program.setSimulationTracer(this.simulationTracer); VM.play(program, OperationRegistry.getTable()); createResult = program.getResult(); getTrace().merge(program.getTrace()); @@ -959,6 +1012,10 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), stackPushZero(); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } + if (createResult.getException() != null) { return; } else { @@ -971,6 +1028,10 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), // IN SUCCESS PUSH THE ADDRESS INTO THE STACK stackPush(new DataWord(newAddress)); + + if (simulationTracer != null) { + simulationTracer.exitFrame(); + } } // 5. REFUND THE REMAIN Energy @@ -1006,6 +1067,10 @@ public void callToAddress(MessageCall msg) { return; } + if (simulationTracer != null) { + simulationTracer.enterFrame(); + } + byte[] data = memoryChunk(msg.getInDataOffs().intValue(), msg.getInDataSize().intValue()); // FETCH THE SAVED STORAGE @@ -1052,6 +1117,9 @@ public void callToAddress(MessageCall msg) { if (senderBalance < endowment) { stackPushZero(); refundEnergy(msg.getEnergy().longValue(), REFUND_ENERGY_FROM_MESSAGE_CALL); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } return; } } else { @@ -1061,6 +1129,9 @@ public void callToAddress(MessageCall msg) { if (senderBalance < endowment) { stackPushZero(); refundEnergy(msg.getEnergy().longValue(), REFUND_ENERGY_FROM_MESSAGE_CALL); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } return; } } @@ -1094,6 +1165,12 @@ public void callToAddress(MessageCall msg) { } deposit.addBalance(senderAddress, -endowment); contextBalance = deposit.addBalance(contextAddress, endowment); + if (simulationTracer != null + && msg.getOpCode() != Op.DELEGATECALL + && msg.getOpCode() != Op.CALLCODE) { + simulationTracer.onTransfer(stripTronPrefix(senderAddress), + stripTronPrefix(contextAddress), endowment); + } } else { try { VMUtils.validateForSmartContract(deposit, senderAddress, contextAddress, @@ -1107,6 +1184,13 @@ public void callToAddress(MessageCall msg) { } deposit.addTokenBalance(senderAddress, tokenId, -endowment); deposit.addTokenBalance(contextAddress, tokenId, endowment); + if (simulationTracer != null + && msg.getOpCode() != Op.DELEGATECALL + && msg.getOpCode() != Op.CALLCODE) { + simulationTracer.onTokenTransfer(stripTronPrefix(senderAddress), + stripTronPrefix(contextAddress), + Long.parseLong(new String(tokenId)), endowment); + } } } @@ -1146,6 +1230,7 @@ this, new DataWord(contextAddress), program.setContractVersion(invoke.getDeposit() .getContract(codeAddress).getContractVersion()); } + program.setSimulationTracer(this.simulationTracer); VM.play(program, OperationRegistry.getTable()); callResult = program.getResult(); @@ -1164,6 +1249,10 @@ this, new DataWord(contextAddress), stackPushZero(); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } + if (callResult.getException() != null) { return; } @@ -1171,6 +1260,9 @@ this, new DataWord(contextAddress), // 4. THE FLAG OF SUCCESS IS ONE PUSHED INTO THE STACK deposit.commit(); stackPushOne(); + if (simulationTracer != null) { + simulationTracer.exitFrame(); + } } if (byTestingSuite()) { @@ -1180,6 +1272,9 @@ this, new DataWord(contextAddress), // 4. THE FLAG OF SUCCESS IS ONE PUSHED INTO THE STACK deposit.commit(); stackPushOne(); + if (simulationTracer != null) { + simulationTracer.exitFrame(); + } } // 3. APPLY RESULTS: result.getHReturn() into out_memory allocated @@ -1658,6 +1753,10 @@ public void callToPrecompiledAddress(MessageCall msg, return; } + if (simulationTracer != null) { + simulationTracer.enterFrame(); + } + Repository deposit = getContractState().newRepositoryChild(); byte[] senderAddress = getContextAddress(); @@ -1685,6 +1784,9 @@ public void callToPrecompiledAddress(MessageCall msg, if (senderBalance < endowment) { stackPushZero(); refundEnergy(msg.getEnergy().longValue(), REFUND_ENERGY_FROM_MESSAGE_CALL); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } return; } byte[] data = this.memoryChunk(msg.getInDataOffs().intValue(), @@ -1697,6 +1799,13 @@ public void callToPrecompiledAddress(MessageCall msg, try { MUtil.transfer(deposit, senderAddress, contextAddress, msg.getEndowment().value().longValueExact()); + if (simulationTracer != null + && msg.getOpCode() != Op.DELEGATECALL + && msg.getOpCode() != Op.CALLCODE) { + simulationTracer.onTransfer(stripTronPrefix(senderAddress), + stripTronPrefix(contextAddress), + msg.getEndowment().value().longValueExact()); + } } catch (ContractValidateException e) { throw new BytecodeExecutionException("transfer failure"); } @@ -1709,6 +1818,13 @@ public void callToPrecompiledAddress(MessageCall msg, } deposit.addTokenBalance(senderAddress, tokenId, -endowment); deposit.addTokenBalance(contextAddress, tokenId, endowment); + if (simulationTracer != null + && msg.getOpCode() != Op.DELEGATECALL + && msg.getOpCode() != Op.CALLCODE) { + simulationTracer.onTokenTransfer(stripTronPrefix(senderAddress), + stripTronPrefix(contextAddress), + Long.parseLong(new String(tokenId)), endowment); + } } } @@ -1718,6 +1834,9 @@ public void callToPrecompiledAddress(MessageCall msg, // regard as consumed the energy this.refundEnergy(0, CALL_PRE_COMPILED); //matches cpp logic this.stackPushZero(); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } } else { // Delegate or not. if is delegated, we will use msg sender, otherwise use contract address if (msg.getOpCode() == Op.DELEGATECALL) { @@ -1737,10 +1856,16 @@ public void callToPrecompiledAddress(MessageCall msg, this.stackPushOne(); returnDataBuffer = out.getRight(); deposit.commit(); + if (simulationTracer != null) { + simulationTracer.exitFrame(); + } } else { // spend all energy on failure, push zero and revert state changes this.refundEnergy(0, CALL_PRE_COMPILED); this.stackPushZero(); + if (simulationTracer != null) { + simulationTracer.revertFrame(); + } if (Objects.nonNull(this.result.getException())) { throw result.getException(); } diff --git a/actuator/src/main/java/org/tron/core/vm/program/listener/BufferingSimulationTracer.java b/actuator/src/main/java/org/tron/core/vm/program/listener/BufferingSimulationTracer.java new file mode 100644 index 00000000000..91a9f710dd2 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/program/listener/BufferingSimulationTracer.java @@ -0,0 +1,124 @@ +package org.tron.core.vm.program.listener; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import org.tron.common.runtime.vm.LogInfo; + +public class BufferingSimulationTracer implements SimulationTracer { + + public enum EntryKind { EXPLICIT, TRANSFER, TOKEN_TRANSFER } + + public static final class Entry { + private final long seq; + private final EntryKind kind; + private final LogInfo logInfo; + private final byte[] fromEvm; + private final byte[] toEvm; + private final long tokenId; + private final long amount; + + private Entry(long seq, EntryKind kind, LogInfo logInfo, + byte[] fromEvm, byte[] toEvm, long tokenId, long amount) { + this.seq = seq; + this.kind = kind; + this.logInfo = logInfo; + this.fromEvm = fromEvm; + this.toEvm = toEvm; + this.tokenId = tokenId; + this.amount = amount; + } + + public long getSeq() { return seq; } + + public EntryKind getKind() { return kind; } + + public LogInfo getLogInfo() { return logInfo; } + + public byte[] getFromEvm() { return fromEvm; } + + public byte[] getToEvm() { return toEvm; } + + public long getTokenId() { return tokenId; } + + public long getAmount() { return amount; } + } + + private final Deque> stack = new ArrayDeque<>(); + private long seq; + + public BufferingSimulationTracer() { + beginCall(); + } + + /** Reset per-call state. Drops any leftover frames and starts a fresh root. */ + public void beginCall() { + stack.clear(); + stack.push(new ArrayList<>()); + } + + /** Drop all buffered entries for the current call (used on top-level revert). */ + public void dropCall() { + stack.clear(); + stack.push(new ArrayList<>()); + } + + /** Returns the root frame's entries in {@code seq} order. */ + public List snapshotCall() { + if (stack.isEmpty()) { + return Collections.emptyList(); + } + List all = new ArrayList<>(stack.peekLast()); + all.sort((a, b) -> Long.compare(a.seq, b.seq)); + return all; + } + + @Override + public void enterFrame() { + stack.push(new ArrayList<>()); + } + + @Override + public void exitFrame() { + if (stack.size() <= 1) { + return; + } + List top = stack.pop(); + stack.peek().addAll(top); + } + + @Override + public void revertFrame() { + if (stack.size() <= 1) { + return; + } + stack.pop(); + } + + @Override + public void onTransfer(byte[] fromEvm, byte[] toEvm, long amount) { + if (stack.isEmpty()) { + return; + } + stack.peek().add(new Entry(seq++, EntryKind.TRANSFER, null, fromEvm, toEvm, 0L, amount)); + } + + @Override + public void onTokenTransfer(byte[] fromEvm, byte[] toEvm, long tokenId, long amount) { + if (stack.isEmpty()) { + return; + } + stack.peek().add(new Entry(seq++, EntryKind.TOKEN_TRANSFER, null, + fromEvm, toEvm, tokenId, amount)); + } + + @Override + public void onLog(LogInfo logInfo) { + if (stack.isEmpty()) { + return; + } + stack.peek().add(new Entry(seq++, EntryKind.EXPLICIT, logInfo, null, null, 0L, 0L)); + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/program/listener/SimulationTracer.java b/actuator/src/main/java/org/tron/core/vm/program/listener/SimulationTracer.java new file mode 100644 index 00000000000..5d571536b0f --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/program/listener/SimulationTracer.java @@ -0,0 +1,42 @@ +package org.tron.core.vm.program.listener; + +import org.tron.common.runtime.vm.LogInfo; + +/** + * Tracer surface for {@code eth_simulateV1}. Captures EVM value transfers + * and explicit LOG opcode emissions into a single ordered stream so the + * RPC response can interleave them by emission order (matching geth's + * {@code OnEnter}/{@code OnLog} interleaving). Implementations are + * expected to keep a per-sub-call frame stack and a shared monotonic + * sequence counter; reverted frames discard their entries. + */ +public interface SimulationTracer { + + /** Push a new frame for a sub-call. */ + void enterFrame(); + + /** Pop the top frame and merge its entries into the parent. */ + void exitFrame(); + + /** Pop the top frame and drop its entries (sub-call reverted or threw). */ + void revertFrame(); + + /** + * Record a value transfer at the current top frame. {@code fromEvm} and + * {@code toEvm} must be EVM 20-byte form (Tron's leading {@code 0x41} + * prefix already stripped) since the bytes flow into ERC-20 + * {@code Transfer(address,address,uint256)} indexed topics. + */ + void onTransfer(byte[] fromEvm, byte[] toEvm, long amount); + + /** + * Record a TRC-10 token transfer at the current top frame. Same address + * encoding rules as {@link #onTransfer}. {@code tokenId} is the Tron asset + * id (matches the {@code TriggerSmartContract.tokenId} proto type); + * {@code amount} is the transferred quantity in the token's smallest unit. + */ + void onTokenTransfer(byte[] fromEvm, byte[] toEvm, long tokenId, long amount); + + /** Record an explicit EVM LOG opcode emission at the current top frame. */ + void onLog(LogInfo logInfo); +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0482643d8d0..f95f4a8d2a6 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -203,6 +203,10 @@ import org.tron.core.store.WitnessStore; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.listener.BufferingSimulationTracer; +import org.tron.core.vm.program.listener.SimulationTracer; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; import org.tron.core.zen.ShieldedTRC20ParametersBuilder; import org.tron.core.zen.ShieldedTRC20ParametersBuilder.ShieldedTRC20ParametersType; import org.tron.core.zen.ZenTransactionBuilder; @@ -3115,22 +3119,8 @@ public Transaction callConstantContract(TransactionCapsule trxCap, throw new ContractValidateException("this node does not support constant"); } - Block headBlock; - List blockCapsuleList = chainBaseManager.getBlockStore() - .getBlockByLatestNum(1); - if (CollectionUtils.isEmpty(blockCapsuleList)) { - throw new HeaderNotFound("latest block not found"); - } else { - headBlock = blockCapsuleList.get(0).getInstance(); - } - - BlockCapsule headBlockCapsule = new BlockCapsule(headBlock); - TransactionContext context = new TransactionContext(headBlockCapsule, trxCap, - StoreFactory.getInstance(), true, false); - VMActuator vmActuator = new VMActuator(true); - - vmActuator.validate(context); - vmActuator.execute(context); + BlockCapsule headBlockCapsule = loadHeadBlockCapsule(); + TransactionContext context = executeOneConstantInternal(trxCap, headBlockCapsule, null, null); ProgramResult result = context.getProgramResult(); if (!isEstimating && result.getException() != null @@ -3164,6 +3154,158 @@ public Transaction callConstantContract(TransactionCapsule trxCap, return trxCap.getInstance(); } + private BlockCapsule loadHeadBlockCapsule() throws HeaderNotFound { + List blockCapsuleList = chainBaseManager.getBlockStore() + .getBlockByLatestNum(1); + if (CollectionUtils.isEmpty(blockCapsuleList)) { + throw new HeaderNotFound("latest block not found"); + } + return new BlockCapsule(blockCapsuleList.get(0).getInstance()); + } + + private TransactionContext executeOneConstantInternal(TransactionCapsule trxCap, + BlockCapsule headBlockCapsule, Repository injectedRoot, SimulationTracer tracer) + throws ContractValidateException, ContractExeException, VMIllegalException { + TransactionContext context = new TransactionContext(headBlockCapsule, trxCap, + StoreFactory.getInstance(), true, false); + VMActuator vmActuator = new VMActuator(true); + if (injectedRoot != null) { + vmActuator.setInjectedRootRepository(injectedRoot); + } + if (tracer != null) { + vmActuator.setSimulationTracer(tracer); + } + vmActuator.validate(context); + vmActuator.execute(context); + return context; + } + + public static final class SimulateCallOutcome { + + private final ProgramResult result; + private final List tracerEntries; + + SimulateCallOutcome(ProgramResult result, List tracerEntries) { + this.result = result; + this.tracerEntries = tracerEntries; + } + + public ProgramResult getResult() { + return result; + } + + public List getTracerEntries() { + return tracerEntries; + } + } + + public static final class SimulateOutcome { + + private final BlockCapsule headBlockCapsule; + private final List calls; + + SimulateOutcome(BlockCapsule headBlockCapsule, List calls) { + this.headBlockCapsule = headBlockCapsule; + this.calls = calls; + } + + public BlockCapsule getHeadBlockCapsule() { + return headBlockCapsule; + } + + public List getCalls() { + return calls; + } + } + + public SimulateOutcome simulateConstantContracts(List trxCaps, + boolean traceTransfers, boolean validation) + throws ContractValidateException, ContractExeException, HeaderNotFound, VMIllegalException { + + if (!Args.getInstance().isSupportConstant()) { + throw new ContractValidateException("this node does not support constant"); + } + + BlockCapsule headBlockCapsule = loadHeadBlockCapsule(); + Repository sharedRoot = RepositoryImpl.createRoot(StoreFactory.getInstance()); + BufferingSimulationTracer tracer = traceTransfers ? new BufferingSimulationTracer() : null; + + List outcomes = new ArrayList<>(trxCaps.size()); + for (int i = 0; i < trxCaps.size(); i++) { + TransactionCapsule trxCap = trxCaps.get(i); + Repository perCallChild = sharedRoot.newRepositoryChild(); + + if (validation) { + String preCheckError = validateSenderForSimulate(trxCap, perCallChild); + if (preCheckError != null) { + ProgramResult synthetic = new ProgramResult(); + synthetic.setException(new RuntimeException(preCheckError)); + outcomes.add(new SimulateCallOutcome(synthetic, java.util.Collections.emptyList())); + continue; + } + } + + if (tracer != null) { + tracer.beginCall(); + } + TransactionContext ctx; + try { + ctx = executeOneConstantInternal(trxCap, headBlockCapsule, perCallChild, tracer); + } catch (RuntimeException e) { + logger.warn("Simulate call {} failed for reason: {}", i, e.getMessage()); + throw e; + } + ProgramResult result = ctx.getProgramResult(); + + List entries; + if (result.getException() != null || result.isRevert()) { + result.getLogInfoList().clear(); + entries = java.util.Collections.emptyList(); + if (tracer != null) { + tracer.dropCall(); + } + } else { + perCallChild.commit(); + entries = tracer != null ? tracer.snapshotCall() : java.util.Collections.emptyList(); + } + outcomes.add(new SimulateCallOutcome(result, entries)); + } + return new SimulateOutcome(headBlockCapsule, outcomes); + } + + private static String validateSenderForSimulate(TransactionCapsule trxCap, + Repository perCallChild) { + if (trxCap.getInstance().getRawData().getContractCount() == 0) { + return null; + } + Contract contract = trxCap.getInstance().getRawData().getContract(0); + byte[] sender; + long callValue; + try { + if (contract.getType() == ContractType.TriggerSmartContract) { + TriggerSmartContract trigger = contract.getParameter().unpack(TriggerSmartContract.class); + sender = trigger.getOwnerAddress().toByteArray(); + callValue = trigger.getCallValue(); + } else if (contract.getType() == ContractType.CreateSmartContract) { + CreateSmartContract create = contract.getParameter().unpack(CreateSmartContract.class); + sender = create.getOwnerAddress().toByteArray(); + callValue = create.getNewContract().getCallValue(); + } else { + return null; + } + } catch (InvalidProtocolBufferException e) { + return "could not parse contract: " + e.getMessage(); + } + AccountCapsule account = perCallChild.getAccount(sender); + if (account == null) { + return "sender account does not exist"; + } + if (account.getBalance() < callValue) { + return "insufficient balance for value: have=" + account.getBalance() + ", want=" + callValue; + } + return null; + } + public SmartContract getContract(GrpcAPI.BytesMessage bytesMessage) { byte[] address = bytesMessage.getValue().toByteArray(); AccountCapsule accountCapsule = chainBaseManager.getAccountStore().get(address); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 50da763b8b9..b164452789d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java @@ -27,6 +27,8 @@ import org.tron.core.services.jsonrpc.types.BlockResult; import org.tron.core.services.jsonrpc.types.BuildArguments; import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.services.jsonrpc.types.SimulateBlockResult; +import org.tron.core.services.jsonrpc.types.SimulateV1Args; import org.tron.core.services.jsonrpc.types.TransactionReceipt; import org.tron.core.services.jsonrpc.types.TransactionResult; import org.tron.json.JSONObject; @@ -169,6 +171,16 @@ String getCall(CallArguments transactionCall, Object blockNumOrTag) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, JsonRpcInternalException; + @JsonRpcMethod("eth_simulateV1") + @JsonRpcErrors({ + @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), + @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), + }) + List ethSimulateV1(SimulateV1Args args, Object blockNumOrTag) + throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, + JsonRpcInternalException; + @JsonRpcMethod("net_peerCount") String getPeerCount(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index 4d919b81ece..11425561922 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -12,6 +12,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.google.common.annotations.VisibleForTesting; @@ -23,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -55,6 +57,7 @@ import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.DecodeUtil; import org.tron.core.Wallet; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; @@ -85,11 +88,16 @@ import org.tron.core.services.jsonrpc.types.BlockResult; import org.tron.core.services.jsonrpc.types.BuildArguments; import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.services.jsonrpc.types.SimulateBlock; +import org.tron.core.services.jsonrpc.types.SimulateBlockResult; +import org.tron.core.services.jsonrpc.types.SimulateCallResult; +import org.tron.core.services.jsonrpc.types.SimulateV1Args; import org.tron.core.services.jsonrpc.types.TransactionReceipt; import org.tron.core.services.jsonrpc.types.TransactionReceipt.TransactionContext; import org.tron.core.services.jsonrpc.types.TransactionResult; import org.tron.core.store.StorageRowStore; import org.tron.core.vm.program.Storage; +import org.tron.core.vm.program.listener.BufferingSimulationTracer; import org.tron.json.JSON; import org.tron.program.Version; import org.tron.protos.Protocol.Account; @@ -1050,6 +1058,297 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) ByteArray.fromHexString(transactionCall.resolveData())); } + private static final int MAX_SIMULATE_CALLS_PER_BLOCK = 32; + private static final String SIMULATE_BLOCK_HASH_PREFIX = "sim:"; + private static final long BLOCK_INTERVAL_MS = 3000L; + private static final String TRANSFER_TOPIC_HEX = + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + /** + * keccak256("TRC10Transfer(address,address,uint256,uint256)") — synthetic + * topic[0] for TRC-10 transfer logs, distinguishing them from ERC-20 + * Transfer (same synthetic-log address, different signature). + */ + private static final String TRC10_TRANSFER_TOPIC_HEX = + ByteArray.toHexString(Hash.sha3( + "TRC10Transfer(address,address,uint256,uint256)" + .getBytes(java.nio.charset.StandardCharsets.UTF_8))); + private static final String ERC7528_NATIVE_ADDRESS = + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + + @Override + public List ethSimulateV1(SimulateV1Args args, Object blockParamObj) + throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, + JsonRpcInternalException { + + if (args == null || args.getBlockStateCalls() == null) { + throw new JsonRpcInvalidParamsException("blockStateCalls is required"); + } + if (args.getBlockStateCalls().size() != 1) { + throw new JsonRpcInvalidParamsException("only single-block simulation supported"); + } + + String blockNumOrTag; + if (blockParamObj == null) { + blockNumOrTag = LATEST_STR; + } else if (blockParamObj instanceof String) { + blockNumOrTag = (String) blockParamObj; + } else { + throw new JsonRpcInvalidParamsException("invalid block tag"); + } + if (!LATEST_STR.equalsIgnoreCase(blockNumOrTag) + && !"pending".equalsIgnoreCase(blockNumOrTag)) { + throw new JsonRpcInvalidParamsException("only latest/pending block tag supported"); + } + + SimulateBlock block = args.getBlockStateCalls().get(0); + if (block == null || block.getCalls() == null) { + throw new JsonRpcInvalidParamsException("calls is required"); + } + if (block.getBlockOverrides() != null) { + throw new JsonRpcInvalidParamsException("blockOverrides not supported"); + } + if (block.getStateOverrides() != null) { + throw new JsonRpcInvalidParamsException("stateOverrides not supported"); + } + if (!block.getUnknown().isEmpty()) { + throw new JsonRpcInvalidParamsException("unknown fields in blockStateCalls[0]: " + + block.getUnknown().keySet()); + } + if (block.getCalls().size() > MAX_SIMULATE_CALLS_PER_BLOCK) { + throw new JsonRpcInvalidParamsException("too many calls; max " + + MAX_SIMULATE_CALLS_PER_BLOCK); + } + + Wallet.SimulateOutcome outcome; + try { + List trxCaps = new ArrayList<>(block.getCalls().size()); + for (CallArguments call : block.getCalls()) { + trxCaps.add(buildSimulateTransactionCapsule(call)); + } + outcome = wallet.simulateConstantContracts(trxCaps, args.isTraceTransfers(), + args.isValidation()); + } catch (ContractValidateException | VMIllegalException e) { + String msg = e.getMessage() != null ? e.getMessage() : CONTRACT_VALIDATE_ERROR; + throw new JsonRpcInvalidRequestException(msg); + } catch (JsonRpcInvalidParamsException | JsonRpcInvalidRequestException e) { + throw e; + } catch (Exception e) { + String msg = e.getMessage() != null ? e.getMessage().replaceAll("[\"]", "'") : JSON_ERROR; + throw new JsonRpcInternalException(msg); + } + + return Collections.singletonList(buildSimulateBlockResult(outcome, block.getCalls(), + args.isTraceTransfers(), args.isReturnFullTransactions())); + } + + private TransactionCapsule buildSimulateTransactionCapsule(CallArguments call) + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException, + ContractValidateException { + byte[] owner = addressCompatibleToByteArray(call.getFrom()); + String resolvedData = call.resolveData(); + byte[] data = resolvedData == null ? new byte[0] : ByteArray.fromHexString(resolvedData); + long value = call.parseValue(); + + if (call.getTo() == null || call.getTo().isEmpty()) { + SmartContract.Builder contract = SmartContract.newBuilder() + .setOriginAddress(ByteString.copyFrom(owner)) + .setBytecode(ByteString.copyFrom(data)) + .setCallValue(value) + .setConsumeUserResourcePercent(100) + .setOriginEnergyLimit(1); + CreateSmartContract.Builder deployBuilder = CreateSmartContract.newBuilder(); + deployBuilder.setOwnerAddress(ByteString.copyFrom(owner)); + deployBuilder.setNewContract(contract.build()); + return wallet.createTransactionCapsule(deployBuilder.build(), + ContractType.CreateSmartContract); + } + + byte[] to = addressCompatibleToByteArray(call.getTo()); + TriggerSmartContract trigger = triggerCallContract(owner, to, value, data, 0L, null); + return wallet.createTransactionCapsule(trigger, ContractType.TriggerSmartContract); + } + + private SimulateBlockResult buildSimulateBlockResult(Wallet.SimulateOutcome outcome, + List calls, boolean traceTransfers, boolean returnFullTransactions) + throws JsonRpcInvalidParamsException { + BlockCapsule head = outcome.getHeadBlockCapsule(); + SimulateBlockResult br = new SimulateBlockResult(); + long headNum = head.getNum(); + byte[] headHash = head.getBlockId().getBytes(); + byte[] simBlockHash = Hash.sha3( + (SIMULATE_BLOCK_HASH_PREFIX + ByteArray.toHexString(headHash) + ":1").getBytes()); + String simBlockHashRaw = ByteArray.toHexString(simBlockHash); + String simBlockHashHex = ByteArray.toJsonHex(simBlockHash); + + br.setNumber(ByteArray.toJsonHex(headNum + 1)); + br.setHash(simBlockHashHex); + br.setParentHash(ByteArray.toJsonHex(headHash)); + br.setNonce(ByteArray.toJsonHex(new byte[8])); + br.setSha3Uncles(ByteArray.toJsonHex(new byte[32])); + br.setLogsBloom(ByteArray.toJsonHex(new byte[256])); + br.setTransactionsRoot(ByteArray.toJsonHex(new byte[32])); + br.setStateRoot(ByteArray.toJsonHex(new byte[32])); + br.setReceiptsRoot(ByteArray.toJsonHex(new byte[32])); + br.setMiner(ByteArray.toJsonHex(new byte[20])); + br.setDifficulty("0x0"); + br.setTotalDifficulty("0x0"); + br.setExtraData("0x"); + br.setSize("0x0"); + br.setGasLimit(ByteArray.toJsonHex(CommonParameter.getInstance().maxEnergyLimitForConstant)); + br.setTimestamp(ByteArray.toJsonHex((head.getTimeStamp() + BLOCK_INTERVAL_MS) / 1000)); + br.setBaseFeePerGas("0x0"); + br.setUncles(new String[0]); + + long totalGasUsed = 0L; + int logIndex = 0; + List callResults = new ArrayList<>(outcome.getCalls().size()); + Object[] transactions = new Object[outcome.getCalls().size()]; + for (int i = 0; i < outcome.getCalls().size(); i++) { + Wallet.SimulateCallOutcome callOutcome = outcome.getCalls().get(i); + org.tron.common.runtime.ProgramResult pr = callOutcome.getResult(); + CallArguments call = calls.get(i); + + byte[] txHashBytes = Hash.sha3( + (SIMULATE_BLOCK_HASH_PREFIX + ByteArray.toHexString(headHash) + ":" + i).getBytes()); + String txHashRaw = ByteArray.toHexString(txHashBytes); + String txHashHex = ByteArray.toJsonHex(txHashBytes); + + SimulateCallResult scr = new SimulateCallResult(); + scr.setReturnData(ByteArray.toJsonHex(pr.getHReturn())); + scr.setGasUsed(ByteArray.toJsonHex(pr.getEnergyUsed())); + scr.setTransactionHash(txHashHex); + scr.setTransactionIndex(ByteArray.toJsonHex(i)); + totalGasUsed += pr.getEnergyUsed(); + + boolean reverted = pr.isRevert(); + boolean failed = pr.getException() != null || reverted; + scr.setStatus(failed ? "0x0" : "0x1"); + + List logs = new ArrayList<>(); + if (!failed) { + byte[] contractAddr = pr.getContractAddress(); + if (contractAddr != null && contractAddr.length > 0) { + scr.setContractAddress(ByteArray.toJsonHexAddress(contractAddr)); + } + for (BufferingSimulationTracer.Entry entry : callOutcome.getTracerEntries()) { + TronJsonRpc.LogFilterElement el = entryToLogFilterElement(entry, simBlockHashRaw, + headNum + 1, txHashRaw, i, logIndex++, traceTransfers); + if (el != null) { + logs.add(el); + } + } + } + scr.setLogs(logs); + + if (failed) { + byte[] revertData = pr.getHReturn(); + if (revertData != null && revertData.length > 0) { + scr.setErrorData(ByteArray.toJsonHex(revertData)); + } + if (reverted) { + scr.setErrorMessage("REVERT opcode executed" + tryDecodeRevertReason(revertData)); + } else if (pr.getException() != null) { + scr.setErrorMessage(pr.getException().getMessage()); + } + } + + callResults.add(scr); + transactions[i] = returnFullTransactions + ? buildFullTransaction(call, txHashHex, simBlockHashHex, headNum + 1, i) + : txHashHex; + } + br.setGasUsed(ByteArray.toJsonHex(totalGasUsed)); + br.setCalls(callResults); + br.setTransactions(transactions); + return br; + } + + private TransactionResult buildFullTransaction(CallArguments call, String txHashHex, + String blockHashHex, long blockNumber, int txIndex) + throws JsonRpcInvalidParamsException { + String fromHex = call.getFrom() == null ? null + : ByteArray.toJsonHexAddress(addressCompatibleToByteArray(call.getFrom())); + String toHex = call.getTo() == null || call.getTo().isEmpty() ? null + : ByteArray.toJsonHexAddress(addressCompatibleToByteArray(call.getTo())); + long gas = call.getGas() == null || call.getGas().isEmpty() + ? 0L : parseQuantityValue(call.getGas()); + long value = call.parseValue(); + String data = call.resolveData(); + String inputHex = data == null ? "0x" : (data.startsWith("0x") ? data : "0x" + data); + return new TransactionResult(txHashHex, blockHashHex, blockNumber, txIndex, + fromHex, toHex, gas, value, inputHex); + } + + private TronJsonRpc.LogFilterElement entryToLogFilterElement( + BufferingSimulationTracer.Entry entry, String blockHashRaw, long blockNum, + String txHashRaw, int callIndex, int logIdx, boolean traceTransfers) { + + String addressRaw; + List topics; + String dataHex; + + if (entry.getKind() == BufferingSimulationTracer.EntryKind.TRANSFER) { + if (!traceTransfers) { + return null; + } + addressRaw = ERC7528_NATIVE_ADDRESS; + topics = new ArrayList<>(3); + topics.add(new DataWord(ByteArray.fromHexString(TRANSFER_TOPIC_HEX))); + topics.add(new DataWord(leftPad32(entry.getFromEvm()))); + topics.add(new DataWord(leftPad32(entry.getToEvm()))); + dataHex = ByteArray.toHexString(leftPad32(longToBytes(entry.getAmount()))); + } else if (entry.getKind() == BufferingSimulationTracer.EntryKind.TOKEN_TRANSFER) { + if (!traceTransfers) { + return null; + } + addressRaw = ERC7528_NATIVE_ADDRESS; + topics = new ArrayList<>(4); + topics.add(new DataWord(ByteArray.fromHexString(TRC10_TRANSFER_TOPIC_HEX))); + topics.add(new DataWord(leftPad32(entry.getFromEvm()))); + topics.add(new DataWord(leftPad32(entry.getToEvm()))); + topics.add(new DataWord(leftPad32(longToBytes(entry.getTokenId())))); + dataHex = ByteArray.toHexString(leftPad32(longToBytes(entry.getAmount()))); + } else { + org.tron.common.runtime.vm.LogInfo li = entry.getLogInfo(); + byte[] addr = li.getAddress(); + if (addr != null && addr.length > 0 && addr[0] == DecodeUtil.addressPreFixByte) { + byte[] stripped = new byte[addr.length - 1]; + System.arraycopy(addr, 1, stripped, 0, stripped.length); + addressRaw = ByteArray.toHexString(stripped); + } else { + addressRaw = addr == null ? "" : ByteArray.toHexString(addr); + } + topics = new ArrayList<>(li.getTopics()); + dataHex = li.getData() == null ? "" : ByteArray.toHexString(li.getData()); + } + + return new TronJsonRpc.LogFilterElement( + blockHashRaw, blockNum, txHashRaw, callIndex, + addressRaw, topics, dataHex, logIdx, false, + System.currentTimeMillis()); + } + + private static byte[] leftPad32(byte[] src) { + if (src == null) { + return new byte[32]; + } + if (src.length >= 32) { + return src; + } + byte[] out = new byte[32]; + System.arraycopy(src, 0, out, 32 - src.length, src.length); + return out; + } + + private static byte[] longToBytes(long v) { + byte[] out = new byte[8]; + for (int i = 7; i >= 0; i--) { + out[i] = (byte) (v & 0xff); + v >>>= 8; + } + return out; + } + @Override public String getPeerCount() { // return the peer list count diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlock.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlock.java new file mode 100644 index 00000000000..691c5e59994 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlock.java @@ -0,0 +1,40 @@ +package org.tron.core.services.jsonrpc.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@NoArgsConstructor +@ToString +public class SimulateBlock { + + @Getter + @Setter + private List calls; + + @Getter + @Setter + private Object blockOverrides; + + @Getter + @Setter + private Object stateOverrides; + + private final Map unknown = new HashMap<>(); + + @JsonAnySetter + public void putUnknown(String key, Object value) { + unknown.put(key, value); + } + + @JsonAnyGetter + public Map getUnknown() { + return unknown; + } +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlockResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlockResult.java new file mode 100644 index 00000000000..7f6b0bab654 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlockResult.java @@ -0,0 +1,14 @@ +package org.tron.core.services.jsonrpc.types; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@JsonPropertyOrder(alphabetic = true) +public class SimulateBlockResult extends BlockResult { + + @Getter + @Setter + private List calls; +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateCallResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateCallResult.java new file mode 100644 index 00000000000..e560bf0032d --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateCallResult.java @@ -0,0 +1,62 @@ +package org.tron.core.services.jsonrpc.types; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; + +@JsonPropertyOrder(alphabetic = true) +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class SimulateCallResult { + + @Getter + @Setter + private String returnData; + + @Getter + @Setter + private String gasUsed; + + @Getter + @Setter + private String status; + + @Getter + @Setter + private List logs; + + @Getter + @Setter + private String transactionHash; + + @Getter + @Setter + private String transactionIndex; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Getter + @Setter + private String contractAddress; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Getter + @Setter + private String errorCode; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Getter + @Setter + private String errorMessage; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Getter + @Setter + private String errorData; +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateV1Args.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateV1Args.java new file mode 100644 index 00000000000..b355fcacc13 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateV1Args.java @@ -0,0 +1,30 @@ +package org.tron.core.services.jsonrpc.types; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class SimulateV1Args { + + @Getter + @Setter + private List blockStateCalls; + + @Getter + @Setter + private boolean traceTransfers; + + @Getter + @Setter + private boolean returnFullTransactions; + + @Getter + @Setter + private boolean validation; +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java index 4f11c1a5908..ff6f93bfc24 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java @@ -157,4 +157,22 @@ public TransactionResult(Transaction tx, Wallet wallet) { parseSignature(tx); } + + public TransactionResult(String hash, String blockHash, long blockNumber, int txIndex, + String from, String to, long gas, long value, String input) { + this.hash = hash; + this.nonce = "0x0"; + this.blockHash = blockHash; + this.blockNumber = ByteArray.toJsonHex(blockNumber); + this.transactionIndex = ByteArray.toJsonHex(txIndex); + this.from = from; + this.to = to; + this.gas = ByteArray.toJsonHex(gas); + this.gasPrice = "0x0"; + this.value = ByteArray.toJsonHex(value); + this.input = input == null ? "0x" : input; + this.v = ByteArray.toJsonHex(new byte[1]); + this.r = ByteArray.toJsonHex(new byte[32]); + this.s = ByteArray.toJsonHex(new byte[32]); + } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java new file mode 100644 index 00000000000..bce464e9a42 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java @@ -0,0 +1,194 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Test; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.services.jsonrpc.types.SimulateBlock; +import org.tron.core.services.jsonrpc.types.SimulateBlockResult; +import org.tron.core.services.jsonrpc.types.SimulateCallResult; +import org.tron.core.services.jsonrpc.types.SimulateV1Args; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class EthSimulateV1ArgsTest { + + private TronJsonRpcImpl rpc; + + @After + public void tearDown() throws Exception { + if (rpc != null) { + rpc.close(); + rpc = null; + } + } + + @Test + public void rejectsNullBlockStateCalls() throws Exception { + rpc = newRpc(); + SimulateV1Args args = new SimulateV1Args(); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("blockStateCalls")); + } + + @Test + public void rejectsEmptyBlockStateCalls() throws Exception { + rpc = newRpc(); + SimulateV1Args args = new SimulateV1Args(); + args.setBlockStateCalls(new ArrayList<>()); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("single-block")); + } + + @Test + public void rejectsMultipleBlocks() throws Exception { + rpc = newRpc(); + SimulateV1Args args = new SimulateV1Args(); + args.setBlockStateCalls(Arrays.asList(emptyBlock(), emptyBlock())); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("single-block")); + } + + @Test + public void rejectsBlockOverrides() throws Exception { + rpc = newRpc(); + SimulateBlock block = emptyBlock(); + block.setBlockOverrides(new ObjectMapper().createObjectNode()); + SimulateV1Args args = wrap(block); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("blockOverrides")); + } + + @Test + public void rejectsStateOverrides() throws Exception { + rpc = newRpc(); + SimulateBlock block = emptyBlock(); + block.setStateOverrides(new ObjectMapper().createObjectNode()); + SimulateV1Args args = wrap(block); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("stateOverrides")); + } + + @Test + public void rejectsUnknownBlockField() throws Exception { + rpc = newRpc(); + String json = "{\"calls\":[],\"futureFeature\":42}"; + SimulateBlock block = new ObjectMapper().readValue(json, SimulateBlock.class); + SimulateV1Args args = wrap(block); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("unknown")); + assertTrue(e.getMessage().contains("futureFeature")); + } + + @Test + public void rejectsTooManyCalls() throws Exception { + rpc = newRpc(); + SimulateBlock block = emptyBlock(); + List calls = new ArrayList<>(); + for (int i = 0; i < 33; i++) { + calls.add(new CallArguments()); + } + block.setCalls(calls); + SimulateV1Args args = wrap(block); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("too many")); + } + + @Test + public void rejectsHexBlockNumberTag() throws Exception { + rpc = newRpc(); + SimulateV1Args args = wrap(emptyBlock()); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "0x12345")); + assertTrue(e.getMessage().contains("latest")); + } + + @Test + public void rejectsEarliestTag() throws Exception { + rpc = newRpc(); + SimulateV1Args args = wrap(emptyBlock()); + assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "earliest")); + } + + @Test + public void simulateBlockResultJsonRoundTrip() throws Exception { + SimulateBlockResult result = new SimulateBlockResult(); + result.setNumber("0x1"); + result.setHash("0xabc"); + SimulateCallResult c0 = new SimulateCallResult(); + c0.setStatus("0x1"); + c0.setReturnData("0xdeadbeef"); + SimulateCallResult c1 = new SimulateCallResult(); + c1.setStatus("0x0"); + c1.setReturnData("0x"); + result.setCalls(Arrays.asList(c0, c1)); + result.setTransactions(new Object[] {"0xhash0", "0xhash1"}); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(result); + ObjectNode parsed = (ObjectNode) mapper.readTree(json); + + ArrayNode calls = (ArrayNode) parsed.get("calls"); + assertNotNull("calls field must be present", calls); + assertEquals(2, calls.size()); + assertEquals("0x1", calls.get(0).get("status").asText()); + assertEquals("0xdeadbeef", calls.get(0).get("returnData").asText()); + + ArrayNode txs = (ArrayNode) parsed.get("transactions"); + assertNotNull("transactions field must be present", txs); + assertEquals(2, txs.size()); + assertEquals("0xhash0", txs.get(0).asText()); + } + + private static SimulateBlock emptyBlock() { + SimulateBlock block = new SimulateBlock(); + block.setCalls(new ArrayList<>()); + return block; + } + + private static SimulateV1Args wrap(SimulateBlock block) { + SimulateV1Args args = new SimulateV1Args(); + args.setBlockStateCalls(Collections.singletonList(block)); + return args; + } + + private static TronJsonRpcImpl newRpc() throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet); + rpc.setManager(mockManager); + return rpc; + } +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java new file mode 100644 index 00000000000..453456b2c30 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java @@ -0,0 +1,357 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpc; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.services.jsonrpc.types.SimulateBlock; +import org.tron.core.services.jsonrpc.types.SimulateBlockResult; +import org.tron.core.services.jsonrpc.types.SimulateCallResult; +import org.tron.core.services.jsonrpc.types.SimulateV1Args; +import org.tron.core.services.jsonrpc.types.TransactionResult; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass; + +@Slf4j +public class EthSimulateV1IntegrationTest extends BaseTest { + + // SimpleStorage.sol — uint256 value; set/get/setRevert(uint256). solc 0.8.35 + // --evm-version paris --optimize --metadata-hash none. + private static final String SIMPLE_STORAGE_BYTECODE = + "6080604052348015600f57600080fd5b5060f08061001e6000396000f3fe6080604052348015600f57" + + "600080fd5b506004361060465760003560e01c80632e8f88e614604b5780633fa4f24514605c5780" + + "6360fe47b11460765780636d4ce63c146086575b600080fd5b605a605636600460cb565b608d565b" + + "005b606460005481565b60405190815260200160405180910390f35b605a608136600460cb565b60" + + "0055565b6000546064565b600081905560405162461bcd60e51b815260c290600401602080825260" + + "0490820152636e6f706560e01b604082015260600190565b60405180910390fd5b60006020828403" + + "121560dc57600080fd5b503591905056fea164736f6c6343000823000a"; + + private static final String SEL_SET = "60fe47b1"; + private static final String SEL_GET = "6d4ce63c"; + private static final String SEL_SET_REVERT = "2e8f88e6"; + + private static final String OWNER_ADDRESS; + private static final String STORAGE_TRON_ADDR_HEX; + private static final String STORAGE_EVM_ADDR_HEX_PREFIXED; + private static final long OWNER_BALANCE = 10_000_000_000L; + + static { + Args.setParam(new String[] {"--output-directory", dbPath(), "--debug"}, + TestConstants.TEST_CONF); + Args.getInstance().setSupportConstant(true); + OWNER_ADDRESS = Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc"; + // Pre-installed SimpleStorage contract address — same address used by @Before + // to write runtime bytecode + ContractCapsule into the chain stores. + STORAGE_TRON_ADDR_HEX = Wallet.getAddressPreFixString() + + "00000000000000000000000000000000000c0de1"; + STORAGE_EVM_ADDR_HEX_PREFIXED = "0x00000000000000000000000000000000000c0de1"; + } + + @Resource + private NodeInfoService nodeInfoService; + @Resource + private Wallet wallet; + + private TronJsonRpcImpl tronJsonRpc; + private byte[] ownerBytes; + + @Before + public void init() { + // Enable post-Byzantium opcodes (SHR/SHL/SAR via Constantinople, etc.) so + // solc 0.8.x dispatch bytecode runs. ConfigLoader.disable=true prevents the + // chain's dynamic-properties store from reloading these flags back to 0 + // — same pattern as AllowTvmCompatibleEvmTest.beforeClass(). + ConfigLoader.disable = true; + VMConfig.initAllowTvmConstantinople(1); + VMConfig.initAllowTvmSolidity059(1); + VMConfig.initAllowTvmIstanbul(1); + VMConfig.initAllowTvmLondon(1); + VMConfig.initAllowTvmCompatibleEvm(1); + // Mainnet has long passed this hardfork; the flag governs whether child + // RepositoryImpl deep-copies the parent's Storage on first read. Without it, + // child mutations alias the parent and reverted SSTOREs leak across calls + // (RepositoryImpl.getStorage, ~line 715). + CommonParameter.ENERGY_LIMIT_HARD_FORK = true; + + ownerBytes = ByteArray.fromHexString(OWNER_ADDRESS); + AccountCapsule owner = new AccountCapsule(ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ownerBytes), Protocol.AccountType.Normal, OWNER_BALANCE); + dbManager.getAccountStore().put(ownerBytes, owner); + + long headNum = 1L; + BlockCapsule head = new BlockCapsule(headNum, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "0304f784e4e7bae517bcab94c3e0c9214fb4ac7ff9d7d5a937d1f40031f87b81"))), + System.currentTimeMillis(), + ByteString.copyFrom(ownerBytes)); + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(headNum); + dbManager.getBlockIndexStore().put(head.getBlockId()); + dbManager.getBlockStore().put(head.getBlockId().getBytes(), head); + + // Pre-install SimpleStorage's runtime bytecode at STORAGE_TRON_ADDR so the + // trigger tests can resolve the contract via the on-chain contract store + // (VMActuator.validate rejects unknown contract addresses). Mirrors the + // pattern from TriggerSmartContractServletTest.before(). + byte[] storageAddr = ByteArray.fromHexString(STORAGE_TRON_ADDR_HEX); + Repository rootRepository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + rootRepository.createAccount(storageAddr, Protocol.AccountType.Contract); + rootRepository.createContract(storageAddr, new ContractCapsule( + SmartContractOuterClass.SmartContract.newBuilder() + .setContractAddress(ByteString.copyFrom(storageAddr)) + .build())); + rootRepository.saveCode(storageAddr, + ByteArray.fromHexString(simpleStorageRuntimeBytecode())); + rootRepository.commit(); + + tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet); + tronJsonRpc.setManager(dbManager); + } + + /** + * Extracts the runtime portion of {@link #SIMPLE_STORAGE_BYTECODE}. The deploy + * bytecode is the 30-byte constructor (`...600080fd5b5060f08061001e6000396000f3fe`, + * which CODECOPYs 0xf0 bytes from offset 0x1e and RETURNs them) followed by + * the runtime. So `runtime = init[60:]`. + */ + private static String simpleStorageRuntimeBytecode() { + return SIMPLE_STORAGE_BYTECODE.substring(60); + } + + /** + * State sharing across calls: set(42) → get() must read 42 from the same + * simulate request. If perCallChild.commit() isn't flushing into sharedRoot + * between calls, call 2 reads 0 and the test fails. + */ + @Test + public void stateSharingAcrossCalls() throws Exception { + SimulateV1Args args = newArgs(false, false, false, + triggerCall(STORAGE_EVM_ADDR_HEX_PREFIXED, SEL_SET, padUint256(42), 0L), + triggerCall(STORAGE_EVM_ADDR_HEX_PREFIXED, SEL_GET, "", 0L)); + + List calls = tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls(); + + assertEquals(2, calls.size()); + assertEquals("call 1 (set) must succeed", "0x1", calls.get(0).getStatus()); + assertEquals("call 2 (get) must succeed", "0x1", calls.get(1).getStatus()); + assertEquals("call 2 must observe call 1's write", + BigInteger.valueOf(42), parseHex(calls.get(1).getReturnData())); + } + + /** + * Revert isolation: set(99) → setRevert(123) → get(). The reverted call's + * storage write must NOT be visible to the subsequent get. + */ + @Test + public void revertIsolatesPerCall() throws Exception { + SimulateV1Args args = newArgs(false, false, false, + triggerCall(STORAGE_EVM_ADDR_HEX_PREFIXED, SEL_SET, padUint256(99), 0L), + triggerCall(STORAGE_EVM_ADDR_HEX_PREFIXED, SEL_SET_REVERT, padUint256(123), 0L), + triggerCall(STORAGE_EVM_ADDR_HEX_PREFIXED, SEL_GET, "", 0L)); + + List calls = tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls(); + + assertEquals(3, calls.size()); + assertEquals("0x1", calls.get(0).getStatus()); + assertEquals("0x0", calls.get(1).getStatus()); + assertEquals("0x1", calls.get(2).getStatus()); + assertEquals("call 3 must see call 1's value 99, not the reverted call 2's 123", + BigInteger.valueOf(99), parseHex(calls.get(2).getReturnData())); + } + + /** + * validation=true must reject a sender that has no account on chain. + */ + @Test + public void validationRejectsUnactivatedSender() throws Exception { + String freshFrom = "0x" + "00000000000000000000000000000000deadbeef"; + CallArguments c = new CallArguments(); + c.setFrom(freshFrom); + c.setTo(OWNER_ADDRESS_HEX_PREFIXED()); + c.setValue("0x1"); + c.setData("0x"); + SimulateV1Args args = newArgs(false, true, false, c); + + List calls = tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls(); + assertEquals(1, calls.size()); + assertEquals("0x0", calls.get(0).getStatus()); + assertNotNull(calls.get(0).getErrorMessage()); + assertTrue("got: " + calls.get(0).getErrorMessage(), + calls.get(0).getErrorMessage().contains("sender account does not exist")); + } + + /** + * validation=true must reject when callValue exceeds the sender's balance. + */ + @Test + public void validationRejectsInsufficientBalance() throws Exception { + CallArguments c = new CallArguments(); + c.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); + c.setTo(OWNER_ADDRESS_HEX_PREFIXED()); + // 2x OWNER_BALANCE + c.setValue("0x" + Long.toHexString(OWNER_BALANCE * 2)); + c.setData("0x"); + SimulateV1Args args = newArgs(false, true, false, c); + + List calls = tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls(); + assertEquals(1, calls.size()); + assertEquals("0x0", calls.get(0).getStatus()); + assertTrue("got: " + calls.get(0).getErrorMessage(), + calls.get(0).getErrorMessage().contains("insufficient balance")); + } + + /** + * CREATE simulation populates contractAddress and the address is a real EVM + * 20-byte form (no Tron 0x41 prefix when serialized as JSON via + * ByteArray.toJsonHexAddress). + */ + @Test + public void createPopulatesContractAddress() throws Exception { + SimulateV1Args args = newArgs(false, false, false, createCall(SIMPLE_STORAGE_BYTECODE)); + List calls = tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls(); + assertEquals(1, calls.size()); + assertEquals("0x1", calls.get(0).getStatus()); + String addr = calls.get(0).getContractAddress(); + assertNotNull(addr); + assertTrue("contractAddress must be 0x-prefixed 20-byte hex, got: " + addr, + addr.startsWith("0x") && addr.length() == 42); + } + + /** + * Drops TRC-10 transfer entries from a reverted sub-call frame — same + * isolation discipline the TRX transfer hooks rely on. Direct buffer + * exercise: enterFrame → onTokenTransfer → revertFrame must clear it. + */ + @Test + public void buffering_dropsTokenTransferOnRevertFrame() { + org.tron.core.vm.program.listener.BufferingSimulationTracer t = + new org.tron.core.vm.program.listener.BufferingSimulationTracer(); + t.beginCall(); + t.enterFrame(); + t.onTokenTransfer(new byte[20], new byte[20], 1_000_001L, 50L); + t.revertFrame(); + assertEquals(0, t.snapshotCall().size()); + } + + /** + * returnFullTransactions changes the shape of `transactions[]`: + * default → array of hash strings; + * true → array of TransactionResult objects (same hash, gasPrice=0x0, etc.). + */ + @Test + public void returnFullTransactionsShape() throws Exception { + SimulateBlockResult hashOnly = tronJsonRpc.ethSimulateV1( + newArgs(false, false, false, createCall(SIMPLE_STORAGE_BYTECODE)), + "latest").get(0); + Object[] hashTxs = hashOnly.getTransactions(); + assertEquals(1, hashTxs.length); + assertTrue("default transactions[] should be hash strings, got: " + hashTxs[0].getClass(), + hashTxs[0] instanceof String); + assertEquals(hashOnly.getCalls().get(0).getTransactionHash(), hashTxs[0]); + + SimulateBlockResult fullTx = tronJsonRpc.ethSimulateV1( + newArgs(false, false, true, createCall(SIMPLE_STORAGE_BYTECODE)), + "latest").get(0); + Object[] fullTxs = fullTx.getTransactions(); + assertEquals(1, fullTxs.length); + assertTrue("with returnFullTransactions=true, entry must be TransactionResult, got: " + + fullTxs[0].getClass(), + fullTxs[0] instanceof TransactionResult); + TransactionResult tx = (TransactionResult) fullTxs[0]; + assertEquals(fullTx.getCalls().get(0).getTransactionHash(), tx.getHash()); + assertEquals("0x0", tx.getGasPrice()); + assertEquals("0x0", tx.getNonce()); + assertNotNull(tx.getFrom()); + // CREATE → to is null + assertEquals(null, tx.getTo()); + // The synthetic block hash must match between the two runs (deterministic + // from head block hash). + assertEquals(hashOnly.getHash(), fullTx.getHash()); + // tx hashes are deterministic per (block, callIndex), so they must match. + assertEquals(hashOnly.getCalls().get(0).getTransactionHash(), tx.getHash()); + } + + // ---- helpers ---- + + private static SimulateV1Args newArgs(boolean traceTransfers, boolean validation, + boolean returnFullTransactions, CallArguments... calls) { + SimulateBlock block = new SimulateBlock(); + block.setCalls(new ArrayList<>(Arrays.asList(calls))); + SimulateV1Args args = new SimulateV1Args(); + args.setBlockStateCalls(new ArrayList<>(Collections.singletonList(block))); + args.setTraceTransfers(traceTransfers); + args.setValidation(validation); + args.setReturnFullTransactions(returnFullTransactions); + return args; + } + + /** CREATE call from the test owner with the given init bytecode. */ + private CallArguments createCall(String initBytecodeHex) { + CallArguments args = new CallArguments(); + args.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); + args.setTo(null); + args.setValue("0x0"); + args.setData("0x" + initBytecodeHex); + return args; + } + + /** TriggerSmartContract call against an explicit contract address. */ + private CallArguments triggerCall(String to, String selector, String calldataTail, long value) { + CallArguments args = new CallArguments(); + args.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); + args.setTo(to); + args.setValue(value == 0 ? "0x0" : "0x" + Long.toHexString(value)); + args.setData("0x" + selector + (calldataTail == null ? "" : calldataTail)); + return args; + } + + private static String OWNER_ADDRESS_HEX_PREFIXED() { + return "0x" + OWNER_ADDRESS; + } + + private static String padUint256(long v) { + String h = Long.toHexString(v); + StringBuilder sb = new StringBuilder(); + for (int i = h.length(); i < 64; i++) { + sb.append('0'); + } + sb.append(h); + return sb.toString(); + } + + private static BigInteger parseHex(String hex) { + if (hex == null || hex.isEmpty() || "0x".equals(hex)) { + return BigInteger.ZERO; + } + return new BigInteger(hex.startsWith("0x") ? hex.substring(2) : hex, 16); + } + +}