Skip to content

playground #115

Description

@dumblepy

nicp_cdk — ICP Playground デプロイ機能 設計書

1. 概要

本設計では、nicp_cdk のCLIである nicp に、DFINITYが提供するICP Playgroundへcanisterを一時デプロイするための専用コマンドを追加する。

ユーザー向けコマンドは次の形とする。

nicp playground

特定canisterのみを対象とする場合は、

nicp playground backend

とする。

この機能は通常のdeployment機能の特殊モードではない。

nicp deploy --playground

のようなインターフェースは採用しない。

nicp playground は、

DFINITY Playground Poolを利用して、実際のICP mainnet上に短命なcanisterを確保・インストール・更新するための専用operation

として独立して実装する。


2. 目的

主な目的は、nicp_cdk を利用するアプリケーションについて、

Nim backend
+
frontend
+
ICP mainnet

を含む実環境に近い統合テストを、cyclesを消費せず短時間実行できるようにすることである。

想定する開発フローは次の通り。

unit test
    ↓
PocketIC / local
    ↓
nicp playground
    ↓
実ICP上で統合テスト
    ↓
staging / production

対象となるテストには、

  • Nim backend canisterの実ICP動作確認

  • frontend → backend通信

  • Candid interface統合

  • query / update call

  • ICP HTTP Gateway経由のアクセス

  • Internet Identity等を利用するブラウザ統合

  • HTTPS outcall等、実ネットワークに依存する機能

  • upgrade時の状態保持

  • frontend asset canisterとの統合

を含む。


3. 非目的

nicp playground は以下を目的としない。

  • 通常mainnet deploymentの代替

  • staging環境の代替

  • 長時間稼働環境

  • cyclesを利用した通常canister作成

  • production controller設定

  • production canister settings管理

  • icp deploy のwrapper

  • dfx の完全再実装

  • IC AgentやICP ingress protocolそのもののNim再実装

通常環境については引き続き icp-cli を利用する。

icp deploy -e staging
icp deploy -e production

Playgroundだけが、

nicp playground

という専用経路を通る。


4. dfx --playground の調査結果

4.1 Playgroundは独立したtestnetではない

dfx の現行実装ではPlaygroundは、ICP mainnetのAPI Gatewayへ接続する特殊なnetwork descriptorとして定義されている。

デフォルト値は、

network name:
playground

provider:
ICP mainnet gateway

Playground Pool:
mwrha-maaaa-aaaab-qabqq-cai

TTL:
1200 seconds

である。

つまり構造は、

nicp / dfx

│ ICP ingress

ICP mainnet


Playground Pool Canister
mwrha-maaaa-aaaab-qabqq-cai

├── temporary canister A
├── temporary canister B
└── temporary canister C

となる。

したがってPlayground対応のために独自replicaや独自ネットワークを構築する必要はない。


5. Playgroundのcanister取得方式

通常のmainnet deploymentでは、cycles ledgerやManagement Canisterを利用してcanisterを新規作成する。

一方、dfx はPlaygroundの場合、

create_canister()

├── normal network
│ ↓
│ cycles ledger / management canister

└── playground

reserve_canister_with_playground()

へ分岐する。

つまりPlaygroundでは、

canisterを作成するのではなく、Poolから短命なcanisterを借りる。


6. getCanisterId

canisterを確保する際、Playground Poolの、

getCanisterId

をupdate callする。

概念的な入力は、

type Nonce = record {
timestamp : int;
nonce : nat;
};

type Origin = record {
origin : text;
tags : vec text;
};

getCanisterId : (Nonce, Origin) -> (CanisterInfo);

戻り値は、

type CanisterInfo = record {
id : principal;
timestamp : int;
};

である。dfx もcanister IDだけでなく取得timestampを保存している。

Playgroundにおいて、

canister ID
+
timestamp

の組は一つのopaque handleとして扱う必要がある。

canister IDだけを保存してはいけない。


7. Proof of Work

getCanisterId は簡易Proof of Workを要求する。

dfx は現在、

timestamp = current Unix time in nanoseconds
nonce = random u32

から、

"motoko-playground" + timestamp + nonce

という文字列を生成する。

その文字列にDJB2系hashを適用する。

概念的には、

hash = 5381

for UTF-16 code unit in input:
hash = hash * 33 + code_unit

で、32bit unsigned integerとしてoverflowさせる。

以下を満たせばPoW成功。

(hash & 0xC0000000) == 0

満たさなければ、

nonce += 1

して再試行する。


8. Playground controllerモデル

Playgroundで借りたcanisterは、ユーザーidentityをcontrollerにはしない。

controllerは引き続き、

Playground Pool Canister

である。

そのためユーザーから直接、

Management Canister
├── install_code
├── update_settings
└── delete_canister

を実行することはできない。

したがって、Playground canisterに対して、

icp canister install ...

をそのまま使う設計は採用できない。


9. installCode

Wasmのinstall / upgrade / reinstallはPoolの、

installCode

を通して実施する。

概念的な型は次の通り。

type CanisterInfo = record {
id : principal;
timestamp : int;
};

type WasmMemoryPersistence = variant {
keep;
replace;
};

