設計メモ / Phase 0

アドレスを変えずに、
鍵を交換できるウォレット

preprod で、Cardano だけで作れる最小の実装。カードも PF 事業者もアプリも要らない。ここが動けば、この一連の資料の主張は実証される。

00 / 範囲

Phase 0 に入れるもの、入れないもの

入れる入れない(Phase 1 以降)
資産を守る validator / 状態 UTxO / ウォレット識別 NFT / 鍵交換の申請・待機・取消・確定 / guardian の M-of-N マイナンバーカード読み取り / PF 事業者連携 / スマホアプリ / パスキー / ZK / 手数料スポンサー / dApp 接続(CIP-141)

Phase 0 の guardian は、単なる Ed25519 の鍵で代用する。「本人確認をどう調達するか」は、この段階では一切関係ない。後から差し替えられる形にしておけばよい。

この段階で証明されること

アドレスを固定したまま操作鍵を差し替えられること。待機期間中に本人が取り消せること。資産が一度も動かないこと。この三つが通れば、残りは全部「繋ぎ込み」になる。

01 / 構造

ウォレットの構成要素

一つのウォレット
スクリプトアドレス(= validator のハッシュ。不変)
│
├─ 状態 UTxO ×1
│    ├─ 識別 NFT(これが「正規の状態」の証拠)
│    └─ datum = WalletState
│
├─ 資産 UTxO ×n
│    └─ datum = walletId(どのウォレットのものか)
│
└─ ※ コラテラル用の平の UTxO は別途、鍵アドレスに必要

アドレスは validator のハッシュから決まる。validator のコードを変えない限りアドレスは変わらない。操作鍵は datum の中身なので、いくら書き換えてもアドレスに影響しない。これがこの設計の全てである。

識別 NFT(one-shot policy)

状態 UTxO の偽造を防ぐ。特定の UTxO を消費することを条件にした minting policy で 1 枚だけ発行する。以後、この NFT を持つ UTxO だけが正規の状態と見なす。policy id をウォレット ID として使う。

これが無いと壊れる

NFT による一意性の担保が無いと、攻撃者が偽の状態 UTxO を作って「私が operatingKey です」と主張できてしまう。資産 validator は必ず、参照した状態 UTxO に正しい NFT が入っているかを確認すること。

02 / データ

Datum と Redeemer

状態 UTxO の datum
WalletState {
  operatingKey   : VerificationKeyHash      -- 通常操作の鍵
  guardians      : List<VerificationKeyHash>
  threshold      : Int                      -- M(guardians の M-of-N)
  recoveryDelay  : Int                      -- 待機時間(ミリ秒)
  pending        : Option<PendingRecovery>
  nonce          : Int                      -- 再生防止
}

PendingRecovery {
  newKey       : VerificationKeyHash
  requestedAt  : PosixTime
}
Redeemer(操作の種類)
Spend             -- 通常送金(資産 UTxO 側)
RequestRecovery   -- 復旧を申請する
CancelRecovery    -- 申請を取り消す
FinalizeRecovery  -- 待機満了後に確定する
UpdateGuardians   -- guardian 構成を変える(通常鍵で)

approvals のリストを datum に持つ設計も考えられるが、Phase 0 では持たない。guardian の承認は次節のとおりトランザクション署名で取るので、申請時に一括で集める形にする。段階的な承認収集は Phase 1 以降の課題にあたる。

03 / 安全性

Wanchain の穴を、構造的に避ける

2026 年 7 月 20 日の流出事故は、複数のフィールドを区切りなしで連結して署名対象のメッセージを自作していたことが原因だった。同じバイト列が別の意味に読めてしまい、署名が転用された。

Cardano では、そもそもメッセージを自作しない

guardian の承認は トランザクションそのものへの署名extra_signatories / required signers)として取る。validator は「このトランザクションに、guardian の鍵ハッシュ X による署名があるか」を確認するだけでよい。
自前でバイト列を組み立てる工程が存在しないので、非単射エンコーディングの脆弱性が入り込む余地がない。これは eUTxO 側の素直な利点にあたる。

それでも守るべきこと

やむを得ず署名対象を自作する場合(Phase 1 以降で外部の attestation を検証するときなど)は、必ず各フィールドに長さを前置するか、スキーマ固定の CBOR で直列化する。「連結して署名」は絶対に書かない。
また nonce の単調増加を全ての状態遷移で強制し、同じ承認が二度通らないようにする。

04 / 検証条件

validator が確認すること

資産 validator(資産 UTxO に付く)

Spend
  • 参照入力に、正しい識別 NFT を持つ状態 UTxO があること
  • その datum の operatingKey による署名がトランザクションにあること
  • その datum の pendingNone であること(復旧中は動かせない)
