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 e0a721db28d..10279b470d9 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -299,6 +299,10 @@ public void execute(Object object) throws ContractExeException { result.setRuntimeError(result.getException().getMessage()); } logger.info("runtime result is :{}", result.getException().getMessage()); + } finally { + if (isConstantCall) { + VMConfig.clearLocalSnapshot(); + } } //use program returned fill context context.setProgramResult(result); diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index 74d332c5611..2c70a21ef3b 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -941,6 +941,21 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_OPTIMIZE_TVM_STORAGE: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_OPTIMIZE_TVM_STORAGE]"); + } + if (dynamicPropertiesStore.getAllowOptimizeTvmStorage() == 1) { + throw new ContractValidateException( + "[ALLOW_OPTIMIZE_TVM_STORAGE] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_OPTIMIZE_TVM_STORAGE] is only allowed to be 1"); + } + break; + } default: break; } @@ -1029,7 +1044,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_PRAGUE(95), // 0, 1 ALLOW_TVM_OSAKA(96), // 0, 1 ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1 - ALLOW_HARDEN_EXCHANGE_CALCULATION(98); // 0, 1 + ALLOW_HARDEN_EXCHANGE_CALCULATION(98), // 0, 1 + ALLOW_OPTIMIZE_TVM_STORAGE(99); // 0, 1 private long code; ProposalType(long code) { diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 3993e8ed835..8f67ed3527d 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -982,9 +982,13 @@ public Pair execute(byte[] data) { } PairingCheck check = PairingCheck.create(); + long deadlineNs = pairingDeadlineNs(); // iterating over all pairs for (int offset = 0; offset < data.length; offset += PAIR_SIZE) { + if (deadlineNs < System.nanoTime()) { + throw Program.Exception.notEnoughTime("precompiled contract pair"); + } Pair pair = decodePair(data, offset); @@ -996,12 +1000,23 @@ public Pair execute(byte[] data) { check.addPair(pair.getLeft(), pair.getRight()); } - check.run(); + if (!check.run(deadlineNs)) { + throw Program.Exception.notEnoughTime("precompiled contract pair"); + } int result = check.result(); return Pair.of(true, new DataWord(result).getData()); } + /** Same skip as {@link Program#checkCPUTimeLimit}: debug and solidity nodes do not enforce it. */ + private long pairingDeadlineNs() { + if (CommonParameter.getInstance().isDebug() + || CommonParameter.getInstance().isSolidityNode()) { + return Long.MAX_VALUE; + } + return getVmShouldEndInUs() * VMConstant.ONE_THOUSAND; + } + private Pair decodePair(byte[] in, int offset) { byte[] x = parseWord(in, offset, 0); diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index 35480935742..9d927f7039b 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -50,6 +50,7 @@ public static void load(StoreFactory storeFactory, boolean isolate) { snapshot.allowTvmSelfdestructRestriction = ds.getAllowTvmSelfdestructRestriction() == 1; snapshot.allowTvmOsaka = ds.getAllowTvmOsaka() == 1; snapshot.allowHardenResourceCalculation = ds.getAllowHardenResourceCalculation() == 1; + snapshot.allowOptimizeTvmStorage = ds.getAllowOptimizeTvmStorage() == 1; if (isolate) { VMConfig.setLocalSnapshot(snapshot); } else { diff --git a/actuator/src/main/java/org/tron/core/vm/program/Storage.java b/actuator/src/main/java/org/tron/core/vm/program/Storage.java index 572af048081..1c2b15af863 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Storage.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Storage.java @@ -9,14 +9,27 @@ import org.tron.common.crypto.Hash; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.ForkController; import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; import org.tron.core.store.StorageRowStore; public class Storage { private static final int PREFIX_BYTES = 16; + private static final int WORD_BYTES = 32; + + private enum ReadKind { + NEW, OLD, EMPTY + } + @Getter private final Map rowCache = new HashMap<>(); + private final Map oldRowKeyOwners = new HashMap<>(); + private final Map readKinds = new HashMap<>(); + private final boolean optimizeTvmStorage; + private final boolean aliasCheckEnabled; + @Getter private byte[] addrHash; @Getter @@ -26,10 +39,12 @@ public class Storage { @Setter private int contractVersion; - public Storage(byte[] address, StorageRowStore store) { + public Storage(byte[] address, StorageRowStore store, boolean optimizeTvmStorage) { addrHash = addrHash(address); this.address = address; this.store = store; + this.optimizeTvmStorage = optimizeTvmStorage; + this.aliasCheckEnabled = ForkController.instance().pass(ForkBlockVersionEnum.VERSION_4_8_2_3); } public Storage(Storage storage) { @@ -37,10 +52,16 @@ public Storage(Storage storage) { this.address = storage.getAddress().clone(); this.store = storage.store; this.contractVersion = storage.contractVersion; - storage.getRowCache().forEach((DataWord rowKey, StorageRowCapsule row) -> { + this.optimizeTvmStorage = storage.optimizeTvmStorage; + this.aliasCheckEnabled = storage.aliasCheckEnabled; + storage.getRowCache().forEach((DataWord key, StorageRowCapsule row) -> { StorageRowCapsule newRow = new StorageRowCapsule(row); - this.rowCache.put(rowKey.clone(), newRow); + this.rowCache.put(key.clone(), newRow); }); + storage.oldRowKeyOwners.forEach((DataWord legacy, DataWord owner) -> + this.oldRowKeyOwners.put(legacy.clone(), owner.clone())); + storage.readKinds.forEach((DataWord key, ReadKind kind) -> + this.readKinds.put(key.clone(), kind)); } private byte[] compose(byte[] key, byte[] addrHash) { @@ -53,6 +74,18 @@ private byte[] compose(byte[] key, byte[] addrHash) { return result; } + private byte[] getOldRowKey(DataWord key) { + return compose(key.getData(), addrHash); + } + + private byte[] getNewRowKey(DataWord key) { + byte[] result = new byte[PREFIX_BYTES + WORD_BYTES]; + arraycopy(addrHash, 0, result, 0, PREFIX_BYTES); + arraycopy(Hash.sha3(ByteUtil.merge(addrHash, key.getData())), 0, result, PREFIX_BYTES, + WORD_BYTES); + return result; + } + // 32 bytes private static byte[] addrHash(byte[] address) { return Hash.sha3(address); @@ -71,6 +104,12 @@ public void generateAddrHash(byte[] trxId) { } public DataWord getValue(DataWord key) { + if (optimizeTvmStorage) { + return getOptimized(key); + } + if (aliasCheckEnabled) { + checkAlias(key); + } if (rowCache.containsKey(key)) { return new DataWord(rowCache.get(key).getValue()); } else { @@ -84,17 +123,88 @@ public DataWord getValue(DataWord key) { } public void put(DataWord key, DataWord value) { + if (!optimizeTvmStorage && aliasCheckEnabled) { + checkAlias(key); + } if (rowCache.containsKey(key)) { rowCache.get(key).setValue(value.getData()); } else { - byte[] rowKey = compose(key.getData(), addrHash); + byte[] rowKey = optimizeTvmStorage ? getNewRowKey(key) : getOldRowKey(key); StorageRowCapsule row = new StorageRowCapsule(rowKey, value.getData()); rowCache.put(key, row); } + if (optimizeTvmStorage) { + ownOldKey(key); + } + } + + private DataWord getOptimized(DataWord key) { + if (rowCache.containsKey(key)) { + return new DataWord(rowCache.get(key).getValue()); + } + if (readKinds.get(key) == ReadKind.EMPTY + || readKinds.get(key) == ReadKind.NEW) { + return null; + } + + byte[] newRowKey = getNewRowKey(key); + StorageRowCapsule newRow = store.get(newRowKey); + if (newRow != null && newRow.getValue() != null) { + readKinds.put(key.clone(), ReadKind.NEW); + if (DataWord.isZero(newRow.getValue())) { + return null; + } + return cacheRead(key, newRowKey, newRow.getValue()); + } + + if (oldKeyTaken(key)) { + return null; + } + + byte[] oldRowKey = getOldRowKey(key); + StorageRowCapsule oldRow = store.get(oldRowKey); + if (oldRow != null && oldRow.getValue() != null) { + readKinds.put(key.clone(), ReadKind.OLD); + return cacheRead(key, oldRowKey, oldRow.getValue()); + } + + readKinds.put(key.clone(), ReadKind.EMPTY); + return null; + } + + private DataWord cacheRead(DataWord key, byte[] rowKey, byte[] value) { + StorageRowCapsule row = new StorageRowCapsule(value.clone()); + row.setRowKey(rowKey); + rowCache.put(key, row); + ownOldKey(key); + return new DataWord(row.getValue()); + } + + private void checkAlias(DataWord key) { + DataWord oldRowKey = new DataWord(getOldRowKey(key)); + DataWord owner = oldRowKeyOwners.get(oldRowKey); + if (owner == null) { + oldRowKeyOwners.put(oldRowKey, key.clone()); + } else if (!owner.equals(key)) { + throw new Program.OutOfTimeException("CPU timeout for storage check"); + } + } + + private void ownOldKey(DataWord key) { + oldRowKeyOwners.putIfAbsent(new DataWord(getOldRowKey(key)), key.clone()); + } + + private boolean oldKeyTaken(DataWord key) { + DataWord owner = oldRowKeyOwners.get(new DataWord(getOldRowKey(key))); + return owner != null && !owner.equals(key); } public void commit() { - rowCache.forEach((DataWord rowKey, StorageRowCapsule row) -> { + if (optimizeTvmStorage) { + commitOptimized(); + return; + } + rowCache.forEach((DataWord key, StorageRowCapsule row) -> { if (row.isDirty()) { if (new DataWord(row.getValue()).isZero()) { this.store.delete(row.getRowKey()); @@ -104,4 +214,32 @@ public void commit() { } }); } + + private void commitOptimized() { + rowCache.forEach((DataWord key, StorageRowCapsule row) -> { + if (!row.isDirty()) { + return; + } + putNew(key, row.getValue()); + ReadKind kind = readKinds.get(key); + if (kind == null || kind == ReadKind.OLD) { + store.delete(getOldRowKey(key)); + } + }); + readKinds.forEach((DataWord key, ReadKind kind) -> { + StorageRowCapsule row = rowCache.get(key); + if (row != null && row.isDirty()) { + return; + } + if (kind == ReadKind.OLD) { + putNew(key, row.getValue()); + store.delete(getOldRowKey(key)); + } + }); + } + + private void putNew(DataWord key, byte[] value) { + store.put(getNewRowKey(key), new StorageRowCapsule(value)); + } + } 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 7801a18798a..5c7691e6dd4 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 @@ -739,7 +739,8 @@ public Storage getStorage(byte[] address) { storage = parentStorage; } } else { - storage = new Storage(address, getStorageRowStore()); + storage = new Storage(address, getStorageRowStore(), + VMConfig.allowOptimizeTvmStorage()); } ContractCapsule contract = getContract(address); if (contract != null) { diff --git a/actuator/src/test/java/org/tron/core/vm/Bn128PairingTimeoutForkTest.java b/actuator/src/test/java/org/tron/core/vm/Bn128PairingTimeoutForkTest.java new file mode 100644 index 00000000000..2bb4c2f6db1 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/Bn128PairingTimeoutForkTest.java @@ -0,0 +1,54 @@ +package org.tron.core.vm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; + +import java.math.BigInteger; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; +import org.tron.core.vm.program.Program.OutOfTimeException; + +public class Bn128PairingTimeoutForkTest { + + @Test + public void expiredDeadlineTimesOut() { + PrecompiledContracts.BN128Pairing pairing = pairing(0); + assertThrows(OutOfTimeException.class, () -> pairing.execute(onePair())); + } + + @Test + public void futureDeadlineReturnsPairingResult() { + PrecompiledContracts.BN128Pairing pairing = pairing(Long.MAX_VALUE / 1000); + Pair out = pairing.execute(onePair()); + assertTrue(out.getLeft()); + assertEquals(32, out.getRight().length); + } + + private static PrecompiledContracts.BN128Pairing pairing(long vmShouldEndInUs) { + PrecompiledContracts.BN128Pairing pairing = new PrecompiledContracts.BN128Pairing(); + pairing.setVmShouldEndInUs(vmShouldEndInUs); + return pairing; + } + + private static byte[] onePair() { + byte[] input = new byte[192]; + write(input, 0, BigInteger.ONE); + write(input, 32, BigInteger.valueOf(2)); + write(input, 64, new BigInteger( + "11559732032986387107991004021392285783925812861821192530917403151452391805634")); + write(input, 96, new BigInteger( + "10857046999023057135944570762232829481370756359578518086990519993285655852781")); + write(input, 128, new BigInteger( + "4082367875863433681332203403145435568316851327593401208105741076214120093531")); + write(input, 160, new BigInteger( + "8495653923123431417604973247489272438418190587263600148770280649306958101930")); + return input; + } + + private static void write(byte[] dest, int offset, BigInteger value) { + byte[] raw = value.toByteArray(); + int src = raw.length > 32 ? raw.length - 32 : 0; + System.arraycopy(raw, src, dest, offset + 32 - (raw.length - src), raw.length - src); + } +} diff --git a/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java b/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java new file mode 100644 index 00000000000..e24f1a691f9 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java @@ -0,0 +1,372 @@ +package org.tron.core.vm.program; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.ForkController; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.store.StorageRowStore; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program.OutOfTimeException; + +public class StorageAliasTest { + + private ForkController controller; + private MockedStatic fork; + private StorageRowStore store; + private Storage storage; + private final DataWord slot = new DataWord(1); + private final DataWord alias = aliasedSlot(1); + + @Before + public void setUp() { + controller = mock(ForkController.class); + fork = mockStatic(ForkController.class); + fork.when(ForkController::instance).thenReturn(controller); + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(true); + VMConfig.setLocalSnapshot(new VMConfig.Snapshot()); + store = mock(StorageRowStore.class); + storage = new Storage(new byte[21], store, false); + } + + @After + public void tearDown() { + VMConfig.clearLocalSnapshot(); + fork.close(); + } + + @Test + public void repeatedWritesIncludingZeroDoNotAlias() { + storage.put(slot, new DataWord(5)); + storage.put(slot.clone(), DataWord.ZERO()); + storage.put(slot.clone(), new DataWord(7)); + assertEquals(new DataWord(7), storage.getValue(slot)); + assertThrows(OutOfTimeException.class, () -> storage.put(alias, new DataWord(8))); + } + + @Test + public void readOfAliasThrowsAndSameSlotReadWriteStillWorks() { + when(store.get(argThat(key -> key != null && key.length == 32))).thenAnswer( + invocation -> new StorageRowCapsule(new DataWord(5).getData())); + assertEquals(new DataWord(5), storage.getValue(slot)); + assertEquals(new DataWord(5), storage.getValue(slot.clone())); + assertThrows(OutOfTimeException.class, () -> storage.getValue(alias)); + storage.put(slot, DataWord.ZERO()); + assertEquals(DataWord.ZERO(), storage.getValue(slot)); + assertThrows(OutOfTimeException.class, () -> storage.put(alias, new DataWord(8))); + } + + @Test + public void readOfAliasAfterWriteThrows() { + when(store.get(argThat(key -> key != null && key.length == 32))).thenAnswer( + invocation -> new StorageRowCapsule(new DataWord(5).getData())); + storage.put(slot, new DataWord(9)); + assertThrows(OutOfTimeException.class, () -> storage.getValue(alias)); + assertEquals(new DataWord(9), storage.getValue(slot)); + } + + @Test + public void aliasedReadsRemainAllowedBeforeFork() { + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); + Storage before = new Storage(new byte[21], store, false); + when(store.get(any(byte[].class))).thenAnswer( + invocation -> new StorageRowCapsule(new DataWord(5).getData())); + assertEquals(new DataWord(5), before.getValue(slot)); + assertEquals(new DataWord(5), before.getValue(alias)); + before.put(alias, new DataWord(8)); + assertEquals(new DataWord(8), before.getValue(alias)); + } + + @Test + public void forkFlagIsCapturedOnceAtConstruction() { + clearInvocations(controller); + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); + Storage before = new Storage(new byte[21], store, false); + before.put(slot, new DataWord(5)); + + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(true); + before.put(alias, new DataWord(8)); + assertEquals(new DataWord(8), before.getValue(alias)); + Storage child = new Storage(before); + child.put(slot, new DataWord(1)); + child.put(alias, new DataWord(2)); + verify(controller, times(1)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + + Storage after = new Storage(new byte[21], store, false); + after.put(alias, new DataWord(8)); + assertThrows(OutOfTimeException.class, () -> after.put(slot, new DataWord(7))); + verify(controller, times(2)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + + clearInvocations(controller); + Storage optimized = new Storage(new byte[21], store, true); + optimized.put(slot, new DataWord(1)); + optimized.put(alias, new DataWord(2)); + assertEquals(new DataWord(2), optimized.getValue(alias)); + verify(controller, times(1)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + } + + @Test + public void aliasedWritesRemainAllowedBeforeFork() { + when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); + Storage before = new Storage(new byte[21], store, false); + before.put(slot, new DataWord(5)); + before.put(alias, new DataWord(8)); + assertEquals(new DataWord(5), before.getValue(slot)); + assertEquals(new DataWord(8), before.getValue(alias)); + } + + @Test + public void copyInheritsWrittenRowsAndAllowsRepeatedWrites() { + storage.put(slot, new DataWord(5)); + Storage child = new Storage(storage); + assertThrows(OutOfTimeException.class, () -> child.put(alias, new DataWord(8))); + child.put(slot.clone(), new DataWord(7)); + assertEquals(new DataWord(7), child.getValue(slot)); + assertEquals(new DataWord(5), storage.getValue(slot)); + } + + @Test + public void discardedChildDoesNotLeakWrittenRows() { + Storage child = new Storage(storage); + child.put(slot, new DataWord(5)); + storage.put(alias, new DataWord(8)); + assertEquals(new DataWord(8), storage.getValue(alias)); + assertEquals(new DataWord(5), child.getValue(slot)); + } + + @Test + public void versionOneUsesHashedLegacyRows() { + storage.setContractVersion(1); + storage.put(slot, new DataWord(5)); + storage.put(alias, new DataWord(8)); + assertFalse(Arrays.equals(storage.getRowCache().get(slot).getRowKey(), + storage.getRowCache().get(alias).getRowKey())); + assertEquals(new DataWord(5), storage.getValue(slot)); + assertEquals(new DataWord(8), storage.getValue(alias)); + } + + @Test + public void migratedNewKeysDoNotConflict() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); + assertEquals(new DataWord(4), optimized.getValue(slot)); + assertEquals(new DataWord(4), optimized.getValue(alias)); + optimized.put(slot, new DataWord(5)); + optimized.put(alias, new DataWord(8)); + assertEquals(new DataWord(5), optimized.getValue(slot)); + assertEquals(new DataWord(8), optimized.getValue(alias)); + } + + @Test + public void optimizedWritesDoNotTimeoutAndCollidingReadIsEmpty() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + optimized.put(slot, new DataWord(5)); + assertNull(optimized.getValue(alias)); + optimized.put(alias, new DataWord(8)); + assertEquals(48, optimized.getRowCache().get(slot).getRowKey().length); + assertEquals(48, optimized.getRowCache().get(alias).getRowKey().length); + assertEquals(new DataWord(5), optimized.getValue(slot)); + assertEquals(new DataWord(8), optimized.getValue(alias)); + assertNull(optimized.getValue(anotherAlias())); + } + + @Test + public void oldKeyIsReturnedOnlyToTheFirstCachedSlot() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + assertNull(optimized.getValue(alias)); + assertEquals(new DataWord(7), optimized.getValue(slot.clone())); + optimized.commit(); + verify(store, times(1)).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 7L)); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void newKeyWinsOverACachedOldKeyCollision() { + Storage optimized = new Storage(new byte[21], store, true); + boolean[] serveNewKey = {true}; + when(store.get(any(byte[].class))).thenAnswer(invocation -> { + byte[] key = invocation.getArgument(0); + if (serveNewKey[0] && key.length == 48) { + return new StorageRowCapsule(new DataWord(4).getData()); + } + if (key.length == 32) { + return new StorageRowCapsule(new DataWord(7).getData()); + } + return null; + }); + assertEquals(new DataWord(4), optimized.getValue(slot)); + serveNewKey[0] = false; + assertNull(optimized.getValue(alias)); + serveNewKey[0] = true; + assertEquals(new DataWord(4), optimized.getValue(alias)); + } + + @Test + public void optimizedCommitDeletesLegacyKey() { + Storage optimized = new Storage(new byte[21], store, true); + optimized.put(slot, new DataWord(5)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), any()); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void readOfOldKeyMigratesOnCommit() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 7L)); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void readOfMissingKeyDoesNotWriteAndRepeatReadSkipsStore() { + Storage optimized = new Storage(new byte[21], store, true); + assertNull(optimized.getValue(slot)); + assertNull(optimized.getValue(slot)); + optimized.commit(); + verify(store, times(2)).get(any()); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void readOfNewKeyDoesNotRewrite() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); + assertEquals(new DataWord(4), optimized.getValue(slot)); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void writeAfterReadKeepsWrittenValueAndDeletesOldKey() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + optimized.put(slot, new DataWord(9)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 9L)); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void readThenWriteOfExistingNewKeyDoesNotDeleteOld() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); + assertEquals(new DataWord(4), optimized.getValue(slot)); + optimized.put(slot, new DataWord(9)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 9L)); + verify(store, never()).delete(any()); + } + + @Test + public void writeThenCacheHitDeletesOldKey() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); + optimized.put(slot, new DataWord(9)); + assertEquals(new DataWord(9), optimized.getValue(slot)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), any()); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void storedZeroOnNewKeyReadsAsEmptyAndSkipsOldKey() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(any(byte[].class))).thenAnswer(invocation -> { + byte[] key = invocation.getArgument(0); + if (key.length == 48) { + return new StorageRowCapsule(DataWord.ZERO().getData()); + } + if (key.length == 32) { + return new StorageRowCapsule(new DataWord(7).getData()); + } + return null; + }); + assertNull(optimized.getValue(slot)); + assertNull(optimized.getValue(slot)); + Storage child = new Storage(optimized); + assertNull(child.getValue(slot)); + verify(store, times(1)).get(argThat(key -> key != null && key.length == 48)); + verify(store, never()).get(argThat(key -> key != null && key.length == 32)); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void writeAfterStoredZeroHitsCacheAndDoesNotDeleteOldKey() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(DataWord.ZERO().getData())); + assertNull(optimized.getValue(slot)); + optimized.put(slot, new DataWord(9)); + assertEquals(new DataWord(9), optimized.getValue(slot)); + optimized.commit(); + verify(store, times(1)).get(any()); + verify(store).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 9L)); + verify(store, never()).delete(any()); + } + + @Test + public void putZeroOverStoredZeroStaysInCache() { + Storage optimized = new Storage(new byte[21], store, true); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(DataWord.ZERO().getData())); + assertNull(optimized.getValue(slot)); + optimized.put(slot, DataWord.ZERO()); + assertEquals(DataWord.ZERO(), optimized.getValue(slot)); + assertEquals(DataWord.ZERO(), optimized.getValue(slot.clone())); + verify(store, times(1)).get(any()); + } + + private static DataWord aliasedSlot(int value) { + byte[] bytes = new DataWord(value).getData().clone(); + bytes[15] = 1; + return new DataWord(bytes); + } + + private static DataWord anotherAlias() { + byte[] bytes = new DataWord(1).getData().clone(); + bytes[14] = 1; + return new DataWord(bytes); + } +} diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 0f74f20d379..99dbbe71824 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -258,6 +258,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] TURKISH_KEY_MIGRATION_DONE = "TURKISH_KEY_MIGRATION_DONE".getBytes(); + private static final byte[] ALLOW_OPTIMIZE_TVM_STORAGE = "ALLOW_OPTIMIZE_TVM_STORAGE".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -3083,6 +3085,21 @@ public long getTurkishKeyMigrationDone() { .orElse(0L); } + public long getAllowOptimizeTvmStorage() { + return Optional.ofNullable(getUnchecked(ALLOW_OPTIMIZE_TVM_STORAGE)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowOptimizeTvmStorage(long value) { + this.put(ALLOW_OPTIMIZE_TVM_STORAGE, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowOptimizeTvmStorage() { + return getAllowOptimizeTvmStorage() == 1L; + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); 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 0f9402641e9..57954f2add4 100644 --- a/common/src/main/java/org/tron/core/config/Parameter.java +++ b/common/src/main/java/org/tron/core/config/Parameter.java @@ -31,7 +31,8 @@ public enum ForkBlockVersionEnum { 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_2(37, 1596780000000L, 70); + VERSION_4_8_2_2(37, 1596780000000L, 70), + VERSION_4_8_2_3(38, 1596780000000L, 70); // if add a version, modify BLOCK_VERSION simultaneously @Getter @@ -80,7 +81,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 = 37; + public static final int BLOCK_VERSION = 38; 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/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 304ced33698..6e77caa0ede 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -46,6 +46,7 @@ public static class Snapshot { public boolean allowTvmSelfdestructRestriction; public boolean allowTvmOsaka; public boolean allowHardenResourceCalculation; + public boolean allowOptimizeTvmStorage; } // HEAD / block-processing config, written by the consensus path; read by everyone with no @@ -204,6 +205,10 @@ public static void initAllowHardenResourceCalculation(long allow) { globalSnapshot.allowHardenResourceCalculation = allow == 1; } + public static void initAllowOptimizeTvmStorage(long allow) { + globalSnapshot.allowOptimizeTvmStorage = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -311,4 +316,8 @@ public static boolean allowTvmOsaka() { public static boolean allowHardenResourceCalculation() { return current().allowHardenResourceCalculation; } + + public static boolean allowOptimizeTvmStorage() { + return current().allowOptimizeTvmStorage; + } } diff --git a/common/src/main/java/org/tron/json/JSON.java b/common/src/main/java/org/tron/json/JSON.java index ddab0af82a8..92e84c18057 100644 --- a/common/src/main/java/org/tron/json/JSON.java +++ b/common/src/main/java/org/tron/json/JSON.java @@ -22,42 +22,47 @@ @Deprecated public final class JSON { - static final ObjectMapper MAPPER = JsonMapper.builder(buildFactory()) - // Fastjson Feature.AllowUnQuotedFieldNames (default ON) - .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) - // Fastjson Feature.AllowSingleQuotes (default ON) - .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) - // Partial compatibility with Fastjson Feature.AllowArbitraryCommas: - // this only covers a single trailing comma like {"a":1,} or [1,2,]. - // Repeated/arbitrary commas like {"a":1,,,,} and [1,,2] remain rejected. - .enable(JsonReadFeature.ALLOW_TRAILING_COMMA) - // Fastjson accepts a leading plus sign for numbers (for example +123, +0.5) - .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) - // Partial compatibility for Fastjson's asymmetric decimal behavior: - // Fastjson accepts +.5 but rejects .5 by default. Jackson cannot model only - // the signed form, so enabling this also accepts .5. - .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) - // Fastjson accepts a trailing decimal point for numbers (for example 5.) - .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS) - // Fastjson accepts leading zeros for numbers (for example 007) - .enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS) - // Fastjson accepts unescaped control chars in strings (for example raw tab/newline) - .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) - // Fastjson accepts Java-style comments (// and /* */) - .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) - // Fastjson Feature.UseBigDecimal (default ON) - // https://github.com/alibaba/fastjson/wiki/deserialize_disable_bigdecimal_cn - .configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true) - // Fastjson Feature.IgnoreNotMatch (default ON) — unknown fields silently ignored - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - // Fastjson 1.x rejects non-comment tokens after the root value - .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true) - // Fastjson serializes empty beans as "{}" without error - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - // Fastjson omits null-valued fields by default (WriteMapNullValue is OFF by default) - // https://github.com/alibaba/fastjson/wiki/WriteNull_cn - .serializationInclusion(JsonInclude.Include.NON_NULL) - .build(); + static final ObjectMapper MAPPER = buildMapper(buildFactory()); + private static final ObjectMapper OUTBOUND_MAPPER = buildMapper(new JsonFactory()); + + private static ObjectMapper buildMapper(JsonFactory factory) { + return JsonMapper.builder(factory) + // Fastjson Feature.AllowUnQuotedFieldNames (default ON) + .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) + // Fastjson Feature.AllowSingleQuotes (default ON) + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) + // Partial compatibility with Fastjson Feature.AllowArbitraryCommas: + // this only covers a single trailing comma like {"a":1,} or [1,2,]. + // Repeated/arbitrary commas like {"a":1,,,,} and [1,,2] remain rejected. + .enable(JsonReadFeature.ALLOW_TRAILING_COMMA) + // Fastjson accepts a leading plus sign for numbers (for example +123, +0.5) + .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) + // Partial compatibility for Fastjson's asymmetric decimal behavior: + // Fastjson accepts +.5 but rejects .5 by default. Jackson cannot model only + // the signed form, so enabling this also accepts .5. + .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) + // Fastjson accepts a trailing decimal point for numbers (for example 5.) + .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS) + // Fastjson accepts leading zeros for numbers (for example 007) + .enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS) + // Fastjson accepts unescaped control chars in strings (for example raw tab/newline) + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) + // Fastjson accepts Java-style comments (// and /* */) + .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) + // Fastjson Feature.UseBigDecimal (default ON) + // https://github.com/alibaba/fastjson/wiki/deserialize_disable_bigdecimal_cn + .configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true) + // Fastjson Feature.IgnoreNotMatch (default ON) — unknown fields silently ignored + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // Fastjson 1.x rejects non-comment tokens after the root value + .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true) + // Fastjson serializes empty beans as "{}" without error + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + // Fastjson omits null-valued fields by default (WriteMapNullValue is OFF by default) + // https://github.com/alibaba/fastjson/wiki/WriteNull_cn + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build(); + } private static JsonFactory buildFactory() { return JsonFactory.builder().streamReadConstraints(StreamReadConstraints.builder() @@ -82,11 +87,15 @@ static boolean isNullLiteral(String text) { } public static JSONObject parseObject(String text) { + return parseObject(text, MAPPER); + } + + private static JSONObject parseObject(String text, ObjectMapper mapper) { if (isNullLiteral(text)) { return null; } try { - JsonNode node = MAPPER.readTree(text); + JsonNode node = mapper.readTree(text); if (node == null || node.isNull()) { return null; } @@ -101,6 +110,10 @@ public static JSONObject parseObject(String text) { } } + static JSONObject outboundParseObject(String text) { + return parseObject(text, OUTBOUND_MAPPER); + } + public static JsonNode parse(String text) { if (isNullLiteral(text)) { return null; diff --git a/common/src/main/java/org/tron/json/JSONObject.java b/common/src/main/java/org/tron/json/JSONObject.java index b96c8f6e420..07aaa7d4ceb 100644 --- a/common/src/main/java/org/tron/json/JSONObject.java +++ b/common/src/main/java/org/tron/json/JSONObject.java @@ -42,6 +42,14 @@ public static JSONObject parseObject(String text) { return JSON.parseObject(text); } + /** + * Parses internally generated outbound JSON with Jackson's default read constraints instead of + * the request limits. Use {@link #parseObject(String)} for external input. + */ + public static JSONObject outboundParseObject(String text) { + return JSON.outboundParseObject(text); + } + public boolean containsKey(String key) { return node.has(key); } diff --git a/crypto/src/main/java/org/tron/common/crypto/zksnark/PairingCheck.java b/crypto/src/main/java/org/tron/common/crypto/zksnark/PairingCheck.java index 6426ea8a9fe..ecbc681cfb8 100644 --- a/crypto/src/main/java/org/tron/common/crypto/zksnark/PairingCheck.java +++ b/crypto/src/main/java/org/tron/common/crypto/zksnark/PairingCheck.java @@ -37,7 +37,7 @@ * success, 0 as fail

