From c3e5433936fdbba3fd3acc7a579d41763d0cdf21 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 17:16:36 +0200 Subject: [PATCH 01/11] chore: migrate motoko/threshold-schnorr to icp-cli Replace dfx.json with icp.yaml, move source to backend/, update mops.toml to moc 1.9.0 + core 2.5.0 + ic 4.0.0, replace inline actor type with import { ic } "mo:ic", add icp-cli Makefile test target and CI workflow. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/threshold-schnorr.yml | 28 +++ .../.devcontainer/devcontainer.json | 20 -- motoko/threshold-schnorr/BUILD.md | 113 ----------- motoko/threshold-schnorr/Makefile | 42 ++-- motoko/threshold-schnorr/README.md | 179 ++++-------------- .../utils => backend}/Hex.mo | 0 .../main.mo => backend/app.mo} | 24 +-- motoko/threshold-schnorr/dfx.json | 15 -- motoko/threshold-schnorr/icp.yaml | 4 + motoko/threshold-schnorr/mops.toml | 10 +- motoko/threshold-schnorr/package-lock.json | 156 --------------- motoko/threshold-schnorr/package.json | 8 - motoko/threshold-schnorr/test.sh | 117 ------------ 13 files changed, 104 insertions(+), 612 deletions(-) create mode 100644 .github/workflows/threshold-schnorr.yml delete mode 100644 motoko/threshold-schnorr/.devcontainer/devcontainer.json delete mode 100644 motoko/threshold-schnorr/BUILD.md rename motoko/threshold-schnorr/{src/schnorr_example_motoko/utils => backend}/Hex.mo (100%) rename motoko/threshold-schnorr/{src/schnorr_example_motoko/main.mo => backend/app.mo} (76%) delete mode 100644 motoko/threshold-schnorr/dfx.json create mode 100644 motoko/threshold-schnorr/icp.yaml delete mode 100644 motoko/threshold-schnorr/package-lock.json delete mode 100644 motoko/threshold-schnorr/package.json delete mode 100755 motoko/threshold-schnorr/test.sh diff --git a/.github/workflows/threshold-schnorr.yml b/.github/workflows/threshold-schnorr.yml new file mode 100644 index 000000000..9f35fe751 --- /dev/null +++ b/.github/workflows/threshold-schnorr.yml @@ -0,0 +1,28 @@ +name: threshold-schnorr + +on: + push: + branches: [master] + pull_request: + paths: + - motoko/threshold-schnorr/** + - .github/workflows/threshold-schnorr.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-threshold-schnorr: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/threshold-schnorr + run: | + icp network start -d + icp deploy + make test diff --git a/motoko/threshold-schnorr/.devcontainer/devcontainer.json b/motoko/threshold-schnorr/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/threshold-schnorr/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/threshold-schnorr/BUILD.md b/motoko/threshold-schnorr/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/motoko/threshold-schnorr/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy - -``` - -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). - -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/motoko/threshold-schnorr/Makefile b/motoko/threshold-schnorr/Makefile index 53c95a079..c77f60f6a 100644 --- a/motoko/threshold-schnorr/Makefile +++ b/motoko/threshold-schnorr/Makefile @@ -1,22 +1,26 @@ -.PHONY: all -all: deploy +.PHONY: test -.PHONY: deploy -.SILENT: deploy -deploy: - dfx deploy +test: + @echo "=== Test 1/4: public_key returns hex for bip340secp256k1 ===" + @result=$$(icp canister call backend public_key '(variant { bip340secp256k1 })') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'public_key_hex' && \ + echo "PASS" || (echo "FAIL" && exit 1) -.PHONY: test -.SILENT: test -test: deploy - npm install - bash test.sh hellohellohellohellohellohello12 + @echo "=== Test 2/4: public_key returns hex for ed25519 ===" + @result=$$(icp canister call backend public_key '(variant { ed25519 })') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'public_key_hex' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3/4: sign returns signature_hex for bip340secp256k1 ===" + @result=$$(icp canister call backend sign '("hello world", variant { bip340secp256k1 }, null)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'signature_hex' && \ + echo "PASS" || (echo "FAIL" && exit 1) -.PHONY: clean -.SILENT: clean -clean: - rm -rf .dfx - rm -rf dist - rm -rf node_modules - rm -rf src/declarations - rm -f .env \ No newline at end of file + @echo "=== Test 4/4: sign returns signature_hex for ed25519 ===" + @result=$$(icp canister call backend sign '("hello world", variant { ed25519 }, null)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'signature_hex' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/threshold-schnorr/README.md b/motoko/threshold-schnorr/README.md index 8ee3f2d1a..a7492edb2 100644 --- a/motoko/threshold-schnorr/README.md +++ b/motoko/threshold-schnorr/README.md @@ -1,171 +1,70 @@ # Threshold Schnorr -We present a minimal example canister smart contract for showcasing the [threshold Schnorr](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr) API. +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/threshold-schnorr) -The example canister is a signing oracle that creates Schnorr signatures with -keys derived based on the canister ID and the chosen algorithm, either BIP340/BIP341 or -Ed25519. +## Overview -More specifically: +This example demonstrates how to use the ICP [threshold Schnorr](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr) API from a Motoko canister. The canister acts as a signing oracle that creates Schnorr signatures using keys derived from the canister ID and the chosen algorithm (BIP340/BIP341 or Ed25519). Canisters do not hold private keys themselves — signing requests are routed to threshold Schnorr subnets that compute signatures using distributed key shares. -- The sample canister receives a request that provides a message and an algorithm ID. -- The sample canister uses the key derivation string for the derivation path. -- The sample canister uses the above to request a signature from the threshold - Schnorr [subnet](https://wiki.internetcomputer.org/wiki/Subnet_blockchain) - (the threshold Schnorr subnet is a subnet generating threshold Schnorr - signatures). +## Build and deploy from the command line -## Deploying from ICP Ninja +### Prerequisites +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/threshold-schnorr) - -## Build and deploy from the command-line - -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) - -### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/) - -### 3. Navigate into the project's directory. - -### 4. Deploy the project to your local environment: - -``` -dfx start --background --clean && dfx deploy +### Install +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/threshold-schnorr ``` -### 5. Update source code with the right key ID - -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`. - -There are four options that are supported: - -* `insecure_test_key_1`: the key ID supported by the `chainkey_testing_canister` - ([link](https://github.com/dfinity/chainkey-testing-canister/)). -* `dfx_test_key`: a default key ID that is used in deploying to a local version of IC (via IC SDK). -* `test_key_1`: a master **test** key ID that is used in mainnet. -* `key_1`: a master **production** key ID that is used in mainnet. - -For example, the default code in `src/schnorr_example_motoko/src/main.mo` -hard-codes the use of `insecure_test_key_1` and derives the key ID as follows and can -be deployed locally: - -```motoko -key_id = { algorithm = algorithm_arg; name = "insecure_test_key_1" } +### Deploy and test +```bash +icp network start -d +icp deploy +make test +icp network stop ``` -> [!WARNING] -> IMPORTANT: To deploy to IC mainnet, one needs to replace `"insecure_test_key_1"` with either `"test_key_1"` or `"key_1"` depending on the desired intent. Both uses of key ID in `src/schnorr_example_motoko/src/main.mo` must be consistent. - -### Deploying +## Key ID configuration -To [deploy via mainnet](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-mainnet.md), run the following commands: +The canister uses `test_key_1` by default (mainnet test key). To use a different key, update the `key_id` value in `backend/app.mo`: -```bash -npm install -dfx deploy --network ic -``` +- `dfx_test_key`: local IC SDK deployment +- `test_key_1`: mainnet test key +- `key_1`: mainnet production key ## Obtaining public keys -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. You can call the `public_key` method. - -### Canister root public key +Call the `public_key` method with the desired algorithm variant: -For obtaining the canister's root public key, the derivation path in the API can be simply left empty. - -### Key derivation +```bash +icp canister call backend public_key '(variant { ed25519 })' +icp canister call backend public_key '(variant { bip340secp256k1 })' +``` -- For obtaining a canister's public key below its root key in the BIP-32 key derivation hierarchy, a derivation path needs to be specified. As explained in the general documentation, each element in the array of the derivation path is either a 32-bit integer encoded as 4 bytes in big endian or a byte array of arbitrary length. The element is used to derive the key in the corresponding level at the derivation hierarchy. -- In the example code above, we use the bytes extracted from the `msg.caller` principal in the `derivation_path`, so that different callers of `public_key()` method of our canister will be able to get their own public keys. +The derivation path uses the caller's principal, so different callers receive different public keys. ## Signing -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. - -## Signature verification - -For completeness of the example, we show that the created signatures can be -verified with the public key corresponding to the same canister and derivation -path in javascript. Note that in contrast to the Rust implementation of this -example, the signature verification is not part of the canister API and happens -externally. - -Ed25519 can be verified as follows: -```javascript - async function run() { - try { - const ed25519 = await import('@noble/ed25519'); - const sha512 = await import('@noble/hashes/sha512'); - - ed25519.etc.sha512Sync = (...m) => sha512.sha512(ed25519.etc.concatBytes(...m)); +Call the `sign` method with a message, algorithm, and optional BIP341 tweak: - const test_sig = '1efa03b7b7f9077449a0f4b3114513f9c90ccf214166a8907c23d9c2bbbd0e0e6e630f67a93c1bd525b626120e86846909aedf4c58763ae8794bcef57401a301'; - const test_pubkey = '566d53caf990f5f096d151df70b2a75107fac6724cb61a9d6d2aa63e1496b003' - const test_msg = Uint8Array.from(Buffer.from("hello", 'utf8')); - - console.log(ed25519.verify(test_sig, test_msg, test_pubkey)); - } - catch(err) { - console.log(err); - } - } - - run(); +```bash +icp canister call backend sign '("hello world", variant { ed25519 }, null)' +icp canister call backend sign '("hello world", variant { bip340secp256k1 }, null)' ``` -BIP340 can be verified as follows: -```javascript - 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); - } - } - - run(); +For BIP341 (Taproot) signing with a tweak: +```bash +icp canister call backend sign '("hello world", variant { bip340secp256k1 }, opt "012345678901234567890123456789012345678901234567890123456789abcd")' ``` -BIP341 can be verified as follows: -```javascript - 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); - - 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; +## Updating the Candid interface - console.log(ecc.verifySchnorr(test_msg, tweaked_test_pubkey, test_sig)); - } - catch(err) { - console.log(err); - } - } - - run(); +```bash +$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo ``` -The call to `verify/verifySchnorr` function should always return `true` for correct parameters -and `false` or error otherwise. - -Similar verifications can be done in many other languages with the help of -cryptographic libraries that support the BIP340/BIP341 and `ed25519` signing. - ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/motoko/threshold-schnorr/src/schnorr_example_motoko/utils/Hex.mo b/motoko/threshold-schnorr/backend/Hex.mo similarity index 100% rename from motoko/threshold-schnorr/src/schnorr_example_motoko/utils/Hex.mo rename to motoko/threshold-schnorr/backend/Hex.mo diff --git a/motoko/threshold-schnorr/src/schnorr_example_motoko/main.mo b/motoko/threshold-schnorr/backend/app.mo similarity index 76% rename from motoko/threshold-schnorr/src/schnorr_example_motoko/main.mo rename to motoko/threshold-schnorr/backend/app.mo index 113c43f2f..3ddf09f69 100644 --- a/motoko/threshold-schnorr/src/schnorr_example_motoko/main.mo +++ b/motoko/threshold-schnorr/backend/app.mo @@ -1,11 +1,9 @@ import Error "mo:core/Error"; -import Option "mo:core/Option"; -import Principal "mo:core/Principal"; -import Text "mo:core/Text"; import Blob "mo:core/Blob"; -import Hex "./utils/Hex"; +import Hex "./Hex"; +import { ic } "mo:ic"; -persistent actor { +actor ThresholdSchnorr { public type SchnorrAlgorithm = { #bip340secp256k1; #ed25519 }; public type KeyId = { @@ -19,22 +17,6 @@ persistent actor { }; }; - // Only the Schnorr methods in the IC management canister is required here. - type IC = actor { - schnorr_public_key : ({ - canister_id : ?Principal; - derivation_path : [Blob]; - key_id : KeyId; - }) -> async ({ public_key : Blob }); - sign_with_schnorr : ({ - message : Blob; - derivation_path : [Blob]; - key_id : KeyId; - aux : ?SchnorrAux; - }) -> async ({ signature : Blob }); - }; - - transient let ic : IC = actor ("aaaaa-aa"); transient let key_id : Text = "test_key_1"; // Use "key_1" for production and "dfx_test_key" locally public shared ({ caller }) func public_key(algorithm : SchnorrAlgorithm) : async { diff --git a/motoko/threshold-schnorr/dfx.json b/motoko/threshold-schnorr/dfx.json deleted file mode 100644 index 16492a0fd..000000000 --- a/motoko/threshold-schnorr/dfx.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "canisters": { - "schnorr_example_motoko": { - "candid": "src/schnorr_example_motoko/schnorr_example.did", - "package": "schnorr_example_motoko", - "main": "src/schnorr_example_motoko/main.mo", - "type": "motoko" - } - }, - "defaults": { - "build": { - "packtool": "mops sources" - } - } -} diff --git a/motoko/threshold-schnorr/icp.yaml b/motoko/threshold-schnorr/icp.yaml new file mode 100644 index 000000000..fb741fade --- /dev/null +++ b/motoko/threshold-schnorr/icp.yaml @@ -0,0 +1,4 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@v5.0.0" diff --git a/motoko/threshold-schnorr/mops.toml b/motoko/threshold-schnorr/mops.toml index ddb2e07c5..7902c42eb 100644 --- a/motoko/threshold-schnorr/mops.toml +++ b/motoko/threshold-schnorr/mops.toml @@ -1,11 +1,15 @@ [toolchain] -moc = "1.5.1" +moc = "1.9.0" [dependencies] -core = "2.4.0" +core = "2.5.0" +ic = "4.0.0" [moc] # M0236: use context dot notation # M0237: redundant explicit implicit arguments # M0223: redundant type instantiation -args = ["-W=M0236,M0237,M0223"] +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" diff --git a/motoko/threshold-schnorr/package-lock.json b/motoko/threshold-schnorr/package-lock.json deleted file mode 100644 index d9afe3a9a..000000000 --- a/motoko/threshold-schnorr/package-lock.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "name": "threshold-schnorr", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@noble/ed25519": "^2.2.0", - "bitcoinjs-lib": "^6.1.0", - "noble-hashes": "^0.3.0", - "tiny-secp256k1": "^2.2.0" - } - }, - "node_modules/@noble/ed25519": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.2.2.tgz", - "integrity": "sha512-8dbNnAGY8497RMEes5MWzjZkKRLWToKwenlrqzHTWVKyOaEzLqU2iPaVJGRvkyEcJL6CmuqSt0paPFB2aR6WwA==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", - "license": "MIT" - }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT" - }, - "node_modules/bip174": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", - "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bitcoinjs-lib": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz", - "integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bech32": "^2.0.0", - "bip174": "^2.1.1", - "bs58check": "^3.0.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/bs58check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", - "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^5.0.0" - } - }, - "node_modules/noble-hashes": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/noble-hashes/-/noble-hashes-0.3.1.tgz", - "integrity": "sha512-TpYvlZvM8nGB582H9qQdTCLTNPS4TX9r5gkB4iiCWlO/URrdFJKAKwzwwEcNYPhLrcmCvBF1Nfm25GMbFWEplw==", - "deprecated": "Switch to namespaced @noble/hashes for security and feature updates", - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/tiny-secp256k1": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz", - "integrity": "sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q==", - "license": "MIT", - "dependencies": { - "uint8array-tools": "0.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", - "license": "MIT" - }, - "node_modules/uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.1" - } - } - } -} diff --git a/motoko/threshold-schnorr/package.json b/motoko/threshold-schnorr/package.json deleted file mode 100644 index 46ad3df4e..000000000 --- a/motoko/threshold-schnorr/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "bitcoinjs-lib": "^6.1.0", - "@noble/ed25519": "^2.2.0", - "noble-hashes": "^0.3.0", - "tiny-secp256k1": "^2.2.0" - } -} \ No newline at end of file diff --git a/motoko/threshold-schnorr/test.sh b/motoko/threshold-schnorr/test.sh deleted file mode 100755 index 932e1bc8b..000000000 --- a/motoko/threshold-schnorr/test.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -export LC_ALL=C -function get_text_in_double_quotes() { - echo -n "$1" | sed -e 's/^[^"]*"//' -e 's/"//g' -e 's/;//g' -} - -function test_impl() { - test -z "$1" && echo "USAGE: $0 " && exit 1 - - message="$1" - echo message="$message" - -# This should be uncommented when dfx deploys an Ed25519 key - - ed25519_sign_cmd="dfx canister call schnorr_example_motoko sign '(\"${message}\" ,(variant { ed25519 }), null)'" - ed25519_sig_raw_output="$(eval ${ed25519_sign_cmd} | grep signature_hex)" - ed25519_sig_hex="$(get_text_in_double_quotes "${ed25519_sig_raw_output}")" - echo ed25519_signature_hex="$ed25519_sig_hex" - - ed25519_public_key_raw_output="$(dfx canister call schnorr_example_motoko public_key '(variant { ed25519 })' | grep public_key_hex)" - ed25519_public_key_hex="$(get_text_in_double_quotes "${ed25519_public_key_raw_output}")" - echo ed25519_public_key_hex="$ed25519_public_key_hex" - - node < sha512.sha512(ed25519.etc.concatBytes(...m)); - - const sig = '${ed25519_sig_hex}'; - const pubkey = '${ed25519_public_key_hex}'; - const msg = Uint8Array.from(Buffer.from("${message}", 'utf8')); - - console.log(ed25519.verify(sig, msg, pubkey)); - } - catch(err) { - console.log(err); - } - } - - run(); -END - - bip340_sign_cmd="dfx canister call schnorr_example_motoko sign '(\"${message}\" ,(variant { bip340secp256k1 }), null)'" - bip340_sig_raw_output="$(eval ${bip340_sign_cmd} | grep signature_hex)" - bip340_sig_hex=$(get_text_in_double_quotes "${bip340_sig_raw_output}") - echo bip340_signature_hex="${bip340_sig_hex}" - - bip340_public_key_raw_output="$(dfx canister call schnorr_example_motoko public_key 'variant { bip340secp256k1 }' | grep public_key_hex)" - bip340_public_key_hex=$(get_text_in_double_quotes "${bip340_public_key_raw_output}") - echo bip340_public_key_hex="${bip340_public_key_hex}" - - node < Date: Wed, 10 Jun 2026 18:32:20 +0200 Subject: [PATCH 02/11] fix: add missing Blob, Principal, Text, Option imports; apply mops check --fix Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/backend/app.mo | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/motoko/threshold-schnorr/backend/app.mo b/motoko/threshold-schnorr/backend/app.mo index 3ddf09f69..0fdee05d8 100644 --- a/motoko/threshold-schnorr/backend/app.mo +++ b/motoko/threshold-schnorr/backend/app.mo @@ -1,5 +1,8 @@ -import Error "mo:core/Error"; import Blob "mo:core/Blob"; +import Error "mo:core/Error"; +import Option "mo:core/Option"; +import Principal "mo:core/Principal"; +import Text "mo:core/Text"; import Hex "./Hex"; import { ic } "mo:ic"; From f09f03f442585908624d8a436eb69443c8dc57a6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 19:15:02 +0200 Subject: [PATCH 03/11] feat: add cryptographic verification for ed25519/bip340/bip341 in test.sh Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/Makefile | 12 ++- motoko/threshold-schnorr/package.json | 11 +++ motoko/threshold-schnorr/test.sh | 117 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 motoko/threshold-schnorr/package.json create mode 100755 motoko/threshold-schnorr/test.sh diff --git a/motoko/threshold-schnorr/Makefile b/motoko/threshold-schnorr/Makefile index c77f60f6a..aa3afd477 100644 --- a/motoko/threshold-schnorr/Makefile +++ b/motoko/threshold-schnorr/Makefile @@ -1,26 +1,30 @@ .PHONY: test test: - @echo "=== Test 1/4: public_key returns hex for bip340secp256k1 ===" + @echo "=== Test 1/5: public_key returns hex for bip340secp256k1 ===" @result=$$(icp canister call backend public_key '(variant { bip340secp256k1 })') && \ echo "$$result" && \ echo "$$result" | grep -q 'public_key_hex' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 2/4: public_key returns hex for ed25519 ===" + @echo "=== Test 2/5: public_key returns hex for ed25519 ===" @result=$$(icp canister call backend public_key '(variant { ed25519 })') && \ echo "$$result" && \ echo "$$result" | grep -q 'public_key_hex' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 3/4: sign returns signature_hex for bip340secp256k1 ===" + @echo "=== Test 3/5: sign returns signature_hex for bip340secp256k1 ===" @result=$$(icp canister call backend sign '("hello world", variant { bip340secp256k1 }, null)') && \ echo "$$result" && \ echo "$$result" | grep -q 'signature_hex' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 4/4: sign returns signature_hex for ed25519 ===" + @echo "=== Test 4/5: sign returns signature_hex for ed25519 ===" @result=$$(icp canister call backend sign '("hello world", variant { ed25519 }, null)') && \ echo "$$result" && \ echo "$$result" | grep -q 'signature_hex' && \ echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 5/5: all three signature types verify cryptographically ===" + @npm install --silent + @chmod +x test.sh && ./test.sh "hello world" && echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/threshold-schnorr/package.json b/motoko/threshold-schnorr/package.json new file mode 100644 index 000000000..0a0d390db --- /dev/null +++ b/motoko/threshold-schnorr/package.json @@ -0,0 +1,11 @@ +{ + "name": "threshold-schnorr-test", + "version": "1.0.0", + "private": true, + "dependencies": { + "@noble/ed25519": "^1.7.3", + "@noble/hashes": "^1.3.0", + "bitcoinjs-lib": "^6.1.5", + "tiny-secp256k1": "^2.2.3" + } +} diff --git a/motoko/threshold-schnorr/test.sh b/motoko/threshold-schnorr/test.sh new file mode 100755 index 000000000..a501ff99f --- /dev/null +++ b/motoko/threshold-schnorr/test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail +export LC_ALL=C + +function get_text_in_double_quotes() { + echo -n "$1" | sed -e 's/^[^"]*"//' -e 's/"//g' -e 's/;//g' +} + +function test_impl() { + test -z "${1:-}" && echo "USAGE: $0 " && exit 1 + + local message="$1" + echo "message=$message" + + local ed25519_sign_cmd="icp canister call backend sign '(\"${message}\" ,(variant { ed25519 }), null)'" + local ed25519_sig_raw_output + ed25519_sig_raw_output="$(eval "${ed25519_sign_cmd}" | grep signature_hex)" + local ed25519_sig_hex + ed25519_sig_hex="$(get_text_in_double_quotes "${ed25519_sig_raw_output}")" + echo "ed25519_signature_hex=$ed25519_sig_hex" + + local ed25519_public_key_raw_output + ed25519_public_key_raw_output="$(icp canister call backend public_key '(variant { ed25519 })' | grep public_key_hex)" + local ed25519_public_key_hex + ed25519_public_key_hex="$(get_text_in_double_quotes "${ed25519_public_key_raw_output}")" + echo "ed25519_public_key_hex=$ed25519_public_key_hex" + + node < sha512.sha512(ed25519.etc.concatBytes(...m)); + + const sig = '${ed25519_sig_hex}'; + const pubkey = '${ed25519_public_key_hex}'; + const msg = Uint8Array.from(Buffer.from("${message}", 'utf8')); + console.log(await ed25519.verify(sig, msg, pubkey)); + } catch(err) { + console.error(err); + process.exit(1); + } + } + run(); +END + + local bip340_sign_cmd="icp canister call backend sign '(\"${message}\" ,(variant { bip340secp256k1 }), null)'" + local bip340_sig_raw_output + bip340_sig_raw_output="$(eval "${bip340_sign_cmd}" | grep signature_hex)" + local bip340_sig_hex + bip340_sig_hex="$(get_text_in_double_quotes "${bip340_sig_raw_output}")" + echo "bip340_signature_hex=$bip340_sig_hex" + + local bip340_public_key_raw_output + bip340_public_key_raw_output="$(icp canister call backend public_key '(variant { bip340secp256k1 })' | grep public_key_hex)" + local bip340_public_key_hex + bip340_public_key_hex="$(get_text_in_double_quotes "${bip340_public_key_raw_output}")" + echo "bip340_public_key_hex=$bip340_public_key_hex" + + node < Date: Wed, 10 Jun 2026 21:31:29 +0200 Subject: [PATCH 04/11] fix: use @noble/ed25519 v2 (sync), 32-byte message for BIP340 verifySchnorr - @noble/ed25519 v2.x: sync verify(), no sha512Sync setup needed - BIP340/BIP341: tiny-secp256k1.verifySchnorr requires exactly 32 bytes; use 'hello world of BIP340 secp256k1' (32 ASCII chars) - Switch inline node scripts from dynamic import() to require() Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/Makefile | 2 +- motoko/threshold-schnorr/package.json | 3 +- motoko/threshold-schnorr/test.sh | 156 ++++++++++---------------- 3 files changed, 62 insertions(+), 99 deletions(-) diff --git a/motoko/threshold-schnorr/Makefile b/motoko/threshold-schnorr/Makefile index aa3afd477..9823ecdff 100644 --- a/motoko/threshold-schnorr/Makefile +++ b/motoko/threshold-schnorr/Makefile @@ -27,4 +27,4 @@ test: @echo "=== Test 5/5: all three signature types verify cryptographically ===" @npm install --silent - @chmod +x test.sh && ./test.sh "hello world" && echo "PASS" || (echo "FAIL" && exit 1) + @chmod +x test.sh && ./test.sh "hello world of BIP340 secp256k1" && echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/threshold-schnorr/package.json b/motoko/threshold-schnorr/package.json index 0a0d390db..58b974c08 100644 --- a/motoko/threshold-schnorr/package.json +++ b/motoko/threshold-schnorr/package.json @@ -3,8 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@noble/ed25519": "^1.7.3", - "@noble/hashes": "^1.3.0", + "@noble/ed25519": "^2.0.0", "bitcoinjs-lib": "^6.1.5", "tiny-secp256k1": "^2.2.3" } diff --git a/motoko/threshold-schnorr/test.sh b/motoko/threshold-schnorr/test.sh index a501ff99f..08f301a35 100755 --- a/motoko/threshold-schnorr/test.sh +++ b/motoko/threshold-schnorr/test.sh @@ -6,112 +6,76 @@ function get_text_in_double_quotes() { echo -n "$1" | sed -e 's/^[^"]*"//' -e 's/"//g' -e 's/;//g' } -function test_impl() { - test -z "${1:-}" && echo "USAGE: $0 " && exit 1 +test -z "${1:-}" && echo "USAGE: $0 " && exit 1 - local message="$1" - echo "message=$message" - - local ed25519_sign_cmd="icp canister call backend sign '(\"${message}\" ,(variant { ed25519 }), null)'" - local ed25519_sig_raw_output - ed25519_sig_raw_output="$(eval "${ed25519_sign_cmd}" | grep signature_hex)" - local ed25519_sig_hex - ed25519_sig_hex="$(get_text_in_double_quotes "${ed25519_sig_raw_output}")" - echo "ed25519_signature_hex=$ed25519_sig_hex" +# BIP340 requires the message to be exactly 32 bytes. +# ed25519 accepts arbitrary-length messages. +# Use a 32-byte ASCII message so the same value works for all three algorithms. +message="${1}" +if [ ${#message} -ne 32 ]; then + echo "ERROR: message must be exactly 32 bytes for BIP340 (got ${#message})" + exit 1 +fi - local ed25519_public_key_raw_output - ed25519_public_key_raw_output="$(icp canister call backend public_key '(variant { ed25519 })' | grep public_key_hex)" - local ed25519_public_key_hex - ed25519_public_key_hex="$(get_text_in_double_quotes "${ed25519_public_key_raw_output}")" - echo "ed25519_public_key_hex=$ed25519_public_key_hex" +echo "message=$message" - node < sha512.sha512(ed25519.etc.concatBytes(...m)); +# --- ed25519 --- +echo "--- ed25519 ---" +ed25519_sign_cmd="icp canister call backend sign '(\"${message}\" ,(variant { ed25519 }), null)'" +ed25519_sig_hex=$(get_text_in_double_quotes "$(eval "${ed25519_sign_cmd}" | grep signature_hex)") +echo "ed25519_signature_hex=$ed25519_sig_hex" +ed25519_pubkey_hex=$(get_text_in_double_quotes "$(icp canister call backend public_key '(variant { ed25519 })' | grep public_key_hex)") +echo "ed25519_public_key_hex=$ed25519_pubkey_hex" - const sig = '${ed25519_sig_hex}'; - const pubkey = '${ed25519_public_key_hex}'; - const msg = Uint8Array.from(Buffer.from("${message}", 'utf8')); - console.log(await ed25519.verify(sig, msg, pubkey)); - } catch(err) { - console.error(err); - process.exit(1); - } - } - run(); +node < Date: Wed, 10 Jun 2026 21:35:22 +0200 Subject: [PATCH 05/11] fix: use 32-byte test message for BIP340 (was 31 bytes) Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/Makefile | 2 +- motoko/threshold-schnorr/test.sh | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/motoko/threshold-schnorr/Makefile b/motoko/threshold-schnorr/Makefile index 9823ecdff..4489cd0ef 100644 --- a/motoko/threshold-schnorr/Makefile +++ b/motoko/threshold-schnorr/Makefile @@ -27,4 +27,4 @@ test: @echo "=== Test 5/5: all three signature types verify cryptographically ===" @npm install --silent - @chmod +x test.sh && ./test.sh "hello world of BIP340 secp256k1" && echo "PASS" || (echo "FAIL" && exit 1) + @chmod +x test.sh && ./test.sh "hello world of BIP340-secp256k1!" && echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/threshold-schnorr/test.sh b/motoko/threshold-schnorr/test.sh index 08f301a35..2ea2c0b5f 100755 --- a/motoko/threshold-schnorr/test.sh +++ b/motoko/threshold-schnorr/test.sh @@ -8,16 +8,11 @@ function get_text_in_double_quotes() { test -z "${1:-}" && echo "USAGE: $0 " && exit 1 -# BIP340 requires the message to be exactly 32 bytes. -# ed25519 accepts arbitrary-length messages. -# Use a 32-byte ASCII message so the same value works for all three algorithms. +# BIP340 requires the message to be exactly 32 bytes; ed25519 accepts any length. +# Pass a 32-byte ASCII string so the same value works for all three algorithms. message="${1}" -if [ ${#message} -ne 32 ]; then - echo "ERROR: message must be exactly 32 bytes for BIP340 (got ${#message})" - exit 1 -fi -echo "message=$message" +echo "message=$message (${#message} bytes)" # --- ed25519 --- echo "--- ed25519 ---" From 53785dcad313cc66cbec3f7a3526cf835a456ab5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:38:25 +0200 Subject: [PATCH 06/11] fix: wire up Node built-in SHA512 for @noble/ed25519 v2 sync verify() Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/test.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/motoko/threshold-schnorr/test.sh b/motoko/threshold-schnorr/test.sh index 2ea2c0b5f..86afaf6d7 100755 --- a/motoko/threshold-schnorr/test.sh +++ b/motoko/threshold-schnorr/test.sh @@ -23,11 +23,14 @@ ed25519_pubkey_hex=$(get_text_in_double_quotes "$(icp canister call backend publ echo "ed25519_public_key_hex=$ed25519_pubkey_hex" node < new Uint8Array(crypto.createHash('sha512').update(ed.etc.concatBytes(...m)).digest()); const sig = Buffer.from("${ed25519_sig_hex}", "hex"); const pubkey = Buffer.from("${ed25519_pubkey_hex}", "hex"); const msg = Buffer.from("${message}", "utf8"); -const ok = verify(sig, msg, pubkey); +const ok = ed.verify(sig, msg, pubkey); console.log("ed25519 verified:", ok); if (!ok) { console.error("ed25519 verification FAILED"); process.exit(1); } END From dafc2497fcb131ab1692020a0e0b36ab1bcac2ad Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:51:53 +0200 Subject: [PATCH 07/11] chore: remove GitHub link and Overview heading; improve tests and README Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/Makefile | 2 +- motoko/threshold-schnorr/README.md | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/motoko/threshold-schnorr/Makefile b/motoko/threshold-schnorr/Makefile index 4489cd0ef..27b4bd7e9 100644 --- a/motoko/threshold-schnorr/Makefile +++ b/motoko/threshold-schnorr/Makefile @@ -14,7 +14,7 @@ test: echo "PASS" || (echo "FAIL" && exit 1) @echo "=== Test 3/5: sign returns signature_hex for bip340secp256k1 ===" - @result=$$(icp canister call backend sign '("hello world", variant { bip340secp256k1 }, null)') && \ + @result=$$(icp canister call backend sign '("hello world of BIP340-secp256k1!", variant { bip340secp256k1 }, null)') && \ echo "$$result" && \ echo "$$result" | grep -q 'signature_hex' && \ echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/threshold-schnorr/README.md b/motoko/threshold-schnorr/README.md index a7492edb2..cd59991af 100644 --- a/motoko/threshold-schnorr/README.md +++ b/motoko/threshold-schnorr/README.md @@ -1,9 +1,5 @@ # Threshold Schnorr -[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/threshold-schnorr) - -## Overview - This example demonstrates how to use the ICP [threshold Schnorr](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr) API from a Motoko canister. The canister acts as a signing oracle that creates Schnorr signatures using keys derived from the canister ID and the chosen algorithm (BIP340/BIP341 or Ed25519). Canisters do not hold private keys themselves — signing requests are routed to threshold Schnorr subnets that compute signatures using distributed key shares. ## Build and deploy from the command line @@ -47,16 +43,16 @@ The derivation path uses the caller's principal, so different callers receive di ## Signing -Call the `sign` method with a message, algorithm, and optional BIP341 tweak: +Call the `sign` method with a message, algorithm, and optional BIP341 tweak. For `bip340secp256k1`, the message must be exactly 32 bytes: ```bash icp canister call backend sign '("hello world", variant { ed25519 }, null)' -icp canister call backend sign '("hello world", variant { bip340secp256k1 }, null)' +icp canister call backend sign '("hello world of BIP340-secp256k1!", variant { bip340secp256k1 }, null)' ``` For BIP341 (Taproot) signing with a tweak: ```bash -icp canister call backend sign '("hello world", variant { bip340secp256k1 }, opt "012345678901234567890123456789012345678901234567890123456789abcd")' +icp canister call backend sign '("hello world of BIP340-secp256k1!", variant { bip340secp256k1 }, opt "012345678901234567890123456789012345678901234567890123456789abcd")' ``` ## Updating the Candid interface From e5c37d8f90f65e749c9528b0d5e84c0a9cfcdadb Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 22:00:41 +0200 Subject: [PATCH 08/11] chore: remove dfx_test_key references Co-Authored-By: Claude Sonnet 4.6 --- motoko/threshold-schnorr/README.md | 1 - motoko/threshold-schnorr/backend/app.mo | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/motoko/threshold-schnorr/README.md b/motoko/threshold-schnorr/README.md index cd59991af..5daaac530 100644 --- a/motoko/threshold-schnorr/README.md +++ b/motoko/threshold-schnorr/README.md @@ -26,7 +26,6 @@ icp network stop The canister uses `test_key_1` by default (mainnet test key). To use a different key, update the `key_id` value in `backend/app.mo`: -- `dfx_test_key`: local IC SDK deployment - `test_key_1`: mainnet test key - `key_1`: mainnet production key diff --git a/motoko/threshold-schnorr/backend/app.mo b/motoko/threshold-schnorr/backend/app.mo index 0fdee05d8..2dd6afdbc 100644 --- a/motoko/threshold-schnorr/backend/app.mo +++ b/motoko/threshold-schnorr/backend/app.mo @@ -20,7 +20,7 @@ actor ThresholdSchnorr { }; }; - transient let key_id : Text = "test_key_1"; // Use "key_1" for production and "dfx_test_key" locally + transient let key_id : Text = "test_key_1"; // Use "key_1" for mainnet production public shared ({ caller }) func public_key(algorithm : SchnorrAlgorithm) : async { #Ok : { public_key_hex : Text }; From 75f2590b6a33c007ed114ed62a74e91c548f5eb6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 00:45:39 +0200 Subject: [PATCH 09/11] chore: bump icp-dev-env to 0.3.2 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/threshold-schnorr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/threshold-schnorr.yml b/.github/workflows/threshold-schnorr.yml index 9f35fe751..60f44f5ea 100644 --- a/.github/workflows/threshold-schnorr.yml +++ b/.github/workflows/threshold-schnorr.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-threshold-schnorr: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 14f053e5ad12e3b6becf036e44256ccd9ab85c14 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 00:50:54 +0200 Subject: [PATCH 10/11] revert: back to icp-dev-env 0.3.1 (0.3.2 not yet on ghcr.io) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/threshold-schnorr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/threshold-schnorr.yml b/.github/workflows/threshold-schnorr.yml index 60f44f5ea..9f35fe751 100644 --- a/.github/workflows/threshold-schnorr.yml +++ b/.github/workflows/threshold-schnorr.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-threshold-schnorr: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 236af88cd11b4c65e660982eadf4668f934ee8d3 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 01:21:22 +0200 Subject: [PATCH 11/11] chore: bump icp-dev-env to 0.3.2 (images now published) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/threshold-schnorr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/threshold-schnorr.yml b/.github/workflows/threshold-schnorr.yml index 9f35fe751..60f44f5ea 100644 --- a/.github/workflows/threshold-schnorr.yml +++ b/.github/workflows/threshold-schnorr.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-threshold-schnorr: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: