Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errata: draft-00, reference code and vectors, v0.5 overview

Spec text licensed CC-BY-4.0, (c) BNAP contributors. Compiled 2026-10-13.

This table lists every inconsistency found between three sources:

Rows 30 and 31 are internal to draft-00 or concern the bnap:notes tab; rows 32 to 34 come from findings outside these sources, supplied with the export task; row 35 comes from the end-to-end conformance run against a real gate.

Rule. Draft-00 wins unless a vector proves otherwise. Where draft-00 is silent, the row says so, and adding a normative rule is marked as an owner decision.

Draft-00 against the vectors. No row: they agree. All 11 full hex values and all 9 shortened values in draft-00 section 14, and both hex blocks (the 305-byte offer and the Response), match vectors.json. The base64url prefixes in the 10.3 example decode to the start of the vector offer, credential and Response, and BNAP-Error: AAEBAgEQ decodes to code 16. The 10.3 arithmetic holds (1,680 sat ceiling, 425 sat before and 340 sat after the discount). The BIP-340 code reproduces official vector 0, and regenerating the vectors gives identical bytes.

#wheredraft-00 saysother source saysresolution (draft-00 wins unless the vector proves otherwise)owner decision needed (yes/no)
1Credential encoding: draft-00 2, 5.1, 15; overview “Credential envelope”The Credential is a TLV stream: 0 version, 2 offer, 4 pay, 6 attest, 8 binding, 10 ext, 12 offer_id; exactly one of offer and offer_idOverview: Credential = {v, offer, pay?, attest[], binding, ext} in deterministic CBOR, with no offer_id. Ref: the credential vector (581 bytes) is TLVTLV, as draft-00 section 15 lists; the vector agrees. The overview is to be brought into lineno
2Scheme payloads: draft-00 5.1; overview “Credential envelope”Each scheme sends a body and a witness; only class S has a witness; the Digest covers bodies, never witnessesOverview: pay = {scheme, payload}, attest = {scheme, payload}, and the Digest hashes each payload. Ref: the notes body and witness are separate records (types 2 and 4 of the pay stream), and the witness signs the DigestBody and witness, as draft-00 section 15 lists (the v0.5.1 Digest was circular)no
3Context: draft-00 5.2; overview “Credential envelope”H_"bnap/ctx"(OfferID ‖ action_scope ‖ lp(pay scheme ID, or empty) ‖ BigSize(n) ‖ lp(attest scheme ID₁) ‖ … ‖ H(ext value, or empty))Overview: H_"bnap/ctx"(OfferID ‖ value scheme ID ‖ sorted attestation scheme IDs ‖ H(ext)), with no action scope, length prefixes or count. Ref: context() follows draft-00; credential.context = e9f56036…12158e6dDraft-00; the vector proves itno
4Digest: draft-00 5.2; overview “Credential envelope”H_"bnap/cred"(Context ‖ lp(binding, or empty) ‖ H(pay body, or empty) ‖ H(attest body₁) ‖ …)Overview: H_"bnap/cred"(Context ‖ binding ‖ H(pay payload) ‖ H(each attest payload)), binding without a length prefix. Ref: digest() follows draft-00; credential.digest = 7c8f6196…d530d4c9, digest_other_binding = c9ef748b…a36a207aDraft-00; the vector proves itno
5ReplayKey: draft-00 6.1; overview “Credential envelope”H_"bnap/replay"(Context ‖ H(pay body, or empty) ‖ H(attest body₁) ‖ … ‖ BigSize(m) ‖ id₁ ‖ … ‖ idₘ) over every UseId and RailId, sorted bytewiseOverview: H_"bnap/replay"(Context ‖ sorted socket use IDs). Ref: replay_key() follows draft-00; credential.replay_key = 74bd8a79…9b0c2771, attest_only.replay_key = c9248cf0…8961be9cDraft-00, as section 15 lists; the vector proves itno
6Response and ack: draft-00 8.1, 15; overview “Receipt”The Response is a TLV stream of 11 records (version, status, replayed, charged, unit, offer_id, digest, metered_units, scheme_result, receipt_key, settled_at) plus ack (type 240) over H_"bnap/ack"(every record outside 240–1000). Section 15 calls this “Same fields as v0.5.1’s formula”Overview: Response = {status, replayed, scheme_result, ack} with the ack over H_"bnap/ack"(Digest ‖ status ‖ charged ‖ unit ‖ H(scheme_result)). Ref: build_response() signs the whole stream; the vector Response holds types 0, 2, 6, 8, 10, 12, 14, 16, 18, 20 and 240, ack c1d4270a…bfef6f70f7Draft-00; the vector proves it. The stream signs more than the v0.5.1 fields (also version, replayed, offer_id, metered_units, receipt_key, settled_at), so section 15 should say “a superset of v0.5.1’s fields”no
7Signature range and coverage: draft-00 2, 4.2; overview “Offer”, “Extensibility”Signature records are types 240–1000. OfferID = H_"bnap/offer"(S₀) over every record except the signature range and native; offer_sig = Sign(offer_key, H_"bnap/offer-sig"(S)) and mac = HMAC-SHA256(K_mac, S), where S is every record outside the signature rangeOverview: “Types 240 to 255 are signature records”; OfferID over “every record below 240 except native”; offer_sig and mac over “every record below 240”. Ref: sig_range() is 240–1000; the vector offer’s GREASE record (type 3,000,000,001, after mac in stream order) is inside OfferID, offer_sig and macDraft-00, as section 15 lists; the vector proves that records above 1000 are coveredno
8Offer record 14: draft-00 4.1; overview “Offer”offer_key: the 32-byte BIP-340 key that signed the offerOverview: op_key_id, “Which online offer key, listed in the manifest, signed this offer”. Ref: record 14 of every vector offer holds the key itself, b28dad5f…fe64add9Draft-00; the vector proves itno
9Offer record set: draft-00 4.1, 4.3, 4.4; overview “Offer”Adds route_scope (4), headers (6), the pay-entry rate (10) and the attest-entry value and epoch. mac may be absent only in offers published in advance, which then MUST carry issued_at. native only in exact-scope offersOverview: no types 4 or 6; pay entries have no rate; attest entries list only scheme ID, parameters and effect; issued_at is for “Self-issued offers only”; scope is always “32-byte hash of the action”. Ref: the route offer vector uses records 4 and 8 and a pay entry with rate 500Draft-00 (section 15 lists route scope and rate); the vector agreesno
10Metering: draft-00 7, 8.1; overview “Uses and outcomes”rate is the price per 1,000 meter units; charged = min(price(⌈units × rate / 1000⌉), price(amount)); the Response carries metered_units and chargedOverview: “For metered actions, the charge is the amount used”; no rate, no metered_units. Vectors: Response charged 340, metered_units 850Draft-00, as section 15 lists; the vector agreesno
11Discounts and route member names: draft-00 4.1, 9.1; overview “Scheme properties and route policy”Attest value is in basis points, 1 to 10,000. Manifest routes use pay and attest: [{"scheme", "effect", "value", "epoch"}]Overview TOML: accept = [...] and attest = [{ scheme = "httpsig:web-bot-auth", effect = "discount", percent = 20 }]. Vectors: manifest "pay": [...] and "value": 2000; offer attest entry value 2000Draft-00; the vector proves basis points and pay. The overview example and the Caparra config should followno
12Effect in the manifest: draft-00 9.1Routes list effect but the text does not say how it is written in JSON (the Offer uses tu64 0 to 3)Vectors: the manifest writes "effect": "discount"; the overview TOML also uses effect = "discount"Draft-00 is silent; the vector settles it: string names required, waive, discount, quota. State this in 9.1no
13Metered route in the manifest: draft-00 9.1, 10.3Routes have price: {unit: amount}, metered and meter, but no rate member, and the meaning of price on a metered route is not stated. 10.3 describes the route as 500 sat per 1,000 tokens up to 2,100 sat per callVectors: the manifest has "price": {"sat": 2100}, the ceiling; the rate 500 appears only in the offerThe vector shows that price is the ceiling. Whether the manifest should also carry the rate is openyes
14Error codes: draft-00 8.2, 15; overview “Errors”16 numbered codes. Section 15 names UNDERPAID, BAD_CREDENTIAL, BINDING and UNAVAILABLE as new, but not REQUIRED (16)Overview: 11 codes, unnumbered, no REQUIRED. The 10.3 example’s BNAP-Error: AAEBAgEQ decodes to 00 01 01 02 01 10, code 16Draft-00’s table. Add REQUIRED to the section 15 rowno
15HTTP header fields: draft-00 10.2; overview “Negotiation”, “Carriers”Offer in WWW-Authenticate: BNAP offer="…"; credential in BNAP-Credential; receipt in BNAP-Receipt (or a trailer, or a final bnap-receipt event); errors in BNAP-Error; optional BNAP-Context. The manifest URI is only Offer record 12Overview: credential in Authorization: BNAP …, receipt in Authentication-Info: bnap="…"; the carriers table lists WWW-Authenticate, Authorization, Authentication-Info; the 402 example adds info="/.well-known/bnap" as an auth-param. Vectors: the offer’s info record is the absolute URI https://api.example/.well-known/bnapDraft-00, as section 15 listsno
16HTTP scope: draft-00 10.1; overview “Carriers”lp(method) ‖ lp(target) ‖ lp(content hash, or empty) ‖ BigSize(h) ‖ listed headers; target is the manifest origin plus the request-target exactly as received, with no normalization; the gate hashes the body itself and Content-Digest is only a SHOULDOverview: the scope covers “Method, normalized absolute URI, Content-Digest”. Ref: http_scope() follows draft-00; action scope 02be38b5…971b3717Draft-00, as section 15 lists; the vector proves itno
17Native envelope: draft-00 5.4, 15; overview “Negotiation”Context = H_"bnap/ctx-native"(OfferID ‖ action_scope ‖ lp(scheme ID)), Digest = H_"bnap/cred-native"(Context ‖ H(native credential bytes)); offers cached by OfferID; receipt in the BNAP response header. Section 15 does not list this changeOverview: Context = H_"bnap/ctx"(OfferID ‖ scheme ID), Digest = H_"bnap/cred"(Context ‖ H(native credential bytes)), the same tags as a BNAP Credential; cache keyed by nonce; receipt in Authentication-Info. Ref: native_context() and native_digest() follow draft-00; context 38a036b6…6cbb1b78, digest 198cd8d6…f0a2a04eDraft-00; the vector proves it. Add a section 15 rowno
18UseId and RailId form: draft-00 6.1; overview “Uses and outcomes”UseId = H_"bnap/use"(lp(scheme ID) ‖ local ID) and RailId = H_"bnap/rail"(lp(rail kind) ‖ canonical payment bytes), 32-byte hashes; evm bytes are lp(CAIP-2 chain ID) ‖ token ‖ payer ‖ authorization nonceOverview: string prefixes such as bnap:notes:<Y>, httpauth:L402:<payment hash>, ln:<payment hash>, evm:<chain>:<token>:<from>:<nonce>. Ref: use_id() and rail_id() follow draft-00; use IDs b7ca97d6…232b6456, 381a38d9…4bc2fb5e; rail ID 5f210bd3…f1867744Draft-00; the vector proves it. Section 15 lists only who supplies the rail bytes; consider a row for the hashing toono
19Who supplies rail IDs; plug traits: draft-00 6.1, 6.3; overview “Uses and outcomes”, “Reference implementation”The scheme returns a rail reference; the operator-trusted rail plug confirms it and returns the canonical bytes; the gate computes the RailId. commit(rk, singles, counters) also takes rate and quota countersOverview: “A scheme that relies on a rail payment MUST also report that payment’s canonical rail ID”; the Rust sketch has Verified { rail_ids: Vec<RailId> }, UseStore::commit(ids, key) with no counters, and no body and witness splitDraft-00, as section 15 lists. The Caparra trait sketch should followno
20Use models: draft-00 6.2, 7; overview “Uses and outcomes”single, rate(k, epoch) and reusable, where reusable consumes nothing beyond the outcome record; limits come from a quota attestation (effect 3) counted against a registered quota_idOverview: reusable(quota), “A metered allowance per use ID”Draft-00no
21Timeout defaults: draft-00 2, 9.1; overview “Uses and outcomes”limits holds replay_window and pending_timeout, with no default for either and no rule that they are present. Only the size limits have defaultsOverview: “pending_timeout (default 300 s)”; Caparra config replay_window_s = 86400, pending_timeout_s = 300. Vectors: the manifest uses 86400 and 300Draft-00 is silent; the vector is an example, not a default. Choose defaults (300 s and 86,400 s) or make both members requiredyes
22ES256 co-signature: draft-00 8.1, 15; overview “Receipt”One ack, BIP-340, type 240; no other signature record is defined. Section 15: the 240–1000 range leaves “Room for more signature types later”Overview: “An operator whose keys live in an HSM MAY add an ES256 signature as well; BIP-340 stays mandatory”No ES256 record exists in draft-00. Keep the MAY only if a record type is assignedyes
23GREASE scope: draft-00 2; overview “Offer”, “Extensibility”, “Negotiation”GREASE is TLV types 3,000,000,001 to 3,000,000,999, odd only; senders SHOULD add one record to every Offer and CredentialOverview: “scheme IDs and carrier names reserve GREASE values too”; the 402 example lists GREASE inside pay; servers SHOULD include one. Vectors: the offer has one GREASE record (value 0a0a); the credential has none, which a SHOULD allowsDraft-00 defines no GREASE scheme IDs or carrier names. Keep or drop that overview featureyes
24Native challenge minting: draft-00 12; overview “Negotiation”The gate MUST bound native challenges per client address and per offer; it SHOULD mint native challenges at most once per offer nonceOverview: “A server MUST mint at most one invoice per offer nonce”Draft-00 wins by rule (SHOULD), but this weakens a MUST and section 15 gives no reasonyes
25Class None value schemes: draft-00 4.3, 7, 12; overview “Credential envelope”Class None needs an exact-scope offer (4.3), never appears in a route-scope offer (12), and may not waive, discount or carry a quota (7). Nothing limits a None value scheme to price 0Overview class table: None is “Allowed only at price 0, and never with a waive or discount effect”Draft-00 is silent on the price-0 half, so the overview rule is not contradicted. Adding it to draft-00 section 7 is a normative changeyes
26Unit strings: draft-00 4.1; overview “Offer”, “Value rails”sat, msat, an ISO 4217 code (minor units) or a CAIP-19 asset IDOverview: “sat, an ISO 4217 code or a CAIP-19 asset”, no msat; the guardrails write usd, eur in lower case. Vectors: sat onlyDraft-00: msat is allowed, and fiat units are ISO 4217 codes, which are upper case (USD, EUR). Draft-00 could say that units compare exactlyno
27Manifest and pinning: draft-00 9.1 to 9.3; overview “Manifest and pinning”Member required (the bnap-required flag); bnap: 1, origin, issued. A manifest signed by the current root keeps the same backup; the backup signs only a rotation; accept a higher seq or identical bytes; issued checked with 60 s of skew; pin until min(issued + max_age, now + 30 days); no two rotations within 30 daysOverview: “A manifest MAY set bnap-required”; it “lists versions”; a client accepts a refetched manifest “if the root or the backup signed it with a higher seq”. Vectors: the manifest uses required, bnap, origin, issued and backupDraft-00, as section 15 lists for the backup and issued-time rulesno
28Tab references: draft-00 1, 12, 15; overview “Abstract and status”, “Security and privacy considerations”Calls the overview “the first tab”, which is still trueOverview: the bnap:notes spec is “the second tab” and draft-00 is the “third tab”; the doc now orders the tabs overview, draft-00, bnap:notes, v0.3, v0.2Texts stay verbatim; README.md maps each tab reference to its fileno
29Stale open question: draft-00 4.1, 14; overview “Open questions”Defines the pay and attest encodings and publishes vectorsOverview, unticked: “Socket byte layout. Define the Offer’s pay and attest record encodings and publish test vectors”Done by draft-00 and vectors.json; tick it when the overview is brought into lineno
30Header size, internal to draft-00: draft-00 2, 10.2A Credential may be up to 16,384 bytes (2); gates MUST accept a BNAP-Credential field of at least 12,288 bytes (10.2)Arithmetic: base64url of 16,384 bytes is 21,846 characters, and 12,288 characters carry 9,216 bytes. Vectors: the credential is 581 bytes (775 characters), far below bothNo vector settles it. Raise the field minimum to 21,846 bytes, or cap Credentials on HTTP at 9,216 bytesyes
31bnap:notes tab, outside the three-way comparison: draft-00 2, 5.1, 11; schemes/bnap-notes.mdSchemes split body and witness. The bnap:notes registry example gives body = "TLV, section 3 of the scheme spec" and a witness over the socket Digest; the credential travels in BNAP-Credential. Section 2 lets a scheme keep JSON or CBOR payloadsThe bnap:notes tab is still the v0.4 profile: a CBOR SPEND, its own H_"bnap/spend" Digest and ack, Authorization: BNAP (or BNAP-Authorization) and Authentication-Info. It has no numbered section 3Draft-00 wins for every socket part. The notes scheme spec needs a draft-00 rewrite, and its body encoding (TLV, as the registry example says, or CBOR) needs choosingyes
32x402 on Lightning (external finding): overview “Abstract and status”, “Plug catalogue”, “Badges”, “Roadmap”, “Open questions”Nothing beyond the RailId rule: one Lightning payment offered under L402, MPP and x402 admits one actionOverview: owner decision “No x402 on Lightning until its spec is published”; catalogue “Pending a published spec”; Agent-1 and v0.6 “once its spec is published”; sources link coinbase/x402. External: the x402 exact scheme on Lightning (lnbtc) was published 2026-09-23 in x402-foundation/x402The Agent-1 condition is met; x402 on Lightning stays deferred to v0.6. Update the catalogue and badge wording. The open question on the network label and on a client-chosen nonce (class O or A) can now be checked against the published specno
33MPP receipts (external finding): draft-00 5.4, 10.2; overview “Negotiation”The receipt “goes in the BNAP response header next to the scheme’s own”, that is BNAP-ReceiptOverview: “the receipt goes in Authentication-Info next to the scheme’s own”. External: MPP (draft-httpauth-payment-01) returns its receipt in a Payment-Receipt header, not Authentication-InfoDraft-00: BNAP-Receipt travels next to MPP’s Payment-Receipt. The MPP registry entry should name that fieldno
34L402 (external finding): draft-00 4.1, 5.4, 10.3; overview “Plug catalogue”; bnap:notes “Sources”Native credential bytes “such as the L402 macaroon:preimage token”; the 10.3 example sends WWW-Authenticate: L402 macaroon="…", invoice="…"Overview catalogue: “L402 (bLIP-26)”; the bnap:notes tab links bLIP-26 at blob/master/blip-0026.md. External: bLIP-26 is still an unmerged PRCite bLIP-26 as an open PR. The L402 adapter accepts both token= and macaroon=, and both L402 and LSAT. Draft-00’s example stays as it isno
35MPP native entry (end-to-end finding): draft-00 4.1, 4.2 step 3, 5.4; registry payment:lightning/chargeThe native artifact embeds the OfferID “in the way its scheme’s registry entry defines, such as … an MPP id”; 5.4 lists “an HMAC’d MPP id” as a stateless artifactRegistry (before): mpp-challenge “the challenge’s HMAC’d id carries the OfferID”, and a bolt11 entry listed beside it as if always present. draft-httpauth-payment-01: the id is the server’s HMAC over the bound challenge parameters, so it cannot also be the OfferID; opaque is the parameter for server data. Caparra’s gate and the conformance runner (profile bnap-conf-mpp-2): the mpp-challenge entry is the Payment challenge exactly as sent, the OfferID is in opaque as base64url of the JCS object {"bnap_offer": hex(OfferID)}, covered by the HMAC id, and the invoice is methodDetails.invoice inside the challengeThe OfferID goes in opaque, under the HMAC’d id; a gate that puts it in id itself fails OFR-NATIVE-EMBED. A bolt11 entry is optional for MPP. The registry wording is updated; draft-00 4.2 should read “an MPP opaque covered by its HMAC’d id” in the next revisionno

35 rows; 8 need an owner decision (13, 21, 22, 23, 24, 25, 30, 31).