Skip to content

Commit b59683f

Browse files
committed
test(vm): cover TIP-854 calldata canonicalization for sign-validation precompiles
Add regression tests for the Osaka-gated length guard on validateMultiSign and batchValidateSign. Each precompile's existing unit test file gets three new @test methods covering the four buckets from the TIP discussion: - non-32-aligned length (r = 1, 31) - fewer than H = 5 static-head words (lengths 0, 32, 64, 96, 128) - 32-aligned but tail not a multiple of I (I = 5 / 6) - null calldata - canonicalized well-formed input unchanged pre- vs post-activation - pre-activation behaviour (legacy decoder path / outer catch) preserved OperationsTest gets one integration test that drives the reject through Program#callToPrecompiledAddress and asserts the outer-frame containment invariant: inner CALL pushes 0, the outer frame sees no propagated exception, and subsequent stack operations still succeed. Every test resets allowTvmOsaka to 0 in finally so state does not leak to neighbouring tests.
1 parent 1336418 commit b59683f

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignContractTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.junit.Test;
1111
import org.tron.common.crypto.ECKey;
1212
import org.tron.common.crypto.Hash;
13+
import org.tron.common.utils.ByteUtil;
1314
import org.tron.common.utils.StringUtil;
1415
import org.tron.common.utils.client.utils.AbiUtil;
1516
import org.tron.core.db.TransactionTrace;
@@ -130,6 +131,87 @@ public void correctionTest() {
130131
System.gc(); // force triggering full gc to avoid timeout for next test
131132
}
132133

134+
// TIP-854: after activation, batchValidateSign (H=5, I=6) must reject calldata
135+
// whose byte length is incompatible with the (words - 5) / 6 shape the per-call
136+
// energy formula already assumes, returning (false, empty). The guard lives in
137+
// doExecute(); the outer try/catch does not mask it because the guard does not
138+
// throw (pure arithmetic + a static getter).
139+
@Test
140+
public void testTip854RejectsMalformedCalldata() {
141+
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 2_000_000);
142+
VMConfig.initAllowTvmOsaka(1);
143+
try {
144+
// Bucket 1: 32-aligned head + sub-word trailing bytes (r=1, r=31).
145+
for (int r : new int[]{1, 31}) {
146+
byte[] data = new byte[(5 + 6) * 32 + r];
147+
Pair<Boolean, byte[]> ret = contract.execute(data);
148+
Assert.assertFalse("non-32-aligned len=" + data.length, ret.getLeft());
149+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
150+
}
151+
// Bucket 2: fewer than the static head's 5 words.
152+
for (int bytes : new int[]{0, 32, 64, 96, 128}) {
153+
Pair<Boolean, byte[]> ret = contract.execute(new byte[bytes]);
154+
Assert.assertFalse("len=" + bytes + " < 5 words", ret.getLeft());
155+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
156+
}
157+
// Bucket 3: 32-aligned but tail not a multiple of I=6 words (k = 1..5).
158+
for (int k = 1; k <= 5; k++) {
159+
byte[] data = new byte[(5 + k) * 32];
160+
Pair<Boolean, byte[]> ret = contract.execute(data);
161+
Assert.assertFalse("aligned bad-tail k=" + k, ret.getLeft());
162+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
163+
}
164+
// Null calldata: explicit spec clause.
165+
Pair<Boolean, byte[]> ret = contract.execute(null);
166+
Assert.assertFalse("null calldata", ret.getLeft());
167+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
168+
} finally {
169+
VMConfig.initAllowTvmOsaka(0);
170+
}
171+
System.gc();
172+
}
173+
174+
// TIP-854 Compatibility: for canonically-shaped calldata — all 65-byte real
175+
// signatures so each bytes[i] encodes in exactly 4 words (1 length + 3 content)
176+
// — total length equals 5*32 + 6*32*N, so pre- and post-activation must be
177+
// observationally identical.
178+
@Test
179+
public void testTip854CanonicalInputUnchanged() {
180+
contract.setConstantCall(true);
181+
List<Object> signatures = new ArrayList<>();
182+
List<Object> addresses = new ArrayList<>();
183+
byte[] hash = Hash.sha3(longData);
184+
for (int i = 0; i < 8; i++) {
185+
ECKey key = new ECKey();
186+
signatures.add(Hex.toHexString(key.sign(hash).toByteArray()));
187+
addresses.add(StringUtil.encode58Check(key.getAddress()));
188+
}
189+
190+
VMConfig.initAllowTvmOsaka(0);
191+
Pair<Boolean, byte[]> pre = validateMultiSign(hash, signatures, addresses);
192+
VMConfig.initAllowTvmOsaka(1);
193+
try {
194+
Pair<Boolean, byte[]> post = validateMultiSign(hash, signatures, addresses);
195+
Assert.assertEquals(pre.getLeft(), post.getLeft());
196+
Assert.assertArrayEquals(pre.getValue(), post.getValue());
197+
} finally {
198+
VMConfig.initAllowTvmOsaka(0);
199+
}
200+
System.gc();
201+
}
202+
203+
// TIP-854: before activation the guard is not consulted. Malformed calldata
204+
// that would raise inside doExecute gets collapsed to (true, 32-byte zero) by
205+
// the outer catch — this is the legacy behaviour and must be preserved.
206+
@Test
207+
public void testTip854PreActivationNoOp() {
208+
VMConfig.initAllowTvmOsaka(0);
209+
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 2_000_000);
210+
Pair<Boolean, byte[]> ret = contract.execute(new byte[(5 + 1) * 32]);
211+
Assert.assertTrue("pre-activation must not take the new reject path", ret.getLeft());
212+
Assert.assertEquals(32, ret.getRight().length);
213+
}
214+
133215
Pair<Boolean, byte[]> validateMultiSign(byte[] hash, List<Object> signatures,
134216
List<Object> addresses) {
135217
List<Object> parameters = Arrays.asList("0x" + Hex.toHexString(hash), signatures, addresses);

framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,49 @@ Op.CALL, new DataWord(10000),
786786
VMConfig.initAllowTvmSelfdestructRestriction(0);
787787
}
788788

