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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ is baked into the jar.
The pinned version lives in `gradle.properties`:

```properties
yanoVersion = 0.1.0-pre13
yanoVersion = 0.1.0-pre16
```

Resolution order (first hit wins), which mirrors what `NodeLocator` does at
Expand Down
113 changes: 113 additions & 0 deletions adr/047-bounded-cip30-signer-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# ADR-047: Bounded multi-address signer search

- Status: Implemented in development; real extension/ledger qualification pending
- Date: 2026-09-08
- Related: ADR-035 (CIP-30), ADR-037 (HD accounts), ADR-042 (transaction review)
- Scope: Software-wallet CIP-30 `signTx` and `signData`; not hardware signing

## Problem

The desktop can derive many receive addresses but the dApp transaction signer previously
used only receive index 0. An account enrolling payment keys from indexes 0, 1 and 2 could
collect only the first witness. Address-use discovery is insufficient: an unused address
can still be an explicit transaction authority.

## Final search rule

1. Restrict search to the currently unlocked HD account. Never try other HD accounts or
seed groups implicitly. Only payment chains receive (role 0) and change (role 1) are searched.
2. First examine public paths known in this unlock session: addresses displayed/exported
through the address view and funded paths discovered by balance scanning. Duplicates
are removed. These exact known indexes may lie beyond the fallback window; do not derive
the intervening range just because a high index is known. Cap known paths at 10,000.
3. Then examine receive indexes 0–29 and change indexes 0–29, irrespective of usage or gaps.
Stop once every outstanding target has matched. This means 30 per chain, not 30 combined.
4. If hashes remain unmatched, ask explicitly whether to extend this request through index
49 on both chains. Declining expansion retains the initial result; it does not authorize
signing. Never extend automatically or beyond 49 except for exact known paths from step 2.
5. Targets come from transaction-body `required_signers` and payment credentials of resolved
spending/collateral inputs. Do not sign merely because an address appears in outputs or
reference inputs. Resolve inputs against the selected backend and check their identities;
unresolved inputs fail closed. Script-payment inputs do not imply payment-key witnesses.
6. Retain the existing single-account stake-key relevance check for required signers,
withdrawals and certificates; do not derive stake keys for every payment index. This
increment does not add discovery of arbitrary native-script-only, governance or other
certificate authorities that are not named in required signers or input credentials.
7. Pre-existing vkey witnesses must verify against the original body before they count as
satisfied. Do not return them again or add duplicate/unrelated primary-payment witnesses.
8. Show the selected HD account, matched derivation paths, search window and unmatched hashes
before approving. One approval signs all listed keys over the same original body bytes.
Expansion and final approval are separate decisions. An unmatched hash may belong to
another wallet, another account or an index outside the search; do not label it invalid.
9. For `partialSign=true`, return the reviewed relevant witnesses and leave unmatched targets
to other participants. No matches is an explicit failure. For `partialSign=false`, reject
if any of the collected target hashes remain unmatched; this is not a ledger-validation
promise or a general native-script satisfiability solver.
10. Bind approval to the exact request bytes, partial-sign flag, unlocked session and backend
connection. Consume the approval once. Account/network changes reject and require review
again. Sign the original CBOR, never a reconstructed body. Search makes no signatures.

Input resolution plus derivation has a 10-second deadline per search attempt. At most two
search workers and two queued attempts are allowed; cancellation or failure signs nothing.
Review accepts at most 64 KiB transaction bytes and 256 unique spending/collateral inputs.
These are defensive client limits, not claims about ledger transaction limits.

## Discovery and compatibility

Known-path metadata is currently session-local. A fresh unlock/seed restoration searches the
30/50 windows until high paths are displayed or rediscovered. Persistent public path metadata
is a separate enhancement; never imply a gap scan recovers every unused enrolled key.

`getUsedAddresses` remains an address-use API, not a complete key inventory. Do not put unused
addresses into it to work around a dApp. Kavach's transaction precheck was adjusted to call
`signTx` without treating those lists as proof of the wallet's entire signing capability;
its backend still validates witness membership and signatures on the unchanged transaction.

This implementation leaves `signData` at its existing index-0 behavior. COSE multi-address
lookup needs its own address binding and consent work. Hardware paths are not expanded.

