Skip to content

Commit

Permalink
add BIP341 to threshold Schnorr docs (#3949)
Browse files Browse the repository at this point in the history
  • Loading branch information
altkdf authored Jan 21, 2025
1 parent f187fed commit 4120515
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The API provides two methods:

Signatures will have different encoding depending on the provided `key_id` algorithm:

- `bip340secp256k1`: The signature will be encoded according to [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), using 64 bytes.
- `bip340secp256k1`: The signature will be encoded according to [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), using 64 bytes. Optionally, the caller can provide a 0 or 32-byte taproot merkle tree root as a key tweak as defined in [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki).

- `ed25519`: The signature will be encoded according to [RFC8032, 5.1.6 Sign](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6), using 64 bytes.

Expand All @@ -42,59 +42,98 @@ To deploy code using the `sign_with_schnorr` method, you will need to specify th

- `dfx_test_key`: Only available on the local replica started by dfx.
- `test_key_1`: Test key available on the ICP mainnet.
- `key_1`: Production key available on the ICP mainnet.
- `key_1`: Production key available on the ICP mainnet.

Also, optionally you can provide a 0 or 32-byte taproot merkle tree root as a
key tweak for `bip340secp256k1` as defined in
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki).

<AdornedTabs groupId="language">
<TabItem value="motoko" label="Motoko" default>

```motoko no-repl
  public shared ({ caller }) func sign(message_arg : Text, algorithm_arg : SchnorrAlgotirhm) : async {
    #Ok : { signature_hex : Text };
    #Err : Text;
  } {
    try {
      Cycles.add(25_000_000_000);
      let { signature } = await ic.sign_with_schnorr({
        message = Text.encodeUtf8(message_arg);
        derivation_path = [Principal.toBlob(caller)];
        key_id = { algorithm = algorithm_arg; name = "dfx_test_key" };
      });
      #Ok({ signature_hex = Hex.encode(Blob.toArray(signature)) });
    } catch (err) {
      #Err(Error.message(err));
    };
  };
public shared ({ caller }) func sign(message_arg : Text, algorithm : SchnorrAlgorithm, bip341TweakHex : ?Text) : async {
#ok : { signature_hex : Text };
#err : Text;
} {
let aux = switch (Option.map(bip341TweakHex, tryHexToTweak)) {
case (null) null;
case (?#ok some) ?some;
case (?#err err) return #err err;
};
try {
Cycles.add<system>(25_000_000_000);
let signArgs = {
message = Text.encodeUtf8(message_arg);
derivation_path = [Principal.toBlob(caller)];
key_id = { algorithm; name = "insecure_test_key_1" };
aux;
};
let { signature } = await ic.sign_with_schnorr(signArgs);
#ok({ signature_hex = Hex.encode(Blob.toArray(signature)) });
} catch (err) {
#err(Error.message(err));
};
};
```

<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/motoko/threshold-schnorr/src/schnorr_example_motoko/main.mo#L60">
<div align="center">View in the full example.</div>
</a>
</TabItem>

<TabItem value="rust" label="Rust">

```rust
#[update]
async fn sign(message: String, algorithm: SchnorrAlgorithm) -> Result<SignatureReply, String> {
    let internal_request = ManagementCanisterSignatureRequest {
        message: message.as_bytes().to_vec(),
        derivation_path: vec![ic_cdk::api::caller().as_slice().to_vec()],
        key_id: SchnorrKeyIds::TestKeyLocalDevelopment.to_key_id(algorithm),
    };

    let (internal_reply,): (ManagementCanisterSignatureReply,) =
        ic_cdk::api::call::call_with_payment(
            mgmt_canister_id(),
            "sign_with_schnorr",
            (internal_request,),
            25_000_000_000,
        )
        .await
        .map_err(|e| format!("sign_with_schnorr failed {e:?}"))?;

    Ok(SignatureReply {
        signature_hex: hex::encode(&internal_reply.signature),
    })
async fn sign(
message: String,
algorithm: SchnorrAlgorithm,
opt_merkle_tree_root_hex: Option<String>,
) -> Result<SignatureReply, String> {
let aux = opt_merkle_tree_root_hex
.map(|hex| {
hex::decode(&hex)
.map_err(|e| format!("failed to decode hex: {e:?}"))
.and_then(|bytes| {
if bytes.len() == 32 || bytes.is_empty() {
Ok(SignWithSchnorrAux::Bip341(SignWithBip341Aux {
merkle_root_hash: ByteBuf::from(bytes),
}))
} else {
Err(format!(
"merkle tree root bytes must be 0 or 32 bytes long but got {}",
bytes.len()
))
}
})
})
.transpose()?;

let internal_request = ManagementCanisterSignatureRequest {
message: message.as_bytes().to_vec(),
derivation_path: vec![ic_cdk::api::caller().as_slice().to_vec()],
key_id: SchnorrKeyIds::ChainkeyTestingCanisterKey1.to_key_id(algorithm),
aux,
};

let (internal_reply,): (ManagementCanisterSignatureReply,) =
ic_cdk::api::call::call_with_payment(
mgmt_canister_id(),
"sign_with_schnorr",
(internal_request,),
26_153_846_153,
)
.await
.map_err(|e| format!("sign_with_schnorr failed {e:?}"))?;

Ok(SignatureReply {
signature_hex: hex::encode(&internal_reply.signature),
})
}
```

