Skip to content

Commit

Permalink
fix: clear up signature Fp2 component order
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 27, 2025
1 parent a89c216 commit 8e0c82c
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 61 deletions.
25 changes: 15 additions & 10 deletions contracts/0.8.25/lib/BLS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ library BLS {
Fp2 y;
}

bytes constant DST = bytes("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_");

bytes1 constant BLS_BYTE_WITHOUT_FLAGS_MASK = bytes1(0x1f);

/// @notice PRECOMPILED CONTRACT ADDRESSES
Expand Down Expand Up @@ -94,12 +96,14 @@ library BLS {
encodedX[0] = encodedX[0] & BLS_BYTE_WITHOUT_FLAGS_MASK;
// NOTE: the "flag bits" of the second half of `encodedX` are always == 0x0

// NOTE: order is important here for decoding point...
uint256 aa = sliceToUint(encodedX, 48, 64);
uint256 ab = sliceToUint(encodedX, 64, 96);
uint256 ba = sliceToUint(encodedX, 0, 16);
uint256 bb = sliceToUint(encodedX, 16, 48);
Fp2 memory X = Fp2(Fp(aa, ab), Fp(ba, bb));
// Signature encoding has first Fp compnenet in the second half of the byte array
// With Fp components packed as 48 bytes
uint256 c0_a = sliceToUint(encodedX, 48, 64); // first Fp.a is 16 bytes
uint256 c0_b = sliceToUint(encodedX, 64, 96); // second Fp.b uint256 is 32 bytes
// and the second Fp component in the first half
uint256 c1_a = sliceToUint(encodedX, 0, 16);
uint256 c2_b = sliceToUint(encodedX, 16, 48);
Fp2 memory X = Fp2(Fp(c0_a, c0_b), Fp(c1_a, c2_b));
return G2Point(X, Y);
}

Expand All @@ -109,7 +113,7 @@ library BLS {
return abi.decode(output, (G2Point));
}

function hashToFieldFp2(bytes32 message, bytes memory dst) private view returns (Fp2[2] memory) {
function hashToFieldFp2(bytes32 message, bytes memory dst) internal view returns (Fp2[2] memory) {
// 1. len_in_bytes = count * m * L
// so always 2 * 2 * 64 = 256
uint16 lenInBytes = 256;
Expand Down Expand Up @@ -167,7 +171,7 @@ library BLS {

function hashToCurveG2(bytes32 message) internal view returns (G2Point memory) {
// 1. u = hash_to_field(msg, 2)
Fp2[2] memory u = hashToFieldFp2(message, bytes("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"));
Fp2[2] memory u = hashToFieldFp2(message, DST);
// 2. Q0 = map_to_curve(u[0])
G2Point memory q0 = mapFp2ToG2(u[0]);
// 3. Q1 = map_to_curve(u[1])
Expand All @@ -191,7 +195,7 @@ library BLS {
bytes32 message,
bytes memory dst,
uint16 lenInBytes
) private pure returns (bytes32[] memory) {
) internal pure returns (bytes32[] memory) {
// 1. ell = ceil(len_in_bytes / b_in_bytes)
// b_in_bytes seems to be 32 for sha256
// ceil the division
Expand Down Expand Up @@ -244,6 +248,7 @@ library BLS {
}

(bool success, bytes memory output) = BLS12_PAIRING_CHECK.staticcall(input);

if (!success) {
revert InvalidSignature();
}
Expand All @@ -252,7 +257,7 @@ library BLS {

function verifyDeposit(
bytes calldata pubkey, // must be 48 bytes
bytes32 withdrawal, // 32 bytes
bytes32 withdrawal,
uint256 amount,
bytes memory signature, // must be 96 bytes
Fp memory pubkeyYComponent,
Expand Down
42 changes: 25 additions & 17 deletions test/0.8.25/vaults/BLS.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from "chai";

import { getPublicKey, PointG1, PointG2, sign, verify } from "@noble/bls12-381";
import { SecretKey, verify } from "@chainsafe/blst";

import { ether } from "lib";

Expand All @@ -16,34 +16,42 @@ const STATIC_DEPOSIT = {
describe("BLS.sol", () => {
it("can create a deposit from test key", async () => {
// deposit message
const pubkey = Buffer.from(getPublicKey(BLS_TEST_KEY)).toString("hex");
const privateKey = SecretKey.fromHex(BLS_TEST_KEY);
const pubkey = privateKey.toPublicKey();
const pubkeyCompressed = pubkey.toHex(true);

const withdrawalCredentials = STATIC_DEPOSIT.withdrawalCredentials;
const amount = STATIC_DEPOSIT.amount;

// deposit message + domain
const messageHex = Buffer.from(await computeDepositMessageRoot(pubkey, withdrawalCredentials, amount)).toString(
"hex",
);
const message = await computeDepositMessageRoot(pubkey.toHex(true), withdrawalCredentials, amount);
const messageHex = Buffer.from(message).toString("hex");

const sig = await sign(messageHex, BLS_TEST_KEY);
const signature = Buffer.from(sig).toString("hex");
const sigG2 = PointG2.fromSignature(sig);
const signature = privateKey.sign(message);
const signatureCompressed = signature.toHex(true);

const result = await verify(sig, messageHex, pubkey);
const result = verify(message, pubkey, signature);
expect(result).to.be.true;

const pubkeyG1 = PointG1.fromHex(pubkey);
// Y coordinate of Fp component of pubkey is last 48 bytes of uncompressed pubkey(g1 point)
const pubkeyY = Buffer.from(pubkey.toBytes(false).slice(48)).toString("hex");
// the signature is a G2 point, so we need to extract the two components of Y coordinate (which is Fp2) from it
// first Fp of Y coordinate is last 48 bytes of signature
const sigY_c0 = Buffer.from(signature.toBytes(false).slice(96 + 48, 96 + 48 * 2)).toString("hex");
// second Fp is 48 bytes before first one
const sigY_c1 = Buffer.from(signature.toBytes(false).slice(96, 96 + 48)).toString("hex");

console.log({
pubkey,
pubkey: pubkeyCompressed,
withdrawalCredentials: withdrawalCredentials,
amount: amount.toString(),
signature,
pubkeyX: pubkeyG1.x.value.toString(16),
pubkeyY: pubkeyG1.y.value.toString(16),
signatureX: { c0: sigG2.x.c0.value.toString(16), c1: sigG2.x.c1.value.toString(16) },
signatureY: { c0: sigG2.y.c0.value.toString(16), c1: sigG2.y.c1.value.toString(16) },

signature: signatureCompressed,
signatureFull: Buffer.from(signature.toBytes(false)).toString("hex"),
pubkeyY,
sigY: {
c0: sigY_c0,
c1: sigY_c1,
},
messageHex,
});
});
Expand Down
58 changes: 24 additions & 34 deletions test/0.8.25/vaults/contracts/BLS.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,29 @@ contract BLSHarness {
) public view returns (bytes32) {
return SSZ.depositMessageSigningRoot(pubkey, withdrawal, amount);
}

function hashToFieldFp2(
bytes calldata pubkey,
bytes32 withdrawal,
uint64 amount
) public view returns (BLS.Fp2[2] memory) {
bytes32 _msg = SSZ.depositMessageSigningRoot(pubkey, withdrawal, amount);
return BLS.hashToFieldFp2(_msg, BLS.DST);
}

function hashToCurveG2(
bytes calldata pubkey,
bytes32 withdrawal,
uint64 amount
) public view returns (BLS.G2Point memory) {
return BLS.hashToCurveG2(SSZ.depositMessageSigningRoot(pubkey, withdrawal, amount));
}
}

struct PrecomputedDepositMessage {
bytes pubkey;
bytes32 withdrawal;
uint64 amount;
uint256 amount;
bytes signature;
BLS.Fp pubkeyYComponent;
BLS.Fp2 signatureYComponent;
Expand Down Expand Up @@ -72,38 +89,15 @@ contract BLSVerifyingKeyTest is Test {
StdAssertions.assertEq32(root, deposit.validMsgHash);
}

function test_decodeG2Point() public view {
PrecomputedDepositMessage memory deposit = STATIC_DEPOSIT_MESSAGE();
BLS.G2Point memory g2 = harness.decodeG2Point(deposit.signature, deposit.signatureYComponent);

BLS.Fp memory computed_x_c0 = wrapFp(
hex"12273c21949d56e83c491af148c760f9b38b233eb782c293a60fa2ffe0ee109db23aa9d8c69305c758bb229a1132a365"
);

StdAssertions.assertEq(abi.encode(g2.x.c0), abi.encode(computed_x_c0));

BLS.Fp memory computed_x_c1 = wrapFp(
hex"15a4323c090fb311b0f4f082b1b1004c5efb7a3b283415aac499be6045a8d3840ab57e714f2144fabe761e1fde46d1d5"
);

StdAssertions.assertEq(abi.encode(g2.x.c1), abi.encode(computed_x_c1));

// SignatureY is put into G2 as is
// SignatureY c0
StdAssertions.assertEq(abi.encode(g2.y.c0), abi.encode(deposit.signatureYComponent.c0));
// SignatureY c1
StdAssertions.assertEq(abi.encode(g2.y.c1), abi.encode(deposit.signatureYComponent.c1));
}

function wrapFp(bytes memory data) internal pure returns (BLS.Fp memory) {
require(data.length == 48, "Invalid Fp length");
uint256 a = BLS.sliceToUint(data, 0, 16);
uint256 b = BLS.sliceToUint(data, 16, 48);
return BLS.Fp(a, b);
}

function pad16(bytes memory data) internal pure returns (bytes memory) {
return bytes.concat(hex"00000000000000000000000000000000", data);
function wrapFp2(bytes memory x, bytes memory y) internal pure returns (BLS.Fp2 memory) {
return BLS.Fp2(wrapFp(x), wrapFp(y));
}

function STATIC_DEPOSIT_MESSAGE() internal pure returns (PrecomputedDepositMessage memory) {
Expand All @@ -112,17 +106,13 @@ contract BLSVerifyingKeyTest is Test {
hex"b79902f435d268d6d37ac3ab01f4536a86c192fa07ba5b63b5f8e4d0e05755cfeab9d35fbedb9c02919fe02a81f8b06d",
0xf3d93f9fbc6a229f3b11340b4b52ae53833813efab76e812d1d014163259ef1f,
1 ether,
hex"95a4323c090fb311b0f4f082b1b1004c5efb7a3b283415aac499be6045a8d3840ab57e714f2144fabe761e1fde46d1d512273c21949d56e83c491af148c760f9b38b233eb782c293a60fa2ffe0ee109db23aa9d8c69305c758bb229a1132a365",
hex"b357f146f53de27ae47d6d4bff5e8cc8342d94996143b2510452a3565701c3087a0ba04bed41d208eb7d2f6a50debeac09bf3fcf5c28d537d0fe4a52bb976d0c19ea37a31b6218f321a308f8017e5fd4de63df270f37df58c059c75f0f98f980",
wrapFp(
hex"19b71bd2a9ebf09809b6c380a1d1de0c2d9286a8d368a2fc75ad5ccc8aec572efdff29d50b68c63e00f6ce017c24e083"
),
BLS.Fp2(
wrapFp(
hex"0d12d727285533d0ec733bcd80cd1e15d002c19985aaa3ad40fca85f73e1a7f96f50d0c78add58b7d55a4ebd11051f37"
),
wrapFp(
hex"0b8cfab59498fbe811a1a5009beb975a5c144acfbd9423a279d75a2cce89312366d1ca701697d05c15689820eb4cd96d"
)
wrapFp2(
hex"10d96c5dcc6e32bcd43e472317e18ad94dde89c9361d79bec5378c72214083ea40f3dc43ee759025eb4c25150e1943bf",
hex"160f8d804d277c7a079f451bce224fd42397e75676d965a1ebe79e53beeb2cb48be01f4dc93c0bad8ae7560c3e8048fb"
),
0xa0ea5aa96388d0375c9181eac29fa198cea873c818efe7442bd49c03948f2a69
);
Expand Down

0 comments on commit 8e0c82c

Please sign in to comment.