Skip to content

rpc: Core-compatible REST gateway so a remote bip300301_enforcer can use bitcoin-rs as its mainchain node - #49

Merged
metaphorics merged 10 commits into
mainfrom
devin/1786435561-rest-gateway
Aug 11, 2026
Merged

rpc: Core-compatible REST gateway so a remote bip300301_enforcer can use bitcoin-rs as its mainchain node#49
metaphorics merged 10 commits into
mainfrom
devin/1786435561-rest-gateway

Conversation

@metaphorics

@metaphorics metaphorics commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the minimal Bitcoin Core-compatible REST surface an unmodified bip300301_enforcer consumes, so it can be pointed at bitcoin-rs instead of bitcoind. No BIP300/301 consensus logic is ported.

The enforcer's REST client dictates the shape: it builds its base URL as http://{--node-rpc-addr} (app/main.rs), sends no Authorization header, and only calls GET /rest/chaininfo.json and GET /rest/headers/{hash}.json?count=N (lib/validator/main_rest_client.rs); it reads any 404 on /rest/* as "REST not enabled". So REST is served on the existing JSON-RPC listener/port, unauthenticated, off by default behind Core's rest=1 — not a second listener — and 404 is reserved for exactly the cases that detection is meant to catch (gateway disabled, unknown route, missing output format).

Endpoints: chaininfo.json (reuses getblockchaininfo), headers/{hash}.{json,hex,bin}. Status semantics follow Core: count defaults to 5 and must be in 1..=2000 (out of range → 400, no silent clamping), unknown query parameters and valueless keys are ignored, a well-formed hash that is not on the active chain → 200 with an empty body, a bad hash or unsupported extension → 400 Invalid hash: <hash> (hash only, no extension), a missing extension → 404 output format not found. Header JSON fields match the enforcer's deserializer, including bits as unprefixed hex so CompactTarget::from_unprefixed_hex round-trips. /rest/block/*, /rest/tx/*, /rest/mempool/*, /rest/blockhashbyheight/* are deliberately not implemented — the enforcer gets raw blocks via getblock verbosity 0 over JSON-RPC.

Server change is confined to serve_connection, which previously hard-rejected any non-POST request line:

request = read_request(..)                  // now parses method + path; GET may omit Content-Length,
if request.method == "GET" {                // POST still errors without it (unchanged)
    response = if path.starts_with("/rest/") { rest::route(ctx, path, query, rest_enabled) }
               else { 404 }                 // never falls through into the auth/JSON-RPC path
    write_response(..); continue            // keep-alive preserved, no auth check
}
if !auth.validate_header(..) { 401 }        // POST path byte-identical to before

Header traversal mirrors Core's rest_headers, whose loop condition is ActiveChain().Contains(pindex): the requested hash must be positively on the applied chain, otherwise the result is the empty 200. That covers side branches, orphans, and headers already known to the tree but not yet applied — during header-first sync a header above the applied tip yields [], and a walk that reaches the applied tip stops there rather than continuing up the header chain. One block_tree read snapshot per request serves the whole walk via parent pointers (resolve the hash once, resolve the terminal height once, walk ancestors, reverse), so a large count no longer costs a full Context::blocks scan per header — count=2000 on a 6100-block chain went from seconds to ~5 ms — and the walk cannot splice records from two branches mid-reorg. The linkage check is kept as a defense against an internally inconsistent tree and truncates the tail at the first broken link.

Two known gaps for enforcer operators, documented in docs/rest-interface.md rather than papered over: getnetworkinfo reports version 10000 (major 1) which the enforcer's version gate rejects, so it must run with --bitcoin-core-skip-version-check; and bitcoin-rs publishes no ZMQ pubsequence topic, so the enforcer's mempool sync needs an explicit --node-zmq-addr-sequence or a no-mempool/bounded mode. pubsequence can follow separately.

