Skip to content
Merged
7 changes: 7 additions & 0 deletions js/packages/truapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ sub.unsubscribe();
- **Sandbox bootstrap** (`@parity/truapi/sandbox`) that detects the host environment, builds the
matching provider, and exposes a cached client — see below.

## Development escape hatches

- **`development_createAccountProof(client, request)`** — `account.createAccountProof`
with `context` given as the exact 32-byte hex the proof is bound to, instead of a
product-namespaced `ProductProofContext`. Yet to be removed before a production
release; it lives entirely in `src/development.ts`.

## Sandbox bootstrap

`@parity/truapi/sandbox` wires up a client for browser-embedded hosts: it detects whether the app
Expand Down
51 changes: 51 additions & 0 deletions js/packages/truapi/src/development.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, test } from "bun:test";
import { okAsync } from "neverthrow";
import { development_createAccountProof } from "./development.js";
import type { HostAccountCreateProofRequest } from "./generated/index.js";

const context = `0x${"ab".repeat(32)}` as const;
const base = {
keyHandle: { dotNsIdentifier: "dim2.dot", derivationIndex: { tag: "Index", value: 0 } },
ringLocation: { chainId: `0x${"00".repeat(32)}`, junctions: [] },
message: "0x01",
} as const;

function stub() {
const seen: HostAccountCreateProofRequest[] = [];
const client = {
account: {
createAccountProof(request: HostAccountCreateProofRequest) {
seen.push(request);
return okAsync({
proof: "0x",
contextualAlias: { context, alias: "0x" },
ringIndex: 0,
ringRevision: 0,
});
},
},
};
return {
seen,
client: client as unknown as Parameters<typeof development_createAccountProof>[0],
};
}

describe("development_createAccountProof", () => {
test("forwards the request with the raw context marker", async () => {
const { seen, client } = stub();
await development_createAccountProof(client, { ...base, context });
expect(seen).toEqual([
{ ...base, context: { productId: "raw:", suffix: { tag: "Raw", value: context } } },
]);
});

test("rejects contexts that are not 32 bytes of hex", () => {
const { client } = stub();
for (const bad of ["0x00", "ab".repeat(32), `0x${"zz".repeat(32)}`]) {
expect(() =>
development_createAccountProof(client, { ...base, context: bad as `0x${string}` }),
).toThrow(TypeError);
}
});
});
60 changes: 60 additions & 0 deletions js/packages/truapi/src/development.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// TODO(development_createAccountProof): dev-only escape hatch, yet to be
// removed before a production release. Everything for it lives in this file,
// its test and one re-export in `index.ts`; delete those to remove it.
import type {
HostAccountCreateProofRequest,
HostAccountCreateProofResponse,
ProductProofContext,
TrUApiClient,
VersionedHostAccountCreateProofError,
} from "./generated/index.js";
import type { ResultAsync } from "./generated/client.js";
import type { CallErrorValue, HexString } from "./scale.js";

/** `productId` the signing host reads as "use the suffix bytes verbatim". */
const RAW_PROOF_CONTEXT_PRODUCT_ID = "raw:";

/** Same as `HostAccountCreateProofRequest`, with the 32-byte context given raw. */
export interface DevelopmentCreateProofRequest extends Omit<
HostAccountCreateProofRequest,
"context"
> {
/** The exact 32 bytes the proof is bound to, as `0x`-prefixed hex. */
context: HexString;
}

/**
* `account.createAccountProof` with a verbatim 32-byte proof context instead of
* a product-namespaced one.
*
*/
export function development_createAccountProof(
client: Pick<TrUApiClient, "account">,
request: DevelopmentCreateProofRequest,
): ResultAsync<
HostAccountCreateProofResponse,
CallErrorValue<VersionedHostAccountCreateProofError>
> {
const { context, ...rest } = request;
return client.account.createAccountProof({
...rest,
context: rawProofContext(context),
});
}

function rawProofContext(context: HexString): ProductProofContext {
const digits = context.startsWith("0x") ? context.slice(2) : null;
if (
digits === null ||
digits.length !== 64 ||
!/^[0-9a-fA-F]*$/.test(digits)
) {
throw new TypeError(
`development_createAccountProof: context must be 32 bytes of 0x-prefixed hex, got ${JSON.stringify(context)}`,
);
}
return {
productId: RAW_PROOF_CONTEXT_PRODUCT_ID,
suffix: { tag: "Raw", value: context },
};
}
1 change: 1 addition & 0 deletions js/packages/truapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * as scale from "./scale.js";
export type { Codec, HexString } from "./scale.js";
export * from "./generated/index.js";
export * from "./well-known-chains.js";
export * from "./development.js";
7 changes: 7 additions & 0 deletions rust/crates/truapi-host-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ make headless install # build dependencies and install truapi-host once
truapi-host signing-host
```

### Raw proof contexts (development only)

A product can bind a ring-VRF proof to 32 bytes of its choosing instead of a
product-namespaced context by calling `development_createAccountProof` from
`@parity/truapi`; the signing host honours it as is. Yet to be removed before a
production release.

### Browser products

`truapi-host dev` is one command for "run this product as if it were inside a
Expand Down
8 changes: 4 additions & 4 deletions rust/crates/truapi-server/src/runtime/pairing_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ use zeroize::Zeroizing;

use super::ring_vrf_registry::{RingVrfRegistryStore, validate_owner_listing};
use super::signing_host::ring_vrf::{
ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, context_bytes,
create_proof, member_from_entropy, sign_from_entropy,
ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, create_proof,
development_context_bytes, member_from_entropy, sign_from_entropy,
};

/// Distinguishes all remote authority request entrypoints by wire label.
Expand Down Expand Up @@ -2127,7 +2127,7 @@ impl PairingHost {
{
self.ring_resolver.validate(&request.ring_location).await?;
self.current_private_session(session)?;
let context = context_bytes(&request.context);
let context = development_context_bytes(&request.context);
let alias = alias_from_entropy(&entropy, &context)?;
return Ok(v01::ContextualAlias {
context,
Expand Down Expand Up @@ -2160,7 +2160,7 @@ impl PairingHost {
.resolve(&request.ring_location, &[MemberCandidate { member }])
.await?;
self.current_private_session(session)?;
let context = context_bytes(&request.context);
let context = development_context_bytes(&request.context);
let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?;
return Ok(v01::HostAccountCreateProofResponse {
proof,
Expand Down
8 changes: 4 additions & 4 deletions rust/crates/truapi-server/src/runtime/signing_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ use crate::runtime::statement_allowance::CollectionCandidate;
#[cfg(not(target_arch = "wasm32"))]
use crate::runtime::statement_allowance::collection::PersonhoodCollection;
use ring_vrf::{
ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, context_bytes,
create_proof, member_from_entropy, sign_from_entropy,
ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, create_proof,
development_context_bytes, member_from_entropy, sign_from_entropy,
};
use sso_replay::SsoReplayLocks;

Expand Down Expand Up @@ -839,7 +839,7 @@ impl ProductAuthority for SigningHost {
.resolve_ring_vrf_key_for_ring(session, &request.key_handle, &request.ring_location)
.await?;
self.ring_resolver.validate(&request.ring_location).await?;
let context = context_bytes(&request.context);
let context = development_context_bytes(&request.context);
let alias = alias_from_entropy(&entropy, &context)?;
Ok(v01::ContextualAlias {
context,
Expand All @@ -866,7 +866,7 @@ impl ProductAuthority for SigningHost {
// Reject a stale request if the local session disconnected or changed
// while its chain snapshot was being resolved.
self.require_current_session(session)?;
let context = context_bytes(&request.context);
let context = development_context_bytes(&request.context);
let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?;
Ok(v01::HostAccountCreateProofResponse {
proof,
Expand Down
21 changes: 21 additions & 0 deletions rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,27 @@ impl RingResolver for ChainRingResolver {
}
}

// TODO(development_createAccountProof): dev-only escape hatch, yet to be
// removed before a production release. Delete this module and point its
// callers in `signing_host.rs` and `pairing_host.rs` back at `context_bytes`.
mod development {
use truapi::v01::{DerivationIndex, ProductProofContext};

const RAW_CONTEXT_PRODUCT_ID: &str = "raw:";

/// [`super::context_bytes`], except that the `raw:` product id (which
/// dotNS cannot issue) makes the `Raw` suffix the context, verbatim.
pub(in crate::runtime) fn development_context_bytes(context: &ProductProofContext) -> [u8; 32] {
if context.product_id == RAW_CONTEXT_PRODUCT_ID
&& let DerivationIndex::Raw(bytes) = context.suffix
{
return bytes;
}
super::context_bytes(context)
}
}
pub(in crate::runtime) use development::development_context_bytes;

pub(in crate::runtime) fn context_bytes(context: &ProductProofContext) -> [u8; 32] {
let suffix = derivation_index_bytes(&context.suffix);
let mut input = Vec::with_capacity(9 + context.product_id.len() + suffix.len());
Expand Down