Skip to content

Commit a5d741c

Browse files
Add type: "derived" InputRequest for wallet-computed cryptographic values
Adds a fourth `InputRequest` variant `{ type: "derived", algorithm, args }` that asks the wallet to run a named cryptographic algorithm over its own state (view key, wallet-maintained counters, etc.) plus dapp-supplied args, and substitutes the result into a transaction input slot. The dapp never observes the wallet-side inputs — only the output. Strict opt-in via a new `algorithmsAllowed?: AlgorithmGrant[]` field on `ConnectOptions`: each grant authorizes exactly one `(algorithm, program, function, inputPosition)` call site, all four fields required and exact-match. The wallet refuses every derived request whose tuple is not present. No broad default. A new `algorithmsSupported(): Promise<string[]>` adapter method lets dapps discover what a wallet implements before populating the allowlist. Wallets without derived-input support return `[]` (default) and connections with non-empty `algorithmsAllowed` throw `WalletConnectOptionsNotSupportedError` via the existing `hasUnsupportedConnectOptions` path. Inaugural algorithm: `program-scoped-address-blind`. Inputs: `{ "domain-separator": field }`. Output: `address`. Valid slot positions: `address`, `group`, `scalar`, `field`. Updates the PrivateInputs example with a fourth `Derived` mode on primitive slots whose baseType is in an algorithm's `validSlotTypes`, plus a connect-time `AlgorithmGrant[]` editor with an "Auto-grant this function's eligible slots" convenience button. The grant JSON preview now includes `algorithmsAllowed`. See docs/adapter-privacy-extension.md §"Derived inputs" for the spec and docs/dapp-privacy-quickstart.md for an implementor's guide.
1 parent e2d22e8 commit a5d741c

15 files changed

Lines changed: 631 additions & 18 deletions

File tree

.changeset/derived-inputs.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@provablehq/aleo-types': minor
3+
'@provablehq/aleo-wallet-standard': minor
4+
'@provablehq/aleo-wallet-adaptor-core': minor
5+
'@provablehq/aleo-wallet-adaptor-leo': minor
6+
'@provablehq/aleo-wallet-adaptor-fox': minor
7+
'@provablehq/aleo-wallet-adaptor-soter': minor
8+
'@provablehq/aleo-wallet-adaptor-puzzle': minor
9+
'@provablehq/aleo-wallet-adaptor-shield': minor
10+
'@provablehq/aleo-wallet-adaptor-react': minor
11+
---
12+
13+
Add `type: "derived"` InputRequest for wallet-evaluated cryptographic algorithms
14+
15+
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.
16+
17+
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.
18+
19+
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).
20+
21+
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.
22+
23+
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.
24+
25+
See `docs/adapter-privacy-extension.md` § "Derived inputs" for the full spec, and `docs/dapp-privacy-quickstart.md` for an implementor's guide.

docs/adapter-privacy-extension.md

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Goal
44

5-
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.
5+
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.
66

77
## Wire-level types
88

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

1212
type InputRequest =
1313
| { 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`.
14-
| { 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`.
14+
| { 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`.
15+
| { 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.
1516

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

1920
interface RecordView {
2021
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).
2122
}
23+
24+
// Re-exports the existing LiteralType enum from `@provablehq/aleo-types` (`packages/aleo-types/src/data.ts`).
25+
type LiteralType =
26+
| "address" | "bool" | "group"
27+
| "u8" | "u16" | "u32" | "u64" | "u128"
28+
| "i8" | "i16" | "i32" | "i64" | "i128"
29+
| "field" | "scalar" | "signature";
30+
31+
// New algorithms are added to this literal union as they're standardized. The `(string & {})` permits
32+
// unknown values for forward-compat — the wallet validates against its own `algorithmsSupported()` list at runtime.
33+
type KnownAlgorithm = "program-scoped-address-blind";
34+
type AlgorithmName = KnownAlgorithm | (string & {});
35+
36+
interface AlgorithmArg {
37+
type: LiteralType; // parsing directive — the wallet decodes `value` according to this Aleo primitive type
38+
value: string; // an Aleo literal in canonical string form (e.g. "12345field", "100u64", "true")
39+
}
2240
```
2341

2442
`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:
@@ -33,6 +51,7 @@ The remaining (legacy) fields — including `recordPlaintext`, `commitment`, `ta
3351
The `InputRequest` sends a request to the wallet (which is then authorized by the user) to do the following:
3452
1. Input the user's address into a position where there's an address, group, scalar, or field input.
3553
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.
54+
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.
3655

3756
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.
3857

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

6080
type RecordAccessGrant =
@@ -75,6 +95,16 @@ interface FieldGrant {
7595
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.
7696
readAccess?: boolean; // undefined → true; false → field is usable as a filter key but plaintext is withheld on decrypt
7797
}
98+
99+
// Each grant authorizes one specific call site. All four fields are required and exact-match;
100+
// there is no wildcard. A dapp that wants to use the same algorithm at multiple call sites lists
101+
// each one as its own entry.
102+
interface AlgorithmGrant {
103+
algorithm: AlgorithmName; // must appear in the wallet's `algorithmsSupported()` list
104+
program: string; // must also appear in `programs`
105+
function: string; // exact transition name within `program`
106+
inputPosition: number; // 0-based index into the function's input slots
107+
}
78108
```
79109

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

122152
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.
123153

154+
### Derived inputs
155+
156+
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.
157+
158+
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.
159+
160+
#### Discovery
161+
162+
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`.
163+
164+
Every `algorithmsAllowed[].algorithm` is validated at connect time against the wallet's `algorithmsSupported()`. Unknown names are rejected.
165+
166+
#### Output type and slot compatibility
167+
168+
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).
169+
170+
#### Algorithm catalog
171+
172+
##### `program-scoped-address-blind`
173+
174+
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.
175+
176+
| Property | Value |
177+
|---|---|
178+
| `args` (dapp-provided) | `{ "domain-separator": AlgorithmArg<field> }` |
179+
| wallet-derived inputs | program address (as field), active view key (as field), wallet-maintained counter (as field) |
180+
| output type | `address` |
181+
| valid input slot positions | `address`, `group`, `scalar`, `field` |
182+
183+
Algorithm (pseudo, matching the wallet's reference implementation):
184+
185+
```
186+
r = Poseidon4.hashToScalar([programAddrField, domainSeparatorField, viewKeyField, counterField])
187+
blinded = BHP256.commitToGroup(signerGroup.toBitsLE(), r)
188+
result = Address.fromGroup(blinded)
189+
```
190+
191+
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`.
192+
193+
Future algorithms are added to `KnownAlgorithm` and documented under their own catalog subsection in this spec.
194+
195+
#### Interaction with `readAddress: false`
196+
197+
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.
198+
199+
#### Interaction with `programs`
200+
201+
`algorithmsAllowed[].program` must appear in `programs`. The wallet rejects mismatches at connect time. This mirrors the subset constraint already enforced for `recordAccess.programs[].program`.
202+
124203
### Address exposure
125204

126205
`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.
@@ -165,8 +244,10 @@ flowchart TD
165244
D --> R["fulfillInputRequests.ts"]
166245
R -- "type: record" --> R1["filter unspent records<br/>by where clause"]
167246
R -- "type: user" --> R3["render typed form<br/>from program signature"]
247+
R -- "type: derived" --> R4["run algorithm via SDK<br/>using wallet-derived secrets"]
168248
R1 --> F["confirm screen<br/>shows every fulfilled value"]
169249
R3 --> F
250+
R4 --> F
170251
F -- user confirms --> G["initializeGenericTransaction<br/>(fulfilled string[], lockedRecords)"]
171252
G ===> H["worker.ts → SDK<br/>UNCHANGED"]
172253
@@ -195,3 +276,10 @@ The worker boundary still receives `string[]`. All fulfillment is wallet-side; t
195276
| `connect()` | `recordAccess.programs[].program` not present in `programs` | connect-time validation error |
196277
| `requestRecords` | called against a program in `programs` but absent from `recordAccess.programs[]` (when `recordAccess` is set) | permission error at gate |
197278
| `requestRecords` | `includePlaintext: true` while `decryptPermission: NoDecrypt` | permission error at gate (today's behavior, restated) |
279+
| `type: "derived"` | `(algorithm, program, function, inputPosition)` tuple not in `algorithmsAllowed` | permission error at gate |
280+
| `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`) |
281+
| `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 |
282+
| `type: "derived"` | `AlgorithmArg.value` does not parse as a literal of `AlgorithmArg.type` | validation error before gate |
283+
| `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 |
284+
| `connect()` | `algorithmsAllowed[].program` not present in `programs` | connect-time validation error |
285+
| `connect()` | `algorithmsAllowed[].algorithm` not in the wallet's `algorithmsSupported()` | connect-time validation error |

docs/dapp-privacy-quickstart.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ How to use the wallet-adapter's new privacy features from a dapp. For the full s
44

55
## What's new
66

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

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

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

2022
## Wiring connect-time options
2123

@@ -133,6 +135,49 @@ await executeTransaction({
133135
});
134136
```
135137

138+
## Derived inputs (`type: "derived"`)
139+
140+
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.**
141+
142+
Strictly opt-in via `algorithmsAllowed` at connect time. Each grant authorizes exactly one `(algorithm, program, function, inputPosition)` call site:
143+
144+
```ts
145+
import { ALGORITHM_SCHEMAS } from '@provablehq/aleo-types';
146+
147+
<AleoWalletProvider
148+
// ...
149+
algorithmsAllowed={[
150+
{ algorithm: 'program-scoped-address-blind',
151+
program: 'myapp.aleo', function: 'vote', inputPosition: 0 },
152+
]}
153+
>
154+
```
155+
156+
Discovery: call `useWallet().algorithmsSupported()` (no connection required) to see which algorithms the active adapter implements. Wallets without derived-input support return `[]`.
157+
158+
At execute time, pass `{ type: "derived", algorithm, args }` in the matching slot:
159+
160+
```ts
161+
await executeTransaction({
162+
program: 'myapp.aleo',
163+
function: 'vote',
164+
inputs: [
165+
{ type: 'derived',
166+
algorithm: 'program-scoped-address-blind',
167+
args: {
168+
// Pre-encode anything non-primitive to an Aleo literal; the wallet only
169+
// accepts AlgorithmArg values that are LiteralType-parseable strings.
170+
'domain-separator': { type: 'field', value: '12345field' },
171+
},
172+
label: 'Your private voter handle',
173+
},
174+
// ...rest of the function's inputs
175+
],
176+
});
177+
```
178+
179+
`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".
180+
136181
## Error classes
137182

138183
Imported from `@provablehq/aleo-wallet-adaptor-core`:
@@ -152,3 +197,4 @@ If your existing dapp:
152197
- **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.
153198
- **Composes `TransactionOptions.inputs` as plain strings** — keep doing that. The new `InputRequest` shapes are opt-in per-slot.
154199
- **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.
200+
- **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.

0 commit comments

Comments
 (0)