- 参照入力に、正しい識別 NFT を持つ状態 UTxO があること
- その datum の
operatingKeyによる署名がトランザクションにあること - その datum の
pendingがNoneであること(復旧中は動かせない)
preprod で、Cardano だけで作れる最小の実装。カードも PF 事業者もアプリも要らない。ここが動けば、この一連の資料の主張は実証される。
| 入れる | 入れない(Phase 1 以降) |
|---|---|
| 資産を守る validator / 状態 UTxO / ウォレット識別 NFT / 鍵交換の申請・待機・取消・確定 / guardian の M-of-N | マイナンバーカード読み取り / PF 事業者連携 / スマホアプリ / パスキー / ZK / 手数料スポンサー / dApp 接続(CIP-141) |
Phase 0 の guardian は、単なる Ed25519 の鍵で代用する。「本人確認をどう調達するか」は、この段階では一切関係ない。後から差し替えられる形にしておけばよい。
アドレスを固定したまま操作鍵を差し替えられること。待機期間中に本人が取り消せること。資産が一度も動かないこと。この三つが通れば、残りは全部「繋ぎ込み」になる。
スクリプトアドレス(= validator のハッシュ。不変) │ ├─ 状態 UTxO ×1 │ ├─ 識別 NFT(これが「正規の状態」の証拠) │ └─ datum = WalletState │ ├─ 資産 UTxO ×n │ └─ datum = walletId(どのウォレットのものか) │ └─ ※ コラテラル用の平の UTxO は別途、鍵アドレスに必要
アドレスは validator のハッシュから決まる。validator のコードを変えない限りアドレスは変わらない。操作鍵は datum の中身なので、いくら書き換えてもアドレスに影響しない。これがこの設計の全てである。
状態 UTxO の偽造を防ぐ。特定の UTxO を消費することを条件にした minting policy で 1 枚だけ発行する。以後、この NFT を持つ UTxO だけが正規の状態と見なす。policy id をウォレット ID として使う。
NFT による一意性の担保が無いと、攻撃者が偽の状態 UTxO を作って「私が operatingKey です」と主張できてしまう。資産 validator は必ず、参照した状態 UTxO に正しい NFT が入っているかを確認すること。
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
}Spend -- 通常送金(資産 UTxO 側) RequestRecovery -- 復旧を申請する CancelRecovery -- 申請を取り消す FinalizeRecovery -- 待機満了後に確定する UpdateGuardians -- guardian 構成を変える(通常鍵で)
approvals のリストを datum に持つ設計も考えられるが、Phase 0 では持たない。guardian の承認は次節のとおりトランザクション署名で取るので、申請時に一括で集める形にする。段階的な承認収集は Phase 1 以降の課題にあたる。
2026 年 7 月 20 日の流出事故は、複数のフィールドを区切りなしで連結して署名対象のメッセージを自作していたことが原因だった。同じバイト列が別の意味に読めてしまい、署名が転用された。
guardian の承認は トランザクションそのものへの署名(extra_signatories / required signers)として取る。validator は「このトランザクションに、guardian の鍵ハッシュ X による署名があるか」を確認するだけでよい。
自前でバイト列を組み立てる工程が存在しないので、非単射エンコーディングの脆弱性が入り込む余地がない。これは eUTxO 側の素直な利点にあたる。
やむを得ず署名対象を自作する場合(Phase 1 以降で外部の attestation を検証するときなど)は、必ず各フィールドに長さを前置するか、スキーマ固定の CBOR で直列化する。「連結して署名」は絶対に書かない。
また nonce の単調増加を全ての状態遷移で強制し、同じ承認が二度通らないようにする。
operatingKey による署名がトランザクションにあることpending が None であること(復旧中は動かせない)通常送金では参照入力(CIP-31)として読むだけにする。消費しないので、複数の送金が同時に走っても競合しない。状態 UTxO を消費するのは、鍵や設定を変えるときだけである。ここを間違えると、連続送金が詰まる。
pending が Noneguardians のうち threshold 人以上の署名があることpending = Some{newKey, requestedAt}requestedAt が、このトランザクションの有効期間の下限と一致することoperatingKey・guardians・threshold・recoveryDelay が入力から変わっていないnonce が +1pending が SomeoperatingKey による署名があること(=本人がまだ鍵を持っている)pending = Nonenonce +1、NFT 引き継ぎpending が Some{newKey, requestedAt}requestedAt + recoveryDelayoperatingKey = newKey、pending = Nonenonce +1、NFT 引き継ぎoperatingKey による署名pending が None(復旧中は構成を変えられない)threshold が 1 以上、guardians の長さ以下nonce +1、NFT 引き継ぎFinalizeRecovery に署名を要求しないのは重要な設計判断にあたる。要求すると、新しい鍵を持つ人が自分で提出しなければならず、手数料も自分で払うことになる。誰でも提出できるようにしておけば、リレイヤーが代行できる。
Cardano の validator は「今何時か」を知らない。分かるのはトランザクションの有効期間(validity range)だけである。したがってこう扱う。
RequestRecovery のとき
tx の有効期間の下限 = requestedAt として datum に記録
(下限より前には実行され得ないので、
「この時刻以降に申請された」ことが保証される)
FinalizeRecovery のとき
tx の有効期間の下限 ≧ requestedAt + recoveryDelay
を validator が確認する
(下限がその時刻以降なら、待機は満了している)有効期間の上限も設定すること。下限だけのトランザクションは扱いが面倒になる場合がある。また下限を 過去に大きく取れば時刻を偽装できる ので、requestedAt は「下限と一致すること」を条件にし、かつ上限との幅を短く保つ運用にする。
operatingKey が変わるFinalizeRecovery が失敗するthreshold 未満の署名では申請が失敗するpending = Some)は送金が失敗するnonce)aiken-upgradable-multisig(署名者の追加・削除・閾値変更が実装済み。待機期間は無いのでそこは自作)| 足すもの | 難度 | 備考 |
|---|---|---|
| attestation 検証 | 低 | 自社が Ed25519 で署名した「本人確認済み」を guardian の一人として扱う。Plutus の組込関数で検証できる |
| カード読み取り | 低 | iOS の CoreNFC。PF 事業者の API を叩くだけ |
| パスキー | 低 | 端末内の Ed25519 鍵を Face ID で解錠する形(水準 2)。オンチェーンには影響しない |
| リレイヤー | 中 | 必須。コラテラルと手数料を出し、トランザクションを組んで提出する |
| アプリ | 中 | 残高表示、送金、復旧フロー、通知 |
attestation を出す主体が実質的な guardianになる。第 VIII 部のとおり、単独で鍵を差し替えられない構成にしておかないと、暗号資産交換業の登録が必要になる可能性がある。
また識別子の問題(電子証明書のシリアル番号を識別子に使えない)も、ここで初めて実務上の課題になる。Phase 0 には一切関係しない。
身元の出所は差し替え可能にしておく。validator が見るのは「信頼できる発行者が署名した attestation」であって、公的個人認証そのものではない。日本では JPKI、他の国では別の発行者、という形にできる。標準としては国際的に書ける。