type InstallMode = variant {
install;
reinstall;
upgrade : opt record {
wasm_memory_persistence : opt WasmMemoryPersistence;
};
};

type InstallArgs = record {
arg : blob;
wasm_module : blob;
mode : InstallMode;
canister_id : principal;
};

type Origin = record {
origin : text;
tags : vec text;
};

type InstallConfig = record {
profiling : bool;
is_whitelisted : bool;
origin : Origin;
start_page : opt nat32;
page_limit : opt nat32;
};

処理は、

PlaygroundClient

Pool.installCode

Pool validates CanisterInfo

Wasm transformation

Management Canister.install_code

Pool refresh

new CanisterInfo

となる。dfx もPoolの installCode を直接利用している。


10. Wasm transformation

通常のbackend Wasmはそのままinstallされない。

Playground PoolはWasmをtransformし、Playground環境用の制約を適用する。

現在のPoolでは例えば、

heap memory   <= 1 GiB
stable memory <= 1 GiB

という制限や、cycles・Management Canister callへの変換処理が行われる。

Pool自身にはManagement Canister互換のwrapperが存在し、

create_canister
install_code
uninstall_code
canister_status
start_canister
stop_canister
delete_canister
...

等をPlayground canisterから利用できるようにしている。

したがって、Playground対応のためにNim canister runtimeそのものへ特殊処理を組み込む必要はない。

Playground deploymentはCLI側の責務とする。


11. TTL

デフォルトPlayground PoolのTTLは、

1200 seconds

20 minutes

である。

さらに重要なのは、successful install / upgradeによってTTLが更新されることである。

installCode 完了後、Poolは、

pool.refresh(...)

を実行し、新しいtimestampを返す。

例:

12:00 reserve

12:01 install

有効期限 ≈ 12:21

12:15 upgrade

timestamp refresh

有効期限 ≈ 12:35

したがって nicp playground は、installCode が返した最新timestampを必ず保存する。


12. CLI仕様

基本コマンド:

nicp playground

対象canister指定:

nicp playground backend

新しいcanisterを強制的に取得:

nicp playground --fresh

既存canisterへreinstall:

nicp playground --reinstall

frontend asset syncを省略:

nicp playground --no-sync

v1ではofficial Playgroundをデフォルトとする。

将来拡張として、

nicp playground --pool-canister <principal>

を検討する。


13. 採用しないCLI

以下は採用しない。

nicp deploy --playground
nicp deploy backend --playground

また、

proc deploy*(playground = false)

のようなAPIも採用しない。

通常deploymentとPlaygroundではcanister lifecycleそのものが異なるためである。

通常:

create

user-controlled canister

install_code

persistent

Playground:

reserve

pool-controlled canister

Pool.installCode

TTL

reuse

これらを一つのdeploy abstractionへ統合すると、

  • cycles

  • controller

  • settings

  • install method

  • state

  • expiration

  • frontend authorization

  • retry

等のPlayground固有分岐が通常deployment側へ漏れる。


14. CLI entrypoint

現在の src/cli/nicp.nim に、

import ./nicp_functions/playground_impl

を追加する。

dispatchMulti は、

when isMainModule:
import cligen

dispatchMulti(
[new_impl.new],
[c_headers_impl.cHeaders],
[development_build_impl.developmentBuild],
[production_build_impl.productionBuild],
[network_impl.network],
[playground_impl.playground]
)

とする。

現行CLIのentrypointは dispatchMulti に各operationを直接登録する構成なので、この追加方法が既存設計と整合する。


15. playground_impl.nim

新規ファイル:

src/cli/nicp_functions/playground_impl.nim

ユーザー向けentrypointは、

proc playground*(
canister: string = "",
fresh: bool = false,
reinstall: bool = false,
noSync: bool = false
): int

とする。

責務はCLIとの境界だけとする。

proc playground*(...): int =
let options = PlaygroundOptions(
canister: canister,
fresh: fresh,
reinstall: reinstall,
noSync: noSync
)

try:
let result = runPlayground(options)
printPlaygroundResult(result)
return 0
except PlaygroundError as e:
printPlaygroundError(e)
return 1

playground_impl.nim に、

  • PoW

  • Candid encoding

  • state storage

  • subprocess execution

  • Pool protocol

等は実装しない。


16. 内部関数名

CLI関数名:

proc playground*()

内部orchestrator:

proc runPlayground*(
options: PlaygroundOptions
): PlaygroundResult

とする。

deploy() という名前はユーザー向けAPIには使用しない。

内部でも、

deploy

より、

runPlayground
reserveCanister
installPlaygroundCanister
syncPlaygroundAssets

のように責務が明示された名前を使用する。


17. モジュール構造

推奨構造:

src/
└── cli/
├── nicp.nim

└── nicp_functions/
├── new_impl.nim
├── c_headers_impl.nim
├── development_build_impl.nim
├── production_build_impl.nim
├── network_impl.nim

├── playground_impl.nim

└── playground/
├── config.nim
├── pow.nim
├── protocol.nim
├── transport.nim
├── state.nim
├── artifact_store.nim
├── asset.nim
├── project.nim
├── runner.nim
└── playground.did

18. モジュール責務