* * Usage:
  • add pairs sequentially with {@link #addPair(BN128G1, BN128G2)}
  • run check - * with {@link #run()} after all paris have been added
  • get result with {@link + * with {@link #run(long)} after all paris have been added
  • get result with {@link * #result()}
* * Arithmetic has been ported from libff @@ -228,7 +228,7 @@ public void addPair(BN128G1 g1, BN128G2 g2) { pairs.add(Pair.of(g1, g2)); } - public void run() { + public boolean run(long vmShouldEndInNs) { for (Pair pair : pairs) { @@ -238,10 +238,16 @@ public void run() { { product = product.mul(miller); } + + if (vmShouldEndInNs < System.nanoTime()) { + return false; + } } // finalize product = finalExponentiation(product); + + return vmShouldEndInNs >= System.nanoTime(); } public int result() { diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index ac54cb2b7ff..7d0b0634c7e 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1524,6 +1524,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowHardenExchangeCalculation()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowOptimizeTvmStorage") + .setValue(dbManager.getDynamicPropertiesStore().getAllowOptimizeTvmStorage()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index 543deab2fc6..0442078fdec 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -412,6 +412,10 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) .saveAllowHardenExchangeCalculation(entry.getValue()); break; } + case ALLOW_OPTIMIZE_TVM_STORAGE: { + manager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java index 36f7ee4928d..cfcf573992f 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -150,7 +150,8 @@ public static void write(Manager manager, BlockCapsule block) { return; } long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW; - Storage storage = new Storage(HISTORY_STORAGE_ADDRESS, manager.getStorageRowStore()); + Storage storage = new Storage(HISTORY_STORAGE_ADDRESS, manager.getStorageRowStore(), + manager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); storage.put(new DataWord(slot), new DataWord(block.getParentHash().getBytes())); storage.commit(); } diff --git a/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java b/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java index 370a81eef4e..844d606ebb2 100644 --- a/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java @@ -28,7 +28,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String transactionID = ByteArray .toHexString(transactionCapsule.getTransactionId().getBytes()); GrpcAPI.Return result = wallet.broadcastTransaction(transaction); - JSONObject res = JSONObject.parseObject(JsonFormat.printToString(result, params.isVisible())); + JSONObject res = JSONObject.outboundParseObject( + JsonFormat.printToString(result, params.isVisible())); res.put("txid", transactionID); response.getWriter().println(res.toJSONString()); } catch (Exception e) { diff --git a/framework/src/main/java/org/tron/core/services/http/CreateShieldedTransactionWithoutSpendAuthSigServlet.java b/framework/src/main/java/org/tron/core/services/http/CreateShieldedTransactionWithoutSpendAuthSigServlet.java index eb870bd1721..8b46d4544f5 100644 --- a/framework/src/main/java/org/tron/core/services/http/CreateShieldedTransactionWithoutSpendAuthSigServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/CreateShieldedTransactionWithoutSpendAuthSigServlet.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Component; import org.tron.api.GrpcAPI.PrivateParametersWithoutAsk; import org.tron.core.Wallet; -import org.tron.json.JSON; import org.tron.json.JSONObject; import org.tron.protos.Protocol.Transaction; @@ -32,7 +31,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) .createShieldedTransactionWithoutSpendAuthSig(build.build()) .getInstance(); String txString = Util.printCreateTransaction(tx, params.isVisible()); - JSONObject jsonObject = JSON.parseObject(txString); + JSONObject jsonObject = JSONObject.outboundParseObject(txString); if (jsonObject.containsKey("txID")) { jsonObject.remove("txID"); } diff --git a/framework/src/main/java/org/tron/core/services/http/GetContractInfoServlet.java b/framework/src/main/java/org/tron/core/services/http/GetContractInfoServlet.java index 6a1549bd398..76a74dcbf27 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetContractInfoServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetContractInfoServlet.java @@ -38,7 +38,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println("{}"); } else { JSONObject jsonSmartContract = JSONObject - .parseObject(JsonFormat.printToString(smartContract, visible)); + .outboundParseObject(JsonFormat.printToString(smartContract, visible)); response.getWriter().println(jsonSmartContract.toJSONString()); } } catch (Exception e) { @@ -66,7 +66,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println("{}"); } else { JSONObject jsonSmartContract = JSONObject - .parseObject(JsonFormat.printToString(smartContract, visible)); + .outboundParseObject(JsonFormat.printToString(smartContract, visible)); response.getWriter().println(jsonSmartContract.toJSONString()); } } catch (Exception e) { diff --git a/framework/src/main/java/org/tron/core/services/http/GetContractServlet.java b/framework/src/main/java/org/tron/core/services/http/GetContractServlet.java index 3565d3121f6..bfd014aac20 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetContractServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetContractServlet.java @@ -38,7 +38,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println("{}"); } else { JSONObject jsonSmartContract = JSONObject - .parseObject(JsonFormat.printToString(smartContract, visible)); + .outboundParseObject(JsonFormat.printToString(smartContract, visible)); response.getWriter().println(jsonSmartContract.toJSONString()); } } catch (Exception e) { @@ -66,7 +66,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println("{}"); } else { JSONObject jsonSmartContract = JSONObject - .parseObject(JsonFormat.printToString(smartContract, visible)); + .outboundParseObject(JsonFormat.printToString(smartContract, visible)); response.getWriter().println(jsonSmartContract.toJSONString()); } } catch (Exception e) { diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java index 5d0a09b1a68..72239e66574 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java @@ -29,7 +29,7 @@ private JSONObject convertLogAddressToTronAddress(TransactionInfo transactionInf transactionInfo = transactionInfo.toBuilder().clearLog().addAllLog(newLogList).build(); } - return JSONObject.parseObject(JsonFormat.printToString(transactionInfo, visible)); + return JSONObject.outboundParseObject(JsonFormat.printToString(transactionInfo, visible)); } private String printTransactionInfoList(TransactionInfoList list, boolean selfType) { diff --git a/framework/src/main/java/org/tron/core/services/http/ScanAndMarkNoteByIvkServlet.java b/framework/src/main/java/org/tron/core/services/http/ScanAndMarkNoteByIvkServlet.java index b91fd394442..f96f77bfb81 100644 --- a/framework/src/main/java/org/tron/core/services/http/ScanAndMarkNoteByIvkServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/ScanAndMarkNoteByIvkServlet.java @@ -27,7 +27,7 @@ private String convertOutput(GrpcAPI.DecryptNotesMarked notes, boolean visible) if (notes.getNoteTxsCount() == 0) { return resultString; } else { - JSONObject markedNotes = JSONObject.parseObject(resultString); + JSONObject markedNotes = JSONObject.outboundParseObject(resultString); JSONArray array = markedNotes.getJSONArray("noteTxs"); for (int index = 0; index < array.size(); index++) { JSONObject item = array.getJSONObject(index); diff --git a/framework/src/main/java/org/tron/core/services/http/ScanNoteByIvkServlet.java b/framework/src/main/java/org/tron/core/services/http/ScanNoteByIvkServlet.java index c8e25a2fc37..bd1c92d13ec 100644 --- a/framework/src/main/java/org/tron/core/services/http/ScanNoteByIvkServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/ScanNoteByIvkServlet.java @@ -25,7 +25,7 @@ public static String convertOutput(GrpcAPI.DecryptNotes notes, boolean visible) if (notes.getNoteTxsCount() == 0) { return resultString; } else { - JSONObject jsonNotes = JSONObject.parseObject(resultString); + JSONObject jsonNotes = JSONObject.outboundParseObject(resultString); JSONArray array = jsonNotes.getJSONArray("noteTxs"); for (int index = 0; index < array.size(); index++) { JSONObject item = array.getJSONObject(index); diff --git a/framework/src/main/java/org/tron/core/services/http/ScanShieldedTRC20NotesByIvkServlet.java b/framework/src/main/java/org/tron/core/services/http/ScanShieldedTRC20NotesByIvkServlet.java index d9da2453c9b..f3b4dbb884d 100644 --- a/framework/src/main/java/org/tron/core/services/http/ScanShieldedTRC20NotesByIvkServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/ScanShieldedTRC20NotesByIvkServlet.java @@ -24,7 +24,7 @@ public static String convertOutput(GrpcAPI.DecryptNotesTRC20 notes, boolean visi if (notes.getNoteTxsCount() == 0) { return resultString; } else { - JSONObject jsonNotes = JSONObject.parseObject(resultString); + JSONObject jsonNotes = JSONObject.outboundParseObject(resultString); JSONArray array = jsonNotes.getJSONArray("noteTxs"); for (int index = 0; index < array.size(); index++) { JSONObject item = array.getJSONObject(index); diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 5be2495e1f7..3047428fccc 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -109,7 +109,7 @@ public static void rejectIfEventsPresent(String[] eventsParams) { public static String printTransactionFee(String transactionFee) { JSONObject jsonObject = new JSONObject(); - JSONObject receipt = JSONObject.parseObject(transactionFee); + JSONObject receipt = JSONObject.outboundParseObject(transactionFee); jsonObject.put("Receipt", receipt.get("receipt")); return jsonObject.toJSONString(); } @@ -140,7 +140,7 @@ public static JSONObject printBlockToJSON(Block block, boolean selfType) { JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); jsonObject.put("block_header", - JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); + JSONObject.outboundParseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); @@ -150,7 +150,8 @@ public static JSONObject printBlockToJSON(Block block, boolean selfType) { public static String printTransactionList(TransactionList list, boolean selfType) { List transactions = list.getTransactionList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = JSONObject.outboundParseObject( + JsonFormat.printToString(list, selfType)); JSONArray jsonArray = new JSONArray(); transactions.stream() .forEach(transaction -> jsonArray.add(printTransactionToJSON(transaction, selfType))); @@ -160,7 +161,8 @@ public static String printTransactionList(TransactionList list, boolean selfType } public static String printTransactionIdList(TransactionIdList list, boolean selfType) { - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = JSONObject.outboundParseObject( + JsonFormat.printToString(list, selfType)); return jsonObject.toJSONString(); } @@ -190,7 +192,7 @@ public static String printCreateTransaction(Transaction transaction, boolean sel public static String printTransactionExtention(TransactionExtention transactionExtention, boolean selfType) { String string = JsonFormat.printToString(transactionExtention, selfType); - JSONObject jsonObject = JSONObject.parseObject(string); + JSONObject jsonObject = JSONObject.outboundParseObject(string); if (transactionExtention.getResult().getResult()) { JSONObject transactionObject = printTransactionToJSON(transactionExtention.getTransaction(), selfType); @@ -208,7 +210,7 @@ public static String printEstimateEnergyMessage(GrpcAPI.EstimateEnergyMessage me public static String printTransactionSignWeight(TransactionSignWeight transactionSignWeight, boolean selfType) { String string = JsonFormat.printToString(transactionSignWeight, selfType); - JSONObject jsonObject = JSONObject.parseObject(string); + JSONObject jsonObject = JSONObject.outboundParseObject(string); JSONObject jsonObjectExt = jsonObject.getJSONObject(TRANSACTION); if (jsonObjectExt != null) { jsonObjectExt.put(TRANSACTION, @@ -222,7 +224,7 @@ public static String printTransactionSignWeight(TransactionSignWeight transactio public static String printTransactionApprovedList(TransactionApprovedList transactionApprovedList, boolean selfType) { String string = JsonFormat.printToString(transactionApprovedList, selfType); - JSONObject jsonObject = JSONObject.parseObject(string); + JSONObject jsonObject = JSONObject.outboundParseObject(string); JSONObject jsonObjectExt = jsonObject.getJSONObject(TRANSACTION); if (jsonObjectExt != null) { jsonObjectExt.put(TRANSACTION, @@ -249,7 +251,7 @@ public static byte[] generateContractAddress(Transaction trx, byte[] ownerAddres public static JSONObject printTransactionToJSON(Transaction transaction, boolean selfType) { JSONObject jsonTransaction = JSONObject - .parseObject(JsonFormat.printToString(transaction, selfType)); + .outboundParseObject(JsonFormat.printToString(transaction, selfType)); JSONArray contracts = new JSONArray(); transaction.getRawData().getContractList().stream().forEach(contract -> { try { @@ -260,7 +262,7 @@ public static JSONObject printTransactionToJSON(Transaction transaction, boolean CreateSmartContract deployContract = contractParameter .unpack(CreateSmartContract.class); contractJson = JSONObject - .parseObject(JsonFormat.printToString(deployContract, selfType)); + .outboundParseObject(JsonFormat.printToString(deployContract, selfType)); byte[] ownerAddress = deployContract.getOwnerAddress().toByteArray(); byte[] contractAddress = generateContractAddress(transaction, ownerAddress); jsonTransaction.put(CONTRACT_ADDRESS, ByteArray.toHexString(contractAddress)); @@ -268,8 +270,9 @@ public static JSONObject printTransactionToJSON(Transaction transaction, boolean default: Class clazz = TransactionFactory.getContract(contract.getType()); if (clazz != null) { - contractJson = JSONObject - .parseObject(JsonFormat.printToString(contractParameter.unpack(clazz), selfType)); + String parameterJson = JsonFormat.printToString( + contractParameter.unpack(clazz), selfType); + contractJson = JSONObject.outboundParseObject(parameterJson); } break; } @@ -289,9 +292,8 @@ public static JSONObject printTransactionToJSON(Transaction transaction, boolean } }); - JSONObject rawData = JSONObject.parseObject(jsonTransaction.get("raw_data").toString()); + JSONObject rawData = jsonTransaction.getJSONObject("raw_data"); rawData.put("contract", contracts); - jsonTransaction.put("raw_data", rawData); String rawDataHex = ByteArray.toHexString(transaction.getRawData().toByteArray()); jsonTransaction.put("raw_data_hex", rawDataHex); String txID = ByteArray.toHexString(Sha256Hash @@ -538,7 +540,8 @@ public static String convertOutput(Account account) { if (account.getAssetIssuedID().isEmpty()) { return JsonFormat.printToString(account, false); } else { - JSONObject accountJson = JSONObject.parseObject(JsonFormat.printToString(account, false)); + JSONObject accountJson = JSONObject.outboundParseObject( + JsonFormat.printToString(account, false)); String assetId = accountJson.get("asset_issued_ID").toString(); accountJson.put("asset_issued_ID", ByteString.copyFrom(ByteArray.fromHexString(assetId)).toStringUtf8()); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index ca249da4e5d..6f66a0b2c70 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -35,6 +35,7 @@ public class JsonRpcServlet extends RateLimiterServlet { private static final ObjectMapper MAPPER = buildMapper(); + private static final ObjectMapper OUTBOUND_MAPPER = new ObjectMapper(); private static ObjectMapper buildMapper() { JsonFactory factory = JsonFactory.builder() @@ -241,7 +242,7 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes JsonNode responseNode; try { - responseNode = MAPPER.readTree(responseBytes); + responseNode = OUTBOUND_MAPPER.readTree(responseBytes); } catch (IOException e) { writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true); return; 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 6be47886117..bda7be2b648 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 @@ -93,7 +93,7 @@ import org.tron.core.services.jsonrpc.types.TransactionResult; import org.tron.core.store.StorageRowStore; import org.tron.core.vm.program.Storage; -import org.tron.json.JSON; +import org.tron.json.JSONObject; import org.tron.program.Version; import org.tron.protos.Protocol.Account; import org.tron.protos.Protocol.Block; @@ -627,7 +627,8 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT } StorageRowStore store = manager.getStorageRowStore(); - Storage storage = new Storage(addressByte, store); + Storage storage = new Storage(addressByte, store, + manager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); storage.setContractVersion(smartContract.getVersion()); storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); @@ -1177,7 +1178,8 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, tx = setTransactionPermissionId(args.getPermissionId(), txBuilder.build()); TransactionJson transactionJson = new TransactionJson(); - transactionJson.setTransaction(JSON.parseObject(Util.printCreateTransaction(tx, false))); + transactionJson.setTransaction( + JSONObject.outboundParseObject(Util.printCreateTransaction(tx, false))); return transactionJson; } catch (JsonRpcInvalidParamsException e) { @@ -1244,7 +1246,7 @@ private TransactionJson buildTriggerSmartContractTransaction(byte[] ownerAddress String jsonString = Util.printTransaction(trxExtBuilder.build().getTransaction(), args.isVisible()); TransactionJson transactionJson = new TransactionJson(); - transactionJson.setTransaction(JSON.parseObject(jsonString)); + transactionJson.setTransaction(JSONObject.outboundParseObject(jsonString)); return transactionJson; } @@ -1260,8 +1262,8 @@ private TransactionJson createTransactionJson(GeneratedMessageV3.Builder buil tx = setTransactionExtraData(args.getExtraData(), tx, args.isVisible()); TransactionJson transactionJson = new TransactionJson(); - transactionJson - .setTransaction(JSON.parseObject(Util.printCreateTransaction(tx, args.isVisible()))); + transactionJson.setTransaction( + JSONObject.outboundParseObject(Util.printCreateTransaction(tx, args.isVisible()))); return transactionJson; } catch (ContractValidateException e) { diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index 64f2befa2ef..d7f322f0525 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.1-1-gbd2450fe06"; - public static final String VERSION_CODE = "18828"; - private static final String VERSION = "4.8.2.2"; + public static final String VERSION_NAME = "GreatVoyage-v4.8.2.2-1-gf3e81404fe"; + public static final String VERSION_CODE = "18830"; + private static final String VERSION = "4.8.2.3"; public static String getVersion() { return VERSION; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java new file mode 100644 index 00000000000..c0120059b84 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java @@ -0,0 +1,261 @@ +package org.tron.common.runtime.vm; + +import static org.tron.protos.Protocol.Transaction.Result.contractResult.OUT_OF_TIME; +import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; + +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.crypto.Hash; +import org.tron.common.runtime.Runtime; +import org.tron.common.runtime.TvmTestUtils; +import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.ForkController; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Transaction; + +public class OptimizeTvmStorageTest extends VMTestBase { + + private static final long FEE_LIMIT = 1_000_000_000L; + private static final String NON_ALIAS = + "6001600155600260025500"; + private static final String ALIAS = + "600160015560027f00000000000000000000000000000001000000000000000000000000000000015500"; + + @Override + protected void beforeDestroy() { + VMConfig.initAllowOptimizeTvmStorage(0); + } + + @Test + public void optimizedKeyKeepsFullSlotAndTombstone() { + activateFork(); + VMConfig.initAllowOptimizeTvmStorage(1); + byte[] address = account("00000000000000000000000000000000000000c1"); + rootRepository.createAccount(address, AccountType.Normal); + rootRepository.addBalance(address, 1_000_000L); + + byte[] slotA = word(1); + byte[] slotB = word(1); + slotB[15] = 1; + rootRepository.putStorageValue(address, new DataWord(slotA), new DataWord(5)); + rootRepository.putStorageValue(address, new DataWord(slotA), new DataWord(0)); + rootRepository.commit(); + rootRepository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + rootRepository.putStorageValue(address, new DataWord(slotB), new DataWord(6)); + rootRepository.commit(); + + byte[] newA = optimizedKey(address, slotA); + byte[] newB = optimizedKey(address, slotB); + Assert.assertEquals(48, newA.length); + Assert.assertFalse(Arrays.equals(newA, newB)); + byte[] storedA = dbManager.getStorageRowStore().get(newA).getValue(); + byte[] storedB = dbManager.getStorageRowStore().get(newB).getValue(); + Assert.assertEquals(32, storedA.length); + Assert.assertTrue(new DataWord(storedA).isZero()); + Assert.assertEquals(6L, new DataWord(storedB).longValue()); + + byte[] legacyOnly = word(2); + byte[] legacyKey = legacyKey(address, legacyOnly); + dbManager.getStorageRowStore().put(legacyKey, new StorageRowCapsule(new DataWord(7).getData())); + rootRepository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + DataWord loaded = rootRepository.getStorageValue(address, new DataWord(legacyOnly)); + Assert.assertEquals(7L, loaded.longValue()); + rootRepository.putStorageValue(address, new DataWord(legacyOnly), new DataWord(9)); + rootRepository.commit(); + byte[] migrated = dbManager.getStorageRowStore().get(optimizedKey(address, legacyOnly)) + .getValue(); + Assert.assertEquals(9L, new DataWord(migrated).longValue()); + Assert.assertFalse(dbManager.getStorageRowStore().has(legacyKey)); + DataWord after = RepositoryImpl.createRoot(StoreFactory.getInstance()) + .getStorageValue(address, new DataWord(legacyOnly)); + Assert.assertEquals(9L, after.longValue()); + } + + @Test + public void migratedWriteClearsSharedLegacyRow() { + activateFork(); + VMConfig.initAllowOptimizeTvmStorage(1); + byte[] address = account("00000000000000000000000000000000000000c3"); + rootRepository.createAccount(address, AccountType.Normal); + rootRepository.commit(); + + byte[] slotA = word(1); + byte[] slotB = word(1); + slotB[15] = 1; + byte[] legacy = legacyKey(address, slotA); + Assert.assertArrayEquals(legacy, legacyKey(address, slotB)); + dbManager.getStorageRowStore().put(legacy, + new StorageRowCapsule(new DataWord(100).getData())); + + rootRepository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + rootRepository.putStorageValue(address, new DataWord(slotA), DataWord.ZERO()); + rootRepository.commit(); + + rootRepository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + Assert.assertNull(rootRepository.getStorageValue(address, new DataWord(slotB))); + Assert.assertFalse(dbManager.getStorageRowStore().has(legacy)); + Assert.assertNull(rootRepository.getStorageValue(address, new DataWord(slotA))); + byte[] stored = dbManager.getStorageRowStore() + .get(optimizedKey(address, slotA)).getValue(); + Assert.assertTrue(new DataWord(stored).isZero()); + } + + @Test + public void zeroThenNonZeroKeepsLegacyChargeBeforeProposal() throws Exception { + activateFork(); + long fresh = deploy("600160005500").getResult().getEnergyUsed(); + long zeroThenSet = deploy("6000600055600160005500").getResult().getEnergyUsed(); + // Proposal off: a cached 0 still counts as an existing value, so the second + // write is RESET (5000), not SET (20000). Delta against a fresh SET is + // two extra PUSH1 (6) + 5000 - 20000. + Assert.assertEquals(-9994L, zeroThenSet - fresh); + } + + @Test + public void zeroThenNonZeroKeepsResetChargeWhenOptimized() throws Exception { + activateFork(); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); + try { + long fresh = deploy("FreshSet", "600160005500").getResult().getEnergyUsed(); + long zeroThenSet = deploy("ZeroThenSet", "6000600055600160005500") + .getResult().getEnergyUsed(); + // A zero written earlier in this transaction stays in the cache, so the + // following non-zero write is RESET (5000), same as before the proposal. + Assert.assertEquals(-9994L, zeroThenSet - fresh); + } finally { + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); + } + } + + @Test + public void tombstoneThenNonZeroChargesAsFirstWrite() throws Exception { + activateFork(); + dbManager.getDynamicPropertiesStore().saveAllowTvmConstantinople(1); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); + VMConfig.initAllowOptimizeTvmStorage(1); + try { + String init = "6006600c60003960066000f3600160005500"; + Runtime seededRuntime = deploy("Seeded", init); + Runtime freshRuntime = deploy("Fresh", init); + Assert.assertEquals(SUCCESS, seededRuntime.getResult().getResultCode()); + Assert.assertEquals(SUCCESS, freshRuntime.getResult().getResultCode()); + byte[] seeded = seededRuntime.getResult().getContractAddress(); + byte[] fresh = freshRuntime.getResult().getContractAddress(); + + RepositoryImpl repo = RepositoryImpl.createRoot(StoreFactory.getInstance()); + repo.putStorageValue(seeded, new DataWord(0), DataWord.ZERO()); + repo.commit(); + DataWord tombstone = RepositoryImpl.createRoot(StoreFactory.getInstance()) + .getStorageValue(seeded, new DataWord(0)); + Assert.assertNull(tombstone); + + byte[] caller = Hex.decode(OWNER_ADDRESS); + Runtime seededCall = TvmTestUtils.processTransactionAndReturnRuntime( + TvmTestUtils.generateTriggerSmartContractAndGetTransaction( + caller, seeded, new byte[0], 0, FEE_LIMIT), + dbManager, null); + Runtime freshCall = TvmTestUtils.processTransactionAndReturnRuntime( + TvmTestUtils.generateTriggerSmartContractAndGetTransaction( + caller, fresh, new byte[0], 0, FEE_LIMIT), + dbManager, null); + Assert.assertEquals(SUCCESS, seededCall.getResult().getResultCode()); + Assert.assertEquals(SUCCESS, freshCall.getResult().getResultCode()); + Assert.assertEquals(freshCall.getResult().getEnergyUsed(), + seededCall.getResult().getEnergyUsed()); + DataWord written = RepositoryImpl.createRoot(StoreFactory.getInstance()) + .getStorageValue(seeded, new DataWord(0)); + Assert.assertEquals(1L, written.longValue()); + } finally { + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); + dbManager.getDynamicPropertiesStore().saveAllowTvmConstantinople(0); + } + } + + @Test + public void distinctSlotsStaySuccessfulAfterFork() throws Exception { + activateFork(); + Runtime runtime = deploy(NON_ALIAS); + Assert.assertEquals(SUCCESS, runtime.getResult().getResultCode()); + Assert.assertNull(runtime.getResult().getException()); + } + + @Test + public void aliasedSstoreTimesOutAfterFork() throws Exception { + activateFork(); + dbManager.getDynamicPropertiesStore().saveAllowTvmCompatibleEvm(0); + VMConfig.initAllowTvmCompatibleEvm(0); + byte[] address = account("00000000000000000000000000000000000000c2"); + rootRepository.createAccount(address, AccountType.Normal); + byte[] slotA = word(1); + byte[] slotB = word(1); + slotB[15] = 1; + rootRepository.putStorageValue(address, new DataWord(slotA), new DataWord(1)); + try { + rootRepository.putStorageValue(address, new DataWord(slotB), new DataWord(2)); + Assert.fail("aliased sstore must time out"); + } catch (OutOfTimeException expected) { + Assert.assertNotNull(expected); + } + + Runtime runtime = deploy(ALIAS); + Assert.assertEquals(OUT_OF_TIME, runtime.getResult().getResultCode()); + Assert.assertTrue(runtime.getResult().getException() instanceof OutOfTimeException); + } + + private Runtime deploy(String codeHex) throws Exception { + return deploy("Alias", codeHex); + } + + private Runtime deploy(String name, String codeHex) throws Exception { + byte[] caller = Hex.decode(OWNER_ADDRESS); + Transaction trx = TvmTestUtils.generateDeploySmartContractAndGetTransaction( + name, caller, "[]", codeHex, 0, FEE_LIMIT, 100, null, 0); + return TvmTestUtils.processTransactionAndReturnRuntime(trx, dbManager, null); + } + + private void activateFork() { + ForkController.instance().init(chainBaseManager); + byte[] stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + dbManager.getDynamicPropertiesStore().statsByVersion( + ForkBlockVersionEnum.VERSION_4_8_2_3.getValue(), stats); + long interval = dbManager.getDynamicPropertiesStore().getMaintenanceTimeInterval(); + long hardForkTime = ((ForkBlockVersionEnum.VERSION_4_8_2_3.getHardForkTime() - 1) + / interval + 1) * interval; + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(hardForkTime + 1); + Assert.assertTrue(ForkController.instance().pass(ForkBlockVersionEnum.VERSION_4_8_2_3)); + } + + private static byte[] account(String tail) { + return Hex.decode("41" + tail); + } + + private static byte[] word(int value) { + byte[] out = new byte[32]; + out[31] = (byte) value; + return out; + } + + private static byte[] optimizedKey(byte[] address, byte[] slot) { + byte[] addrHash = Hash.sha3(address); + byte[] key = new byte[48]; + System.arraycopy(addrHash, 0, key, 0, 16); + System.arraycopy(Hash.sha3(ByteUtil.merge(addrHash, slot)), 0, key, 16, 32); + return key; + } + + private static byte[] legacyKey(byte[] address, byte[] slot) { + byte[] key = new byte[32]; + System.arraycopy(Hash.sha3(address), 0, key, 0, 16); + System.arraycopy(slot, 16, key, 16, 16); + return key; + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java index 845db6dd6af..6dc561034db 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java @@ -103,6 +103,7 @@ private static VMConfig.Snapshot snapshotGlobal() { snapshot.allowTvmSelfdestructRestriction = VMConfig.allowTvmSelfdestructRestriction(); snapshot.allowTvmOsaka = VMConfig.allowTvmOsaka(); snapshot.allowHardenResourceCalculation = VMConfig.allowHardenResourceCalculation(); + snapshot.allowOptimizeTvmStorage = VMConfig.allowOptimizeTvmStorage(); return snapshot; } } diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java index be5a012c852..ff2eb5163ae 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -57,17 +57,21 @@ public void resetState() { // Storage.commit() translates a zero write into a row delete (see // Storage#commit), so writing ZERO to every slot the suite touches is // the cheapest way to clear leftover state between tests. - Storage storage = new Storage(addr, chainBaseManager.getStorageRowStore()); + Storage storage = openStorage(); for (long slot : new long[]{0L, 99L, 499L, 776L}) { storage.put(new DataWord(slot), DataWord.ZERO()); } storage.commit(); } + private Storage openStorage() { + return new Storage(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, + dbManager.getStorageRowStore(), + dbManager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + } + private DataWord readSlot(long slot) { - Storage storage = new Storage( - HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, - chainBaseManager.getStorageRowStore()); + Storage storage = openStorage(); return storage.getValue(new DataWord(slot)); } @@ -285,9 +289,7 @@ public void generateBlockWritesParentHashBeforeTxLoop() throws Exception { AccountStateCallBack spy = Mockito.spy(realCb); AtomicReference captured = new AtomicReference<>(); Mockito.doAnswer(inv -> { - Storage st = new Storage( - HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, - chainBaseManager.getStorageRowStore()); + Storage st = openStorage(); captured.set(st.getValue(new DataWord(expectedSlot))); return inv.callRealMethod(); }).when(spy).executeGenerateFinish(); diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java index 2dd15392684..442b7c397f3 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java @@ -1,6 +1,7 @@ package org.tron.core.db; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -23,6 +24,7 @@ import org.tron.core.config.args.Args; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program.IllegalOperationException; import org.tron.core.vm.program.Storage; import org.tron.protos.Protocol; @@ -88,7 +90,7 @@ public void cleanup() { chainBaseManager.getContractStore().delete(addr); chainBaseManager.getAccountStore().delete(addr); - Storage storage = new Storage(addr, chainBaseManager.getStorageRowStore()); + Storage storage = openStorage(); for (long slot : new long[]{0L, 1L, 50L, 100L, 900L, 999L, 1000L}) { storage.put(new DataWord(slot), DataWord.ZERO()); } @@ -96,9 +98,7 @@ public void cleanup() { } private void writeSlot(long slot, byte[] hash) { - Storage storage = new Storage( - HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, - chainBaseManager.getStorageRowStore()); + Storage storage = openStorage(); storage.put(new DataWord(slot), new DataWord(hash)); storage.commit(); } @@ -115,6 +115,12 @@ private BlockCapsule blockAt(long num) { return block; } + private Storage openStorage() { + return new Storage(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, + dbManager.getStorageRowStore(), + dbManager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + } + private static byte[] uint256(long n) { return new DataWord(n).getData(); } @@ -240,4 +246,36 @@ public void vmExecutionDoesNotInvalidOpcodeUnderShanghai() throws Exception { assertFalse("normal read must not revert", result.getRuntime().getResult().isRevert()); } + + @Test + public void writeUsesDatabaseFlagWhenVmConfigIsOff() { + VMConfig.initAllowOptimizeTvmStorage(0); + chainBaseManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); + byte[] parent = new byte[32]; + Arrays.fill(parent, (byte) 0x11); + BlockCapsule block = new BlockCapsule( + 1000L, + Sha256Hash.wrap(parent), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + HistoryBlockHashUtil.write(dbManager, block); + + long slot = 999L; + DataWord expected = new DataWord(parent); + Storage optimized = new Storage( + HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, + chainBaseManager.getStorageRowStore(), + true); + Storage legacy = new Storage( + HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, + chainBaseManager.getStorageRowStore(), + false); + assertEquals(expected, optimized.getValue(new DataWord(slot))); + assertNull(legacy.getValue(new DataWord(slot))); + assertFalse(VMConfig.allowOptimizeTvmStorage()); + + chainBaseManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); + optimized.put(new DataWord(slot), DataWord.ZERO()); + optimized.commit(); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/OutboundJsonTest.java b/framework/src/test/java/org/tron/core/services/http/OutboundJsonTest.java new file mode 100644 index 00000000000..f76765c0817 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/OutboundJsonTest.java @@ -0,0 +1,191 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.api.GrpcAPI.TransactionIdList; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.StringUtil; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.config.args.Args; +import org.tron.json.JSONArray; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; +import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; + +public class OutboundJsonTest { + + private static final String OWNER_ADDRESS = "41c076305e35aea1fe45a772fcaaab8a36e87bdb55"; + private static final ByteString OWNER = + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + private static final long TIMESTAMP = 1_700_000_000_000L; + + private long savedHttpMaxMessageSize; + + @Before + public void setUp() { + JsonFormat.clearInt64AsString(); + savedHttpMaxMessageSize = Args.getInstance().getHttpMaxMessageSize(); + Args.getInstance().setHttpMaxMessageSize(1_024); + } + + @After + public void tearDown() { + JsonFormat.clearInt64AsString(); + Args.getInstance().setHttpMaxMessageSize(savedHttpMaxMessageSize); + } + + @Test + public void testPrintTransactionWithAbi() { + int entryCount = 2; + SmartContract.ABI.Builder abi = SmartContract.ABI.newBuilder(); + for (int i = 0; i < entryCount; i++) { + abi.addEntrys(SmartContract.ABI.Entry.newBuilder().setName("f" + i) + .setType(SmartContract.ABI.Entry.EntryType.Function)); + } + ByteString bytecode = ByteString.copyFrom(new byte[] {0}); + CreateSmartContract create = CreateSmartContract.newBuilder().setOwnerAddress(OWNER) + .setNewContract(SmartContract.newBuilder().setOriginAddress(OWNER).setAbi(abi) + .setBytecode(bytecode)) + .build(); + Transaction transaction = transaction(ContractType.CreateSmartContract, Any.pack(create)); + JSONObject output = Util.printTransactionToJSON(transaction, false); + JSONObject contract = output.getJSONObject("raw_data").getJSONArray("contract") + .getJSONObject(0); + JSONObject value = contract.getJSONObject("parameter").getJSONObject("value"); + JSONArray entries = value.getJSONObject("new_contract").getJSONObject("abi") + .getJSONArray("entrys"); + assertEquals(entryCount, entries.size()); + assertEquals("f0", entries.getJSONObject(0).getString("name")); + assertEquals("f1", entries.getJSONObject(entryCount - 1).getString("name")); + assertEquals("Function", entries.getJSONObject(0).getString("type")); + assertEquals("CreateSmartContract", contract.getString("type")); + assertEquals(OWNER_ADDRESS, value.getString("owner_address")); + byte[] contractAddress = Util.generateContractAddress(transaction, OWNER.toByteArray()); + assertEquals(ByteArray.toHexString(contractAddress), + output.getString("contract_address")); + assertTransactionIdentity(transaction, output); + + JSONObject serialized = JSONObject.outboundParseObject(output.toJSONString()); + assertEquals(output, serialized); + } + + @Test + public void testPrintTransactionIdList() { + int count = 3; + TransactionIdList.Builder builder = TransactionIdList.newBuilder(); + for (int i = 0; i < count; i++) { + builder.addTxId("tx" + i); + } + TransactionIdList list = builder.build(); + JSONObject output = JSONObject.outboundParseObject(Util.printTransactionIdList(list, false)); + JSONArray ids = output.getJSONArray("txId"); + assertEquals(count, ids.size()); + assertEquals("tx0", ids.getString(0)); + assertEquals("tx" + (count - 1), ids.getString(count - 1)); + } + + @Test + public void testPrintTransactionPreservesVisibleAndInt64Formatting() { + long callValue = 9_007_199_254_740_993L; + TriggerSmartContract trigger = TriggerSmartContract.newBuilder().setOwnerAddress(OWNER) + .setContractAddress(OWNER).setCallValue(callValue).build(); + Transaction transaction = transaction(ContractType.TriggerSmartContract, Any.pack(trigger)); + for (boolean visible : new boolean[] {false, true}) { + for (boolean int64AsString : new boolean[] {false, true}) { + JsonFormat.setInt64AsString(int64AsString); + JSONObject output = Util.printTransactionToJSON(transaction, visible); + JSONObject rawData = output.getJSONObject("raw_data"); + JSONObject contract = rawData.getJSONArray("contract").getJSONObject(0); + JSONObject value = contract.getJSONObject("parameter").getJSONObject("value"); + String address = visible ? StringUtil.encode58Check(OWNER.toByteArray()) : OWNER_ADDRESS; + assertEquals(address, value.getString("owner_address")); + assertEquals(address, value.getString("contract_address")); + assertEquals("TriggerSmartContract", contract.getString("type")); + assertEquals(int64AsString, value.unwrap().get("call_value").isTextual()); + assertEquals(int64AsString, rawData.unwrap().get("timestamp").isTextual()); + assertEquals(callValue, value.getLongValue("call_value")); + assertEquals(TIMESTAMP, rawData.getLongValue("timestamp")); + assertTransactionIdentity(transaction, output); + } + } + } + + @Test + public void testPrintTransactionFeePreservesReceipt() { + String input = "{\"receipt\":{\"energy_fee\":100,\"net_fee\":20}}"; + JSONObject output = JSONObject.parseObject(Util.printTransactionFee(input)); + assertEquals(JSONObject.parseObject(input).getJSONObject("receipt"), + output.getJSONObject("Receipt")); + assertEquals(100, output.getJSONObject("Receipt").getLongValue("energy_fee")); + assertEquals(20, output.getJSONObject("Receipt").getLongValue("net_fee")); + } + + @Test + public void testGetContractPreservesAddressesForGetAndPost() throws Exception { + SmartContract contract = SmartContract.newBuilder().setName("normal-contract") + .setOriginAddress(OWNER).setContractAddress(OWNER).build(); + Wallet wallet = mock(Wallet.class); + when(wallet.getContract(BytesMessage.newBuilder().setValue(OWNER).build())) + .thenReturn(contract); + GetContractServlet servlet = new GetContractServlet(); + ReflectionTestUtils.setField(servlet, "wallet", wallet); + for (boolean visible : new boolean[] {false, true}) { + String address = visible ? StringUtil.encode58Check(OWNER.toByteArray()) : OWNER_ADDRESS; + MockHttpServletRequest get = new MockHttpServletRequest("GET", "/wallet/getcontract"); + get.setParameter("value", address); + get.setParameter("visible", Boolean.toString(visible)); + MockHttpServletResponse getResponse = new MockHttpServletResponse(); + servlet.doGet(get, getResponse); + assertContractResponse(getResponse, address); + + JSONObject body = new JSONObject(); + body.put("value", address); + body.put("visible", visible); + MockHttpServletRequest post = new MockHttpServletRequest("POST", "/wallet/getcontract"); + post.setContentType("application/json"); + post.setContent(body.toJSONString().getBytes(StandardCharsets.UTF_8)); + MockHttpServletResponse postResponse = new MockHttpServletResponse(); + servlet.doPost(post, postResponse); + assertContractResponse(postResponse, address); + } + } + + private static void assertContractResponse(MockHttpServletResponse response, String address) + throws Exception { + assertEquals(200, response.getStatus()); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(response.getContentAsString(), "normal-contract", body.getString("name")); + assertEquals(address, body.getString("origin_address")); + assertEquals(address, body.getString("contract_address")); + } + + private static Transaction transaction(ContractType type, Any parameter) { + Contract contract = Contract.newBuilder().setType(type).setParameter(parameter).build(); + return Transaction.newBuilder().setRawData(Transaction.raw.newBuilder() + .setTimestamp(TIMESTAMP).addContract(contract)).build(); + } + + private static void assertTransactionIdentity(Transaction transaction, JSONObject output) { + assertEquals(ByteArray.toHexString(transaction.getRawData().toByteArray()), + output.getString("raw_data_hex")); + assertEquals(ByteArray.toHexString(new TransactionCapsule(transaction) + .getTransactionId().getBytes()), output.getString("txID")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index d6c843b5aea..bbba8ea3d46 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -12,6 +12,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.JsonRpcServer; import java.io.IOException; import java.io.InputStream; @@ -415,6 +417,40 @@ public void tooManyTokens_returnsParseError() throws Exception { message.contains("Token count") && message.contains("exceeds the maximum allowed")); } + @Test + public void batchResponse_preservesResultsWithinByteLimit() throws Exception { + CommonParameter.getInstance().jsonRpcMaxBatchSize = 2; + ObjectNode scalarResponse = MAPPER.createObjectNode(); + scalarResponse.put("jsonrpc", "2.0"); + scalarResponse.put("result", "ok"); + scalarResponse.put("id", 41); + ObjectNode arrayResponse = MAPPER.createObjectNode(); + arrayResponse.put("jsonrpc", "2.0"); + ArrayNode result = arrayResponse.putArray("result"); + result.add(0).add(1).add(2); + arrayResponse.put("id", 42); + byte[] scalarBytes = MAPPER.writeValueAsBytes(scalarResponse); + byte[] arrayBytes = MAPPER.writeValueAsBytes(arrayResponse); + // Include the batch brackets and comma; the response fits the byte limit exactly. + CommonParameter.getInstance().jsonRpcMaxResponseSize = + scalarBytes.length + arrayBytes.length + 3; + doAnswer(inv -> { + InputStream in = inv.getArgument(0); + OutputStream out = inv.getArgument(1); + JsonNode request = MAPPER.readTree(in); + out.write(request.get("id").asInt() == 41 ? scalarBytes : arrayBytes); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":41},{\"id\":42}]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertTrue(body.isArray()); + assertEquals(2, body.size()); + assertEquals(scalarResponse, body.get(0)); + assertEquals(arrayResponse, body.get(1)); + } + // --- helpers --- private MockHttpServletResponse doPost(String body) throws Exception { diff --git a/framework/src/test/java/org/tron/json/JsonTest.java b/framework/src/test/java/org/tron/json/JsonTest.java index 081e51d5571..d38123bffdd 100644 --- a/framework/src/test/java/org/tron/json/JsonTest.java +++ b/framework/src/test/java/org/tron/json/JsonTest.java @@ -408,6 +408,55 @@ public void testJsonMapperHasConfiguredConstraints() { assertEquals((long) Constant.MAX_TOKEN_COUNT, sr.getMaxTokenCount()); } + @Test + public void testOutboundParseObjectPreservesCompatibility() { + for (String text : Arrays.asList(null, "", " \n\t", "null", "/* comment */ null")) { + assertNull(JSONObject.outboundParseObject(text)); + } + for (String text : Arrays.asList( + "{unquoted:'value', trailing:1,}", + "{a:+1,b:-2,c:.3,d:-.4,e:+.5,f:+6.,g:007}", + "{/* comment */a:'line1\n\tline2'} // trailing comment", + "{amount:1,amount:2,decimal:0.12345678901234567890123456789}", + "{large:9223372036854775808,missing:null,array:[{nested:true},false]}")) { + assertEquals(JSONObject.parseObject(text).toJSONString(), + JSONObject.outboundParseObject(text).toJSONString()); + } + assertEquals(new BigDecimal("0.12345678901234567890123456789"), + JSONObject.outboundParseObject("{value:0.12345678901234567890123456789}") + .getBigDecimal("value")); + } + + @Test + public void testOutboundParseObjectPreservesParseErrors() { + for (String text : Arrays.asList("[]", "1", "{a:abc}", "{} {}", "NULL", "{a:NaN}")) { + JSONException expected = assertThrows(JSONException.class, + () -> JSONObject.parseObject(text)); + JSONException actual = assertThrows(JSONException.class, + () -> JSONObject.outboundParseObject(text)); + assertEquals(expected.getMessage(), actual.getMessage()); + } + } + + @Test + public void testOutboundParseObjectPreservesArray() { + String text = "{\"values\":[0,1,2]}"; + JSONObject parsed = JSONObject.outboundParseObject(text); + JSONArray values = parsed.getJSONArray("values"); + assertEquals(3, values.size()); + assertEquals(Integer.valueOf(0), values.get(0)); + assertEquals(Integer.valueOf(2), values.get(2)); + assertEquals(text, parsed.toJSONString()); + } + + @Test + public void testOutboundParseObjectPreservesNestedObjects() { + String text = "{\"outer\":{\"inner\":{\"value\":1}}}"; + JSONObject parsed = JSONObject.outboundParseObject(text); + assertEquals(1, parsed.getJSONObject("outer").getJSONObject("inner").getIntValue("value")); + assertEquals(text, parsed.toJSONString()); + } + @Test public void testParseObjectRejectsOverDepth() { StringBuilder open = new StringBuilder();