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
71 changes: 71 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 @@ -40,6 +40,12 @@
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.math.ec.ECPoint;
import org.tron.common.crypto.Blake2bfMessageDigest;
import org.tron.common.crypto.Hash;
import org.tron.common.crypto.Rsv;
Expand Down Expand Up @@ -106,6 +112,7 @@ public class PrecompiledContracts {

private static final EthRipemd160 ethRipemd160 = new EthRipemd160();
private static final Blake2F blake2F = new Blake2F();
private static final P256Verify p256Verify = new P256Verify();

// FreezeV2 PrecompileContracts
private static final GetChainParameter getChainParameter = new GetChainParameter();
Expand Down Expand Up @@ -199,6 +206,8 @@ public class PrecompiledContracts {
"0000000000000000000000000000000000000000000000000000000000020003");
private static final DataWord blake2FAddr = new DataWord(
"0000000000000000000000000000000000000000000000000000000000020009");
private static final DataWord p256VerifyAddr = new DataWord(
"0000000000000000000000000000000000000000000000000000000000000100");

public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) {
try {
Expand Down Expand Up @@ -281,6 +290,9 @@ public static PrecompiledContract getContractForAddress(DataWord address) {
if (VMConfig.allowTvmCompatibleEvm() && address.equals(blake2FAddr)) {
return blake2F;
}
if (VMConfig.allowTvmOsaka() && address.equals(p256VerifyAddr)) {
return p256Verify;
}

if (VMConfig.allowTvmFreezeV2()) {
if (address.equals(getChainParameterAddr)) {
Expand Down Expand Up @@ -2221,4 +2233,63 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
}
}

public static class P256Verify extends PrecompiledContract {

private static final X9ECParameters CURVE = SECNamedCurves.getByName("secp256r1");
private static final ECDomainParameters DOMAIN = new ECDomainParameters(
CURVE.getCurve(), CURVE.getG(), CURVE.getN(), CURVE.getH());
private static final BigInteger N = CURVE.getN();
private static final BigInteger P = CURVE.getCurve().getField().getCharacteristic();
private static final int INPUT_LEN = 160;
private static final long ENERGY = 6900L;

@Override
public long getEnergyForData(byte[] data) {
return ENERGY;
}

@Override
public Pair<Boolean, byte[]> execute(byte[] data) {
if (data == null || data.length != INPUT_LEN) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
try {
byte[] hash = copyOfRange(data, 0, 32);
BigInteger r = bytesToBigInteger(copyOfRange(data, 32, 64));
BigInteger s = bytesToBigInteger(copyOfRange(data, 64, 96));
BigInteger qx = bytesToBigInteger(copyOfRange(data, 96, 128));
BigInteger qy = bytesToBigInteger(copyOfRange(data, 128, 160));

if (r.signum() <= 0 || r.compareTo(N) >= 0
|| s.signum() <= 0 || s.compareTo(N) >= 0) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
if (qx.signum() < 0 || qx.compareTo(P) >= 0
|| qy.signum() < 0 || qy.compareTo(P) >= 0) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
if (qx.signum() == 0 && qy.signum() == 0) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}

ECPoint point;
try {
point = CURVE.getCurve().createPoint(qx, qy);
} catch (IllegalArgumentException e) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
if (!point.isValid()) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}

ECDSASigner verifier = new ECDSASigner();
verifier.init(false, new ECPublicKeyParameters(point, DOMAIN));
boolean ok = verifier.verifySignature(hash, r, s);
return Pair.of(true, ok ? dataOne() : EMPTY_BYTE_ARRAY);
} catch (Throwable any) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Assert;
import org.junit.Test;
import org.tron.common.runtime.vm.DataWord;
import org.tron.common.utils.ByteArray;
import org.tron.common.utils.ByteUtil;
import org.tron.core.vm.PrecompiledContracts;
import org.tron.core.vm.config.ConfigLoader;
Expand Down Expand Up @@ -74,4 +76,55 @@ public void testEIP7823DisabledShouldPass() {
ConfigLoader.disable = false;
}
}

// P256VERIFY address per TIP-7951 / EIP-7951.
private static final DataWord P256_VERIFY_ADDR =
new DataWord(
"0000000000000000000000000000000000000000000000000000000000000100");

// First entry from the geth conformance vectors — known-valid signature.
private static final String VALID_P256_INPUT =
"4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4d"
+ "a73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac"
+ "36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d60"
+ "4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff3"
+ "7618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
private static final byte[] EXPECTED_VALID_OUTPUT = ByteUtil.merge(
new byte[31], new byte[]{0x01});

@Test
public void testP256VerifyEnabled() {
ConfigLoader.disable = true;
VMConfig.initAllowTvmOsaka(1);
try {
PrecompiledContracts.PrecompiledContract contract =
PrecompiledContracts.getContractForAddress(P256_VERIFY_ADDR);
Assert.assertNotNull("P256VERIFY must be registered when osaka is on",
contract);
Assert.assertTrue(contract instanceof PrecompiledContracts.P256Verify);

Pair<Boolean, byte[]> result = contract.execute(
ByteArray.fromHexString(VALID_P256_INPUT));
Assert.assertTrue(result.getLeft());
Assert.assertArrayEquals(EXPECTED_VALID_OUTPUT, result.getRight());
Assert.assertEquals(6900L, contract.getEnergyForData(
ByteArray.fromHexString(VALID_P256_INPUT)));
} finally {
VMConfig.initAllowTvmOsaka(0);
ConfigLoader.disable = false;
}
}

@Test
public void testP256VerifyDisabledShouldReturnNull() {
ConfigLoader.disable = true;
VMConfig.initAllowTvmOsaka(0);
try {
Assert.assertNull(
"P256VERIFY must NOT be registered when osaka is off",
PrecompiledContracts.getContractForAddress(P256_VERIFY_ADDR));
} finally {
ConfigLoader.disable = false;
}
}
}
150 changes: 150 additions & 0 deletions framework/src/test/java/org/tron/common/runtime/vm/P256VerifyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.tron.common.runtime.vm;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.InputStream;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Assert;
import org.junit.Test;
import org.tron.common.utils.ByteArray;
import org.tron.core.vm.PrecompiledContracts;

@Slf4j
public class P256VerifyTest {

private static final PrecompiledContracts.P256Verify CONTRACT =
new PrecompiledContracts.P256Verify();

public static class TestCase {
public String Input;
public String Expected;
public String Name;
public int Gas;
public boolean NoBenchmark;
}

private static byte[] hex(String s) {
return ByteArray.fromHexString(s);
}

private static byte[] success() {
byte[] r = new byte[32];
r[31] = 0x01;
return r;
}

@Test
public void gethConformanceVectors() throws Exception {
ObjectMapper mapper = new ObjectMapper();
List<TestCase> cases;
try (InputStream is = P256VerifyTest.class.getResourceAsStream(
"/precompiles/p256verify_test_vectors.json")) {
Assert.assertNotNull("test vectors resource missing", is);
cases = mapper.readerForListOf(TestCase.class).readValue(is);
}
Assert.assertFalse("vector list empty", cases.isEmpty());

for (TestCase tc : cases) {
byte[] input = ByteArray.fromHexString(tc.Input);
byte[] expected = tc.Expected == null || tc.Expected.isEmpty()
? new byte[0]
: ByteArray.fromHexString(tc.Expected);

Pair<Boolean, byte[]> result = CONTRACT.execute(input);

Assert.assertTrue(tc.Name + ": precompile must not revert", result.getLeft());
Assert.assertArrayEquals(tc.Name + ": output mismatch",
expected, result.getRight());
Assert.assertEquals(tc.Name + ": gas mismatch",
tc.Gas, CONTRACT.getEnergyForData(input));
}
}

@Test
public void rejectsNullInput() {
Pair<Boolean, byte[]> r = CONTRACT.execute(null);
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

@Test
public void rejectsEmptyInput() {
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[0]);
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

@Test
public void rejectsShortInput() {
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[159]);
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

@Test
public void rejectsLongInput() {
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[161]);
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

@Test
public void rejectsInfinityPoint() {
// Valid h, r, s plus qx=qy=0 -> infinity-encoded public key.
String input =
"4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4d"
+ "a73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac"
+ "36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d60"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000";
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

/**
* Public key coordinates are valid field elements but the point is NOT on
* the secp256r1 curve (they happen to be the secp256k1 base point). The
* precompile must fail the on-curve check before attempting verification.
* Input lifted from Besu's P256VerifyPrecompiledContractTest.
*/
@Test
public void rejectsOffCurvePoint() {
String input =
"44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56"
+ "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
+ "30dae23890abb63e378e003d7f1d5006ab23cc7b3b65b3d0c7b45c7e1e2e08b9"
+ "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
+ "b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777";
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(new byte[0], r.getRight());
}

/**
* The recovered point's x-coordinate exceeds n; verification must still
* succeed because R'.x mod n == r. Input lifted from Besu's
* testModularComparisonWhenRPrimeExceedsN.
*/
@Test
public void acceptsModularComparisonWhenRPrimeExceedsN() {
String input =
"BB5A52F42F9C9261ED4361F59422A1E30036E7C32B270C8807A419FECA605023"
+ "000000000000000000000000000000004319055358E8617B0C46353D039CDAAB"
+ "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254E"
+ "0AD99500288D466940031D72A9F5445A4D43784640855BF0A69874D2DE5FE103"
+ "C5011E6EF2C42DCD50D5D3D29F99AE6EBA2C80C9244F4C5422F0979FF0C3BA5E";
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
Assert.assertTrue(r.getLeft());
Assert.assertArrayEquals(success(), r.getRight());
}

@Test
public void gasCostIsConstant6900() {
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(null));
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[0]));
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[160]));
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[1024]));
}
}
Loading
Loading