config.nim

Playground固有定数。

const
OfficialPlaygroundCanister* =
"mwrha-maaaa-aaaab-qabqq-cai"

PlaygroundTtlSeconds* = 1200

PlaygroundEnvironment* = "playground"


pow.nim

Proof of Work。

type
PlaygroundNonce* = object
timestamp*: CandidInt
nonce*: CandidNat

proc createPlaygroundNonce*(): PlaygroundNonce
proc motokoHash*(input: string): uint32
proc validProof*(timestamp: CandidInt, nonce: CandidNat): bool

ネットワーク依存性を持たせない。


protocol.nim

Playground protocolの高レベル型。

type
PlaygroundCanisterInfo* = object
id*: Principal
timestamp*: CandidInt

PlaygroundInstallMode* = enum
pimInstall
pimUpgrade
pimReinstall

PlaygroundClient* = ref object
transport*: PlaygroundTransport
poolCanister*: Principal

API:

proc reserve*(
client: PlaygroundClient
): PlaygroundCanisterInfo

proc install*(
client: PlaygroundClient,
info: PlaygroundCanisterInfo,
wasm: seq[byte],
initArg: seq[byte],
mode: PlaygroundInstallMode,
isAssetCanister: bool
): PlaygroundCanisterInfo

proc forward*(
client: PlaygroundClient,
info: PlaygroundCanisterInfo,
methodName: string,
args: seq[byte]
): seq[byte]


transport.nim

icp-cli を利用したnetwork transport。

type
PlaygroundTransport* = ref object

proc call*(
transport: PlaygroundTransport,
canisterId: Principal,
methodName: string,
candidFile: string,
argsFile: string
): PlaygroundCallResult

identity・署名・ingress・request polling等は icp-cli に委譲する。


state.nim

Playground reservationの状態管理。


artifact_store.nim

icp build が生成したWasm artifactの取得。


asset.nim

frontend asset canisterの、

  • install

  • authorize

  • sync

を担当する。


project.nim

icp.yaml のPlayground environmentや対象canisterの検証。


runner.nim

Playground operation全体のorchestration。

中心API:

proc runPlayground*(
options: PlaygroundOptions
): PlaygroundResult

19. icp-cli をtransportとして使用する

Playground protocolを実装するために、Nim側で、

  • HTTP transport

  • request envelope

  • CBOR

  • identity signing

  • delegation

  • ingress expiry

  • request status polling

  • certificate verification

を再実装するのは避ける。

代わりに、

nicp

│ subprocess

icp-cli

│ signed ICP ingress

ICP mainnet

とする。

現行 icp canister call は任意canister IDに対し、Candid interface・引数形式・update/query等を扱えるため、このtransport用途に利用できる。


20. playground.did

nicp_cdk repository内にPlayground Pool用Candid interfaceを保持する。

例:

type CanisterInfo = record {
id : principal;
timestamp : int;
};

type Nonce = record {
timestamp : int;
nonce : nat;
};

type Origin = record {
origin : text;
tags : vec text;
};

type WasmMemoryPersistence = variant {
keep;
replace;
};

type InstallMode = variant {
install;
reinstall;
upgrade : opt record {
wasm_memory_persistence : opt WasmMemoryPersistence;
};
};

type InstallArgs = record {
arg : blob;
wasm_module : blob;
mode : InstallMode;
canister_id : principal;
};

type InstallConfig = record {
profiling : bool;
is_whitelisted : bool;
origin : Origin;
start_page : opt nat32;
page_limit : opt nat32;
};

service : {
getCanisterId :
(Nonce, Origin) -> (CanisterInfo);

installCode :
(CanisterInfo, InstallArgs, InstallConfig)
-> (CanisterInfo);

callForward :
(CanisterInfo, text, blob)
-> (blob);
};

Pool upstream変更時に差分を確認できるよう、このファイルには参照元DFINITY SDK revisionをコメントで残す。


21. Candid int / nat

Playground実装を契機として、nicp_cdk のCandid型対応も修正する。

Candidの、

int
nat

は固定bit幅整数ではない。

したがって、

Candid int ≠ Nim int
Candid int ≠ int64

Candid nat ≠ uint
Candid nat ≠ uint64

とする。


22. CandidNat

推奨型:

type
CandidNat* = object
limbs*: seq[uint32]

little-endian limbとする。

正規化条件:

zero:
limbs.len == 0

non-zero:
limbs[^1] != 0


23. CandidInt

type
CandidInt* = object
negative*: bool
magnitude*: CandidNat

zeroの場合、

negative = false
magnitude = 0

へ正規化する。


24. CandidValue変更

現状の、

of ctInt:
intVal*: int

of ctNat:
natVal*: uint

を、

of ctInt:
intVal*: CandidInt

of ctNat:
natVal*: CandidNat

へ変更する。

固定幅型は変更しない。

int8  → int8
int16 → int16
int32 → int32
int64 → int64

nat8 → uint8
nat16 → uint16
nat32 → uint32
nat64 → uint64


25. 任意精度LEB128

既存の、

proc encodeSLEB128*(n: int32): seq[byte]

は固定幅整数用helperとして残してもよいが、Candid int encodingには使用しない。

