Repository navigation
Conversation
ty-everett
left a comment
There was a problem hiding this comment.
Maintainer review: keep this draft and rebuild a focused diff on current main, preserving the already-merged overlay/security work. Resolve paid-result recovery before release. The documented BRC-178 differences in paths, fee defaults, content hashing and payment timing also need an explicit interoperability decision and shared vectors. Recheck the AuthFetch buffer assumption and peer floor against the current SDK, finish generated API documentation, and obtain complete exact-head CI and zero-new-finding analysis. Distinct public keys alone should not be described as independent operators.
| { queryId: request.queryId, host, payload, supplement }, | ||
| originator | ||
| ) | ||
| await store.completeSettle(request.queryId) |
There was a problem hiding this comment.
[P1] Preserve an idempotent recovery path for paid delivery. completeSettle releases payload/supplement and marks the query settled before this HTTP response is delivered. If the response is lost, a retry returns ERR_QUERY_SETTLED/409, while the client has already broadcast payment. Persist the paid outcome and retrievable signed delivery, bound retention explicitly, and test lost responses, duplicate collection and process restart without charging twice or losing the answer.
Design for a new @bsv/eqc package (EQC client, protocol core, host handlers) and an additive registerRouter hook in @bsv/overlay-express. Records the BRC-178 ambiguities resolved during design and how BRC-136 topic anchors serve as a consistency signal for raced overlay lookups. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the static host list with LookupResolver-style discovery from DEFAULT_SLAP_TRACKERS, with the ls_slap step answered free and covered by the target query's fee. Default floorFeeSats to 1000 and maxFeeSats to 2000. Rename the HTTP binding from /brc178/* to /economic/*. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Eighteen test-first tasks covering the protocol core, host handlers, the EQC client, end-to-end tests, the overlay-express registerRouter hook, and governance registration. Planning surfaced that AuthFetch pays any well-formed HTTP 402 with no cap, so the design now requires a transport whose wallet facade cannot create actions. The design also gains the maxHosts and now options and a source layout and test split that match the plan. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
decodeOutpointList's three unguarded varint reads let Utils.Reader propagate a plain Error (not TypeError) for a 0xff-prefixed 64-bit varint above 2^53, breaking the module's malformed-input contract. Add one guarded read helper used at all three call sites (count, outputIndex, contextLength) and cover each position with a test. Also add a test proving a rebuilt LookupAnswer (Atomic BEEF per output) canonicalizes back to the identical payload and content hash; canonicalizeLookupAnswer already handles Atomic BEEF correctly via Transaction.fromBEEF/Beef.mergeBeef, so no implementation change was needed there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
InMemoryPendingStore.put replaced a record that was already stored under the same queryId. The old record's bytes were never released, so repeating one queryId drove the counter past maxBytes and made every later put fail with too-many-pending; worse, an overwrite reset a settling record to pending, letting beginSettle succeed twice and breaking the atomic claim two concurrent identical queries rely on. put now answers 'duplicate' for a queryId it already holds that is not purge-eligible, leaving the record and the byte counter untouched. The query handler re-reads the record and shares the existing-record branch, so both racers get the one stored attestation. The byte budget also counted number[] elements while maxBytes was documented as 256 MiB. A V8 number[] element costs about 8 bytes, so the store under-estimated its own footprint roughly 8x on a free route. sizeOf now multiplies by that constant; the option name and default are unchanged and now mean estimated heap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Utils.toArray(envelope.transaction, 'base64') ran before the try that
guards Transaction.fromAtomicBEEF, so a non-base64 transaction field
threw a raw Error instead of returning { ok: false, reason: 'malformed' }
as the PaymentVerification union promises. Move the decode inside the
try block and add a test asserting a non-base64 transaction is rejected
as malformed without touching the wallet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…enges Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
referenceFor took Math.max over every winner's claimed blockHeight with no corroboration, so a single dishonest winner could set the topic's reference tip and TAC, marking honest hosts lagging and excusing a genuinely diverged minority host in classifyMinority. Per controller ruling R11: the reference height for a topic is now the highest blockHeight reported by at least two distinct winners. With no such corroborated height there is no reference, every winner that reported the topic is unknown, and blockHeight/tac are omitted. A winner above the reference is unknown, below it is lagging, at it is agreed only when every winner at that height shares one TAC (otherwise diverged). classifyMinority uses the same corroborated reference and no longer treats an uncorroborated or TAC-less reference as an excuse. Rewrote the one consistency.test.ts case that encoded the old uncorroborated semantics, and added tests for the A/B/C poisoning scenario, a single reporting winner, and classifyMinority not excusing a diverged host behind a fabricated height. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Controller ruling R12: a resolver failure must not be cached. discover() now signals a resolver rejection as undefined, distinct from a successful but empty answer, and hostsFor skips the cache write on failure while still returning that call's additionalHosts. Controller ruling R13: message-list targets now cache per recipient (message-list:<recipient>) so resolving message boxes for two identities never mixes their hosts, while hostOverrides/additionalHosts stay keyed under the shared 'message-list' market key as the design document states. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getParams and post compared decoded text.length against the byte caps after the whole response had already been buffered, so an untrusted host could force an unbounded allocation before the cap ever fired, and a multi-byte UTF-8 body could exceed the byte cap while passing the character-count check. getParams now rejects immediately on an oversized content-length header, and otherwise streams response.body through a reader, counting bytes and cancelling the reader plus aborting the request as soon as MAX_PARAMS_BYTES is crossed (falling back to arrayBuffer() only when no body stream is available). post measures arrayBuffer().byteLength against maxResponseBytes before decoding, with a comment noting that AuthFetch itself buffers the whole response with no AbortSignal, so this check cannot bound or cancel allocation below the SDK boundary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`EQC.params()` negative-cached every failure for the full `paramsTtlMs` and re-threw a fabricated error, so a two-second connectivity drop while candidates probed its hosts left `ERR_EQC_NO_HOSTS` standing for five minutes. Add `TransportStatusError`, carrying the HTTP status, and cache a refusal only for a 4xx answer, which is the host stating it runs no market. Timeouts, network errors, parse errors, and 5xx answers are re-thrown unchanged and uncached, and a cache hit re-throws the very error the transport raised. The ranking tests asserted exact arrival order across 8 ms gaps on real timers, which a loaded runner can reorder. Every gap an assertion depends on is now at least 25 ms, with the race windows and host timeouts scaled to match, and every assertion kept as strong as it was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Host half of the final review fix wave (rulings R17, R18, R20-R23). - requiredShare: the remainder-free Fibonacci share. Collect requires it at the quotedFeeSats the host signed and no longer re-evaluates floorFeeSats, so a fee above the floor can never be refused at rank 1. - A wallet that throws during internalizeAction is a 'wallet-error': logged with queryId, rank, client and txid, answered 500, settle claim released. Only accepted !== true is still a 402. - The advertised topK is enforced on the query route. - maxPayloadBytes defaults to 2 MiB and bounds the estimated encoded delivery. - The pending store budgets the stored query, and an expired unsettled record releases its bytes and its per-client slot at once. Collect holds the payload before settling so a mid-settle release cannot empty a paid delivery. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Client half of the final review fix wave (rulings R19, R20, R24-R27, R31). - nonPayingWallet is an allowlist: only the five calls BRC-103 authentication makes reach the wallet, listCertificates answers an empty list, and every other method rejects by name, so a host cannot harvest certificates. - DiscoveredHost carries identityKeys, every key advertised for a URL. A session key outside the list rejects the host for the query as a soft event; a signer that differs from the session key stays hard. - collect-failed is a hard reputation event; diverged-answer is soft. - Hosts of equal reputation are shuffled before the maxHosts cut. - paramsTimeoutMs (2000 ms) bounds each params probe, and a passing failure is negative-cached for min(paramsTtlMs, 15 s) and recorded as a soft event. - Hosts advertising a smaller topK are skipped, and a ranked host whose share is below its advertised minPayoutSats gets no output and no collect. - Tests pin the client-side supplement check and the per-host deadline on authenticated calls, in the loopback network and over real HTTP. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… code Documentation half of the final review fix wave (rulings R17, R20, R21, R27-R30). - The wallet a registerRouter factory receives is the BRC-103 authentication wallet: no storage provider, cannot create or internalize actions. The eqc examples build a storage-backed wallet from the same root key instead, and the overlay-express README, docs page, and JSDoc say so. - The threshold counts identity keys, not operators; hostOverrides pinning is the defence for value-bearing reads. - Authenticated responses are buffered by AuthFetch before the byte cap can apply; maxHosts and hostTimeoutMs bound the exposure. - The enforceable per-delivery minimum is max(minPayoutSats, share at rank K of K), and maxPayloadBytes relates to the server response limit. - The design document matches the code: corroborated consistency reference, transport caps, params caching, reputation events, decision 6, host options, pending store accounting, wallet facade, discovery identity keys. - The implementation plan gains frontmatter and its two quoted links no longer trip the docs link checker, so docs-site validate passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kills the two string mutants the eqc-protocol mutation target left alive in requiredShare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Moves the development toolchain to the versions main now uses (Vitest 5, V8 coverage 5.0.1, tsdown 0.23.0, oxlint 1.83, @types/node 26.6.1) and widens the @bsv/sdk peer to ^2.8.0 || ^3.0.0. SDK 2.8.0 is the first release whose AuthFetch accepts bounded transport options and whose SLAP advertisement template enforces canonical names and https/wss/js8 URIs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sements @bsv/sdk 2.8.0 lets AuthFetch take a response byte cap and a fetch client, so the transport now passes maxResponseBytes and its fetch through instead of rechecking a fully buffered body. The same release only builds and decodes SLAP advertisements with canonical ls_ names and https, wss or js8 URIs, so plain http hosts under the local preset can only come from configuration. Tests build malformed advertisements without the template, move the http case to hostOverrides, advertise e2e hosts over https with a loopback fetch, and pin the SDK-level byte cap. Docs drop the stale buffering limitation and state that distinct keys are not evidence of independent operators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root entry grew from the SDK 3.0.0 trust-boundary graph it pulls in (AuthFetch, LookupResolver, SLAP template, wallet-result validation); @bsv/eqc itself is about 32 KB. Records the measured sizes in the browser artifact disposition and sets limits with roughly 15% headroom, matching the convention used for other SDK 3 consumers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sorts with one explicit UTF-16 code-unit comparator (never localeCompare, which would make canonical bytes depend on the runtime locale) instead of nested ternaries and a bare sort, splits the discovery, consistency, and query-validation functions below the complexity limit, writes the random range constants as powers of two, uses optional chains and a length matcher, and names the JSON round trip the test loopback performs. Output and wire encodings are unchanged; the property suite still pins them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the ts2md doc script used across the stack and the generated references for the @bsv/eqc and @bsv/eqc/host entries, linked from the README and the package page. The e2e loopback fetch now refuses Request inputs instead of silently dropping their method and body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5bb4950 to
50fa5b8
Compare
| collect, | ||
| mount(router) { | ||
| router.get(ECONOMIC_PATHS.params, params) | ||
| router.post(ECONOMIC_PATHS.query, query) |
| mount(router) { | ||
| router.get(ECONOMIC_PATHS.params, params) | ||
| router.post(ECONOMIC_PATHS.query, query) | ||
| router.post(ECONOMIC_PATHS.collect, collect) |
| const wallet = new HostWallet() | ||
| const app = express() | ||
| app.use(express.json({ limit: '1mb' })) | ||
| app.use(createAuthMiddleware({ wallet, allowUnauthenticated: true, logLevel: 'error' })) |
Follows main's conventions for the newly reported rules: in-memory store methods and the non-paying wallet facade return settled promises instead of async functions without an await, sequential work is expressed as a pump or an ordered reduce chain, and independent work (topic anchors, test-wallet output checks) runs concurrently. Splits the consistency reference computation below the complexity limit. Refreshes the measured browser sizes. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uery-client-9f9bde
…o claude/eqc-economic-query-client-9f9bde # Conflicts: # governance/test-quality/policy.json
…o claude/eqc-economic-query-client-9f9bde # Conflicts: # docs/reference/package-api-migrations.md # docs/reference/stack-facts.md # governance/repository-health/baselines.json
…o claude/eqc-economic-query-client-9f9bde
…o claude/eqc-economic-query-client-9f9bde
…o claude/eqc-economic-query-client-9f9bde
Main's @bsv/sdk 4.0 (#1243) and @bsv/color-permission-module raised the same package counts this branch raises; the auto-merge kept one increment, so the supply-chain count, package ledger and test-governance totals now include both. EQC's @bsv/sdk peer adds ^4.0.0 alongside its SDK2 and SDK3 ranges, as main does for every first-party package; its tests, typecheck and browser contract pass on the SDK 4 candidate, and the recorded browser sizes are re-measured against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|



Program and scope
docs/superpowers/specs/2026-09-18-eqc-brc178-design.md, the plan atdocs/superpowers/plans/2026-09-18-eqc-economic-query-client.md.@bsv/eqc: protocol core (canonical JSON query IDs, canonical payload hashing, wallet-only BRC-77 attestations, Fibonacci payouts, BRC-29 derivation bound to query and rank), theEQCclient (new EQC(wallet); SLAP-bootstrapped discovery likeLookupResolver, race, one payout transaction, collect, hash verification, local reputation), and framework-agnostic host handlers at@bsv/eqc/hostforGET /economic/params,POST /economic/query,POST /economic/collect.@bsv/overlay-express: additiveregisterRouter(path, factory)that mounts application routers after the BRC-103 middleware and before the admin routes and 404 handler.infra/message-box-serverandinfra/overlay-server(both consume published packages, so they need@bsv/eqcon npm first),collectionGrantstorage, multi-host message box anointing and send fan-out, collection grants end to end, host gossip, query deposits.5bb4950775e9ffcb0a4f46b7235e5e5e1ede248dImpact
Affected packages/services and intended patch versions (publication occurs only
through the release workflow after approval):
@bsv/eqc— new,0.1.0(release notes0.0.0->0.1.0, minor). Peer dependency:@bsv/sdkonly. Noexpressor@bsv/overlayimport; host handlers are typed structurally.@bsv/overlay-express—2.7.1->2.8.0(additive method and two exported types; no existing behaviour changed).Security-sensitive surface, for the reviewer:
createActionper query, capped bymaxFeeSats(default 2000), and makes no wallet call when fewer thanthresholdhosts attest one hash.AuthFetchpays any well-formed HTTP 402 with no cap. The transport therefore gives it a wallet allowlist (getPublicKey,createSignature,verifySignature,createHmac,verifyHmac); an end-to-end test has a host demand 1,000,000 satoshis and asserts the wallet is asked for exactly one action.message-listis served only to the authenticated recipient; the free attestation path is bounded (entries, estimated heap bytes including query params, per-client cap, TTL).hostOverridespinning is the defence for value-bearing reads;AuthFetchbuffers authenticated responses before any byte cap can apply (an@bsv/sdklimitation); the walletregisterRouterhands out has no storage provider and cannot internalize payments, so operators must pass a storage-backed wallet built from the same root key.Verification
5bb495077, all exit 0):pnpm --filter @bsv/eqc test— 21 files, 280 tests passed, 0 skipped;test:property6 passed; end-to-end suite (real HTTP, realcreateAuthMiddleware, realAuthFetch) run three more times, 6/6 eachpnpm --filter @bsv/eqc typecheck,lint,format:check,buildpnpm --filter @bsv/eqc pack:check— packed payload, all conditional exports, source maps, publint, strict type resolution, esm/cjs clean consumerspnpm --filter @bsv/eqc test:browser— exact-tarball browser contract verifiedpnpm --filter @bsv/overlay-express test— 13 suites, 344 tests passedpnpm health:check— 43 projects, 35 public packages, 0 contract findings, 0 control errorspnpm typecheck,pnpm lint,pnpm format:check(repo-wide)pnpm docs:facts:check— 0 findings;pnpm --filter docs-site validate— 125 files, frontmatter and links OKnode scripts/check-sdk-peer.mjs— 38 manifests, no violationspnpm install --frozen-lockfile --ignore-scripts— tree stays cleanpnpm build,pnpm audit:security, andpnpm test:mutation --target eqc-protocol(89.47%, floor 85).test:coveragereported statements 95.34%, branches 90.54%, functions 100%, lines 97.45% before the final fix wave added 76 tests.pack:checkandtest:browserabove; the client bundle contains only@bsv/eqcand@bsv/sdk.browser-budget.jsonfor@bsv/eqcentry.— measured vite raw 508859 / gzip 131707 / brotli 110327, esbuild raw 393706 / gzip 120831 / brotli 103337, budget set at 1.1x of the first measurement.compatibility, public API, artifacts, dependencies, docs, and operations
(every task passed a task-scoped review; a whole-branch review with seven review seats and three adversarial verifiers per finding confirmed 18 findings, all fixed and re-reviewed 18/18 addressed; 44 Minor findings deferred, see below)
scope-based skip is expected and validated by the merge gate
Security and dependencies
audit results were reviewed
(including accepted or false-positive issue states) and zero unreviewed hotspots;
Sonar's aggregate
Quality Gate passedverdict alone is not merge evidenceand removal condition (not applicable: none added)
Dependency evidence
@bsv/eqcadds no runtime dependency. Its dev dependencies (vitest,@vitest/coverage-v8,tsdown,fast-check,oxlint,express,@types/express,@bsv/auth-express-middlewareworkspace link) are versions already used elsewhere in the workspace.@bsv/sdkis a peer (^2.4.1) and a dev dependency (workspace:^), never a dependency;check-sdk-peer.mjspasses.vitest@4.1.11instance and move its transitivetinyexec1.2.4 -> 1.3.1 andpicomatch4.0.5 -> 4.0.7.--frozen-lockfilepasses. Regenerating from the base lockfile withpnpm install --lockfile-only --ignore-scriptsmay give a smaller diff.pnpm audit:securityreported clean at an earlier head; CodeQL pending.@bsv/eqc0.1.0 (new),@bsv/overlay-express2.8.0.Release and operations
controlling program
Completion evidence
passing checks, resolved alerts, measurements, or an approved exception
guidance are current or concretely not applicable
being handed to another contributor as “complete”
restriction is assumed
Notes for the reviewer
/economic/*rather than/brc178/*; the default floor is 1000 satoshis rather than 2; theoverlay-lookupcontent hash covers the sorted outpoint section only, with BEEF delivered beside it (honest hosts hold different proof state, so hashing BEEF would never reach a threshold); the client broadcasts with a normalcreateActionrather than after delivery (a host'sinternalizeActionbroadcasts on receipt anyway); a host's required share is the remainder-free Fibonacci share of the fee it quoted (the share "at its own floor" including the rounding remainder is not monotone, so an honest rank-1 host could refuse a larger payment).Co-Authored-Byline its own session named (Claude Fable 5.1,Claude Sonnet 5,Claude Opus 5 (1M context)). Say if you want them rewritten to one form before merge.ERR_EQC_PAYMENTwith a misleading message when every ranked host is below its advertisedminPayoutSats;hostSetHintcrowding; the test-quality policy's invariants list not naming the newrequiredShareproperty).packages/overlays/overlay-express/API.md(ts2md-generated) does not yet list the two new exported types.🤖 Generated with Claude Code