Skip to content

Commit 736e0f1

Browse files
Merge pull request #6720 from yanghang8612/feat/tip-7951-p256verify
feat(vm): add P256VERIFY precompile (TIP-7951)
2 parents 23a6e3f + a802b20 commit 736e0f1

6 files changed

Lines changed: 6190 additions & 0 deletions

File tree

actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
import org.apache.commons.lang3.ArrayUtils;
4141
import org.apache.commons.lang3.tuple.Pair;
4242
import org.apache.commons.lang3.tuple.Triple;
43+
import org.bouncycastle.asn1.sec.SECNamedCurves;
44+
import org.bouncycastle.asn1.x9.X9ECParameters;
45+
import org.bouncycastle.crypto.params.ECDomainParameters;
46+
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
47+
import org.bouncycastle.crypto.signers.ECDSASigner;
48+
import org.bouncycastle.math.ec.ECPoint;
4349
import org.tron.common.crypto.Blake2bfMessageDigest;
4450
import org.tron.common.crypto.Hash;
4551
import org.tron.common.crypto.Rsv;
@@ -106,6 +112,7 @@ public class PrecompiledContracts {
106112

107113
private static final EthRipemd160 ethRipemd160 = new EthRipemd160();
108114
private static final Blake2F blake2F = new Blake2F();
115+
private static final P256Verify p256Verify = new P256Verify();
109116

110117
// FreezeV2 PrecompileContracts
111118
private static final GetChainParameter getChainParameter = new GetChainParameter();
@@ -199,6 +206,8 @@ public class PrecompiledContracts {
199206
"0000000000000000000000000000000000000000000000000000000000020003");
200207
private static final DataWord blake2FAddr = new DataWord(
201208
"0000000000000000000000000000000000000000000000000000000000020009");
209+
private static final DataWord p256VerifyAddr = new DataWord(
210+
"0000000000000000000000000000000000000000000000000000000000000100");
202211

203212
public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) {
204213
try {
@@ -281,6 +290,9 @@ public static PrecompiledContract getContractForAddress(DataWord address) {
281290
if (VMConfig.allowTvmCompatibleEvm() && address.equals(blake2FAddr)) {
282291
return blake2F;
283292
}
293+
if (VMConfig.allowTvmOsaka() && address.equals(p256VerifyAddr)) {
294+
return p256Verify;
295+
}
284296

285297
if (VMConfig.allowTvmFreezeV2()) {
286298
if (address.equals(getChainParameterAddr)) {
@@ -2221,4 +2233,59 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
22212233
}
22222234
}
22232235

2236+
public static class P256Verify extends PrecompiledContract {
2237+
2238+
private static final X9ECParameters CURVE = SECNamedCurves.getByName("secp256r1");
2239+
private static final ECDomainParameters DOMAIN = new ECDomainParameters(
2240+
CURVE.getCurve(), CURVE.getG(), CURVE.getN(), CURVE.getH());
2241+
private static final BigInteger N = CURVE.getN();
2242+
private static final BigInteger P = CURVE.getCurve().getField().getCharacteristic();
2243+
private static final int INPUT_LEN = 160;
2244+
private static final long ENERGY = 6900L;
2245+
2246+
@Override
2247+
public long getEnergyForData(byte[] data) {
2248+
return ENERGY;
2249+
}
2250+
2251+
@Override
2252+
public Pair<Boolean, byte[]> execute(byte[] data) {
2253+
if (data == null || data.length != INPUT_LEN) {
2254+
return Pair.of(true, EMPTY_BYTE_ARRAY);
2255+
}
2256+
try {
2257+
byte[] hash = copyOfRange(data, 0, 32);
2258+
BigInteger r = bytesToBigInteger(copyOfRange(data, 32, 64));
2259+
BigInteger s = bytesToBigInteger(copyOfRange(data, 64, 96));
2260+
BigInteger qx = bytesToBigInteger(copyOfRange(data, 96, 128));
2261+
BigInteger qy = bytesToBigInteger(copyOfRange(data, 128, 160));
2262+
2263+
if (r.signum() <= 0 || r.compareTo(N) >= 0
2264+
|| s.signum() <= 0 || s.compareTo(N) >= 0) {
2265+
return Pair.of(true, EMPTY_BYTE_ARRAY);
2266+
}
2267+
if (qx.signum() < 0 || qx.compareTo(P) >= 0
2268+
|| qy.signum() < 0 || qy.compareTo(P) >= 0) {
2269+
return Pair.of(true, EMPTY_BYTE_ARRAY);
2270+
}
2271+
if (qx.signum() == 0 && qy.signum() == 0) {
2272+
return Pair.of(true, EMPTY_BYTE_ARRAY);
2273+
}
2274+
2275+
ECPoint point = CURVE.getCurve().createPoint(qx, qy);
2276+
DOMAIN.validatePublicPoint(point);
2277+
2278+
ECDSASigner verifier = new ECDSASigner();
2279+
verifier.init(false, new ECPublicKeyParameters(point, DOMAIN));
2280+
boolean ok = verifier.verifySignature(hash, r, s);
2281+
return Pair.of(true, ok ? dataOne() : EMPTY_BYTE_ARRAY);
2282+
} catch (Exception e) {
2283+
// Off-curve point: createPoint / validatePublicPoint throw IllegalArgumentException.
2284+
// Crafted signature: BouncyCastle has a known NPE bug inside verifySignature.
2285+
// EIP-7951 mandates the precompile never reverts; map any failure to (true, empty).
2286+
return Pair.of(true, EMPTY_BYTE_ARRAY);
2287+
}
2288+
}
2289+
}
2290+
22242291
}

framework/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def configureTestTask = { Task t ->
125125
}
126126
t.maxHeapSize = "512m"
127127
t.maxParallelForks = Math.max(1, Math.min(4, Runtime.runtime.availableProcessors()))
128+
t.systemProperty 'runPrecompileBenchmark',
129+
System.getProperty('runPrecompileBenchmark', 'false')
128130
t.doFirst {
129131
t.forkEvery = 100
130132
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.apache.commons.lang3.tuple.Pair;
55
import org.junit.Assert;
66
import org.junit.Test;
7+
import org.tron.common.runtime.vm.DataWord;
8+
import org.tron.common.utils.ByteArray;
79
import org.tron.common.utils.ByteUtil;
810
import org.tron.core.vm.PrecompiledContracts;
911
import org.tron.core.vm.config.ConfigLoader;
@@ -74,4 +76,55 @@ public void testEIP7823DisabledShouldPass() {
7476
ConfigLoader.disable = false;
7577
}
7678
}
79+
80+
// P256VERIFY address per TIP-7951 / EIP-7951.
81+
private static final DataWord P256_VERIFY_ADDR =
82+
new DataWord(
83+
"0000000000000000000000000000000000000000000000000000000000000100");
84+
85+
// First entry from the geth conformance vectors — known-valid signature.
86+
private static final String VALID_P256_INPUT =
87+
"4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4d"
88+
+ "a73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac"
89+
+ "36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d60"
90+
+ "4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff3"
91+
+ "7618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
92+
private static final byte[] EXPECTED_VALID_OUTPUT = ByteUtil.merge(
93+
new byte[31], new byte[]{0x01});
94+
95+
@Test
96+
public void testP256VerifyEnabled() {
97+
ConfigLoader.disable = true;
98+
VMConfig.initAllowTvmOsaka(1);
99+
try {
100+
PrecompiledContracts.PrecompiledContract contract =
101+
PrecompiledContracts.getContractForAddress(P256_VERIFY_ADDR);
102+
Assert.assertNotNull("P256VERIFY must be registered when osaka is on",
103+
contract);
104+
Assert.assertTrue(contract instanceof PrecompiledContracts.P256Verify);
105+
106+
Pair<Boolean, byte[]> result = contract.execute(
107+
ByteArray.fromHexString(VALID_P256_INPUT));
108+
Assert.assertTrue(result.getLeft());
109+
Assert.assertArrayEquals(EXPECTED_VALID_OUTPUT, result.getRight());
110+
Assert.assertEquals(6900L, contract.getEnergyForData(
111+
ByteArray.fromHexString(VALID_P256_INPUT)));
112+
} finally {
113+
VMConfig.initAllowTvmOsaka(0);
114+
ConfigLoader.disable = false;
115+
}
116+
}
117+
118+
@Test
119+
public void testP256VerifyDisabledShouldReturnNull() {
120+
ConfigLoader.disable = true;
121+
VMConfig.initAllowTvmOsaka(0);
122+
try {
123+
Assert.assertNull(
124+
"P256VERIFY must NOT be registered when osaka is off",
125+
PrecompiledContracts.getContractForAddress(P256_VERIFY_ADDR));
126+
} finally {
127+
ConfigLoader.disable = false;
128+
}
129+
}
77130
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package org.tron.common.runtime.vm;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import java.io.InputStream;
5+
import java.util.List;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.apache.commons.lang3.tuple.Pair;
8+
import org.junit.Assert;
9+
import org.junit.Test;
10+
import org.tron.common.utils.ByteArray;
11+
import org.tron.core.vm.PrecompiledContracts;
12+
13+
@Slf4j
14+
public class P256VerifyTest {
15+
16+
private static final PrecompiledContracts.P256Verify CONTRACT =
17+
new PrecompiledContracts.P256Verify();
18+
19+
public static class TestCase {
20+
public String Input;
21+
public String Expected;
22+
public String Name;
23+
public int Gas;
24+
public boolean NoBenchmark;
25+
}
26+
27+
private static byte[] hex(String s) {
28+
return ByteArray.fromHexString(s);
29+
}
30+
31+
private static byte[] success() {
32+
byte[] r = new byte[32];
33+
r[31] = 0x01;
34+
return r;
35+
}
36+
37+
@Test
38+
public void gethConformanceVectors() throws Exception {
39+
ObjectMapper mapper = new ObjectMapper();
40+
List<TestCase> cases;
41+
try (InputStream is = P256VerifyTest.class.getResourceAsStream(
42+
"/precompiles/p256verify_test_vectors.json")) {
43+
Assert.assertNotNull("test vectors resource missing", is);
44+
cases = mapper.readerForListOf(TestCase.class).readValue(is);
45+
}
46+
Assert.assertFalse("vector list empty", cases.isEmpty());
47+
48+
for (TestCase tc : cases) {
49+
byte[] input = ByteArray.fromHexString(tc.Input);
50+
byte[] expected = tc.Expected == null || tc.Expected.isEmpty()
51+
? new byte[0]
52+
: ByteArray.fromHexString(tc.Expected);
53+
54+
Pair<Boolean, byte[]> result = CONTRACT.execute(input);
55+
56+
Assert.assertTrue(tc.Name + ": precompile must not revert", result.getLeft());
57+
Assert.assertArrayEquals(tc.Name + ": output mismatch",
58+
expected, result.getRight());
59+
Assert.assertEquals(tc.Name + ": gas mismatch",
60+
tc.Gas, CONTRACT.getEnergyForData(input));
61+
}
62+
}
63+
64+
@Test
65+
public void rejectsNullInput() {
66+
Pair<Boolean, byte[]> r = CONTRACT.execute(null);
67+
Assert.assertTrue(r.getLeft());
68+
Assert.assertArrayEquals(new byte[0], r.getRight());
69+
}
70+
71+
@Test
72+
public void rejectsEmptyInput() {
73+
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[0]);
74+
Assert.assertTrue(r.getLeft());
75+
Assert.assertArrayEquals(new byte[0], r.getRight());
76+
}
77+
78+
@Test
79+
public void rejectsShortInput() {
80+
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[159]);
81+
Assert.assertTrue(r.getLeft());
82+
Assert.assertArrayEquals(new byte[0], r.getRight());
83+
}
84+
85+
@Test
86+
public void rejectsLongInput() {
87+
Pair<Boolean, byte[]> r = CONTRACT.execute(new byte[161]);
88+
Assert.assertTrue(r.getLeft());
89+
Assert.assertArrayEquals(new byte[0], r.getRight());
90+
}
91+
92+
@Test
93+
public void rejectsInfinityPoint() {
94+
// Valid h, r, s plus qx=qy=0 -> infinity-encoded public key.
95+
String input =
96+
"4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4d"
97+
+ "a73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac"
98+
+ "36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d60"
99+
+ "0000000000000000000000000000000000000000000000000000000000000000"
100+
+ "0000000000000000000000000000000000000000000000000000000000000000";
101+
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
102+
Assert.assertTrue(r.getLeft());
103+
Assert.assertArrayEquals(new byte[0], r.getRight());
104+
}
105+
106+
/**
107+
* Public key coordinates are valid field elements but the point is NOT on
108+
* the secp256r1 curve (they happen to be the secp256k1 base point). The
109+
* precompile must fail the on-curve check before attempting verification.
110+
* Input lifted from Besu's P256VerifyPrecompiledContractTest.
111+
*/
112+
@Test
113+
public void rejectsOffCurvePoint() {
114+
String input =
115+
"44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56"
116+
+ "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
117+
+ "30dae23890abb63e378e003d7f1d5006ab23cc7b3b65b3d0c7b45c7e1e2e08b9"
118+
+ "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
119+
+ "b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777";
120+
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
121+
Assert.assertTrue(r.getLeft());
122+
Assert.assertArrayEquals(new byte[0], r.getRight());
123+
}
124+
125+
/**
126+
* The recovered point's x-coordinate exceeds n; verification must still
127+
* succeed because R'.x mod n == r. Input lifted from Besu's
128+
* testModularComparisonWhenRPrimeExceedsN.
129+
*/
130+
@Test
131+
public void acceptsModularComparisonWhenRPrimeExceedsN() {
132+
String input =
133+
"BB5A52F42F9C9261ED4361F59422A1E30036E7C32B270C8807A419FECA605023"
134+
+ "000000000000000000000000000000004319055358E8617B0C46353D039CDAAB"
135+
+ "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254E"
136+
+ "0AD99500288D466940031D72A9F5445A4D43784640855BF0A69874D2DE5FE103"
137+
+ "C5011E6EF2C42DCD50D5D3D29F99AE6EBA2C80C9244F4C5422F0979FF0C3BA5E";
138+
Pair<Boolean, byte[]> r = CONTRACT.execute(hex(input));
139+
Assert.assertTrue(r.getLeft());
140+
Assert.assertArrayEquals(success(), r.getRight());
141+
}
142+
143+
@Test
144+
public void gasCostIsConstant6900() {
145+
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(null));
146+
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[0]));
147+
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[160]));
148+
Assert.assertEquals(6900L, CONTRACT.getEnergyForData(new byte[1024]));
149+
}
150+
}

0 commit comments

Comments
 (0)