## Validation

`DappSignerSearchTest` covers unused receive 0/1/2, change keys, 29/30/49/50 boundaries,
known higher indexes, selected-account isolation, input/collateral versus output/reference
ownership, missing/mismatched input resolution, existing witness verification and duplicate
suppression, relevant stake keys, malformed/bounded requests, and multi-key signatures over
an indefinite-CBOR regression body.

`Cip30SignerSearchApprovalTest` exercises the actual gate-to-signer wiring: one approval for
multiple keys, explicit extension, declined extension with partial signing, full-sign rejection,
no-match rejection, user rejection, session/network/body changes, one-use tickets and generated
high-index metadata. Existing signer, connector, wallet and application tests also run.

These are local cryptographic and integration-of-components tests. They do not claim an actual
extension session, hardware approval or full node validation of the new multi-address workflow.
Keep Kavach's pending setup/backend alive when restarting only Yano to qualify it interactively.

## COSE data-signing extension (2026-09-08)

The same software-account search now applies to CIP-30 `signData`: known payment
paths, then receive/change 0–29, then explicit extension to 0–49. Targets come only
from the requested address's payment credential; key reward addresses retain the
single selected-account stake key. The current bounded address profile accepts key
enterprise and key-payment base addresses, plus key reward addresses; other address
forms fail closed. Network mismatches and unmatched keys reject without falling back
to receive index 0. There are no node/history/gap queries during data-key discovery.

The approval displays the matched path, full requested address and payload hex, and
states that a data signature can authorize later actions. A one-use ticket binds
address and payload to the reviewed session and backend connection; changing any of
these, rejecting consent or calling the signer without a ticket fails closed.
The bounded search executor/timeouts are shared with transaction signing.

Kavach exposes an explicit pending wallet-credential selector. It may construct a
testnet enterprise address for that exact payment hash when the wallet does not list
it; Yano decides ownership and obtains consent. Kavach still verifies the resulting
COSE key/signature against the expected credential and proof digest. It never tries
another authority automatically after a signing failure. The iPhone approval path
and fee-wallet transaction-signature path remain distinct.

Regression tests cover unused receive/change keys, verifiable COSE evidence and exact
returned public keys, 30/50 boundaries, known high indexes, other-account/network and
malformed-address rejection, explicit expansion, denied consent, request mutation,
account/network switches, and one-use approvals. Full core/connector/app tests and
Kavach frontend build/tests pass. Live user-wallet confirmation is recorded separately.
21 changes: 17 additions & 4 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,23 @@ Verification-only flags (used by the screenshot/e2e harness):
all go through it, so behavior can't drift.
- **Managed node = child process** (not in-process): crash isolation and the
node stays a native-imageable binary. Overrides via `-D` sysprops that MUST
precede `-jar`: `quarkus.profile=<network>,wallet`, `quarkus.http.port`,
`yano.server.port`, `yano.storage.path`. The `,wallet` profile is required so
real networks enable the wallet APIs (address/tx/reward history) — only
`%devnet` turns them on by itself.
precede `-jar`: `-Xmx`, then `quarkus.profile=<network>,<sizing>,wallet`,
`quarkus.http.port`, `yano.server.port`, `yano.storage.path`. The `,wallet`
profile is required so real networks enable the wallet APIs (scan index,
address-first-seen, UTxO state) — only `%devnet` turns them on by itself — and
it stays last because a later profile wins. The sizing profile (`medium` by
default) sets RocksDB caches and the decoded-block queue for a 4 GiB+ desktop.
It carries no heap of its own: `yano.sh` pairs each profile with an `-Xmx` and
we do not go through `yano.sh`, so the launcher passes `-Xmx` itself. The JVM
reads no `JAVA_OPTS`, and a `-Xmx` after `-jar` is a program argument the JVM
ignores — hence the ordering, pinned by `NodeLaunchCommandTest`.
- **Managed node sizing is user-editable**: `~/.yano-wallet/<network>/node/node.properties`
(beside `node.log`), written as a commented template on first managed start.
`sizingProfile=xsmall|small|medium|large` picks the profile and its heap
(384m/384m/1536m/2g, mirroring `yano.sh:355-373`); `maxHeap=` overrides just
the heap. Bad values are logged and ignored rather than failing the start, and
edits apply on the next node start. `-Dyano.wallet.node.sizing-profile` and
`-Dyano.wallet.node.max-heap` on the *wallet* do the same for a dev run.
- **Node not native for the UI.** Per ADR-033, the JavaFX UI ships via
jlink/jpackage (Gluon's GraalVM is frozen below JDK 25); the node stays the
GraalVM-native binary. Keep `wallet-app`/`wallet-ui` framework-light so a
Expand Down
93 changes: 93 additions & 0 deletions docs/mempool-coin-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Yano mempool-aware coin selection

Yano transaction builders request `include_mempool=true` on address/payment-
credential listings and individual output resolution. The node excludes pending-
spent inputs and includes unspent pending change, with filtering and ordering
before pagination. The wallet also applies its local input reservations.

The node must support the published listing contract. This was verified against
port 7070 using the supplied test address: the overlay excluded a confirmed
outpoint and returned a different pending output with `block: null`. A 200 alone
would not have established support. There is no custom capability-header check,
no new supplier class, and no confirmed-only fallback on lookup failures. Nodes
that silently ignore the parameter are outside this preview version's contract.

## Selection and submission

`YanoNodeBackend.selectionUtxoSupplier()` uses the existing `YanoUtxoSupplier`
with the node overlay and a local reservation tracker. Software ADA/native-asset sends, composed
transactions and hardware builders use this view. Address and payment-credential
lookups filter the same outpoints. Full listings continue past fully excluded
pages; explicit pages are formed after filtering, preserving server order.
Lookup failures abort selection rather than returning partial funds.

The Yano transaction processor parses regular inputs from the submitted CBOR and
reserves them before sending the HTTP request. This covers software, hardware
and dApp submissions through that processor. A stale draft — a different
transaction using a reserved input — is rejected locally before another request
is made. Reference inputs are not
reserved. Pending outputs returned by the node are eligible, including change for subsequent
sends. The wallet does not construct or resubmit parent transactions itself.

The wallet connection persists reservations in the network's `pending-inputs.json`
file, independently of the history display records. This survives wallet restarts
and reconnects, and covers hardware submissions whose history records omit CBOR.
Storage errors block submission/selection. This is local tracking for one wallet
application; it is not a reservation service shared by other wallet apps or
concurrently running processes. Diagnostic backends without a configured storage
path track only for their process lifetime. Existing submissions made before this
tracking was installed cannot be reconstructed from this file.

## Refresh and failures

Before selection/submission the tracker refreshes against the node:

- A definite HTTP 4xx rejection releases that submission's reservations immediately.
- Confirmation removes its reservations once the node UTxO index is caught up.
- Passing the transaction's upper validity slot also releases reservations once
the index is caught up. There is no arbitrary wall-clock unlock.
- Transport errors and HTTP 5xx responses have uncertain outcomes, so reservations
remain until confirmation or expiry. Lookup failures never unlock inputs. A wallet
send keeps its signed draft for a retry in both cases; only a 4xx marks it failed.
- Resending the identical signed transaction is not a conflict — only a different
transaction over the same inputs is. That resend is how an uncertain outcome is
settled, and it cannot pay twice. Yano answers a transaction already in its
mempool with 200. One already in a block is refused for spending its own inputs,
so before any 4xx releases inputs the wallet asks `GET /txs/{hash}`; if the
transaction is there, the send is reported as submitted. If that lookup fails,
the outcome stays unknown: the reservation is kept and the send can be retried.

The history UI's existing five-minute “failed” timeout is only an advisory guess,
not a definite node rejection, and does not release input reservations. The current
transaction lookup cannot distinguish a dropped mempool transaction from one
still pending. A transaction without an upper validity bound cannot be safely
expired by this mechanism; it needs confirmation or a definitive rejection.

A submission conflict still discards the stale draft. The Send screen offers
**Rebuild & review**, which refreshes selection and requests a new approval; it
never automatically resubmits the signed transaction. Spending from another
wallet can still cause a node conflict because its submissions are unknown here.
When Yano names the claiming transaction and input in its conflict response, the
wallet persists that exact input under its actual owner. Rebuild excludes it,
while releasing the rejected draft's other inputs. The existing confirmation
refresh clears it once the owner is indexed. An unknown owner's TTL cannot be
inferred from the rejected draft: if the owner is dropped rather than confirmed,
the current API cannot prove that it is safe to unlock this learned reservation.

## Unchanged views

`utxoSupplier()` remains the confirmed view for balances and existing CIP-30
queries. Confirmed history is unchanged. Yaci DevKit uses its existing supplier
and processor, with no local exclusion or Yano-specific query parameters.

## Tests

`PendingInputsTest` and `YanoMempoolViewTest` cover actual ADA and token draft
construction using pending change, exclusion
across server pages, explicit pagination, payment credentials and ordering,
restart persistence, rejection and expiry cleanup, uncertain outcomes, lagging
indexes, lookup/storage failures, stale-draft rejection and non-Yano isolation.
Tests use a stub node and public fixture keys; they do not submit user funds.

Listing and individual-output errors remain explicit. `block: null` is accepted
for pending outputs; it does not alter the confirmed balance/history view.
59 changes: 59 additions & 0 deletions docs/wallet-address-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Shelley wallet address discovery

Balance discovery scans receive (`1852'/1815'/account'/0/index`) and change
(`1852'/1815'/account'/1/index`) addresses independently. Each chain stops after
20 consecutive addresses with no transaction history by default. Spent addresses
reset the gap, just like funded addresses. Watch-only accounts derive both chains
from the stored account public key. Account discovery probes both chains too.

The previous scan stopped at receive index 99 and never queried change addresses.
It could therefore omit funds in restored wallets, including older Yoroi wallets.

The scan now continues to the unused gap, with a safety ceiling of 10,000 addresses
per chain. Reaching that ceiling without a gap is an **incomplete scan**, not a
successful balance. For unusually large accounts, increase the JVM property
`yano.wallet.scan.max-addresses-per-chain` (for example through
`JAVA_TOOL_OPTIONS=-Dyano.wallet.scan.max-addresses-per-chain=20000`) and retry.
This restarts discovery with the larger bound; it does not persist a scan cursor.

## Nodes without address history

The pinned Yano `0.1.0-pre13` distribution does not serve address transaction
history. Its `404` is different from a definite empty history result. A missing
Yano route, or a `503` explicitly identifying a disabled address-history index,
uses bounded UTxO-only recovery instead of failing the balance screen.

Recovery scans indices **0–199 on both chains** by default, without stopping at
empty-address gaps. The dashboard calls this **Known balance — scan incomplete**
and displays the exact scanned ranges. More funds may exist outside those ranges;
without history, no gap-based algorithm can establish completeness. A range can
extend further if history becomes unavailable after discovery has already advanced.
Each refresh retries history, so restoring the endpoint restores normal gap scans.

For a deeper recovery scan, launch with:

```bash
JAVA_TOOL_OPTIONS=-Dyano.wallet.scan.historyless-addresses-per-chain=1000 ./gradlew :wallet-app:run
```

The general 10,000-address safety ceiling still applies. Wider recovery windows
make more node requests and take longer; dashboard refreshes do not overlap.
Transport errors, generic server errors, malformed responses, and failed UTxO pages
still fail the refresh. Blockfrost-style stores' address-not-found `404` is treated
as unused, per their endpoint convention. Account-profile discovery still requires
history; UTxO-only recovery applies to addresses within an already opened account.

## Payments and scope

Software ADA/native-asset payments use the same discovery result to enumerate
funded addresses. Signing preserves the full derivation path, including the change
role, and returns change to the existing primary receive address. Transaction
previews include the discovered payment credentials in wallet ownership. An
incomplete recovery scan makes the preview unchecked, since a partial ownership
set must not be used to assert a complete value difference.

This change covers Shelley **base** addresses. Byron recovery is intentionally
out of scope. Enterprise-address discovery, hardware payments, the existing primary-address-only
staking/governance/minting paths, and CIP-30 address selection/signing are not
expanded by this change. The standard unused-address gap still applies; a wallet that used
addresses beyond a gap needs a larger gap setting through the scan API.
Loading
Loading