追加API:

proc encodeULEB128*(
value: CandidNat
): seq[byte]

proc encodeSLEB128*(
value: CandidInt
): seq[byte]

proc decodeULEB128Big*(
data: openArray[byte],
offset: var int
): CandidNat

proc decodeSLEB128Big*(
data: openArray[byte],
offset: var int
): CandidInt

現在のencoderでは ctInt が固定幅整数へ狭められる経路があるため、任意精度化は独立した修正対象になる。


26. convenience conversion

ユーザーが常にlimbを直接扱う必要はない。

proc candidInt*(value: int): CandidInt
proc candidInt*(value: int64): CandidInt
proc candidInt*(value: string): CandidInt

proc candidNat*(value: uint): CandidNat
proc candidNat*(value: uint64): CandidNat
proc candidNat*(value: string): CandidNat

を提供する。

例えば、

let timestamp =
candidInt("1787000000000000000")

のように利用できる。


27. Playground v1とCandid encoderの依存関係

Playground v1ではtransportに icp-cli を利用するため、Pool callの引数はCandid textとして渡せる。

したがって、

Playground機能完成をCandid arbitrary precision対応でブロックしない。

実装順として、

Playground transport

└── icp-cli Candid encoding

を利用可能とする。

ただし nicp_cdk 全体としては CandidInt / CandidNat 対応を並行して実装する。

最終的にはPlayground protocol内部型にも CandidInt / CandidNat を使用する。


28. Origin

dfx はPlayground requestのoriginとして、

dfx

を使用する。

nicp はこれを偽装しない。

origin = "nicp"
tags = []

とする。

Poolは空origin等を拒否する実装になっている。

これによってDFINITY側の統計でも、

dfx
nicp

を区別できる。


29. Playground state

保存場所:

.icp/cache/nicp-playground/state.json

形式:

{
"schema_version": 1,
"pool_canister": "mwrha-maaaa-aaaab-qabqq-cai",
"ttl_seconds": 1200,
"environment": "playground",
"canisters": {
"backend": {
"id": "xxxxx-xxxxx-...",
"timestamp_nanos": "1787000000000000000"
},
"frontend": {
"id": "yyyyy-yyyyy-...",
"timestamp_nanos": "1787000000000000000"
}
}
}

timestampはJSON numberではなくdecimal stringとする。

理由:

  • Candid int は任意精度

  • JavaScript等でJSONを読んでもprecisionを失わない

  • 将来timestamp representationが拡張されても安全


30. state validity

local state上では、

now - timestamp < TTL

なら再利用候補とする。

ただし境界付近のraceを避けるため安全marginを置く。

例えば、

TTL         = 1200 sec
safety = 30 sec

reuse limit = 1170 sec

とする。

age < 1170 sec

upgrade候補

age >= 1170 sec

新規reserve

local clock skewやPool側state不一致があるため、これはあくまでoptimistic判定である。

最終的な正しさはPoolの installCode responseで判断する。


31. icp-cli environment

生成する icp.yaml に、

environments:

  • name: playground
    network: ic
    canisters:

    • backend
    • frontend

を追加する。

このenvironmentは、

icp deploy -e playground

を実行するためのものではない。

nicp playground が、

icp canister link
icp build
icp sync

等のicp-cli project functionalityを利用するために存在する。

READMEには、

The playground environment is managed by nicp playground.
Do not deploy it with icp deploy -e playground.

と明記する。


32. icp canister link

Poolから得たcanister IDを icp-cli project modelへ登録する。

icp canister link 
backend
<principal>
-e playground
--force

icp-cli は外部で作成されたcanister IDをenvironmentへ関連付ける canister link を正式に持っている。

構造:

Playground Pool

CanisterInfo

nicp

icp canister link

icp-cli ID mapping

33. ephemeral mapping

connected networkのID mappingは icp-cli により、

.icp/data/mappings/<environment>.ids.json

へ保存される。

そのためPlaygroundでは、

.icp/data/mappings/playground.ids.json

が生成される。

これは20分で無効になるため、Gitへcommitしてはいけない。

生成テンプレートの .gitignore に、

# Ephemeral ICP Playground mapping
.icp/data/mappings/playground.ids.json

を追加する。

一方、

.icp/data/mappings/staging.ids.json
.icp/data/mappings/production.ids.json

等には影響を与えない。


34. nicp new の変更

現在生成されるプロジェクトに、

- name: playground
network: ic

を追加する。

frontendあり:

environments:


  • name: local
    network: local
    canisters:

    • backend
    • frontend
  • name: playground
    network: ic
    canisters:

    • backend
    • frontend
  • name: staging
    network: ic
    canisters:

    • backend
    • frontend
  • name: production
    network: ic
    canisters:

    • backend
    • frontend
  • backend only:

      - name: playground
    network: ic
    canisters:
  • backend

  • 35. Playgroundではsettingsを付けない

    現在のテンプレートではstaging/productionに、

    settings:
    backend:
    environment_variables:
    APP_ENV: "staging"

    等を設定している。

    Playgroundではこれを設定しない。

    - name: playground
    network: ic
    canisters:
  • backend
  • frontend
  • のみとする。

    理由は、canister controllerがPlayground Poolであり、通常の update_settings をユーザーidentityから実行できないからである。Pool側も設定変更を制限している。


    36. unsupported settings検出

    nicp playground のpreflightで、Playground environmentに通常canister settingsが指定されていないか検査する。

    対象例:

    controllers
    environment_variables
    compute_allocation
    memory_allocation
    freezing_threshold
    reserved_cycles_limit
    ...

    存在する場合、silent ignoreしない。

    エラー例:

    Playground canisters remain controlled by the ICP Playground pool.
    
    
    
    

    The following canister settings cannot be applied:
    backend.environment_variables

    Remove these settings from the playground environment.


    37. backend build

    全target canisterについて、

    reserve

    link

    を完了してからbuildする。

    icp build -e playground

    順序:

    Resolve ALL canister IDs

    Link ALL canister IDs

    Build

    Install

    とする。

    これはfrontend/backend間でID依存がある場合に、build開始前に全IDを確定させるためである。


    38. 現行Nim build scriptの修正

    現行生成テンプレートでは、

    ${DFX_NETWORK:-local}

    を使ってdevelopment / production buildを切り替えている。

    icp-cli ベースのprojectとしては、

    ICP_CLI_ENVIRONMENT

    を使用するべきである。

    修正:

    if [ "${ICP_CLI_ENVIRONMENT:-local}" = "local" ]; then
    nicp developmentBuild
    else
    nicp productionBuild
    fi

    icp-cli はbuild scriptへ ICP_CLI_ENVIRONMENTICP_WASM_OUTPUT_PATH を渡す。

    結果:

    local       → developmentBuild
    playground → productionBuild
    staging → productionBuild
    production → productionBuild

    39. artifact取得

    icp build のWasm artifactを取得する。

    現在の icp-cli はbuild artifact storeを、

    .icp/cache/artifacts/

    配下に持つ。

    ただしこれは icp-cli 内部実装であるため、直接参照箇所を、

    artifact_store.nim

    だけに限定する。

    proc findBuiltWasm*(
    projectRoot: string,
    canisterName: string
    ): string

    将来 icp-cli がartifact APIを公開した場合は、この実装だけ差し替える。


    40. gzip Wasm

    Playground Poolへ渡すWasmはraw Wasmを基本とする。

    artifact先頭が、

    1F 8B

    ならgzip artifactと判定する。

    v1では自動展開よりも明示的エラーを推奨する。

    The built Wasm artifact is gzip-compressed.
    ICP Playground installation requires an uncompressed Wasm module.

    必要になれば将来transparent decompressionを追加する。


    41. install mode

    状態によってmodeを決定する。

    新規reserve:

    install

    有効なstateあり:

    upgrade

    --reinstall:

    reinstall

    --fresh:

    existing stateを使用せずreserve
    → install

    42. --fresh

    デフォルトでは有効な既存Playground canisterを再利用する。

    理由:

    • canister IDを維持できる

    • frontend/backend参照が維持される

    • Pool capacityを不必要に消費しない

    • upgradeでTTLを更新できる

    --fresh の場合のみ新規reservationを取得する。

    古いcanisterはPoolのTTL回収に任せる。


    43. stale state recovery

    state上では有効でも、Pool側では既に無効になっていることがある。

    例:

    state:
    backend = { id=A, timestamp=T }

    Pool:
    A already expired/recycled

    installCode が、

    Cannot find canister

    等でrejectした場合、

    install

    stale error

    remove local state

    reserve new ID

    icp canister link --force

    rebuild if required

    retry install once

    とする。

    retry回数は1回に限定する。

    無限retryは禁止。


    44. ID変更時のrebuild

    一つでもcanister IDが変わった場合、frontend等にIDが埋め込まれる可能性がある。

    したがって、

    backend A
    ↓ stale
    backend B

    となったら、

    ID resolution phase

    全canister ID再確定

    build phaseから再開

    する。

    個別installだけをretryしない。


    45. frontend asset canister

    frontendが存在する場合、

    backend
    frontend asset canister

    の2つをPlaygroundからreserveする。

    frontendのasset canister WasmはPoolのallowlist対象としてinstallする。

    そのため、

    is_whitelisted = true

    を指定する。dfx もasset canisterの場合にこの扱いを行う。

    backendについては、

    is_whitelisted = false

    とする。


    46. asset uploader authorization

    Poolがfrontend canisterのcontrollerなので、user identityをasset uploaderとしてauthorizeする必要がある。

    まず、

    icp identity principal

    で現在identityのPrincipalを取得する。icp-cli は選択中identityのPrincipalを出力するcommandを提供している。

    次に、

    Pool.callForward(
    frontend CanisterInfo,
    "authorize",
    candid(user principal)
    )

    を実行する。

    dfx も同じ callForward を使ってasset uploaderをauthorizeしている。


    47. frontend sync

    authorize後、

    icp sync frontend -e playground

    を実行する。

    ただし icp-cli のsync recipeがPlayground linked canisterでそのまま利用可能かはintegration testで固定する。

    v1の実装では、

    install asset Wasm

    authorize identity

    icp sync

    を標準経路とし、失敗時はupstream stderrを保持する。


    48. frontendのcanister ID discovery問題

    現行 icp-cli のfrontendでは、

    PUBLIC_CANISTER_ID:backend

    等をcanister environment variableとして設定し、asset canisterが ic_env cookieを提供する仕組みがある。

    これらのbinding設定は update_settings を利用する。

    しかしPlaygroundではユーザーはcontrollerではない。

    したがって、

    通常の icp-cli deploymentが行うruntime canister environment bindingを、そのままPlaygroundで再現することはできない。


    49. nicp生成frontendの解決策

    nicp new が生成するfrontendには、Playground専用fallbackを設ける。

    backend ID確定後、

    VITE_NICP_CANISTER_ID_BACKEND=<principal>
    VITE_NICP_NETWORK=playground

    をbuild/sync subprocess environmentへ注入する。

    frontend側:

    const backendCanisterId =
    canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ??
    import.meta.env.VITE_NICP_CANISTER_ID_BACKEND;

    通常環境:

    PUBLIC_CANISTER_ID:backend

    Playground:

    VITE_NICP_CANISTER_ID_BACKEND

    を利用する。

    これにより通常deploymentの仕組みを維持しながらPlaygroundだけfallbackできる。


    50. generic frontendへの対応

    任意の既存frontendについて自動変換までは行わない。

    runtime bindingだけに依存するfrontendを検出できた場合は、

    This frontend requires runtime PUBLIC_CANISTER_ID bindings.

    ICP Playground canisters are controlled by the Playground pool,
    so these settings cannot be applied automatically.

    Configure a Playground build-time canister ID fallback.

    と明示する。

    silent failureは禁止。


    51. init arguments

    Playground installは、

    arg : blob

    を必要とする。

    backendがinit args不要の場合は、Candid空tuple、

    ()

    をserializeして使用する。

    将来的には、

    proc resolveInitArgs*(
    canisterName: string,
    environment: string
    ): seq[byte]

    を実装する。

    ただし icp.yaml resolutionロジックを nicp に丸ごと複製するのは避ける。

    可能な限り icp-cli 側のmanifest resolutionを再利用する。


    52. runner.nim

    中心データ型:

    type
    PlaygroundOptions* = object
    canister*: string
    fresh*: bool
    reinstall*: bool
    noSync*: bool

    PlaygroundDeployment* = object
    name*: string
    canisterId*: Principal
    timestamp*: CandidInt
    isFrontend*: bool

    PlaygroundResult* = object
    canisters*: seq[PlaygroundDeployment]

    中心関数:

    proc runPlayground*(
    options: PlaygroundOptions
    ): PlaygroundResult

    53. 全体state machine

    nicp playground


    ┌────────────────────┐
    │ Preflight │
    ├────────────────────┤
    │ icp-cli exists │
    │ icp.yaml exists │
    │ playground env │
    │ settings validate │
    │ CI restriction │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Load State │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Resolve Targets │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Resolve IDs │
    ├────────────────────┤
    │ reuse or reserve │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Link All IDs │
    │ via icp-cli │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Build │
    │ -e playground │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Install Backend │
    │ via Pool │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Install Frontend │
    │ if present │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Authorize Uploader │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ icp sync │
    │ unless --no-sync │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Save State │
    └──────────┬─────────┘

    ┌────────────────────┐
    │ Print Result │
    └────────────────────┘

    54. preflight

    最低限以下を検査する。

    icp executable exists
    icp.yaml exists
    playground environment exists
    target canister exists
    no unsupported Playground settings
    official Playground not used from CI
    Playground DID available
    project state directory writable

    frontendありの場合:

    frontend fallback configuration
    asset sync capability

    も検査する。


    55. CI制限

    現行 dfx はCIを検知し、official mainnet Playground Poolへのreservationを拒否する。

    nicp もこの方針を維持する。

    if CI == true
    and pool == OfficialPlaygroundCanister
    → error

    --force-ci のような回避optionは設けない。

    将来custom/local Playground Poolをサポートした場合、そのPoolについてはCI利用を許可できる。


    56. Pool capacity

    Poolに利用可能なcanisterがない場合、Poolは待機時間を含むrejectを返す実装を持つ。

    この場合自動retryは行わない。

    表示:

    ICP Playground is currently at capacity.

    The Playground pool reported that no temporary canister is available.
    Try again after approximately N seconds.


    57. asset Wasm allowlist mismatch

    asset canister WasmがPool allowlistに存在しない場合を専用errorとする。

    peAssetNotWhitelisted

    dfx もこのケースを特別扱いしている。

    表示:

    The asset canister Wasm used by the installed icp-cli
    is not currently allowlisted by ICP Playground.

    This is an upstream Playground / asset-canister version mismatch.


    58. Error model

    type
    PlaygroundErrorKind* = enum
    peIcpCliMissing
    peProjectNotFound
    peEnvironmentMissing
    peTargetNotFound
    peUnsupportedSettings
    peCiNotAllowed
    pePoolUnavailable
    pePoolCapacity
    peInvalidProof
    peInvalidResponse
    peStaleCanister
    peBuildFailed
    peArtifactMissing
    peCompressedArtifact
    peInstallRejected
    peAssetNotWhitelisted
    peAuthorizeFailed
    peSyncFailed
    peStateReadFailed
    peStateWriteFailed

    高レベル理由とupstream stderrを分離する。

    例:

    Failed to reserve an ICP Playground canister.

    Cause:
    Playground proof-of-work was rejected.

    Upstream:
    <icp-cli / replica error>


    59. subprocess abstraction

    外部commandを直接各moduleから呼ばない。

    type
    CommandRunner* = ref object

    proc run*(
    runner: CommandRunner,
    command: string,
    args: seq[string],
    extraEnv: Table[string, string] = initTablestring, string
    ): CommandResult

    これを、

    • icp canister call

    • icp canister link

    • icp build

    • icp sync

    • icp identity principal

    全てに利用する。

    testではfake runnerへ交換できる。


    60. security

    以下を遵守する。

    • identity secretを nicp が読む必要はない

    • PEM/private keyをstateへ保存しない

    • subprocess command lineへsecretを追加しない

    • Playground stateはcanister IDとtimestampのみ

    • upstream outputにsecretが含まれ得る場合はlog levelを考慮

    • custom pool principalは必ずPrincipal validationする

    identity管理は icp-cli に委譲する。


    61. Logging

    通常:

    Reserving Playground canister 'backend'...
    Reserved backend: xxxxx-...

    Linking Playground canister IDs...
    Building project for playground...
    Installing backend...
    Installed backend.

    Installing frontend...
    Authorizing asset uploader...
    Syncing frontend assets...

    ICP Playground deployment completed.

    --debug 相当の既存logging方針があれば、

    • exact icp-cli command

    • raw Pool response

    • state transition

    • selected install mode

    を追加表示する。


    62. 成功時出力

    backend only:

    ICP Playground deployment completed.

    backend
    Canister ID: xxxxx-xxxxx-...
    Expires: approximately 20 minutes after the latest install

    frontendあり:

    ICP Playground deployment completed.

    backend
    Canister ID: xxxxx-xxxxx-...

    frontend
    Canister ID: yyyyy-yyyyy-...
    URL: https://yyyyy-yyyyy-....icp0.io

    Temporary canisters expire approximately 20 minutes
    after their latest successful installation.


    63. Unit test — PoW

    固定vectorで検証する。

    timestamp
    nonce
    expected hash
    expected valid/invalid

    DFINITY SDKのRust実装と同じ値になるfixtureを用意する。

    境界:

    hash & 0xC0000000 == 0
    hash & 0xC0000000 != 0

    64. Unit test — Candid integer

    最低限、

    0
    1
    -1
    127
    128
    -128
    -129
    int64.high
    int64.low
    int64.high + 1
    10^100
    -(10^100)

    をencode/decode round tripする。

    nat:

    0
    127
    128
    uint64.high
    uint64.high + 1
    10^100

    を確認する。


    65. Unit test — state

    検証項目:

    • empty state

    • save/load

    • decimal timestamp

    • malformed JSON

    • unsupported schema version

    • TTL valid

    • TTL expired

    • safety margin

    • pool principal mismatch

    • timestamp refresh

    • --fresh


    66. Unit test — protocol

    fake PlaygroundTransport を使う。

    検証:

    reserve
    install
    upgrade
    reinstall
    callForward
    asset install

    Candid argumentsもfixture化する。


    67. Unit test — orchestration

    fake CommandRunner とfake PlaygroundClient を利用する。

    期待されるcommand順:

    reserve backend
    reserve frontend

    link backend
    link frontend

    build

    install backend
    install frontend

    identity principal
    authorize frontend

    sync frontend

    を検証する。


    68. stale retry test

    existing state A

    install A

    stale error

    reserve B

    link B

    rebuild

    install B

    を確認する。

    二回目も失敗した場合は終了する。


    69. integration test

    DFINITY SDKにあるPlayground backend実装を利用または参考にし、ローカルPoolで検証する。

    backend test:

    reserve

    build Nim greet

    install

    call greet

    upgrade

    same canister ID

    timestamp changed

    frontend test:

    reserve backend
    reserve frontend

    install backend
    install asset

    authorize

    sync

    browser

    frontend → backend

    70. official Playground smoke test

    official Poolを使う自動CIテストは行わない。

    release前のmanual smoke testとする。

    最低確認:

    nicp playground backend

    その後、

    icp canister call backend greet '("ICP")' -e playground

    等で実canisterを確認する。

    続けて、

    nicp playground backend

    を再実行し、

    same canister ID
    new timestamp
    upgrade succeeded

    を確認する。


    71. frontend smoke test

    nicp playground

    実行後、

    frontend URL

    Browser

    frontend asset canister

    backend Playground canister

    が成立することを確認する。

    Playwright等による手動/ローカルE2Eも可能にする。

    official Poolを使ったCI自動化は行わない。


    72. 実装Phase 1 — backend

    最初にbackend onlyを完成させる。

    対象:

    playground command
    playground_impl
    runner
    config
    PoW
    transport
    protocol
    state
    project validation
    reserve
    canister link
    build
    artifact lookup
    installCode
    upgrade
    reinstall
    stale recovery
    logging
    unit tests

    受け入れ条件:

    nicp playground backend

    で実ICP上にNim backendをデプロイできる。


    73. Phase 2 — Candid arbitrary precision

    並行またはPhase 1直後に、

    CandidInt
    CandidNat
    big ULEB128
    big SLEB128
    CandidValue migration
    tests

    を実装する。

    Playground timestampを含め、Candid仕様に正しい整数モデルへ移行する。


    74. Phase 3 — frontend

    追加:

    asset canister detection
    whitelisted install
    identity principal
    callForward authorize
    icp sync
    build-time canister ID fallback
    frontend template changes
    browser integration test

    75. Phase 4 — hardening

    追加候補:

    • environment-specific init args

    • arbitrary multi-canister projects

    • custom Playground Pool

    • local Playground Pool

    • asset canister version detection

    • icp-cli artifact path依存の除去

    • generic frontend ID injection hooks

    • richer Playground status command

    • manual release/retire operation


    76. nicp playground の責務境界

    nicp playground が行うもの:

    PoW
    Pool reservation
    Playground state
    icp-cli ID link
    Playground build orchestration
    Pool.installCode
    asset authorization
    frontend sync
    TTL handling
    stale recovery

    行わないもの:

    normal canister creation
    cycles ledger
    wallet handling
    production controller management
    normal icp deploy
    identity private key management
    ICP Agent implementation

    77. architecture summary

                        nicp


    playground_impl


    PlaygroundRunner

    ┌──────────────┼───────────────┐
    │ │ │
    ▼ ▼ ▼
    ProjectResolver StateStore PlaygroundClient

    ┌───────┴───────┐
    │ │
    ▼ ▼
    PoW Transport


    icp-cli

    ┌────────────────────────────┼─────────────┐
    │ │ │
    ▼ ▼ ▼
    build/link call sync
    │ │ │
    └───────────────┬────────────┴─────────────┘

    ICP mainnet


    Playground Pool

    ┌─────────┴──────────┐
    ▼ ▼
    backend frontend

    78. 最重要設計判断

    項目 判断
    CLI nicp playground
    CLI関数 playground_impl.playground
    汎用deploy関数 作らない
    orchestrator runner.runPlayground
    通常deployment icp deploy に任せる
    Playground create Pool getCanisterId
    Playground install Pool installCode
    identity / signing icp-cli
    project ID integration icp canister link
    build icp build -e playground
    frontend upload authorize後 icp sync
    state {id, timestamp}
    TTL 1200秒
    stale recovery 1回だけ再reserve
    Candid int CandidInt 任意精度
    Candid nat CandidNat 任意精度
    official Playground CI 禁止
    environment settings Playgroundでは禁止
    frontend ID build-time fallback

    79. Acceptance criteria

    Backend

    以下をすべて満たす。

    ✓ dfx不要
    

    ✓ icp-cli + nicpのみで動作

    ✓ cycles不要

    ✓ nicp playground backend が成功

    ✓ 実ICP mainnet上へNim Wasmをinstall

    ✓ 初回はPoolからcanisterをreserve

    ✓ 同一セッション内の再実行では同じIDへupgrade

    ✓ upgradeでTTL更新

    ✓ expired/stale canisterから自動recover

    ✓ retryは最大1回

    ✓ ephemeral mappingをGitへcommitしない

    ✓ production/staging mappingを変更しない

    ✓ official PlaygroundをCIで使用しない

    Frontend

    ✓ frontend canisterもPoolからreserve

    ✓ asset canister Wasmをallowlisted install

    ✓ current identityをauthorize

    ✓ icp syncでasset upload

    ✓ generated frontendからbackend IDを解決

    ✓ frontend → backend call成功

    ✓ unsupported runtime bindingをsilent ignoreしない

    Candid

    ✓ Candid intを固定幅Nim intとして扱わない

    ✓ Candid natを固定幅uintとして扱わない

    ✓ arbitrary precision LEB128を実装

    ✓ Playground timestampをlosslessに扱う


    80. 結論

    nicp_cdk におけるICP Playground対応は、通常deploymentのoptionとして実装しない。

    外部インターフェースは、

    nicp playground

    とし、実装入口も、

    playground_impl.playground()

    とする。

    内部処理は、

    nicp playground

    runPlayground()

    PoW

    Pool.getCanisterId

    CanisterInfo { id, timestamp }

    icp canister link

    icp build -e playground

    Pool.installCode

    new CanisterInfo

    state保存

    を基本経路とする。

    frontendが存在する場合のみ、

    asset install

    Pool.callForward("authorize")

    icp sync

    を追加する。

    この構成にすることで、

    • dfx 非依存

    • icp-cli project modelとの整合

    • 通常deploymentとの明確な分離

    • Playground固有ロジックの隔離

    • 実ICPを利用した20分程度の統合テスト

    • frontend/backendのfull-stackテスト

    • 将来の icp-cli native Playground対応への移行

    を同時に満たす。

    特に設計上の中心原則は、

    nicp playground はdeployの一モードではなく、Playground Poolを操作する独立した一時環境operationである。

    とする。

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      No labels
      No labels

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions