ZenGo-X / multi-party-ecdsa

Rust implementation of {t,n}-threshold ECDSA (elliptic curve digital signature algorithm).
GNU General Public License v3.0
977 stars 310 forks source link

Inconsistent Public Keys #173

Open artrepreneur opened 2 years ago

artrepreneur commented 2 years ago

When signing using gg18, if the same message is signed multiple times as in: it will result in a unique (r,s) pair each time.

Example, Round 1: ./gg18_sign_client http://127.0.0.1:8000 keys.store "68656c6c6f20776f726c64" ./gg18_sign_client http://127.0.0.1:8000 keys2.store "68656c6c6f20776f726c64"

R: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(9870cfd7bc3fa04b93976960579d316bb9d015d2abcbc1e204747d023f95f0c3)))) } s: Secp256k1Scalar { purpose: "add", fe: Zeroizing(Some(SK(SecretKey(46f5dfd387beb601cac41fce7079a6564740986a0ac186624de44e4d3860cf0f)))) } recid: 0

Round 2: ./gg18_sign_client http://127.0.0.1:8000 keys.store "68656c6c6f20776f726c64" ./gg18_sign_client http://127.0.0.1:8000 keys2.store "68656c6c6f20776f726c64"

R: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(82bfefea43e679127cf6932bcb06ecec281f1a7e5586727cc853346cacaaae0f)))) } s: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(33ab25e2d97be2af94e1046a5b29a11c3a46d7b7093b1cc03333a20c81006dee)))) } recid: 0

This results in different public keys as well. Also public keys do not match the public keys in keys.store. What explains this discrepancy?

artrepreneur commented 2 years ago

The issue seems to be that the message is not keccak256 or sha3 hashed before signing. Instead, it's hex'd and byte padded with leading zeros.

KunPengRen commented 2 years ago

Are you fix this problem?

KunPengRen commented 2 years ago

I try the public key recovery in python where I got the code from https://replit.com/@nakov/ECDSA-public-key-recovery-in-Python#main.py Message: hello sha3 of message: 0x3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392 round1: party 1 Output Signature: R: SecretKey(3c2bf68ef8bb5860c664f6d4cc7067b3f678853f2cdbc9dc793b45cdb5fc7529) s: SecretKey(49d104fe4d2d8bb2953e4e1434f4d675d4049c95592a669571f36dfb1682ece5)

Public key recovery: Recovered public key from signature: (0xe71cc44b7a738250fe80c40f1b64a95686a927d4400ca7d404a010c0e7366b40, 0xef39e52814806ebe53dafa767315f13c176e7c281350cc0800d64435e6671fc) Recovered public key from signature: (0xadef7738efde07bd3e2400513222c73fde564fb55ff4f637a7a05ba6e626921, 0xd27f1343640de071b479135d8004c6856bf626460dd4c7e6a2209a298d645854)

round2: party 1 Output Signature: R: SecretKey(e0eb7000356ab7d2b2b5ef3e0c8894e701f88f064e81b0c0b177abe5d205b716) s: SecretKey(57d1c4a16811439c98b00ec69daea6ee370ba3524a407d7095552edb09dd3dc7)

Public key recovery: Recovered public key from signature: (0xe71cc44b7a738250fe80c40f1b64a95686a927d4400ca7d404a010c0e7366b40, 0xef39e52814806ebe53dafa767315f13c176e7c281350cc0800d64435e6671fc) Recovered public key from signature: (0xcd6e8976589705f0f4cc22f553e64340dbc6b44ee581b41d463dd6c054a5a805, 0x9f08c42e2bf1768fb5925c788798c6b9e238d87e4b4d8793d46dc60bb481e26d)

The two results have a same public key (0xe71cc44b7a738250fe80c40f1b64a95686a927d4400ca7d404a010c0e7366b40, 0xef39e52814806ebe53dafa767315f13c176e7c281350cc0800d64435e6671fc), So I think it's consistent.

alexshchur commented 2 years ago