Coverage: unit tests for routing/count boundaries/error precedence (count is validated before the hash, as Core does)/active-chain rules/JSON field shapes, socket-level tests in crates/rpc/tests/auth.rs (REST 200 without credentials, 404 when disabled, POST JSON-RPC still 401/200 with and without auth, non-/rest/ GET → 404, keep-alive serving two REST requests on one connection), plus two rounds of runtime verification against a live regtest node peered to Bitcoin Core 27.0 (see PR comments) which is what surfaced the BITCOIN_RS_REST=1 env-parsing crash, the header-cache scan cost, the applied-vs-header tip boundary, and the Core status/message divergences fixed here.

Link to Devin session: https://app.devin.ai/sessions/2cf54a23e1494fd080fa7541dadf06ff
Requested by: @metaphorics

@metaphorics metaphorics self-assigned this Aug 11, 2026
@devin-ai-integration

Copy link
Copy Markdown
Original prompt from a

@gosuda/bitcoin-rs Minimally merge this. https://github.com/LayerTwo-Labs/bip300301_enforcer/

@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added an optional, disabled-by-default Bitcoin Core-compatible REST gateway.
    • Added /rest/chaininfo.json and /rest/headers/<hash> endpoints with JSON, hexadecimal, and binary responses.
    • Added header validation, active-chain traversal, response limits, and persistent HTTP connections.
    • REST requests can operate without authentication when enabled; JSON-RPC authentication remains enforced.
    • Added configuration support through TOML, CLI options, and the BITCOIN_RS_REST environment variable.
  • Documentation

    • Documented REST configuration, endpoints, limits, authentication, and disabled-by-default behavior.

Walkthrough

The change adds an optional Bitcoin Core-compatible REST gateway. Configuration supports rest=1, CLI/config layers, and BITCOIN_RS_REST. The RPC server accepts unauthenticated REST GET requests while retaining authentication for JSON-RPC POST requests.

Changes

REST gateway

