Skip to content

Latest commit

 

History

History
71 lines (61 loc) · 21 KB

BACKLOG.md

File metadata and controls

71 lines (61 loc) · 21 KB

Yano Wallet — Backlog (Epics)

Known gaps and planned work, kept out of the ADRs so status is scannable. Everything core is device/on-chain-verified as of 2026-07-17 (see ADR-033/034/035 trackers); these epics are the delta to "no known gaps + publicly releasable".

# Epic Status Notes
E1 Hardware reward withdrawal — device test code + tests done Needs rewards to exist; plan: devnet run (HardwareStakeService.buildWithdrawal).
E2 DRep unregister — click-through built Deposit-reclaim flow (draftDRepDeregistration); never exercised on-device. Cheap win.
E3 Hardware signData (CIP-8) not built Ledger app has a message-signing instruction; dApps needing signData on hardware currently get a clear error.
E4 dApp txs with certs/withdrawals on hardware half done — Init signs, Reg fails at an unknown stage LedgerTxTranslator now translates certificates (legacy 0/1, delegation 2, Conway 7/8 with deposit) and withdrawals over ANY credential, not just key paths — see the E4 commit. Verified on device: a CIP-113 setup transaction, carrying two Conway registrations over SCRIPT credentials, signs on a Ledger. Not working: the second transaction of that flow is rejected at the mint stage (0x0b) with 0x6E07 ERR_INVALID_DATA, on a TOKEN sub-message (payload 4+nameLen+8). The first transaction, which also mints, signs fine. Our mint serialization matches ledgerjs byte for byte — serializeMintBasicParams = uint32(groupCount), serializeAssetGroup = policyId
E5 CIP-95 governance for dApps not built getPubDRepKey/getRegisteredPubStakeKeys etc.; advertise via getExtensions. Enables govtool-style dApps.
E6 M5 distribution + transport hardening designed (ADR-035) Chrome/Firefox/Edge listings (pinned key → stable id), privacy policy, Native Messaging host + manifest (installer-registered), retire the localhost WS for released builds. Pairs with code-signing.
E7 Release engineering — first real run workflow written Never run: tag v<version> to let release.yml build all four platforms, verify Win/Linux packages (only macOS built locally), run the portable BYO-Java zip on a clean Java-25 machine. build.yml (compile + test, no packaging) runs on every push to main and PR.
E8 Code-signing + notarization researched macOS Developer ID ($99/yr) + notarytool; Windows Azure Trusted Signing (~$10/mo); wire into CI gated on secrets. Until then: unsigned-install docs.
E9 Multi-account support (CIP-1852) built — ADR-037, needs manual verification MA-M1/M2/M3 done: grouped list, sidebar switcher, software + hardware add-account, restore discovery. Deferred polish: per-row balances, rename account, auto-scan at end of restore.
E10 HW-M5: more devices not built Trezor (USB) and/or Keystone (QR, airgapped) behind the same HardwareWalletService SPI.
E11 HW-M6: Speculos CI not built Ledger emulator in CI so the device protocol gets automated regression tests.
E12 YubiKey/FIDO2 vault factor designed (ADR-036) Envelope v3, HMAC challenge-response mixed into the KDF; Y-M1 YubiKey OTP, Y-M2 FIDO2 hmac-secret.
E13 CIP-45 (cross-device connect) deferred by decision Revisit if dApp adoption grows or a mobile/companion story appears (ADR-035 records the rationale).
E14 M3 leftovers minor Multi-tab behavior, bridge lifecycle polish, per-account CIP-30 nuances (after E9).
E15 Backend flavors — yaci-store / Yaci DevKit planned — ADR-038 Money path already works (wallet is Blockfrost-shaped; CCL's BF services never call /genesis). Needs: flavor probe (/status 404 ⇒ yaci-store), pill via /blocks/latest, DRep via /governance-state/dreps/{id}, devkit preset. Blocker: yaci-store exposes no protocol magic → fingerprint workaround + refuse-mainnet rule.
E16 yaci-store: expose network magic filed — yaci-store#1018 Asks for /api/v1/genesis (or network_magic on /network), plus an indexer-lag indicator as a secondary. Filed generically — it does not mention this wallet (stealth). Once shipped: restores the plain magic check for both flavors, drops ADR-038 §2b entirely, and removes the hardcoded "devnet == magic 42" assumption.
E17 Managed-node resilience: guard the chainstate, and tell the user how to recover not built Earned by a real corruption on 2026-08-13 (see below). Four parts, prevention first: (a) data-dir lock — a FileLock on <dataDir>/.lock held for the wallet's lifetime, so a second wallet says "Another Yano Wallet is using ~/.yano-wallet; close it, or use --data-dir=…" instead of opening the same RocksDB. (b) node-version guard — stamp the node version into the chainstate on creation and refuse to open it with an older node. (c) failure classification + recovery offer — ManagedNode.failureReason() hands back a raw string today; classify the node's exit output (Cannot repair epoch nonce state, startup IllegalStateException, SIGSEGV in librocksdbjni, port in use, disk full) and offer the matching action. For an unusable chainstate that is "Delete local chain data and resync", stating the cost (preprod ≈ 33 GB, ~40 min at the measured ~3,000 blocks/s) and the reassurance that makes it a safe click: chainstate is derived data — keys live in <dataDir>/<network>/wallets/vault.json and cannot be lost by resyncing. Rename to chainstate.corrupt-<timestamp> rather than delete, so the evidence survives. (d) node.log append/rotate — ManagedNode.spawn() uses redirectOutput(File), which truncates on every start; on 2026-08-13 a second node wiped the very log being used to diagnose a sync problem.

| E18 | Upstream relay failover + per-network relay settings | designing | The wallet launches the node with a single upstream (yano.remote.*), so a slow or dead relay stalls the sync with nowhere to go — see the incident notes. Yano already recovers on NO_PROGRESS / BODY_FETCH_STUCK, it just needs alternatives. Plan: yano.upstream.mode=trusted-failover with bulk-source: single-trusted (failover, not parallel download), 2+ default relays per network in WalletNetwork, passed as -Dyano.upstream.peers[i].{host,port,priority} from ManagedNode.spawn(). Custom relays take priority 0..n with defaults appended as fallback, plus a "use only my relays" opt-out; per-network override persisted like ConnectorSettingsStore, removable to revert to defaults; protocol-magic never user-editable (a wrong-network relay must fail at handshake, never sync a foreign chain into the chainstate); takes effect on restart. Reachable from the Connect screen and Advanced Settings — a dead relay must be fixable before a failed start, and node readiness is GET /status == 200 (not sync progress) so the user can always get in. Needs a sync-status indicator that distinguishes at tip / catching up / not syncing — today they look identical, which is also what made a healthy at-tip preprod look "very slow". Deferred, off by default, advanced-only: peer discovery — Yano already defaults discovery.peer-snapshot-urls to https://book.play.dev.cardano.org/environments/<network>/peer-snapshot.json (verified live 2026-08-13: mainnet 153 KB/470 pools, preprod 13 KB/52, preview 20 KB/80, each carrying NetworkMagic), so it is a flag flip rather than a hosting job. It is also the only answer to hardcoded relays rotting, since those files are on-chain-registered relays and work from a cold start. Wants testing first, and it changes a desktop wallet's network footprint. |

| E19 | Chained-submit retry — defensive, never observed firing | shipped but unproven — yano-wallet#2, yano#66 | TransactionValidationService.validate(byte[]) resolves through resolveFromUtxoState (utxoState.getUtxo(outpoint)) — the persisted set only, no mempool overlay — so on a source reading, a transaction spending an output of another transaction still in the mempool cannot resolve. WalletCip30Wallet.submitTx retries in that case, but only when the node said UtxoNotFound and the transaction spends an output this wallet submitted moments ago, polling up to CHAIN_WAIT (45s). Read the history before touching it: this was built while chasing the 2026-08-13 CIP-113 failure and attributed to it. That failure was actually InvalidSignaturesInWitnesses from the wallet re-encoding datums, fixed separately. Once that was fixed the same chained pair submitted and confirmed with the wait never firing — because the dApp waits for the parent to confirm before submitting the child. So the gap is real as written but has never been seen in the field, and a dApp that submits back to back without waiting is the only thing that would prove it. Decide deliberately whether to keep ~60 lines of guarded complexity in the submit path for an unobserved case, rather than carrying it by default. | | E20 | Warm-up: use the wallet while the node is still starting | built 2026-08-13 | A managed node binds no HTTP port until its startup sequence finishes, and on a chain that is already synced that sequence includes rebuilding the account-history index: measured 881 blocks/s over preview's 4,567,221 blocks ≈ 86 minutes with /status refusing every connection throughout (~/.yano-wallet/preview/node/node.log from that run has 453 reconcile lines and no Listening on). Two bugs fell out of that. (a) The start could never succeed: REAL_NETWORK_START_TIMEOUT was a 45-minute ceiling on the whole start, so it killed a working node roughly halfway through and discarded the rebuild. It is now a silence window — any growth of the node's log restarts it (ManagedNode#startAndAwaitReady), so only a node that has actually stopped doing anything times out. (b) The wait was illegible: nodeStartupStatus() returned one fixed sentence and always passed blockNumber = 0, so an 86-minute rebuild and a hang looked identical. NodeStartupProgress now parses the node's own progress line out of the log tail — the only channel available before HTTP exists — and the UI shows phase, position, percentage and a rate-based ETA. The connect is now two-phase: beginManagedConnect spawns the node and publishes a connection whose repository half works immediately (WalletService.localOnly, chain calls throw NodeNotReadyException rather than returning empty), and completeManagedConnect waits for the API on its own thread — never on the single backend executor, which the old blocking connect occupied for the whole start while every screen queued behind it. So the wallet lands on Onboarding in seconds and the user creates or restores a wallet during the rebuild. Unlock stays gated until the node answers, deliberately: WalletService.Session belongs to the service that created it, so a session opened during warm-up would keep the dead suppliers for its whole life. Follow-ups: the node serving /status with a warming-up phase before the rebuild would remove the log parsing entirely (cleanest fix, needs a Yano release); the screenshot harness's --node=… pre-seed still uses the blocking connect(); and Onboarding's "Switch network" leaves a warming node running rather than offering to stop it. | | E21 | Chrome Web Store submission — the extension id is the whole risk | zip built in CI 2026-08-14; submission not started | ./gradlew connectorZip packages wallet-connector/ (manifest at archive root, dev/ and README excluded) and both workflows publish it: build.yml per PR as an artifact, release.yml as its own OS-independent job attached to the release. Verified locally that the zip's manifest.json key derives exactly bjnkcmbkjaebecgllkgbeapbjcknnedn — the id NativeMessagingInstaller.EXTENSION_ID allowlists — so no signing key is needed to produce a working build and none belongs in CI; extension-key.pem signs only a .crx, which Chrome will not install from outside the store. No build has ever read that pem — it was used once, offline, to generate the pair, and the public half is committed in manifest.json, so the dev identity survives in git without it. Losing it costs only the ability to sign a .crx under this id, which nothing reachable needs. Submission will change the id, and that is not avoidable: the Web Store signs with its own key pair and derives the published id from it, so bjnk… is a development identity only. Verified 2026-08-14 — the supported order is publish first, then copy the public key from the Developer Dashboard into manifest.json key so local unpacked loads converge on the store id; uploads carrying key are sometimes rejected with "key field is not allowed in manifest" (reports of when it fires are inconsistent, and it is ignored when it does not), so strip it for the upload. The consequence to plan for is the native messaging allowlist: NativeMessagingInstaller.EXTENSION_ID is a single constant baked into allowed_origins, so on the day the store id lands, every store user loses the transport silently — with --enable-ws-connector off by default there is no fallback and no UI explanation, dApps just cannot connect. allowed_origins takes a list; allowlist old and new together so sideloaded testers keep working. That change is known and cheap but cannot be written until a store id exists. It also has an ordering constraint, because the host manifest is written by the wallet and only on the Settings button — install() is reachable solely from installNativeMessagingHost(), and nothing rewrites it at launch. So a store install running against an older wallet names only the old id and the host never starts. Ship the wallet release carrying both ids before the listing goes public, and make "re-run Install browser connector" a release note. Better: have the wallet rewrite the host manifest at startup whenever the contents differ (idempotent, a few lines) — that removes the manual step for this transition and every later one, and is worth landing before submission. Once the store key is adopted into manifest.json, the store id becomes the single id for local unpacked loads, CI zips and store installs alike. Also still open from ADR-035 M5: store listing copy, privacy policy, and the Windows registry path. | | E22 | History needs a node transaction index | local fallback built 2026-08-19 (ADR-043); node side still open | No published Yano release serves /accounts/{stake}/transactions, /accounts/{stake}/rewards or /txs/{hash}/status — verified against the pre12 and pre13 sources, so this is a long-standing gap, not a pre13 regression. The wallet now falls back to its own record of what it submitted (the pending store, read as a log) and labels the screen Local history only; getTxStatus falls back to /txs/{hash}, which is what stops every locally-recorded transaction decaying to failed five minutes after it was submitted. What the fallback cannot do: show funds received from elsewhere, show anything sent before it shipped or from another wallet on the same seed, or show rewards — Staking still reports those unavailable, because there is no local equivalent to fall back to. The real fix is upstream (Yano PR #58); when a release serves the index the fallback stops firing on its own, with no flag to unset. | | E23 | Back-to-back sends: coin selection cannot see in-flight transactions | deferred 2026-08-19 — wait for confirmation between sends | The node chains fine: Yano c241db80 (feat: support chained transactions in mempool, in pre13) makes mempool admission resolve inputs created by other mempool transactions, closing the E19 gap on the node side. The wallet is now the weak link. Its UtxoSupplier is a stock CCL DefaultUtxoSupplier over /addresses/{address}/utxos, and that route is persisted-only — DefaultUtxoStore.getUtxosByAddress reads RocksDB cfAddr and has no mempool reference at all (the overlay commit touches DefaultMemPool, TransactionValidationService, BlockBuildUtxoOverlay and UtxoEventHandler, not UtxoResource/UtxoState). So immediately after submitting tx A: A's input still reads unspent and may be re-selected for tx B (conflicting spend), and A's change output is invisible and cannot be spent. The wallet has no in-flight UTxO exclusion — verified, the only inFlight references are history-row rendering. Whether two quick sends work is therefore luck of the UTxO set: several independent UTxOs and selection dodges it; one consolidated UTxO and tx B reuses the same input. Note the asymmetry — the dApp path retries (WalletCip30Wallet.submitTx waits up to CHAIN_WAIT 45s for the parent to land) but the wallet UI send path does not: WalletService.Session#submit marks FAILED on the first rejection. Fix if revisited (wallet-side, small): in YanoNodeBackend.utxoSupplier(), wrap the supplier to subtract inputs consumed by in-flight PendingTransactions and optionally add their change outputs — the data is already in pending-transactions.json, and each record carries signedCborHex to decode them from. Deliberately NOT built: Satya's call was to let the first transaction confirm before sending the next, rather than carry speculative UTxO bookkeeping. |

Ordering is not priority; pick by need. Cheap verification wins: E1, E2, E7.

Incident notes

2026-08-13 — mainnet chainstate corrupted by a concurrent, downgraded node. The release wallet (node 0.1.0-pre12) was syncing mainnet when a dev wallet (./gradlew :wallet-app:run) was started against the same default data dir. Two compounding faults: two nodes on one RocksDB, and the dev build silently using 0.1.0-pre8 — resolveYanoDist ranks a project-local .yano-node/ above the pinned release, and that link pointed at a jar built 2026-07-15, older than even the worktree source beside it. Result: Cannot repair epoch nonce state: missing local block body 0 while replaying to 905, then a SIGSEGV in librocksdbjni. The dev node also truncated the release node's node.log (same data dir → same log path), destroying the sync measurement in progress. Mitigated by disabling the stale link (.yano-node/yano.jar → yano.jar.pre8-stale-disabled), so dev and release now run the same node. E17 is the durable fix. Related: the build only logs the resolved node when resolution fails — a line naming the resolved jar and version at startup would have made the downgrade obvious the first time.

Mainnet sync is peer-bound, not resource-bound. Measured the same night on a 128 GB machine, all runs on node 0.1.0-pre12 from an empty chainstate:

run time to first block rate
mainnet 23:59 110 s ~10 blocks/s — bursts of 100 blocks in 6 ms, then 0.0 % CPU for ~48 s
preprod 23:55 0.6 s ~3,000 blocks/s sustained
mainnet 00:12 6 s ~5,165 blocks/s sustained, no stalls, 113–147 % CPU

The machine load was identical across all three (~535 % CPU of unrelated devnet-cluster nodes, load average 8), so the 500× swing between the two mainnet runs is not CPU, heap or packaging — no -Xmx is set anywhere, and at 128 GB the JVM default is ~32 GB. The node does 5,000+ Byron blocks/s when fed; when it is not, it idles at 0 % CPU waiting on backbone.cardano.iog.io:3001. What changed upstream between 23:59 and 00:12 is unknown — transient congestion, new client throttling, or a different host behind that DNS name. Hence E18: the wallet ran with exactly one upstream and no alternative to fail over to.