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
154 changes: 154 additions & 0 deletions framework/src/test/java/org/tron/common/crypto/zksnark/BN128Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.tron.common.crypto.zksnark;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import java.math.BigInteger;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;

/**
* Regression baseline for the alt_bn128 (BN254) curve primitives used by the
* ecAdd / ecMul / ecPairing precompiles (addresses 0x06 / 0x07 / 0x08).
*
* <p>Prior to this class the BN128 stack ({@link BN128Fp}, {@link BN128G1},
* {@link BN128G2}, {@link PairingCheck}) had no direct unit coverage — only the
* gas-accounting paths were exercised through Solidity contracts in IstanbulTest.
* These tests lock in point validation, generator membership, the G1 vs G2
* subgroup-check asymmetry, and pairing correctness against known generators.
*
* <p>The G1 subgroup behaviour is the core of HackerOne report #3769516: because
* the alt_bn128 G1 cofactor is 1, the on-curve check performed by
* {@code BN128Fp.create} is itself the subgroup check, so every on-curve G1 point
* is a valid group member and no separate isGroupMember() call is required.
*/
public class BN128Test {

// 32-byte big-endian coordinates, parsed by Fp.create via new BigInteger(1, v).

// G1 generator (1, 2)
private static final byte[] G1_X =
Hex.decode("0000000000000000000000000000000000000000000000000000000000000001");
private static final byte[] G1_Y =
Hex.decode("0000000000000000000000000000000000000000000000000000000000000002");
// -G1 = (1, p - 2): affine negation, p is the F_p modulus.
private static final byte[] G1_NEG_Y =
Hex.decode("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45");

// Subgroup order r of the alt_bn128 G1/G2 groups.
private static final BigInteger R = new BigInteger(
"21888242871839275222246405745257275088548364400416034343698204186575808495617");

// G2 generator: x = a + b*i, y = c + d*i (EIP-197 standard generator).
// BN128G2.create(a, b, c, d) expects this coordinate order.
private static final byte[] G2_A =
Hex.decode("1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed");
private static final byte[] G2_B =
Hex.decode("198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2");
private static final byte[] G2_C =
Hex.decode("12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa");
private static final byte[] G2_D =
Hex.decode("090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b");

private static byte[] word(long v) {
byte[] w = new byte[32];
for (int i = 0; i < 8; i++) {
w[31 - i] = (byte) (v >>> (8 * i));
}
return w;
}

// ---- BN128Fp / point-on-curve validation ----------------------------------

@Test
public void testG1GeneratorIsOnCurve() {
assertNotNull("G1 generator (1,2) must be accepted", BN128G1.create(G1_X, G1_Y));
}

@Test
public void testPointAtInfinityAccepted() {
// (0, 0) encodes the point at infinity and must be accepted.
assertNotNull(BN128G1.create(word(0), word(0)));
}

@Test
public void testOffCurvePointRejected() {
// (1, 1): y^2 = 1, x^3 + 3 = 4, not on the curve -> rejected.
assertNull(BN128G1.create(word(1), word(1)));
}

@Test
public void testCoordinateAboveFieldModulusRejected() {
// x = p (the field modulus) is not a valid F_p element together with y = 2.
byte[] pBytes =
Hex.decode("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47");
assertNull(BN128G1.create(pBytes, G1_Y));
}

// ---- G1 subgroup behaviour (HackerOne #3769516) ---------------------------

@Test
public void testG1GeneratorHasOrderR() {
// alt_bn128 G1 cofactor == 1, so any on-curve point is in the order-r
// subgroup. Confirm the generator genuinely has order r: r * G == O.
BN128G1 g = BN128G1.create(G1_X, G1_Y);
assertNotNull(g);
assertEquals("r * G must be the point at infinity", true, g.mul(R).isZero());
}

// ---- BN128G2 / subgroup check ---------------------------------------------

@Test
public void testG2GeneratorIsGroupMember() {
assertNotNull("G2 generator must pass the subgroup check",
BN128G2.create(G2_A, G2_B, G2_C, G2_D));
}

@Test
public void testG2OffCurveRejected() {
// All-ones coordinates are not on the twist curve -> rejected.
byte[] one = word(1);
assertNull(BN128G2.create(one, one, one, one));
}

// ---- PairingCheck correctness ---------------------------------------------

@Test
public void testPairingEmptyIsOne() {
// The empty product is the identity; ecPairing of no pairs returns 1.
PairingCheck check = PairingCheck.create();
check.run();
assertEquals(1, check.result());
}

@Test
public void testPairingNegationCancels() {
// e(G1, G2) * e(-G1, G2) == 1, the canonical pairing sanity check
// (mirrors pairing([P1, -P1], [P2, P2]) == true).
BN128G1 g1 = BN128G1.create(G1_X, G1_Y);
BN128G1 negG1 = BN128G1.create(G1_X, G1_NEG_Y);
BN128G2 g2 = BN128G2.create(G2_A, G2_B, G2_C, G2_D);
assertNotNull(g1);
assertNotNull(negG1);
assertNotNull(g2);

PairingCheck check = PairingCheck.create();
check.addPair(g1, g2);
check.addPair(negG1, g2);
check.run();
assertEquals("e(G1,G2)*e(-G1,G2) must equal 1", 1, check.result());
}

@Test
public void testPairingSinglePairIsNotOne() {
// e(G1, G2) alone is a non-trivial element, so the check returns 0.
BN128G1 g1 = BN128G1.create(G1_X, G1_Y);
BN128G2 g2 = BN128G2.create(G2_A, G2_B, G2_C, G2_D);

PairingCheck check = PairingCheck.create();
check.addPair(g1, g2);
check.run();
assertEquals(0, check.result());
}
}
100 changes: 100 additions & 0 deletions framework/src/test/java/org/tron/common/runtime/vm/Blake2FTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.tron.common.runtime.vm;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import org.tron.core.vm.PrecompiledContracts.Blake2F;

