From 0aa7f6a9e28a80cf32fc2ec668d7951ecd618019 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 19 May 2026 14:41:15 +0100 Subject: [PATCH 1/4] implement eth_simulateV1 JSON-RPC method for trading-flow case --- .../org/tron/core/actuator/VMActuator.java | 38 +- .../org/tron/core/vm/OperationActions.java | 3 + .../org/tron/core/vm/program/Program.java | 131 ++++- .../listener/BufferingSimulationTracer.java | 124 +++++ .../vm/program/listener/SimulationTracer.java | 42 ++ .../src/main/java/org/tron/core/Wallet.java | 173 +++++- .../core/services/jsonrpc/TronJsonRpc.java | 12 + .../services/jsonrpc/TronJsonRpcImpl.java | 313 +++++++++++ .../services/jsonrpc/types/CallArguments.java | 24 + .../services/jsonrpc/types/SimulateBlock.java | 40 ++ .../jsonrpc/types/SimulateBlockResult.java | 14 + .../jsonrpc/types/SimulateCallResult.java | 62 +++ .../jsonrpc/types/SimulateV1Args.java | 30 + .../jsonrpc/types/TransactionResult.java | 18 + .../core/jsonrpc/EthSimulateV1ArgsTest.java | 208 +++++++ .../jsonrpc/EthSimulateV1IntegrationTest.java | 516 ++++++++++++++++++ .../services/jsonrpc/BuildArgumentsTest.java | 2 +- .../services/jsonrpc/CallArgumentsTest.java | 2 +- 18 files changed, 1730 insertions(+), 22 deletions(-) create mode 100644 actuator/src/main/java/org/tron/core/vm/program/listener/BufferingSimulationTracer.java create mode 100644 actuator/src/main/java/org/tron/core/vm/program/listener/SimulationTracer.java create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlock.java create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateBlockResult.java create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateCallResult.java create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/types/SimulateV1Args.java create mode 100644 framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java create mode 100644 framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java 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..bd600bece81 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -199,6 +199,10 @@ import org.tron.core.store.MarketPairPriceToOrderStore; import org.tron.core.store.MarketPairToPriceStore; import org.tron.core.store.StoreFactory; +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.store.VotesStore; import org.tron.core.store.WitnessStore; import org.tron.core.utils.TransactionUtil; @@ -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,157 @@ 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..d9b9d3355fb 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; @@ -55,6 +56,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,9 +87,14 @@ 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.vm.program.listener.BufferingSimulationTracer; import org.tron.core.store.StorageRowStore; import org.tron.core.vm.program.Storage; import org.tron.json.JSON; @@ -1050,6 +1057,312 @@ 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 List.of(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(); + String tokenIdStr = call.getTokenId(); + long tokenValue = call.parseTokenValue(); + long tokenId = 0L; + if (tokenIdStr != null && !tokenIdStr.isEmpty()) { + try { + tokenId = Long.parseLong(tokenIdStr); + } catch (NumberFormatException e) { + throw new JsonRpcInvalidParamsException("invalid tokenId: " + tokenIdStr); + } + } + + 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()); + if (tokenIdStr != null && !tokenIdStr.isEmpty()) { + deployBuilder.setCallTokenValue(tokenValue); + deployBuilder.setTokenId(tokenId); + } + return wallet.createTransactionCapsule(deployBuilder.build(), + ContractType.CreateSmartContract); + } + + byte[] to = addressCompatibleToByteArray(call.getTo()); + TriggerSmartContract trigger = triggerCallContract(owner, to, value, data, tokenValue, + (tokenIdStr == null || tokenIdStr.isEmpty()) ? null : tokenIdStr); + 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/CallArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java index 1715636a2a4..9e7beedbc46 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java @@ -49,6 +49,22 @@ public class CallArguments { @Getter @Setter private String nonce; // not used + /** + * Tron-specific extension for {@code eth_simulateV1}: decimal-string TRC-10 + * asset id (matches {@link org.tron.core.services.jsonrpc.JsonRpcApiUtil + * #triggerCallContract}'s 6th arg). Null/empty means "no TRC-10 transfer". + */ + @Getter + @Setter + private String tokenId; + /** + * Tron-specific extension for {@code eth_simulateV1}: hex-quantity TRC-10 + * amount (e.g. {@code "0x64"} = 100). Null/empty means 0. Ignored when + * {@code tokenId} is null/empty. + */ + @Getter + @Setter + private String tokenValue; /** * Returns {@code input} if non-null, else {@code data}. Pure @@ -108,4 +124,12 @@ public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestE public long parseValue() throws JsonRpcInvalidParamsException { return parseQuantityValue(value); } + + /** Parsed TRC-10 token amount; 0 when {@code tokenValue} is null/empty. */ + public long parseTokenValue() throws JsonRpcInvalidParamsException { + if (tokenValue == null || tokenValue.isEmpty()) { + return 0L; + } + return parseQuantityValue(tokenValue); + } } \ No newline at end of file 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..3e92bd64106 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java @@ -0,0 +1,208 @@ +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.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(List.of(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 rejectsNonNumericTokenId() throws Exception { + rpc = newRpc(); + CallArguments c = new CallArguments(); + c.setFrom("0x0000000000000000000000000000000000000000"); + c.setTo("0x0000000000000000000000000000000000000001"); + c.setData("0x"); + c.setTokenId("not-a-number"); + SimulateBlock block = new SimulateBlock(); + block.setCalls(List.of(c)); + SimulateV1Args args = wrap(block); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> rpc.ethSimulateV1(args, "latest")); + assertTrue(e.getMessage().contains("tokenId")); + } + + @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(List.of(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(List.of(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..3fe6fa29d00 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java @@ -0,0 +1,516 @@ +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.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.AssetIssueCapsule; +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.AssetIssueContractOuterClass.AssetIssueContract; +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"; + + // ERC-7528 native pseudo-address (TRX + TRC-10 synthetic logs share it). + private static final String ERC7528_NATIVE_LOWER = + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + // keccak256("Transfer(address,address,uint256)"). + private static final String TRANSFER_TOPIC_LOWER = + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + // TRC-10 testing token (>=1_000_001 required by VMConstant.MIN_TOKEN_ID). + private static final String TRC10_TOKEN_ID = "1000001"; + + 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 String SINK_TRON_ADDR_HEX; + private static final String SINK_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"; + // "Accept-anything" sink: runtime bytecode is `00` (STOP). No Solidity + // non-payable guard, so it accepts both TRX value and TRC-10 transfers + // without reverting. Used by the mixed TRX+TRC-10 test where the regular + // Solidity-compiled SimpleStorage would reject msg.value > 0. + SINK_TRON_ADDR_HEX = Wallet.getAddressPreFixString() + + "00000000000000000000000000000000000c0de2"; + SINK_EVM_ADDR_HEX_PREFIXED = "0x00000000000000000000000000000000000c0de2"; + } + + @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.initAllowTvmTransferTrc10(1); + 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())); + + // Sink contract — `00` (STOP) so it accepts arbitrary calldata, TRX value, + // and TRC-10 transfers without reverting. + byte[] sinkAddr = ByteArray.fromHexString(SINK_TRON_ADDR_HEX); + rootRepository.createAccount(sinkAddr, Protocol.AccountType.Contract); + rootRepository.createContract(sinkAddr, new ContractCapsule( + SmartContractOuterClass.SmartContract.newBuilder() + .setContractAddress(ByteString.copyFrom(sinkAddr)) + .build())); + rootRepository.saveCode(sinkAddr, new byte[] {0x00}); + + 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); + } + + /** + * Top-level TRC-10 transfer (depth 0): owner sends 50 units of token + * 1000001 to the pre-deployed SimpleStorage contract, invoking get() + * (a view function that returns the slot value — picked because it + * doesn't revert on incoming TRC-10). + * + *

Expect exactly one synthetic log on the call result: + * address = ERC-7528 native pseudo-address (lowercased), topic[0] = + * keccak256("TRC10Transfer(address,address,uint256,uint256)"), + * topic[1] = padded sender (EVM 20-byte form), topic[2] = padded + * recipient, topic[3] = padded uint256(tokenId), data = padded + * uint256(amount). + */ + @Test + public void traceTrc10TopLevelCall() throws Exception { + seedTrc10(500L); + + CallArguments c = new CallArguments(); + c.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); + c.setTo(STORAGE_EVM_ADDR_HEX_PREFIXED); + c.setData("0x" + SEL_GET); + c.setTokenId(TRC10_TOKEN_ID); + c.setTokenValue("0x32"); // 50 + SimulateV1Args args = newArgs(true, false, false, c); + + SimulateCallResult call = + tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls().get(0); + + assertEquals("0x1", call.getStatus()); + assertEquals("expected exactly one synthetic TRC10Transfer log", + 1, call.getLogs().size()); + + TronJsonRpc.LogFilterElement log = call.getLogs().get(0); + assertEquals(ERC7528_NATIVE_LOWER, log.getAddress()); + String[] topics = log.getTopics(); + assertEquals(4, topics.length); + assertEquals(trc10TransferTopic(), topics[0]); + assertEquals(padAddressTopic(OWNER_ADDRESS.substring(2)), topics[1]); + assertEquals(padAddressTopic(STORAGE_EVM_ADDR_HEX_PREFIXED.substring(2)), topics[2]); + assertEquals(padUint256Hex(1_000_001L), topics[3]); + assertEquals(padUint256Hex(50L), log.getData()); + + // No commit to disk: owner's TRC-10 balance is unchanged. + AccountCapsule reread = dbManager.getAccountStore().get(ownerBytes); + assertEquals(Long.valueOf(500L), reread.getAssetMapV2().get(TRC10_TOKEN_ID)); + } + + /** + * Mixed top-level transfer: a single call with both {@code value > 0} + * (TRX) and {@code tokenValue > 0} (TRC-10). Both synthetic logs must + * appear in the same call result with consecutive {@code logIndex} — + * TRX first, then TRC-10 — matching VMActuator's depth-0 emission order + * at lines 559-569 (TRX block before TRC-10 block). + */ + @Test + public void traceTrc10MixedWithTrx() throws Exception { + seedTrc10(500L); + + CallArguments c = new CallArguments(); + c.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); + // Sink accepts arbitrary calldata + value; SimpleStorage would revert + // because Solidity inlines a non-payable check on every external method. + c.setTo(SINK_EVM_ADDR_HEX_PREFIXED); + c.setData("0x"); + c.setValue("0x64"); // 100 sun TRX + c.setTokenId(TRC10_TOKEN_ID); + c.setTokenValue("0x32"); // 50 TRC-10 + SimulateV1Args args = newArgs(true, false, false, c); + + SimulateCallResult call = + tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls().get(0); + + assertEquals("0x1", call.getStatus()); + assertEquals("expected two synthetic transfer logs (TRX + TRC-10)", + 2, call.getLogs().size()); + + TronJsonRpc.LogFilterElement trxLog = call.getLogs().get(0); + TronJsonRpc.LogFilterElement trc10Log = call.getLogs().get(1); + assertEquals(TRANSFER_TOPIC_LOWER, trxLog.getTopics()[0]); + assertEquals(trc10TransferTopic(), trc10Log.getTopics()[0]); + assertEquals(padUint256Hex(100L), trxLog.getData()); + assertEquals(padUint256Hex(50L), trc10Log.getData()); + assertEquals(padUint256Hex(1_000_001L), trc10Log.getTopics()[3]); + } + + /** + * 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<>(List.of(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); + } + + /** + * Register an AssetIssue for {@link #TRC10_TOKEN_ID} in the V2 store + * (V2 keys by tokenId, matching {@code AllowSameTokenName == 1}) and + * seed the owner's account with the requested balance. VMUtils + * validateForSmartContract requires both the AssetIssue and a non-zero + * owner balance to allow the transfer. + */ + private void seedTrc10(long ownerAmount) { + dbManager.getDynamicPropertiesStore().saveAllowSameTokenName(1L); + AssetIssueContract asset = AssetIssueContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ownerBytes)) + .setName(ByteString.copyFromUtf8("TRC10")) + .setId(TRC10_TOKEN_ID) + .setTotalSupply(1_000_000_000L) + .setTrxNum(1) + .setNum(1) + .build(); + AssetIssueCapsule cap = new AssetIssueCapsule(asset); + dbManager.getAssetIssueV2Store().put(cap.createDbV2Key(), cap); + + AccountCapsule owner = dbManager.getAccountStore().get(ownerBytes); + owner.setInstance(owner.getInstance().toBuilder() + .putAssetV2(TRC10_TOKEN_ID, ownerAmount) + .build()); + dbManager.getAccountStore().put(ownerBytes, owner); + } + + /** Lower-case hex of keccak256("TRC10Transfer(address,address,uint256,uint256)"). */ + private static String trc10TransferTopic() { + return "0x" + ByteArray.toHexString(org.tron.common.crypto.Hash.sha3( + "TRC10Transfer(address,address,uint256,uint256)" + .getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } + + /** Pad a 20-byte EVM address (hex without 0x) to a 32-byte topic hex string with 0x prefix. */ + private static String padAddressTopic(String evmHex20) { + return "0x" + "0".repeat(24) + evmHex20.toLowerCase(java.util.Locale.ROOT); + } + + /** uint256 hex of a non-negative long, 0x-prefixed and left-padded to 32 bytes. */ + private static String padUint256Hex(long v) { + return "0x" + padUint256(v); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 753d93d47f4..2c8f54517ea 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -41,7 +41,7 @@ public void testBuildArgument() { CallArguments callArguments = new CallArguments( "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", - "", "", "0"); + "", "", "0", null, null); BuildArguments args = new BuildArguments(callArguments); Assert.assertEquals("0x0000000000000000000000000000000000000000", args.getFrom()); Assert.assertEquals("0x0000000000000000000000000000000000000001", args.getTo()); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java index 4ebfc3c1872..0f3a95ef2ad 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java @@ -31,7 +31,7 @@ public void init() { "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", - "", "", "0"); + "", "", "0", null, null); } @Test From 42eb1037cd6e2f3e9e21809cb2f6bdf454d6fd5d Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 19 May 2026 16:22:08 +0100 Subject: [PATCH 2/4] fix linter --- framework/src/main/java/org/tron/core/Wallet.java | 11 ++++++----- .../tron/core/services/jsonrpc/TronJsonRpcImpl.java | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index bd600bece81..f95f4a8d2a6 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -199,14 +199,14 @@ import org.tron.core.store.MarketPairPriceToOrderStore; import org.tron.core.store.MarketPairToPriceStore; import org.tron.core.store.StoreFactory; -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.store.VotesStore; 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; @@ -3273,7 +3273,8 @@ public SimulateOutcome simulateConstantContracts(List trxCap return new SimulateOutcome(headBlockCapsule, outcomes); } - private static String validateSenderForSimulate(TransactionCapsule trxCap, Repository perCallChild) { + private static String validateSenderForSimulate(TransactionCapsule trxCap, + Repository perCallChild) { if (trxCap.getInstance().getRawData().getContractCount() == 0) { return null; } 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 d9b9d3355fb..5fa0880832f 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 @@ -24,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; @@ -94,9 +95,9 @@ 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.vm.program.listener.BufferingSimulationTracer; 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; @@ -1136,7 +1137,7 @@ public List ethSimulateV1(SimulateV1Args args, Object block throw new JsonRpcInternalException(msg); } - return List.of(buildSimulateBlockResult(outcome, block.getCalls(), + return Collections.singletonList(buildSimulateBlockResult(outcome, block.getCalls(), args.isTraceTransfers(), args.isReturnFullTransactions())); } From 941346654464fe7f476b46fb75edfd068f91df8f Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 19 May 2026 16:29:19 +0100 Subject: [PATCH 3/4] fix linter --- .../org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java | 10 ++++++---- .../core/jsonrpc/EthSimulateV1IntegrationTest.java | 7 +++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java index 3e92bd64106..1e526a8ff07 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java @@ -12,6 +12,8 @@ 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; @@ -64,7 +66,7 @@ public void rejectsEmptyBlockStateCalls() throws Exception { public void rejectsMultipleBlocks() throws Exception { rpc = newRpc(); SimulateV1Args args = new SimulateV1Args(); - args.setBlockStateCalls(List.of(emptyBlock(), emptyBlock())); + args.setBlockStateCalls(Arrays.asList(emptyBlock(), emptyBlock())); JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, () -> rpc.ethSimulateV1(args, "latest")); assertTrue(e.getMessage().contains("single-block")); @@ -137,7 +139,7 @@ public void rejectsNonNumericTokenId() throws Exception { c.setData("0x"); c.setTokenId("not-a-number"); SimulateBlock block = new SimulateBlock(); - block.setCalls(List.of(c)); + block.setCalls(Collections.singletonList(c)); SimulateV1Args args = wrap(block); JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, () -> rpc.ethSimulateV1(args, "latest")); @@ -163,7 +165,7 @@ public void simulateBlockResultJsonRoundTrip() throws Exception { SimulateCallResult c1 = new SimulateCallResult(); c1.setStatus("0x0"); c1.setReturnData("0x"); - result.setCalls(List.of(c0, c1)); + result.setCalls(Arrays.asList(c0, c1)); result.setTransactions(new Object[] {"0xhash0", "0xhash1"}); ObjectMapper mapper = new ObjectMapper(); @@ -190,7 +192,7 @@ private static SimulateBlock emptyBlock() { private static SimulateV1Args wrap(SimulateBlock block) { SimulateV1Args args = new SimulateV1Args(); - args.setBlockStateCalls(List.of(block)); + args.setBlockStateCalls(Collections.singletonList(block)); return args; } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java index 3fe6fa29d00..99c2ff05f84 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java @@ -8,6 +8,7 @@ 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; @@ -422,7 +423,7 @@ private static SimulateV1Args newArgs(boolean traceTransfers, boolean validation SimulateBlock block = new SimulateBlock(); block.setCalls(new ArrayList<>(Arrays.asList(calls))); SimulateV1Args args = new SimulateV1Args(); - args.setBlockStateCalls(new ArrayList<>(List.of(block))); + args.setBlockStateCalls(new ArrayList<>(Collections.singletonList(block))); args.setTraceTransfers(traceTransfers); args.setValidation(validation); args.setReturnFullTransactions(returnFullTransactions); @@ -506,7 +507,9 @@ private static String trc10TransferTopic() { /** Pad a 20-byte EVM address (hex without 0x) to a 32-byte topic hex string with 0x prefix. */ private static String padAddressTopic(String evmHex20) { - return "0x" + "0".repeat(24) + evmHex20.toLowerCase(java.util.Locale.ROOT); + char[] zeros = new char[24]; + java.util.Arrays.fill(zeros, '0'); + return "0x" + new String(zeros) + evmHex20.toLowerCase(java.util.Locale.ROOT); } /** uint256 hex of a non-negative long, 0x-prefixed and left-padded to 32 bytes. */ From bfe6ddf311fd8c03dc7fb0e7a421cbcdefd3ac97 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 19 May 2026 17:41:36 +0100 Subject: [PATCH 4/4] remove extra method params from eth_simulateV1 --- .../services/jsonrpc/TronJsonRpcImpl.java | 17 +- .../services/jsonrpc/types/CallArguments.java | 24 --- .../core/jsonrpc/EthSimulateV1ArgsTest.java | 16 -- .../jsonrpc/EthSimulateV1IntegrationTest.java | 162 ------------------ .../services/jsonrpc/BuildArgumentsTest.java | 2 +- .../services/jsonrpc/CallArgumentsTest.java | 2 +- 6 files changed, 3 insertions(+), 220 deletions(-) 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 5fa0880832f..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 @@ -1148,16 +1148,6 @@ private TransactionCapsule buildSimulateTransactionCapsule(CallArguments call) String resolvedData = call.resolveData(); byte[] data = resolvedData == null ? new byte[0] : ByteArray.fromHexString(resolvedData); long value = call.parseValue(); - String tokenIdStr = call.getTokenId(); - long tokenValue = call.parseTokenValue(); - long tokenId = 0L; - if (tokenIdStr != null && !tokenIdStr.isEmpty()) { - try { - tokenId = Long.parseLong(tokenIdStr); - } catch (NumberFormatException e) { - throw new JsonRpcInvalidParamsException("invalid tokenId: " + tokenIdStr); - } - } if (call.getTo() == null || call.getTo().isEmpty()) { SmartContract.Builder contract = SmartContract.newBuilder() @@ -1169,17 +1159,12 @@ private TransactionCapsule buildSimulateTransactionCapsule(CallArguments call) CreateSmartContract.Builder deployBuilder = CreateSmartContract.newBuilder(); deployBuilder.setOwnerAddress(ByteString.copyFrom(owner)); deployBuilder.setNewContract(contract.build()); - if (tokenIdStr != null && !tokenIdStr.isEmpty()) { - deployBuilder.setCallTokenValue(tokenValue); - deployBuilder.setTokenId(tokenId); - } return wallet.createTransactionCapsule(deployBuilder.build(), ContractType.CreateSmartContract); } byte[] to = addressCompatibleToByteArray(call.getTo()); - TriggerSmartContract trigger = triggerCallContract(owner, to, value, data, tokenValue, - (tokenIdStr == null || tokenIdStr.isEmpty()) ? null : tokenIdStr); + TriggerSmartContract trigger = triggerCallContract(owner, to, value, data, 0L, null); return wallet.createTransactionCapsule(trigger, ContractType.TriggerSmartContract); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java index 9e7beedbc46..1715636a2a4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java @@ -49,22 +49,6 @@ public class CallArguments { @Getter @Setter private String nonce; // not used - /** - * Tron-specific extension for {@code eth_simulateV1}: decimal-string TRC-10 - * asset id (matches {@link org.tron.core.services.jsonrpc.JsonRpcApiUtil - * #triggerCallContract}'s 6th arg). Null/empty means "no TRC-10 transfer". - */ - @Getter - @Setter - private String tokenId; - /** - * Tron-specific extension for {@code eth_simulateV1}: hex-quantity TRC-10 - * amount (e.g. {@code "0x64"} = 100). Null/empty means 0. Ignored when - * {@code tokenId} is null/empty. - */ - @Getter - @Setter - private String tokenValue; /** * Returns {@code input} if non-null, else {@code data}. Pure @@ -124,12 +108,4 @@ public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestE public long parseValue() throws JsonRpcInvalidParamsException { return parseQuantityValue(value); } - - /** Parsed TRC-10 token amount; 0 when {@code tokenValue} is null/empty. */ - public long parseTokenValue() throws JsonRpcInvalidParamsException { - if (tokenValue == null || tokenValue.isEmpty()) { - return 0L; - } - return parseQuantityValue(tokenValue); - } } \ 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 index 1e526a8ff07..bce464e9a42 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1ArgsTest.java @@ -130,22 +130,6 @@ public void rejectsHexBlockNumberTag() throws Exception { assertTrue(e.getMessage().contains("latest")); } - @Test - public void rejectsNonNumericTokenId() throws Exception { - rpc = newRpc(); - CallArguments c = new CallArguments(); - c.setFrom("0x0000000000000000000000000000000000000000"); - c.setTo("0x0000000000000000000000000000000000000001"); - c.setData("0x"); - c.setTokenId("not-a-number"); - SimulateBlock block = new SimulateBlock(); - block.setCalls(Collections.singletonList(c)); - SimulateV1Args args = wrap(block); - JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, - () -> rpc.ethSimulateV1(args, "latest")); - assertTrue(e.getMessage().contains("tokenId")); - } - @Test public void rejectsEarliestTag() throws Exception { rpc = newRpc(); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java index 99c2ff05f84..453456b2c30 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/EthSimulateV1IntegrationTest.java @@ -21,7 +21,6 @@ import org.tron.common.utils.Sha256Hash; import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; -import org.tron.core.capsule.AssetIssueCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.ContractCapsule; import org.tron.core.config.args.Args; @@ -40,7 +39,6 @@ import org.tron.core.vm.repository.Repository; import org.tron.core.vm.repository.RepositoryImpl; import org.tron.protos.Protocol; -import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; import org.tron.protos.contract.SmartContractOuterClass; @Slf4j @@ -61,20 +59,9 @@ public class EthSimulateV1IntegrationTest extends BaseTest { private static final String SEL_GET = "6d4ce63c"; private static final String SEL_SET_REVERT = "2e8f88e6"; - // ERC-7528 native pseudo-address (TRX + TRC-10 synthetic logs share it). - private static final String ERC7528_NATIVE_LOWER = - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; - // keccak256("Transfer(address,address,uint256)"). - private static final String TRANSFER_TOPIC_LOWER = - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; - // TRC-10 testing token (>=1_000_001 required by VMConstant.MIN_TOKEN_ID). - private static final String TRC10_TOKEN_ID = "1000001"; - 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 String SINK_TRON_ADDR_HEX; - private static final String SINK_EVM_ADDR_HEX_PREFIXED; private static final long OWNER_BALANCE = 10_000_000_000L; static { @@ -87,13 +74,6 @@ public class EthSimulateV1IntegrationTest extends BaseTest { STORAGE_TRON_ADDR_HEX = Wallet.getAddressPreFixString() + "00000000000000000000000000000000000c0de1"; STORAGE_EVM_ADDR_HEX_PREFIXED = "0x00000000000000000000000000000000000c0de1"; - // "Accept-anything" sink: runtime bytecode is `00` (STOP). No Solidity - // non-payable guard, so it accepts both TRX value and TRC-10 transfers - // without reverting. Used by the mixed TRX+TRC-10 test where the regular - // Solidity-compiled SimpleStorage would reject msg.value > 0. - SINK_TRON_ADDR_HEX = Wallet.getAddressPreFixString() - + "00000000000000000000000000000000000c0de2"; - SINK_EVM_ADDR_HEX_PREFIXED = "0x00000000000000000000000000000000000c0de2"; } @Resource @@ -111,7 +91,6 @@ public void init() { // chain's dynamic-properties store from reloading these flags back to 0 // — same pattern as AllowTvmCompatibleEvmTest.beforeClass(). ConfigLoader.disable = true; - VMConfig.initAllowTvmTransferTrc10(1); VMConfig.initAllowTvmConstantinople(1); VMConfig.initAllowTvmSolidity059(1); VMConfig.initAllowTvmIstanbul(1); @@ -151,17 +130,6 @@ public void init() { .build())); rootRepository.saveCode(storageAddr, ByteArray.fromHexString(simpleStorageRuntimeBytecode())); - - // Sink contract — `00` (STOP) so it accepts arbitrary calldata, TRX value, - // and TRC-10 transfers without reverting. - byte[] sinkAddr = ByteArray.fromHexString(SINK_TRON_ADDR_HEX); - rootRepository.createAccount(sinkAddr, Protocol.AccountType.Contract); - rootRepository.createContract(sinkAddr, new ContractCapsule( - SmartContractOuterClass.SmartContract.newBuilder() - .setContractAddress(ByteString.copyFrom(sinkAddr)) - .build())); - rootRepository.saveCode(sinkAddr, new byte[] {0x00}); - rootRepository.commit(); tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet); @@ -277,91 +245,6 @@ public void createPopulatesContractAddress() throws Exception { addr.startsWith("0x") && addr.length() == 42); } - /** - * Top-level TRC-10 transfer (depth 0): owner sends 50 units of token - * 1000001 to the pre-deployed SimpleStorage contract, invoking get() - * (a view function that returns the slot value — picked because it - * doesn't revert on incoming TRC-10). - * - *

Expect exactly one synthetic log on the call result: - * address = ERC-7528 native pseudo-address (lowercased), topic[0] = - * keccak256("TRC10Transfer(address,address,uint256,uint256)"), - * topic[1] = padded sender (EVM 20-byte form), topic[2] = padded - * recipient, topic[3] = padded uint256(tokenId), data = padded - * uint256(amount). - */ - @Test - public void traceTrc10TopLevelCall() throws Exception { - seedTrc10(500L); - - CallArguments c = new CallArguments(); - c.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); - c.setTo(STORAGE_EVM_ADDR_HEX_PREFIXED); - c.setData("0x" + SEL_GET); - c.setTokenId(TRC10_TOKEN_ID); - c.setTokenValue("0x32"); // 50 - SimulateV1Args args = newArgs(true, false, false, c); - - SimulateCallResult call = - tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls().get(0); - - assertEquals("0x1", call.getStatus()); - assertEquals("expected exactly one synthetic TRC10Transfer log", - 1, call.getLogs().size()); - - TronJsonRpc.LogFilterElement log = call.getLogs().get(0); - assertEquals(ERC7528_NATIVE_LOWER, log.getAddress()); - String[] topics = log.getTopics(); - assertEquals(4, topics.length); - assertEquals(trc10TransferTopic(), topics[0]); - assertEquals(padAddressTopic(OWNER_ADDRESS.substring(2)), topics[1]); - assertEquals(padAddressTopic(STORAGE_EVM_ADDR_HEX_PREFIXED.substring(2)), topics[2]); - assertEquals(padUint256Hex(1_000_001L), topics[3]); - assertEquals(padUint256Hex(50L), log.getData()); - - // No commit to disk: owner's TRC-10 balance is unchanged. - AccountCapsule reread = dbManager.getAccountStore().get(ownerBytes); - assertEquals(Long.valueOf(500L), reread.getAssetMapV2().get(TRC10_TOKEN_ID)); - } - - /** - * Mixed top-level transfer: a single call with both {@code value > 0} - * (TRX) and {@code tokenValue > 0} (TRC-10). Both synthetic logs must - * appear in the same call result with consecutive {@code logIndex} — - * TRX first, then TRC-10 — matching VMActuator's depth-0 emission order - * at lines 559-569 (TRX block before TRC-10 block). - */ - @Test - public void traceTrc10MixedWithTrx() throws Exception { - seedTrc10(500L); - - CallArguments c = new CallArguments(); - c.setFrom(OWNER_ADDRESS_HEX_PREFIXED()); - // Sink accepts arbitrary calldata + value; SimpleStorage would revert - // because Solidity inlines a non-payable check on every external method. - c.setTo(SINK_EVM_ADDR_HEX_PREFIXED); - c.setData("0x"); - c.setValue("0x64"); // 100 sun TRX - c.setTokenId(TRC10_TOKEN_ID); - c.setTokenValue("0x32"); // 50 TRC-10 - SimulateV1Args args = newArgs(true, false, false, c); - - SimulateCallResult call = - tronJsonRpc.ethSimulateV1(args, "latest").get(0).getCalls().get(0); - - assertEquals("0x1", call.getStatus()); - assertEquals("expected two synthetic transfer logs (TRX + TRC-10)", - 2, call.getLogs().size()); - - TronJsonRpc.LogFilterElement trxLog = call.getLogs().get(0); - TronJsonRpc.LogFilterElement trc10Log = call.getLogs().get(1); - assertEquals(TRANSFER_TOPIC_LOWER, trxLog.getTopics()[0]); - assertEquals(trc10TransferTopic(), trc10Log.getTopics()[0]); - assertEquals(padUint256Hex(100L), trxLog.getData()); - assertEquals(padUint256Hex(50L), trc10Log.getData()); - assertEquals(padUint256Hex(1_000_001L), trc10Log.getTopics()[3]); - } - /** * Drops TRC-10 transfer entries from a reverted sub-call frame — same * isolation discipline the TRX transfer hooks rely on. Direct buffer @@ -471,49 +354,4 @@ private static BigInteger parseHex(String hex) { return new BigInteger(hex.startsWith("0x") ? hex.substring(2) : hex, 16); } - /** - * Register an AssetIssue for {@link #TRC10_TOKEN_ID} in the V2 store - * (V2 keys by tokenId, matching {@code AllowSameTokenName == 1}) and - * seed the owner's account with the requested balance. VMUtils - * validateForSmartContract requires both the AssetIssue and a non-zero - * owner balance to allow the transfer. - */ - private void seedTrc10(long ownerAmount) { - dbManager.getDynamicPropertiesStore().saveAllowSameTokenName(1L); - AssetIssueContract asset = AssetIssueContract.newBuilder() - .setOwnerAddress(ByteString.copyFrom(ownerBytes)) - .setName(ByteString.copyFromUtf8("TRC10")) - .setId(TRC10_TOKEN_ID) - .setTotalSupply(1_000_000_000L) - .setTrxNum(1) - .setNum(1) - .build(); - AssetIssueCapsule cap = new AssetIssueCapsule(asset); - dbManager.getAssetIssueV2Store().put(cap.createDbV2Key(), cap); - - AccountCapsule owner = dbManager.getAccountStore().get(ownerBytes); - owner.setInstance(owner.getInstance().toBuilder() - .putAssetV2(TRC10_TOKEN_ID, ownerAmount) - .build()); - dbManager.getAccountStore().put(ownerBytes, owner); - } - - /** Lower-case hex of keccak256("TRC10Transfer(address,address,uint256,uint256)"). */ - private static String trc10TransferTopic() { - return "0x" + ByteArray.toHexString(org.tron.common.crypto.Hash.sha3( - "TRC10Transfer(address,address,uint256,uint256)" - .getBytes(java.nio.charset.StandardCharsets.UTF_8))); - } - - /** Pad a 20-byte EVM address (hex without 0x) to a 32-byte topic hex string with 0x prefix. */ - private static String padAddressTopic(String evmHex20) { - char[] zeros = new char[24]; - java.util.Arrays.fill(zeros, '0'); - return "0x" + new String(zeros) + evmHex20.toLowerCase(java.util.Locale.ROOT); - } - - /** uint256 hex of a non-negative long, 0x-prefixed and left-padded to 32 bytes. */ - private static String padUint256Hex(long v) { - return "0x" + padUint256(v); - } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 2c8f54517ea..753d93d47f4 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -41,7 +41,7 @@ public void testBuildArgument() { CallArguments callArguments = new CallArguments( "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", - "", "", "0", null, null); + "", "", "0"); BuildArguments args = new BuildArguments(callArguments); Assert.assertEquals("0x0000000000000000000000000000000000000000", args.getFrom()); Assert.assertEquals("0x0000000000000000000000000000000000000001", args.getTo()); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java index 0f3a95ef2ad..4ebfc3c1872 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java @@ -31,7 +31,7 @@ public void init() { "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", - "", "", "0", null, null); + "", "", "0"); } @Test