Skip to content

Commit 2e748ec

Browse files
authored
add BIP-341 in Motoko threshold Schnorr example (#1076)
1 parent da45b63 commit 2e748ec

File tree

8 files changed

+432
-87
lines changed

8 files changed

+432
-87
lines changed

.github/workflows/motoko-threshold-schnorr-example.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ jobs:
2222
- name: Motoko Threshold Schnorr Darwin
2323
run: |
2424
dfx start --background
25-
npm install @noble/curves
2625
pushd motoko/threshold-schnorr
26+
npm install
2727
make test
2828
motoko-threshold-schnorr-linux:
2929
runs-on: ubuntu-20.04
@@ -34,6 +34,6 @@ jobs:
3434
- name: Motoko Threshold Schnorr Linux
3535
run: |
3636
dfx start --background
37-
npm install @noble/curves
3837
pushd motoko/threshold-schnorr
38+
npm install
3939
make test

motoko/threshold-schnorr/Makefile

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@ all: deploy
66
deploy:
77
dfx deploy
88

9+
.PHONY: mock
10+
.SILENT: mock
11+
mock: deploy
12+
SCHNORR_MOCK_CANISTER_ID=$(shell dfx canister id chainkey_testing_canister); \
13+
SCHNORR_EXAMPLE_CANISTER_ID=$(shell dfx canister id schnorr_example_motoko); \
14+
echo "Changing to using mock canister instead of management canister for signing"; \
15+
CMD="dfx canister call "$${SCHNORR_EXAMPLE_CANISTER_ID}" for_test_only_change_management_canister_id '("\"$${SCHNORR_MOCK_CANISTER_ID}\"")'"; \
16+
eval "$${CMD}"
17+
918
.PHONY: test
1019
.SILENT: test
11-
test: deploy
12-
bash test.sh hello
20+
test: mock
21+
bash test.sh hellohellohellohellohellohello12
1322

1423
.PHONY: clean
1524
.SILENT: clean

motoko/threshold-schnorr/README.md

+127-46
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
We present a minimal example canister smart contract for showcasing the [threshold Schnorr](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-sign_with_schnorr) API.
44

55
The example canister is a signing oracle that creates Schnorr signatures with
6-
keys derived based on the canister ID and the chosen algorithm, either BIP340 or
6+
keys derived based on the canister ID and the chosen algorithm, either BIP340/BIP341 or
77
Ed25519.
88

99
More specifically:
@@ -27,6 +27,7 @@ This example requires an installation of:
2727

2828
- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install).
2929
- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples`
30+
- [x] For running tests also install [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
3031

3132
Begin by opening a terminal window.
3233

@@ -64,37 +65,71 @@ If you open the URL in a web browser, you will see a web UI that shows the
6465
public methods the canister exposes. Since the canister exposes `public_key`,
6566
`sign`, and `verify`, those are rendered in the web UI.
6667

68+
### Step 3 (optional): Run tests
69+
70+
```bash
71+
npm install
72+
make test
73+
```
74+
75+
#### What this does
76+
77+
- `npm install` installs test javascript dependencies
78+
- `make test` deploys and tests the canister code on the local version of the
79+
IC, and also makes the canister call a mock canister for Schnorr API instead
80+
of the management canister
81+
82+
6783
## Deploying the canister on the mainnet
6884

69-
To deploy this canister the mainnet, one needs to do two things:
85+
To deploy this canister on the mainnet, one needs to do two things:
7086

7187
- Acquire cycles (equivalent of "gas" in other blockchains). This is necessary for all canisters.
7288
- Update the sample source code to have the right key ID. This is unique to this canister.
7389

7490
### Acquire cycles to deploy
7591

76-
Deploying to the Internet Computer requires [cycles](https://internetcomputer.org/docs/current/developer-docs/getting-started/tokens-and-cycles) (the equivalent of "gas" on other blockchains).
92+
Deploying to the Internet Computer requires
93+
[cycles](https://internetcomputer.org/docs/current/developer-docs/getting-started/tokens-and-cycles)
94+
(the equivalent of "gas" on other blockchains).
95+
96+
#### Update management canister ID reference for testing
97+
98+
The latest version of `dfx`, `v0.24.3`, does not yet support BIP341
99+
`opt_merkle_tree_root_hex` that is not `None`. Therefore, for local tests, [the
100+
chain-key testing canister](https://github.com/dfinity/chainkey-testing-canister)
101+
can be installed and used instead of the management canister. Note also that the
102+
chain-key testing canister is deployed on the mainnet and can be used for mainnet
103+
testing to reduce the costs, see the linked repo for more details.
104+
105+
This sample canister allows the caller to change the management canister address
106+
for Schnorr by calling the `for_test_only_change_management_canister_id`
107+
endpoint with the target canister principal. With `dfx`, this can be done
108+
automatically with `make mock`, which will install the chain-key testing canister
109+
and use it instead of the management canister. Note that `dfx` should be running
110+
to successfully run `make mock`.
77111

78112
### Update source code with the right key ID
79113

80114
To deploy the sample code, the canister needs the right key ID for the right environment. Specifically, one needs to replace the value of the `key_id` in the `src/schnorr_example_motoko/src/lib.rs` file of the sample code. Before deploying to mainnet, one should modify the code to use the right name of the `key_id`.
81115

82-
There are three options that are planed to be supported:
116+
There are four options that are supported:
83117

118+
* `insecure_test_key_1`: the key ID supported by the `chainkey_testing_canister`
119+
([link](https://github.com/dfinity/chainkey-testing-canister/)).
84120
* `dfx_test_key`: a default key ID that is used in deploying to a local version of IC (via IC SDK).
85121
* `test_key_1`: a master **test** key ID that is used in mainnet.
86122
* `key_1`: a master **production** key ID that is used in mainnet.
87123

88124
For example, the default code in `src/schnorr_example_motoko/src/main.mo`
89-
hard-codes the used of `dfx_test_key` and derives the key ID as follows and can
125+
hard-codes the use of `insecure_test_key_1` and derives the key ID as follows and can
90126
be deployed locally:
91127

92-
93128
```motoko
94-
key_id = { algorithm = algorithm_arg; name = "dfx_test_key" }
129+
key_id = { algorithm = algorithm_arg; name = "insecure_test_key_1" }
95130
```
96131

97-
IMPORTANT: To deploy to IC mainnet, one needs to replace `"dfx_test_key"` with
132+
IMPORTANT: To deploy to IC mainnet, one needs to replace `"insecure_test_key_1"` with
98133
either `"test_key_1"` or `"key_1"` depending on the desired intent. Both uses of
99134
key ID in `src/schnorr_example_motoko/src/main.mo` must be consistent.
100135

@@ -124,7 +159,7 @@ mainnet.
124159

125160
### Using the Candid UI
126161

127-
If you deployed your canister locally or to the mainnet, you should have a URL to the Candid web UI where you can access the public methods. We can call the `public-key` method.
162+
If you deployed your canister locally or to the mainnet, you should have a URL to the Candid web UI where you can access the public methods. We can call the `public_key` method.
128163

129164
In the example below, the method returns
130165
`6e48e755842d0323be83edc7fc8766a20423c8127f7731993873d2f123d01a34` as the
@@ -141,23 +176,23 @@ Ed25519 public key.
141176

142177

143178
### Code walkthrough
144-
Open the file `lib.rs`, which will show the following Motoko code that
179+
Open the file `main.mo`, which will show the following Motoko code that
145180
demonstrates how to obtain a Schnorr public key.
146181

147182
```motoko
148-
public shared ({ caller }) func public_key(algorithm_arg : SchnorrAlgotirhm) : async {
149-
#Ok : { public_key_hex : Text };
150-
#Err : Text;
183+
public shared ({ caller }) func public_key(algorithm : SchnorrAlgorithm) : async {
184+
#ok : { public_key_hex : Text };
185+
#err : Text;
151186
} {
152187
try {
153188
let { public_key } = await ic.schnorr_public_key({
154189
canister_id = null;
155190
derivation_path = [Principal.toBlob(caller)];
156-
key_id = { algorithm = algorithm_arg; name = "dfx_test_key" };
191+
key_id = { algorithm; name = "insecure_test_key_1" };
157192
});
158193
#Ok({ public_key_hex = Hex.encode(Blob.toArray(public_key)) });
159194
} catch (err) {
160-
#Err(Error.message(err));
195+
#err(Error.message(err));
161196
};
162197
};
163198
```
@@ -187,20 +222,28 @@ For obtaining the canister's root public key, the derivation path in the API can
187222
Computing threshold Schnorr signatures is the core functionality of this feature. **Canisters do not hold Schnorr keys themselves**, but keys are derived from a master key held by dedicated subnets. A canister can request the computation of a signature through the management canister API. The request is then routed to a subnet holding the specified key and the subnet computes the requested signature using threshold cryptography. Thereby, it derives the canister root key or a key obtained through further derivation, as part of the signature protocol, from a shared secret and the requesting canister's principal identifier. Thus, a canister can only request signatures to be created for its canister root key or a key derived from it. This means, that canisters "control" their private Schnorr keys in that they decide when signatures are to be created with them, but don't hold a private key themselves.
188223

189224
```motoko
190-
public shared ({ caller }) func sign(message_arg : Text, algorithm_arg : SchnorrAlgotirhm) : async {
191-
#Ok : { signature_hex : Text };
192-
#Err : Text;
225+
public shared ({ caller }) func sign(message_arg : Text, algorithm : SchnorrAlgorithm, bip341TweakHex : ?Text) : async {
226+
#ok : { signature_hex : Text };
227+
#err : Text;
193228
} {
229+
let aux = switch (Option.map(bip341TweakHex, tryHexToTweak)) {
230+
case (null) null;
231+
case (?#ok some) ?some;
232+
case (?#err err) return #err err;
233+
};
234+
194235
try {
195-
Cycles.add(25_000_000_000);
196-
let { signature } = await ic.sign_with_schnorr({
236+
Cycles.add<system>(25_000_000_000);
237+
let signArgs = {
197238
message = Text.encodeUtf8(message_arg);
198239
derivation_path = [Principal.toBlob(caller)];
199-
key_id = { algorithm = algorithm_arg; name = "dfx_test_key" };
200-
});
201-
#Ok({ signature_hex = Hex.encode(Blob.toArray(signature)) });
240+
key_id = { algorithm; name = "insecure_test_key_1" };
241+
aux;
242+
};
243+
let { signature } = await ic.sign_with_schnorr(signArgs);
244+
#ok({ signature_hex = Hex.encode(Blob.toArray(signature)) });
202245
} catch (err) {
203-
#Err(Error.message(err));
246+
#err(Error.message(err));
204247
};
205248
};
206249
```
@@ -215,41 +258,79 @@ externally.
215258

216259
Ed25519 can be verified as follows:
217260
```javascript
218-
import('@noble/curves/ed25519').then((ed25519) => { verify(ed25519.ed25519); })
219-
.catch((err) => { console.log(err) });
261+
async function run() {
262+
try {
263+
const ed25519 = await import('@noble/ed25519');
264+
const sha512 = await import('@noble/hashes/sha512');
220265

221-
function verify(ed25519) {
222-
const test_sig = '1efa03b7b7f9077449a0f4b3114513f9c90ccf214166a8907c23d9c2bbbd0e0e6e630f67a93c1bd525b626120e86846909aedf4c58763ae8794bcef57401a301'
223-
const test_pubkey = '566d53caf990f5f096d151df70b2a75107fac6724cb61a9d6d2aa63e1496b003'
224-
const test_msg = Uint8Array.from(Buffer.from("hello", 'utf8'));
266+
ed25519.etc.sha512Sync = (...m) => sha512.sha512(ed25519.etc.concatBytes(...m));
225267

226-
console.log(ed25519.verify(test_sig, test_msg, test_pubkey));
227-
}
268+
const test_sig = '1efa03b7b7f9077449a0f4b3114513f9c90ccf214166a8907c23d9c2bbbd0e0e6e630f67a93c1bd525b626120e86846909aedf4c58763ae8794bcef57401a301';
269+
const test_pubkey = '566d53caf990f5f096d151df70b2a75107fac6724cb61a9d6d2aa63e1496b003'
270+
const test_msg = Uint8Array.from(Buffer.from("hello", 'utf8'));
271+
272+
console.log(ed25519.verify(test_sig, test_msg, test_pubkey));
273+
}
274+
catch(err) {
275+
console.log(err);
276+
}
277+
}
278+
279+
run();
228280
```
229281

230282
BIP340 can be verified as follows:
231283
```javascript
232-
import('@noble/curves/secp256k1').then((bip340) => { verify(bip340.schnorr); })
233-
.catch((err) => { console.log(err) });
284+
async function run() {
285+
try {
286+
const ecc = await import('tiny-secp256k1');
287+
288+
const test_sig = Buffer.from('311e1dceddd1380d0424e01b19711e926ca2f26c0dda57b405bec1359510674871a22487c96afa4a4bf47858d1d79caa400bb51ab793d9fad2a689f8bfc681aa', 'hex');
289+
const test_pubkey = Buffer.from('02472bb4da5c5ce627d599feba90d0257a558d4e226f9fc7914f811e301ad06f38'.substring(2), 'hex');
290+
const test_msg = Uint8Array.from(Buffer.from("hellohellohellohellohellohello12", 'utf8'));
291+
292+
console.log(ecc.verifySchnorr(test_msg, test_pubkey, test_sig));
293+
}
294+
catch(err) {
295+
console.log(err);
296+
}
297+
}
298+
299+
run();
300+
```
301+
302+
BIP341 can be verified as follows:
303+
```javascript
304+
async function run() {
305+
try {
306+
const bip341 = await import('bitcoinjs-lib/src/payments/bip341.js');
307+
const bitcoin = await import('bitcoinjs-lib');
308+
const ecc = await import('tiny-secp256k1');
234309

235-
function verify(bip340) {
236-
const test_sig = '1b64ca7a7f02c76633954f320675267685b3b80560eb6a35cda20291ddefc709364e59585771c284e46264bfbb0620e23eb8fb274994f7a6f2fcbc8a9430e5d7';
237-
// the first byte of the BIP340 public key is truncated
238-
const pubkey = '0341d7cf39688e10b5f11f168ad0a9e790bcb429d7d486eab07d2c824b85821470'.substring(2)
239-
const test_msg = Uint8Array.from(Buffer.from("hello", 'utf8'));
310+
bitcoin.initEccLib(ecc);
240311

241-
console.log(bip340.verify(test_sig, test_msg, test_pubkey));
242-
}
312+
const test_tweak = Buffer.from('012345678901234567890123456789012345678901234567890123456789abcd', 'hex');
313+
const test_sig = Buffer.from('3c3e51fc771a5a8cb553bf2dd151bb02d0f473ff274a92d32310267977918d72121f97c318226422c033d33daf376d42c9a07e71643ff332cb30611fe5e163da', 'hex');
314+
const test_pubkey = Buffer.from('02472bb4da5c5ce627d599feba90d0257a558d4e226f9fc7914f811e301ad06f38'.substring(2), 'hex');
315+
const test_msg = Uint8Array.from(Buffer.from("hellohellohellohellohellohello12", 'utf8'));
316+
317+
const tweaked_test_pubkey = bip341.tweakKey(test_pubkey, test_tweak).x;
318+
319+
console.log(ecc.verifySchnorr(test_msg, tweaked_test_pubkey, test_sig));
320+
}
321+
catch(err) {
322+
console.log(err);
323+
}
324+
}
325+
326+
run();
243327
```
244328

245-
The call to `verify` function should always return `true` for correct parameters
329+
The call to `verify/verifySchnorr` function should always return `true` for correct parameters
246330
and `false` or error otherwise.
247331

248332
Similar verifications can be done in many other languages with the help of
249-
cryptographic libraries that support the `bip340secp256k1` signing *with
250-
arbitrary message length* as specified in
251-
[BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#user-content-Messages_of_Arbitrary_Size)
252-
and `ed25519` signing.
333+
cryptographic libraries that support the BIP340/BIP341 and `ed25519` signing.
253334

254335
## Conclusion
255336

motoko/threshold-schnorr/dfx.json

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"package": "schnorr_example_motoko",
66
"main": "src/schnorr_example_motoko/main.mo",
77
"type": "motoko"
8+
},
9+
"chainkey_testing_canister": {
10+
"type": "custom",
11+
"candid": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.did",
12+
"wasm": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.wasm.gz"
813
}
914
}
1015
}

0 commit comments

Comments
 (0)