@artrepreneur , reading your logs I can make an assumption that in each of the rounds you're running the command from within the same/single directory and just use different keystores.

If you confirm this is the case, and if you still experience this problem -- let me know, and I would explain how to solve it :)

I ran into this issue by myself, though it got manifested in a different way in my case. This makes me think the documentation describing the demo might be improved. I'll be happy to create a PR if anyone's interested.

alexreyes commented 2 years ago

@alexshchur I'm running into the same issue now, would you be able to explain how to resolve this?

alexshchur commented 2 years ago

So, I think the intent and the way the codebase is designed assumes that, given the initial params config defining signers amount as 3, you would deploy this codebases onto 3 separate machines (or at least 3 separate folders).

After doing that, as explained in the doc, you probably want to run gg18 demo by launching sm_manager service and then consequently run ./gg18_keygen_client http://127.0.0.1:8001 keys.store in each of your "signer" folders.

You will end up with 3 similar folders, each having /keys.store, but with their own/distinct content (private keys + some protocol-specific data)

After that you would be able to launch commands like ./gg18_sign_client http://127.0.0.1:8001 keys.store {your_message_encoded_as_hex} by collecting the quorum of signers. The produced output should be consistent.

Let me know if that works

alexreyes commented 2 years ago

@alexshchur Thanks for response! I just tried that and ended up with the same problems as @artrepreneur.

In client1 folder I ran: ./gg18_sign_client http://127.0.0.1:8000 keys.store "68656c6c6f20776f726c64"

In client2 folder I ran: ./gg18_sign_client http://127.0.0.1:8000 keys.store "68656c6c6f20776f726c64"

The output for the above was:

R: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(c04f7900cbc14b656f84bb02d95a432b59744a7fa9ac5bef09e19b6a7ec4fd95)))) }
s: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(3b9801c860eaa4f2f0fc93a324ccc3c254a1aa6c007d1b27bdb994f445e1690f)))) } 

Afterwards, I ran the above two commands in the same folders again, and the output I received was:

R: Secp256k1Scalar { purpose: "from_bigint", fe: Zeroizing(Some(SK(SecretKey(6eca8f618e625263f7192944d08fea1c003855a50b433a17d75139be8a7f1cdc)))) }
s: Secp256k1Scalar { purpose: "add", fe: Zeroizing(Some(SK(SecretKey(7723a5de15ff670bcf827b7454a41937bbbb9ae47736b1bac319bba7d7fa4428)))) } 

Which is a different signature (with a different public key) than the first signature, despite signing the same message.

My goal is to sign a tx on ethereum that sends eth, but right now I'm stuck on even generating consistent eth addresses/signatures.

Were you able to get consistent public keys when signing messages? If so, how?

alexshchur commented 2 years ago

@alexreyes hm hm it sounds like you're doing everything right. It's weird that you don't receive consistent output.

Check out this my playground codebase. I've recorded a video demonstrating the whole process of creating signatures and then consistently recovering the same public key: https://github.com/alexshchur/tss-experiments#video

KunPengRen commented 2 years ago

@alexreyes @alexshchur From my testing, A message can be generated with two different signatures twice, but the public key will be recovered the same.

alexreyes commented 2 years ago

@alexshchur thanks for the video + sending your repo! That was super helpful and we were able to get it working. I really appreciate it!

@KunPengRen Yup, that seems to be the case. Thanks for confirming!

neocho commented 2 years ago

hey @alexshchur, thanks for the video. I tried out your script. I did the signing process and ended up with the signature.

But when I tried to use ethers.utils.verifyMessage() it outputted a different address than ethers.utils.recoverAddress().

I also tried to verify the message on Etherscan (https://etherscan.io/verifiedSignatures) from ethers.utils.recoverAddress() but it fails.

I generated the signature for the message on Etherscan using ethers.utils.joinSignature().

Do you have any thoughts on this? Maybe I'm messing up somewhere? Thanks!

Screen Shot 2022-07-09 at 2 39 45 AM
alexshchur commented 2 years ago

@neocho verifyMessage is a bit different. If you look under the hood of what it does with the supplied message arg, you'll find out that it prepends that ethereum prefix:

export const messagePrefix = "\x19Ethereum Signed Message:\n";

Essentially you end up dealing with a totally different message to recover against

neocho commented 2 years ago

Ahh I see! Will check that out 💯

neocho commented 2 years ago

Hey @alexshchur, have you been able to verify a signature with the recovered address with a tool like this https://etherscan.io/verifiedSignatures? I've been trying to get it to work with recoveredAddress but no luck still. Thanks :)

alexshchur commented 2 years ago

Hey @alexshchur, have you been able to verify a signature with the recovered address with a tool like this https://etherscan.io/verifiedSignatures? I've been trying to get it to work with recoveredAddress but no luck still. Thanks :)

Hey @neocho , I suppose it's already solved for you? Saw the conversations in TG channel ;)

Will repost Steph's response from there for others who might be curious

const { ethers } = require("ethers");

var R = "50a0feece3643fc91bd9eadd760da2c4f1da20cfae78cda5f296c393d67ce78e"
var s = "0c566fa493e38bd18d77904165bd8cb8a7f30687a286d2ff3f20b8e3f5304ddf"
var r = "1c" // 0: 1b, 1: 1c

var raw_msg = "68656c6c6f20776f726c64"

var msg = ethers.utils.hashMessage(raw_msg)

console.log("hashed msg", msg)

var signature = "0x" + R + s + r;

var pk = "0x02e65f1f00fd4207a3ea57f450dc1447de38280620c08f78872a39c042ec77a41a"

let reAddr = ethers.utils.computeAddress(pk)
console.log(reAddr)

let rPk = ethers.utils.recoverPublicKey(
  msg,
  signature
);
console.log(pk, ethers.utils.computePublicKey(rPk, true))

let signingAddress = ethers.utils.verifyMessage(raw_msg, signature);     
console.log(signingAddress);
neocho commented 2 years ago

Hey! @alexshchur yea unfortunately I kept getting different addresses after signing. 🤣🤣 I think I'll do a fresh clone and go from there.

artrepreneur commented 2 years ago

Problem is solved for me too. Sigs are consistent. If using Web3.js use something like:

let myMsgHashAndPrefix = web3.eth.accounts.hashMessage(); let netSigningMsg = myMsgHashAndPrefix.substr(2); let keyFileName = 'keys.store'; ./gg18_sign_client http://127.0.0.1:8000 '+ keyFileName +' ' + netSigningMsg;

Then combine R, s and recid to create a concatenated signature as say "sig", and recover the address with ethers.js like:

sigAddress = ethers.utils.recoverAddress(myMsgHashAndPrefix, sig);

lazarus521 commented 2 years ago

@alexreyes @neocho @alexshchur can you explain what you did to get this working? I'm running into this same issue (same message, different signatures, different public keys) for ETH. If it makes a difference, I'm signing an EIP-1559 transaction type.

For greater context, I using a 2-2 scheme that's based on the gotham-city repo (which uses multi-party-ecdsa). Each node is on its own process and storing its own data. I was able to get Bitcoin signing working no problem.

The message I'm signing is a curv::BigInt that represents the keccak256 hash of the RLP-encoded transaction. Steps to produce this:

Any help would be appreciated.

luluzhou1 commented 9 months ago

The problem of the example given in the readme is that: "68656c6c6f20776f726c64" is not a hash value with size = 32 byte. However, the signing protocol assumed that the signer input a hash value with size = 32. If the input size is < 32 byte, it will be padded. That's why the recovery of public key does not work properly when using ethers.util. The proper way is to sign with the command: ./gg18_sign_client http://127.0.0.1:8000/ keys.store1 "50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750" and ./gg18_sign_client http://127.0.0.1:8000/ keys.store2 "50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750" , where "50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750" is the hash value of "hello".

venuswhispers commented 1 month ago

will check soon