Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/derived-inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@provablehq/aleo-types': minor
'@provablehq/aleo-wallet-standard': minor
'@provablehq/aleo-wallet-adaptor-core': minor
'@provablehq/aleo-wallet-adaptor-leo': minor
'@provablehq/aleo-wallet-adaptor-fox': minor
'@provablehq/aleo-wallet-adaptor-soter': minor
'@provablehq/aleo-wallet-adaptor-puzzle': minor
'@provablehq/aleo-wallet-adaptor-shield': minor
'@provablehq/aleo-wallet-adaptor-react': minor
---

Add `type: "derived"` InputRequest for wallet-evaluated cryptographic algorithms

A new `InputRequest` variant lets a dapp ask the wallet to compute a value by running a named cryptographic algorithm over the wallet's own state (view key, wallet-maintained counters, etc.) plus dapp-supplied `args`, and substitute the result into a transaction input slot. The dapp never observes the wallet-side inputs — only the output.

Strictly opt-in: a new `algorithmsAllowed?: AlgorithmGrant[]` field on `ConnectOptions` authorizes derived inputs at exact `(algorithm, program, function, inputPosition)` call sites. All four fields are required and exact-match; there is no broad default. The wallet refuses every derived request whose tuple is not present.

A new adapter method `algorithmsSupported(): Promise<string[]>` lets a dapp discover which algorithms a wallet implements before populating `algorithmsAllowed`. Wallets without derived-input support return `[]` (the base implementation's default).

Inaugural algorithm: `program-scoped-address-blind`. Inputs (dapp-provided): `{ "domain-separator": field }`. Output type: `address`. Valid input slot positions: `address`, `group`, `scalar`, `field`. The output is a per-program blinded address whose link to the active address is hidden by a BHP256 commitment.

The `<AleoWalletProvider>` React component accepts a new optional `algorithmsAllowed` prop and forwards it on connect; the `useWallet()` context exposes `algorithmsSupported`. Existing usages without these are unaffected.

See `docs/adapter-privacy-extension.md` § "Derived inputs" for the full spec, and `docs/dapp-privacy-quickstart.md` for an implementor's guide.
92 changes: 90 additions & 2 deletions docs/adapter-privacy-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Goal

Let dapps emit `TransactionOptions` whose `inputs` slots are not always literal Aleo values. Each non-literal slot is a **request** to the wallet — to prompt the user or to auto-select an owned record matching dapp-supplied criteria. The wallet fulfills the request before passing the transaction to the SDK.
Let dapps emit `TransactionOptions` whose `inputs` slots are not always literal Aleo values. Each non-literal slot is a **request** to the wallet — to prompt the user, to auto-select an owned record matching dapp-supplied criteria, or to compute a value by running a named cryptographic algorithm over the user's wallet state. The wallet fulfills the request before passing the transaction to the SDK.

## Wire-level types

Expand All @@ -11,14 +11,32 @@ type Input = string | InputRequest;

type InputRequest =
| { type: "address"; label?: string } // Specification to fill the input field with the active address. Allowed in an input position with an aleo type of: `address, group, scalar, or field`.
| { type: "record"; program: string; filters?: RecordFilters; uid?: string }; // Specification to use a record from a specific program. When `uid` is present, it pins the exact record previously returned by `requestRecords` and `filters` is ignored. When absent, the wallet picks any unspent record matching `filters`. Allowed in an input position with an aleo type of: `record, dynamic_record, or external_record`.
| { type: "record"; program: string; filters?: RecordFilters; uid?: string } // Specification to use a record from a specific program. When `uid` is present, it pins the exact record previously returned by `requestRecords` and `filters` is ignored. When absent, the wallet picks any unspent record matching `filters`. Allowed in an input position with an aleo type of: `record, dynamic_record, or external_record`.
| { type: "derived"; algorithm: AlgorithmName; args: Record<string, AlgorithmArg>; label?: string }; // Specification to fill the input field with the output of a wallet-evaluated cryptographic algorithm. Each algorithm declares its expected `args` schema and its output Aleo type; the output type determines which input positions are valid. Strictly opt-in — the wallet refuses every derived request whose (algorithm, program, function, inputPosition) tuple is not in the connection's `algorithmsAllowed`. See "Derived inputs" below.

type RecordFilters = Record<string, RecordFieldFilter>; // keys are top-level record field names or dotted paths into struct fields, e.g. "amount" or "data.amount".
type RecordFieldFilter = { eq?: string, gte?: string, lte?: string, neq?: string, }; // potential matching conditions, AND-combined.

interface RecordView {
fields: Record<string, string>; // parsed structured form of the record's plaintext. Only fields the dapp has read access to are present; redacted fields are omitted (not present-with-undefined).
}

// Re-exports the existing LiteralType enum from `@provablehq/aleo-types` (`packages/aleo-types/src/data.ts`).
type LiteralType =
| "address" | "bool" | "group"
| "u8" | "u16" | "u32" | "u64" | "u128"
| "i8" | "i16" | "i32" | "i64" | "i128"
| "field" | "scalar" | "signature";

// New algorithms are added to this literal union as they're standardized. The `(string & {})` permits
// unknown values for forward-compat — the wallet validates against its own `algorithmsSupported()` list at runtime.
type KnownAlgorithm = "program-scoped-address-blind";
type AlgorithmName = KnownAlgorithm | (string & {});

interface AlgorithmArg {
type: LiteralType; // parsing directive — the wallet decodes `value` according to this Aleo primitive type
value: string; // an Aleo literal in canonical string form (e.g. "12345field", "100u64", "true")
}
```

`requestRecords` continues to return its existing wallet-defined record shape (e.g. Shield's `OwnedRecord` at `shield-extension/src/background/types/RecordScanner.ts:83`). Two optional fields are added additively to each returned record:
Expand All @@ -33,6 +51,7 @@ The remaining (legacy) fields — including `recordPlaintext`, `commitment`, `ta
The `InputRequest` sends a request to the wallet (which is then authorized by the user) to do the following:
1. Input the user's address into a position where there's an address, group, scalar, or field input.
2. Use a record whose fields match the `filters` on specific record's members and filter for records that match them if applicable, returning an error if the condition cannot be applied or a record matching it cannot be found.
3. Run a named cryptographic algorithm over the wallet's state (view key, program-scoped counters, etc.) plus dapp-supplied arguments, and use the output as input. Authorized only at exact `(algorithm, program, function, inputPosition)` call sites.

The wallet has the program's source, so it reads a function's parameter signature for input position `i` and renders the form control accordingly. `label` is UX-only.

Expand All @@ -55,6 +74,7 @@ interface ConnectHistory {
programs?: string[]; // unchanged — program-level gate for both transaction execution and record operations
readAddress?: boolean; // new — opt-in address withholding; default true
recordAccess?: RecordAccessGrant; // new — opt-in record/field narrowing
algorithmsAllowed?: AlgorithmGrant[]; // new — strict opt-in derived-input allowlist; default undefined → every `type: "derived"` request is refused
}

type RecordAccessGrant =
Expand All @@ -75,6 +95,16 @@ interface FieldGrant {
name: string; // a record-body field name (e.g. "amount", "data.amount"), or a `$`-prefixed envelope-metadata name from the reserved set: `$commitment`, `$tag`, `$transitionId`, `$transactionId`, `$outputIndex`, `$transactionIndex`, `$transitionIndex`, `$owner`, `$sender`. The `$` prefix prevents collision with any body field named identically.
readAccess?: boolean; // undefined → true; false → field is usable as a filter key but plaintext is withheld on decrypt
}

// Each grant authorizes one specific call site. All four fields are required and exact-match;
// there is no wildcard. A dapp that wants to use the same algorithm at multiple call sites lists
// each one as its own entry.
interface AlgorithmGrant {
algorithm: AlgorithmName; // must appear in the wallet's `algorithmsSupported()` list
program: string; // must also appear in `programs`
function: string; // exact transition name within `program`
inputPosition: number; // 0-based index into the function's input slots
}
```

| Configuration | Meaning |
Expand Down Expand Up @@ -121,6 +151,55 @@ Rationale: when the dapp asks for narrow field access, it has explicitly given u

Under `readAddress: false`, the strip rules tighten further independently of the grant: `owner`, `sender`, `commitment`, `tag`, `transitionId`, `transactionId`, and `recordPlaintext` are always omitted, and any `recordView.fields` whose Aleo type is `address` and whose plaintext equals the active address is omitted. `uid` and `recordView` (with non-address fields) remain the dapp's only handles to the record.

### Derived inputs

A `type: "derived"` request asks the wallet to compute a value by running a named cryptographic algorithm over the wallet's own state (the view key, a per-(origin, program) counter, etc.) combined with the dapp's `args`, then substitute the result into the input slot. The dapp never observes the wallet-side inputs — only the final output.

Derived inputs are **strictly opt-in**: the wallet refuses every derived request whose `(algorithm, program, function, inputPosition)` tuple is not present in `algorithmsAllowed`. There is no broad default. The four fields are required and exact-match — a grant for `(algorithm: X, program: "p.aleo", function: "f", inputPosition: 0)` does not authorize the same algorithm at `inputPosition: 1`, at a different function, or at a different program. A dapp that wants the same algorithm at multiple call sites lists each one as its own entry.

#### Discovery

Adapters expose `algorithmsSupported(): Promise<AlgorithmName[]>`. A dapp calls this before connect to learn which algorithms a wallet implements, then requests a matching subset in `algorithmsAllowed`. Wallets that don't implement derived inputs at all return an empty array or throw `MethodNotImplementedError`.

Every `algorithmsAllowed[].algorithm` is validated at connect time against the wallet's `algorithmsSupported()`. Unknown names are rejected.

#### Output type and slot compatibility

Each `KnownAlgorithm` has a fixed Aleo output type, declared in the catalog below. The wallet additionally validates at execute time that the function's signature at `inputs[i]` is a valid position for that output type — same rules as `type: "address"` (e.g. an `address`-typed output is valid in `address` / `group` / `scalar` / `field` slots).

#### Algorithm catalog

##### `program-scoped-address-blind`

Produces a blinded address scoped to a specific program. Two dapps using the same wallet against different programs derive different blinded addresses. Whether the same dapp derives the same blinded address across executions is governed by wallet-internal state; the dapp can neither control nor observe that state.

| Property | Value |
|---|---|
| `args` (dapp-provided) | `{ "domain-separator": AlgorithmArg<field> }` |
| wallet-derived inputs | program address (as field), active view key (as field), wallet-maintained counter (as field) |
| output type | `address` |
| valid input slot positions | `address`, `group`, `scalar`, `field` |

Algorithm (pseudo, matching the wallet's reference implementation):

```
r = Poseidon4.hashToScalar([programAddrField, domainSeparatorField, viewKeyField, counterField])
blinded = BHP256.commitToGroup(signerGroup.toBitsLE(), r)
result = Address.fromGroup(blinded)
```

The dapp never observes the view key, the counter, or the intermediate scalar `r`. The output is an address whose link to the active address is hidden by the BHP256 commitment — recovering the active address from the output is computationally infeasible without `r`.

Future algorithms are added to `KnownAlgorithm` and documented under their own catalog subsection in this spec.

#### Interaction with `readAddress: false`

Derived inputs are allowed under `readAddress: false`. The dapp does not learn the active address through the derived flow — it only sees the algorithm's output. For `program-scoped-address-blind` the output is itself a fresh blinded address, so it does not leak the active address even when the dapp inspects the resulting transaction. Algorithms whose output is the active address itself (or trivially reversible to it) must not be admitted to `KnownAlgorithm` for this reason.

#### Interaction with `programs`

`algorithmsAllowed[].program` must appear in `programs`. The wallet rejects mismatches at connect time. This mirrors the subset constraint already enforced for `recordAccess.programs[].program`.

### Address exposure

`readAddress?: boolean` controls whether the dapp learns the user's address. Defaults to `true` (undefined treated as `true`); `false` is opt-in for privacy-preserving dapps.
Expand Down Expand Up @@ -165,8 +244,10 @@ flowchart TD
D --> R["fulfillInputRequests.ts"]
R -- "type: record" --> R1["filter unspent records<br/>by where clause"]
R -- "type: user" --> R3["render typed form<br/>from program signature"]
R -- "type: derived" --> R4["run algorithm via SDK<br/>using wallet-derived secrets"]
R1 --> F["confirm screen<br/>shows every fulfilled value"]
R3 --> F
R4 --> F
F -- user confirms --> G["initializeGenericTransaction<br/>(fulfilled string[], lockedRecords)"]
G ===> H["worker.ts → SDK<br/>UNCHANGED"]

Expand Down Expand Up @@ -195,3 +276,10 @@ The worker boundary still receives `string[]`. All fulfillment is wallet-side; t
| `connect()` | `recordAccess.programs[].program` not present in `programs` | connect-time validation error |
| `requestRecords` | called against a program in `programs` but absent from `recordAccess.programs[]` (when `recordAccess` is set) | permission error at gate |
| `requestRecords` | `includePlaintext: true` while `decryptPermission: NoDecrypt` | permission error at gate (today's behavior, restated) |
| `type: "derived"` | `(algorithm, program, function, inputPosition)` tuple not in `algorithmsAllowed` | permission error at gate |
| `type: "derived"` | `algorithm` not in the wallet's `algorithmsSupported()` list | permission error at gate (also caught at connect time if the dapp listed it in `algorithmsAllowed`) |
| `type: "derived"` | `args` missing a key required by the algorithm's schema, has an extra key, or an `AlgorithmArg.type` mismatches the algorithm's expected type for that key | validation error before gate |
| `type: "derived"` | `AlgorithmArg.value` does not parse as a literal of `AlgorithmArg.type` | validation error before gate |
| `type: "derived"` | algorithm output type incompatible with the function signature at `inputs[i]` (e.g. an `address`-producing algorithm used in a `u64` slot) | validation error before gate |
| `connect()` | `algorithmsAllowed[].program` not present in `programs` | connect-time validation error |
| `connect()` | `algorithmsAllowed[].algorithm` not in the wallet's `algorithmsSupported()` | connect-time validation error |
48 changes: 47 additions & 1 deletion docs/dapp-privacy-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ How to use the wallet-adapter's new privacy features from a dapp. For the full s

## What's new

Two connect-time grants and two transaction-input request types:
Three connect-time grants and three transaction-input request types:

| Grant | Type | Effect |
|---|---|---|
| `readAddress` | `boolean` (default `true`) | When `false`, the dapp transacts without learning the active address. Requires `decryptPermission: NoDecrypt`. |
| `recordAccess` | `RecordAccessGrant` (default `undefined` = broad) | Per-program / per-record / per-field narrowing of record reads. |
| `algorithmsAllowed` | `AlgorithmGrant[]` (default `undefined` = none) | Strict opt-in allowlist for `type: "derived"` requests. Each entry authorizes one exact `(algorithm, program, function, inputPosition)` call site. |

| `InputRequest` slot type | Shape | Valid in |
|---|---|---|
| `{ type: "address" }` | wallet injects active address | `address`, `group`, `scalar`, `field` |
| `{ type: "record", program, uid }` | pin specific record by handle | `record`, `dynamic_record`, `external_record` |
| `{ type: "record", program, filters }` | wallet auto-selects matching record | same |
| `{ type: "derived", algorithm, args }` | wallet runs a named crypto algorithm | depends on algorithm — see catalog |

## Wiring connect-time options

Expand Down Expand Up @@ -133,6 +135,49 @@ await executeTransaction({
});
```

## Derived inputs (`type: "derived"`)

A `type: "derived"` slot tells the wallet to compute a value by running a named cryptographic algorithm over its own state (view key, wallet-maintained counters, etc.) plus your `args`, and substitute the result. **You never see the wallet-side inputs — only the output.**

Strictly opt-in via `algorithmsAllowed` at connect time. Each grant authorizes exactly one `(algorithm, program, function, inputPosition)` call site:

```ts
import { ALGORITHM_SCHEMAS } from '@provablehq/aleo-types';

<AleoWalletProvider
// ...
algorithmsAllowed={[
{ algorithm: 'program-scoped-address-blind',
program: 'myapp.aleo', function: 'vote', inputPosition: 0 },
]}
>
```

Discovery: call `useWallet().algorithmsSupported()` (no connection required) to see which algorithms the active adapter implements. Wallets without derived-input support return `[]`.

At execute time, pass `{ type: "derived", algorithm, args }` in the matching slot:

```ts
await executeTransaction({
program: 'myapp.aleo',
function: 'vote',
inputs: [
{ type: 'derived',
algorithm: 'program-scoped-address-blind',
args: {
// Pre-encode anything non-primitive to an Aleo literal; the wallet only
// accepts AlgorithmArg values that are LiteralType-parseable strings.
'domain-separator': { type: 'field', value: '12345field' },
},
label: 'Your private voter handle',
},
// ...rest of the function's inputs
],
});
```

`ALGORITHM_SCHEMAS` from `@provablehq/aleo-types` ships the args schema, output type, and valid slot positions for every known algorithm — use it to render correct forms or pre-validate shapes. Full algorithm catalog: see [`adapter-privacy-extension.md`](./adapter-privacy-extension.md) § "Algorithm catalog".

## Error classes

Imported from `@provablehq/aleo-wallet-adaptor-core`:
Expand All @@ -152,3 +197,4 @@ If your existing dapp:
- **Reads records via `requestRecords`** — return shape is unchanged when no grant is set. The new `recordView` / `uid` fields are additive optional keys you can ignore.
- **Composes `TransactionOptions.inputs` as plain strings** — keep doing that. The new `InputRequest` shapes are opt-in per-slot.
- **Wants to adopt narrowed grants** — populate `recordAccess` at the provider level; use `RecordGrant.fields: []` to mint records usable purely for `uid` pinning with zero plaintext leakage.
- **Wants to use derived inputs** — populate `algorithmsAllowed` with one grant per call site, then place `{ type: "derived", algorithm, args }` in the corresponding `inputs[i]`. There is no broad default — empty `algorithmsAllowed` refuses every derived request.
Loading