diff --git a/docs/rfcs/0004-ringlocation-redesign.md b/docs/rfcs/0004-ringlocation-redesign.md new file mode 100644 index 00000000..1a3aba7e --- /dev/null +++ b/docs/rfcs/0004-ringlocation-redesign.md @@ -0,0 +1,203 @@ +# RFC 0004 — Redesign `host_account_create_proof` + +| | | +| --------------- |-----------------------------------------------------------------------------------------------------------| +| **Start Date** | 2026-03-16 | +| **Description** | Junction-based RingLocation, context-scoped proofs, and a specified host member-key selection contract | +| **Authors** | Valentin Sergeev | + +## Summary + +Redesign `host_account_create_proof` and `host_account_get_alias`: + +1. **Junction-based ring addressing** — replace the `ring_root_hash`-based `RingLocation` with a struct carrying a required `chain_id` and a `Vec` path of stable, immutable identifiers. +2. **Member-key-based, context-scoped proofs** — replace `domain: ProductAccountId` with a `ProductProofContext { product_id, suffix }` struct. The proof is created with a member key the host holds (selected for the requested ring); the context scopes the derived alias for unlinkability. +3. **Richer output and errors** — return `contextual_alias`, `ring_index`, and `ring_revision`; specify host member-key selection; add a `NotMember` error. + +No protocol version bump is required: the current shape of these methods is unusable (the `ring_root_hash` race makes it broken by construction) and is not implemented or consumed anywhere yet, so it can be replaced in place. + +## Motivation + +- **Request invalidation.** `ring_root_hash` changes whenever ring membership changes, invalidating any in-flight proof request built against the previous root. +- **No revision in the response.** Downstream consumers (coinage's recycler transaction extension, the `personhoodInfoByProof` precompile) need the ring revision and index, which the current `Vec` return cannot carry. +- **Hints can't address multi-ring pallets.** With the membership pallet, one pallet instance hosts rings from multiple collections, each identified by `(collection_id, ring_index)`. `RingLocationHint`'s optional `pallet_instance` cannot disambiguate them. +- **`domain: ProductAccountId` is the wrong input.** Proof generation depends only on which member key proves membership in the requested ring — the host holds one or more member keys (possibly different keys for different rings) and selects the right one. A derived product account and its derivation index have nothing to do with that. The old signature conflated product-account derivation with proof generation; unlinkability instead comes from the `context` (the same member key under different contexts yields different, unlinkable aliases), so the request needs an explicit, product-scoped context rather than a derivation index. +- **Member-key selection is unspecified.** A host may hold several member keys but the API hides them (exposing them leaks identity). Without a defined selection contract, two hosts can derive different aliases for the same request. +- **No "not a member" error.** A user who has not reached full personhood is not in the ring. `CreateProofErr` cannot distinguish this from "ring does not exist", so products can't route the user to onboarding. + +## Status Quo + +```rust +struct RingLocationHint { pallet_instance: Option } +struct RingLocation { genesis_hash: GenesisHash, ring_root_hash: Vec, hints: Option } +type RingVrfProof = Vec; + +fn host_account_create_proof(domain: ProductAccountId, ring: RingLocation, message: Vec) + -> Result; +fn host_account_get_alias(domain: ProductAccountId) + -> Result; +``` + +## Design + +### Ring addressing + +`chain_id` is a required field (not a junction) so a location can never omit its chain; the junctions address the ring within it. All identifiers are stable for the ring's lifetime, so the host can resolve the current root and the caller's index internally without a membership-change race. New `RingLocationJunction` variants can be added without breaking consumers. (The junction pattern is borrowed from XCM's `MultiLocation`.) + +```rust +enum RingLocationJunction { + PalletInstance(u8), + CollectionId(Vec), +} + +struct RingLocation { + chain_id: GenesisHash, + junctions: Vec, +} +``` + +### Product-scoped proof context + +`domain: ProductAccountId` is replaced by a product-scoped context struct. `product_id` is the existing dotNS product identifier; `suffix` distinguishes contexts within that product: + +```rust +struct ProductProofContext { + product_id: String, // dotNS product identifier, e.g. "my-product.dot" + suffix: Vec, // arbitrary bytes distinguishing contexts within the product +} + +// 32-byte context bound into the proof. +fn product_context_bytes(ctx: ProductProofContext) -> [u8; 32] { + blake2b256(utf8("product/") ++ utf8(ctx.product_id) ++ utf8("/") ++ ctx.suffix) +} +``` + +- **Product-scoped.** The `product//` prefix stops a malicious product from choosing a suffix that collides with another product's context and thereby links its aliases. This is a privacy boundary. +- **Arbitrary-byte suffix.** Some contexts need more than one index — e.g. a pgas claim derives its context from two `u32`s (period and sequence). A single-index suffix would make them unrepresentable. + +### `create_proof` and `get_alias` + +The proof is created with a member key the host holds; the host selects which key based on the requested ring (see below). Both methods take the same `(context, ring)` so they derive the same alias. + +```rust +struct RingVrfProof { + proof: Vec, + contextual_alias: ContextualAlias, + ring_index: u32, + ring_revision: u32, +} + +fn host_account_create_proof(context: ProductProofContext, ring: RingLocation, message: Vec) + -> Result; + +fn host_account_get_alias(context: ProductProofContext, ring: RingLocation) + -> Result; +``` + +`ring_index` / `ring_revision` let products call downstream precompiles without a separate lookup. `contextual_alias` is an ergonomics optimization — the same value `get_alias` returns for the same `(context, ring)` — saving a round trip when a caller needs both proof and alias (e.g. a voting contract keying votes by alias). The host MUST select the member key identically in both methods so the two aliases match. + +### Host member-key selection + +The host may hold multiple member keys; the API exposes neither the keys nor their ids. The host MUST: + +1. Define the **"PoP" ring collection** as the collection corresponding to full-personhood rings. +2. Choose a member key that is present in / logically corresponds to the requested `RingLocation`. +3. If correspondence is not determinable, fall back to a key corresponding to the "PoP" ring. +4. If multiple keys correspond to the same ring, consistently pick any one — the choice MUST be stable across calls for the same inputs so the alias is stable. + +**Out of scope:** explicit member-key management (letting the caller reference a specific key rather than having the host infer one) is left to a future RFC — exposing keys or their ids is a separate, larger design with its own privacy considerations. + +### Errors + +`create_proof` and `get_alias` take the same `(context, ring)` and perform the same ring resolution and member-key selection, so they share the same failure modes and carry identical error sets: + +```rust +enum CreateProofErr { RingNotFound, NotMember, Rejected, Unknown } +enum GetAliasErr { RingNotFound, NotMember, Rejected, Unknown } +``` + +`NotMember` is returned when the selected member key is not a member of the requested ring — most importantly when the user has not yet reached full personhood — letting products distinguish it from `RingNotFound` and route to onboarding. + +### Usage + +`ring_root_hash`, `hints`, and the `domain` parameter are gone — products never fetch or hash ring roots or manage derivation indices. + +```rust +let location = RingLocation { + chain_id: chain_genesis, + junctions: vec![ + RingLocationJunction::PalletInstance(42), + RingLocationJunction::CollectionId(collection), + ], +}; +let result = host_account_create_proof( + ProductProofContext { product_id, suffix }, + location, + message, +)?; +// result.proof / contextual_alias / ring_index / ring_revision +``` + +### Accounts Protocol companion + +The methods above are the **TrUAPI** boundary (product ↔ Host). The same operations also cross the **Accounts Protocol** boundary (Host ↔ Account Holder, which custodies the member keys and does the selection and proof generation). The companion methods reuse the same types, with `calling_product_id: ProductId` prepended — at the TrUAPI boundary the Host already knows the calling product, but here it is the caller acting on a product's behalf, so the Account Holder needs it named to scope context derivation and permissioning: + +```rust +fn create_account_proof( + calling_product_id: ProductId, + context: ProductProofContext, + ring: RingLocation, + message: Vec, +) -> Result; + +fn get_account_alias( + calling_product_id: ProductId, + context: ProductProofContext, + ring: RingLocation, +) -> Result; +``` + +## Out of Scope: Product-SDK Helpers (Non-Normative) + +These live at the product-sdk level, not in truAPI; the host implements none of them. Documented only because they shape how products build a `ProductProofContext`. + +**Default context.** For contexts that need no suffix, the sdk can use a canonical default: + +```rust +const SINGLETON_PROOF_SUFFIX: [u8; 1] = [0]; +fn singleton_proof_context(product_id: String) -> ProductProofContext { + ProductProofContext { product_id, suffix: SINGLETON_PROOF_SUFFIX.to_vec() } +} +``` + +**Context ↔ accountId linkability.** To set an account as the alias for a context, the sdk needs a canonical suffix → `DerivationIndex` mapping (`host_account_get_account` takes `ProductAccountId = (ProductId, DerivationIndex)`): + +```rust +fn product_account_id_for_proof_context(product_id: String, suffix: [u8; 4]) -> ProductAccountId { + ProductAccountId { product_id, derivation_index: u32_from_be_bytes(suffix) } +} +fn u32_from_be_bytes(bytes: [u8; 4]) -> u32; // big-endian +``` + +Defined only for 4-byte suffixes to keep a bijection with `u32`. Hashing arbitrary bytes down to 4 was rejected — the space is too small (high collision risk). This is a helper-level limit only: truAPI still accepts arbitrary-byte suffixes, so products not needing a 1:1 context→account mapping are unaffected. + +## Drawbacks + +- **Host complexity** — the host must resolve the root from the junction path and implement member-key selection (PoP fallback + stable tiebreak). +- **No type-level junction validation** — `chain_id` is mandatory, but the `junctions` vector has no enforced ordering; malformed paths are handled at runtime. + +## Alternatives + +- Keep `ring_root_hash` with product-side retry — doesn't solve revision visibility; adds complexity to every product. +- Keep `domain: ProductAccountId` plus a separate context — keeps proof generation tied to a derived product account instead of the host's member key for the ring. +- Single-`u32` suffix — too narrow; real contexts (pgas claims) need more. +- XCM `MultiLocation` directly — overly general; only the junction pattern is borrowed. + +## References + +- [Host API Design Document v0.5](https://docs.google.com/document/d/1AxKjF15y7gmdl-a6twc5wd8R5xcxKxMO8Ahp2l20v0g/edit?usp=sharing) +- Technical Design: Sybil-Resistant Voting with Personhood — driving product for the member-key-based proof model, the `contextual_alias` response, and `NotMember`. +- [Polkadot People Registry / Ring VRF](https://forum.polkadot.network/t/the-people-registry/12749) +- [individuality#878](https://github.com/paritytech/individuality/pull/878) — alias-account assignment for derived product addresses +- [individuality#891](https://github.com/paritytech/individuality/pull/891) — `personhoodInfoByProof` precompile (motivates the richer response) +- [triangle-js-sdks#81 comment](https://github.com/paritytech/triangle-js-sdks/pull/81) — feedback on moving `ring_index` to output and abstraction concerns diff --git a/docs/rfcs/_index.md b/docs/rfcs/_index.md index 3bac6f9a..b9d617c7 100644 --- a/docs/rfcs/_index.md +++ b/docs/rfcs/_index.md @@ -8,17 +8,18 @@ created: 2026-03-13 # RFCs -| Number | Title | Status | Author | PR | -| ------ | ------------------------------------------------------------------------ | ------------------ | ------------- | --------------------------------------------------------------- | -| 0001 | [RFC Title](0001-template.md) | accepted | @ownerhandle | — | -| 0002 | [Permission Model for Host API](0002-permission-model.md) | accepted | @johnthecat | [#66](https://github.com/paritytech/triangle-js-sdks/pull/66) | -| 0006 | [Payment Host API](0006-payments.md) | accepted | Valentin Sergeev | [#94](https://github.com/paritytech/triangle-js-sdks/pull/94) | -| 0007 | [Deterministic Entropy Derivation for Products](0007-derive-entropy.md) | accepted | Valentin Sergeev | [#95](https://github.com/paritytech/triangle-js-sdks/pull/95) | -| 0008 | [Statement Store Host API v0.2](0008-statement-store.md) | draft | @johnthecat | [#118](https://github.com/paritytech/triangle-js-sdks/pull/118) | -| 0009 | [Unauthenticated Product Access](0009-unauthenticated-product-access.md) | accepted | Filippo Vecchiato | [#128](https://github.com/paritytech/triangle-js-sdks/pull/128) | -| 0010 | [W3S Allowance Management in TrUAPI](0010-allowance.md) | accepted | Valentin Sergeev | — | -| 0015 | [Get User Primary DotNS Name](0015-get-user-id.md) | accepted | Valentin Sergeev | [#144](https://github.com/paritytech/triangle-js-sdks/pull/144) | -| 0017 | [Coinage Payment User Agent API](0017-coinage-payment.md) | accepted | @replghost | — | -| 0019 | [Scheduled Push Notifications](0019-scheduled-notifications.md) | accepted | @johnthecat | — | -| 0020 | [Remove `context` from `create_transaction` and mirror in Accounts Protocol](0020-create-transaction.md) | accepted | Valentin Sergeev | — | -| 0021 | [Add Coins variant to PaymentTopUpSource](0021-payment-topup-coins.md) | accepted | @filippovecchiato | — | +| Number | Title | Status | Author | PR | +| ------ | -------------------------------------------------------------------------------------------------------- | -------- | ----------------- | --------------------------------------------------------------- | +| 0001 | [RFC Title](0001-template.md) | accepted | @ownerhandle | — | +| 0002 | [Permission Model for Host API](0002-permission-model.md) | accepted | @johnthecat | [#66](https://github.com/paritytech/triangle-js-sdks/pull/66) | +| 0004 | [Redesign `host_account_create_proof`](0004-ringlocation-redesign.md) | draft | Valentin Sergeev | [#18](https://github.com/paritytech/truapi/pull/18) | +| 0006 | [Payment Host API](0006-payments.md) | accepted | Valentin Sergeev | [#94](https://github.com/paritytech/triangle-js-sdks/pull/94) | +| 0007 | [Deterministic Entropy Derivation for Products](0007-derive-entropy.md) | accepted | Valentin Sergeev | [#95](https://github.com/paritytech/triangle-js-sdks/pull/95) | +| 0008 | [Statement Store Host API v0.2](0008-statement-store.md) | draft | @johnthecat | [#118](https://github.com/paritytech/triangle-js-sdks/pull/118) | +| 0009 | [Unauthenticated Product Access](0009-unauthenticated-product-access.md) | accepted | Filippo Vecchiato | [#128](https://github.com/paritytech/triangle-js-sdks/pull/128) | +| 0010 | [W3S Allowance Management in TrUAPI](0010-allowance.md) | accepted | Valentin Sergeev | — | +| 0015 | [Get User Primary DotNS Name](0015-get-user-id.md) | accepted | Valentin Sergeev | [#144](https://github.com/paritytech/triangle-js-sdks/pull/144) | +| 0017 | [Coinage Payment User Agent API](0017-coinage-payment.md) | accepted | @replghost | — | +| 0019 | [Scheduled Push Notifications](0019-scheduled-notifications.md) | accepted | @johnthecat | — | +| 0020 | [Remove `context` from `create_transaction` and mirror in Accounts Protocol](0020-create-transaction.md) | accepted | Valentin Sergeev | — | +| 0021 | [Add Coins variant to PaymentTopUpSource](0021-payment-topup-coins.md) | accepted | @filippovecchiato | — | diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts index a70a4b79..fe4c3b7e 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts @@ -15,7 +15,9 @@ import { HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, ProductAccountTxPayload, + ProductProofContext, RemotePermissionRequest, + RingLocation, } from "@parity/truapi"; import type { @@ -47,18 +49,23 @@ export interface AccountAccessReview { } /** - * Review shown before a product asks to alias another product account. + * Review shown before a product derives a contextual alias (RFC 0004). */ export interface AccountAliasReview { /** - * Product currently handling the request. + * Product requesting the alias. */ - requestingProductId: string; + callingProductId: string; /** - * Product whose account is being requested. + * Product-scoped context the alias is bound to. */ - targetProductId: string; + context: ProductProofContext; + + /** + * Ring the alias is derived against. + */ + ringLocation: RingLocation; } /** @@ -118,6 +125,31 @@ export type CoreStorageKey = */ | { tag: "LastProcessedPairingStatement"; value?: undefined }; +/** + * Review shown before a product creates a ring-VRF proof (RFC 0004). + */ +export interface CreateProofReview { + /** + * Product requesting the proof. + */ + callingProductId: string; + + /** + * Product-scoped context the proof's alias is bound to. + */ + context: ProductProofContext; + + /** + * Ring the proof is generated against. + */ + ringLocation: RingLocation; + + /** + * Opaque message bound into the proof. + */ + message: Uint8Array; +} + /** * Review shown before a transaction-creation request is sent to the paired wallet. */ @@ -249,9 +281,13 @@ export type UserConfirmationReview = */ | { tag: "CreateTransaction"; value: CreateTransactionReview } /** - * Allow a product to request another product account alias. + * Allow a product to derive a contextual alias for a ring. */ | { tag: "AccountAlias"; value: AccountAliasReview } + /** + * Allow a product to create a ring-VRF proof for a ring. + */ + | { tag: "CreateProof"; value: CreateProofReview } /** * Allow a product to learn the user's primary identity. */ @@ -281,13 +317,14 @@ export const AccountAccessReview: S.Codec = S.lazy( ); /** - * Review shown before a product asks to alias another product account. + * Review shown before a product derives a contextual alias (RFC 0004). */ export const AccountAliasReview: S.Codec = S.lazy( (): S.Codec => S.Struct({ - requestingProductId: S.str, - targetProductId: S.str, + callingProductId: S.str, + context: ProductProofContext, + ringLocation: RingLocation, }) as S.Codec, ); @@ -332,6 +369,19 @@ export const CoreStorageKey: S.Codec = S.lazy( }), ); +/** + * Review shown before a product creates a ring-VRF proof (RFC 0004). + */ +export const CreateProofReview: S.Codec = S.lazy( + (): S.Codec => + S.Struct({ + callingProductId: S.str, + context: ProductProofContext, + ringLocation: RingLocation, + message: S.Bytes(), + }) as S.Codec, +); + /** * Review shown before a transaction-creation request is sent to the paired wallet. */ @@ -432,6 +482,7 @@ export const UserConfirmationReview: S.Codec = S.lazy( SignRaw: SignRawReview, CreateTransaction: CreateTransactionReview, AccountAlias: AccountAliasReview, + CreateProof: CreateProofReview, IdentityDisclosure: IdentityDisclosureReview, ResourceAllocation: HostRequestResourceAllocationRequest, PreimageSubmit: PreimageSubmitReview, diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index 330f2f7b..8b91d084 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -22,7 +22,8 @@ use truapi::latest::{ HostRequestResourceAllocationRequest, HostSignPayloadRequest, HostSignPayloadWithLegacyAccountRequest, HostSignRawRequest, HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, NotificationId, - ProductAccountTxPayload, RemotePermissionRequest, RemotePermissionResponse, ThemeVariant, + ProductAccountTxPayload, ProductProofContext, RemotePermissionRequest, + RemotePermissionResponse, RingLocation, ThemeVariant, }; use url::Url; @@ -547,13 +548,28 @@ pub enum CreateTransactionReview { LegacyAccount(LegacyAccountTxPayload), } -/// Review shown before a product asks to alias another product account. +/// Review shown before a product derives a contextual alias (RFC 0004). #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct AccountAliasReview { - /// Product currently handling the request. - pub requesting_product_id: String, - /// Product whose account is being requested. - pub target_product_id: String, + /// Product requesting the alias. + pub calling_product_id: String, + /// Product-scoped context the alias is bound to. + pub context: ProductProofContext, + /// Ring the alias is derived against. + pub ring_location: RingLocation, +} + +/// Review shown before a product creates a ring-VRF proof (RFC 0004). +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct CreateProofReview { + /// Product requesting the proof. + pub calling_product_id: String, + /// Product-scoped context the proof's alias is bound to. + pub context: ProductProofContext, + /// Ring the proof is generated against. + pub ring_location: RingLocation, + /// Opaque message bound into the proof. + pub message: Vec, } /// Review shown before a product asks to access another product account. @@ -589,8 +605,10 @@ pub enum UserConfirmationReview { SignRaw(SignRawReview), /// Create a transaction with a product or legacy account. CreateTransaction(CreateTransactionReview), - /// Allow a product to request another product account alias. + /// Allow a product to derive a contextual alias for a ring. AccountAlias(AccountAliasReview), + /// Allow a product to create a ring-VRF proof for a ring. + CreateProof(CreateProofReview), /// Allow a product to learn the user's primary identity. IdentityDisclosure(IdentityDisclosureReview), /// Allocate resources for the requesting product. diff --git a/rust/crates/truapi-server/src/host_logic/sso/messages.rs b/rust/crates/truapi-server/src/host_logic/sso/messages.rs index 5af8e3b8..b46d4fe8 100644 --- a/rust/crates/truapi-server/src/host_logic/sso/messages.rs +++ b/rust/crates/truapi-server/src/host_logic/sso/messages.rs @@ -21,9 +21,9 @@ use parity_scale_codec::{Decode, Encode, OptionBool}; use truapi::latest::{ - AccountId, AllocatableResource, HostAccountGetAliasResponse, HostSignPayloadRequest, - HostSignRawRequest, LegacyAccountTxPayload, ProductAccountId, ProductAccountTxPayload, - RawPayload, + AccountId, AllocatableResource, HostAccountCreateProofResponse, HostAccountGetAliasResponse, + HostSignPayloadRequest, HostSignRawRequest, LegacyAccountTxPayload, ProductAccountId, + ProductAccountTxPayload, ProductProofContext, RawPayload, RingLocation, }; use crate::host_logic::session::SsoSessionInfo; @@ -215,22 +215,60 @@ pub struct SignRawLegacyResponse { pub signature: Result, String>, } -/// Request sent when a product asks the signing host for a ring-VRF alias. +/// Failure returned by the Account Holder for a ring-VRF proof or alias request. /// -/// Used by `Account::get_account_alias`; the product account identifies the -/// alias target, while `product_id` identifies the caller that the signing host is -/// authorizing over the SSO channel. +/// Mirrors the identical error sets of `Account::create_account_proof` and +/// `Account::get_account_alias` (RFC 0004): the two operations perform the same +/// ring resolution and member-key selection, so they share these failure modes. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum RingVrfError { + /// The `RingLocation` did not resolve to a known ring. + RingNotFound, + /// The selected member key is not a member of the requested ring. + NotMember, + /// User or Account Holder rejected the request. + Rejected, + /// Catch-all failure, carrying a diagnostic reason. + Unknown { reason: String }, +} + +/// Request sent when a product asks the Account Holder for a contextual alias. +/// +/// Used by `Account::get_account_alias`; `calling_product_id` names the caller +/// so the Account Holder can scope context derivation, while `context` and +/// `ring_location` select the member key and bind the derived alias (RFC 0004). #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RingVrfAliasRequest { - pub product_account_id: ProductAccountId, - pub product_id: String, + pub calling_product_id: String, + pub context: ProductProofContext, + pub ring_location: RingLocation, } -/// Response returned by the signing host for a ring-VRF alias request. +/// Response returned by the Account Holder for a ring-VRF alias request. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RingVrfAliasResponse { pub responding_to: String, - pub payload: Result, + pub payload: Result, +} + +/// Request sent when a product asks the Account Holder for a ring-VRF proof. +/// +/// Used by `Account::create_account_proof`; carries the same `(context, +/// ring_location)` as the alias request plus the opaque `message` bound into +/// the proof (RFC 0004). +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RingVrfProofRequest { + pub calling_product_id: String, + pub context: ProductProofContext, + pub ring_location: RingLocation, + pub message: Vec, +} + +/// Response returned by the Account Holder for a ring-VRF proof request. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RingVrfProofResponse { + pub responding_to: String, + pub payload: Result, } /// Request sent when a product asks the signing host to allocate SSO-backed @@ -354,6 +392,7 @@ pub enum SsoRemoteResponse { Sign(SigningResponse), SignRawLegacy(SignRawLegacyResponse), RingVrfAlias(RingVrfAliasResponse), + RingVrfProof(RingVrfProofResponse), ResourceAllocation(ResourceAllocationResponse), CreateTransaction(CreateTransactionResponse), } @@ -447,6 +486,11 @@ fn remote_response_for_message( { Some(SsoRemoteResponse::RingVrfAlias(response)) } + v1::RemoteMessage::RingVrfProofResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::RingVrfProof(response)) + } v1::RemoteMessage::SignRawLegacyResponse(response) if response.responding_to == expected_remote_message_id => { @@ -503,18 +547,41 @@ pub fn sign_raw_legacy_message( } } -/// Build a signing-host account-alias request message. +/// Build an Account Holder contextual-alias request message. pub fn alias_request_message( message_id: String, - product_account_id: ProductAccountId, - product_id: String, + calling_product_id: String, + context: ProductProofContext, + ring_location: RingLocation, ) -> RemoteMessage { RemoteMessage { message_id, data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasRequest( RingVrfAliasRequest { - product_account_id, - product_id, + calling_product_id, + context, + ring_location, + }, + )), + } +} + +/// Build an Account Holder ring-VRF proof request message. +pub fn proof_request_message( + message_id: String, + calling_product_id: String, + context: ProductProofContext, + ring_location: RingLocation, + message: Vec, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofRequest( + RingVrfProofRequest { + calling_product_id, + context, + ring_location, + message, }, )), } diff --git a/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs b/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs index cbd0204a..9d83d055 100644 --- a/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs +++ b/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs @@ -10,8 +10,8 @@ use parity_scale_codec::{Decode, Encode}; use super::{ CreateTransactionLegacyRequest, CreateTransactionRequest, CreateTransactionResponse, ResourceAllocationRequest, ResourceAllocationResponse, RingVrfAliasRequest, - RingVrfAliasResponse, SignRawLegacyRequest, SignRawLegacyResponse, SigningRequest, - SigningResponse, + RingVrfAliasResponse, RingVrfProofRequest, RingVrfProofResponse, SignRawLegacyRequest, + SignRawLegacyResponse, SigningRequest, SigningResponse, }; /// v1 messages exchanged with the paired signing host over the encrypted SSO channel. @@ -32,4 +32,6 @@ pub enum RemoteMessage { CreateTransactionLegacyRequest(CreateTransactionLegacyRequest), SignRawLegacyRequest(SignRawLegacyRequest), SignRawLegacyResponse(SignRawLegacyResponse), + RingVrfProofRequest(RingVrfProofRequest), + RingVrfProofResponse(RingVrfProofResponse), } diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index 808d0c38..78b13e6f 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -39,6 +39,7 @@ use crate::host_logic::product_account::{ use crate::host_logic::session::SessionInfo; #[cfg(test)] use crate::host_logic::session::SessionState; +use crate::host_logic::sso::messages::RingVrfError; use crate::runtime::bulletin_rpc::BulletinSubmitError; #[cfg(test)] use crate::subscription::Spawner; @@ -50,8 +51,9 @@ pub(crate) use services::RuntimeServices; pub(crate) use signing_host::{LocalActivation, SigningHost as SigningHostRole}; use authority::{ - AuthorityCancelError, AuthorityError, AuthoritySession, CreateTransactionAuthorityRequest, - SignPayloadAuthorityRequest, SignRawAuthorityRequest, + AccountAliasAuthorityRequest, AuthorityCancelError, AuthorityError, AuthoritySession, + CreateProofAuthorityRequest, CreateTransactionAuthorityRequest, SignPayloadAuthorityRequest, + SignRawAuthorityRequest, }; use futures::{FutureExt, StreamExt, pin_mut}; @@ -140,10 +142,10 @@ use truapi::{CallContext, CallError, CancellationReason, Subscription}; #[cfg(test)] use truapi_platform::Platform; use truapi_platform::{ - AccountAccessReview, AccountAliasReview, CreateTransactionReview, IdentityDisclosureReview, - PermissionAuthorizationRequest, PermissionAuthorizationStatus, PreimageSubmitReview, - ProductContext, SessionUiInfo, SignPayloadReview, SignRawReview, UserConfirmationReview, - normalize_product_identifier, + AccountAccessReview, AccountAliasReview, CreateProofReview, CreateTransactionReview, + IdentityDisclosureReview, PermissionAuthorizationRequest, PermissionAuthorizationStatus, + PreimageSubmitReview, ProductContext, SessionUiInfo, SignPayloadReview, SignRawReview, + UserConfirmationReview, normalize_product_identifier, }; pub(super) const REMOTE_PERMISSION_DENIED_REASON: &str = "Permission denied"; @@ -188,9 +190,10 @@ fn remote_authority_context_until( cx } -async fn remote_authority_call(cx: &CallContext, call: F) -> Result +async fn remote_authority_call(cx: &CallContext, call: F) -> Result where - F: Future>, + F: Future>, + E: From, { let call = call.fuse(); let cancelled = cx.cancel().cancelled().fuse(); @@ -204,7 +207,7 @@ where reason = cancelled => { let error = authority_cancellation_error(cx, reason); let _ = call.await; - Err(error) + Err(error.into()) }, () = timeout => { let reason = CancellationReason::TimedOut { @@ -213,7 +216,7 @@ where cx.cancel().cancel_with_reason(reason.clone()); let error = authority_cancellation_error(cx, reason); let _ = call.await; - Err(error) + Err(error.into()) } } } else { @@ -222,7 +225,7 @@ where reason = cancelled => { let error = authority_cancellation_error(cx, reason); let _ = call.await; - Err(error) + Err(error.into()) }, } } @@ -828,60 +831,111 @@ impl Account for ProductRuntimeHost { cx: &CallContext, request: HostAccountGetAliasRequest, ) -> Result> { - let HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest { product_account_id }) = - request; - let product_account_id = Self::normalize_product_account_id(product_account_id); + let HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest { + context, + ring_location, + }) = request; let Some(session) = self.authority.current_session() else { return Err(CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::NotConnected, + v01::HostAccountGetAliasError::Rejected, ))); }; - let product_account_id = product_account_id.map_err(|()| { - CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::DomainNotValid, - )) - })?; - - let product_id = self.product_id(); - if product_account_id.dot_ns_identifier != product_id { - let confirmed = self - .services - .platform - .confirm_user_action(UserConfirmationReview::AccountAlias(AccountAliasReview { - requesting_product_id: product_id.clone(), - target_product_id: product_account_id.dot_ns_identifier.clone(), - })) - .await - .map_err(|err| CallError::HostFailure { - reason: format!("account alias confirmation failed: {err:?}"), - })?; - if !confirmed { - return Err(CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::Rejected, - ))); - } + let calling_product_id = self.product_id(); + let confirmed = self + .services + .platform + .confirm_user_action(UserConfirmationReview::AccountAlias(AccountAliasReview { + calling_product_id: calling_product_id.clone(), + context: context.clone(), + ring_location: ring_location.clone(), + })) + .await + .map_err(|err| CallError::HostFailure { + reason: format!("account alias confirmation failed: {err:?}"), + })?; + if !confirmed { + return Err(CallError::Domain(HostAccountGetAliasError::V1( + v01::HostAccountGetAliasError::Rejected, + ))); } - self.authority - .account_alias(cx, &session, product_account_id, product_id) - .await - .map(HostAccountGetAliasResponse::V1) - .map_err(|err| { - CallError::Domain(HostAccountGetAliasError::V1( - account_get_error_from_authority(err), - )) - }) + let cx = remote_authority_context(cx); + remote_authority_call( + &cx, + self.authority.account_alias( + &cx, + &session, + AccountAliasAuthorityRequest { + calling_product_id, + context, + ring_location, + }, + ), + ) + .await + .map(HostAccountGetAliasResponse::V1) + .map_err(|err| CallError::Domain(HostAccountGetAliasError::V1(ring_vrf_alias_error(err)))) } #[instrument(skip_all, fields(runtime.method = "account.create_account_proof"))] async fn create_account_proof( &self, - _cx: &CallContext, - _request: HostAccountCreateProofRequest, + cx: &CallContext, + request: HostAccountCreateProofRequest, ) -> Result> { - Err(CallError::Unsupported) + let HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { + context, + ring_location, + message, + }) = request; + + let Some(session) = self.authority.current_session() else { + return Err(CallError::Domain(HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::Rejected, + ))); + }; + + let calling_product_id = self.product_id(); + let confirmed = self + .services + .platform + .confirm_user_action(UserConfirmationReview::CreateProof(CreateProofReview { + calling_product_id: calling_product_id.clone(), + context: context.clone(), + ring_location: ring_location.clone(), + message: message.clone(), + })) + .await + .map_err(|err| CallError::HostFailure { + reason: format!("ring-VRF proof confirmation failed: {err:?}"), + })?; + if !confirmed { + return Err(CallError::Domain(HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::Rejected, + ))); + } + + let cx = remote_authority_context(cx); + remote_authority_call( + &cx, + self.authority.create_proof( + &cx, + &session, + CreateProofAuthorityRequest { + calling_product_id, + context, + ring_location, + message, + }, + ), + ) + .await + .map(HostAccountCreateProofResponse::V1) + .map_err(|err| { + CallError::Domain(HostAccountCreateProofError::V1(ring_vrf_proof_error(err))) + }) } #[instrument(skip_all, fields(runtime.method = "account.get_legacy_accounts"))] @@ -992,16 +1046,21 @@ fn connected_session_ui_info(session: &SessionInfo) -> SessionUiInfo { } } -fn account_get_error_from_authority(err: AuthorityError) -> v01::HostAccountGetError { +fn ring_vrf_alias_error(err: RingVrfError) -> v01::HostAccountGetAliasError { match err { - AuthorityError::Rejected => v01::HostAccountGetError::Rejected, - AuthorityError::Disconnected => v01::HostAccountGetError::NotConnected, - AuthorityError::Cancelled(err) => v01::HostAccountGetError::Unknown { - reason: err.to_string(), - }, - AuthorityError::Unavailable { reason } - | AuthorityError::NotSupported { reason } - | AuthorityError::Unknown { reason } => v01::HostAccountGetError::Unknown { reason }, + RingVrfError::RingNotFound => v01::HostAccountGetAliasError::RingNotFound, + RingVrfError::NotMember => v01::HostAccountGetAliasError::NotMember, + RingVrfError::Rejected => v01::HostAccountGetAliasError::Rejected, + RingVrfError::Unknown { reason } => v01::HostAccountGetAliasError::Unknown { reason }, + } +} + +fn ring_vrf_proof_error(err: RingVrfError) -> v01::HostAccountCreateProofError { + match err { + RingVrfError::RingNotFound => v01::HostAccountCreateProofError::RingNotFound, + RingVrfError::NotMember => v01::HostAccountCreateProofError::NotMember, + RingVrfError::Rejected => v01::HostAccountCreateProofError::Rejected, + RingVrfError::Unknown { reason } => v01::HostAccountCreateProofError::Unknown { reason }, } } @@ -2019,7 +2078,9 @@ impl Notifications for ProductRuntimeHost { #[cfg(test)] mod tests { use super::*; - use crate::host_logic::sso::messages::{RemoteMessageData, v1}; + use crate::host_logic::sso::messages::{ + RemoteMessage, RemoteMessageData, RingVrfAliasResponse, RingVrfProofResponse, v1, + }; use crate::test_support::*; use std::sync::Mutex; use std::sync::atomic::Ordering; @@ -2393,50 +2454,47 @@ mod tests { assert!(matches!( err, CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::NotConnected + v01::HostAccountGetAliasError::Rejected )) )); } #[test] - fn get_account_alias_rejects_invalid_product_identifier() { + fn get_account_alias_rejected_when_user_declines() { let host = ProductRuntimeHost::new(stub_platform(), runtime_config("myapp.dot"), test_spawner()); - let mut session = sso_session_info(); - session.root_entropy_source = session_info().root_entropy_source; - host.test_session_state().set_session(session); + host.test_session_state().set_session(sso_session_info()); let cx = CallContext::new(); let err = futures::executor::block_on( - host.get_account_alias(&cx, account_alias_request("example.com")), + host.get_account_alias(&cx, account_alias_request("myapp.dot")), ) .unwrap_err(); assert!(matches!( err, CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::DomainNotValid + v01::HostAccountGetAliasError::Rejected )) )); } #[test] - fn get_account_alias_same_domain_returns_sso_response() { + fn get_account_alias_returns_sso_alias() { let session = sso_session_info(); let platform = Arc::new(StubPlatform { + account_alias_confirmed: true, sso_response_script: Some(sso_success_response_script( &session, - crate::host_logic::sso::messages::RemoteMessage { + RemoteMessage { message_id: "wallet-alias-1".to_string(), - data: crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasResponse( - crate::host_logic::sso::messages::RingVrfAliasResponse { - responding_to: "alias-1".to_string(), - payload: Ok(v01::HostAccountGetAliasResponse { - context: [9; 32], - alias: vec![1, 2, 3], - }), - }, - ), - ), + data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasResponse( + RingVrfAliasResponse { + responding_to: "alias-1".to_string(), + payload: Ok(v01::ContextualAlias { + context: [9; 32], + alias: vec![1, 2, 3], + }), + }, + )), }, )), ..Default::default() @@ -2456,120 +2514,99 @@ mod tests { assert_eq!(inner.context, [9; 32]); assert_eq!(inner.alias, vec![1, 2, 3]); let message = submitted_remote_message(&platform, &session); - let crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasRequest(request), - ) = message.data + let RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasRequest(request)) = message.data else { panic!("expected ring VRF alias request"); }; - assert_eq!(request.product_account_id.dot_ns_identifier, "myapp.dot"); - assert_eq!(request.product_id, "myapp.dot"); + assert_eq!(request.calling_product_id, "myapp.dot"); + assert_eq!(request.context.product_id, "myapp.dot"); + assert_eq!(request.ring_location.chain_id, [1; 32]); + } + + #[test] + fn create_account_proof_requires_session() { + let host = + ProductRuntimeHost::new(stub_platform(), runtime_config("myapp.dot"), test_spawner()); + let cx = CallContext::new(); + let err = futures::executor::block_on( + host.create_account_proof(&cx, create_proof_request("myapp.dot")), + ) + .unwrap_err(); + assert!(matches!( + err, + CallError::Domain(HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::Rejected + )) + )); } #[test] - fn get_account_alias_normalizes_remote_request_identifier() { + fn create_account_proof_returns_sso_proof() { let session = sso_session_info(); let platform = Arc::new(StubPlatform { + create_proof_confirmed: true, sso_response_script: Some(sso_success_response_script( &session, - crate::host_logic::sso::messages::RemoteMessage { - message_id: "wallet-alias-1".to_string(), - data: crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasResponse( - crate::host_logic::sso::messages::RingVrfAliasResponse { - responding_to: "alias-1".to_string(), - payload: Ok(v01::HostAccountGetAliasResponse { + RemoteMessage { + message_id: "wallet-proof-1".to_string(), + data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofResponse( + RingVrfProofResponse { + responding_to: "proof-1".to_string(), + payload: Ok(v01::HostAccountCreateProofResponse { + proof: vec![0xaa, 0xbb], + contextual_alias: v01::ContextualAlias { context: [9; 32], alias: vec![1, 2, 3], - }), - }, - ), - ), + }, + ring_index: 5, + ring_revision: 7, + }), + }, + )), }, )), ..Default::default() }); let host = ProductRuntimeHost::new( platform.clone(), - runtime_config("MyApp.DOT"), + runtime_config("myapp.dot"), test_spawner(), ); host.test_session_state().set_session(session.clone()); - let cx = CallContext::with_request_id("alias-1".to_string()); - futures::executor::block_on( - host.get_account_alias(&cx, account_alias_request("MyApp.DOT")), + let cx = CallContext::with_request_id("proof-1".to_string()); + let response = futures::executor::block_on( + host.create_account_proof(&cx, create_proof_request("myapp.dot")), ) .unwrap(); + let HostAccountCreateProofResponse::V1(inner) = response; + assert_eq!(inner.proof, vec![0xaa, 0xbb]); + assert_eq!(inner.ring_index, 5); + assert_eq!(inner.ring_revision, 7); let message = submitted_remote_message(&platform, &session); - let crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasRequest(request), - ) = message.data + let RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofRequest(request)) = message.data else { - panic!("expected ring VRF alias request"); + panic!("expected ring VRF proof request"); }; - assert_eq!(request.product_account_id.dot_ns_identifier, "myapp.dot"); - assert_eq!(request.product_id, "myapp.dot"); - } - - #[test] - fn get_account_alias_cross_domain_rejects_when_user_declines() { - let host = - ProductRuntimeHost::new(stub_platform(), runtime_config("myapp.dot"), test_spawner()); - host.test_session_state().set_session(session_info()); - let cx = CallContext::new(); - let err = futures::executor::block_on( - host.get_account_alias(&cx, account_alias_request("other.dot")), - ) - .unwrap_err(); - assert!(matches!( - err, - CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetError::Rejected - )) - )); - } - - #[test] - fn get_account_alias_cross_domain_maps_confirmation_failure_to_host_failure() { - let host = ProductRuntimeHost::new( - Arc::new(StubPlatform { - account_alias_error: Some("modal failed"), - ..Default::default() - }), - runtime_config("myapp.dot"), - test_spawner(), - ); - host.test_session_state().set_session(session_info()); - let cx = CallContext::new(); - let err = futures::executor::block_on( - host.get_account_alias(&cx, account_alias_request("other.dot")), - ) - .unwrap_err(); - assert!( - matches!(err, CallError::HostFailure { reason } if reason.contains("modal failed")) - ); + assert_eq!(request.calling_product_id, "myapp.dot"); + assert_eq!(request.context.product_id, "myapp.dot"); + assert_eq!(request.message, vec![4, 5, 6]); } #[test] - fn get_account_alias_cross_domain_accepts_confirmation_then_returns_sso_response() { + fn create_account_proof_maps_not_member_error() { let session = sso_session_info(); let platform = Arc::new(StubPlatform { - account_alias_confirmed: true, + create_proof_confirmed: true, sso_response_script: Some(sso_success_response_script( &session, - crate::host_logic::sso::messages::RemoteMessage { - message_id: "wallet-alias-2".to_string(), - data: crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasResponse( - crate::host_logic::sso::messages::RingVrfAliasResponse { - responding_to: "alias-2".to_string(), - payload: Ok(v01::HostAccountGetAliasResponse { - context: [8; 32], - alias: vec![4, 5, 6], - }), - }, - ), - ), + RemoteMessage { + message_id: "wallet-proof-1".to_string(), + data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofResponse( + RingVrfProofResponse { + responding_to: "proof-1".to_string(), + payload: Err(RingVrfError::NotMember), + }, + )), }, )), ..Default::default() @@ -2579,21 +2616,17 @@ mod tests { runtime_config("myapp.dot"), test_spawner(), ); - host.test_session_state().set_session(session.clone()); - let cx = CallContext::with_request_id("alias-2".to_string()); - let response = futures::executor::block_on( - host.get_account_alias(&cx, account_alias_request("other.dot")), + host.test_session_state().set_session(session); + let cx = CallContext::with_request_id("proof-1".to_string()); + let err = futures::executor::block_on( + host.create_account_proof(&cx, create_proof_request("myapp.dot")), ) - .unwrap(); - let HostAccountGetAliasResponse::V1(inner) = response; - assert_eq!(inner.context, [8; 32]); - assert_eq!(inner.alias, vec![4, 5, 6]); - let message = submitted_remote_message(&platform, &session); + .unwrap_err(); assert!(matches!( - message.data, - crate::host_logic::sso::messages::RemoteMessageData::V1( - crate::host_logic::sso::messages::v1::RemoteMessage::RingVrfAliasRequest(_) - ) + err, + CallError::Domain(HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::NotMember + )) )); } diff --git a/rust/crates/truapi-server/src/runtime/authority.rs b/rust/crates/truapi-server/src/runtime/authority.rs index 60db64cb..8d0af644 100644 --- a/rust/crates/truapi-server/src/runtime/authority.rs +++ b/rust/crates/truapi-server/src/runtime/authority.rs @@ -9,17 +9,18 @@ use core::fmt; use core::time::Duration; use std::sync::Arc; use truapi::latest::{ - HostAccountGetAliasResponse, HostCreateTransactionResponse, + HostAccountCreateProofResponse, HostAccountGetAliasResponse, HostCreateTransactionResponse, HostRequestResourceAllocationRequest, HostRequestResourceAllocationResponse, HostSignPayloadRequest, HostSignPayloadResponse, HostSignPayloadWithLegacyAccountRequest, HostSignRawRequest, HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, - ProductAccountId, ProductAccountTxPayload, + ProductAccountId, ProductAccountTxPayload, ProductProofContext, RingLocation, }; use truapi::versioned::account::{HostRequestLoginError, HostRequestLoginResponse}; use truapi::{CallContext, CallError, CancellationReason}; use truapi_platform::ProductContext; use crate::host_logic::session::{SessionInfo, SessionState}; +use crate::host_logic::sso::messages::RingVrfError; use crate::host_logic::statement_store::statement_public_key_from_secret; /// Secret key allocated for Bulletin preimage submission. @@ -123,6 +124,17 @@ impl AuthorityError { } } +impl From for RingVrfError { + fn from(err: AuthorityError) -> Self { + match err { + AuthorityError::Rejected => RingVrfError::Rejected, + other => RingVrfError::Unknown { + reason: other.reason(), + }, + } + } +} + /// Cancellation cause for an account-authority call. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct AuthorityCancelError { @@ -209,6 +221,30 @@ pub(crate) enum CreateTransactionAuthorityRequest { }, } +/// Contextual-alias request forwarded to the account authority (RFC 0004). +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct AccountAliasAuthorityRequest { + /// Calling product, so the Account Holder can scope context derivation. + pub calling_product_id: String, + /// Product-scoped context the derived alias is bound to. + pub context: ProductProofContext, + /// Ring whose member key the Account Holder selects. + pub ring_location: RingLocation, +} + +/// Ring-VRF proof request forwarded to the account authority (RFC 0004). +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct CreateProofAuthorityRequest { + /// Calling product, so the Account Holder can scope context derivation. + pub calling_product_id: String, + /// Product-scoped context the derived alias is bound to. + pub context: ProductProofContext, + /// Ring whose member key the Account Holder selects. + pub ring_location: RingLocation, + /// Opaque message bound into the proof. + pub message: Vec, +} + /// Statement-store allowance signing material held by the authority layer. #[derive(Clone, PartialEq, Eq)] pub(crate) struct StatementStoreAllowanceKey { @@ -288,14 +324,27 @@ pub(crate) trait ProductAuthority: Send + Sync { request: CreateTransactionAuthorityRequest, ) -> Result; - /// Request an alias proof for a product account in another product context. + /// Derive a product-scoped contextual alias for a ring (RFC 0004). + /// + /// The Account Holder selects the member key for `ring_location` and derives + /// the alias bound to `context`; `create_proof` derives the same alias. async fn account_alias( &self, cx: &CallContext, session: &AuthoritySession, - product_account_id: ProductAccountId, - requesting_product_id: String, - ) -> Result; + request: AccountAliasAuthorityRequest, + ) -> Result; + + /// Create a ring-VRF proof bound to a context and message (RFC 0004). + /// + /// Uses the same ring resolution and member-key selection as `account_alias`, + /// so the returned `contextual_alias` matches that method's output. + async fn create_proof( + &self, + cx: &CallContext, + session: &AuthoritySession, + request: CreateProofAuthorityRequest, + ) -> Result; /// Ask the account authority to allocate product-scoped resources. async fn allocate_resources( diff --git a/rust/crates/truapi-server/src/runtime/pairing_host.rs b/rust/crates/truapi-server/src/runtime/pairing_host.rs index 050c394f..6d3603a9 100644 --- a/rust/crates/truapi-server/src/runtime/pairing_host.rs +++ b/rust/crates/truapi-server/src/runtime/pairing_host.rs @@ -15,9 +15,10 @@ use sso_channel::SsoDisconnectMonitor; use super::allowances::{self, AllowanceCacheKey, AllowanceResource}; use super::auth_state::AuthStateMachine; use super::authority::{ - AuthorityError, AuthoritySession, BulletinAllowanceKey, CreateTransactionAuthorityRequest, - ProductAuthority, SignPayloadAuthorityRequest, SignRawAuthorityRequest, - StatementStoreAllowanceKey, authority_session, require_current_session, + AccountAliasAuthorityRequest, AuthorityError, AuthoritySession, BulletinAllowanceKey, + CreateProofAuthorityRequest, CreateTransactionAuthorityRequest, ProductAuthority, + SignPayloadAuthorityRequest, SignRawAuthorityRequest, StatementStoreAllowanceKey, + authority_session, require_current_session, }; use super::connected_session_ui_info; use super::identity::resolve_session_identity_with_chain; @@ -29,6 +30,7 @@ use crate::chain_runtime::ChainRuntime; use crate::host_logic::entropy::derive_product_entropy_from_source; use crate::host_logic::session::{SessionInfo, SessionState, encode_persisted_session}; use crate::host_logic::session_store::SessionStoreChangeNotifier; +use crate::host_logic::sso::messages::RingVrfError; use crate::subscription::Spawner; use futures::StreamExt; @@ -739,12 +741,20 @@ impl PairingHost { &self, cx: &CallContext, session: &AuthoritySession, - product_account_id: v01::ProductAccountId, - requesting_product_id: String, - ) -> Result { + request: AccountAliasAuthorityRequest, + ) -> Result { let session = self.current_private_session(session)?; - self.remote_account_alias(cx, &session, product_account_id, requesting_product_id) - .await + self.remote_account_alias(cx, &session, request).await + } + + async fn create_proof( + &self, + cx: &CallContext, + session: &AuthoritySession, + request: CreateProofAuthorityRequest, + ) -> Result { + let session = self.current_private_session(session)?; + self.remote_create_proof(cx, &session, request).await } async fn allocate_resources( @@ -899,11 +909,18 @@ impl ProductAuthority for PairingHost { &self, cx: &CallContext, session: &AuthoritySession, - product_account_id: v01::ProductAccountId, - requesting_product_id: String, - ) -> Result { - PairingHost::account_alias(self, cx, session, product_account_id, requesting_product_id) - .await + request: AccountAliasAuthorityRequest, + ) -> Result { + PairingHost::account_alias(self, cx, session, request).await + } + + async fn create_proof( + &self, + cx: &CallContext, + session: &AuthoritySession, + request: CreateProofAuthorityRequest, + ) -> Result { + PairingHost::create_proof(self, cx, session, request).await } async fn allocate_resources( diff --git a/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs b/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs index cf5a04e4..2edd1591 100644 --- a/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs +++ b/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs @@ -1,8 +1,9 @@ //! SSO statement-store channel to the paired remote signing host. use super::super::authority::{ - AuthorityCancelError, AuthorityError, BulletinAllowanceKey, CreateTransactionAuthorityRequest, - SignPayloadAuthorityRequest, SignRawAuthorityRequest, StatementStoreAllowanceKey, + AccountAliasAuthorityRequest, AuthorityCancelError, AuthorityError, BulletinAllowanceKey, + CreateProofAuthorityRequest, CreateTransactionAuthorityRequest, SignPayloadAuthorityRequest, + SignRawAuthorityRequest, StatementStoreAllowanceKey, }; use super::super::sso_remote::{ RemoteResponseWait, SSO_LOCAL_DISCONNECT_REASON, SSO_PEER_DISCONNECT_REASON, @@ -14,10 +15,11 @@ use super::AuthorityRequestKind; use super::PairingHost; use crate::host_logic::session::{SessionInfo, SessionState, SsoSessionInfo}; use crate::host_logic::sso::messages::{ - OnExistingAllowancePolicy, RemoteMessage, RemoteMessageData, SsoAllocatedResource, - SsoAllocationOutcome, SsoRemoteResponse, SsoSessionStatement, alias_request_message, - build_outgoing_request_statement, create_transaction_message, decode_sso_session_statement, - resource_allocation_message, sign_payload_message, sign_raw_message, v1, + OnExistingAllowancePolicy, RemoteMessage, RemoteMessageData, RingVrfError, + SsoAllocatedResource, SsoAllocationOutcome, SsoRemoteResponse, SsoSessionStatement, + alias_request_message, build_outgoing_request_statement, create_transaction_message, + decode_sso_session_statement, proof_request_message, resource_allocation_message, + sign_payload_message, sign_raw_message, v1, }; use crate::host_logic::statement_store::parse_new_statements_result; @@ -28,13 +30,17 @@ use truapi::{CallContext, latest}; const UNEXPECTED_SSO_SIGNING_RESPONSE: &str = "Unexpected SSO response for signing request"; const UNEXPECTED_SSO_TRANSACTION_RESPONSE: &str = "Unexpected SSO response for transaction request"; +const UNEXPECTED_SSO_ALIAS_RESPONSE: &str = "Unexpected SSO response for account alias request"; +const UNEXPECTED_SSO_PROOF_RESPONSE: &str = "Unexpected SSO response for ring-VRF proof request"; #[derive(Clone, Copy, Debug, derive_more::Display)] enum RemoteAction { #[display("{_0}")] Signing(AuthorityRequestKind), #[display("account-alias")] - AccountAlias, + RingVrfAlias, + #[display("ring-vrf-proof")] + RingVrfProof, #[display("resource-allocation")] ResourceAllocation, } @@ -356,25 +362,51 @@ impl PairingHost { &self, cx: &CallContext, session: &SessionInfo, - product_account_id: latest::ProductAccountId, - requesting_product_id: String, - ) -> Result { + request: AccountAliasAuthorityRequest, + ) -> Result { let message_id = sso_message_id(); let message = alias_request_message( - message_id.clone(), - product_account_id, - requesting_product_id, + message_id, + request.calling_product_id, + request.context, + request.ring_location, ); let response = self - .submit_remote_message(cx, session, RemoteAction::AccountAlias, message) + .submit_remote_message(cx, session, RemoteAction::RingVrfAlias, message) .await .map_err(remote_authority_error)?; let SsoRemoteResponse::RingVrfAlias(response) = response else { - return Err(AuthorityError::Unknown { - reason: "Unexpected SSO response for account alias request".to_string(), + return Err(RingVrfError::Unknown { + reason: UNEXPECTED_SSO_ALIAS_RESPONSE.to_string(), + }); + }; + response.payload + } + + pub(super) async fn remote_create_proof( + &self, + cx: &CallContext, + session: &SessionInfo, + request: CreateProofAuthorityRequest, + ) -> Result { + let message_id = sso_message_id(); + let message = proof_request_message( + message_id, + request.calling_product_id, + request.context, + request.ring_location, + request.message, + ); + let response = self + .submit_remote_message(cx, session, RemoteAction::RingVrfProof, message) + .await + .map_err(remote_authority_error)?; + let SsoRemoteResponse::RingVrfProof(response) = response else { + return Err(RingVrfError::Unknown { + reason: UNEXPECTED_SSO_PROOF_RESPONSE.to_string(), }); }; - response.payload.map_err(remote_authority_error) + response.payload } pub(super) async fn remote_allocate_resources( diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index 14f3cb93..886853b8 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -13,9 +13,10 @@ use std::sync::{Arc, Mutex}; pub(crate) use local_activation::LocalActivation; use super::authority::{ - AuthorityError, AuthoritySession, BulletinAllowanceKey, CreateTransactionAuthorityRequest, - ProductAuthority, SignPayloadAuthorityRequest, SignRawAuthorityRequest, - StatementStoreAllowanceKey, authority_session, require_current_session, + AccountAliasAuthorityRequest, AuthorityError, AuthoritySession, BulletinAllowanceKey, + CreateProofAuthorityRequest, CreateTransactionAuthorityRequest, ProductAuthority, + SignPayloadAuthorityRequest, SignRawAuthorityRequest, StatementStoreAllowanceKey, + authority_session, require_current_session, }; use super::connected_session_ui_info; use crate::host_logic::entropy::derive_product_entropy; @@ -25,6 +26,7 @@ use crate::host_logic::product_account::{ derive_root_keypair_from_entropy, }; use crate::host_logic::session::SessionState; +use crate::host_logic::sso::messages::RingVrfError; use crate::runtime::auth_state::AuthStateMachine; use truapi::versioned::account::{HostRequestLoginError, HostRequestLoginResponse}; @@ -212,14 +214,24 @@ impl ProductAuthority for SigningHost { &self, _cx: &CallContext, _session: &AuthoritySession, - _product_account_id: v01::ProductAccountId, - _requesting_product_id: String, - ) -> Result { - Err(AuthorityError::Unavailable { + _request: AccountAliasAuthorityRequest, + ) -> Result { + Err(RingVrfError::Unknown { reason: "signing host: ring-VRF alias derivation not yet implemented".to_string(), }) } + async fn create_proof( + &self, + _cx: &CallContext, + _session: &AuthoritySession, + _request: CreateProofAuthorityRequest, + ) -> Result { + Err(RingVrfError::Unknown { + reason: "signing host: ring-VRF proof generation not yet implemented".to_string(), + }) + } + async fn allocate_resources( &self, _cx: &CallContext, @@ -858,18 +870,6 @@ mod tests { let session = authority.current_session().expect("connected"); let cx = CallContext::new(); - let alias = futures::executor::block_on(authority.account_alias( - &cx, - &session, - v01::ProductAccountId { - dot_ns_identifier: "other.dot".to_string(), - derivation_index: 0, - }, - "myapp.dot".to_string(), - )) - .expect_err("alias deferred"); - assert!(matches!(alias, AuthorityError::Unavailable { .. })); - let alloc = futures::executor::block_on(authority.allocate_resources( &cx, &session, diff --git a/rust/crates/truapi-server/src/test_support.rs b/rust/crates/truapi-server/src/test_support.rs index 62bd9baf..02c9a17a 100644 --- a/rust/crates/truapi-server/src/test_support.rs +++ b/rust/crates/truapi-server/src/test_support.rs @@ -30,7 +30,7 @@ use parity_scale_codec::{Decode, Encode}; use schnorrkel::{ExpansionMode, MiniSecretKey}; use sha2::Sha256; use truapi::v01; -use truapi::versioned::account::HostAccountGetAliasRequest; +use truapi::versioned::account::{HostAccountCreateProofRequest, HostAccountGetAliasRequest}; use truapi::versioned::resource_allocation::HostRequestResourceAllocationRequest; use truapi_platform::{ AccountAccessReview, AuthPresenter, AuthState, ChainProvider, @@ -72,6 +72,8 @@ pub(crate) struct StubPlatform { pub(crate) remote_permission_denied: bool, pub(crate) account_alias_confirmed: bool, pub(crate) account_alias_error: Option<&'static str>, + pub(crate) create_proof_confirmed: bool, + pub(crate) create_proof_error: Option<&'static str>, pub(crate) account_access_confirmed: bool, pub(crate) account_access_error: Option<&'static str>, pub(crate) account_access_reviews: Arc>>, @@ -588,13 +590,6 @@ pub(crate) fn account_id(identifier: &str, derivation_index: u32) -> v01::Produc } } -/// Account-alias request fixture for a product identifier. -pub(crate) fn account_alias_request(identifier: &str) -> HostAccountGetAliasRequest { - HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest { - product_account_id: account_id(identifier, 0), - }) -} - /// Raw signing payload fixture. pub(crate) fn raw_payload() -> v01::RawPayload { v01::RawPayload::Bytes { @@ -602,6 +597,39 @@ pub(crate) fn raw_payload() -> v01::RawPayload { } } +/// Product-scoped proof context fixture for `product_id`. +pub(crate) fn product_proof_context(product_id: &str) -> v01::ProductProofContext { + v01::ProductProofContext { + product_id: product_id.to_string(), + suffix: vec![7], + } +} + +/// Ring-location fixture addressing a single pallet-instance ring. +pub(crate) fn ring_location_fixture() -> v01::RingLocation { + v01::RingLocation { + chain_id: [1; 32], + junctions: vec![v01::RingLocationJunction::PalletInstance(42)], + } +} + +/// Contextual-alias request fixture for `product_id`. +pub(crate) fn account_alias_request(product_id: &str) -> HostAccountGetAliasRequest { + HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest { + context: product_proof_context(product_id), + ring_location: ring_location_fixture(), + }) +} + +/// Ring-VRF proof request fixture for `product_id`. +pub(crate) fn create_proof_request(product_id: &str) -> HostAccountCreateProofRequest { + HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { + context: product_proof_context(product_id), + ring_location: ring_location_fixture(), + message: vec![4, 5, 6], + }) +} + /// Structured signing payload fixture. pub(crate) fn sign_payload_data() -> v01::HostSignPayloadData { v01::HostSignPayloadData { @@ -908,6 +936,9 @@ fn retarget_sso_response(mut response: RemoteMessage, message_id: &str) -> Remot RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasResponse(response)) => { response.responding_to = message_id.to_string(); } + RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofResponse(response)) => { + response.responding_to = message_id.to_string(); + } RemoteMessageData::V1(v1::RemoteMessage::SignRawLegacyResponse(response)) => { response.responding_to = message_id.to_string(); } @@ -1252,6 +1283,9 @@ impl UserConfirmation for StubPlatform { UserConfirmationReview::AccountAlias(_) => { (self.account_alias_error, self.account_alias_confirmed) } + UserConfirmationReview::CreateProof(_) => { + (self.create_proof_error, self.create_proof_confirmed) + } UserConfirmationReview::AccountAccess(review) => { self.account_access_reviews .lock() diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 16deb1f1..f742a88b 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -180,19 +180,18 @@ fn version_index(version: u8) -> u8 { } #[test] -fn deferred_account_proof_returns_framework_unsupported() { +fn account_proof_declined_confirmation_returns_rejected() { let core = make_core(); let request = account::HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { - product_account_id: v01::ProductAccountId { - dot_ns_identifier: "myapp.dot".to_string(), - derivation_index: 0, + context: v01::ProductProofContext { + product_id: "myapp.dot".to_string(), + suffix: Vec::new(), }, ring_location: v01::RingLocation { - genesis_hash: vec![0u8; 32], - ring_root_hash: vec![1u8; 32], - hints: None, + chain_id: [0u8; 32], + junctions: vec![v01::RingLocationJunction::PalletInstance(0)], }, - context: Vec::new(), + message: Vec::new(), }); let ids = request_ids("account_create_account_proof").expect("known request method"); @@ -208,7 +207,12 @@ fn deferred_account_proof_returns_framework_unsupported() { ); assert_eq!(response.request_id, "p:account-proof"); assert_eq!(response.payload.id, ids.response_id); - assert_eq!(response.payload.value, vec![0x00u8, 0x01u8, 0x02u8]); + // The wire-shape platform declines the confirmation prompt, so the proof + // request maps to a `Rejected` domain error in the standard Result-Err envelope. + let expected = versioned_result_err_payload(account::HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::Rejected, + )); + assert_eq!(response.payload.value, expected); } #[test] diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 1ccaac15..6abe004f 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -62,13 +62,16 @@ pub trait Account: Send + Sync { Err(CallError::unavailable()) } - /// Retrieve a contextual alias for a product account. + /// Retrieve the contextual alias for a context and ring. /// /// ```ts + /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// /// const result = await truapi.account.getAccountAlias({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, + /// context: ["truapi-playground.dot", "0x00"], + /// ringLocation: { + /// chainId: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// junctions: [{ tag: "PalletInstance", value: 42 }], /// }, /// }); /// assert(result.isOk(), "getAccountAlias failed:", result); @@ -83,21 +86,18 @@ pub trait Account: Send + Sync { Err(CallError::unavailable()) } - /// Generate a ring VRF proof for a product account. + /// Generate a ring VRF proof; the host selects the member key for the ring. /// /// ```ts /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.account.createAccountProof({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// context: ["truapi-playground.dot", "0x00"], /// ringLocation: { - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, - /// ringRootHash: "0x...", + /// chainId: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// junctions: [{ tag: "PalletInstance", value: 42 }], /// }, - /// context: "0x48656c6c6f", + /// message: "0x48656c6c6f", /// }); /// assert(result.isOk(), "createAccountProof failed:", result); /// console.log("account proof created:", result.value); diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index a8cd326d..2708a906 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -27,14 +27,17 @@ pub mod latest { use crate::versioned::{self, Versioned}; pub use crate::v01::{ - AccountId, AllocatableResource, AllocationOutcome, GenericError, HostSignPayloadData, - NotificationId, OperationStartedResult, ProductAccountId, RawPayload, RemotePermission, - RuntimeApi, RuntimeSpec, RuntimeType, StorageQueryItem, StorageQueryType, - StorageResultItem, ThemeVariant, TxPayloadExtension, + AccountId, AllocatableResource, AllocationOutcome, ContextualAlias, GenericError, + HostSignPayloadData, NotificationId, OperationStartedResult, ProductAccountId, + ProductProofContext, RawPayload, RemotePermission, RingLocation, RuntimeApi, RuntimeSpec, + RuntimeType, StorageQueryItem, StorageQueryType, StorageResultItem, ThemeVariant, + TxPayloadExtension, }; pub type LatestOf = ::Latest; + pub type HostAccountCreateProofResponse = + LatestOf; pub type HostAccountGetAliasResponse = LatestOf; pub type HostCreateTransactionResponse = diff --git a/rust/crates/truapi/src/v01/account.rs b/rust/crates/truapi/src/v01/account.rs index f3b928ec..2cfc1198 100644 --- a/rust/crates/truapi/src/v01/account.rs +++ b/rust/crates/truapi/src/v01/account.rs @@ -1,3 +1,4 @@ +use crate::v01::transaction::GenesisHash; use parity_scale_codec::{Decode, Encode}; /// Identifies a product-specific account by combining a dotNS domain name with a @@ -32,40 +33,54 @@ pub struct ProductAccount { /// A privacy-preserving alias derived via ring VRF, bound to a specific context. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct HostAccountGetAliasResponse { - /// 32-byte context identifier. +pub struct ContextualAlias { + /// 32-byte context identifier the alias is bound to. pub context: [u8; 32], /// Ring VRF alias (variable length). pub alias: Vec, } -/// Hints for locating a ring on-chain. +/// A single step in a [`RingLocation`] path, addressing a ring within a chain. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct RingLocationHint { - /// Optional pallet instance index. - pub pallet_instance: Option, +pub enum RingLocationJunction { + /// Pallet instance hosting the ring collection. + PalletInstance(u8), + /// Ring collection identifier within the pallet. + CollectionId(Vec), } -/// Locates a specific ring on a specific chain for ring VRF operations. +/// Locates a ring for ring VRF operations using only identifiers that are +/// stable across membership changes. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RingLocation { - /// Chain genesis hash. - pub genesis_hash: Vec, - /// Root hash of the ring. - pub ring_root_hash: Vec, - /// Optional location hints. - pub hints: Option, + /// Genesis hash of the chain hosting the ring. + pub chain_id: GenesisHash, + /// Path addressing the ring within the chain. + pub junctions: Vec, } -/// Request to create a ring VRF proof for a product account. +/// A product-scoped proof context: a product and a context within it. +/// +/// Hashed (with a `product//` prefix) into the 32-byte context bound +/// to a ring VRF proof, so contexts cannot collide across products and the same +/// member key under different contexts yields unlinkable aliases. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct ProductProofContext { + /// dotNS product identifier (e.g. `"my-product.dot"`) scoping the context. + pub product_id: String, + /// Arbitrary-byte suffix distinguishing contexts within the product. + pub suffix: Vec, +} + +/// Request to create a ring VRF proof. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountCreateProofRequest { - /// Product account that should create the proof. - pub product_account_id: ProductAccountId, - /// Ring location to use for proof generation. + /// Product-scoped context the derived alias is bound to. + pub context: ProductProofContext, + /// Ring to generate the proof against; the host selects the member key. pub ring_location: RingLocation, - /// Context bytes bound to the proof. - pub context: Vec, + /// Opaque message bound into the proof. + pub message: Vec, } /// User's authentication state. @@ -118,6 +133,21 @@ pub enum HostAccountGetError { pub enum HostAccountCreateProofError { /// Ring not available at the specified location. RingNotFound, + /// The selected member key is not a member of the requested ring. + NotMember, + /// User or host rejected. + Rejected, + /// Catch-all. + Unknown { reason: String }, +} + +/// Error returned when contextual alias derivation fails. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum HostAccountGetAliasError { + /// Ring not available at the specified location. + RingNotFound, + /// The selected member key is not a member of the requested ring. + NotMember, /// User or host rejected. Rejected, /// Catch-all. @@ -156,18 +186,27 @@ pub enum HostGetUserIdError { Unknown { reason: String }, } -/// Request to retrieve a contextual alias for a product account. +/// Request to retrieve the contextual alias for a context and ring. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountGetAliasRequest { - /// Product account to derive the alias for. - pub product_account_id: ProductAccountId, + /// Product-scoped context to derive the alias for. + pub context: ProductProofContext, + /// Ring whose member key the host should use; matches `create_proof`. + pub ring_location: RingLocation, } -/// Response containing a ring VRF proof. +/// Response containing a ring VRF proof and the values needed to verify it +/// against a downstream precompile. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountCreateProofResponse { /// Variable-length ring VRF proof bytes. pub proof: Vec, + /// Alias derived for the request's context. + pub contextual_alias: ContextualAlias, + /// Index of the selected member key within the ring. + pub ring_index: u32, + /// Ring revision the proof was generated against. + pub ring_revision: u32, } /// Response containing all legacy (user-imported) accounts owned by the user. diff --git a/rust/crates/truapi/src/versioned/account.rs b/rust/crates/truapi/src/versioned/account.rs index 1d82fc62..8ce339a9 100644 --- a/rust/crates/truapi/src/versioned/account.rs +++ b/rust/crates/truapi/src/versioned/account.rs @@ -7,8 +7,8 @@ truapi_macros::versioned_type! { pub enum HostAccountGetResponse { V1 => v01::HostAccountGetResponse } pub enum HostAccountGetError { V1 => v01::HostAccountGetError } pub enum HostAccountGetAliasRequest { V1 => v01::HostAccountGetAliasRequest } - pub enum HostAccountGetAliasResponse { V1 => v01::HostAccountGetAliasResponse } - pub enum HostAccountGetAliasError { V1 => v01::HostAccountGetError } + pub enum HostAccountGetAliasResponse { V1 => v01::ContextualAlias } + pub enum HostAccountGetAliasError { V1 => v01::HostAccountGetAliasError } pub enum HostAccountCreateProofRequest { V1 => v01::HostAccountCreateProofRequest } pub enum HostAccountCreateProofResponse { V1 => v01::HostAccountCreateProofResponse } pub enum HostAccountCreateProofError { V1 => v01::HostAccountCreateProofError }