From 7ce3486fb421414e8afb07569f3711bbee767891 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 15:36:35 -0600 Subject: [PATCH 1/7] feat: ICP hello-world project with Makefile driver --- Makefile | 36 +++++++++++ packages/icp-hello/.gitignore | 25 ++++++++ packages/icp-hello/README.md | 59 +++++++++++++++++++ packages/icp-hello/dfx.json | 16 +++++ .../icp-hello/src/icp-hello-backend/main.mo | 5 ++ 5 files changed, 141 insertions(+) create mode 100644 Makefile create mode 100644 packages/icp-hello/.gitignore create mode 100644 packages/icp-hello/README.md create mode 100644 packages/icp-hello/dfx.json create mode 100644 packages/icp-hello/src/icp-hello-backend/main.mo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..315880f --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +SHELL := /bin/bash + +DFX ?= $(HOME)/.local/share/dfx/bin/dfx +ICP_HELLO_DIR ?= packages/icp-hello +ICP_NETWORK ?= local +ICP_CANISTER ?= icp-hello-backend + +.PHONY: icp-check icp-new icp-replica-start icp-deploy icp-call icp-replica-stop icp-hello + +icp-check: + @test -x "$(DFX)" || (echo "dfx not found at $(DFX)"; exit 1) + @$(DFX) --version + +icp-new: icp-check + @if [ -d "$(ICP_HELLO_DIR)" ]; then \ + echo "exists: $(ICP_HELLO_DIR)"; \ + else \ + mkdir -p "$(dir $(ICP_HELLO_DIR))"; \ + cd "$(dir $(ICP_HELLO_DIR))" && "$(DFX)" new "$(notdir $(ICP_HELLO_DIR))" --type motoko --no-frontend; \ + fi + +icp-replica-start: icp-check + cd "$(ICP_HELLO_DIR)" && ( "$(DFX)" stop >/dev/null 2>&1 || true ) + cd "$(ICP_HELLO_DIR)" && "$(DFX)" start --clean --background + +icp-deploy: icp-check + cd "$(ICP_HELLO_DIR)" && "$(DFX)" deploy --network "$(ICP_NETWORK)" + +icp-call: icp-check + cd "$(ICP_HELLO_DIR)" && "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" greet '("ICP learner")' + +icp-replica-stop: icp-check + cd "$(ICP_HELLO_DIR)" && "$(DFX)" stop + +icp-hello: icp-new icp-replica-start icp-deploy icp-call + @echo "ICP hello-world done." diff --git a/packages/icp-hello/.gitignore b/packages/icp-hello/.gitignore new file mode 100644 index 0000000..49c89a1 --- /dev/null +++ b/packages/icp-hello/.gitignore @@ -0,0 +1,25 @@ +# Various IDEs and Editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temporary files +.dfx/ + +# generated files +**/declarations/ + +# rust +target/ + +# frontend code +node_modules/ +dist/ +.svelte-kit/ + +# environment variables +.env diff --git a/packages/icp-hello/README.md b/packages/icp-hello/README.md new file mode 100644 index 0000000..42d89c5 --- /dev/null +++ b/packages/icp-hello/README.md @@ -0,0 +1,59 @@ +# `icp-hello` + +Welcome to your new `icp-hello` project and to the Internet Computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle. + +To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens. + +To learn more before you start working with `icp-hello`, see the following documentation available online: + +- [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) +- [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) +- [Motoko Programming Language Guide](https://internetcomputer.org/docs/current/motoko/main/motoko) +- [Motoko Language Quick Reference](https://internetcomputer.org/docs/current/motoko/main/language-manual) + +If you want to start working on your project right away, you might want to try the following commands: + +```bash +cd icp-hello/ +dfx help +dfx canister --help +``` + +## Running the project locally + +If you want to test your project locally, you can use the following commands: + +```bash +# Starts the replica, running in the background +dfx start --background + +# Deploys your canisters to the replica and generates your candid interface +dfx deploy +``` + +Once the job completes, your application will be available at `http://localhost:4943?canisterId={asset_canister_id}`. + +If you have made changes to your backend canister, you can generate a new candid interface with + +```bash +npm run generate +``` + +at any time. This is recommended before starting the frontend development server, and will be run automatically any time you run `dfx deploy`. + +If you are making frontend changes, you can start a development server with + +```bash +npm start +``` + +Which will start a server at `http://localhost:8080`, proxying API requests to the replica at port 4943. + +### Note on frontend environment variables + +If you are hosting frontend code somewhere without using DFX, you may need to make one of the following adjustments to ensure your project does not fetch the root key in production: + +- set`DFX_NETWORK` to `ic` if you are using Webpack +- use your own preferred method to replace `process.env.DFX_NETWORK` in the autogenerated declarations + - Setting `canisters -> {asset_canister_id} -> declarations -> env_override to a string` in `dfx.json` will replace `process.env.DFX_NETWORK` with the string in the autogenerated declarations +- Write your own `createActor` constructor diff --git a/packages/icp-hello/dfx.json b/packages/icp-hello/dfx.json new file mode 100644 index 0000000..373c7ec --- /dev/null +++ b/packages/icp-hello/dfx.json @@ -0,0 +1,16 @@ +{ + "canisters": { + "icp-hello-backend": { + "main": "src/icp-hello-backend/main.mo", + "type": "motoko" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + "output_env_file": ".env", + "version": 1 +} \ No newline at end of file diff --git a/packages/icp-hello/src/icp-hello-backend/main.mo b/packages/icp-hello/src/icp-hello-backend/main.mo new file mode 100644 index 0000000..dedf488 --- /dev/null +++ b/packages/icp-hello/src/icp-hello-backend/main.mo @@ -0,0 +1,5 @@ +persistent actor { + public query func greet(name : Text) : async Text { + return "Hello, " # name # "!"; + }; +}; From d771948fbac870de0b27df28b2290e2cca616a75 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 15:51:48 -0600 Subject: [PATCH 2/7] Expand ICP demo to inter-canister byte messaging --- Makefile | 30 ++++++---- packages/icp-hello/README.md | 59 ------------------- packages/icp-hello/dfx.json | 16 ----- .../icp-hello/src/icp-hello-backend/main.mo | 5 -- .../.gitignore | 0 packages/icp-intercanister/README.md | 43 ++++++++++++++ packages/icp-intercanister/dfx.json | 23 ++++++++ .../src/byte_receiver/main.mo | 5 ++ .../icp-intercanister/src/byte_sender/main.mo | 7 +++ 9 files changed, 95 insertions(+), 93 deletions(-) delete mode 100644 packages/icp-hello/README.md delete mode 100644 packages/icp-hello/dfx.json delete mode 100644 packages/icp-hello/src/icp-hello-backend/main.mo rename packages/{icp-hello => icp-intercanister}/.gitignore (100%) create mode 100644 packages/icp-intercanister/README.md create mode 100644 packages/icp-intercanister/dfx.json create mode 100644 packages/icp-intercanister/src/byte_receiver/main.mo create mode 100644 packages/icp-intercanister/src/byte_sender/main.mo diff --git a/Makefile b/Makefile index 315880f..aa1b555 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,40 @@ SHELL := /bin/bash DFX ?= $(HOME)/.local/share/dfx/bin/dfx -ICP_HELLO_DIR ?= packages/icp-hello +ICP_DEMO_DIR ?= packages/icp-intercanister ICP_NETWORK ?= local -ICP_CANISTER ?= icp-hello-backend +ICP_CANISTER ?= byte_sender +ICP_BYTES ?= '(vec { 73; 67; 80; 32; 111; 99; 97; 112 })' -.PHONY: icp-check icp-new icp-replica-start icp-deploy icp-call icp-replica-stop icp-hello +.PHONY: icp-check icp-new icp-replica-start icp-deploy icp-call icp-replica-stop icp-hello icp-intercanister icp-check: @test -x "$(DFX)" || (echo "dfx not found at $(DFX)"; exit 1) @$(DFX) --version icp-new: icp-check - @if [ -d "$(ICP_HELLO_DIR)" ]; then \ - echo "exists: $(ICP_HELLO_DIR)"; \ + @if [ -f "$(ICP_DEMO_DIR)/dfx.json" ]; then \ + echo "exists: $(ICP_DEMO_DIR)"; \ else \ - mkdir -p "$(dir $(ICP_HELLO_DIR))"; \ - cd "$(dir $(ICP_HELLO_DIR))" && "$(DFX)" new "$(notdir $(ICP_HELLO_DIR))" --type motoko --no-frontend; \ + echo "missing $(ICP_DEMO_DIR)/dfx.json"; \ + exit 1; \ fi icp-replica-start: icp-check - cd "$(ICP_HELLO_DIR)" && ( "$(DFX)" stop >/dev/null 2>&1 || true ) - cd "$(ICP_HELLO_DIR)" && "$(DFX)" start --clean --background + cd "$(ICP_DEMO_DIR)" && ( "$(DFX)" stop >/dev/null 2>&1 || true ) + cd "$(ICP_DEMO_DIR)" && "$(DFX)" start --clean --background icp-deploy: icp-check - cd "$(ICP_HELLO_DIR)" && "$(DFX)" deploy --network "$(ICP_NETWORK)" + cd "$(ICP_DEMO_DIR)" && "$(DFX)" deploy --network "$(ICP_NETWORK)" icp-call: icp-check - cd "$(ICP_HELLO_DIR)" && "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" greet '("ICP learner")' + cd "$(ICP_DEMO_DIR)" && "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" send "$(ICP_BYTES)" icp-replica-stop: icp-check - cd "$(ICP_HELLO_DIR)" && "$(DFX)" stop + cd "$(ICP_DEMO_DIR)" && "$(DFX)" stop icp-hello: icp-new icp-replica-start icp-deploy icp-call - @echo "ICP hello-world done." + @echo "ICP onboarding/inter-canister demo done." + +icp-intercanister: icp-new icp-replica-start icp-deploy icp-call + @echo "ICP inter-canister demo done." diff --git a/packages/icp-hello/README.md b/packages/icp-hello/README.md deleted file mode 100644 index 42d89c5..0000000 --- a/packages/icp-hello/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# `icp-hello` - -Welcome to your new `icp-hello` project and to the Internet Computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle. - -To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens. - -To learn more before you start working with `icp-hello`, see the following documentation available online: - -- [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) -- [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) -- [Motoko Programming Language Guide](https://internetcomputer.org/docs/current/motoko/main/motoko) -- [Motoko Language Quick Reference](https://internetcomputer.org/docs/current/motoko/main/language-manual) - -If you want to start working on your project right away, you might want to try the following commands: - -```bash -cd icp-hello/ -dfx help -dfx canister --help -``` - -## Running the project locally - -If you want to test your project locally, you can use the following commands: - -```bash -# Starts the replica, running in the background -dfx start --background - -# Deploys your canisters to the replica and generates your candid interface -dfx deploy -``` - -Once the job completes, your application will be available at `http://localhost:4943?canisterId={asset_canister_id}`. - -If you have made changes to your backend canister, you can generate a new candid interface with - -```bash -npm run generate -``` - -at any time. This is recommended before starting the frontend development server, and will be run automatically any time you run `dfx deploy`. - -If you are making frontend changes, you can start a development server with - -```bash -npm start -``` - -Which will start a server at `http://localhost:8080`, proxying API requests to the replica at port 4943. - -### Note on frontend environment variables - -If you are hosting frontend code somewhere without using DFX, you may need to make one of the following adjustments to ensure your project does not fetch the root key in production: - -- set`DFX_NETWORK` to `ic` if you are using Webpack -- use your own preferred method to replace `process.env.DFX_NETWORK` in the autogenerated declarations - - Setting `canisters -> {asset_canister_id} -> declarations -> env_override to a string` in `dfx.json` will replace `process.env.DFX_NETWORK` with the string in the autogenerated declarations -- Write your own `createActor` constructor diff --git a/packages/icp-hello/dfx.json b/packages/icp-hello/dfx.json deleted file mode 100644 index 373c7ec..0000000 --- a/packages/icp-hello/dfx.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "canisters": { - "icp-hello-backend": { - "main": "src/icp-hello-backend/main.mo", - "type": "motoko" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/packages/icp-hello/src/icp-hello-backend/main.mo b/packages/icp-hello/src/icp-hello-backend/main.mo deleted file mode 100644 index dedf488..0000000 --- a/packages/icp-hello/src/icp-hello-backend/main.mo +++ /dev/null @@ -1,5 +0,0 @@ -persistent actor { - public query func greet(name : Text) : async Text { - return "Hello, " # name # "!"; - }; -}; diff --git a/packages/icp-hello/.gitignore b/packages/icp-intercanister/.gitignore similarity index 100% rename from packages/icp-hello/.gitignore rename to packages/icp-intercanister/.gitignore diff --git a/packages/icp-intercanister/README.md b/packages/icp-intercanister/README.md new file mode 100644 index 0000000..7e26f74 --- /dev/null +++ b/packages/icp-intercanister/README.md @@ -0,0 +1,43 @@ +# ICP Inter-Canister Byte Demo + +This package is a local ICP onboarding prototype with two Motoko canisters: + +- `byte_receiver`: receives `vec nat8` and returns it unchanged. +- `byte_sender`: calls `byte_receiver.accept(...)` and returns the remote result. + +The goal is to validate inter-canister messaging with an arbitrary byte sequence. + +## Layout + +- `src/byte_receiver/main.mo` +- `src/byte_sender/main.mo` +- `dfx.json` + +## Local Run + +From repo root: + +```bash +make icp-intercanister +``` + +This will: + +1. start local replica +2. deploy both canisters +3. call `byte_sender.send(...)` with a sample byte payload + +Stop local replica when done: + +```bash +make icp-replica-stop +``` + +## Manual Call Example + +```bash +cd packages/icp-intercanister +~/.local/share/dfx/bin/dfx canister call --network local byte_sender send '(vec { 73; 67; 80; 32; 111; 99; 97; 112 })' +``` + +Expected response is the same byte vector echoed back. diff --git a/packages/icp-intercanister/dfx.json b/packages/icp-intercanister/dfx.json new file mode 100644 index 0000000..35fe6cd --- /dev/null +++ b/packages/icp-intercanister/dfx.json @@ -0,0 +1,23 @@ +{ + "canisters": { + "byte_receiver": { + "main": "src/byte_receiver/main.mo", + "type": "motoko" + }, + "byte_sender": { + "dependencies": [ + "byte_receiver" + ], + "main": "src/byte_sender/main.mo", + "type": "motoko" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + "output_env_file": ".env", + "version": 1 +} diff --git a/packages/icp-intercanister/src/byte_receiver/main.mo b/packages/icp-intercanister/src/byte_receiver/main.mo new file mode 100644 index 0000000..09dcf99 --- /dev/null +++ b/packages/icp-intercanister/src/byte_receiver/main.mo @@ -0,0 +1,5 @@ +persistent actor { + public func accept(bytes : [Nat8]) : async [Nat8] { + bytes + }; +}; diff --git a/packages/icp-intercanister/src/byte_sender/main.mo b/packages/icp-intercanister/src/byte_sender/main.mo new file mode 100644 index 0000000..4b9dd22 --- /dev/null +++ b/packages/icp-intercanister/src/byte_sender/main.mo @@ -0,0 +1,7 @@ +import Receiver "canister:byte_receiver"; + +persistent actor { + public func send(bytes : [Nat8]) : async [Nat8] { + await Receiver.accept(bytes) + }; +}; From 9bfeb855b9fec41b9f58c546c6c595f7baf6cf9d Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 20:55:04 -0600 Subject: [PATCH 3/7] feat(icp-intercanister): add two-canister OCapN deliver-with-resolver prototype --- .../src/byte_receiver/main.mo | 26 ++++++++++++++++ .../icp-intercanister/src/byte_sender/main.mo | 28 +++++++++++++++++ packages/icp-intercanister/src/ocapn_types.mo | 30 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 packages/icp-intercanister/src/ocapn_types.mo diff --git a/packages/icp-intercanister/src/byte_receiver/main.mo b/packages/icp-intercanister/src/byte_receiver/main.mo index 09dcf99..e904f75 100644 --- a/packages/icp-intercanister/src/byte_receiver/main.mo +++ b/packages/icp-intercanister/src/byte_receiver/main.mo @@ -1,5 +1,31 @@ +import Ocapn "../ocapn_types"; + persistent actor { public func accept(bytes : [Nat8]) : async [Nat8] { bytes }; + + /// Minimal selected-case behavior for + /// `op_delivers::OpDeliverTest::test_deliver_with_resolver`. + public func ocapn_handle(message : Ocapn.OcapnMessage) : async Ocapn.OcapnResolution { + let resolver = switch (message.resolve_me_desc) { + case (?desc) desc; + case null "missing-resolver"; + }; + + switch (message.op) { + case (#deliver) { + { + resolver; + args = [#symbol("fulfill"), #list(message.args)]; + }; + }; + case (#deliver_only) { + { + resolver; + args = [#symbol("break"), #text("deliver-only has no response in this prototype")]; + }; + }; + }; + }; }; diff --git a/packages/icp-intercanister/src/byte_sender/main.mo b/packages/icp-intercanister/src/byte_sender/main.mo index 4b9dd22..00e9bd4 100644 --- a/packages/icp-intercanister/src/byte_sender/main.mo +++ b/packages/icp-intercanister/src/byte_sender/main.mo @@ -1,7 +1,35 @@ +import Blob "mo:base/Blob"; import Receiver "canister:byte_receiver"; +import Ocapn "../ocapn_types"; persistent actor { public func send(bytes : [Nat8]) : async [Nat8] { await Receiver.accept(bytes) }; + + private func selectedCaseArgs() : [Ocapn.OcapnValue] { + [ + #text("foo"), + #int(1), + #bool(false), + #bytes(Blob.fromArray([98, 97, 114])), + #list([#text("baz")]), + ]; + }; + + public func ocapn_deliver_with_resolver_ok() : async Bool { + let args = selectedCaseArgs(); + let message : Ocapn.OcapnMessage = { + op = #deliver; + args; + resolve_me_desc = ?"$0"; + }; + + let resolution = await Receiver.ocapn_handle(message); + let expected : Ocapn.OcapnResolution = { + resolver = "$0"; + args = [#symbol("fulfill"), #list(args)]; + }; + resolution == expected + }; }; diff --git a/packages/icp-intercanister/src/ocapn_types.mo b/packages/icp-intercanister/src/ocapn_types.mo new file mode 100644 index 0000000..a2cf724 --- /dev/null +++ b/packages/icp-intercanister/src/ocapn_types.mo @@ -0,0 +1,30 @@ +import Blob "mo:base/Blob"; + +module { + /* + * Refs: + * - https://github.com/ocapn/ocapn-test-suite/blob/main/tests/op_delivers.py + * (`OpDeliverTest::test_deliver_with_resolver`) + * - https://github.com/ocapn/ocapn/issues/3#issuecomment-1552187671 + */ + public type OcapnValue = { + #unit; + #bool : Bool; + #int : Int; + #text : Text; + #symbol : Text; + #bytes : Blob; + #list : [OcapnValue]; + }; + + public type OcapnMessage = { + op : { #deliver; #deliver_only }; + args : [OcapnValue]; + resolve_me_desc : ?Text; + }; + + public type OcapnResolution = { + resolver : Text; + args : [OcapnValue]; + }; +}; From 515bffdc736d1b580316f0cffead98e17057cf99 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 20:55:04 -0600 Subject: [PATCH 4/7] build/docs: add ocapn make targets, replica port preflight, and README updates --- Makefile | 36 +++++++--- packages/icp-intercanister/CONTRIBUTING.md | 24 +++++++ packages/icp-intercanister/README.md | 80 +++++++++++++++++++--- 3 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 packages/icp-intercanister/CONTRIBUTING.md diff --git a/Makefile b/Makefile index aa1b555..efa57f8 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,17 @@ SHELL := /bin/bash DFX ?= $(HOME)/.local/share/dfx/bin/dfx +DFX_ENV ?= TERM=xterm-256color ICP_DEMO_DIR ?= packages/icp-intercanister ICP_NETWORK ?= local ICP_CANISTER ?= byte_sender -ICP_BYTES ?= '(vec { 73; 67; 80; 32; 111; 99; 97; 112 })' +ICP_BYTES ?= (vec { 73; 67; 80; 32; 111; 99; 97; 112 }) -.PHONY: icp-check icp-new icp-replica-start icp-deploy icp-call icp-replica-stop icp-hello icp-intercanister +.PHONY: icp-check icp-new icp-port-check icp-replica-start icp-deploy icp-call icp-ocapn-case icp-replica-stop icp-hello icp-intercanister icp-ocapn-deliver-with-resolver ocapn icp-check: @test -x "$(DFX)" || (echo "dfx not found at $(DFX)"; exit 1) - @$(DFX) --version + @$(DFX_ENV) "$(DFX)" --version icp-new: icp-check @if [ -f "$(ICP_DEMO_DIR)/dfx.json" ]; then \ @@ -20,21 +21,40 @@ icp-new: icp-check exit 1; \ fi +icp-port-check: + @if command -v lsof >/dev/null 2>&1; then \ + if lsof -nP -iTCP:4943 -sTCP:LISTEN >/dev/null 2>&1; then \ + echo "port 4943 already in use; dfx cannot start local replica."; \ + lsof -nP -iTCP:4943 -sTCP:LISTEN; \ + echo "stop that process (or kill its PID) and retry."; \ + exit 1; \ + fi; \ + fi + icp-replica-start: icp-check - cd "$(ICP_DEMO_DIR)" && ( "$(DFX)" stop >/dev/null 2>&1 || true ) - cd "$(ICP_DEMO_DIR)" && "$(DFX)" start --clean --background + cd "$(ICP_DEMO_DIR)" && ( $(DFX_ENV) "$(DFX)" stop >/dev/null 2>&1 || true ) + @$(MAKE) icp-port-check + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" start --clean --background icp-deploy: icp-check - cd "$(ICP_DEMO_DIR)" && "$(DFX)" deploy --network "$(ICP_NETWORK)" + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" deploy --network "$(ICP_NETWORK)" icp-call: icp-check - cd "$(ICP_DEMO_DIR)" && "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" send "$(ICP_BYTES)" + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" send '$(ICP_BYTES)' + +icp-ocapn-case: icp-check + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" canister call --network "$(ICP_NETWORK)" "byte_sender" ocapn_deliver_with_resolver_ok icp-replica-stop: icp-check - cd "$(ICP_DEMO_DIR)" && "$(DFX)" stop + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" stop icp-hello: icp-new icp-replica-start icp-deploy icp-call @echo "ICP onboarding/inter-canister demo done." icp-intercanister: icp-new icp-replica-start icp-deploy icp-call @echo "ICP inter-canister demo done." + +icp-ocapn-deliver-with-resolver: icp-new icp-replica-start icp-deploy icp-ocapn-case + @echo "ICP OCapN selected-case demo done." + +ocapn: icp-ocapn-deliver-with-resolver diff --git a/packages/icp-intercanister/CONTRIBUTING.md b/packages/icp-intercanister/CONTRIBUTING.md new file mode 100644 index 0000000..419acb7 --- /dev/null +++ b/packages/icp-intercanister/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing (ICP Inter-Canister Prototype) + +## Interop Constraint Rule + +This package models protocol-facing data. Type and wire-shape changes can break +interoperability even when local tests pass. + +When you add or change protocol/message/value types (for example in +`src/ocapn_types.mo`), you must: + +1. cite the external constraint source in code comments near the changed type +2. include the source link in the PR/commit message +3. explain whether the change is: + - required by an external spec/test suite, or + - a local prototype choice that is explicitly temporary + +Minimum acceptable sources: + +- OCapN spec/drafts (`ocapn/ocapn`) +- OCapN test suite behavior (`ocapn/ocapn-test-suite`) +- ICP/Candid reference docs (for wire-level Candid choices) + +If no external source exists yet, add a `TODO(spec-gap)` comment and describe +the interoperability risk. diff --git a/packages/icp-intercanister/README.md b/packages/icp-intercanister/README.md index 7e26f74..9ef3cd7 100644 --- a/packages/icp-intercanister/README.md +++ b/packages/icp-intercanister/README.md @@ -1,11 +1,20 @@ -# ICP Inter-Canister Byte Demo +# OCapN-on-ICP Inter-Canister Prototype -This package is a local ICP onboarding prototype with two Motoko canisters: +This package explores one concrete OCapN interoperability goal on ICP: -- `byte_receiver`: receives `vec nat8` and returns it unchanged. -- `byte_sender`: calls `byte_receiver.accept(...)` and returns the remote result. +- implement the selected OCapN deliver test behavior + (`op_delivers::OpDeliverTest::test_deliver_with_resolver`) +- represent OCapN-style messages/values in Motoko +- execute the scenario across two canisters on a local ICP replica -The goal is to validate inter-canister messaging with an arbitrary byte sequence. +The inter-canister plumbing in this package is in service of that goal. + +Current canisters: + +- `byte_receiver`: OCapN-message receiver for the selected-case behavior, plus a byte echo method. +- `byte_sender`: test driver that performs inter-canister calls and validates expected resolution shape. + +Before changing protocol-facing types, read `CONTRIBUTING.md`. ## Layout @@ -13,7 +22,7 @@ The goal is to validate inter-canister messaging with an arbitrary byte sequence - `src/byte_sender/main.mo` - `dfx.json` -## Local Run +## Run From repo root: @@ -21,11 +30,35 @@ From repo root: make icp-intercanister ``` -This will: +This command: 1. start local replica 2. deploy both canisters -3. call `byte_sender.send(...)` with a sample byte payload +3. calls `byte_sender.send(...)` with a sample byte payload + +Run the selected OCapN deliver-with-resolver case between canisters: + +```bash +make icp-ocapn-deliver-with-resolver +``` + +This package targets an +[OCapN test-suite](https://github.com/ocapn/ocapn-test-suite) case: + +- `tests/op_delivers.py::OpDeliverTest::test_deliver_with_resolver` + +In this prototype, that case is exercised by calling +`byte_sender.ocapn_deliver_with_resolver_ok`, which expects: + +```candid +(true) +``` + +The selected-case method models: + +- message: `op:deliver` +- args: `["foo", 1, false, b"bar", ["baz"]]` +- expected resolution args: `[symbol("fulfill"), original_args]` Stop local replica when done: @@ -41,3 +74,34 @@ cd packages/icp-intercanister ``` Expected response is the same byte vector echoed back. + +Manual selected-case call: + +```bash +cd packages/icp-intercanister +TERM=xterm-256color ~/.local/share/dfx/bin/dfx canister call --network local byte_sender ocapn_deliver_with_resolver_ok +``` + +## Where Local State Lives + +Documented by ICP `dfx start` docs: + +- shared local network data root (Linux): + - `$HOME/.local/share/dfx/network/local` +- project-specific local network data root: + - `/.dfx/network/local` + +Source: + +- https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-start + +- Project-local `.dfx/`: + - build outputs, generated interfaces, local canister IDs +- Replica runtime internals under the shared-root path above: + - observed on this setup (`dfx 0.30.2`) under subpaths like + `.../state/replicated_state/...` + - exact internal directory/file layout is implementation detail and may change + across `dfx`/replica versions. + +So if you are looking for raw local canister state, inspect the shared local +network data root as well as project `.dfx`. From 3a6af339dcbde25dbe95ad741d00fb88b24eb893 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 22:59:32 -0600 Subject: [PATCH 5/7] feat(icp-intercanister): add OCapN protocol/boundary and selected-case flow - define OCapN protocol message and resolution structures in Motoko - separate transport-facing VatTP boundary from protocol message modeling - implement selected-case inter-canister deliver-with-resolver flow over handleInbound - add reusable ref_send helper module for channel/envelope send mechanics - simplify sender/receiver public surface around handleInbound for prototype path - update Makefile targets for replica/deploy/call flow of the selected-case demo - update README to describe OCapN-first prototype goals, references, and run path --- Makefile | 4 +- packages/icp-intercanister/README.md | 22 +++--- .../src/byte_receiver/main.mo | 50 +++++++++---- .../icp-intercanister/src/byte_sender/main.mo | 48 +++++++++--- .../icp-intercanister/src/ocapn_protocol.mo | 73 +++++++++++++++++++ packages/icp-intercanister/src/ocapn_types.mo | 11 --- packages/icp-intercanister/src/ref_send.mo | 49 +++++++++++++ .../icp-intercanister/src/vattp_boundary.mo | 67 +++++++++++++++++ 8 files changed, 274 insertions(+), 50 deletions(-) create mode 100644 packages/icp-intercanister/src/ocapn_protocol.mo create mode 100644 packages/icp-intercanister/src/ref_send.mo create mode 100644 packages/icp-intercanister/src/vattp_boundary.mo diff --git a/Makefile b/Makefile index efa57f8..e11f351 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,6 @@ DFX ?= $(HOME)/.local/share/dfx/bin/dfx DFX_ENV ?= TERM=xterm-256color ICP_DEMO_DIR ?= packages/icp-intercanister ICP_NETWORK ?= local -ICP_CANISTER ?= byte_sender -ICP_BYTES ?= (vec { 73; 67; 80; 32; 111; 99; 97; 112 }) .PHONY: icp-check icp-new icp-port-check icp-replica-start icp-deploy icp-call icp-ocapn-case icp-replica-stop icp-hello icp-intercanister icp-ocapn-deliver-with-resolver ocapn @@ -40,7 +38,7 @@ icp-deploy: icp-check cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" deploy --network "$(ICP_NETWORK)" icp-call: icp-check - cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" canister call --network "$(ICP_NETWORK)" "$(ICP_CANISTER)" send '$(ICP_BYTES)' + cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" canister call --network "$(ICP_NETWORK)" "byte_sender" ocapn_deliver_with_resolver_ok icp-ocapn-case: icp-check cd "$(ICP_DEMO_DIR)" && $(DFX_ENV) "$(DFX)" canister call --network "$(ICP_NETWORK)" "byte_sender" ocapn_deliver_with_resolver_ok diff --git a/packages/icp-intercanister/README.md b/packages/icp-intercanister/README.md index 9ef3cd7..3eb33f3 100644 --- a/packages/icp-intercanister/README.md +++ b/packages/icp-intercanister/README.md @@ -11,8 +11,8 @@ The inter-canister plumbing in this package is in service of that goal. Current canisters: -- `byte_receiver`: OCapN-message receiver for the selected-case behavior, plus a byte echo method. -- `byte_sender`: test driver that performs inter-canister calls and validates expected resolution shape. +- `byte_receiver`: inbound handler canister; `handleInbound` is the sole public entrypoint. +- `byte_sender`: test driver that performs inter-canister calls, validates expected resolution shape, and also supports `handleInbound`. Before changing protocol-facing types, read `CONTRIBUTING.md`. @@ -20,6 +20,8 @@ Before changing protocol-facing types, read `CONTRIBUTING.md`. - `src/byte_receiver/main.mo` - `src/byte_sender/main.mo` +- `src/vattp_boundary.mo` (transport/session boundary interfaces) +- `src/ocapn_protocol.mo` (OCapN/CapTP message and reference-lifecycle boundary interfaces) - `dfx.json` ## Run @@ -34,7 +36,7 @@ This command: 1. start local replica 2. deploy both canisters -3. calls `byte_sender.send(...)` with a sample byte payload +3. calls `byte_sender.ocapn_deliver_with_resolver_ok` Run the selected OCapN deliver-with-resolver case between canisters: @@ -59,6 +61,7 @@ The selected-case method models: - message: `op:deliver` - args: `["foo", 1, false, b"bar", ["baz"]]` - expected resolution args: `[symbol("fulfill"), original_args]` +- transport in this prototype path: `to_candid` / `from_candid` bytes for `OcapnMessage` and `OcapnResolution` Stop local replica when done: @@ -66,20 +69,17 @@ Stop local replica when done: make icp-replica-stop ``` -## Manual Call Example +## Manual Call ```bash cd packages/icp-intercanister -~/.local/share/dfx/bin/dfx canister call --network local byte_sender send '(vec { 73; 67; 80; 32; 111; 99; 97; 112 })' +TERM=xterm-256color ~/.local/share/dfx/bin/dfx canister call --network local byte_sender ocapn_deliver_with_resolver_ok ``` -Expected response is the same byte vector echoed back. - -Manual selected-case call: +Expected response: -```bash -cd packages/icp-intercanister -TERM=xterm-256color ~/.local/share/dfx/bin/dfx canister call --network local byte_sender ocapn_deliver_with_resolver_ok +```candid +(true) ``` ## Where Local State Lives diff --git a/packages/icp-intercanister/src/byte_receiver/main.mo b/packages/icp-intercanister/src/byte_receiver/main.mo index e904f75..b25485f 100644 --- a/packages/icp-intercanister/src/byte_receiver/main.mo +++ b/packages/icp-intercanister/src/byte_receiver/main.mo @@ -1,31 +1,53 @@ -import Ocapn "../ocapn_types"; +import OcapnProtocol "../ocapn_protocol"; +import OcapnTypes "../ocapn_types"; +import Vattp "../vattp_boundary"; persistent actor { - public func accept(bytes : [Nat8]) : async [Nat8] { - bytes + // Minimal echo callable; `call` mirrors callable-object naming conventions. + func call(args : [OcapnTypes.OcapnValue]) : [OcapnTypes.OcapnValue] { + args }; /// Minimal selected-case behavior for /// `op_delivers::OpDeliverTest::test_deliver_with_resolver`. - public func ocapn_handle(message : Ocapn.OcapnMessage) : async Ocapn.OcapnResolution { - let resolver = switch (message.resolve_me_desc) { - case (?desc) desc; - case null "missing-resolver"; - }; - - switch (message.op) { - case (#deliver) { + func ocapn_handle( + message : OcapnProtocol.OcapnMessage + ) : async OcapnProtocol.OcapnResolution { + switch (message) { + case (#deliver(deliver)) { + let resolver = switch (deliver.resolve_me_desc) { + case (?desc) desc; + case null "missing-resolver"; + }; { resolver; - args = [#symbol("fulfill"), #list(message.args)]; + args = [#symbol("fulfill"), #list(call(deliver.args))]; }; }; - case (#deliver_only) { + case (#deliver_only(_)) { { - resolver; + resolver = "deliver-only"; args = [#symbol("break"), #text("deliver-only has no response in this prototype")]; }; }; + case (_) { + { + resolver = "unsupported-op"; + args = [#symbol("break"), #text("unsupported op for selected-case prototype")]; + }; + }; + }; + }; + + public func handleInbound( + envelope : Vattp.TurnEnvelope + ) : async Vattp.TurnResult { + let parsed : ?OcapnProtocol.OcapnMessage = from_candid(envelope.payload); + switch (parsed) { + case (?message) #ok(to_candid(await ocapn_handle(message))); + // Fallback keeps the byte-echo prototype path alive while handleInbound + // is the sole public entrypoint. + case null #ok(envelope.payload); }; }; }; diff --git a/packages/icp-intercanister/src/byte_sender/main.mo b/packages/icp-intercanister/src/byte_sender/main.mo index 00e9bd4..8ad7360 100644 --- a/packages/icp-intercanister/src/byte_sender/main.mo +++ b/packages/icp-intercanister/src/byte_sender/main.mo @@ -1,13 +1,20 @@ import Blob "mo:base/Blob"; +import Principal "mo:base/Principal"; import Receiver "canister:byte_receiver"; -import Ocapn "../ocapn_types"; +import OcapnProtocol "../ocapn_protocol"; +import OcapnTypes "../ocapn_types"; +import RefSend "../ref_send"; +import Vattp "../vattp_boundary"; persistent actor { - public func send(bytes : [Nat8]) : async [Nat8] { - await Receiver.accept(bytes) + let theChannel : RefSend.Channel = { + peerPrincipal = Principal.fromActor(Receiver); + sessionId = "selected-case"; + channelId = "selected-case"; + var turnId : Nat64 = 1; }; - private func selectedCaseArgs() : [Ocapn.OcapnValue] { + private func selectedCaseArgs() : [OcapnTypes.OcapnValue] { [ #text("foo"), #int(1), @@ -18,18 +25,37 @@ persistent actor { }; public func ocapn_deliver_with_resolver_ok() : async Bool { - let args = selectedCaseArgs(); - let message : Ocapn.OcapnMessage = { - op = #deliver; - args; - resolve_me_desc = ?"$0"; + let echoProxy = object { + let myChannel = theChannel; + let to = "echo-gc"; + + public func call( + args : [OcapnTypes.OcapnValue], + resolveMeDesc : ?Text, + ) : async OcapnProtocol.OcapnResolution { + let message : OcapnProtocol.OcapnMessage = #deliver({ + to; + args; + answerPosition = null; + resolve_me_desc = resolveMeDesc; + }); + await RefSend.sendMessage(myChannel, message); + }; }; - let resolution = await Receiver.ocapn_handle(message); - let expected : Ocapn.OcapnResolution = { + let args = selectedCaseArgs(); + let resolution = await echoProxy.call(args, ?"$0"); + + let expected : OcapnProtocol.OcapnResolution = { resolver = "$0"; args = [#symbol("fulfill"), #list(args)]; }; resolution == expected }; + + public func handleInbound( + _envelope : Vattp.TurnEnvelope + ) : async Vattp.TurnResult { + #err("byte_sender does not handle inbound protocol messages in this prototype") + }; }; diff --git a/packages/icp-intercanister/src/ocapn_protocol.mo b/packages/icp-intercanister/src/ocapn_protocol.mo new file mode 100644 index 0000000..c38addf --- /dev/null +++ b/packages/icp-intercanister/src/ocapn_protocol.mo @@ -0,0 +1,73 @@ +import OcapnTypes "ocapn_types"; + +module { + /* + * Refs: + * - OCapN test suite: + * https://github.com/ocapn/ocapn-test-suite + * - OCapN netlayer draft: + * https://github.com/ocapn/ocapn/blob/main/draft-specifications/Netlayers.md + * + * OCapN/CapTP protocol data structures (messages only). + * No method interfaces are defined here. + */ + + public type RefId = Text; + public type Desc = Text; + public type PublicKey = Blob; + public type Location = Text; + public type Signature = Blob; + + public type OpStartSession = { + captpVersion : Text; + sessionPubkey : PublicKey; + location : Location; + locationSig : Signature; + }; + + public type OpListen = { + to : Desc; + resolveMeDesc : Desc; + }; + + public type OpDeliverOnly = { + to : Desc; + args : [OcapnTypes.OcapnValue]; + }; + + public type OpDeliver = { + to : Desc; + args : [OcapnTypes.OcapnValue]; + answerPosition : ?Nat; + resolve_me_desc : ?Text; + }; + + public type OpAbort = { + reason : Text; + }; + + public type OpGcExport = { + exportPosition : Nat; + wireDelta : Int; + }; + + public type OpGcAnswer = { + answerPosition : Nat; + }; + + // Top-level OCapN/CapTP protocol messages. + public type OcapnMessage = { + #start_session : OpStartSession; + #listen : OpListen; + #deliver_only : OpDeliverOnly; + #deliver : OpDeliver; + #abort : OpAbort; + #gc_export : OpGcExport; + #gc_answer : OpGcAnswer; + }; + + public type OcapnResolution = { + resolver : Text; + args : [OcapnTypes.OcapnValue]; + }; +}; diff --git a/packages/icp-intercanister/src/ocapn_types.mo b/packages/icp-intercanister/src/ocapn_types.mo index a2cf724..f33def7 100644 --- a/packages/icp-intercanister/src/ocapn_types.mo +++ b/packages/icp-intercanister/src/ocapn_types.mo @@ -16,15 +16,4 @@ module { #bytes : Blob; #list : [OcapnValue]; }; - - public type OcapnMessage = { - op : { #deliver; #deliver_only }; - args : [OcapnValue]; - resolve_me_desc : ?Text; - }; - - public type OcapnResolution = { - resolver : Text; - args : [OcapnValue]; - }; }; diff --git a/packages/icp-intercanister/src/ref_send.mo b/packages/icp-intercanister/src/ref_send.mo new file mode 100644 index 0000000..4650033 --- /dev/null +++ b/packages/icp-intercanister/src/ref_send.mo @@ -0,0 +1,49 @@ +import Principal "mo:base/Principal"; +import OcapnProtocol "./ocapn_protocol"; +import OcapnTypes "./ocapn_types"; +import Vattp "./vattp_boundary"; + +/// Refs: +/// - E `Ref.send` abstraction in e-on-java: +/// https://github.com/kpreid/e-on-java/tree/master/src/jsrc/org/erights/e/elib/ref +/// - OCapN message model (`deliver`, `deliver-only`, resolution): +/// https://github.com/ocapn/ocapn/blob/main/draft-specifications/Protocol.md +/// - OCapN selected test case (`deliver_with_resolver`): +/// https://github.com/ocapn/ocapn-test-suite/blob/main/tests/op_delivers.py +module { + public type Channel = { + peerPrincipal : Principal; + sessionId : Text; + channelId : Text; + var turnId : Nat64; + }; + + public func sendMessage( + channel : Channel, + message : OcapnProtocol.OcapnMessage, + ) : async OcapnProtocol.OcapnResolution { + let turnId = channel.turnId; + channel.turnId += 1; + let peer : Vattp.VattpBoundary = actor (Principal.toText(channel.peerPrincipal)); + let envelope : Vattp.TurnEnvelope = { + sessionId = channel.sessionId; + channelId = channel.channelId; + turnId; + payload = to_candid(message); + }; + let turnResult = await peer.handleInbound(envelope); + let resolutionDecoded : ?OcapnProtocol.OcapnResolution = switch (turnResult) { + case (#ok(payload)) from_candid(payload); + case (#err(_)) null; + }; + switch (resolutionDecoded) { + case (?value) value; + case null { + { + resolver = "decode-error"; + args = [#symbol("break"), #text("unable to decode OcapnResolution from candid bytes")]; + }; + }; + }; + }; +}; diff --git a/packages/icp-intercanister/src/vattp_boundary.mo b/packages/icp-intercanister/src/vattp_boundary.mo new file mode 100644 index 0000000..9a01e71 --- /dev/null +++ b/packages/icp-intercanister/src/vattp_boundary.mo @@ -0,0 +1,67 @@ +import Principal "mo:base/Principal"; +import Blob "mo:base/Blob"; + +module { + /* + * Refs: + * - e-on-java net/vattp interfaces: + * https://github.com/kpreid/e-on-java/tree/master/src/jsrc/net/vattp + * - Connection.java + * - Handler.java + * - Reactor.java + * - OCapN netlayer draft: + * https://github.com/ocapn/ocapn/blob/main/draft-specifications/Netlayers.md + * + * This file captures VatTP/netlayer-like responsibilities that remain above + * ICP transport substrate concerns. + */ + + public type SessionId = Text; + public type ChannelId = Text; + public type Locator = Text; + + public type Endpoint = { + canister : Principal; + exportId : Text; + }; + + // Transport envelope only; payload semantics live in `ocapn_protocol.mo`. + public type TurnEnvelope = { + sessionId : SessionId; + channelId : ChannelId; + turnId : Nat64; + payload : Blob; + }; + + public type TurnResult = { + #ok : Blob; + #err : Text; + }; + + // Java analogue: Handler.run(record, len) + // Remaining responsibility: transport-level inbound turn handling. + public type InboundHandler = { + handleInbound : shared TurnEnvelope -> async TurnResult; + }; + + /* + * Outstanding wrinkle: + * - Historically, VatTP had explicit connection/session lifecycle concerns + * (see e-on-java `Connection`/`Reactor`). + * - In the current split for this prototype, start-session handshake semantics + * are modeled at OCapN protocol level (`ocapn_protocol.mo` `#start_session`), + * not as separate VatTP-boundary callbacks. + * - For pure inter-canister ICP flows, vat-pair state plus sequence tracking + * acts as the implicit logical channel. + */ + + // Remaining responsibility: locator routing above raw canister ids. + public type Routing = { + resolveLocator : shared Locator -> async ?Endpoint; + }; + + // Consolidated VatTP/netlayer boundary interface. + public type VattpBoundary = actor { + handleInbound : shared TurnEnvelope -> async TurnResult; + }; +}; From cd65ecbb70d2f4fb824239971ca28ff7efa07924 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 23:12:03 -0600 Subject: [PATCH 6/7] docs(icp-intercanister): record unforgeable descriptor TODO - mark current Desc=Text modeling as forgeable placeholder - require session-scoped descriptor table validation for inbound authority checks - note swissnum-strength requirement for bootstrap/discovery references --- packages/icp-intercanister/src/ocapn_protocol.mo | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/icp-intercanister/src/ocapn_protocol.mo b/packages/icp-intercanister/src/ocapn_protocol.mo index c38addf..a9f3007 100644 --- a/packages/icp-intercanister/src/ocapn_protocol.mo +++ b/packages/icp-intercanister/src/ocapn_protocol.mo @@ -13,6 +13,11 @@ module { */ public type RefId = Text; + // TODO(ocap-authority): `Desc` is currently a forgeable `Text` placeholder. + // Replace this with session-scoped import/export descriptor positions (or + // equivalent opaque ids) validated against per-session tables on inbound + // messages, so descriptor guessing cannot escalate authority. + // For bootstrap/discovery refs, require unguessable swissnum-strength tokens. public type Desc = Text; public type PublicKey = Blob; public type Location = Text; From 2e9d677e9f859d8fe7467e3c80929240a8af764a Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 19 Feb 2026 23:15:36 -0600 Subject: [PATCH 7/7] docs(icp-intercanister): add swissnum helper and integration TODOs - add packages/icp-intercanister/src/swissnum.mo with IC-entropy-based swissnum minting helper and notes - document follow-on hardening to hash/KDF into fixed-size opaque tokens - add TODOs in sender/receiver to replace forgeable textual refs and integrate descriptor-table checks --- .../src/byte_receiver/main.mo | 2 + .../icp-intercanister/src/byte_sender/main.mo | 2 + packages/icp-intercanister/src/swissnum.mo | 56 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 packages/icp-intercanister/src/swissnum.mo diff --git a/packages/icp-intercanister/src/byte_receiver/main.mo b/packages/icp-intercanister/src/byte_receiver/main.mo index b25485f..46e5e16 100644 --- a/packages/icp-intercanister/src/byte_receiver/main.mo +++ b/packages/icp-intercanister/src/byte_receiver/main.mo @@ -3,6 +3,8 @@ import OcapnTypes "../ocapn_types"; import Vattp "../vattp_boundary"; persistent actor { + // TODO(ocap-authority): integrate `src/swissnum.mo` for bootstrap/discovery + // refs and enforce descriptor-table validation in `ocapn_handle`. // Minimal echo callable; `call` mirrors callable-object naming conventions. func call(args : [OcapnTypes.OcapnValue]) : [OcapnTypes.OcapnValue] { args diff --git a/packages/icp-intercanister/src/byte_sender/main.mo b/packages/icp-intercanister/src/byte_sender/main.mo index 8ad7360..0dc87dd 100644 --- a/packages/icp-intercanister/src/byte_sender/main.mo +++ b/packages/icp-intercanister/src/byte_sender/main.mo @@ -27,6 +27,8 @@ persistent actor { public func ocapn_deliver_with_resolver_ok() : async Bool { let echoProxy = object { let myChannel = theChannel; + // TODO(ocap-authority): replace this forgeable textual target with a + // swissnum/bootstrap flow and descriptor-table positions. let to = "echo-gc"; public func call( diff --git a/packages/icp-intercanister/src/swissnum.mo b/packages/icp-intercanister/src/swissnum.mo new file mode 100644 index 0000000..f928d21 --- /dev/null +++ b/packages/icp-intercanister/src/swissnum.mo @@ -0,0 +1,56 @@ +import Blob "mo:base/Blob"; +import Nat64 "mo:base/Nat64"; +import Principal "mo:base/Principal"; +import Text "mo:base/Text"; + +module { + /// Refs: + /// - OCapN swissnum object lookup usage in tests (`fetch` arg is opaque bytes): + /// https://github.com/ocapn/ocapn-test-suite/blob/main/utils/captp.py + /// - ICP randomness source (`raw_rand`) via management canister. + /// + /// Notes: + /// - Descriptor-table positions (import/export/answer) are authority-safe only + /// when scoped to a session and validated on inbound messages. + /// - Swissnums are bearer secrets for bootstrap/discovery, so they must be + /// high-entropy and unguessable. + /// - This helper mints swissnum tokens from IC entropy and context data. + /// A follow-on hardening step should hash/KDF this to a fixed-width token. + + public type State = { + var nextCounter : Nat64; + }; + + public func emptyState() : State { + { + var nextCounter = 0; + }; + }; + + let IC : actor { + raw_rand : () -> async Blob; + } = actor ("aaaaa-aa"); + + public func mint( + state : State, + caller : Principal, + sessionId : Text, + ) : async Blob { + state.nextCounter += 1; + let entropy = await IC.raw_rand(); + + // Domain-separated context so minted values are namespaced to this usage. + let context = Text.encodeUtf8( + "ocapn-swissnum:v1:" + # Principal.toText(caller) + # ":" + # sessionId + # ":" + # Nat64.toText(state.nextCounter) + ); + + // TODO(ocap-authority): hash/KDF (entropy || context) to a fixed-size token. + // For now we return opaque bytes with fresh entropy and context mixed in. + Blob.fromArray(Blob.toArray(entropy) # Blob.toArray(context)); + }; +};