<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/rust/threshold-schnorr/src/schnorr_example_rust/src/wasm_only.rs#L94">
<div align="center">View in the full example.</div>
</a>
</TabItem>
</AdornedTabs>

Expand All @@ -120,63 +159,151 @@ The first byte of the BIP340 public key needs to be removed for verification. Be
<TabItem value="javascript" label="JavaScript" default>

```js
import('@noble/curves/secp256k1').then((bip340) => { verify(bip340.schnorr); })
  .catch((err) => { console.log(err) });
// BIP340 verification
async function run() {
try {
const ecc = await import('tiny-secp256k1');

const test_sig = Buffer.from('311e1dceddd1380d0424e01b19711e926ca2f26c0dda57b405bec1359510674871a22487c96afa4a4bf47858d1d79caa400bb51ab793d9fad2a689f8bfc681aa', 'hex');
const test_pubkey = Buffer.from('02472bb4da5c5ce627d599feba90d0257a558d4e226f9fc7914f811e301ad06f38'.substring(2), 'hex');
const test_msg = Uint8Array.from(Buffer.from("hellohellohellohellohellohello12", 'utf8'));

console.log(ecc.verifySchnorr(test_msg, test_pubkey, test_sig));
}
catch(err) {
console.log(err);
}
}

function verify(bip340) {
  const test_sig = '1b64ca7a7f02c76633954f320675267685b3b80560eb6a35cda20291ddefc709364e59585771c284e46264bfbb0620e23eb8fb274994f7a6f2fcbc8a9430e5d7';
  // the first byte of the BIP340 public key is truncated
  const pubkey = '0341d7cf39688e10b5f11f168ad0a9e790bcb429d7d486eab07d2c824b85821470'.substring(2)
  const test_msg = Uint8Array.from(Buffer.from("hello", 'utf8'));
run();

// BIP341 verification
async function run() {
try {
const bip341 = await import('bitcoinjs-lib/src/payments/bip341.js');
const bitcoin = await import('bitcoinjs-lib');
const ecc = await import('tiny-secp256k1');

bitcoin.initEccLib(ecc);

  console.log(bip340.verify(test_sig, test_msg, test_pubkey));
const test_tweak = Buffer.from('012345678901234567890123456789012345678901234567890123456789abcd', 'hex');
const test_sig = Buffer.from('3c3e51fc771a5a8cb553bf2dd151bb02d0f473ff274a92d32310267977918d72121f97c318226422c033d33daf376d42c9a07e71643ff332cb30611fe5e163da', 'hex');
const test_pubkey = Buffer.from('02472bb4da5c5ce627d599feba90d0257a558d4e226f9fc7914f811e301ad06f38'.substring(2), 'hex');
const test_msg = Uint8Array.from(Buffer.from("hellohellohellohellohellohello12", 'utf8'));

const tweaked_test_pubkey = bip341.tweakKey(test_pubkey, test_tweak).x;

console.log(ecc.verifySchnorr(test_msg, tweaked_test_pubkey, test_sig));
}
catch(err) {
console.log(err);
}
}
```

run();
```
<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/motoko/threshold-schnorr/test.sh#L56">
<div align="center">View in the full example.</div>
</a>
</TabItem>

<TabItem value="rust" label="Rust">

```rust
#[query]
async fn verify(
    signature_hex: String,
    message: String,
    public_key_hex: String,
    algorithm: SchnorrAlgorithm,
signature_hex: String,
message: String,
public_key_hex: String,
opt_merkle_tree_root_hex: Option<String>,
algorithm: SchnorrAlgorithm,
) -> Result<SignatureVerificationReply, String> {
    let sig_bytes = hex::decode(&signature_hex).expect("failed to hex-decode signature");
    let msg_bytes = message.as_bytes();
    let pk_bytes = hex::decode(&public_key_hex).expect("failed to hex-decode public key");

    match algorithm {
        SchnorrAlgorithm::Bip340Secp256k1 => {
            verify_bip340_secp256k1(&sig_bytes, msg_bytes, &pk_bytes)
        }
        SchnorrAlgorithm::Ed25519 => verify_ed25519(&sig_bytes, &msg_bytes, &pk_bytes),
    }
let sig_bytes = hex::decode(&signature_hex).expect("failed to hex-decode signature");
let msg_bytes = message.as_bytes();
let pk_bytes = hex::decode(&public_key_hex).expect("failed to hex-decode public key");

match algorithm {
SchnorrAlgorithm::Bip340Secp256k1 => match opt_merkle_tree_root_hex {
Some(merkle_tree_root_hex) => {
let merkle_tree_root_bytes = hex::decode(&merkle_tree_root_hex)
.expect("failed to hex-decode merkle tree root");
verify_bip341_secp256k1(&sig_bytes, msg_bytes, &pk_bytes, &merkle_tree_root_bytes)
}
None => verify_bip340_secp256k1(&sig_bytes, msg_bytes, &pk_bytes),
},
SchnorrAlgorithm::Ed25519 => {
if let Some(_) = opt_merkle_tree_root_hex {
return Err("ed25519 does not support merkle tree root verification".to_string());
}
verify_ed25519(&sig_bytes, &msg_bytes, &pk_bytes)
}
}
}