789+
// TIP-854 outer-frame containment: a CALL to validateMultiSign or
790+
// batchValidateSign with malformed calldata must (a) push 0 onto the outer
791+
// stack, (b) leave the outer frame free of any propagated exception, and
792+
// (c) allow the outer frame to continue executing afterwards.
793+
@Test
794+
public void testTip854OuterFrameContainment() throws ContractValidateException {
795+
byte prePrefixByte = DecodeUtil.addressPreFixByte;
796+
DecodeUtil.addressPreFixByte = Constant.ADD_PRE_FIX_BYTE_MAINNET;
797+
VMConfig.initAllowTvmOsaka(1);
798+
try {
799+
for (PrecompiledContracts.PrecompiledContract contract :
800+
new PrecompiledContracts.PrecompiledContract[]{
801+
new PrecompiledContracts.ValidateMultiSign(),
802+
new PrecompiledContracts.BatchValidateSign()}) {
803+
invoke = new ProgramInvokeMockImpl();
804+
InternalTransaction interTrx = new InternalTransaction(
805+
Protocol.Transaction.getDefaultInstance(),
806+
InternalTransaction.TrxType.TRX_UNKNOWN_TYPE);
807+
program = new Program(new byte[0], new byte[0], invoke, interTrx);
808+
// inDataSize=0 ⇒ data=[] ⇒ fewer than H=5 head words ⇒ guard rejects.
809+
MessageCall messageCall = new MessageCall(
810+
Op.CALL, new DataWord(10000),
811+
DataWord.ZERO(), DataWord.ZERO(),
812+
DataWord.ZERO(), DataWord.ZERO(),
813+
DataWord.ZERO(), DataWord.ZERO(),
814+
DataWord.ZERO(), false);
815+
program.callToPrecompiledAddress(messageCall, contract);
816+
817+
Assert.assertNull(contract.getClass().getSimpleName()
818+
+ ": outer frame must not inherit an exception",
819+
program.getResult().getException());
820+
Assert.assertEquals(contract.getClass().getSimpleName() + ": inner CALL pushes 0",
821+
DataWord.ZERO(), program.getStack().pop());
822+
// Outer frame continues: another stack op works without throwing.
823+
program.stackPush(new DataWord(1));
824+
Assert.assertEquals(new DataWord(1), program.getStack().pop());
825+
}
826+
} finally {
827+
VMConfig.initAllowTvmOsaka(0);
828+
DecodeUtil.addressPreFixByte = prePrefixByte;
829+
}
830+
}
831+
789832
@Test
790833
public void testOtherOperations() throws ContractValidateException {
791834
invoke = new ProgramInvokeMockImpl();

framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignContractTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,110 @@ public void testDifferentCase() {
155155
}
156156

157157

158+
// TIP-854: after activation, validateMultiSign (H=5, I=5) must reject calldata
159+
// whose byte length is incompatible with the (words - 5) / 5 shape the per-call
160+
// energy formula already assumes, returning (false, empty).
161+
@Test
162+
public void testTip854RejectsMalformedCalldata() {
163+
VMConfig.initAllowTvmOsaka(1);
164+
try {
165+
// Bucket 1: 32-aligned head + sub-word trailing bytes (r=1, r=31).
166+
for (int r : new int[]{1, 31}) {
167+
byte[] data = new byte[(5 + 5) * 32 + r];
168+
Pair<Boolean, byte[]> ret = contract.execute(data);
169+
Assert.assertFalse("non-32-aligned len=" + data.length, ret.getLeft());
170+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
171+
}
172+
// Bucket 2: fewer than the static head's 5 words.
173+
for (int bytes : new int[]{0, 32, 64, 96, 128}) {
174+
Pair<Boolean, byte[]> ret = contract.execute(new byte[bytes]);
175+
Assert.assertFalse("len=" + bytes + " < 5 words", ret.getLeft());
176+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
177+
}
178+
// Bucket 3: 32-aligned but tail not a multiple of I=5 words (k = 1..4).
179+
for (int k = 1; k <= 4; k++) {
180+
byte[] data = new byte[(5 + k) * 32];
181+
Pair<Boolean, byte[]> ret = contract.execute(data);
182+
Assert.assertFalse("aligned bad-tail k=" + k, ret.getLeft());
183+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
184+
}
185+
// Null calldata: explicit spec clause.
186+
Pair<Boolean, byte[]> ret = contract.execute(null);
187+
Assert.assertFalse("null calldata", ret.getLeft());
188+
Assert.assertSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
189+
} finally {
190+
VMConfig.initAllowTvmOsaka(0);
191+
}
192+
}
193+
194+
// TIP-854 Compatibility: for canonically-shaped calldata (real 65-byte sigs,
195+
// total length == 5*32 + 5*32*N), behaviour must be identical pre- vs
196+
// post-activation — the guard is a no-op for well-formed inputs.
197+
@Test
198+
public void testTip854CanonicalInputUnchanged() {
199+
ECKey key = new ECKey();
200+
AccountCapsule toAccount = new AccountCapsule(ByteString.copyFrom(key.getAddress()),
201+
Protocol.AccountType.Normal,
202+
System.currentTimeMillis(), true, dbManager.getDynamicPropertiesStore());
203+
ECKey key1 = new ECKey();
204+
ECKey key2 = new ECKey();
205+
Protocol.Permission activePermission =
206+
Protocol.Permission.newBuilder()
207+
.setType(Protocol.Permission.PermissionType.Active)
208+
.setId(2)
209+
.setPermissionName("active")
210+
.setThreshold(2)
211+
.setOperations(ByteString.copyFrom(ByteArray
212+
.fromHexString("0000000000000000000000000000000000000000000000000000000000000000")))
213+
.addKeys(Protocol.Key.newBuilder().setAddress(ByteString.copyFrom(key1.getAddress()))
214+
.setWeight(1).build())
215+
.addKeys(Protocol.Key.newBuilder().setAddress(ByteString.copyFrom(key2.getAddress()))
216+
.setWeight(1).build())
217+
.build();
218+
toAccount.updatePermissions(toAccount.getPermissionById(0), null,
219+
Collections.singletonList(activePermission));
220+
dbManager.getAccountStore().put(key.getAddress(), toAccount);
221+
222+
byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData);
223+
byte[] merged = ByteUtil.merge(key.getAddress(), ByteArray.fromInt(2), data);
224+
byte[] toSign = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), merged);
225+
List<Object> signs = new ArrayList<>();
226+
signs.add(Hex.toHexString(key1.sign(toSign).toByteArray()));
227+
signs.add(Hex.toHexString(key2.sign(toSign).toByteArray()));
228+
229+
VMConfig.initAllowTvmOsaka(0);
230+
Pair<Boolean, byte[]> pre =
231+
validateMultiSign(StringUtil.encode58Check(key.getAddress()), 2, data, signs);
232+
VMConfig.initAllowTvmOsaka(1);
233+
try {
234+
Pair<Boolean, byte[]> post =
235+
validateMultiSign(StringUtil.encode58Check(key.getAddress()), 2, data, signs);
236+
Assert.assertEquals(pre.getLeft(), post.getLeft());
237+
Assert.assertArrayEquals(pre.getValue(), post.getValue());
238+
Assert.assertArrayEquals(DataWord.ONE().getData(), post.getValue());
239+
} finally {
240+
VMConfig.initAllowTvmOsaka(0);
241+
}
242+
}
243+
244+
// TIP-854: before activation, malformed calldata reaches the legacy decoder.
245+
// Assert the guard is not taken — this precompile has no outer catch, so a
246+
// too-short input raises inside the decoder; that is the documented
247+
// pre-activation failure mode the TIP explicitly preserves.
248+
@Test
249+
public void testTip854PreActivationNoOp() {
250+
VMConfig.initAllowTvmOsaka(0);
251+
contract.setRepository(RepositoryImpl.createRoot(StoreFactory.getInstance()));
252+
try {
253+
Pair<Boolean, byte[]> ret = contract.execute(new byte[(5 + 1) * 32]);
254+
// If the decoder happened to handle it without raising, we must not have
255+
// taken the post-activation reject path (false, empty).
256+
Assert.assertNotSame(ByteUtil.EMPTY_BYTE_ARRAY, ret.getRight());
257+
} catch (RuntimeException expectedLegacyBehaviour) {
258+
// Pre-activation: decoder may throw — this is the existing behaviour.
259+
}
260+
}
261+
158262
Pair<Boolean, byte[]> validateMultiSign(String address, int permissionId, byte[] hash,
159263
List<Object> signatures) {
160264
List<Object> parameters = Arrays

0 commit comments

Comments
 (0)