/**
* Direct execution regression baseline for the BLAKE2 F-compression precompile
* (EIP-152, address 0x09 on the compatible-EVM path).
*
* <p>The precompile was previously only exercised indirectly through a Solidity
* contract. These tests pin the canonical EIP-152 "vector 4" input/output pair
* and the input-validation rules: the input must be exactly 213 bytes and the
* final flag byte must be 0x00 or 0x01.
*
* <p>Input layout (213 bytes): rounds(4) | h(64) | m(128) | t(16) | f(1).
*/
public class Blake2FTest {

private static final Blake2F BLAKE2F = new Blake2F();

// EIP-152 test vector 4: rounds = 12, f = 1.
private static final byte[] VECTOR_4_INPUT = Hex.decode(
"0000000c"
+ "48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5"
+ "d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b"
+ "6162630000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "03000000000000000000000000000000"
+ "01");

private static final byte[] VECTOR_4_OUTPUT = Hex.decode(
"ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"
+ "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923");

@Test
public void matchesEip152Vector4() {
Pair<Boolean, byte[]> result = BLAKE2F.execute(VECTOR_4_INPUT);

assertTrue(result.getLeft());
assertEquals("output must be 64 bytes", 64, result.getRight().length);
assertArrayEquals("must match EIP-152 vector 4", VECTOR_4_OUTPUT, result.getRight());
}

@Test
public void energyEqualsRounds() {
// Gas cost equals the round count (0x0000000c = 12) for well-formed input.
assertEquals(12L, BLAKE2F.getEnergyForData(VECTOR_4_INPUT));
}

@Test
public void zeroRoundsIsAllowed() {
byte[] input = VECTOR_4_INPUT.clone();
input[0] = 0;
input[1] = 0;
input[2] = 0;
input[3] = 0;

Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertTrue(result.getLeft());
assertEquals(64, result.getRight().length);
assertEquals(0L, BLAKE2F.getEnergyForData(input));
}

@Test
public void rejectsWrongLength() {
// 212 bytes — one short of the required 213.
byte[] input = new byte[212];
Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("incorrect length must fail", result.getLeft());
}

@Test
public void rejectsInvalidFinalFlag() {
byte[] input = VECTOR_4_INPUT.clone();
input[212] = 0x02; // flag must be 0x00 or 0x01

Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("invalid finalization flag must fail", result.getLeft());
assertEquals("invalid flag must price to zero", 0L, BLAKE2F.getEnergyForData(input));
}
Comment on lines +74 to +89
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert empty output on rejected inputs.

These failure-path tests only check the status bit. If execute() ever returns false but still emits stale/non-empty bytes, this suite would miss it.

Suggested test tightening
   `@Test`
   public void rejectsWrongLength() {
     // 212 bytes — one short of the required 213.
     byte[] input = new byte[212];
     Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
     assertFalse("incorrect length must fail", result.getLeft());
+    assertEquals("failed precompile must not return output", 0, result.getRight().length);
   }
@@
   public void rejectsInvalidFinalFlag() {
     byte[] input = VECTOR_4_INPUT.clone();
     input[212] = 0x02; // flag must be 0x00 or 0x01

     Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
     assertFalse("invalid finalization flag must fail", result.getLeft());
+    assertEquals("failed precompile must not return output", 0, result.getRight().length);
     assertEquals("invalid flag must price to zero", 0L, BLAKE2F.getEnergyForData(input));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void rejectsWrongLength() {
// 212 bytes — one short of the required 213.
byte[] input = new byte[212];
Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("incorrect length must fail", result.getLeft());
}
@Test
public void rejectsInvalidFinalFlag() {
byte[] input = VECTOR_4_INPUT.clone();
input[212] = 0x02; // flag must be 0x00 or 0x01
Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("invalid finalization flag must fail", result.getLeft());
assertEquals("invalid flag must price to zero", 0L, BLAKE2F.getEnergyForData(input));
}
public void rejectsWrongLength() {
// 212 bytes — one short of the required 213.
byte[] input = new byte[212];
Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("incorrect length must fail", result.getLeft());
assertEquals("failed precompile must not return output", 0, result.getRight().length);
}
`@Test`
public void rejectsInvalidFinalFlag() {
byte[] input = VECTOR_4_INPUT.clone();
input[212] = 0x02; // flag must be 0x00 or 0x01
Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertFalse("invalid finalization flag must fail", result.getLeft());
assertEquals("failed precompile must not return output", 0, result.getRight().length);
assertEquals("invalid flag must price to zero", 0L, BLAKE2F.getEnergyForData(input));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/test/java/org/tron/common/runtime/vm/Blake2FTest.java` around
lines 74 - 89, For both rejectsWrongLength and rejectsInvalidFinalFlag, extend
the assertions to ensure that when BLAKE2F.execute(input) returns false the
output bytes are empty/stale-free: after calling BLAKE2F.execute(input) assert
result.getLeft() is false and assert that result.getRight() is either null or
contains only zero bytes (no non-zero/stale data); keep the existing energy
assertion in rejectsInvalidFinalFlag (BLAKE2F.getEnergyForData(input) == 0L).
This uses the existing BLAKE2F.execute and result.getRight() to validate no
output is produced on rejection.


@Test
public void nonFinalBlockFlagZeroAccepted() {
byte[] input = VECTOR_4_INPUT.clone();
input[212] = 0x00; // f = 0 is valid (non-final block)

Pair<Boolean, byte[]> result = BLAKE2F.execute(input);
assertTrue(result.getLeft());
assertEquals(64, result.getRight().length);
}
}
120 changes: 120 additions & 0 deletions framework/src/test/java/org/tron/common/runtime/vm/ECRecoverTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.tron.common.runtime.vm;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.math.BigInteger;
import java.util.Arrays;
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import org.tron.common.crypto.ECKey;
import org.tron.common.crypto.ECKey.ECDSASignature;
import org.tron.core.vm.PrecompiledContracts.ECRecover;

/**
* Direct execution regression baseline for the ECRECOVER precompile (address 0x01).
*
* <p>Before this class the precompile body had no asserting test — it was only
* invoked once without checking the recovered address, plus a non-asserting
* microbenchmark. These tests exercise the success path (recovered address must
* match the signer) and the failure paths (bad v, malformed r/s, short input),
* all of which must return an empty result rather than throw.
*
* <p>ECDSA signing here is deterministic (RFC 6979), so a fixed private key
* yields a stable signature and the test needs no externally pinned r/s vector.
*/
public class ECRecoverTest {

private static final ECRecover EC_RECOVER = new ECRecover();

// Fixed placeholder private key — deterministic so the recovered address is stable.
private static final BigInteger PRIV =
new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16);
private static final byte[] HASH =
Hex.decode("47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad");

private static byte[] fixed32(BigInteger b) {
byte[] x = b.toByteArray();
byte[] out = new byte[32];
if (x.length > 32) {
System.arraycopy(x, x.length - 32, out, 0, 32);
} else {
System.arraycopy(x, 0, out, 32 - x.length, x.length);
}
return out;
}

/** Build a 128-byte ECRECOVER input: hash(32) | v(32, right-aligned) | r(32) | s(32). */
private static byte[] buildInput(byte[] hash, ECDSASignature sig, byte v) {
byte[] input = new byte[128];
System.arraycopy(hash, 0, input, 0, 32);
input[63] = v;
System.arraycopy(fixed32(sig.r), 0, input, 64, 32);
System.arraycopy(fixed32(sig.s), 0, input, 96, 32);
return input;
}

@Test
public void recoversSignerAddress() {
ECKey key = ECKey.fromPrivate(PRIV);
ECDSASignature sig = key.sign(HASH);

Pair<Boolean, byte[]> result = EC_RECOVER.execute(buildInput(HASH, sig, sig.v));

assertTrue(result.getLeft());
assertEquals("recovered output must be a 32-byte word", 32, result.getRight().length);

// The precompile left-pads the 21-byte TRON address into a 32-byte word.
byte[] expected = key.getAddress();
byte[] tail = Arrays.copyOfRange(result.getRight(), 32 - expected.length, 32);
assertArrayEquals("recovered address must match the signer", expected, tail);
Comment on lines +69 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the full 32-byte return word.

This only checks the tail bytes, so a regression that returns the right address with non-zero left padding would still pass. Compare against the fully padded 32-byte expected word instead.

Proposed change
-    // The precompile left-pads the 21-byte TRON address into a 32-byte word.
-    byte[] expected = key.getAddress();
-    byte[] tail = Arrays.copyOfRange(result.getRight(), 32 - expected.length, 32);
-    assertArrayEquals("recovered address must match the signer", expected, tail);
+    // The precompile left-pads the 21-byte TRON address into a 32-byte word.
+    byte[] expected = new byte[32];
+    byte[] address = key.getAddress();
+    System.arraycopy(address, 0, expected, 32 - address.length, address.length);
+    assertArrayEquals("recovered address must match the signer", expected, result.getRight());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The precompile left-pads the 21-byte TRON address into a 32-byte word.
byte[] expected = key.getAddress();
byte[] tail = Arrays.copyOfRange(result.getRight(), 32 - expected.length, 32);
assertArrayEquals("recovered address must match the signer", expected, tail);
// The precompile left-pads the 21-byte TRON address into a 32-byte word.
byte[] expected = new byte[32];
byte[] address = key.getAddress();
System.arraycopy(address, 0, expected, 32 - address.length, address.length);
assertArrayEquals("recovered address must match the signer", expected, result.getRight());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/test/java/org/tron/common/runtime/vm/ECRecoverTest.java` around
lines 69 - 72, The test currently only compares the trailing bytes by extracting
tail from result.getRight(), which misses verifying the left padding; replace
that check by constructing a full 32-byte expected word (left-pad
key.getAddress() with zeros to length 32) and assertArrayEquals that full
expected 32-byte array against result.getRight(); update/removing references to
the temporary tail variable and keep using result.getRight() as the actual
value.

}

@Test
public void rejectsInvalidRecoveryId() {
ECKey key = ECKey.fromPrivate(PRIV);
ECDSASignature sig = key.sign(HASH);

// v = 17 is not a valid recovery id; recovery must fail -> empty result.
Pair<Boolean, byte[]> result = EC_RECOVER.execute(buildInput(HASH, sig, (byte) 17));

assertTrue(result.getLeft());
assertEquals("invalid v must yield empty output", 0, result.getRight().length);
}

@Test
public void rejectsNonZeroVPadding() {
ECKey key = ECKey.fromPrivate(PRIV);
ECDSASignature sig = key.sign(HASH);

// The 32-byte v word must be zero except its last byte; pollute a high byte.
byte[] input = buildInput(HASH, sig, sig.v);
input[32] = 0x01;

Pair<Boolean, byte[]> result = EC_RECOVER.execute(input);
assertEquals("non-zero v padding must yield empty output", 0, result.getRight().length);
Comment on lines +96 to +97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check the execution status in the polluted-v case.

This case currently passes on any empty output, even if execute() starts reporting failure. Add the same result.getLeft() assertion used by the other malformed-input tests so the regression baseline matches the intended contract.

Proposed change
     Pair<Boolean, byte[]> result = EC_RECOVER.execute(input);
+    assertTrue(result.getLeft());
     assertEquals("non-zero v padding must yield empty output", 0, result.getRight().length);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Pair<Boolean, byte[]> result = EC_RECOVER.execute(input);
assertEquals("non-zero v padding must yield empty output", 0, result.getRight().length);
Pair<Boolean, byte[]> result = EC_RECOVER.execute(input);
assertTrue(result.getLeft());
assertEquals("non-zero v padding must yield empty output", 0, result.getRight().length);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/test/java/org/tron/common/runtime/vm/ECRecoverTest.java` around
lines 96 - 97, The test currently only asserts that the output is empty for the
polluted-`v` case but omits verifying the execution status; update the
ECRecoverTest polluted-`v` case to assert the boolean execution result
(result.getLeft()) is false like the other malformed-input tests so the contract
is enforced when calling EC_RECOVER.execute(input); locate the Pair<Boolean,
byte[]> result variable and add the same result.getLeft() assertion used
elsewhere.

}

@Test
public void rejectsZeroSignatureComponents() {
// r = s = 0 is not a valid signature; must fail gracefully.
byte[] input = new byte[128];
System.arraycopy(HASH, 0, input, 0, 32);
input[63] = 27;

Pair<Boolean, byte[]> result = EC_RECOVER.execute(input);
assertTrue(result.getLeft());
assertEquals("zero r/s must yield empty output", 0, result.getRight().length);
}

@Test
public void handlesShortInputWithoutThrowing() {
// Truncated input must not throw — the precompile swallows the exception
// and returns an empty result.
Pair<Boolean, byte[]> result = EC_RECOVER.execute(new byte[64]);
assertTrue(result.getLeft());
assertEquals(0, result.getRight().length);
}
}
Loading
Loading