fn verify_bip340_secp256k1(
    sig_bytes: &[u8],
    msg_bytes: &[u8],
    secp1_pk_bytes: &[u8],
sig_bytes: &[u8],
msg_bytes: &[u8],
secp1_pk_bytes: &[u8],
) -> Result<SignatureVerificationReply, String> {
    assert_eq!(secp1_pk_bytes.len(), 33);
    assert_eq!(sig_bytes.len(), 64);
assert_eq!(secp1_pk_bytes.len(), 33);

    let sig =
        k256::schnorr::Signature::try_from(sig_bytes).expect("failed to deserialize signature");
let sig = bitcoin::secp256k1::schnorr::Signature::from_slice(sig_bytes)
.expect("failed to deserialize signature");

    let vk = k256::schnorr::VerifyingKey::from_bytes(&secp1_pk_bytes[1..])
        .expect("failed to deserialize BIP340 encoding into public key");
let pk = bitcoin::secp256k1::XOnlyPublicKey::from_slice(&secp1_pk_bytes[1..])
.expect("failed to deserialize BIP340 encoding into public key");

    let is_signature_valid = vk.verify_raw(&msg_bytes, &sig).is_ok();
let secp256k1_engine = Secp256k1::new();
let msg =
bitcoin::secp256k1::Message::from_digest_slice(msg_bytes).expect("failed to parse message");
let is_signature_valid = pk.verify(&secp256k1_engine, &msg, &sig).is_ok();

    Ok(SignatureVerificationReply { is_signature_valid })
Ok(SignatureVerificationReply { is_signature_valid })
}
```

fn verify_bip341_secp256k1(
sig_bytes: &[u8],
msg_bytes: &[u8],
secp1_pk_bytes: &[u8],
merkle_tree_root_bytes: &[u8],
) -> Result<SignatureVerificationReply, String> {
assert_eq!(secp1_pk_bytes.len(), 33);

let pk = XOnlyPublicKey::from_slice(&secp1_pk_bytes[1..]).unwrap();
let tweaked_pk_bytes = {
let secp256k1_engine = Secp256k1::new();
let merkle_root = if merkle_tree_root_bytes.len() == 0 {
None
} else {
Some(
bitcoin::hashes::Hash::from_slice(&merkle_tree_root_bytes)
.expect("failed to create TapBranchHash"),
)
};

pk.tap_tweak(&secp256k1_engine, merkle_root)
.0
.to_inner()
.serialize()
};

let sig = bitcoin::secp256k1::schnorr::Signature::from_slice(sig_bytes)
.expect("failed to deserialize signature");

let pk = bitcoin::secp256k1::XOnlyPublicKey::from_slice(&tweaked_pk_bytes)
.expect("failed to deserialize tweaked BIP340 encoding into public key");

let secp256k1_engine = Secp256k1::new();
let msg =
bitcoin::secp256k1::Message::from_digest_slice(msg_bytes).expect("failed to parse message");
let is_signature_valid = pk.verify(&secp256k1_engine, &msg, &sig).is_ok();

Ok(SignatureVerificationReply { is_signature_valid })
}
```
<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/rust/threshold-schnorr/src/schnorr_example_rust/src/wasm_only.rs#L141">
<div align="center">View in the full example.</div>
</a>
</TabItem>
</AdornedTabs>

Expand All @@ -198,7 +325,9 @@ function verify(ed25519) {
  }

```

<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/motoko/threshold-schnorr/test.sh#L25">
<div align="center">View in the full example.</div>
</a>
</TabItem>
<TabItem value="rust" label="Rust">

Expand Down Expand Up @@ -241,6 +370,9 @@ fn verify_ed25519(
    Ok(SignatureVerificationReply { is_signature_valid })
}
```
<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/rust/threshold-schnorr/src/schnorr_example_rust/src/wasm_only.rs#L141">
<div align="center">View in the full example.</div>
</a>
</TabItem>
</AdornedTabs>

Expand Down Expand Up @@ -272,7 +404,9 @@ To obtain public keys, you will need to call the `schnorr_public_key` method of
  };
```

<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/motoko/threshold-schnorr/src/schnorr_example_motoko/main.mo#L44">
<div align="center">View in the full example.</div>
</a>
</TabItem>

<TabItem value="rust" label="Rust">
Expand All @@ -296,7 +430,9 @@ async fn public_key(algorithm: SchnorrAlgorithm) -> Result<PublicKeyReply, Strin
    })
}
```

<a href="https://github.com/dfinity/examples/blob/013d77e0e12aafaabb8137b910bbc1acce66f1f7/rust/threshold-schnorr/src/schnorr_example_rust/src/wasm_only.rs#L76">
<div align="center">View in the full example.</div>
</a>
</TabItem>
</AdornedTabs>

Expand Down
Loading

0 comments on commit 4120515

Please sign in to comment.