状態 UTxO は消費しない

通常送金では参照入力(CIP-31)として読むだけにする。消費しないので、複数の送金が同時に走っても競合しない。状態 UTxO を消費するのは、鍵や設定を変えるときだけである。ここを間違えると、連続送金が詰まる。

状態 validator(状態 UTxO に付く)

RequestRecovery
  • 入力の pendingNone
  • guardians のうち threshold 人以上の署名があること
  • 出力の pending = Some{newKey, requestedAt}
  • requestedAt が、このトランザクションの有効期間の下限と一致すること
  • operatingKeyguardiansthresholdrecoveryDelay が入力から変わっていない
  • 識別 NFT が出力に引き継がれ、出力先が同じスクリプトアドレスであること
  • nonce が +1
CancelRecovery
  • 入力の pendingSome
  • operatingKey による署名があること(=本人がまだ鍵を持っている)
  • 出力の pending = None
  • 他の全フィールドが不変、nonce +1、NFT 引き継ぎ
FinalizeRecovery
  • 入力の pendingSome{newKey, requestedAt}
  • トランザクションの有効期間の下限 ≧ requestedAt + recoveryDelay
  • 出力の operatingKey = newKeypending = None
  • 他のフィールド不変、nonce +1、NFT 引き継ぎ
  • 署名は要求しない。条件は既に満たされているので、誰が提出してもよい
UpdateGuardians
  • operatingKey による署名
  • pendingNone(復旧中は構成を変えられない)
  • 新しい threshold が 1 以上、guardians の長さ以下
  • nonce +1、NFT 引き継ぎ

FinalizeRecovery に署名を要求しないのは重要な設計判断にあたる。要求すると、新しい鍵を持つ人が自分で提出しなければならず、手数料も自分で払うことになる。誰でも提出できるようにしておけば、リレイヤーが代行できる。

05 / 時間

待機期間の実装

Cardano の validator は「今何時か」を知らない。分かるのはトランザクションの有効期間(validity range)だけである。したがってこう扱う。

時間の判定
RequestRecovery のとき
  tx の有効期間の下限 = requestedAt として datum に記録
  (下限より前には実行され得ないので、
    「この時刻以降に申請された」ことが保証される)

FinalizeRecovery のとき
  tx の有効期間の下限 ≧ requestedAt + recoveryDelay
  を validator が確認する
  (下限がその時刻以降なら、待機は満了している)
よくある落とし穴

有効期間の上限も設定すること。下限だけのトランザクションは扱いが面倒になる場合がある。また下限を 過去に大きく取れば時刻を偽装できる ので、requestedAt は「下限と一致すること」を条件にし、かつ上限との幅を短く保つ運用にする。

06 / 完了条件

これが通れば Phase 0 は終わり

通常送金ができる(状態 UTxO は参照のみで消費されない)
連続して 2 件の送金を同時に提出しても、両方通る
復旧申請 → 待機 → 確定で operatingKey が変わる
その前後でアドレスが変わっていない
資産 UTxO が一度も動いていない
待機中に旧鍵で取り消せる
待機満了前の FinalizeRecovery が失敗する
threshold 未満の署名では申請が失敗する
復旧中(pending = Some)は送金が失敗する
NFT を持たない偽の状態 UTxO を参照した送金が失敗する
同じ申請トランザクションを再提出しても通らない(nonce
07 / 道具と見積り

使うもの

見込んでおくこと

08 / 次

Phase 1 で足すもの

足すもの難度備考
attestation 検証自社が Ed25519 で署名した「本人確認済み」を guardian の一人として扱う。Plutus の組込関数で検証できる
カード読み取りiOS の CoreNFC。PF 事業者の API を叩くだけ
パスキー端末内の Ed25519 鍵を Face ID で解錠する形(水準 2)。オンチェーンには影響しない
リレイヤー必須。コラテラルと手数料を出し、トランザクションを組んで提出する
アプリ残高表示、送金、復旧フロー、通知
Phase 1 で初めて出てくる論点

attestation を出す主体が実質的な guardianになる。第 VIII 部のとおり、単独で鍵を差し替えられない構成にしておかないと、暗号資産交換業の登録が必要になる可能性がある。
また識別子の問題(電子証明書のシリアル番号を識別子に使えない)も、ここで初めて実務上の課題になる。Phase 0 には一切関係しない。

身元の出所は差し替え可能にしておく。validator が見るのは「信頼できる発行者が署名した attestation」であって、公的個人認証そのものではない。日本では JPKI、他の国では別の発行者、という形にできる。標準としては国際的に書ける。