herumi / bls

286 stars 129 forks source link

Need pointers to send pre-computed hashToPoint(G1) for Solidity pairings check(EIP-197) #110

Closed wanderingelephants closed 5 days ago

wanderingelephants commented 1 week ago

We are using DKG with bls-wasm. The secret shares, aggregation and verification works fine. Critical step for us is to get the aggregated signature verified in Ethereum Smart contract via pre-compile (EIP-197) We have initiated bls with curveType 4. " import bls from 'bls-wasm' await bls.init(4) "

However, we need the pre-computed hashToPoint (G1) to pass to the pairing function.

Another library, mcl-wasm outputs that point along with signature, but bls-wasm does not. So, we thought if we could set the same DST and MapToMode for bls-wasm/mcl-wasm, then signature generated would be identical from both libraries (for the same private key and message).

Tried bls.setDstG1 with values like “BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_ROPOP” , “QUUX-V01-CS02-with-expander”. But the signature does not even change in bls-wasm, which means the setters have no effect.

Then , tried setting the domain and map mode in mcl-wasm also, to values like BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_ROPOP , QUUX-V01-CS02-with-expander and 0/1, hoping it would get aligned with defaults under bls-wasm. But no luck.

How do we get hashToPoint (G1) that corresponds to the signature created in bls-wasm. Since DKG is working well, we would like to use bls-wasm for share, sign, aggregate, verify. Computing Point-on-Curve in Solidity will be expensive and we would like to send pre-computed value. Moreover, without the correct DST, MaptoMode and other params that participated in the signature, we will not get the correct hashToPoint (G1).

Any pointers would be helpful. Thank you.

wanderingelephants commented 1 week ago

and generator for pubKey has been set to be conformant with EIP-197 (bn_snark1)

const P2 = new bls.PublicKey() //https://eips.ethereum.org/EIPS/eip-197 P2.setStr('1 10857046999023057135944570762232829481370756359578518086990519993285655852781 11559732032986387107991004021392285783925812861821192530917403151452391805634 8495653923123431417604973247489272438418190587263600148770280649306958101930 4082367875863433681332203403145435568316851327593401208105741076214120093531') bls.setGeneratorOfPublicKey(P2)

mratsim commented 1 week ago

Does this help? https://github.com/thehubbleproject/hubble-bls/blob/master/src/hashToField.ts And there are MCL functions: https://github.com/thehubbleproject/hubble-bls/blob/master/src/mcl.ts

wanderingelephants commented 1 week ago

thank you. these are good pointers to verify "ourselves", which we will do. however, because we are developing a trustless ethereum based MPC/DKG app, the verification of the aggregated signature needs to be done in the smart contract. we need 4 inputs for the pairings check. we have 3 as follows: 1. we have the Signature (on G1), 2. we have G2 and therefore -G2, 3. we have PublicKey (on G2). What we do not get from bls-wasm as return from sign() is Hash-to-Point (along with signature). Now, we tried constructing Hash-to-Point ourselves (like the links u posted), and we do get "something", but because we do not know which DOMAIN was used by bls-wasm, the reconstructed Hash-to-Point is different and therefore the pairing check fails.

herumi commented 1 week ago

The Hash-to-Point for alt_bn128(MCL_ZKSNARK1) is not defined at https://datatracker.ietf.org/doc/rfc9380/, or https://eips.ethereum.org/EIPS/eip-197, so mcl does not offer a standard method of MCL_ZKSNARK1. If there is a specification of it, I can implement it but it will take a long time.

wanderingelephants commented 1 week ago

Thank you. Full implementation is not needed. All we need is the "intermediate" G1 point that gets multiplied by secret to get the signature on G1. I have created a working test case to demonstrate our thought process. With fixed Private key listed here, it may be easy to reconcile numbers and data.

== Node.js Test Case ==

import bls from 'bls-wasm'

const messageString = 'hello world';

it('should create hash to G1 for private key and verify on Solidity precompile EIP-197', async function () { const secretHex = 'a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113'; const fr = bls.hashToFr(messageString); //const fr = bls.hashToFr(Uint8Array.from([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100])); console.log('fr', fr.serializeToHexStr(), fr.getStr()) const hashToG1 = mcl.mul(mcl.g1(), fr.serializeToHexStr()); //simple multiplication with G1(1, 2) console.log('hashToG1', hashToG1.getStr()); const signature = mcl.mul(hashToG1, secretHex); //this will deserialize secret hex to Fr and then multiply console.log("signature via simple hash-to-g1", signature.serializeToHexStr(), signature.getStr()); const signatureNegated = mcl.neg(signature); console.log("signatureNegated", signatureNegated.getStr())

const bls_sk = bls.deserializeHexStrToSecretKey(secretHex);
const pubkey = bls_sk.getPublicKey();
console.log('pubkey', pubkey.serializeToHexStr(), pubkey.getStr())

//Send to solidity smart contract for pairing check (EIP-197). See Solidity block below
//( signatureNegated, G2, hashToG1, pubkey) -- ABOVE VALUES FEEDED to EIP-197 precompile. TESTED WITH CONTRACT. WORKS. 

//repeat for bls-wasm now. 
//Expectation is that since Map-to-Mode is 0, and no message expansion implementation exists, bls-wasm internally must
//also hash the message, multiply by G1(1,2) and then multiply with secret to yield signature, on G1.
await bls.setMapToMode(0); //default is 0, but to be safe ensure it does "old way" of hash to curve

const signatureFromBlsWasm = bls_sk.sign(messageString);
console.log("signatureFromBlsWasm", signatureFromBlsWasm.serializeToHexStr(), signatureFromBlsWasm.getStr())

//We can see 'signatureFromBlsWasm' is different from 'signature via simple hash-to-g1'. (WHY? this can also help us in constructing ourselves)
//Since the signatureFromBlsWasm is ultimately a point on G1, all we need is the "intermediate" point that must be getting 
//multiplied by the secret. Ideally, the library could have returned that point along with signature. e.g. 
//const {signatureFromBlsWasm, hashToCurve} = bls_sk.sign(messageString);

//If outputting `hashToCurve` is not feasible, we just need to know the function/computation so we can do it ourselves, and then send the 
//pairs to solidity pre-compile. It's probably a simple function like hashToG1, but with some variant.

})

//console outputs fr b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcd29 18908686068733863958391780977886442715022598332542852793740553798165073448377 hashToG1 1 3567210644212152763126756499674583268675890469515266663800989995803735329916 8935460740221822196530920031616428628427028042127446405010448917533200610220 signature via simple hash-to-g1 0a82bc115f401a9e4933d401fca2b2f49905409db112a7d5636d0038b4d08f0c 1 5681853735190702260351096642137202416386519356623394956802127542833072865802 12249242727182326567035475477598231492979833022303806857340298885359218354906 signatureNegated 1 5681853735190702260351096642137202416386519356623394956802127542833072865802 9639000144656948655210930267659043595716478134994016805348739009286007853677 pubkey b45b2790b8058b9e12f2ddb09ee03184579565693cc88a86d4ce792b98b77f24f4ebb4e9a6312b2c0d4e820b0bb00c14cf3a5d85d758a5fb608c92be45c6d316 1 16508919248296338289255671794740997203086076897712730882851584570911156034484 10325055825582706116245656751504357396908257293324144243324702227081177918452 3417989850793265196214761327690584133012626059891743866288991798434956706084 418408001777090720908981848772804065048814525758927669011133324657464373326 signareFromBlsWasm b38667aeaf063f4b6dfdea1598191036debca9f13c35bb64898be945f3d0d68b 1 5354988728148300065926067668033756562694638869791343033360785978739353159347 4853766647528160628004504159645844729893699811753983991900039714799579242831

Solidity Smart Contract Function that Works for above outputs, and computes the pairings correctly

function MCL_BLS_TEST() public returns (bool) { G1Point memory negatedSignature = G1Point( 5681853735190702260351096642137202416386519356623394956802127542833072865802, 9639000144656948655210930267659043595716478134994016805348739009286007853677);

    G2Point memory g2Starter = G2Point(
        [11559732032986387107991004021392285783925812861821192530917403151452391805634,
        10857046999023057135944570762232829481370756359578518086990519993285655852781],

        [4082367875863433681332203403145435568316851327593401208105741076214120093531,
        8495653923123431417604973247489272438418190587263600148770280649306958101930]);

    G1Point memory hashToG1 = G1Point(
        3567210644212152763126756499674583268675890469515266663800989995803735329916, 
        8935460740221822196530920031616428628427028042127446405010448917533200610220);

    G2Point memory pubKey = G2Point(
        [10325055825582706116245656751504357396908257293324144243324702227081177918452, 
        16508919248296338289255671794740997203086076897712730882851584570911156034484],

        [418408001777090720908981848772804065048814525758927669011133324657464373326, 
        3417989850793265196214761327690584133012626059891743866288991798434956706084]);

    result = pairing2(negatedSignature, g2Starter, hashToG1, pubKey);
    return result;
}

//mcl is our thin wrapper around mcl-wasm import mcl from 'mcl-wasm' export function mul(g, scalarHex){ return mcl.mul(g, mcl.deserializeHexStrToFr(scalarHex)) } export function neg(g){ return mcl.neg(g) }

wanderingelephants commented 1 week ago

and bls is initialized with 4 (BN_SNARK1) and generator set correctly as follows

await bls.init(4) const P2 = new bls.PublicKey() P2.setStr('1 10857046999023057135944570762232829481370756359578518086990519993285655852781 11559732032986387107991004021392285783925812861821192530917403151452391805634 8495653923123431417604973247489272438418190587263600148770280649306958101930 4082367875863433681332203403145435568316851327593401208105741076214120093531') bls.setGeneratorOfPublicKey(P2)

herumi commented 1 week ago

const fr = bls.hashToFr(messageString);

Though I haven't looked at it in detail yet, how about using hashToField defined in hashToField.ts quoted by @mratsim in stead of bls.hashToFr?

wanderingelephants commented 1 week ago

Thank you for your inputs. Documenting a working unit TC here for future users.

=====

import bls from 'bls-wasm' import mcl from 'mcl-wasm' import {expect} from 'expect';

it.only('should match the signature between bls-wasm and mcl-wasm', async function () {

await bls.init(4);
bls.setMapToMode(0);
await mcl.init(mcl.BN_SNARK1);
mcl.setMapToMode(0)

const messageStr = 'hello world'
const secretHex = 'a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113'

const secret = bls.deserializeHexStrToSecretKey(secretHex)
const sign_bls_wasm = secret.sign(messageStr)

const hashToG1 = await mcl.hashAndMapToG1(messageStr)
const Fr = new mcl.Fr()
Fr.deserialize(Uint8Array.from(secret.serialize()))
const sign_mcl_wasm = mcl.mul(hashToG1, Fr)
expect(sign_bls_wasm.serializeToHexStr()).toEqual(sign_mcl_wasm.serializeToHexStr())
console.log("sign_bls_wasm", sign_bls_wasm.serializeToHexStr())

})

//sign_bls_wasm b38667aeaf063f4b6dfdea1598191036debca9f13c35bb64898be945f3d0d68b

herumi commented 5 days ago

I ran the following two codes and the results were the same.

const mcl = require('./')

mcl.init(mcl.BN_SNARK1).then(()=>{
  const msg = 'hello world'
  const secHex = 'a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113'
  const sec = mcl.deserializeHexStrToFr(secHex)
  console.log(`sec=${sec.serializeToHexStr()}`)
  const h = mcl.hashAndMapToG1(msg)
  const g1 = mcl.mul(h, sec)
  console.log(`sig=${g1.serializeToHexStr()}`)
})
mcl-wasm % node t.js
sec=a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113
sig=b38667aeaf063f4b6dfdea1598191036debca9f13c35bb64898be945f3d0d68b
herumi commented 5 days ago
const bls = require('./')

bls.init(4).then(()=>{
  const msg = 'hello world'
  const secHex = 'a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113'
  const sec = bls.deserializeHexStrToSecretKey(secHex)
  console.log(`sec=${sec.serializeToHexStr()}`)
  const sig = sec.sign(msg)
  console.log(`sig=${sig.serializeToHexStr()}`)
})
bls-wasm% node t.js
sec=a3e9769b84c095eca6b98449ac86b6e2c589834fe24cb8fbb7b36f814fd06113
sig=b38667aeaf063f4b6dfdea1598191036debca9f13c35bb64898be945f3d0d68b

setMapToMode does not affect a mapTo function on BN_SNARK1.

wanderingelephants commented 5 days ago

correct. notice that bls-wasm does not return any equivalent of "h" (const h = mcl.hashAndMapToG1(msg)) as does the mcl block above. what this means is that while bls-wasm can be used standalone to verify signatures, BUT if those signatures have to be submitted to a smart contract, then we need to run code of mcl to get "h". for bls-wasm to be "interoperable" with say eth smart contracts, then it would have helped if sec.sign returned sig and h both. i.e. {sig, h} = sec.sign(msg)

herumi commented 5 days ago

Then, I think that it is better to use mcl-wasm and make a sign and verify function like bls-sig.ts.

wanderingelephants commented 4 days ago

yes. we are now using mcl-wasm, as that is self-contained, and interoperable with ETH smart contract. thank you.

herumi commented 4 days ago

Alternatively, set sec = 1 and get h = sec.sign(msg) using bls-wasm.

wanderingelephants commented 3 days ago

Good idea. works too.

const secretOne = new bls.SecretKey()
secretOne.setInt(1)
const sign_bls_wasm_one = secretOne.sign(messageStr)
console.log('sign_bls_wasm_one', sign_bls_wasm_one.getStr())
console.log('hashToG1', hashToG1.getStr())
expect(sign_bls_wasm_one.getStr()).toEqual(hashToG1.getStr())