diff --git a/.github/workflows/integration-test-multinode.yml b/.github/workflows/integration-test-multinode.yml deleted file mode 100644 index fadfc2168d2..00000000000 --- a/.github/workflows/integration-test-multinode.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Integration Test Multinode (Full) - -on: - push: - branches: [ 'master', 'release_**' ] - pull_request: - branches: [ 'develop', 'release_**' ] - types: [ opened, synchronize, reopened ] - paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', - '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', - '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - multinode-full: - name: Integration Test Multinode Full (JDK 8 / x86_64) - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Checkout java-tron - uses: actions/checkout@v5 - - - name: Set up JDK 8 - uses: actions/setup-java@v5 - with: - java-version: '8' - distribution: 'temurin' - - - name: Cache Gradle packages - uses: actions/cache@v5 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-multinode-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle-multinode- - - - name: Build FullNode.jar - run: ./gradlew clean build -x test --no-daemon - - - name: Build local java-tron Docker image (wraps PR-built FullNode.jar) - run: | - mkdir -p /tmp/tron-image - cp build/libs/FullNode.jar /tmp/tron-image/ - cat > /tmp/tron-image/Dockerfile <<'EOF' - FROM tronprotocol/java-tron:latest - COPY FullNode.jar /java-tron/lib/FullNode.jar - EOF - docker build -t java-tron-local:pr /tmp/tron-image - - - name: Pull integration-test image - run: docker pull troninfra/troninfra-ci:latest - - - name: Extract compose configs to host (for DinD path-alignment) - run: | - # start-multinode.sh builds HOST_COMPOSE_DIR as: - # ${HOST_WORKDIR}/docker/multi-node - # so the files must live at $HOST_WORKDIR/docker/multi-node/ on the - # host. Set HOST_WORKDIR to the workspace root and extract - # /app/docker/ 1:1 into workspace/docker/ — the subdirectories - # (multi-node/, single-node/) don't collide with java-tron's own - # docker/ files. - docker create --name it-extract troninfra/troninfra-ci:latest - docker cp it-extract:/app/docker/. "${{ github.workspace }}/docker/" - docker rm -f it-extract - - - name: Run multinode full tests - run: | - # --network host: multinode tests talk to nodes via 127.0.0.1:50051 etc. - # DinD socket + HOST_WORKDIR path-alignment lets the container orchestrate - # the 3-witness compose stack via the host daemon. - # Don't override --workdir so the container's default /app entrypoint works. - docker run --name integration-multinode \ - --network host \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v "${{ github.workspace }}:${{ github.workspace }}" \ - -v "${{ github.workspace }}/docker/multi-node:/app/docker/multi-node" \ - -e HOST_WORKDIR="${{ github.workspace }}" \ - -e TRON_IMAGE=java-tron-local:pr \ - -e JAVA_HOME=/usr/lib/jvm/temurin-8 \ - -e JAVA_HOME_17=/opt/java/openjdk \ - troninfra/troninfra-ci:latest \ - --multinode --clean - - - name: Extract test reports from container - if: always() - run: | - mkdir -p integration-reports - docker cp integration-multinode:/app/build/reports/. integration-reports/reports/ 2>/dev/null || true - docker cp integration-multinode:/app/build/test-results/. integration-reports/test-results/ 2>/dev/null || true - docker cp integration-multinode:/app/build/test-output.log integration-reports/ 2>/dev/null || true - - - name: Collect witness node logs - if: always() - run: | - mkdir -p integration-reports/node-logs - for c in tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb; do - docker logs "$c" > "integration-reports/node-logs/${c}.log" 2>&1 || true - done - - - name: Tear down compose stack - if: always() - run: | - docker rm -f tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb 2>/dev/null || true - docker network rm multi-node_tron-net 2>/dev/null || true - docker rm -f integration-multinode 2>/dev/null || true - - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v6 - with: - name: integration-multinode-report - path: integration-reports/ - if-no-files-found: warn diff --git a/.github/workflows/integration-test-single-node.yml b/.github/workflows/integration-test-single-node.yml index b0c10247a7f..3c56843c799 100644 --- a/.github/workflows/integration-test-single-node.yml +++ b/.github/workflows/integration-test-single-node.yml @@ -1,4 +1,4 @@ -name: Integration Test Single Node (Full) +name: Integration Test Single Node (Smoke) on: push: @@ -17,7 +17,7 @@ concurrency: jobs: integration: - name: Integration Test Single Node Full (JDK 8 / x86_64) + name: Integration Test Single Node Smoke (JDK 8 / x86_64) runs-on: ubuntu-latest timeout-minutes: 45 @@ -46,7 +46,7 @@ jobs: - name: Pull integration-test image run: docker pull troninfra/troninfra-ci:latest - - name: Run integration tests + - name: Run integration smoke tests run: | # JAVA_HOME=JDK 8 so FullNode runs on the same JVM family as # production (a few assertions check `java.version` starts with @@ -58,9 +58,9 @@ jobs: -e JAVA_HOME_17=/opt/java/openjdk \ -v "${{ github.workspace }}/build/libs/FullNode.jar:/javatron/FullNode.jar:ro" \ troninfra/troninfra-ci:latest \ - --clean + --clean --smoke - - name: Extract test reports from container + - name: Extract smoke test reports from container if: always() run: | mkdir -p integration-reports @@ -71,10 +71,10 @@ jobs: docker cp integration-test:/app/node/data/logs/tron.log integration-reports/ 2>/dev/null || true docker rm -f integration-test 2>/dev/null || true - - name: Upload test reports + - name: Upload smoke test reports if: always() uses: actions/upload-artifact@v6 with: - name: integration-test-report + name: integration-smoke-test-report path: integration-reports/ if-no-files-found: warn 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 d785951027b..e0a721db28d 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -25,6 +25,7 @@ import org.tron.common.runtime.InternalTransaction.TrxType; import org.tron.common.runtime.ProgramResult; import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.ForkController; import org.tron.common.utils.StorageUtils; import org.tron.common.utils.StringUtil; import org.tron.common.utils.WalletUtil; @@ -33,6 +34,7 @@ import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.ContractCapsule; import org.tron.core.capsule.ReceiptCapsule; +import org.tron.core.config.Parameter; import org.tron.core.db.EnergyProcessor; import org.tron.core.db.TransactionContext; import org.tron.core.exception.ContractExeException; @@ -189,7 +191,8 @@ public void execute(Object object) throws ContractExeException { throw e; } - VM.play(program, OperationRegistry.getTable()); + // Prepare the table once for this execution and all nested calls. + VM.play(program, OperationRegistry.prepareAndGetTable(isConstantCall)); result = program.getResult(); if (VMConfig.allowEnergyAdjustment()) { @@ -217,6 +220,9 @@ public void execute(Object object) throws ContractExeException { } else { result.spendEnergy(saveCodeEnergy); if (VMConfig.allowTvmConstantinople()) { + CreateSmartContract createContract = + ContractCapsule.getSmartContractFromTransaction(trx); + checkContractHashFields(createContract.getNewContract()); rootRepository.saveCode(program.getContractAddress().getNoLeadZeroesData(), code); } } @@ -330,6 +336,7 @@ private void create() if (contract == null) { throw new ContractValidateException("Cannot get CreateSmartContract from transaction"); } + SmartContract newSmartContract; if (VMConfig.allowTvmCompatibleEvm()) { newSmartContract = contract.getNewContract().toBuilder().setVersion(1).build(); @@ -341,11 +348,7 @@ private void create() throw new ContractValidateException("OwnerAddress is not equals OriginAddress"); } - byte[] contractName = newSmartContract.getName().getBytes(); - - if (contractName.length > VMConstant.CONTRACT_NAME_LENGTH) { - throw new ContractValidateException("contractName's length cannot be greater than 32"); - } + checkContractNameLength(contract.getNewContract()); long percent = contract.getNewContract().getConsumeUserResourcePercent(); if (percent < 0 || percent > VMConstant.ONE_HUNDRED) { @@ -455,6 +458,22 @@ private void create() } + static void checkContractHashFields(SmartContract contract) { + if (!contract.getCodeHash().isEmpty() || !contract.getTrxHash().isEmpty()) { + MUtil.checkCPUTimeForContractHashFields(); + } + } + + static void checkContractNameLength(SmartContract contract) throws ContractValidateException { + int contractNameLength = + ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2) + ? contract.getNameBytes().size() + : contract.getName().getBytes().length; + if (contractNameLength > VMConstant.CONTRACT_NAME_LENGTH) { + throw new ContractValidateException("contractName's length cannot be greater than 32"); + } + } + /** * ** */ diff --git a/actuator/src/main/java/org/tron/core/vm/Operation.java b/actuator/src/main/java/org/tron/core/vm/Operation.java index 87ff8fce749..80b25262089 100644 --- a/actuator/src/main/java/org/tron/core/vm/Operation.java +++ b/actuator/src/main/java/org/tron/core/vm/Operation.java @@ -52,4 +52,12 @@ public void execute(Program program) { public boolean isEnabled() { return enabled.getAsBoolean(); } + + public Operation adjustCost(Function newCost) { + return new Operation(opcode, require, ret, newCost, action, enabled); + } + + public Operation adjustAction(Consumer newAction) { + return new Operation(opcode, require, ret, cost, newAction, enabled); + } } diff --git a/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java b/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java index 8c078e843a2..28ce2147beb 100644 --- a/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java +++ b/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java @@ -7,12 +7,45 @@ public class OperationRegistry { + private static final Operation DEFAULT_MLOAD = new Operation( + Op.MLOAD, 1, 1, EnergyCost::getMloadCost, OperationActions::mLoadAction); + + private static final Operation DEFAULT_MSTORE = new Operation( + Op.MSTORE, 2, 0, EnergyCost::getMStoreCost, OperationActions::mStoreAction); + + private static final Operation DEFAULT_MSTORE8 = new Operation( + Op.MSTORE8, 2, 0, EnergyCost::getMStore8Cost, OperationActions::mStore8Action); + + private static final Operation ADJUSTED_MLOAD = + DEFAULT_MLOAD.adjustCost(EnergyCost::getMloadCost2); + + private static final Operation ADJUSTED_MSTORE = + DEFAULT_MSTORE.adjustCost(EnergyCost::getMStoreCost2); + + private static final Operation ADJUSTED_MSTORE8 = + DEFAULT_MSTORE8.adjustCost(EnergyCost::getMStore8Cost2); + + private static final Operation DEFAULT_VOTEWITNESS = new Operation( + Op.VOTEWITNESS, 4, 1, EnergyCost::getVoteWitnessCost, + OperationActions::voteWitnessAction, VMConfig::allowTvmVote); + + private static final Operation ADJUSTED_VOTEWITNESS = + DEFAULT_VOTEWITNESS.adjustCost(EnergyCost::getVoteWitnessCost2); + + private static final Operation OSAKA_VOTEWITNESS = + DEFAULT_VOTEWITNESS.adjustCost(EnergyCost::getVoteWitnessCost3); + + private static final Operation DEFAULT_SUICIDE = new Operation( + Op.SUICIDE, 1, 0, EnergyCost::getSuicideCost, OperationActions::suicideAction); + + private static final Operation ADJUSTED_SUICIDE = + DEFAULT_SUICIDE.adjustCost(EnergyCost::getSuicideCost2); + + private static final Operation RESTRICTED_SUICIDE = + DEFAULT_SUICIDE.adjustCost(EnergyCost::getSuicideCost3) + .adjustAction(OperationActions::suicideAction2); + public enum Version { - TRON_V1_0, - TRON_V1_1, - TRON_V1_2, - TRON_V1_3, - TRON_V1_4, TRON_V1_5, // add more // TRON_V2, @@ -21,13 +54,21 @@ public enum Version { private static final Map tableMap = new HashMap<>(); + // The newest version in use. Bump this when a newer operation set is added, + // together with newLatestOperationSet() below. + private static final Version LATEST_VERSION = Version.TRON_V1_5; + static { - tableMap.put(Version.TRON_V1_0, newTronV10OperationSet()); - tableMap.put(Version.TRON_V1_1, newTronV11OperationSet()); - tableMap.put(Version.TRON_V1_2, newTronV12OperationSet()); - tableMap.put(Version.TRON_V1_3, newTronV13OperationSet()); - tableMap.put(Version.TRON_V1_4, newTronV14OperationSet()); - tableMap.put(Version.TRON_V1_5, newTronV15OperationSet()); + tableMap.put(LATEST_VERSION, newLatestOperationSet()); + } + + // Constant calls get a dedicated instance of the newest table, isolated from + // the shared consensus table above. + private static final JumpTable CONSTANT_CALL_TABLE = newLatestOperationSet(); + + // The single place that decides which operation set is the newest. + private static JumpTable newLatestOperationSet() { + return newTronV15OperationSet(); } public static JumpTable newTronV10OperationSet() { @@ -74,28 +115,22 @@ public static JumpTable newTronV15OperationSet() { // Just for warming up class to avoid out_of_time public static void init() {} - public static JumpTable getTable() { - // always get the table which has the newest version - JumpTable table = tableMap.get(Version.TRON_V1_5); - - // next make the corresponding changes, exclude activating opcode - if (VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx()) { - adjustMemOperations(table); - } - - if (VMConfig.allowEnergyAdjustment()) { - adjustForFairEnergy(table); - } - - if (VMConfig.allowTvmSelfdestructRestriction()) { - adjustSelfdestruct(table); - } + public static JumpTable prepareAndGetTable(boolean isConstantCall) { + JumpTable table = getTable(isConstantCall); + // Apply configuration-dependent changes once at the top level. + adjustTable(table); + return table; + } - if (VMConfig.allowTvmOsaka()) { - adjustVoteWitnessCost(table); - } + public static JumpTable getTable(boolean isConstantCall) { + return isConstantCall ? CONSTANT_CALL_TABLE : tableMap.get(LATEST_VERSION); + } - return table; + private static void adjustTable(JumpTable table) { + // Make the corresponding changes, excluding opcode activation. + adjustMemOperations(table); + adjustVoteWitness(table); + adjustSelfdestruct(table); } public static JumpTable newBaseOperationSet() { @@ -331,20 +366,11 @@ public static JumpTable newBaseOperationSet() { EnergyCost::getBaseTierCost, OperationActions::popAction)); - table.set(new Operation( - Op.MLOAD, 1, 1, - EnergyCost::getMloadCost, - OperationActions::mLoadAction)); + table.set(DEFAULT_MLOAD); - table.set(new Operation( - Op.MSTORE, 2, 0, - EnergyCost::getMStoreCost, - OperationActions::mStoreAction)); + table.set(DEFAULT_MSTORE); - table.set(new Operation( - Op.MSTORE8, 2, 0, - EnergyCost::getMStore8Cost, - OperationActions::mStore8Action)); + table.set(DEFAULT_MSTORE8); table.set(new Operation( Op.SLOAD, 1, 1, @@ -449,10 +475,7 @@ public static JumpTable newBaseOperationSet() { EnergyCost::getRevertCost, OperationActions::revertAction)); - table.set(new Operation( - Op.SUICIDE, 1, 0, - EnergyCost::getSuicideCost, - OperationActions::suicideAction)); + table.set(DEFAULT_SUICIDE); return table; } @@ -570,11 +593,7 @@ public static void appendFreezeOperations(JumpTable table) { public static void appendVoteOperations(JumpTable table) { BooleanSupplier proposal = VMConfig::allowTvmVote; - table.set(new Operation( - Op.VOTEWITNESS, 4, 1, - EnergyCost::getVoteWitnessCost, - OperationActions::voteWitnessAction, - proposal)); + table.set(DEFAULT_VOTEWITNESS); table.set(new Operation( Op.WITHDRAWREWARD, 0, 1, @@ -593,23 +612,6 @@ public static void appendLondonOperations(JumpTable table) { proposal)); } - public static void adjustMemOperations(JumpTable table) { - table.set(new Operation( - Op.MLOAD, 1, 1, - EnergyCost::getMloadCost2, - OperationActions::mLoadAction)); - - table.set(new Operation( - Op.MSTORE, 2, 0, - EnergyCost::getMStoreCost2, - OperationActions::mStoreAction)); - - table.set(new Operation( - Op.MSTORE8, 2, 0, - EnergyCost::getMStore8Cost2, - OperationActions::mStore8Action)); - } - public static void appendFreezeV2Operations(JumpTable table) { BooleanSupplier proposal = VMConfig::allowTvmFreezeV2; @@ -664,19 +666,6 @@ public static void appendShangHaiOperations(JumpTable table) { proposal)); } - public static void adjustForFairEnergy(JumpTable table) { - table.set(new Operation( - Op.VOTEWITNESS, 4, 1, - EnergyCost::getVoteWitnessCost2, - OperationActions::voteWitnessAction, - VMConfig::allowTvmVote)); - - table.set(new Operation( - Op.SUICIDE, 1, 0, - EnergyCost::getSuicideCost2, - OperationActions::suicideAction)); - } - public static void appendCancunOperations(JumpTable table) { BooleanSupplier proposal = VMConfig::allowTvmCancun; BooleanSupplier tvmBlobProposal = VMConfig::allowTvmBlob; @@ -722,18 +711,30 @@ public static void appendOsakaOperations(JumpTable table) { proposal)); } - public static void adjustSelfdestruct(JumpTable table) { - table.set(new Operation( - Op.SUICIDE, 1, 0, - EnergyCost::getSuicideCost3, - OperationActions::suicideAction2)); + public static void adjustMemOperations(JumpTable table) { + boolean adjusted = VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx(); + table.set(adjusted ? ADJUSTED_MLOAD : DEFAULT_MLOAD); + table.set(adjusted ? ADJUSTED_MSTORE : DEFAULT_MSTORE); + table.set(adjusted ? ADJUSTED_MSTORE8 : DEFAULT_MSTORE8); } - public static void adjustVoteWitnessCost(JumpTable table) { - table.set(new Operation( - Op.VOTEWITNESS, 4, 1, - EnergyCost::getVoteWitnessCost3, - OperationActions::voteWitnessAction, - VMConfig::allowTvmVote)); + public static void adjustVoteWitness(JumpTable table) { + if (VMConfig.allowTvmOsaka()) { + table.set(OSAKA_VOTEWITNESS); + } else if (VMConfig.allowEnergyAdjustment()) { + table.set(ADJUSTED_VOTEWITNESS); + } else { + table.set(DEFAULT_VOTEWITNESS); + } + } + + public static void adjustSelfdestruct(JumpTable table) { + if (VMConfig.allowTvmSelfdestructRestriction()) { + table.set(RESTRICTED_SUICIDE); + } else if (VMConfig.allowEnergyAdjustment()) { + table.set(ADJUSTED_SUICIDE); + } else { + table.set(DEFAULT_SUICIDE); + } } } diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java index ec1f4363205..6ee8b1245ce 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java @@ -19,6 +19,7 @@ import org.tron.core.vm.VMConstant; import org.tron.core.vm.nativecontract.param.CancelAllUnfreezeV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.protos.Protocol; @Slf4j(topic = "VMProcessor") @@ -39,6 +40,10 @@ public void validate(CancelAllUnfreezeV2Param param, Repository repo) throws Con throw new ContractValidateException( ACCOUNT_EXCEPTION_STR + readableOwnerAddress + NOT_EXIST_STR); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } public Map execute(CancelAllUnfreezeV2Param param, Repository repo) throws ContractExeException { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java index e7e932194ed..96c6e936b36 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java @@ -14,6 +14,7 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.nativecontract.param.FreezeBalanceV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; @Slf4j(topic = "VMProcessor") public class FreezeBalanceV2Processor { @@ -63,6 +64,10 @@ public void validate(FreezeBalanceV2Param param, Repository repo) throws Contrac "Unknown ResourceCode, valid ResourceCode[BANDWIDTH、ENERGY]"); } } + + if (repo.isSelfDestructed(ownerAddress)) { + MUtil.checkCPUTimeForFreezeV2AfterSelfDestruct(); + } } public void execute(FreezeBalanceV2Param param, Repository repo) { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java index af2cbf63a43..73ffb5f294b 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java @@ -23,6 +23,7 @@ import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.nativecontract.param.UnfreezeBalanceV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.core.vm.utils.VoteRewardUtil; import org.tron.protos.Protocol; import org.tron.protos.contract.Common; @@ -86,6 +87,10 @@ public void validate(UnfreezeBalanceV2Param param, Repository repo) throw new ContractValidateException( "Invalid unfreeze_balance, [" + param.getUnfreezeBalance() + "] is invalid"); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } private boolean checkUnfreezeBalance( diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java index 0bcdb10d46f..982031aa672 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java @@ -17,6 +17,7 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.nativecontract.param.WithdrawExpireUnfreezeParam; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.protos.Protocol; @Slf4j(topic = "VMProcessor") @@ -52,6 +53,10 @@ public void validate(WithdrawExpireUnfreezeParam param, Repository repo) throws logger.debug(e.getMessage(), e); throw new ContractValidateException(e.getMessage()); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } private long getTotalWithdrawUnfreeze(List unfrozenV2List, long now) { diff --git a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java index c6347b9a072..30ec0d24f34 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java +++ b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java @@ -131,6 +131,16 @@ public boolean isNewContract(byte[] address) { return repository.isNewContract(address); } + @Override + public void markSelfDestruct(byte[] address) { + repository.markSelfDestruct(address); + } + + @Override + public boolean isSelfDestructed(byte[] address) { + return repository.isSelfDestructed(address); + } + @Override public void updateAccount(byte[] address, AccountCapsule accountCapsule) { repository.updateAccount(address, accountCapsule); 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 590859a9fef..2be1eb80703 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 @@ -512,12 +512,18 @@ public void suicide(DataWord obtainerAddress) { internalTx.setValue(internalTx.getValue() + expireUnfrozenBalance); } } + + getContractState().markSelfDestruct(owner); getResult().addDeleteAccount(this.getContractAddress()); } public void suicide2(DataWord obtainerAddress) { - byte[] owner = getContextAddress(); + + if (getContractState().isSelfDestructed(obtainerAddress.toTronAddress())) { + MUtil.checkCPUTimeForSelfDestructedBeneficiary(); + } + boolean isNewContract = getContractState().isNewContract(owner); if (isNewContract) { suicide(obtainerAddress); @@ -540,6 +546,7 @@ public void suicide2(DataWord obtainerAddress) { "suicide", nonce, getContractState().getAccount(owner).getAssetMapV2()); if (FastByteComparisons.isEqual(owner, obtainer)) { + getContractState().markSelfDestruct(owner); return; } @@ -579,6 +586,8 @@ public void suicide2(DataWord obtainerAddress) { internalTx.setValue(internalTx.getValue() + expireUnfrozenBalance); } } + + getContractState().markSelfDestruct(owner); } public Repository getContractState() { @@ -914,7 +923,8 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), if (VMConfig.allowTvmCompatibleEvm()) { program.setContractVersion(getContractVersion()); } - VM.play(program, OperationRegistry.getTable()); + // Reuse the table prepared by the top-level execution. + VM.play(program, OperationRegistry.getTable(isConstantCall())); createResult = program.getResult(); getTrace().merge(program.getTrace()); // always commit nonce @@ -1146,7 +1156,8 @@ this, new DataWord(contextAddress), program.setContractVersion(invoke.getDeposit() .getContract(codeAddress).getContractVersion()); } - VM.play(program, OperationRegistry.getTable()); + // Reuse the table prepared by the top-level execution. + VM.play(program, OperationRegistry.getTable(isConstantCall())); callResult = program.getResult(); getTrace().merge(program.getTrace()); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java index 8f91d59d0b8..ab86328c428 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java @@ -59,6 +59,10 @@ public interface Repository { boolean isNewContract(byte[] address); + void markSelfDestruct(byte[] address); + + boolean isSelfDestructed(byte[] address); + void updateAccount(byte[] address, AccountCapsule accountCapsule); void updateDynamicProperty(byte[] word, BytesCapsule bytesCapsule); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 62e7ce6ec08..7801a18798a 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -139,6 +139,7 @@ public class RepositoryImpl implements Repository { private final HashMap> delegatedResourceAccountIndexCache = new HashMap<>(); private final HashBasedTable> transientStorage = HashBasedTable.create(); private final HashSet newContractCache = new HashSet<>(); + private final HashSet selfDestructCache = new HashSet<>(); public static void removeLruCache(byte[] address) { } @@ -572,6 +573,29 @@ public boolean isNewContract(byte[] address) { } } + @Override + public void markSelfDestruct(byte[] address) { + selfDestructCache.add(Key.create(address)); + } + + @Override + public boolean isSelfDestructed(byte[] address) { + Key key = Key.create(address); + if (selfDestructCache.contains(key)) { + return true; + } + + if (parent != null) { + boolean isSelfDestructed = parent.isSelfDestructed(address); + if (isSelfDestructed) { + selfDestructCache.add(key); + } + return isSelfDestructed; + } else { + return false; + } + } + @Override public void updateAccount(byte[] address, AccountCapsule accountCapsule) { accountCache.put(Key.create(address), @@ -780,6 +804,7 @@ public void commit() { commitDelegatedResourceAccountIndexCache(repository); commitTransientStorage(repository); commitNewContractCache(repository); + commitSelfDestructCache(repository); } @Override @@ -1142,6 +1167,12 @@ public void commitNewContractCache(Repository deposit) { } } + private void commitSelfDestructCache(Repository deposit) { + if (deposit != null) { + selfDestructCache.forEach(key -> deposit.markSelfDestruct(key.getData())); + } + } + /** * Get the block id from the number. */ diff --git a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java index e07360e6863..c7059016b88 100644 --- a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java +++ b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java @@ -59,6 +59,12 @@ public static boolean isNotNullOrEmpty(String str) { return !isNullOrEmpty(str); } + public static void checkCPUTimeForContractHashFields() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for contract hash fields"); + } + } + public static void checkCPUTime() { if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_7_1)) { throw new OutOfTimeException("CPU timeout for 0x0a executing"); @@ -76,4 +82,23 @@ public static void checkCPUTimeForModExp() { throw new OutOfTimeException("CPU timeout for modExp executing"); } } + + public static void checkCPUTimeForFreezeV2AfterSelfDestruct() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for FreezeBalanceV2 after SELFDESTRUCT"); + } + } + + public static void checkCPUTimeForSelfDestructedBeneficiary() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException( + "CPU timeout for SELFDESTRUCT with selfdestructed beneficiary"); + } + } + + public static void checkCPUTimeForInvalidDelegatedV2Balance() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for invalid delegated V2 balance"); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java index 1af7b55c8b2..026dce74ec8 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java @@ -1336,6 +1336,10 @@ public void clearDelegatedResource() { this.account = builder.build(); } + public boolean hasInvalidDelegatedV2() { + return getDelegatedFrozenV2BalanceForBandwidth() < 0 || getDelegatedFrozenV2BalanceForEnergy() < 0; + } + public void importAsset(byte[] key) { this.account = AssetUtil.importAsset(this.account, key); } diff --git a/common/src/main/java/org/tron/core/config/Parameter.java b/common/src/main/java/org/tron/core/config/Parameter.java index 233f1d9ef7a..0f9402641e9 100644 --- a/common/src/main/java/org/tron/core/config/Parameter.java +++ b/common/src/main/java/org/tron/core/config/Parameter.java @@ -30,7 +30,8 @@ public enum ForkBlockVersionEnum { VERSION_4_8_0_1(33, 1596780000000L, 70), VERSION_4_8_1(34, 1596780000000L, 80), VERSION_4_8_1_1(35, 1596780000000L, 70), - VERSION_4_8_2(36, 1596780000000L, 80); + VERSION_4_8_2(36, 1596780000000L, 80), + VERSION_4_8_2_2(37, 1596780000000L, 70); // if add a version, modify BLOCK_VERSION simultaneously @Getter @@ -79,7 +80,7 @@ public class ChainConstant { public static final int SINGLE_REPEAT = 1; public static final int BLOCK_FILLED_SLOTS_NUMBER = 128; public static final int MAX_FROZEN_NUMBER = 1; - public static final int BLOCK_VERSION = 36; + public static final int BLOCK_VERSION = 37; public static final long FROZEN_PERIOD = 86_400_000L; public static final long DELEGATE_PERIOD = 3 * 86_400_000L; public static final long TRX_PRECISION = 1000_000L; diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index f34d440702e..64f2befa2ef 100644 --- a/framework/src/main/java/org/tron/program/Version.java +++ b/framework/src/main/java/org/tron/program/Version.java @@ -2,9 +2,9 @@ public class Version { - public static final String VERSION_NAME = "GreatVoyage-v4.8.2-6-g348db25bfd"; - public static final String VERSION_CODE = "18825"; - private static final String VERSION = "4.8.2.1"; + public static final String VERSION_NAME = "GreatVoyage-v4.8.2.1-1-gbd2450fe06"; + public static final String VERSION_CODE = "18828"; + private static final String VERSION = "4.8.2.2"; public static String getVersion() { return VERSION; diff --git a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java index ab147f57a79..013ba606e9a 100644 --- a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java +++ b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java @@ -1,6 +1,8 @@ package org.tron.common.runtime; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.same; import java.lang.reflect.Field; import java.util.Collections; @@ -13,19 +15,70 @@ import org.tron.common.runtime.vm.LogInfo; import org.tron.core.actuator.VMActuator; import org.tron.core.db.TransactionContext; +import org.tron.core.vm.JumpTable; import org.tron.core.vm.OperationRegistry; import org.tron.core.vm.VM; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; +import org.tron.core.vm.repository.Repository; public class VMActuatorMockTest { @BeforeClass public static void init() { - // warm up the registry so VM.play(..., OperationRegistry.getTable()) arg eval is safe + // Warm up the registry before VM execution timing starts. OperationRegistry.init(); } + @Test + public void constantCallUsesDedicatedJumpTable() throws Exception { + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + Mockito.when(program.getResult()).thenReturn(new ProgramResult()); + + VMActuator actuator = new VMActuator(true); + Field f = VMActuator.class.getDeclaredField("program"); + f.setAccessible(true); + f.set(actuator, program); + + TransactionContext context = Mockito.mock(TransactionContext.class); + Mockito.when(context.getProgramResult()).thenReturn(new ProgramResult()); + + actuator.execute(context); + + JumpTable transactionTable = OperationRegistry.getTable(false); + JumpTable constantCallTable = OperationRegistry.getTable(true); + vmMock.verify(() -> VM.play(any(), same(constantCallTable))); + vmMock.verify(() -> VM.play(any(), argThat(table -> table != transactionTable))); + } + } + + @Test + public void nonConstantCallUsesSharedJumpTable() throws Exception { + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + Mockito.when(program.getResult()).thenReturn(new ProgramResult()); + + VMActuator actuator = new VMActuator(false); + Field f = VMActuator.class.getDeclaredField("program"); + f.setAccessible(true); + f.set(actuator, program); + + Field repositoryField = VMActuator.class.getDeclaredField("rootRepository"); + repositoryField.setAccessible(true); + repositoryField.set(actuator, Mockito.mock(Repository.class)); + + TransactionContext context = Mockito.mock(TransactionContext.class); + Mockito.when(context.getProgramResult()).thenReturn(new ProgramResult()); + + actuator.execute(context); + + // The non-constant call must receive the shared consensus table itself. + JumpTable shared = OperationRegistry.getTable(false); + vmMock.verify(() -> VM.play(any(), same(shared))); + } + } + private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expectedSize) throws Exception { boolean prevOsaka = VMConfig.allowTvmOsaka(); diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java index a1627f4f2e2..d6c344263dd 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.tron.core.config.Parameter.ChainConstant.FROZEN_PERIOD; +import static org.tron.core.config.Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2; import java.util.List; import java.util.Locale; @@ -16,6 +17,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; @@ -24,6 +26,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.InternalTransaction; import org.tron.common.utils.DecodeUtil; +import org.tron.common.utils.ForkController; import org.tron.core.Constant; import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; @@ -42,6 +45,7 @@ import org.tron.core.vm.config.ConfigLoader; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.Program.OutOfTimeException; import org.tron.core.vm.program.invoke.ProgramInvokeMockImpl; import org.tron.core.vm.repository.Repository; import org.tron.protos.Protocol; @@ -51,7 +55,7 @@ public class OperationsTest extends BaseTest { private ProgramInvokeMockImpl invoke; private Program program; - private final JumpTable jumpTable = OperationRegistry.getTable(); + private final JumpTable jumpTable = OperationRegistry.prepareAndGetTable(false); @Autowired private Wallet wallet; @@ -1182,6 +1186,7 @@ public void testSuicideAction() throws ContractValidateException { program.suicide(new DataWord( dbManager.getAccountStore().getBlackhole().getAddress().toByteArray())); + Assert.assertTrue(program.getContractState().isSelfDestructed(program.getContextAddress())); DecodeUtil.addressPreFixByte = prePrefixByte; VMConfig.initAllowEnergyAdjustment(0); @@ -1240,6 +1245,7 @@ public void testSuicideAction2() throws ContractValidateException { OperationActions.suicideAction2(program); Assert.assertEquals(1, program.getResult().getDeleteAccounts().size()); + Assert.assertTrue(program.getContractState().isSelfDestructed(contractAddr)); invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr); @@ -1256,6 +1262,7 @@ public void testSuicideAction2() throws ContractValidateException { dbManager.getAccountStore().getBlackhole().getAddress().toByteArray())); Assert.assertEquals(0, spyProgram.getResult().getDeleteAccounts().size()); + Assert.assertTrue(spyProgram.getContractState().isSelfDestructed(contractAddr)); DecodeUtil.addressPreFixByte = prePrefixByte; VMConfig.initAllowEnergyAdjustment(0); @@ -1266,6 +1273,31 @@ public void testSuicideAction2() throws ContractValidateException { VMConfig.initAllowTvmVote(0); } + @Test + public void testSuicide2RejectsSelfDestructedBeneficiaryAfterFork() + throws ContractValidateException { + byte[] contractAddr = Hex.decode("41471fd3ad3e9eeadeec4608b92d16ce6b500704cc"); + byte[] beneficiary = Hex.decode("411111111111111111111111111111111111111111"); + invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr); + program = new Program(null, null, invoke, + new InternalTransaction( + Protocol.Transaction.getDefaultInstance(), + InternalTransaction.TrxType.TRX_UNKNOWN_TYPE)); + program.getContractState().markSelfDestruct(beneficiary); + + ForkController forkController = Mockito.mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + Mockito.when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, + () -> program.suicide2(new DataWord(beneficiary))); + Assert.assertEquals( + "CPU timeout for SELFDESTRUCT with selfdestructed beneficiary", + exception.getMessage()); + } + } + @Test public void testVoteWitnessCost() throws ContractValidateException { // Build stack environment, the stack from top to bottom is 0x00, 0x80, 0x00, 0x80 diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java index 2c7aa238033..b949cbd8dc8 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java @@ -217,7 +217,7 @@ public void testWitnessArrayLargerThanAmountArray() { @Test public void testOperationRegistryWithoutOsaka() { VMConfig.initAllowTvmOsaka(0); - JumpTable table = OperationRegistry.getTable(); + JumpTable table = OperationRegistry.prepareAndGetTable(false); Operation voteOp = table.get(Op.VOTEWITNESS); assertTrue(voteOp.isEnabled()); @@ -233,7 +233,7 @@ public void testOperationRegistryWithoutOsaka() { public void testOperationRegistryWithOsaka() { VMConfig.initAllowTvmOsaka(1); try { - JumpTable table = OperationRegistry.getTable(); + JumpTable table = OperationRegistry.prepareAndGetTable(false); Operation voteOp = table.get(Op.VOTEWITNESS); assertTrue(voteOp.isEnabled()); diff --git a/framework/src/test/java/org/tron/core/actuator/ContractHashValidationTest.java b/framework/src/test/java/org/tron/core/actuator/ContractHashValidationTest.java new file mode 100644 index 00000000000..a2dd0a51e4e --- /dev/null +++ b/framework/src/test/java/org/tron/core/actuator/ContractHashValidationTest.java @@ -0,0 +1,67 @@ +package org.tron.core.actuator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.utils.ForkController; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class ContractHashValidationTest { + + @Test + public void acceptsHashFieldsBeforeActivation() { + SmartContract contract = SmartContract.newBuilder() + .setCodeHash(ByteString.copyFromUtf8("code")) + .setTrxHash(ByteString.copyFromUtf8("transaction")) + .build(); + + runWithActivation(false, () -> VMActuator.checkContractHashFields(contract)); + } + + @Test + public void rejectsCodeHashAfterActivation() { + SmartContract contract = SmartContract.newBuilder() + .setCodeHash(ByteString.copyFromUtf8("code")) + .build(); + + OutOfTimeException exception = assertThrows(OutOfTimeException.class, + () -> runWithActivation(true, () -> VMActuator.checkContractHashFields(contract))); + + assertEquals("CPU timeout for contract hash fields", exception.getMessage()); + } + + @Test + public void rejectsTransactionHashAfterActivation() { + SmartContract contract = SmartContract.newBuilder() + .setTrxHash(ByteString.copyFromUtf8("transaction")) + .build(); + + OutOfTimeException exception = assertThrows(OutOfTimeException.class, + () -> runWithActivation(true, () -> VMActuator.checkContractHashFields(contract))); + + assertEquals("CPU timeout for contract hash fields", exception.getMessage()); + } + + @Test + public void acceptsEmptyHashFieldsAfterActivation() { + runWithActivation(true, + () -> VMActuator.checkContractHashFields(SmartContract.getDefaultInstance())); + } + + private void runWithActivation(boolean activated, Runnable action) { + ForkController controller = mock(ForkController.class); + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_2)).thenReturn(activated); + try (MockedStatic controllerMock = Mockito.mockStatic(ForkController.class)) { + controllerMock.when(ForkController::instance).thenReturn(controller); + action.run(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/actuator/ContractNameValidationTest.java b/framework/src/test/java/org/tron/core/actuator/ContractNameValidationTest.java new file mode 100644 index 00000000000..e172829e7d6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/actuator/ContractNameValidationTest.java @@ -0,0 +1,65 @@ +package org.tron.core.actuator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.utils.ForkController; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.exception.ContractValidateException; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class ContractNameValidationTest { + + @Test + public void acceptsThirtyTwoByteNameAfterActivation() throws Throwable { + SmartContract contract = contractWithName("12345678901234567890123456789012"); + + runWithActivation(true, () -> VMActuator.checkContractNameLength(contract)); + } + + @Test + public void rejectsThirtyThreeByteNameAfterActivation() { + SmartContract contract = contractWithName("123456789012345678901234567890123"); + + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> runWithActivation(true, () -> VMActuator.checkContractNameLength(contract))); + + assertEquals("contractName's length cannot be greater than 32", exception.getMessage()); + } + + @Test + public void countsMultibyteNameUsingProtobufBytesAfterActivation() { + SmartContract contract = contractWithName("合合合合合合合合合合合"); + assertEquals(33, contract.getNameBytes().size()); + + assertThrows(ContractValidateException.class, + () -> runWithActivation(true, () -> VMActuator.checkContractNameLength(contract))); + } + + @Test + public void preservesNameValidationBeforeActivation() { + SmartContract contract = contractWithName("123456789012345678901234567890123"); + + assertThrows(ContractValidateException.class, + () -> runWithActivation(false, () -> VMActuator.checkContractNameLength(contract))); + } + + private SmartContract contractWithName(String name) { + return SmartContract.newBuilder().setName(name).build(); + } + + private void runWithActivation(boolean activated, ThrowingRunnable action) throws Throwable { + ForkController controller = mock(ForkController.class); + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_2)).thenReturn(activated); + try (MockedStatic controllerMock = Mockito.mockStatic(ForkController.class)) { + controllerMock.when(ForkController::instance).thenReturn(controller); + action.run(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java b/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java new file mode 100644 index 00000000000..b6568f8a862 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java @@ -0,0 +1,142 @@ +package org.tron.core.vm; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import org.junit.Test; +import org.tron.core.vm.config.VMConfig; + +public class OperationRegistryTest { + + @Test + public void constantAndTransactionExecutionsUseDedicatedTables() { + JumpTable transactionTable = OperationRegistry.prepareAndGetTable(false); + JumpTable constantCallTable = OperationRegistry.prepareAndGetTable(true); + + assertNotSame(transactionTable, constantCallTable); + assertSame(transactionTable, OperationRegistry.getTable(false)); + assertSame(constantCallTable, OperationRegistry.getTable(true)); + } + + @Test + public void transactionExecutionsReuseTable() { + JumpTable first = OperationRegistry.prepareAndGetTable(false); + JumpTable second = OperationRegistry.prepareAndGetTable(false); + + assertSame(first, second); + } + + @Test + public void constantExecutionsReuseTable() { + JumpTable first = OperationRegistry.prepareAndGetTable(true); + JumpTable second = OperationRegistry.prepareAndGetTable(true); + + assertSame(first, second); + } + + @Test + public void constantAdjustmentsDoNotMutateTransactionTable() { + boolean previousHigherLimit = VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx(); + JumpTable transactionTable = OperationRegistry.getTable(false); + JumpTable constantCallTable = OperationRegistry.getTable(true); + try { + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(0); + OperationRegistry.adjustMemOperations(transactionTable); + OperationRegistry.adjustMemOperations(constantCallTable); + Operation transactionMload = transactionTable.get(Op.MLOAD); + Operation constantMload = constantCallTable.get(Op.MLOAD); + + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(1); + OperationRegistry.adjustMemOperations(constantCallTable); + + assertSame(transactionMload, transactionTable.get(Op.MLOAD)); + assertNotSame(constantMload, constantCallTable.get(Op.MLOAD)); + } finally { + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(previousHigherLimit ? 1 : 0); + OperationRegistry.adjustMemOperations(transactionTable); + OperationRegistry.adjustMemOperations(constantCallTable); + } + } + + @Test + public void adjustedOperationsReuseCachedVariants() { + boolean previousHigherLimit = VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx(); + boolean previousEnergyAdjustment = VMConfig.allowEnergyAdjustment(); + boolean previousOsaka = VMConfig.allowTvmOsaka(); + boolean previousSelfdestructRestriction = VMConfig.allowTvmSelfdestructRestriction(); + JumpTable table = OperationRegistry.newTronV15OperationSet(); + + Operation defaultMload = table.get(Op.MLOAD); + Operation defaultMstore = table.get(Op.MSTORE); + Operation defaultMstore8 = table.get(Op.MSTORE8); + Operation defaultVoteWitness = table.get(Op.VOTEWITNESS); + Operation defaultSuicide = table.get(Op.SUICIDE); + + try { + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(1); + VMConfig.initAllowEnergyAdjustment(1); + VMConfig.initAllowTvmOsaka(0); + VMConfig.initAllowTvmSelfdestructRestriction(0); + adjustOperations(table); + + Operation adjustedMload = table.get(Op.MLOAD); + Operation adjustedMstore = table.get(Op.MSTORE); + Operation adjustedMstore8 = table.get(Op.MSTORE8); + Operation adjustedVoteWitness = table.get(Op.VOTEWITNESS); + Operation adjustedSuicide = table.get(Op.SUICIDE); + + assertNotSame(defaultMload, adjustedMload); + assertNotSame(defaultMstore, adjustedMstore); + assertNotSame(defaultMstore8, adjustedMstore8); + assertNotSame(defaultVoteWitness, adjustedVoteWitness); + assertNotSame(defaultSuicide, adjustedSuicide); + + adjustOperations(table); + assertSame(adjustedMload, table.get(Op.MLOAD)); + assertSame(adjustedMstore, table.get(Op.MSTORE)); + assertSame(adjustedMstore8, table.get(Op.MSTORE8)); + assertSame(adjustedVoteWitness, table.get(Op.VOTEWITNESS)); + assertSame(adjustedSuicide, table.get(Op.SUICIDE)); + + VMConfig.initAllowTvmOsaka(1); + VMConfig.initAllowTvmSelfdestructRestriction(1); + adjustOperations(table); + + Operation osakaVoteWitness = table.get(Op.VOTEWITNESS); + Operation restrictedSuicide = table.get(Op.SUICIDE); + assertNotSame(adjustedVoteWitness, osakaVoteWitness); + assertNotSame(adjustedSuicide, restrictedSuicide); + + adjustOperations(table); + assertSame(adjustedMload, table.get(Op.MLOAD)); + assertSame(adjustedMstore, table.get(Op.MSTORE)); + assertSame(adjustedMstore8, table.get(Op.MSTORE8)); + assertSame(osakaVoteWitness, table.get(Op.VOTEWITNESS)); + assertSame(restrictedSuicide, table.get(Op.SUICIDE)); + + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(0); + VMConfig.initAllowEnergyAdjustment(0); + VMConfig.initAllowTvmOsaka(0); + VMConfig.initAllowTvmSelfdestructRestriction(0); + adjustOperations(table); + + assertSame(defaultMload, table.get(Op.MLOAD)); + assertSame(defaultMstore, table.get(Op.MSTORE)); + assertSame(defaultMstore8, table.get(Op.MSTORE8)); + assertSame(defaultVoteWitness, table.get(Op.VOTEWITNESS)); + assertSame(defaultSuicide, table.get(Op.SUICIDE)); + } finally { + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(previousHigherLimit ? 1 : 0); + VMConfig.initAllowEnergyAdjustment(previousEnergyAdjustment ? 1 : 0); + VMConfig.initAllowTvmOsaka(previousOsaka ? 1 : 0); + VMConfig.initAllowTvmSelfdestructRestriction( + previousSelfdestructRestriction ? 1 : 0); + } + } + + private static void adjustOperations(JumpTable table) { + OperationRegistry.adjustMemOperations(table); + OperationRegistry.adjustVoteWitness(table); + OperationRegistry.adjustSelfdestruct(table); + } +} diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java new file mode 100644 index 00000000000..a3c59019b6d --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java @@ -0,0 +1,186 @@ +package org.tron.core.vm.nativecontract; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import static org.tron.core.config.Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2; +import static org.tron.protos.contract.Common.ResourceCode.BANDWIDTH; +import static org.tron.protos.contract.Common.ResourceCode.ENERGY; + +import com.google.protobuf.ByteString; +import org.junit.Assert; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.utils.DecodeUtil; +import org.tron.common.utils.ForkController; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.vm.nativecontract.param.CancelAllUnfreezeV2Param; +import org.tron.core.vm.nativecontract.param.FreezeBalanceV2Param; +import org.tron.core.vm.nativecontract.param.UnfreezeBalanceV2Param; +import org.tron.core.vm.nativecontract.param.WithdrawExpireUnfreezeParam; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.core.vm.repository.Repository; +import org.tron.protos.Protocol; +import org.tron.protos.contract.Common.ResourceCode; + +public class StakeV2AfterSelfDestructTest { + + private static final long NOW = 1_000L; + + @Test + public void freezeAfterSelfDestructIsForkGated() throws Exception { + byte[] ownerAddress = address(1); + AccountCapsule owner = account(ownerAddress, 0, 0); + owner.setBalance(TRX_PRECISION); + + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(repository.getAccount(ownerAddress)).thenReturn(owner); + when(repository.isSelfDestructed(ownerAddress)).thenReturn(true); + + FreezeBalanceV2Param param = new FreezeBalanceV2Param(); + param.setOwnerAddress(ownerAddress); + param.setFrozenBalance(TRX_PRECISION); + param.setResourceType(BANDWIDTH); + FreezeBalanceV2Processor processor = new FreezeBalanceV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + processor.validate(param, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + assertFreezeV2Timeout(() -> processor.validate(param, repository)); + } + } + + @Test + public void invalidDelegatedBalancesBlockWithdrawAndCancelAfterFork() throws Exception { + byte[] ownerAddress = address(1); + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(dynamicStore.getLatestBlockHeaderTimestamp()).thenReturn(NOW); + + WithdrawExpireUnfreezeParam withdrawParam = new WithdrawExpireUnfreezeParam(); + withdrawParam.setOwnerAddress(ownerAddress); + WithdrawExpireUnfreezeProcessor withdrawProcessor = + new WithdrawExpireUnfreezeProcessor(); + CancelAllUnfreezeV2Param cancelParam = new CancelAllUnfreezeV2Param(); + cancelParam.setOwnerAddress(ownerAddress); + CancelAllUnfreezeV2Processor cancelProcessor = new CancelAllUnfreezeV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, -1, 0)); + withdrawProcessor.validate(withdrawParam, repository); + cancelProcessor.validate(cancelParam, repository); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, 0, -1)); + withdrawProcessor.validate(withdrawParam, repository); + cancelProcessor.validate(cancelParam, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, -1, 0)); + assertInvalidDelegatedV2Timeout( + () -> withdrawProcessor.validate(withdrawParam, repository)); + assertInvalidDelegatedV2Timeout( + () -> cancelProcessor.validate(cancelParam, repository)); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, 0, -1)); + assertInvalidDelegatedV2Timeout( + () -> withdrawProcessor.validate(withdrawParam, repository)); + assertInvalidDelegatedV2Timeout( + () -> cancelProcessor.validate(cancelParam, repository)); + } + } + + @Test + public void invalidDelegatedBalancesBlockUnfreezeAfterFork() throws Exception { + byte[] ownerAddress = address(1); + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(dynamicStore.getLatestBlockHeaderTimestamp()).thenReturn(NOW); + + UnfreezeBalanceV2Param bandwidthParam = unfreezeParam(ownerAddress, BANDWIDTH); + UnfreezeBalanceV2Param energyParam = unfreezeParam(ownerAddress, ENERGY); + UnfreezeBalanceV2Processor processor = new UnfreezeBalanceV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, -1, 0, BANDWIDTH)); + processor.validate(bandwidthParam, repository); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, 0, -1, ENERGY)); + processor.validate(energyParam, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, -1, 0, BANDWIDTH)); + assertInvalidDelegatedV2Timeout( + () -> processor.validate(bandwidthParam, repository)); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, 0, -1, ENERGY)); + assertInvalidDelegatedV2Timeout(() -> processor.validate(energyParam, repository)); + } + } + + private static AccountCapsule account(byte[] address, long bandwidth, long energy) { + Protocol.Account.AccountResource resource = Protocol.Account.AccountResource.newBuilder() + .setDelegatedFrozenV2BalanceForEnergy(energy) + .build(); + Protocol.Account account = Protocol.Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .setDelegatedFrozenV2BalanceForBandwidth(bandwidth) + .setAccountResource(resource) + .build(); + return new AccountCapsule(account); + } + + private static AccountCapsule accountWithFrozenV2( + byte[] address, long bandwidth, long energy, ResourceCode resourceCode) { + AccountCapsule accountCapsule = account(address, bandwidth, energy); + if (resourceCode == BANDWIDTH) { + accountCapsule.addFrozenBalanceForBandwidthV2(TRX_PRECISION); + } else { + accountCapsule.addFrozenBalanceForEnergyV2(TRX_PRECISION); + } + return accountCapsule; + } + + private static UnfreezeBalanceV2Param unfreezeParam( + byte[] ownerAddress, ResourceCode resourceCode) { + UnfreezeBalanceV2Param param = new UnfreezeBalanceV2Param(); + param.setOwnerAddress(ownerAddress); + param.setResourceType(resourceCode); + param.setUnfreezeBalance(TRX_PRECISION); + return param; + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = DecodeUtil.addressPreFixByte; + address[address.length - 1] = (byte) suffix; + return address; + } + + private static void assertFreezeV2Timeout(ThrowingRunnable runnable) { + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, runnable); + Assert.assertEquals( + "CPU timeout for FreezeBalanceV2 after SELFDESTRUCT", exception.getMessage()); + } + + private static void assertInvalidDelegatedV2Timeout(ThrowingRunnable runnable) { + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, runnable); + Assert.assertEquals("CPU timeout for invalid delegated V2 balance", exception.getMessage()); + } +} diff --git a/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java new file mode 100644 index 00000000000..10131a06595 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java @@ -0,0 +1,36 @@ +package org.tron.core.vm.repository; + +import org.junit.Assert; +import org.junit.Test; + +public class RepositoryImplSelfDestructTest { + + private static final byte[] ADDRESS = new byte[] {1}; + + @Test + public void committedSelfDestructMarkerIsVisibleToParentAndSibling() { + Repository root = RepositoryImpl.createRoot(null); + Repository child = root.newRepositoryChild(); + + child.markSelfDestruct(ADDRESS); + Assert.assertTrue(child.isSelfDestructed(ADDRESS)); + Assert.assertFalse(root.isSelfDestructed(ADDRESS)); + + child.commit(); + Assert.assertTrue(root.isSelfDestructed(ADDRESS)); + Assert.assertTrue(root.newRepositoryChild().isSelfDestructed(ADDRESS)); + } + + @Test + public void nestedMarkerDoesNotLeakWhenOuterCallIsReverted() { + Repository root = RepositoryImpl.createRoot(null); + Repository outerCall = root.newRepositoryChild(); + Repository nestedCall = outerCall.newRepositoryChild(); + + nestedCall.markSelfDestruct(ADDRESS); + nestedCall.commit(); + + Assert.assertTrue(outerCall.isSelfDestructed(ADDRESS)); + Assert.assertFalse(root.isSelfDestructed(ADDRESS)); + } +}