Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
SHELL := /bin/bash

DFX ?= $(HOME)/.local/share/dfx/bin/dfx
DFX_ENV ?= TERM=xterm-256color
ICP_DEMO_DIR ?= packages/icp-intercanister
ICP_NETWORK ?= local

.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_ENV) "$(DFX)" --version

icp-new: icp-check
@if [ -f "$(ICP_DEMO_DIR)/dfx.json" ]; then \
echo "exists: $(ICP_DEMO_DIR)"; \
else \
echo "missing $(ICP_DEMO_DIR)/dfx.json"; \
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_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_ENV) "$(DFX)" deploy --network "$(ICP_NETWORK)"

icp-call: icp-check
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

icp-replica-stop: icp-check
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
25 changes: 25 additions & 0 deletions packages/icp-intercanister/.gitignore
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions packages/icp-intercanister/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
107 changes: 107 additions & 0 deletions packages/icp-intercanister/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# OCapN-on-ICP Inter-Canister Prototype

This package explores one concrete OCapN interoperability goal on ICP:

- 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 inter-canister plumbing in this package is in service of that goal.

Current canisters:

- `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`.

## Layout

- `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

From repo root:

```bash
make icp-intercanister
```

This command:

1. start local replica
2. deploy both canisters
3. calls `byte_sender.ocapn_deliver_with_resolver_ok`

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]`
- transport in this prototype path: `to_candid` / `from_candid` bytes for `OcapnMessage` and `OcapnResolution`

Stop local replica when done:

```bash
make icp-replica-stop
```

## Manual 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
```

Expected response:

```candid
(true)
```

## 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:
- `<project dir>/.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`.
23 changes: 23 additions & 0 deletions packages/icp-intercanister/dfx.json
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions packages/icp-intercanister/src/byte_receiver/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import OcapnProtocol "../ocapn_protocol";
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
};

/// Minimal selected-case behavior for
/// `op_delivers::OpDeliverTest::test_deliver_with_resolver`.
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(call(deliver.args))];
};
};
case (#deliver_only(_)) {
{
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);
};
};
};
63 changes: 63 additions & 0 deletions packages/icp-intercanister/src/byte_sender/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Blob "mo:base/Blob";
import Principal "mo:base/Principal";
import Receiver "canister:byte_receiver";
import OcapnProtocol "../ocapn_protocol";
import OcapnTypes "../ocapn_types";
import RefSend "../ref_send";
import Vattp "../vattp_boundary";

persistent actor {
let theChannel : RefSend.Channel = {
peerPrincipal = Principal.fromActor(Receiver);
sessionId = "selected-case";
channelId = "selected-case";
var turnId : Nat64 = 1;
};

private func selectedCaseArgs() : [OcapnTypes.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 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(
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 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")
};
};
Loading