Layer / File(s) Summary
REST configuration and startup wiring
crates/node/src/config.rs, crates/node/src/bitcoin_conf_compat.rs, crates/node/src/run.rs
The node parses and merges rest, defaults it to disabled, supports CLI and environment configuration, and passes it to RpcServer::bind.
REST endpoint implementation
crates/rpc/src/rest.rs, crates/rpc/src/handlers.rs, crates/rpc/src/lib.rs
The REST module implements chain information and header endpoints with validation, active-chain traversal, JSON, hexadecimal, and binary responses.
HTTP method and REST routing
crates/rpc/src/server.rs
The server accepts GET and POST, routes /rest/ requests, supports query parsing and keep-alive responses, and preserves JSON-RPC authentication and content-length validation.
Authentication, connection, and documentation validation
crates/rpc/tests/auth.rs, CONCEPTS.md, docs/README.md, docs/rest-interface.md
Tests cover enabled and disabled REST access, JSON-RPC authentication, non-REST GET requests, and persistent connections. Documentation describes configuration, endpoints, limits, and authentication behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses a conventional scope prefix and clearly describes the REST gateway added to the RPC listener.
Description check ✅ Passed The description directly explains the REST gateway, supported endpoints, authentication behavior, compatibility goals, and test coverage.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch devin/1786435561-rest-gateway

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rpc/src/rest.rs`:
- Around line 92-101: Update header_records in crates/rpc/src/rest.rs:92-101 so
side-branch hashes where active_hash != Some(hash) return the Core-compatible
empty result instead of the requested header, and add a regression test covering
that case. Update docs/rest-interface.md:23-25 to remove the statement that
side-branch hashes return their own header.

In `@crates/rpc/src/server.rs`:
- Around line 209-228: Update the request parsing flow around request_target and
keep_alive to parse and validate the HTTP version, then derive persistence
according to HTTP semantics: HTTP/1.1 stays open by default unless Connection:
close is present, while explicit Connection: keep-alive enables persistence
where applicable. Add a socket test sending two HTTP/1.1 GET requests without a
Connection header and verify both are handled on the same connection.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 42695073-2811-48a0-8e19-1e1beb57bab7

📥 Commits

Reviewing files that changed from the base of the PR and between 6113a69 and 4d2799f.

📒 Files selected for processing (11)
  • CONCEPTS.md
  • crates/node/src/bitcoin_conf_compat.rs
  • crates/node/src/config.rs
  • crates/node/src/run.rs
  • crates/rpc/src/handlers.rs
  • crates/rpc/src/lib.rs
  • crates/rpc/src/rest.rs
  • crates/rpc/src/server.rs
  • crates/rpc/tests/auth.rs
  • docs/README.md
  • docs/rest-interface.md
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: test
  • GitHub Check: bench-smoke
🧰 Additional context used
🔍 Remote MCP Github Grep

Relevant compatibility context

  • Bitcoin Core’s current REST docs use GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT>. The PR implements the older /rest/headers/<COUNT>/<BLOCK-HASH>... form, which Bitcoin Core documents as deprecated since v24.0. Verify this is the exact format required by bip300301_enforcer.
  • Bitcoin Core documents that header queries return empty when the hash is not on the active chain. The PR summary says side-branch hashes return their own header, which differs from Core behavior and may affect compatibility.
  • Core’s REST documentation states /rest/chaininfo.json supports JSON output only, matching the PR’s endpoint scope.
🔇 Additional comments (11)
crates/node/src/bitcoin_conf_compat.rs (1)

57-57: LGTM!

Also applies to: 143-145

crates/node/src/config.rs (1)

97-98: LGTM!

Also applies to: 176-176, 237-237, 447-449, 576-578, 669-669

crates/node/src/run.rs (1)

629-629: LGTM!

crates/rpc/src/handlers.rs (1)

32-36: LGTM!

crates/rpc/src/lib.rs (1)

22-23: LGTM!

crates/rpc/src/rest.rs (1)

1-90: LGTM!

Also applies to: 111-203

crates/rpc/src/server.rs (1)

31-32: LGTM!

Also applies to: 43-52, 116-167, 271-289, 344-365, 385-405

crates/rpc/tests/auth.rs (1)

5-70: LGTM!

Also applies to: 113-197

CONCEPTS.md (1)

5-11: LGTM!

docs/README.md (1)

10-11: LGTM!

docs/rest-interface.md (1)

1-22: LGTM!

Also applies to: 27-37

Comment thread crates/rpc/src/rest.rs Outdated
Comment thread crates/rpc/src/server.rs
Comment on lines +209 to +228
let request_target = request_line.trim_end_matches(['\r', '\n']);
let mut request_parts = request_target.split_whitespace();
let Some(method) = request_parts.next() else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid request line",
));
};
let Some(path) = request_parts.next() else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid request line",
));
};
if !matches!(method, "POST" | "GET") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid request method",
));
}

@coderabbitai coderabbitai Bot Aug 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Honor HTTP/1.1 persistent connections by default.

Line 232 defaults keep_alive to false. Line 267 enables it only for Connection: keep-alive. HTTP/1.1 keeps the connection open unless Connection: close is present.

Parse and validate the HTTP version. Derive persistence from the version and Connection header. Add a socket test with two HTTP/1.1 GET requests that omit Connection.

Also applies to: 232-232, 266-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rpc/src/server.rs` around lines 209 - 228, Update the request parsing
flow around request_target and keep_alive to parse and validate the HTTP
version, then derive persistence according to HTTP semantics: HTTP/1.1 stays
open by default unless Connection: close is present, while explicit Connection:
keep-alive enables persistence where applicable. Add a socket test sending two
HTTP/1.1 GET requests without a Connection header and verify both are handled on
the same connection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not taking this here. Requiring an explicit Connection: keep-alive is the server's pre-existing behavior for JSON-RPC POST; this PR only lets GET inherit it. Making HTTP/1.1 persistence the default means changing the shared request parser, which changes behavior for every existing JSON-RPC client, so it belongs in its own change rather than riding along with a REST addition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3b17e568a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rpc/src/rest.rs Outdated
Comment on lines +102 to +104
for height in start.height..=start.height.saturating_add(count.saturating_sub(1)) {
let Some(record) = ctx.block_by_height(height) else {
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep header traversal on one active-chain snapshot

If an active-chain reorg occurs after the initial block_by_height(start.height) check, each subsequent call reloads the current applied tip independently. The loop can therefore return a replacement-branch record at the starting height instead of the requested hash, or splice records from different branches into one response, causing the enforcer to receive a non-contiguous header sequence. Traverse under one tip/tree snapshot or verify every returned header links to the preceding record.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixing. The walk re-reads the active chain per height, so a reorg landing mid-walk can splice records from two branches into one response. Adding linkage verification: each subsequent record's previousblockhash must equal the previous record's hash, and the walk stops at the first break rather than emitting a spliced chain.

Comment thread crates/node/src/config.rs Outdated
"BITCOIN_RS_DATA_DIR" => layer.data_dir = Some(PathBuf::from(value)),
"BITCOIN_RS_STORAGE_BACKEND" => layer.storage_backend = Some(value.to_owned()),
"BITCOIN_RS_RPC_BIND" => layer.rpc_bind = Some(value.parse()?),
"BITCOIN_RS_REST" => layer.rest = Some(value.parse()?),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse the REST environment variable with the boolean parser

When REST is enabled through the environment using the Core-style value BITCOIN_RS_REST=1 (or the project's supported yes/on forms), bool::from_str rejects the value and node startup fails. The adjacent boolean environment settings use parse_bool, which explicitly accepts these forms; REST should use the same parser so layered configuration behaves consistently.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in dae9f8fBITCOIN_RS_REST now goes through the same parse_bool helper as its sibling boolean env vars, so 1/0/yes/no work. This one was not theoretical: BITCOIN_RS_REST=1 aborted the node at startup, which a runtime run against a regtest node caught.

@devin-ai-integration

Copy link
Copy Markdown

Runtime verification — REST gateway on a live regtest node

Built with cargo build --bin bitcoin-rs --no-default-features --features fjall, ran a regtest node with --rest true + RPC auth on 127.0.0.1:18443, peered it to Bitcoin Core 27.0 regtest and synced 2104 blocks including a 1-block reorg, then diffed every response against Core's own -rest=1 surface.

Actual responses (unauthenticated)
$ curl -s -i http://127.0.0.1:18443/rest/chaininfo.json      # no Authorization header
HTTP/1.1 200 OK
Content-Type: application/json

{"chain":"regtest","blocks":2104,"headers":2104,
 "bestblockhash":"0c31a0f5bd3c73dea144b79cf9baa7cb12cbe4144451a995cf6a0fedbfff279d", ...}

$ curl -s -i "http://127.0.0.1:18443/rest/headers/<genesis>.json?count=3"
HTTP/1.1 200 OK
Content-Type: application/json

[{"hash":"0f9188f1...2206","previousblockhash":null,"bits":"207fffff","height":0,"nonce":2,
  "time":1296688602,"version":1,"merkleroot":"4a5e1e4b...a33b"},
 {"height":1,"previousblockhash":"0f9188f1...2206","bits":"207fffff", ...},
 {"height":2,"previousblockhash":"0ad18cb0...9ddc","bits":"207fffff", ...}]

$ curl -s -i "http://127.0.0.1:18443/rest/headers/<genesis>.bin?count=3"
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 240        # 3 x 80 bytes, byte-identical to Core's getblockheader <h> false
  • chaininfo.json field set is identical to Core 27.0; chain/blocks/headers/bestblockhash match Core's values exactly.
  • Headers: strictly ascending from the requested hash (0→1999 from genesis, 2100→2103 mid-chain), previousblockhash chains correctly, bits unprefixed 8-char hex, genesis prev null; all shared fields byte-identical to Core. .hex/.bin are exact 80-bytes-per-header concatenations.
  • REST off by default → 404; enabling verified via CLI, env var, and rest=1 in bitcoin.conf. Regression checks on the same port: POST JSON-RPC 401 without auth / 200 with, non-/rest/ GET → 404 without touching auth, keep-alive pipelining still works for GET and POST.
  • Reorg case: an orphaned tip hash returns only its own header, as documented.

The run found one real bug and several Core divergences, all fixed in dae9f8f/98b4cf8: BITCOIN_RS_REST=1 aborted startup (provided string was not \true` or `false`) because the env parser used str::parse::()instead of the sharedparse_boolhelper its siblings use;count > 2000silently clamped where Core returns 400; unknown query params were rejected where Core ignores them; and a well-formed but unknown block hash returned 404 — which the enforcer would have reported as "REST server is not enabled". Unknown hash now returns200 []` and 404 is reserved for a disabled gateway and unknown routes.

Pre-existing, out of scope here: getblockchaininfo reports difficulty: 1.0 (Core: 4.66e-10) and time/mediantime as 0, and a fresh node reports bestblockhash as 64 zeros before genesis is applied.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@devin fix that as another pr

@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@metaphorics

Copy link
Copy Markdown
Contributor Author

@coderabbitai ultrareview

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

@metaphorics I will perform a complete review of PR #49. I will reassess all changed code paths, not only prior findings.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rpc/src/rest.rs`:
- Around line 61-67: Update route_headers to validate the file extension, then
call parse_count before Hash256::from_str so invalid count responses take
precedence when both inputs are invalid. Preserve the existing hash validation
afterward, and add a regression test covering an invalid hash with an invalid
count.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4a191d2-0ac9-4131-bab6-c8a2c1dac0b7

📥 Commits

Reviewing files that changed from the base of the PR and between 6113a69 and 4b999e8.

📒 Files selected for processing (11)
  • CONCEPTS.md
  • crates/node/src/bitcoin_conf_compat.rs
  • crates/node/src/config.rs
  • crates/node/src/run.rs
  • crates/rpc/src/handlers.rs
  • crates/rpc/src/lib.rs
  • crates/rpc/src/rest.rs
  • crates/rpc/src/server.rs
  • crates/rpc/tests/auth.rs
  • docs/README.md
  • docs/rest-interface.md
📜 Review details
🧰 Additional context used
🔍 Remote MCP Github Grep

Relevant review context

  • Bitcoin Core’s current REST implementation uses count=5 by default and rejects values outside 1..=2000 with HTTP 400, matching the PR’s documented header semantics.
  • Core accepts the modern /rest/headers/<hash>.<ext>?count=<count> form and also retains a deprecated /rest/headers/<count>/<hash> form. The PR summary only mentions the modern form; compatibility with the deprecated form is not established.
  • Core validates the hash after count validation and returns HTTP 400 for malformed hashes.
  • Bitcoin Core registers -zmqpubsequence=<address> and creates a corresponding pubsequence notifier, supporting the PR’s ZMQ documentation.
  • Multiple clients independently consume /rest/chaininfo.json, confirming the endpoint path is commonly relied upon.
🔇 Additional comments (11)
crates/node/src/bitcoin_conf_compat.rs (1)

57-57: LGTM!

Also applies to: 143-145

crates/node/src/config.rs (1)

97-98: LGTM!

Also applies to: 176-176, 237-237, 434-434, 448-450, 577-579, 670-670

crates/node/src/run.rs (1)

629-629: LGTM!

crates/rpc/src/handlers.rs (1)

32-37: LGTM!

crates/rpc/src/lib.rs (1)

22-23: LGTM!

crates/rpc/src/rest.rs (1)

1-60: LGTM!

Also applies to: 69-489

crates/rpc/src/server.rs (1)

31-32: LGTM!

Also applies to: 43-51, 116-122, 136-136, 153-181, 189-190, 202-228, 271-285, 344-366, 385-405

crates/rpc/tests/auth.rs (1)

5-12: LGTM!

Also applies to: 29-88, 114-120, 131-137, 139-197

CONCEPTS.md (1)

5-11: LGTM!

docs/README.md (1)

10-11: LGTM!

docs/rest-interface.md (1)

1-47: LGTM!

Comment thread crates/rpc/src/rest.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b999e8136

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rpc/src/rest.rs Outdated
for height in
start_height.saturating_add(1)..=start_height.saturating_add(count.saturating_sub(1))
{
let Some(record) = ctx.block_by_height(height) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid rescanning the block cache for every REST header

When a node has accumulated many applied records and a client requests a large header batch (up to the supported count=2000), every block_by_height call reaches record_for_hash, which linearly scans Context::blocks for the matching payload. Since apply appends one record per block, serving headers near a million-block tip becomes O(chain length × count)—potentially billions of comparisons per unauthenticated request—so the advertised enforcer sync can time out and consume the bounded RPC workers. Read the headers directly from one block-tree/applied-tip snapshot or otherwise index cache records by hash instead of repeating this scan.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and this is worth more than the traversal itself - thanks. block_by_height goes through record_for_hash, which linearly scans the push-only Context::blocks vector (one entry per applied block, never trimmed) and can additionally hit block_body_metadata on disk, so count=2000 is up to 2000 x O(chain length) comparisons plus disk lookups per unauthenticated request, on the bounded RPC worker pool.

Fixing it by serving headers from the block tree - which is where headers actually live - under a single block_tree read lock: resolve the requested hash to a node, decide active-chain membership from the tip in that same snapshot, then resolve the last wanted height once and collect its ancestors back to the start via parent pointers rather than doing a per-height lookup in a loop (node_at_height_from walks back from the tip when the active-height index isn't trusted, which would just move the O(depth) cost). The block-record cache is then only consulted for the cache-only fallback, which is one lookup rather than count of them.

A side effect worth noting: one ancestor walk inside one locked snapshot is structurally single-branch, so the explicit prev_blockhash linkage check added in a2bc4ef for the earlier multi-snapshot traversal becomes redundant. Observable behavior is unchanged - side-branch hash returns its own header, unknown-but-well-formed hash still returns the empty 200, and the JSON field set stays exactly the eight fields the enforcer reads.

@devin-ai-integration

Copy link
Copy Markdown

Runtime re-verification against Bitcoin Core 27.0 (regtest, 6100→6102 blocks)

Rebuilt the reference environment from scratch: Core 27.0 regtest peer with -rest=1, 6100 mined blocks, bitcoin-rs built with --no-default-features --features fjall, REST on the JSON-RPC listener. Every response was diffed against Core's own REST surface.

Header serving after the tree-snapshot rewrite — 0 diffs vs Core, and bounded by the applied tip

genesis / mid-chain (h=3050) / tip × count = 1, default, 5, 2000 — all 8 enforcer fields (hash, height, version, previousblockhash, merkleroot, time, bits, nonce) byte-match Core, .hex matches Core's .hex, .bin is the 80-bytes-per-header form:

genesis(h=0) count=2000: n=2000 status=200 ct=application/json binlen=160000 -> OK
mid(h=3050)  count=2000: n=2000 status=200 ct=application/json binlen=160000 -> OK
tip(h=6100)  count=2000: n=1    status=200 ct=application/json binlen=80     -> OK
FAILS: 0

Mid-sync (headers arrive before blocks are applied), with each request bracketed by a chaininfo read before and after so the concurrently-advancing tip cannot produce a false result:

blocks=512  headers=6100 [genesis n=769  0..768     blocks 512->1152 above_applied_tip=0 asc=True dups=False link=True]
                         [h1000   n=409  1000..1408 blocks 512->1664 above_applied_tip=0 asc=True dups=False link=True]
blocks=2176 headers=6100 [genesis n=2000 0..1999 ...] [h1000 n=2000 1000..2999 ...]
samples=7 midsync_samples=4 violations=0

With 6100 headers known but only ~512–1152 blocks applied, count=2000 from genesis returned 769 headers (0..768) — the walk stops at the applied tip, not the header tip, which is what Core's ActiveChain() does. A hash known as a header but not yet applied (Core's tip, while bitcoin-rs was behind) returns exactly one header.

Error precedence, count range, query params
count=0 on BAD hash   rs=400 Header count is invalid or out of acceptable range (1-2000): 0 | core=400 identical
count=2001 / 3000     rs=400 identical message                                             | core=400 identical
count=abc / -1 / 2^32 rs=400 identical message                                             | core=400 identical
?limit=5              rs=200 5 headers      ?count=3&foo=1 rs=200 3 headers                | core=200 (params ignored)
unknown 64-hex hash   rs=200 []                                                            | core=200 []
<hash>.txt            rs=400 Invalid hash: <hash>                                          | core=400 identical

chaininfo.json key set is identical to Core's 13 keys. REST off → 404; enabling works via --rest true, BITCOIN_RS_REST=1 (the earlier =1 startup crash is fixed), BITCOIN_RS_REST=0 → 404, and rest=1 in bitcoin.conf. Regression: POST JSON-RPC 401 without auth / 200 with auth, GET / → 404 without touching auth.

Latency and reorg

Per-request on one keep-alive connection, 6100-block chain:

count=   1 ms: 44.0 44.0 44.1     count= 100 ms: 44.1 43.7 44.0
count=2000 ms:  5.7  5.4  5.2  (644 KB body)   <- no chain-length x count scan

(The ~44 ms floor on small bodies is TCP delayed-ACK; a new connection costs a constant ~96 ms for every endpoint including POST JSON-RPC, which is pre-existing and unrelated to REST.)

Reorg via invalidateblock + 3 new blocks (6100 → 6102):

post-reorg rs   walk: (6099,3096d9096a8d) (6100,7f307aed079a) (6101,04929fd9b6bf) (6102,65eae827f1a9)
post-reorg core walk: (6099,3096d9096a8d) (6100,7f307aed079a) (6101,04929fd9b6bf) (6102,65eae827f1a9)

No duplicated or spliced heights.

The run isolated four remaining Core divergences, all now being fixed in this PR: a bare ?count with no = was rejected instead of ignored; a missing extension returned 400 Invalid hash instead of Core's 404 output format not found; the invalid-hash message included the extension; and an orphaned (not invalidated) hash returned its own header where Core returns [].

Not verified: non-regtest networks, TLS/proxy fronting, concurrent load.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@devin Should support ZMQ too. See https://github.com/LayerTwo-Labs/bip300301_enforcer and get full compatibility. Do your best.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@devin Be modular. Don't naively copy bitcoin-core's full surface.

@devin-ai-integration

Copy link
Copy Markdown

On it. Mapping exactly what the enforcer subscribes to and what our publisher already emits before I write anything — I'll come back with the gap and the plan rather than starting from Core's ZMQ surface.

Doing it as its own PR so this one stays a REST change.

@devin-ai-integration

Copy link
Copy Markdown

Agreed — the target is the enforcer's actual consumers, not Core's notification surface. Concretely that means the topics it subscribes to and the frame layout it parses, behind our existing publisher configuration, with nothing added just because bitcoind has it. Same principle as this PR, where /rest/block/*, /rest/tx/*, /rest/mempool/* and /rest/blockhashbyheight/* were deliberately left out.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 667dc49f66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant