DIG Browser is an ungoogled-chromium fork that is a pure RPC consumer of DIG Network content. It
resolves chia:// / dig:// addresses by reading ciphertext + inclusion proofs from an external
DIG node over RPC, then verifies and decrypts client-side, trustlessly. It is NOT a node: it runs
no P2P networking, serves no content to peers, and manages no on-disk content cache. This document is
the authoritative contract an independent reimplementation (or a reviewer) can hold the fork to; it
agrees with the superproject SYSTEM.md ("Roles — serving vs consuming") and the docs.dig.net
protocol pages, and cross-references CLAUDE.md §5.3 (client→node connection order) and §5.1 (store
format).
Grandfathered build/release (§3.6a). The fork keeps its own patch-based build (ungoogled- chromium +
patches/+ the DIG assets indig/) and its ownmasterrelease flow; it is exempt from the ecosystem's standard CI pipeline. DIG-specific policy that can be unit-tested without a Chromium build lives as ES modules underdig/(each with a.test.mjs); the native C++ that wires it into Chromium lives inpatches/ungoogled-chromium/**, and every native mirror carries a pointer back to thedig/module that is the single source of truth for the policy.
DIG Browser MUST:
- resolve
chia://anddig://requests through an EXTERNAL DIG node's read RPC (§3), following the §5.3 node-resolution ladder (§4); - verify every retrieved resource's Merkle inclusion proof against the on-chain-anchored root and
decrypt it client-side, fail-closed, calling the digstore Rust read-crypto DIRECTLY via FFI
(
dig_runtime'sdig_read_verify_decrypt) — the SAME Rust the webpagedig-client-wasmwraps (§5); - inject a
window.chiawallet provider (CHIP-0002) backed by the in-browser wallet (§6); - surface per-resource proof provenance (Shields, §7) and a page-posture control surface
(
dig://control, §8); - expose a discoverable, persisted custom node endpoint setting (§4.3).
DIG Browser MUST NOT:
- run an in-process/embedded DIG node, any P2P networking, provider records, or content serving;
- keep a user-managed on-disk content cache or expose cache-configuration chrome (no
chrome://settings/digcache section, no cache cap/usage/clear UI, noDigCacheHandlerMojo); - carry a SECOND read-crypto implementation — the browser reuses the ONE
digstoreRust read-crypto (linked natively viadig_runtimeFFI). It runs no bespoke native C++ crypto and no wasm; wasm is the webpage binding of that same Rust (hub/extension/SDK), never the browser's.
The trust model is unconditional: the source is never trusted. Whichever node serves the bytes, the browser independently verifies + decrypts them; a proof failure or decrypt failure yields an error page, never partial/unverified content.
chia://<store>/<path>anddig://<store>/<path>resolve DIG store content. The store component is the on-chain store id (or a name that resolves to one); the path selects a resource within the store's current (anchored) root.- Root pinning: the current root for a store is the on-chain-anchored root; the browser obtains it via
the node's
dig.getAnchoredRootand pins reads to it, so served content that does not match the anchored root is rejected. dig://also addresses the browser's own built-in surfaces (dig://control,dig://shields, new tab) served locally fromdig/; those are UI, not network content, and bypass §3–§5.
The browser is a JSON-RPC client of a DIG node's read interface. Against the chosen node base URL (§4) it uses:
GET /health— a cheap, side-effect-free liveness probe. A healthy DIG node answers{ "status": "ok", "mode": "local-node" }; any other/malformed body means "not a usable DIG node".POST /— the JSON-RPC read methods:dig.getContent(blind ciphertext + inclusion proof for a resource),dig.getAnchoredRoot(the on-chain root for root-pinning),dig.getProof, and related read methods. The browser sends only blind retrieval keys / addresses; it never sends URNs or keys.
The browser performs NO write/spend RPC on the read path. Wallet-signed actions go through the
window.chia provider (§6), not the content loader.
For each chia:///dig:// request the browser selects a node endpoint in this fixed order, using the
first that responds healthy (dig/node/dig_source_resolution.mjs is the single source of truth; the
native chrome/browser/dig/dig_url_loader_factory.cc mirrors it in C++):
- Explicit custom endpoint — the user-configured node/RPC URL (§4.3), when set. Overrides the ladder entirely.
http://dig.local— the installed local DIG node (the dig-installer mapsdig.localto the node's privileged loopback listener). Preferred: local, offline-capable, contributes to the network.http://localhost:<port>— the local DIG node's always-on loopback listener (default port8080) whendig.localis not resolvable.https://rpc.dig.net— the public gateway. FINAL fallback only, when no local node is reachable.
Resolution rules:
- The browser never hard-codes
rpc.dig.netas the primary endpoint; it is the safety net. - Reachability is cached per base URL with a short TTL (
PROBE_TTL_MS, 5 s) so a page's many subresources do not each re-probe (ReachabilityMemo); a/healthprobe gates promotion of a candidate to "use". - The ladder ALWAYS terminates at a reachable source (
rpc.dig.netif nothing local answers), so a standalone browser with no local node still resolves every request. - A node-class transport (mTLS) is NOT required of the browser: it is a browser/plain-HTTPS read
client (it cannot present a DIG-identity client cert), so it uses the public read tier of
rpc.dig.netand the local node's loopback listener. (Contrast §5.3's node-class-client mTLS rule, which governs the CLI/SDK, not the browser.)
The browser ships a first-class, discoverable, persisted setting to override the node endpoint (a custom dig-node base URL / RPC URL). When set it takes precedence over the auto-ladder (§4 step 1). Absent an override the 2→3→4 order is mandatory.
After bytes are retrieved from the chosen node, the browser ALWAYS, in-process:
- verifies the resource's Merkle inclusion proof against the pinned on-chain-anchored root;
- derives the resource key from the URN and decrypts the ciphertext;
- reassembles chunks and serves the plaintext to the renderer only if every step succeeds — otherwise it fails closed with an error page.
This is implemented by the digstore Rust read-crypto, called DIRECTLY via FFI. The browser is a
native application, so it links the digstore read-crypto crate (digstore-core, exported over C-ABI by
the dig_runtime cdylib as dig_read_verify_decrypt) and calls the Rust in-process — WASM is for
webpages only. There is exactly ONE trustless read-crypto implementation in the ecosystem — the
digstore Rust — with two bindings over it: the native FFI the browser uses here, and the
dig-client-wasm binding the hub (apps/web/lib/dig-client), the dig-chrome-extension, and
@dignetwork/dig-sdk (all webpages/JS) use. The browser MUST NOT carry a second copy of the crypto
(no bespoke native C++, no wasm). Client-side decryption is load-bearing (it powers Shields, §7) and is
never delegated to the node.
The C++ URL loader (dig_url_loader_factory.cc) owns only the NETWORK side — resolve the endpoint
(§4), fetch the ciphertext + base64 inclusion proof, resolve the chain-anchored root — then hands those
bytes to dig_read_verify_decrypt; it contains NO verify/decrypt logic of its own. A nonzero status
from the FFI (bad input / verification failure / decrypt failure) is rendered as a fail-closed error
page, never partial content.
The browser injects a window.chia provider (built from @dignetwork/chia-provider v0.2.0 — a
git-dependency pin on the v0.2.0 tag, since npm publish is blocked ecosystem-wide, tracked
separately — bundled by dig/provider/build-provider.mjs) exposing the CHIP-0002 method set
(parity with the hub's shared WALLET_METHODS): chip0002_{connect,chainId,getPublicKeys, getAssetCoins,getAssetBalance,signCoinSpends,signMessage},
chia_{getAddress,signMessageByAddress,takeOffer,createOffer,getNfts}. Requests are answered by
the in-browser wallet over a loopback bridge behind a per-origin consent gate; the wallet holds
the keys and signs — dapps and the content loader never see key material.
connect() resolves a boolean (true on approval) per the CHIP-0002/Goby contract, not the
raw wallet payload. Thrown errors carry a stable numeric code from the shared package's CHIP-0002
taxonomy — identical across the DIG Browser and the dig-chrome-extension:
| Code | Name | Meaning |
|---|---|---|
| 4000 | INVALID_PARAMS |
invalid method params |
| 4001 | UNAUTHORIZED |
origin/account not authorized — call connect() first |
| 4002 | USER_REJECTED |
the user rejected the request, or a connect is still pending approval (pending: true) |
| 4003 | SPENDABLE_BALANCE_EXCEEDED |
the requested spend exceeds the spendable balance |
| 4004 | METHOD_NOT_FOUND |
the wallet does not support/cannot find the method (or chain) |
| 4005 | NO_SECRET_KEY |
the wallet lacks a required secret key |
| 4029 | LIMIT_EXCEEDED |
rate-limited |
| 4900 | DISCONNECTED |
the wallet bridge is unreachable/disconnected |
A dApp/agent MUST branch on these numeric codes, not on message text. (Prior to the v0.2.0 bump the browser carried an ad-hoc 4001/4100/4200/4900 scheme that disagreed with the extension's CHIP-0002 numbers on 4001 — fixed; both providers now share one taxonomy.)
dig://shields presents a per-capsule inclusion-proof ledger: for each resource the current page
loaded over chia:///dig://, the ledger records the store/root it verified against and the
proof/decrypt outcome (dig/shields/dig_ledger.mjs is the pure model). Shields is a transparency
surface over the trustless read model (§5); it reads the loader's verification results and never
weakens them.
dig://control is the full-page Control Pane (opened from a dedicated toolbar button;
dig/control/dig_control.mjs is the posture policy). It surfaces the browser's DIG posture (active
source, wallet/connection state, links). The browser MAY link out to the external node's own control
surface (dig://control content served by the node) but is NOT the node's configuration UI — node
configuration lives with the node.
When a local standalone dig-node is present, dig://node (dig/node/dig_node_controller.mjs, DOM
copy dig/node/dig_node.html) drives its control.* admin RPCs (status, hosted-stores list/pin/
unpin, §21 sync status/trigger, config get/set-upstream) over the node's loopback control endpoint,
authorized by the node-issued local control token (X-Dig-Control-Token header /
_control_token param). The catalogued control-plane JSON-RPC error codes are:
| Code | Name | Meaning |
|---|---|---|
| -32030 | UNAUTHORIZED |
missing/blank/wrong control token |
| -32031 | NOT_SUPPORTED |
the operation is unavailable on this build (e.g. no §21 identity) |
| -32032 | CONTROL_ERROR |
the operation failed at runtime |
These numbers are canonical and CLEAR of -32020/-32021/-32022, which are RESERVED for the
onion-routing (private-retrieval) failure codes — the two ranges must never collide, or the browser
could misclassify a real onion failure as a control-plane one (or vice versa). Byte-identical to the
dig-node contract (dig-node-core/src/lib.rs, dig-node-service/src/meta.rs).
- Single read-crypto: the browser's verify+decrypt MUST be the digstore Rust read-crypto called via
the
dig_runtimeFFI (dig_read_verify_decrypt) — the SAMEdigstore-coreRust the hub, extension, and SDK consume through thedig-client-wasmbinding. A bespoke native C++ crypto copy, or loading the wasm into the browser, is a conformance failure (wasm is the webpage binding, not the browser's). - Ladder parity:
dig/node/dig_source_resolution.mjsand its native C++ mirror MUST implement the identical ordering, host/port, probe path, and TTL; any change is made in both, in one unit of work. - No node surface: the shipped browser exposes no node/P2P/cache configuration chrome (§1).
- Cross-doc agreement: this spec MUST agree with
SYSTEM.md(Roles — serving vs consuming) and the docs.dig.net protocol pages; a change to the resolution ladder, the read model, or the wallet method set updates all three in the same unit of work.
The fork is completing its migration to the pure-RPC-consumer contract above (super-repo #44, per the #41 separation-of-concerns ruling). Status of the four code deltas from the previous "browser runs an in-process node" architecture:
- Remove the in-process node — LANDED on the consumer read path:
dig/node/dig_source_resolution.mjsand the nativedig_url_loader_factory.ccno longer have anin-processterminal; the §4 ladder now terminates at the public gatewayrpc.dig.net, and the loader never calls the runtime's node RPC (dig_rpc). Thedig-runtimeDLL is loaded at PostBrowserStart for the in-browser WALLET (§6) and the read-crypto FFI (§5) only. The companiondig-nodechange that makes this a TRUE zero-node browser has landed:dig_runtime_start_wallet()starts the wallet WITHOUT the node engine (nodig_rpc/P2P/cache) — the browser calls it at startup (dig-node #47).rpc.dig.net+ the installer's standalone dig-node (#40) cover content. Ships once verified on a buildable release (#26). - Single-source the read-crypto — LANDED in code (build-gated verify). The former native C++
net::digverify/decrypt (net/url_request/dig_crypto.*,dig_urn.*) is DELETED; the loader now calls the digstore Rust read-crypto directly via thedig_runtimeFFIdig_read_verify_decrypt(§5) — the SAME Rust the webpagedig-client-wasmwraps, so there is exactly ONE trustless read-crypto implementation. WASM is for webpages only; the native browser uses the Rust directly. Client-side decrypt STAYS (it powers Shields). Needs a Chromium build (#26) to wire thedig_runtimelink + regenerated patches and verify end-to-end. - Remove the
chrome://settings/digcache section + theDigCacheHandlerMojo + the "My Node" cache card (§1) — DONE. - Add the custom-node setting (§4.3) — DONE (the
chrome://settings/digsection now hosts the persisted custom node endpoint pref,dig.custom_node_url).
This section is removed once the read-crypto single-sourcing (item 2) lands and the full node removal is build-verified.