-
Notifications
You must be signed in to change notification settings - Fork 0
test(crypto): add regression baseline for crypto #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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()); | ||
| } | ||
| } |
| 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)); | ||
| } | ||
|
|
||
| @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); | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check the execution status in the polluted- This case currently passes on any empty output, even if 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @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); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert empty output on rejected inputs.
These failure-path tests only check the status bit. If
execute()ever returnsfalsebut 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
🤖 Prompt for AI Agents