Skip to content

Commit ad1fb77

Browse files
committed
port pedersen commitment
1 parent c9d1a9a commit ad1fb77

File tree

6 files changed

+527
-0
lines changed

6 files changed

+527
-0
lines changed

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/mpt/mpt-crypto/tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ add_mpt_test(test_elgamal_decrypt_vectors test_elgamal_decrypt_vectors.c mpt-cry
5151
add_mpt_test(test_pok_sk_vectors test_pok_sk_vectors.c mpt-crypto secp256k1 OpenSSL::Crypto)
5252

5353
add_mpt_test(test_same_plaintext_multi_vectors test_same_plaintext_multi_vectors.c mpt-crypto secp256k1 OpenSSL::Crypto)
54+
55+
add_mpt_test(test_pedersen_vectors test_pedersen_vectors.c mpt-crypto secp256k1 OpenSSL::Crypto)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#include <stdio.h>
2+
#include <string.h>
3+
#include <stdlib.h>
4+
#include <secp256k1.h>
5+
#include "secp256k1_mpt.h"
6+
#include "test_utils.h"
7+
8+
/* Fixed blinding factors for deterministic test vectors */
9+
static const unsigned char FIXED_NONCES[10][32] = {
10+
{0x03, 0x67, 0xE5, 0xC8, 0x4F, 0x43, 0x71, 0x4E, 0x13, 0xE3, 0x4E, 0xD2, 0x12, 0xF9, 0x25, 0xB3,
11+
0x75, 0xCD, 0x86, 0x3C, 0xAE, 0x46, 0xEF, 0xE3, 0xD1, 0x05, 0x65, 0x92, 0xC2, 0x55, 0x9D, 0xDA},
12+
{0xA2, 0x68, 0xEE, 0x37, 0xE2, 0x3C, 0x65, 0x6D, 0x05, 0xF4, 0x82, 0x03, 0xFA, 0x23, 0x8D, 0x41,
13+
0x92, 0x4C, 0x64, 0x57, 0x6E, 0x7F, 0x13, 0xA6, 0xD7, 0x29, 0x4D, 0x22, 0xCE, 0x5E, 0x24, 0x49},
14+
{0x6B, 0x51, 0xA9, 0xD4, 0xD9, 0xEA, 0xD9, 0xBE, 0x6C, 0x64, 0x0D, 0x37, 0x42, 0xC3, 0x29, 0x63,
15+
0x0C, 0xFB, 0xC0, 0x1A, 0x2D, 0x6A, 0x6D, 0xF1, 0x2D, 0x32, 0xCF, 0x60, 0x7E, 0xAD, 0xAC, 0x18},
16+
{0x3F, 0x98, 0x46, 0x20, 0xB0, 0x14, 0xF5, 0xE5, 0x7F, 0xFE, 0x80, 0x92, 0x4C, 0x08, 0x95, 0x26,
17+
0xAD, 0xE9, 0x3B, 0x87, 0xE0, 0x80, 0xDD, 0x2C, 0x39, 0xC5, 0xB7, 0xEB, 0xD1, 0xE1, 0xDC, 0x9F},
18+
{0x41, 0xC3, 0x04, 0xBE, 0x98, 0xE5, 0xEB, 0x03, 0x08, 0xF4, 0xC6, 0x21, 0xFE, 0x52, 0xA3, 0xEF,
19+
0x0E, 0x1C, 0xAE, 0x4E, 0xFC, 0xEB, 0xAC, 0x7E, 0x0F, 0xB6, 0x62, 0x75, 0xEA, 0x08, 0x3E, 0x87},
20+
{0x99, 0x55, 0xB9, 0x26, 0xFA, 0xA3, 0xEA, 0x9A, 0x07, 0x74, 0x6C, 0xA5, 0xC0, 0x12, 0xF2, 0xDD,
21+
0x10, 0xD4, 0x4C, 0xF7, 0x32, 0x17, 0x74, 0x6E, 0x5B, 0x37, 0x16, 0x3B, 0xA9, 0xCC, 0xA6, 0x2D},
22+
{0xFF, 0x2F, 0xC7, 0x43, 0xE0, 0x21, 0xC6, 0xA5, 0x20, 0x6E, 0x3E, 0x3E, 0x5A, 0x5C, 0xFA, 0x0C,
23+
0xE5, 0x42, 0x48, 0x2F, 0xF0, 0x21, 0x3A, 0x83, 0xD3, 0x57, 0xD1, 0xF0, 0xC2, 0x0C, 0xE8, 0xB7},
24+
{0x72, 0x0B, 0xF9, 0xF6, 0x37, 0x76, 0x70, 0xE0, 0x00, 0x11, 0xC9, 0x01, 0x52, 0x6A, 0x40, 0x5C,
25+
0xC7, 0x07, 0xBC, 0x92, 0x18, 0xB0, 0xE4, 0x40, 0x2E, 0x0E, 0x18, 0x21, 0x0F, 0x95, 0x3E, 0x54},
26+
{0x6F, 0xEA, 0x36, 0xEA, 0xD5, 0x9A, 0x20, 0xD9, 0xBE, 0x21, 0x57, 0x30, 0xB8, 0xD2, 0x99, 0x5B,
27+
0x67, 0x11, 0x27, 0x21, 0xB6, 0x7E, 0x1E, 0x14, 0xE3, 0xAF, 0xA8, 0x09, 0xE8, 0x3B, 0x28, 0x58},
28+
{0x1C, 0x7D, 0x65, 0x5F, 0x23, 0x2B, 0x29, 0x4A, 0x87, 0xBB, 0xC6, 0x37, 0x53, 0xDA, 0x3B, 0xD6,
29+
0xE6, 0xB4, 0xB7, 0x16, 0xF8, 0xD5, 0x5A, 0xF4, 0x5D, 0x9F, 0xF3, 0xEE, 0x11, 0xD0, 0xC8, 0xAA}
30+
};
31+
32+
/* Test amounts - various values including 0, small, large */
33+
static const uint64_t TEST_AMOUNTS[10] = {
34+
0, /* Zero amount */
35+
1, /* Minimum non-zero */
36+
100, /* Small amount */
37+
555666, /* Medium amount */
38+
1000000, /* One million */
39+
0xFFFFFFFF, /* Max 32-bit */
40+
0x100000000ULL, /* 2^32 */
41+
0x7FFFFFFFFFFFFFFFULL, /* Max signed 64-bit */
42+
12345678901234567ULL, /* Large arbitrary */
43+
9999999999999ULL /* Another large value */
44+
};
45+
46+
static void print_hex_json(const unsigned char* data, size_t len) {
47+
for (size_t i = 0; i < len; i++) {
48+
printf("%02X", data[i]);
49+
}
50+
}
51+
52+
static void generate_test_vector_json(const secp256k1_context* ctx, int index, int is_last) {
53+
uint64_t amount = TEST_AMOUNTS[index];
54+
const unsigned char* rho = FIXED_NONCES[index];
55+
secp256k1_pubkey commitment;
56+
unsigned char commitment_ser[33];
57+
size_t len = 33;
58+
59+
/* Generate commitment */
60+
int result = secp256k1_mpt_pedersen_commit(ctx, &commitment, amount, rho);
61+
EXPECT(result == 1);
62+
63+
/* Serialize commitment */
64+
EXPECT(secp256k1_ec_pubkey_serialize(ctx, commitment_ser, &len, &commitment, SECP256K1_EC_COMPRESSED) == 1);
65+
66+
/* Output JSON */
67+
printf(" {\n");
68+
printf(" \"amount\": %llu,\n", (unsigned long long)amount);
69+
printf(" \"rho\": \""); print_hex_json(rho, 32); printf("\",\n");
70+
printf(" \"expectedCommitment\": \""); print_hex_json(commitment_ser, 33); printf("\"\n");
71+
printf(" }%s\n", is_last ? "" : ",");
72+
}
73+
74+
int main() {
75+
secp256k1_context* ctx = secp256k1_context_create(
76+
SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
77+
EXPECT(ctx != NULL);
78+
79+
/* Output JSON header */
80+
printf("{\n");
81+
printf(" \"description\": \"Pedersen commitment test vectors from C implementation\",\n");
82+
printf(" \"vectors\": [\n");
83+
84+
/* Generate 10 test vectors */
85+
for (int i = 0; i < 10; i++) {
86+
generate_test_vector_json(ctx, i, i == 9);
87+
}
88+
89+
/* Close JSON */
90+
printf(" ]\n");
91+
printf("}\n");
92+
93+
secp256k1_context_destroy(ctx);
94+
return 0;
95+
}
96+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.xrpl.xrpl4j.crypto.mpt.port;
2+
3+
/*-
4+
* ========================LICENSE_START=================================
5+
* xrpl4j :: core
6+
* %%
7+
* Copyright (C) 2020 - 2023 XRPL Foundation and its contributors
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =========================LICENSE_END==================================
21+
*/
22+
23+
import com.google.common.primitives.UnsignedLong;
24+
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
25+
26+
/**
27+
* Port of {@code secp256k1_mpt_pedersen_commit} from commitments.c.
28+
*
29+
* <p>Creates a Pedersen Commitment: C = amount * G + rho * H, where G is the standard
30+
* secp256k1 generator and H is a NUMS (Nothing-Up-My-Sleeve) generator derived using
31+
* hash-to-curve.</p>
32+
*
33+
* <p>The commitment is returned as a 33-byte compressed point.</p>
34+
*/
35+
public interface PedersenCommitmentPort {
36+
37+
/**
38+
* Generates a Pedersen Commitment for the given amount and blinding factor.
39+
*
40+
* <p>Handles the edge case where amount = 0, in which case C = rho * H.</p>
41+
*
42+
* @param amount The value to commit to (64-bit unsigned).
43+
* @param rho The blinding factor (32 bytes, must be a valid scalar).
44+
*
45+
* @return A 33-byte compressed point representing the commitment.
46+
*
47+
* @throws IllegalArgumentException if rho is not a valid scalar.
48+
* @throws IllegalStateException if commitment generation fails.
49+
*/
50+
UnsignedByteArray generateCommitment(UnsignedLong amount, UnsignedByteArray rho);
51+
}
52+
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package org.xrpl.xrpl4j.crypto.mpt.port.bc;
2+
3+
/*-
4+
* ========================LICENSE_START=================================
5+
* xrpl4j :: core
6+
* %%
7+
* Copyright (C) 2020 - 2023 XRPL Foundation and its contributors
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =========================LICENSE_END==================================
21+
*/
22+
23+
import com.google.common.primitives.UnsignedLong;
24+
import org.bouncycastle.math.ec.ECPoint;
25+
import org.xrpl.xrpl4j.codec.addresses.ByteUtils;
26+
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
27+
import org.xrpl.xrpl4j.crypto.HashingUtils;
28+
import org.xrpl.xrpl4j.crypto.mpt.Secp256k1Operations;
29+
import org.xrpl.xrpl4j.crypto.mpt.port.PedersenCommitmentPort;
30+
31+
import java.math.BigInteger;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.Arrays;
34+
35+
/**
36+
* BouncyCastle implementation of {@link PedersenCommitmentPort}.
37+
*
38+
* <p>Port of {@code secp256k1_mpt_pedersen_commit} from commitments.c.</p>
39+
*/
40+
public class BcPedersenCommitmentPort implements PedersenCommitmentPort {
41+
42+
private static final String DOMAIN_SEPARATOR = "MPT_BULLETPROOF_V1_NUMS";
43+
private static final String CURVE_LABEL = "secp256k1";
44+
45+
/**
46+
* Cached H generator point (lazily initialized).
47+
*/
48+
private ECPoint cachedH;
49+
50+
@Override
51+
public UnsignedByteArray generateCommitment(UnsignedLong amount, UnsignedByteArray rho) {
52+
// 0. Input Check - matches: if (!secp256k1_ec_seckey_verify(ctx, rho)) return 0;
53+
if (!Secp256k1Operations.isValidScalar(rho.toByteArray())) {
54+
throw new IllegalArgumentException("rho is not a valid scalar");
55+
}
56+
57+
byte[] mScalar = new byte[32];
58+
59+
try {
60+
// 1. Calculate rho*H (Blinding Term)
61+
// matches: if (!secp256k1_mpt_get_h_generator(ctx, &H)) return 0;
62+
ECPoint H = getHGenerator();
63+
64+
// matches: rH = H; if (!secp256k1_ec_pubkey_tweak_mul(ctx, &rH, rho)) return 0;
65+
BigInteger rhoInt = new BigInteger(1, rho.toByteArray());
66+
ECPoint rH = Secp256k1Operations.multiply(H, rhoInt);
67+
68+
if (rH.isInfinity()) {
69+
throw new IllegalStateException("rH is point at infinity");
70+
}
71+
72+
// 2. Handle Zero Amount Case
73+
// matches: if (amount == 0) { *commitment = rH; return 1; }
74+
if (amount.equals(UnsignedLong.ZERO)) {
75+
return UnsignedByteArray.of(Secp256k1Operations.serializeCompressed(rH));
76+
}
77+
78+
// 3. Calculate m*G (Value Term)
79+
// matches: for (int i = 0; i < 8; i++) { m_scalar[31 - i] = (amount >> (i * 8)) & 0xFF; }
80+
long amountValue = amount.longValue();
81+
for (int i = 0; i < 8; i++) {
82+
mScalar[31 - i] = (byte) ((amountValue >> (i * 8)) & 0xFF);
83+
}
84+
85+
// matches: if (!secp256k1_ec_pubkey_create(ctx, &mG, m_scalar)) goto cleanup;
86+
BigInteger mInt = new BigInteger(1, mScalar);
87+
ECPoint mG = Secp256k1Operations.multiplyG(mInt);
88+
89+
if (mG.isInfinity()) {
90+
throw new IllegalStateException("mG is point at infinity");
91+
}
92+
93+
// 4. Combine: C = mG + rH
94+
// matches: if (!secp256k1_ec_pubkey_combine(ctx, commitment, points, 2)) goto cleanup;
95+
ECPoint commitment = Secp256k1Operations.add(mG, rH);
96+
97+
if (commitment.isInfinity()) {
98+
throw new IllegalStateException("commitment is point at infinity");
99+
}
100+
101+
return UnsignedByteArray.of(Secp256k1Operations.serializeCompressed(commitment));
102+
103+
} finally {
104+
// matches: OPENSSL_cleanse(m_scalar, 32);
105+
Arrays.fill(mScalar, (byte) 0);
106+
}
107+
}
108+
109+
/**
110+
* Gets the H generator point for Pedersen commitments.
111+
*
112+
* <p>Port of {@code secp256k1_mpt_get_h_generator} which derives a NUMS point
113+
* using the label "H" at index 0.</p>
114+
*
115+
* @return The H generator point.
116+
*/
117+
private ECPoint getHGenerator() {
118+
if (cachedH == null) {
119+
cachedH = hashToPointNums("H".getBytes(StandardCharsets.UTF_8), 0);
120+
}
121+
return cachedH;
122+
}
123+
124+
/**
125+
* Deterministically derives a NUMS (Nothing-Up-My-Sleeve) generator point.
126+
*
127+
* <p>Port of {@code secp256k1_mpt_hash_to_point_nums} from commitments.c.</p>
128+
*
129+
* @param label The domain/vector label (e.g., "H").
130+
* @param index The vector index.
131+
*
132+
* @return The derived generator point.
133+
*/
134+
private ECPoint hashToPointNums(byte[] label, int index) {
135+
byte[] domainBytes = DOMAIN_SEPARATOR.getBytes(StandardCharsets.UTF_8);
136+
byte[] curveBytes = CURVE_LABEL.getBytes(StandardCharsets.UTF_8);
137+
byte[] indexBe = ByteUtils.toByteArray(index, 4);
138+
139+
// Try-and-increment loop
140+
for (long ctr = 0; ctr < 0xFFFFFFFFL; ctr++) {
141+
byte[] ctrBe = ByteUtils.toByteArray((int) ctr, 4);
142+
143+
// Build hash input: domainSeparator || curveLabel || label || index || counter
144+
int inputLen = domainBytes.length + curveBytes.length +
145+
(label != null ? label.length : 0) + 4 + 4;
146+
byte[] hashInput = new byte[inputLen];
147+
int offset = 0;
148+
149+
System.arraycopy(domainBytes, 0, hashInput, offset, domainBytes.length);
150+
offset += domainBytes.length;
151+
System.arraycopy(curveBytes, 0, hashInput, offset, curveBytes.length);
152+
offset += curveBytes.length;
153+
if (label != null && label.length > 0) {
154+
System.arraycopy(label, 0, hashInput, offset, label.length);
155+
offset += label.length;
156+
}
157+
System.arraycopy(indexBe, 0, hashInput, offset, 4);
158+
offset += 4;
159+
System.arraycopy(ctrBe, 0, hashInput, offset, 4);
160+
161+
byte[] hash = HashingUtils.sha256(hashInput).toByteArray();
162+
163+
// Construct compressed point candidate: 0x02 || hash
164+
byte[] compressed = new byte[33];
165+
compressed[0] = 0x02;
166+
System.arraycopy(hash, 0, compressed, 1, 32);
167+
168+
try {
169+
ECPoint point = Secp256k1Operations.deserialize(compressed);
170+
if (point != null && !point.isInfinity()) {
171+
return point;
172+
}
173+
} catch (Exception e) {
174+
// Invalid point, continue to next counter
175+
}
176+
}
177+
178+
throw new IllegalStateException("Failed to derive NUMS point (extremely unlikely)");
179+
}
180+
}
181+

0 commit comments

Comments
 (0)