Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,14 @@ private static byte[] extractBytes(byte[] data, int offset, int len) {
return Arrays.copyOfRange(data, offset, offset + len);
}

private static boolean isValidAbiEncoding(byte[] data, int headerWords, int itemWords) {
if (data == null || data.length % WORD_SIZE != 0) {
return false;
}
long tail = (long) data.length - (long) headerWords * WORD_SIZE;
return tail >= 0 && tail % ((long) itemWords * WORD_SIZE) == 0;
}

public abstract static class PrecompiledContract {

protected static final byte[] DATA_FALSE = new byte[WORD_SIZE];
Expand Down Expand Up @@ -938,6 +946,8 @@ public static class ValidateMultiSign extends PrecompiledContract {

private static final int ENGERYPERSIGN = 1500;
private static final int MAX_SIZE = 5;
private static final int ABI_HEADER_WORDS = 5;
private static final int ABI_ITEM_WORDS = 5;


@Override
Expand All @@ -949,6 +959,10 @@ public long getEnergyForData(byte[] data) {

@Override
public Pair<Boolean, byte[]> execute(byte[] rawData) {
if (VMConfig.allowTvmOsaka()
&& !isValidAbiEncoding(rawData, ABI_HEADER_WORDS, ABI_ITEM_WORDS)) {
return Pair.of(false, EMPTY_BYTE_ARRAY);
}
DataWord[] words = DataWord.parseArray(rawData);
byte[] address = words[0].toTronAddress();
int permissionId = words[1].intValueSafe();
Expand Down Expand Up @@ -1021,6 +1035,8 @@ public static class BatchValidateSign extends PrecompiledContract {
private static final String workersName = "validate-sign-contract";
private static final int ENGERYPERSIGN = 1500;
private static final int MAX_SIZE = 16;
private static final int ABI_HEADER_WORDS = 5;
private static final int ABI_ITEM_WORDS = 6;

static {
workers = ExecutorServiceManager.newFixedThreadPool(workersName,
Expand Down Expand Up @@ -1048,6 +1064,10 @@ public Pair<Boolean, byte[]> execute(byte[] data) {

private Pair<Boolean, byte[]> doExecute(byte[] data)
throws InterruptedException, ExecutionException {
if (VMConfig.allowTvmOsaka()
&& !isValidAbiEncoding(data, ABI_HEADER_WORDS, ABI_ITEM_WORDS)) {
return Pair.of(false, EMPTY_BYTE_ARRAY);
}
DataWord[] words = DataWord.parseArray(data);
byte[] hash = words[0].getData();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.junit.Test;
import org.tron.common.crypto.ECKey;
import org.tron.common.crypto.Hash;
import org.tron.common.utils.ByteUtil;
import org.tron.common.utils.StringUtil;
import org.tron.common.utils.client.utils.AbiUtil;
import org.tron.core.db.TransactionTrace;
Expand Down Expand Up @@ -130,6 +131,87 @@ public void correctionTest() {
System.gc(); // force triggering full gc to avoid timeout for next test
}

// TIP-854: after activation, batchValidateSign (H=5, I=6) must reject calldata
// whose byte length is incompatible with the (words - 5) / 6 shape the per-call
// energy formula already assumes, returning (false, empty). The guard lives in
// doExecute(); the outer try/catch does not mask it because the guard does not
// throw (pure arithmetic + a static getter).
@Test
public void testTip854RejectsMalformedCalldata() {
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 2_000_000);
VMConfig.initAllowTvmOsaka(1);
try {
// Bucket 1: 32-aligned head + sub-word trailing bytes (r=1, r=31).
for (int r : new int[]{1, 31}) {
byte[] data = new byte[(5 + 6) * 32 + r];
Pair<Boolean, byte[]> ret = contract.execute(data);
Assert.assertFalse("non-32-aligned len=" + data.length, ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Bucket 2: fewer than the static head's 5 words.
for (int bytes : new int[]{0, 32, 64, 96, 128}) {
Pair<Boolean, byte[]> ret = contract.execute(new byte[bytes]);
Assert.assertFalse("len=" + bytes + " < 5 words", ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Bucket 3: 32-aligned but tail not a multiple of I=6 words (k = 1..5).
for (int k = 1; k <= 5; k++) {
byte[] data = new byte[(5 + k) * 32];
Pair<Boolean, byte[]> ret = contract.execute(data);
Assert.assertFalse("aligned bad-tail k=" + k, ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Null calldata: explicit spec clause.
Pair<Boolean, byte[]> ret = contract.execute(null);
Assert.assertFalse("null calldata", ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
} finally {
VMConfig.initAllowTvmOsaka(0);
}
System.gc();
}

// TIP-854 Compatibility: for canonically-shaped calldata — all 65-byte real
// signatures so each bytes[i] encodes in exactly 4 words (1 length + 3 content)
// — total length equals 5*32 + 6*32*N, so pre- and post-activation must be
// observationally identical.
@Test
public void testTip854CanonicalInputUnchanged() {
contract.setConstantCall(true);
List<Object> signatures = new ArrayList<>();
List<Object> addresses = new ArrayList<>();
byte[] hash = Hash.sha3(longData);
for (int i = 0; i < 8; i++) {
ECKey key = new ECKey();
signatures.add(Hex.toHexString(key.sign(hash).toByteArray()));
addresses.add(StringUtil.encode58Check(key.getAddress()));
}

VMConfig.initAllowTvmOsaka(0);
Pair<Boolean, byte[]> pre = validateMultiSign(hash, signatures, addresses);
VMConfig.initAllowTvmOsaka(1);
try {
Pair<Boolean, byte[]> post = validateMultiSign(hash, signatures, addresses);
Assert.assertEquals(pre.getLeft(), post.getLeft());
Assert.assertArrayEquals(pre.getValue(), post.getValue());
} finally {
VMConfig.initAllowTvmOsaka(0);
}
System.gc();
}

// TIP-854: before activation the guard is not consulted. Malformed calldata
// that would raise inside doExecute gets collapsed to (true, 32-byte zero) by
// the outer catch — this is the legacy behaviour and must be preserved.
@Test
public void testTip854PreActivationNoOp() {
VMConfig.initAllowTvmOsaka(0);
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 2_000_000);
Pair<Boolean, byte[]> ret = contract.execute(new byte[(5 + 1) * 32]);
Assert.assertTrue("pre-activation must not take the new reject path", ret.getLeft());
Assert.assertEquals(32, ret.getRight().length);
}

Pair<Boolean, byte[]> validateMultiSign(byte[] hash, List<Object> signatures,
List<Object> addresses) {
List<Object> parameters = Arrays.asList("0x" + Hex.toHexString(hash), signatures, addresses);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,49 @@ Op.CALL, new DataWord(10000),
VMConfig.initAllowTvmSelfdestructRestriction(0);
}

// TIP-854 outer-frame containment: a CALL to validateMultiSign or
// batchValidateSign with malformed calldata must (a) push 0 onto the outer
// stack, (b) leave the outer frame free of any propagated exception, and
// (c) allow the outer frame to continue executing afterwards.
@Test
public void testTip854OuterFrameContainment() throws ContractValidateException {
byte prePrefixByte = DecodeUtil.addressPreFixByte;
DecodeUtil.addressPreFixByte = Constant.ADD_PRE_FIX_BYTE_MAINNET;
VMConfig.initAllowTvmOsaka(1);
try {
for (PrecompiledContracts.PrecompiledContract contract :
new PrecompiledContracts.PrecompiledContract[]{
new PrecompiledContracts.ValidateMultiSign(),
new PrecompiledContracts.BatchValidateSign()}) {
invoke = new ProgramInvokeMockImpl();
InternalTransaction interTrx = new InternalTransaction(
Protocol.Transaction.getDefaultInstance(),
InternalTransaction.TrxType.TRX_UNKNOWN_TYPE);
program = new Program(new byte[0], new byte[0], invoke, interTrx);
// inDataSize=0 ⇒ data=[] ⇒ fewer than H=5 head words ⇒ guard rejects.
MessageCall messageCall = new MessageCall(
Op.CALL, new DataWord(10000),
DataWord.ZERO(), DataWord.ZERO(),
DataWord.ZERO(), DataWord.ZERO(),
DataWord.ZERO(), DataWord.ZERO(),
DataWord.ZERO(), false);
program.callToPrecompiledAddress(messageCall, contract);

Assert.assertNull(contract.getClass().getSimpleName()
+ ": outer frame must not inherit an exception",
program.getResult().getException());
Assert.assertEquals(contract.getClass().getSimpleName() + ": inner CALL pushes 0",
DataWord.ZERO(), program.getStack().pop());
// Outer frame continues: another stack op works without throwing.
program.stackPush(new DataWord(1));
Assert.assertEquals(new DataWord(1), program.getStack().pop());
}
} finally {
VMConfig.initAllowTvmOsaka(0);
DecodeUtil.addressPreFixByte = prePrefixByte;
}
}

@Test
public void testOtherOperations() throws ContractValidateException {
invoke = new ProgramInvokeMockImpl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,110 @@ public void testDifferentCase() {
}


// TIP-854: after activation, validateMultiSign (H=5, I=5) must reject calldata
// whose byte length is incompatible with the (words - 5) / 5 shape the per-call
// energy formula already assumes, returning (false, empty).
@Test
public void testTip854RejectsMalformedCalldata() {
VMConfig.initAllowTvmOsaka(1);
try {
// Bucket 1: 32-aligned head + sub-word trailing bytes (r=1, r=31).
for (int r : new int[]{1, 31}) {
byte[] data = new byte[(5 + 5) * 32 + r];
Pair<Boolean, byte[]> ret = contract.execute(data);
Assert.assertFalse("non-32-aligned len=" + data.length, ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Bucket 2: fewer than the static head's 5 words.
for (int bytes : new int[]{0, 32, 64, 96, 128}) {
Pair<Boolean, byte[]> ret = contract.execute(new byte[bytes]);
Assert.assertFalse("len=" + bytes + " < 5 words", ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Bucket 3: 32-aligned but tail not a multiple of I=5 words (k = 1..4).
for (int k = 1; k <= 4; k++) {
byte[] data = new byte[(5 + k) * 32];
Pair<Boolean, byte[]> ret = contract.execute(data);
Assert.assertFalse("aligned bad-tail k=" + k, ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
}
// Null calldata: explicit spec clause.
Pair<Boolean, byte[]> ret = contract.execute(null);
Assert.assertFalse("null calldata", ret.getLeft());
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
} finally {
VMConfig.initAllowTvmOsaka(0);
}
}

// TIP-854 Compatibility: for canonically-shaped calldata (real 65-byte sigs,
// total length == 5*32 + 5*32*N), behaviour must be identical pre- vs
// post-activation — the guard is a no-op for well-formed inputs.
@Test
public void testTip854CanonicalInputUnchanged() {
ECKey key = new ECKey();
AccountCapsule toAccount = new AccountCapsule(ByteString.copyFrom(key.getAddress()),
Protocol.AccountType.Normal,
System.currentTimeMillis(), true, dbManager.getDynamicPropertiesStore());
ECKey key1 = new ECKey();
ECKey key2 = new ECKey();
Protocol.Permission activePermission =
Protocol.Permission.newBuilder()
.setType(Protocol.Permission.PermissionType.Active)
.setId(2)
.setPermissionName("active")
.setThreshold(2)
.setOperations(ByteString.copyFrom(ByteArray
.fromHexString("0000000000000000000000000000000000000000000000000000000000000000")))
.addKeys(Protocol.Key.newBuilder().setAddress(ByteString.copyFrom(key1.getAddress()))
.setWeight(1).build())
.addKeys(Protocol.Key.newBuilder().setAddress(ByteString.copyFrom(key2.getAddress()))
.setWeight(1).build())
.build();
toAccount.updatePermissions(toAccount.getPermissionById(0), null,
Collections.singletonList(activePermission));
dbManager.getAccountStore().put(key.getAddress(), toAccount);

byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData);
byte[] merged = ByteUtil.merge(key.getAddress(), ByteArray.fromInt(2), data);
byte[] toSign = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), merged);
List<Object> signs = new ArrayList<>();
signs.add(Hex.toHexString(key1.sign(toSign).toByteArray()));
signs.add(Hex.toHexString(key2.sign(toSign).toByteArray()));

VMConfig.initAllowTvmOsaka(0);
Pair<Boolean, byte[]> pre =
validateMultiSign(StringUtil.encode58Check(key.getAddress()), 2, data, signs);
VMConfig.initAllowTvmOsaka(1);
try {
Pair<Boolean, byte[]> post =
validateMultiSign(StringUtil.encode58Check(key.getAddress()), 2, data, signs);
Assert.assertEquals(pre.getLeft(), post.getLeft());
Assert.assertArrayEquals(pre.getValue(), post.getValue());
Assert.assertArrayEquals(DataWord.ONE().getData(), post.getValue());
} finally {
VMConfig.initAllowTvmOsaka(0);
}
}

// TIP-854: before activation, malformed calldata reaches the legacy decoder.
// Assert the guard is not taken — this precompile has no outer catch, so a
// too-short input raises inside the decoder; that is the documented
// pre-activation failure mode the TIP explicitly preserves.
@Test
public void testTip854PreActivationNoOp() {
VMConfig.initAllowTvmOsaka(0);
contract.setRepository(RepositoryImpl.createRoot(StoreFactory.getInstance()));
try {
Pair<Boolean, byte[]> ret = contract.execute(new byte[(5 + 1) * 32]);
// If the decoder happened to handle it without raising, we must not have
// taken the post-activation reject path (false, empty).
Assert.assertNotSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
} catch (RuntimeException expectedLegacyBehaviour) {
// Pre-activation: decoder may throw — this is the existing behaviour.
}
}

Pair<Boolean, byte[]> validateMultiSign(String address, int permissionId, byte[] hash,
List<Object> signatures) {
List<Object> parameters = Arrays
Expand Down
Loading