From e70c484cb0d91eadeaca916166f2a2b986f1253d Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 30 Jun 2026 08:51:27 +0200 Subject: [PATCH 1/2] RFC-0022: remove dotNsIdentifier from default account access Drop dotNsIdentifier from the seven local account, signing, and statement-store request bodies, callers pass only derivationIndex and the host resolves the caller's own domain. --- .../0022-remove-dotns-from-default-access.md | 229 ++++++++++++++++++ docs/rfcs/_index.md | 1 + rust/crates/truapi/src/api/account.rs | 19 +- rust/crates/truapi/src/api/signing.rs | 9 +- rust/crates/truapi/src/api/statement_store.rs | 15 +- rust/crates/truapi/src/v01/account.rs | 12 +- rust/crates/truapi/src/v01/signing.rs | 10 +- rust/crates/truapi/src/v01/statement_store.rs | 6 +- rust/crates/truapi/src/v01/transaction.rs | 11 +- 9 files changed, 256 insertions(+), 56 deletions(-) create mode 100644 docs/rfcs/0022-remove-dotns-from-default-access.md diff --git a/docs/rfcs/0022-remove-dotns-from-default-access.md b/docs/rfcs/0022-remove-dotns-from-default-access.md new file mode 100644 index 00000000..79b5e5aa --- /dev/null +++ b/docs/rfcs/0022-remove-dotns-from-default-access.md @@ -0,0 +1,229 @@ +# RFC-0022: Remove `dotNsIdentifier` from default account access + +| | | +| --------------- | --------------------------------------------------------------------- | +| **RFC Number** | 22 | +| **Start Date** | 2026-06-30 | +| **Description** | Default account, signing, and statement-store calls address only the caller's own accounts, by `derivationIndex`. | +| **Authors** | Valentin Fernandez | + +## Summary + +The account, signing, and statement-store methods that operate on a product account today take a +`ProductAccountId { dotNsIdentifier, derivationIndex }`. The `dotNsIdentifier` is dropped from these +calls: each resolves against the caller's own dotNS domain, and products pass only `derivationIndex`. + +```typescript +// Before +await truapi.account.getAccount({ + productAccountId: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: 0 }, +}); + +// After +await truapi.account.getAccount({ derivationIndex: 0 }); +``` + +This RFC covers seven call sites: + +- [`getAccount()`](https://paritytech.github.io/truapi/v/main/method/Account/get_account) +- [`getAccountAlias()`](https://paritytech.github.io/truapi/v/main/method/Account/get_account_alias) +- [`createAccountProof()`](https://paritytech.github.io/truapi/v/main/method/Account/create_account_proof) +- [`createTransaction()`](https://paritytech.github.io/truapi/v/main/method/Signing/create_transaction) +- [`signRaw()`](https://paritytech.github.io/truapi/v/main/method/Signing/sign_raw) +- [`signPayload()`](https://paritytech.github.io/truapi/v/main/method/Signing/sign_payload) +- [`statementStore.createProof()`](https://paritytech.github.io/truapi/v/main/method/StatementStore/create_proof) + +`ProductAccountId` itself is retained: a follow-up RFC reuses it as the explicit target identifier for +opt-in cross-product access (see [Future Directions](#future-directions-and-related-material)). + +## Motivation + +The host already knows the calling product's dotNS domain, so making the product pass `dotNsIdentifier` +is redundant — the host can resolve it on its own from the authenticated caller. + +The parameter also has no useful value other than the caller's own domain. The host only ever acts on +the domain the product is currently running under: if a product passes any other dotNS address, the +host rejects the call as invalid. For example, a product running on `truapi-playground.dot` that calls +`signRaw()` with a different dotNS address (say `another-product.dot`) gets back an *invalid* response +— it cannot reach the other product's account this way. So `dotNsIdentifier` can only ever echo the +caller's own domain (redundant) or name a domain the host refuses (a dead value). + +Either way the parameter does not belong on the default call surface. Products should address only +their own accounts, by `derivationIndex`, and the host — the security boundary — should be the sole +authority on which domain a call resolves against, rather than taking it from the caller at all. + +Split out from [#222](https://github.com/paritytech/truapi/issues/222); tracked in +[#243](https://github.com/paritytech/truapi/issues/243). + +## Stakeholders + +- **Product developers** — construct slimmer calls with no domain field to supply or get wrong; calls + address the product's own accounts. +- **Host developers** — resolve the account's domain from the authenticated caller instead of trusting + a parameter on the request. + +## Explanation + +`ProductAccountId` is **unchanged** — it stays available for the explicit external-access surface a +follow-up RFC introduces: + +```rust +struct ProductAccountId { + dot_ns_identifier: String, + derivation_index: u32, +} +``` + +Each of the seven request types drops its `ProductAccountId`-typed field and carries a bare +`derivation_index: u32` instead. The host fills in the caller's own domain. + +Before: + +```rust +struct HostAccountGetRequest { product_account_id: ProductAccountId } +struct HostAccountGetAliasRequest { product_account_id: ProductAccountId } +struct HostAccountCreateProofRequest { + product_account_id: ProductAccountId, + ring_location: RingLocation, + context: Vec, +} +struct ProductAccountTxPayload { + signer: ProductAccountId, + genesis_hash: GenesisHash, + call_data: Vec, + extensions: Vec, + tx_ext_version: u8, +} +struct HostSignRawRequest { account: ProductAccountId, payload: RawPayload } +struct HostSignPayloadRequest { account: ProductAccountId, payload: HostSignPayloadData } +struct RemoteStatementStoreCreateProofRequest { + product_account_id: ProductAccountId, + statement: Statement, +} +``` + +After: + +```rust +struct HostAccountGetRequest { derivation_index: u32 } +struct HostAccountGetAliasRequest { derivation_index: u32 } +struct HostAccountCreateProofRequest { + derivation_index: u32, + ring_location: RingLocation, + context: Vec, +} +struct ProductAccountTxPayload { + derivation_index: u32, + genesis_hash: GenesisHash, + call_data: Vec, + extensions: Vec, + tx_ext_version: u8, +} +struct HostSignRawRequest { derivation_index: u32, payload: RawPayload } +struct HostSignPayloadRequest { derivation_index: u32, payload: HostSignPayloadData } +struct RemoteStatementStoreCreateProofRequest { + derivation_index: u32, + statement: Statement, +} +``` + +In TypeScript the nested identifier object collapses to a single field: + +```typescript +await truapi.account.getAccount({ derivationIndex: 0 }); +await truapi.account.getAccountAlias({ derivationIndex: 0 }); +await truapi.account.createAccountProof({ derivationIndex: 0, ringLocation, context: "0x" }); +await truapi.signing.createTransaction({ derivationIndex: 0, genesisHash, callData, extensions: [], txExtVersion: 0 }); +await truapi.signing.signRaw({ derivationIndex: 0, payload }); +await truapi.signing.signPayload({ derivationIndex: 0, payload }); +await truapi.statementStore.createProof({ derivationIndex: 0, statement }); +``` + +The change is made in place on `v01`; no `v02` of these messages is introduced. Their wire IDs are +unchanged — only the request body shape changes. The `*WithLegacyAccount` signing variants are +unaffected: they identify a legacy account by raw `AccountId`, never by `dotNsIdentifier`. + +`statementStore.createProofAuthorized()` already takes only the statement (it uses a pre-allocated +allowance account) and so needs no change. The deprecated `statementStore.createProof()` is the +statement-store call still carrying `ProductAccountId`, and is covered here for consistency. + +## Drawbacks + +These default methods only ever address the caller's own accounts. Some products may have a legitimate +reason to read, and possibly sign with, *another* product's account — a capability the host does not +expose today, since it rejects any domain other than the caller's. Rather than overloading +`dotNsIdentifier` to provide it, that capability is introduced explicitly, behind a permission, in the +follow-up RFC (see [Future Directions](#future-directions-and-related-material)). + +## Testing, Security, and Privacy + +Removing `dotNsIdentifier` makes the host the sole authority on which domain a call resolves against. +The host already rejects any domain other than the caller's, so a product cannot reach another +product's account today; dropping the field removes that redundant, rejected input entirely rather than +relying on the host to validate a caller-supplied domain on every call. The trust boundary becomes +structural — there is no domain field to validate or get wrong. + +## Performance, Ergonomics, and Compatibility + +### Ergonomics + +Calls are smaller and harder to misuse — there is no domain string to get wrong, and the common case +(a product's own account) needs only an index. + +### Compatibility + +This is a breaking wire change to seven `v01` request bodies, and the protocol gives it **no version +signal**. Each message is wrapped in a `versioned_type!` envelope whose SCALE discriminant byte is the +version (`V1` = codec index `0`), but every one of the protocol's envelopes is currently single-variant +`V1` — there is no live multi-version support and no cross-version conversion anywhere in the tree. The +handshake negotiates only the SCALE *codec* version, not the API version. So editing the body of a +`V1` message keeps the version byte at `0` while changing the bytes that follow it; because SCALE is +positional and not self-describing, an un-upgraded host reads "V1" and silently misdecodes the new +layout (the leading `u32` is consumed as a string-length prefix). There is no clean failure. + +The change is therefore safe **only while the host and product SDK ship together** — i.e. while the +v0.1 wire is not frozen against independently-deployed hosts. That is the repository's current posture: +`v01` is the single, still-evolving wire, breaking changes land in it in place (see +[RFC-0020](0020-create-transaction.md)), and the `versioned_type!` machinery — though fully scaffolded +— has never been exercised with a second variant. This RFC follows that posture. Some of these methods +do have a live consumer (the playground calls `signRaw` and `createProof`); those call sites are +updated in the same change. + +Once the v0.1 wire is frozen and hosts deploy independently of the SDK, a breaking change like this one +must instead add a `V2` variant (`{ V1 => v01::X, V2 => v02::X }`) with hand-written +`FromLatest`/`IntoLatest`, so the version byte distinguishes the shapes (an old host rejects `V2` +cleanly instead of corrupting) and a dual-version host can bridge both. Adopting that for these seven +messages alone — while the rest of the protocol stays single-variant `V1` — would buy no real safety; +it is a protocol-wide discipline to take on at the freeze, tracked separately from this RFC. + +## Prior Art and References + +- [RFC-0020](0020-create-transaction.md) — removed a field from a signing request body in place on + `v01`; establishes the in-place-change precedent followed here. +- [#222](https://github.com/paritytech/truapi/issues/222) — parent issue this is split out from. +- [#243](https://github.com/paritytech/truapi/issues/243) — tracking issue for this RFC and its + follow-up. + +## Unresolved Questions + +- **In-place vs versioned migration.** This RFC changes the seven `v01` bodies in place, which is sound + only while hosts and the SDK ship together (see [Compatibility](#compatibility)). If v0.1 is frozen + against independently-deployed hosts before this lands, it must move to a `V2` envelope instead — and + that decision is really protocol-wide (today all 165 envelopes are single-variant `V1`), not specific + to these seven messages. +- **Replacement field name.** The replacement is `derivation_index: u32` uniformly. An alternative is + to keep the role-indicating names each call uses today (`signer` on `createTransaction`, `account` on + the signing calls) but retype them to `u32`. Uniform `derivation_index` is proposed for consistency. + +## Future Directions and Related Material + +A follow-up RFC introduces cross-product account access explicitly, rather than exposing it by +overloading `dotNsIdentifier`: + +- A new `ExternalAccount` variant on `RemotePermission`, so cross-product access is grantable and + deniable by the user like the other remote permissions. +- Explicit external methods that take a full `ProductAccountId` (the type retained by this RFC) and + require the `ExternalAccount` grant: `getExternalAccount()`, `getExternalAccountAlias()`, + `createExternalAccountProof()`. +- Whether to allow signing with an external account at all, and if so under what permission and + prompting, is left to that RFC. diff --git a/docs/rfcs/_index.md b/docs/rfcs/_index.md index 3bac6f9a..6cbb3068 100644 --- a/docs/rfcs/_index.md +++ b/docs/rfcs/_index.md @@ -22,3 +22,4 @@ created: 2026-03-13 | 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 | — | +| 0022 | [Remove `dotNsIdentifier` from default account access](0022-remove-dotns-from-default-access.md) | draft | Valentin Fernandez | — | diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 7c4e065f..de379ec9 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -35,12 +35,7 @@ pub trait Account: Send + Sync { /// Retrieve a product-scoped account. /// /// ```ts - /// const result = await truapi.account.getAccount({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, - /// }); + /// const result = await truapi.account.getAccount({ derivationIndex: 0 }); /// assert(result.isOk(), "getAccount failed:", result); /// console.log("account retrieved:", result.value); /// ``` @@ -56,12 +51,7 @@ pub trait Account: Send + Sync { /// Retrieve a contextual alias for a product account. /// /// ```ts - /// const result = await truapi.account.getAccountAlias({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, - /// }); + /// const result = await truapi.account.getAccountAlias({ derivationIndex: 0 }); /// assert(result.isOk(), "getAccountAlias failed:", result); /// console.log("account alias:", result.value); /// ``` @@ -80,10 +70,7 @@ pub trait Account: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.account.createAccountProof({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// derivationIndex: 0, /// ringLocation: { /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, /// ringRootHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index be41d9c5..7866f3a7 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -23,10 +23,7 @@ pub trait Signing: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.signing.createTransaction({ - /// signer: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// derivationIndex: 0, /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, /// callData: "0x0000", /// extensions: [], @@ -134,7 +131,7 @@ pub trait Signing: Send + Sync { /// /// ```ts /// const result = await truapi.signing.signRaw({ - /// account: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: 0 }, + /// derivationIndex: 0, /// payload: { /// tag: "Bytes", /// value: { @@ -160,7 +157,7 @@ pub trait Signing: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.signing.signPayload({ - /// account: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: 0 }, + /// derivationIndex: 0, /// payload: { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 5addc9b7..5dd18bea 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -25,10 +25,7 @@ pub trait StatementStore: Send + Sync { /// const statement: Statement = { expiry, topics: [topic] }; /// /// const proofResult = await truapi.statementStore.createProof({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// derivationIndex: 0, /// statement, /// }); /// assert(proofResult.isOk(), "createProof failed:", proofResult); @@ -74,10 +71,7 @@ pub trait StatementStore: Send + Sync { /// const topic: `0x${string}` = `0x${bytes.toHex()}`; /// const statement = { expiry, topics: [topic] }; /// const result = await truapi.statementStore.createProof({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// derivationIndex: 0, /// statement, /// }); /// assert(result.isOk(), "createProof failed:", result); @@ -133,10 +127,7 @@ pub trait StatementStore: Send + Sync { /// const statement = { expiry, topics: [topic] }; /// /// const proofResult = await truapi.statementStore.createProof({ - /// productAccountId: { - /// dotNsIdentifier: "truapi-playground.dot", - /// derivationIndex: 0, - /// }, + /// derivationIndex: 0, /// statement, /// }); /// assert(proofResult.isOk(), "createProof failed:", proofResult); diff --git a/rust/crates/truapi/src/v01/account.rs b/rust/crates/truapi/src/v01/account.rs index f3b928ec..e8feefaa 100644 --- a/rust/crates/truapi/src/v01/account.rs +++ b/rust/crates/truapi/src/v01/account.rs @@ -60,8 +60,8 @@ pub struct RingLocation { /// Request to create a ring VRF proof for a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountCreateProofRequest { - /// Product account that should create the proof. - pub product_account_id: ProductAccountId, + /// Derivation index of the caller's product account that should create the proof. + pub derivation_index: u32, /// Ring location to use for proof generation. pub ring_location: RingLocation, /// Context bytes bound to the proof. @@ -127,8 +127,8 @@ pub enum HostAccountCreateProofError { /// Request to retrieve a product-scoped account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountGetRequest { - /// Product account to retrieve. - pub product_account_id: ProductAccountId, + /// Derivation index of the caller's product account to retrieve. + pub derivation_index: u32, } /// Response containing a product-scoped account. @@ -159,8 +159,8 @@ pub enum HostGetUserIdError { /// Request to retrieve a contextual alias for a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountGetAliasRequest { - /// Product account to derive the alias for. - pub product_account_id: ProductAccountId, + /// Derivation index of the caller's product account to derive the alias for. + pub derivation_index: u32, } /// Response containing a ring VRF proof. diff --git a/rust/crates/truapi/src/v01/signing.rs b/rust/crates/truapi/src/v01/signing.rs index f0c1324d..243b5a16 100644 --- a/rust/crates/truapi/src/v01/signing.rs +++ b/rust/crates/truapi/src/v01/signing.rs @@ -1,7 +1,5 @@ use parity_scale_codec::{Decode, Encode}; -use super::ProductAccountId; - /// Full Substrate extrinsic signing payload with all fields needed for signature /// generation. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] @@ -41,8 +39,8 @@ pub struct HostSignPayloadData { /// Request to sign an extrinsic payload with a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostSignPayloadRequest { - /// Product account that will sign this payload. - pub account: ProductAccountId, + /// Derivation index of the caller's product account that will sign this payload. + pub derivation_index: u32, /// The extrinsic payload to sign. pub payload: HostSignPayloadData, } @@ -65,8 +63,8 @@ pub enum RawPayload { /// A raw signing request pairing an account with the payload to sign. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostSignRawRequest { - /// Product account that will sign this payload. - pub account: ProductAccountId, + /// Derivation index of the caller's product account that will sign this payload. + pub derivation_index: u32, /// The payload to sign. pub payload: RawPayload, } diff --git a/rust/crates/truapi/src/v01/statement_store.rs b/rust/crates/truapi/src/v01/statement_store.rs index c4217005..82cecf61 100644 --- a/rust/crates/truapi/src/v01/statement_store.rs +++ b/rust/crates/truapi/src/v01/statement_store.rs @@ -1,7 +1,5 @@ use parity_scale_codec::{Decode, Encode}; -use super::ProductAccountId; - /// Cryptographic proof for a statement. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum StatementProof { @@ -65,8 +63,8 @@ pub struct SignedStatement { /// Request to create a cryptographic proof for a statement. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteStatementStoreCreateProofRequest { - /// Product account that should create the proof. - pub product_account_id: ProductAccountId, + /// Derivation index of the caller's product account that should create the proof. + pub derivation_index: u32, /// Statement to prove. pub statement: Statement, } diff --git a/rust/crates/truapi/src/v01/transaction.rs b/rust/crates/truapi/src/v01/transaction.rs index 82db1b4a..7f6ac2e0 100644 --- a/rust/crates/truapi/src/v01/transaction.rs +++ b/rust/crates/truapi/src/v01/transaction.rs @@ -1,7 +1,5 @@ use parity_scale_codec::{Decode, Encode}; -use super::ProductAccountId; - /// A 32-byte chain genesis hash used to identify the target chain. pub type GenesisHash = [u8; 32]; @@ -22,12 +20,13 @@ pub struct TxPayloadExtension { /// Transaction payload for a product account. /// /// Contains everything the host needs to construct a signed extrinsic. -/// The signer is a [`ProductAccountId`]; the host resolves the -/// corresponding key pair through its account management layer. +/// The signer is identified by its derivation index within the caller's own +/// product; the host resolves the corresponding key pair through its account +/// management layer. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct ProductAccountTxPayload { - /// Product account that will sign the transaction. - pub signer: ProductAccountId, + /// Derivation index of the caller's product account that will sign the transaction. + pub derivation_index: u32, /// Chain where the transaction will execute. pub genesis_hash: GenesisHash, /// SCALE-encoded Call data. From ec1e2ba6eecd22b36bbf95ba168545c1937a1cfc Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Wed, 1 Jul 2026 08:09:55 +0200 Subject: [PATCH 2/2] refactor: make dotNsIdentifier optional with permissioned external access --- README.md | 2 +- .../0022-remove-dotns-from-default-access.md | 221 ++++++++---------- docs/rfcs/_index.md | 2 +- rust/crates/truapi/src/api/account.rs | 13 +- rust/crates/truapi/src/api/signing.rs | 25 +- rust/crates/truapi/src/api/statement_store.rs | 6 +- rust/crates/truapi/src/v01/account.rs | 24 +- rust/crates/truapi/src/v01/permissions.rs | 8 +- rust/crates/truapi/src/v01/signing.rs | 9 +- rust/crates/truapi/src/v01/statement_store.rs | 5 +- rust/crates/truapi/src/v01/transaction.rs | 11 +- 11 files changed, 169 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index a7a206b9..475df767 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ const transport = createTransport(createMessagePortProvider(port)); const truapi = createClient(transport); const result = await truapi.accountManagement.accountGet({ - productAccountId: { dotNsIdentifier: "my-product.dot", derivationIndex: 0 }, + productAccountId: { derivationIndex: 0 }, }); ``` diff --git a/docs/rfcs/0022-remove-dotns-from-default-access.md b/docs/rfcs/0022-remove-dotns-from-default-access.md index 79b5e5aa..78b2f4d4 100644 --- a/docs/rfcs/0022-remove-dotns-from-default-access.md +++ b/docs/rfcs/0022-remove-dotns-from-default-access.md @@ -1,28 +1,34 @@ -# RFC-0022: Remove `dotNsIdentifier` from default account access +# RFC-0022: Optional `dotNsIdentifier` and permissioned external-account access | | | | --------------- | --------------------------------------------------------------------- | | **RFC Number** | 22 | | **Start Date** | 2026-06-30 | -| **Description** | Default account, signing, and statement-store calls address only the caller's own accounts, by `derivationIndex`. | +| **Description** | Account, signing, and statement-store calls address the caller's own accounts by default, and a foreign dotNS identifier opts into permissioned cross-product access. | | **Authors** | Valentin Fernandez | ## Summary -The account, signing, and statement-store methods that operate on a product account today take a -`ProductAccountId { dotNsIdentifier, derivationIndex }`. The `dotNsIdentifier` is dropped from these -calls: each resolves against the caller's own dotNS domain, and products pass only `derivationIndex`. +The account, signing, and statement-store methods that operate on a product account take a +`ProductAccountId { dotNsIdentifier, derivationIndex }`. `dotNsIdentifier` is now **optional**. Omit it +and the call resolves against the caller's own dotNS domain, so the common case only needs a +`derivationIndex`. ```typescript -// Before +// Own account, the common case +await truapi.account.getAccount({ productAccountId: { derivationIndex: 0 } }); + +// Another product's account, requires the ExternalAccount permission await truapi.account.getAccount({ - productAccountId: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: 0 }, + productAccountId: { dotNsIdentifier: "another-product.dot", derivationIndex: 0 }, }); - -// After -await truapi.account.getAccount({ derivationIndex: 0 }); ``` +Supplying a `dotNsIdentifier` that names a different product is cross-product access, gated behind a +new `ExternalAccount` remote permission. When the field is omitted the host resolves the account's +domain from the authenticated caller. When it names a foreign domain the host accepts or rejects the +call based on whether the user granted `ExternalAccount`. + This RFC covers seven call sites: - [`getAccount()`](https://paritytech.github.io/truapi/v/main/method/Account/get_account) @@ -33,51 +39,50 @@ This RFC covers seven call sites: - [`signPayload()`](https://paritytech.github.io/truapi/v/main/method/Signing/sign_payload) - [`statementStore.createProof()`](https://paritytech.github.io/truapi/v/main/method/StatementStore/create_proof) -`ProductAccountId` itself is retained: a follow-up RFC reuses it as the explicit target identifier for -opt-in cross-product access (see [Future Directions](#future-directions-and-related-material)). - ## Motivation -The host already knows the calling product's dotNS domain, so making the product pass `dotNsIdentifier` -is redundant — the host can resolve it on its own from the authenticated caller. +For the common case, a product addressing its own accounts, `dotNsIdentifier` is redundant. The host +already knows the calling product's dotNS domain and can resolve it from the authenticated caller. +Making the field optional lets that case carry only a `derivationIndex`. -The parameter also has no useful value other than the caller's own domain. The host only ever acts on -the domain the product is currently running under: if a product passes any other dotNS address, the -host rejects the call as invalid. For example, a product running on `truapi-playground.dot` that calls -`signRaw()` with a different dotNS address (say `another-product.dot`) gets back an *invalid* response -— it cannot reach the other product's account this way. So `dotNsIdentifier` can only ever echo the -caller's own domain (redundant) or name a domain the host refuses (a dead value). +The field still serves a real purpose, though. Some products have a legitimate reason to read, and +possibly sign with, *another* product's account. That capability should be explicit and consented, not +an unguarded consequence of an always-present parameter. So instead of dropping `dotNsIdentifier` +entirely, which would remove the capability, or leaving it mandatory, which offers no isolation, it +becomes an optional opt-in: absent for own-product access, present and permissioned for cross-product +access. -Either way the parameter does not belong on the default call surface. Products should address only -their own accounts, by `derivationIndex`, and the host — the security boundary — should be the sole -authority on which domain a call resolves against, rather than taking it from the caller at all. - -Split out from [#222](https://github.com/paritytech/truapi/issues/222); tracked in +Split out from [#222](https://github.com/paritytech/truapi/issues/222), tracked in [#243](https://github.com/paritytech/truapi/issues/243). ## Stakeholders -- **Product developers** — construct slimmer calls with no domain field to supply or get wrong; calls - address the product's own accounts. -- **Host developers** — resolve the account's domain from the authenticated caller instead of trusting - a parameter on the request. +- **Product developers** omit the domain for their own accounts. To reach another product's account + they supply a `dotNsIdentifier` and hold the `ExternalAccount` grant. +- **Host developers** resolve the domain from the authenticated caller when it is omitted, and enforce + the `ExternalAccount` permission when a foreign domain is supplied. ## Explanation -`ProductAccountId` is **unchanged** — it stays available for the explicit external-access surface a -follow-up RFC introduces: +`ProductAccountId` carries an optional domain: ```rust struct ProductAccountId { - dot_ns_identifier: String, + dot_ns_identifier: Option, derivation_index: u32, } ``` -Each of the seven request types drops its `ProductAccountId`-typed field and carries a bare -`derivation_index: u32` instead. The host fills in the caller's own domain. +Resolution rules: + +- `dot_ns_identifier == None` resolves to the caller's own product. +- `dot_ns_identifier == Some(domain)` where `domain` is the caller's own also resolves to the caller's + own product. +- `dot_ns_identifier == Some(domain)` where `domain` is a different product is cross-product access. It + is permitted only when the user has granted the caller the `ExternalAccount` permission, otherwise + the host rejects the call. -Before: +Each of the seven request types carries a `ProductAccountId`: ```rust struct HostAccountGetRequest { product_account_id: ProductAccountId } @@ -102,46 +107,30 @@ struct RemoteStatementStoreCreateProofRequest { } ``` -After: +In TypeScript `dotNsIdentifier` is optional on the nested identifier, so own-account calls pass only +the index: -```rust -struct HostAccountGetRequest { derivation_index: u32 } -struct HostAccountGetAliasRequest { derivation_index: u32 } -struct HostAccountCreateProofRequest { - derivation_index: u32, - ring_location: RingLocation, - context: Vec, -} -struct ProductAccountTxPayload { - derivation_index: u32, - genesis_hash: GenesisHash, - call_data: Vec, - extensions: Vec, - tx_ext_version: u8, -} -struct HostSignRawRequest { derivation_index: u32, payload: RawPayload } -struct HostSignPayloadRequest { derivation_index: u32, payload: HostSignPayloadData } -struct RemoteStatementStoreCreateProofRequest { - derivation_index: u32, - statement: Statement, -} +```typescript +await truapi.account.getAccount({ productAccountId: { derivationIndex: 0 } }); +await truapi.account.getAccountAlias({ productAccountId: { derivationIndex: 0 } }); +await truapi.account.createAccountProof({ productAccountId: { derivationIndex: 0 }, ringLocation, context: "0x" }); +await truapi.signing.createTransaction({ signer: { derivationIndex: 0 }, genesisHash, callData, extensions: [], txExtVersion: 0 }); +await truapi.signing.signRaw({ account: { derivationIndex: 0 }, payload }); +await truapi.signing.signPayload({ account: { derivationIndex: 0 }, payload }); +await truapi.statementStore.createProof({ productAccountId: { derivationIndex: 0 }, statement }); ``` -In TypeScript the nested identifier object collapses to a single field: +### `ExternalAccount` permission -```typescript -await truapi.account.getAccount({ derivationIndex: 0 }); -await truapi.account.getAccountAlias({ derivationIndex: 0 }); -await truapi.account.createAccountProof({ derivationIndex: 0, ringLocation, context: "0x" }); -await truapi.signing.createTransaction({ derivationIndex: 0, genesisHash, callData, extensions: [], txExtVersion: 0 }); -await truapi.signing.signRaw({ derivationIndex: 0, payload }); -await truapi.signing.signPayload({ derivationIndex: 0, payload }); -await truapi.statementStore.createProof({ derivationIndex: 0, statement }); -``` +A new `ExternalAccount` variant on `RemotePermission` governs cross-product access. Like `ChainSubmit` +and `StatementSubmit`, it is triggered implicitly by the business call. The first time a product issues +one of these calls with a foreign `dotNsIdentifier`, the host prompts for the grant, and the user's +decision persists. A product that only ever addresses its own accounts never sees the prompt. -The change is made in place on `v01`; no `v02` of these messages is introduced. Their wire IDs are -unchanged — only the request body shape changes. The `*WithLegacyAccount` signing variants are -unaffected: they identify a legacy account by raw `AccountId`, never by `dotNsIdentifier`. +The change is made in place on `v01`, with no `v02` of these messages. Their wire IDs are unchanged. +Only the request body shape changes, and `dotNsIdentifier` moves from a required `String` to an +optional one. The `*WithLegacyAccount` signing variants are unaffected, since they identify a legacy +account by raw `AccountId`, never by `dotNsIdentifier`. `statementStore.createProofAuthorized()` already takes only the statement (it uses a pre-allocated allowance account) and so needs no change. The deprecated `statementStore.createProof()` is the @@ -149,81 +138,67 @@ statement-store call still carrying `ProductAccountId`, and is covered here for ## Drawbacks -These default methods only ever address the caller's own accounts. Some products may have a legitimate -reason to read, and possibly sign with, *another* product's account — a capability the host does not -expose today, since it rejects any domain other than the caller's. Rather than overloading -`dotNsIdentifier` to provide it, that capability is introduced explicitly, behind a permission, in the -follow-up RFC (see [Future Directions](#future-directions-and-related-material)). +Cross-product access adds a permission surface the host must enforce and the user must reason about. +The capability is opt-in on both sides, though: a product reaches another product's account only by +supplying a foreign domain, and only after the user grants `ExternalAccount`. Products that never opt +in are unaffected, and the default call surface stays minimal. ## Testing, Security, and Privacy -Removing `dotNsIdentifier` makes the host the sole authority on which domain a call resolves against. -The host already rejects any domain other than the caller's, so a product cannot reach another -product's account today; dropping the field removes that redundant, rejected input entirely rather than -relying on the host to validate a caller-supplied domain on every call. The trust boundary becomes -structural — there is no domain field to validate or get wrong. +Cross-product access is only possible through an explicit foreign `dotNsIdentifier` combined with a +user-granted `ExternalAccount` permission. Own-account calls omit the domain entirely, so the host +resolves it from the authenticated caller and there is no caller-supplied domain to validate in the +common case. The host stays the sole authority on which domain a call resolves against, and the +permission prompt keeps the user in the loop before any product reads or signs with another product's +account. ## Performance, Ergonomics, and Compatibility ### Ergonomics -Calls are smaller and harder to misuse — there is no domain string to get wrong, and the common case -(a product's own account) needs only an index. +The common case, a product's own account, needs only an index, and there is no domain string to supply +or get wrong. Cross-product access is expressed by the presence of a domain plus a permission, so the +capability is discoverable instead of hidden behind an always-present field. ### Compatibility This is a breaking wire change to seven `v01` request bodies, and the protocol gives it **no version signal**. Each message is wrapped in a `versioned_type!` envelope whose SCALE discriminant byte is the -version (`V1` = codec index `0`), but every one of the protocol's envelopes is currently single-variant -`V1` — there is no live multi-version support and no cross-version conversion anywhere in the tree. The -handshake negotiates only the SCALE *codec* version, not the API version. So editing the body of a -`V1` message keeps the version byte at `0` while changing the bytes that follow it; because SCALE is -positional and not self-describing, an un-upgraded host reads "V1" and silently misdecodes the new -layout (the leading `u32` is consumed as a string-length prefix). There is no clean failure. - -The change is therefore safe **only while the host and product SDK ship together** — i.e. while the -v0.1 wire is not frozen against independently-deployed hosts. That is the repository's current posture: -`v01` is the single, still-evolving wire, breaking changes land in it in place (see -[RFC-0020](0020-create-transaction.md)), and the `versioned_type!` machinery — though fully scaffolded -— has never been exercised with a second variant. This RFC follows that posture. Some of these methods -do have a live consumer (the playground calls `signRaw` and `createProof`); those call sites are -updated in the same change. +version (`V1` is codec index `0`), but every one of the protocol's envelopes is currently +single-variant `V1`. There is no live multi-version support and no cross-version conversion anywhere in +the tree. The handshake negotiates only the SCALE *codec* version, not the API version. So editing the +body of a `V1` message keeps the version byte at `0` while changing the bytes that follow it. Because +SCALE is positional and not self-describing, an un-upgraded host reads "V1" and silently misdecodes the +new layout. There is no clean failure. + +The change is therefore safe only while the host and product SDK ship together, that is, while the +v0.1 wire is not frozen against independently-deployed hosts. That is the repository's current posture. +`v01` is the single, still-evolving wire, and breaking changes land in it in place (see +[RFC-0020](0020-create-transaction.md)). The `versioned_type!` machinery, though fully scaffolded, has +never been exercised with a second variant. This RFC follows that posture. Some of these methods do +have a live consumer (the playground calls `signRaw` and `createProof`), and those call sites are +updated in the same change. The PR stays a draft until the protocol-wide versioning strategy is +settled. Once the v0.1 wire is frozen and hosts deploy independently of the SDK, a breaking change like this one must instead add a `V2` variant (`{ V1 => v01::X, V2 => v02::X }`) with hand-written `FromLatest`/`IntoLatest`, so the version byte distinguishes the shapes (an old host rejects `V2` cleanly instead of corrupting) and a dual-version host can bridge both. Adopting that for these seven -messages alone — while the rest of the protocol stays single-variant `V1` — would buy no real safety; -it is a protocol-wide discipline to take on at the freeze, tracked separately from this RFC. +messages alone, while the rest of the protocol stays single-variant `V1`, would buy no real safety. It +is a protocol-wide discipline to take on at the freeze, tracked separately from this RFC. ## Prior Art and References -- [RFC-0020](0020-create-transaction.md) — removed a field from a signing request body in place on - `v01`; establishes the in-place-change precedent followed here. -- [#222](https://github.com/paritytech/truapi/issues/222) — parent issue this is split out from. -- [#243](https://github.com/paritytech/truapi/issues/243) — tracking issue for this RFC and its - follow-up. +- [RFC-0002](0002-permission-model.md), the permission model that `ExternalAccount` extends. +- [RFC-0020](0020-create-transaction.md), which removed a field from a signing request body in place on + `v01` and establishes the in-place-change precedent followed here. +- [#222](https://github.com/paritytech/truapi/issues/222), the parent issue this is split out from. +- [#243](https://github.com/paritytech/truapi/issues/243), the tracking issue for this RFC. ## Unresolved Questions - **In-place vs versioned migration.** This RFC changes the seven `v01` bodies in place, which is sound only while hosts and the SDK ship together (see [Compatibility](#compatibility)). If v0.1 is frozen - against independently-deployed hosts before this lands, it must move to a `V2` envelope instead — and - that decision is really protocol-wide (today all 165 envelopes are single-variant `V1`), not specific - to these seven messages. -- **Replacement field name.** The replacement is `derivation_index: u32` uniformly. An alternative is - to keep the role-indicating names each call uses today (`signer` on `createTransaction`, `account` on - the signing calls) but retype them to `u32`. Uniform `derivation_index` is proposed for consistency. - -## Future Directions and Related Material - -A follow-up RFC introduces cross-product account access explicitly, rather than exposing it by -overloading `dotNsIdentifier`: - -- A new `ExternalAccount` variant on `RemotePermission`, so cross-product access is grantable and - deniable by the user like the other remote permissions. -- Explicit external methods that take a full `ProductAccountId` (the type retained by this RFC) and - require the `ExternalAccount` grant: `getExternalAccount()`, `getExternalAccountAlias()`, - `createExternalAccountProof()`. -- Whether to allow signing with an external account at all, and if so under what permission and - prompting, is left to that RFC. + against independently-deployed hosts before this lands, it must move to a `V2` envelope instead, and + that decision is really protocol-wide (today all envelopes are single-variant `V1`), not specific to + these seven messages. diff --git a/docs/rfcs/_index.md b/docs/rfcs/_index.md index 6cbb3068..f58f9c9f 100644 --- a/docs/rfcs/_index.md +++ b/docs/rfcs/_index.md @@ -22,4 +22,4 @@ created: 2026-03-13 | 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 | — | -| 0022 | [Remove `dotNsIdentifier` from default account access](0022-remove-dotns-from-default-access.md) | draft | Valentin Fernandez | — | +| 0022 | [Optional `dotNsIdentifier` and permissioned external-account access](0022-remove-dotns-from-default-access.md) | draft | Valentin Fernandez | [#245](https://github.com/paritytech/truapi/pull/245) | diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index de379ec9..a60e7f3d 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -35,9 +35,16 @@ pub trait Account: Send + Sync { /// Retrieve a product-scoped account. /// /// ```ts - /// const result = await truapi.account.getAccount({ derivationIndex: 0 }); + /// const result = await truapi.account.getAccount({ productAccountId: { derivationIndex: 0 } }); /// assert(result.isOk(), "getAccount failed:", result); /// console.log("account retrieved:", result.value); + /// + /// // To read another product's account, pass its dotNS domain. This is cross-product + /// // access and requires the `ExternalAccount` permission — host-side enforcement is + /// // not yet implemented. + /// // await truapi.account.getAccount({ + /// // productAccountId: { dotNsIdentifier: "another-product.dot", derivationIndex: 0 }, + /// // }); /// ``` #[wire(request_id = 22)] async fn get_account( @@ -51,7 +58,7 @@ pub trait Account: Send + Sync { /// Retrieve a contextual alias for a product account. /// /// ```ts - /// const result = await truapi.account.getAccountAlias({ derivationIndex: 0 }); + /// const result = await truapi.account.getAccountAlias({ productAccountId: { derivationIndex: 0 } }); /// assert(result.isOk(), "getAccountAlias failed:", result); /// console.log("account alias:", result.value); /// ``` @@ -70,7 +77,7 @@ pub trait Account: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.account.createAccountProof({ - /// derivationIndex: 0, + /// productAccountId: { derivationIndex: 0 }, /// ringLocation: { /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, /// ringRootHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 7866f3a7..cf33a0d6 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -23,7 +23,7 @@ pub trait Signing: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.signing.createTransaction({ - /// derivationIndex: 0, + /// signer: { derivationIndex: 0 }, /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, /// callData: "0x0000", /// extensions: [], @@ -31,6 +31,17 @@ pub trait Signing: Send + Sync { /// }); /// assert(result.isOk(), "createTransaction failed:", result); /// console.log("transaction created:", result.value); + /// + /// // To sign with another product's account, set `signer.dotNsIdentifier`. This is + /// // cross-product access and requires the `ExternalAccount` permission — host-side + /// // enforcement is not yet implemented. + /// // await truapi.signing.createTransaction({ + /// // signer: { dotNsIdentifier: "another-product.dot", derivationIndex: 0 }, + /// // genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// // callData: "0x0000", + /// // extensions: [], + /// // txExtVersion: 0, + /// // }); /// ``` #[wire(request_id = 30)] async fn create_transaction( @@ -131,7 +142,7 @@ pub trait Signing: Send + Sync { /// /// ```ts /// const result = await truapi.signing.signRaw({ - /// derivationIndex: 0, + /// account: { derivationIndex: 0 }, /// payload: { /// tag: "Bytes", /// value: { @@ -141,6 +152,14 @@ pub trait Signing: Send + Sync { /// }); /// assert(result.isOk(), "signRaw failed:", result); /// console.log("raw bytes signed:", result.value); + /// + /// // To sign with another product's account, set `account.dotNsIdentifier`. This is + /// // cross-product access and requires the `ExternalAccount` permission — host-side + /// // enforcement is not yet implemented. + /// // await truapi.signing.signRaw({ + /// // account: { dotNsIdentifier: "another-product.dot", derivationIndex: 0 }, + /// // payload: { tag: "Bytes", value: { bytes: "0x48656c6c6f2c20776f726c6421" } }, + /// // }); /// ``` #[wire(request_id = 114)] async fn sign_raw( @@ -157,7 +176,7 @@ pub trait Signing: Send + Sync { /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// /// const result = await truapi.signing.signPayload({ - /// derivationIndex: 0, + /// account: { derivationIndex: 0 }, /// payload: { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 5dd18bea..1bdaa510 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -25,7 +25,7 @@ pub trait StatementStore: Send + Sync { /// const statement: Statement = { expiry, topics: [topic] }; /// /// const proofResult = await truapi.statementStore.createProof({ - /// derivationIndex: 0, + /// productAccountId: { derivationIndex: 0 }, /// statement, /// }); /// assert(proofResult.isOk(), "createProof failed:", proofResult); @@ -71,7 +71,7 @@ pub trait StatementStore: Send + Sync { /// const topic: `0x${string}` = `0x${bytes.toHex()}`; /// const statement = { expiry, topics: [topic] }; /// const result = await truapi.statementStore.createProof({ - /// derivationIndex: 0, + /// productAccountId: { derivationIndex: 0 }, /// statement, /// }); /// assert(result.isOk(), "createProof failed:", result); @@ -127,7 +127,7 @@ pub trait StatementStore: Send + Sync { /// const statement = { expiry, topics: [topic] }; /// /// const proofResult = await truapi.statementStore.createProof({ - /// derivationIndex: 0, + /// productAccountId: { derivationIndex: 0 }, /// statement, /// }); /// assert(proofResult.isOk(), "createProof failed:", proofResult); diff --git a/rust/crates/truapi/src/v01/account.rs b/rust/crates/truapi/src/v01/account.rs index e8feefaa..e3b11167 100644 --- a/rust/crates/truapi/src/v01/account.rs +++ b/rust/crates/truapi/src/v01/account.rs @@ -1,11 +1,17 @@ use parity_scale_codec::{Decode, Encode}; -/// Identifies a product-specific account by combining a dotNS domain name with a +/// Identifies a product-specific account by an optional dotNS domain name and a /// derivation index. +/// +/// When `dot_ns_identifier` is `None`, the account resolves against the caller's +/// own product. When it is `Some(domain)` naming a different product, the call is +/// cross-product access and requires the +/// [`ExternalAccount`](crate::v01::RemotePermission::ExternalAccount) permission. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct ProductAccountId { - /// A dotNS domain name identifier (e.g., `"my-product.dot"`). - pub dot_ns_identifier: String, + /// Optional dotNS domain name identifier (e.g., `"my-product.dot"`). `None` + /// targets the caller's own product. + pub dot_ns_identifier: Option, /// Key derivation index for generating product-specific accounts. pub derivation_index: u32, } @@ -60,8 +66,8 @@ pub struct RingLocation { /// Request to create a ring VRF proof for a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountCreateProofRequest { - /// Derivation index of the caller's product account that should create the proof. - pub derivation_index: u32, + /// Account that should create the proof. + pub product_account_id: ProductAccountId, /// Ring location to use for proof generation. pub ring_location: RingLocation, /// Context bytes bound to the proof. @@ -127,8 +133,8 @@ pub enum HostAccountCreateProofError { /// Request to retrieve a product-scoped account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountGetRequest { - /// Derivation index of the caller's product account to retrieve. - pub derivation_index: u32, + /// Account to retrieve. + pub product_account_id: ProductAccountId, } /// Response containing a product-scoped account. @@ -159,8 +165,8 @@ pub enum HostGetUserIdError { /// Request to retrieve a contextual alias for a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostAccountGetAliasRequest { - /// Derivation index of the caller's product account to derive the alias for. - pub derivation_index: u32, + /// Account to derive the alias for. + pub product_account_id: ProductAccountId, } /// Response containing a ring VRF proof. diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index c4873a6f..86eaf486 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -31,8 +31,9 @@ pub enum HostDevicePermissionRequest { /// One remote-operation permission requested by the product (RFC 0002). /// -/// `ChainSubmit`, `PreimageSubmit`, and `StatementSubmit` are also triggered -/// implicitly by the corresponding business calls when not yet granted. +/// `ChainSubmit`, `PreimageSubmit`, `StatementSubmit`, and `ExternalAccount` are +/// also triggered implicitly by the corresponding business calls when not yet +/// granted. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] pub enum RemotePermission { /// Outbound HTTP/WebSocket access to a set of domains. @@ -53,6 +54,9 @@ pub enum RemotePermission { /// Submitting statements on behalf of the user via `remote_statement_store_submit`. #[display("submit statements")] StatementSubmit, + /// Accessing accounts of another product, addressed by a foreign dotNS identifier. + #[display("access external product accounts")] + ExternalAccount, } /// remote-permission request (RFC 0002). diff --git a/rust/crates/truapi/src/v01/signing.rs b/rust/crates/truapi/src/v01/signing.rs index 243b5a16..78e22d2c 100644 --- a/rust/crates/truapi/src/v01/signing.rs +++ b/rust/crates/truapi/src/v01/signing.rs @@ -1,3 +1,4 @@ +use super::ProductAccountId; use parity_scale_codec::{Decode, Encode}; /// Full Substrate extrinsic signing payload with all fields needed for signature @@ -39,8 +40,8 @@ pub struct HostSignPayloadData { /// Request to sign an extrinsic payload with a product account. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostSignPayloadRequest { - /// Derivation index of the caller's product account that will sign this payload. - pub derivation_index: u32, + /// Account that will sign this payload. + pub account: ProductAccountId, /// The extrinsic payload to sign. pub payload: HostSignPayloadData, } @@ -63,8 +64,8 @@ pub enum RawPayload { /// A raw signing request pairing an account with the payload to sign. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostSignRawRequest { - /// Derivation index of the caller's product account that will sign this payload. - pub derivation_index: u32, + /// Account that will sign this payload. + pub account: ProductAccountId, /// The payload to sign. pub payload: RawPayload, } diff --git a/rust/crates/truapi/src/v01/statement_store.rs b/rust/crates/truapi/src/v01/statement_store.rs index 82cecf61..972baf18 100644 --- a/rust/crates/truapi/src/v01/statement_store.rs +++ b/rust/crates/truapi/src/v01/statement_store.rs @@ -1,3 +1,4 @@ +use super::ProductAccountId; use parity_scale_codec::{Decode, Encode}; /// Cryptographic proof for a statement. @@ -63,8 +64,8 @@ pub struct SignedStatement { /// Request to create a cryptographic proof for a statement. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteStatementStoreCreateProofRequest { - /// Derivation index of the caller's product account that should create the proof. - pub derivation_index: u32, + /// Account that should create the proof. + pub product_account_id: ProductAccountId, /// Statement to prove. pub statement: Statement, } diff --git a/rust/crates/truapi/src/v01/transaction.rs b/rust/crates/truapi/src/v01/transaction.rs index 7f6ac2e0..6c4f485a 100644 --- a/rust/crates/truapi/src/v01/transaction.rs +++ b/rust/crates/truapi/src/v01/transaction.rs @@ -1,3 +1,4 @@ +use super::ProductAccountId; use parity_scale_codec::{Decode, Encode}; /// A 32-byte chain genesis hash used to identify the target chain. @@ -19,14 +20,12 @@ pub struct TxPayloadExtension { /// Transaction payload for a product account. /// -/// Contains everything the host needs to construct a signed extrinsic. -/// The signer is identified by its derivation index within the caller's own -/// product; the host resolves the corresponding key pair through its account -/// management layer. +/// Contains everything the host needs to construct a signed extrinsic. The host +/// resolves the signer's key pair through its account management layer. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct ProductAccountTxPayload { - /// Derivation index of the caller's product account that will sign the transaction. - pub derivation_index: u32, + /// Account that will sign the transaction. + pub signer: ProductAccountId, /// Chain where the transaction will execute. pub genesis_hash: GenesisHash, /// SCALE-encoded Call data.