design: initial shielded outputs support#111
Open
r4mmer wants to merge 5 commits into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design: wallet-service shielded outputs support
Initial design proposal for indexing, registering, and serving shielded outputs in the
wallet-service. The work is split into three sibling RFCs:0000-daemon-and-database.md0001-wallet-registration.md0002-shielded-outputs-wallet-api.mdMotivation
Hathor is introducing shielded outputs, a new output type that hides amount (and optionally token) using Pedersen commitments, Bulletproofs, and surjection proofs. The
wallet-serviceis the canonical indexer for hosted Hathor wallets and must index shielded outputs the same way it indexes transparent outputs.The challenge is that shielded outputs are physically very different from transparent outputs: rows are an order of magnitude larger (~770–930 bytes of cryptographic material), ownership is determined by a different mechanism (script-extracted
spend_partial_addressrather than a fullnode-pre-decoded address), and recovering the value requires CPU-bound cryptographic work. Bolting these into the existing transparent code path would balloon row sizes, branch every hot-path query on a new dimension, and couple the well-tested transparent flow to an experimental new flow.Acceptance criteria
The design aims to satisfy the following goals:
/v2, noAccept-Versionheader. Old clients reading the same endpoints see a strict superset of today's response shape; existing fields keep their meaning.Design overview
The decisions below are the ones most likely to attract pushback. Each is argued in the corresponding RFC's Rationale and alternatives section; flagged here so reviewers can jump straight to the tradeoff.
Separate tables, not a
kindflag ontx_outputSix new tables (
shielded_tx_output,shielded_address,shielded_address_balance,shielded_wallet_balance,shielded_address_tx_history,shielded_wallet_tx_history) hold all shielded state. Extendingtx_outputwith nullable shielded columns and akinddiscriminator was rejected, 99% of historical rows would carry columns they never use, every existing query would need aWHERE kindclause, and a bug in shielded ingestion could corrupt rows the transparent code path also reads. The only shared concept across the two paths is the vertex, not the storage layer.Synchronous in-line rewind on the daemon
When a shielded output's
spend_partial_addressmatches an owned row, the daemon synchronously callsrewindAmountShieldedOutput/rewindFullShieldedOutput(~1 ms per match) inside the same per-vertex DB transaction. Async post-processing was rejected because it would open an inconsistency window between transparent and shielded views and complicate reorg ordering. The match step itself is O(1) on an indexed key, so unowned outputs cost essentially nothing.Per-index pre-derived scan privkeys
shielded_address.scan_privkeystores each child privkey ahead of time; the API never runs HD derivation on the read path. The wallet-service has two execution environments (daemon, Lambda API) and Lambda CPU per request is cost-sensitive, so HD derivation is moved to the gap-extension boundary which is rare.Wallet-load endpoint reused for shielded upgrade
There is no separate upgrade endpoint. The existing
wallet.loadhandler (POST wallet/init) is extended with four optional fields (scanXpriv,spendXpub, plus signatures over both) and reconciles its stored state against the request. A reconciliation matrix in the RFC enumerates all six (wallet exists?) × (keys stored?) × (keys submitted?) cases, including a409 Conflictfor the case where submitted keys differ from those already stored.Storage split, but the API merges by default
Storage stays per-kind (separate
tx_output/shielded_tx_output, separate balance/history tables), butGET wallet/balancesreturns one merged number per token in the existingbalance.unlockedfield, andGET wallet/utxosreturns merged results unless filtered with a new optionalkind=transparent|shieldedquery parameter. A wallet UI almost always wants one number per token; doing the merge once in the wallet-service is cheaper than multiplying it across the client ecosystem. If a future feature needs the per-kind split exposed, the API can grow a query parameter additively.Backward-compatibility analysis
The RFC includes a per-endpoint table covering the four combinations of
{old client, new client} × {wallet has shielded keys, wallet doesn't}. The conclusion: no fund loss in any scenario. The only observable old-client effects are in the rare downgrade scenario (user upgraded, then opened an old client), where they see a cosmetic balance discrepancy or stumble on a shielded UTXO at signing time — both recoverable.Out of scope
References
feat/shielded-outputs)HathorNetwork/hathor-wallet-service