Skip to content

Add Captive Core block fetcher#9

Open
GabrielCartier wants to merge 28 commits into
mainfrom
feature/captive-core
Open

Add Captive Core block fetcher#9
GabrielCartier wants to merge 28 commits into
mainfrom
feature/captive-core

Conversation

@GabrielCartier
Copy link
Copy Markdown
Contributor

@GabrielCartier GabrielCartier commented May 7, 2026

Adds a second fetch backend (fetch captive-core) that streams Stellar blocks via an embedded Captive Core subprocess, as an alternative to the existing RPC fetcher. Also folds in a one-shot fix tool to rewrite v1-fetcher merged blocks and a battlefield-style integration test harness that exercises both backends.

What changed

New: fetch captive-core command (cmd/firestellar/fetcher_captive_core.go)

  • Runs stellar-core in-process via the Stellar SDK's ledgerbackend.CaptiveStellarCore
  • Supports mainnet, testnet, and custom networks via --stellar-core-network
  • Custom network requires --stellar-core-network-passphrase and --stellar-core-history-archive-urls; passphrase/archive flags also override the defaults for mainnet/testnet
  • Bundles SDF default TOML configs for mainnet and testnet; custom network requires an explicit --stellar-core-conf path
  • Persists the last fired block to {state-dir}/cursor.json after each successful emission; restarts resume at last_fired_block + 1 instead of replaying from <first-streamable-block>. Pass --ignore-cursor to force a fresh start (e.g. when running under a supervisor that already tracks downstream state).
  • Dev script at devel/captive-core.sh

Fix: hash encoding in RPC fetcher (rpc/fetcher.go)

  • ledger[0].Hash from stellar-rpc is hex, not base64 — switched to hex.DecodeString
  • PreviousLedgerHash is already raw bytes on the XDR struct — removed the incorrect base64.DecodeString(HexString()) round-trip, now uses [:] directly
  • Block IDs (used by firecore as one-block filenames) switched from base64 to hex to avoid / characters acting as path separators

Refactor: isMainnet boolnetworkPassphrase string

  • Fetcher now holds the full passphrase string instead of a boolean flag, allowing the RPC fetcher to also work against arbitrary networks

New: fix-block-hashes tool (cmd/tools/fix/tools_fix_block_hashes.go)

  • One-shot maintenance command to rewrite merged-blocks produced by the buggy v1 RPC fetcher
  • Recovery is purely local: the v1 fetcher accidentally stored base64.Decode(hex_string) as the ledger hash, which is a reversible identity round-trip on 64-char base64-alphabet input. ConvertBrokenHash re-encodes the garbage bytes back to base64 (yielding the original hex string) and hex-decodes that to the original 32-byte hash — no external source needed.
  • Cross-checks the recovered hash against the v1 pbbstream.Block.Id / ParentId (which were accidentally already correct hex thanks to the same round-trip identity); bails on any mismatch.
  • Range must be closed and 100-aligned; non-overlapping ranges can run in parallel across shards.

New: tool-compare-merged-blocks (cmd/firestellar/tool_compare_merged_blocks.go)

  • Diffs two merged-block stores over a block range; useful for validating a new fetcher (e.g. captive-core) against existing v1 RPC output.
  • --sanitize-reference / --sanitize-current applies the same ConvertBrokenHash recovery as fix-block-hashes to either side before comparing, so legacy v1 stores can be diffed against correctly-hashed v2 stores.

New: battlefield-style integration test harness (test/)

  • test/lib/devstack/ — launches a local Stellar devstack (validator + follower via docker-compose under test/scripts/dev/)
  • test/lib/firehose/ — in-process fetchers for both backends (inprocess_rpc.go, inprocess_cc.go) plus a shared Fetcher interface
  • test/lib/snapshot/ — golden-file snapshot diffing against test/snapshots/**/*.expected.json
  • test/lib/stellar/ + test/lib/xdr/ — Horizon client helpers + XDR decoders used by the scenarios
  • test/scenarios/ — payment / account / asset / offer / multisig / soroban / fee-bump scenarios driving both backends and asserting equivalent output
  • test/README.md documents how to bring up the stack and run the suite

Tooling note — go-json-experiment pin downgrade

  • go.mod pins github.com/go-json-experiment/json to the Oct 2023 snapshot to match firehose-core's internal firehose-core/json/marshallers.go (old API names NewMarshalers / MarshalFuncV2)
  • The Jan 2025+ snapshots renamed these to JoinMarshalers / MarshalToFunc; cmd/firestellar/tool_decode_block.go was using the new names and has been reverted to the old ones to compile against the downgraded dep
  • FIXME comments in both go.mod and tool_decode_block.go flag this for a flip-back once firehose-core upstream bumps its own pin

Notable side effects

  • rpc.NewFetcher signature change: callers must pass networkPassphrase string instead of isMainnet bool
  • Existing v1-fetcher merged-blocks in storage have incorrect hash bytes; operators should run fix-block-hashes against the historical range before swapping the reader over to the v2 captive-core path

Test plan

  • go build ./... clean
  • go vet ./... clean
  • fetch captive-core against mainnet + testnet streams blocks end-to-end
  • fix-block-hashes rewrites a sample range and the resulting block hashes match fetch captive-core output for the same ledgers
  • test/scenarios/... pass against both RPC and captive-core in-process fetchers via the devstack

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new block-fetch backend that uses an embedded Captive Stellar Core subprocess, while also correcting RPC fetcher hash handling and generalizing network selection to support non-(mainnet|testnet) passphrases.

Changes:

  • Introduces fetch captive-core command (and dev script) to stream ledgers via ledgerbackend.CaptiveStellarCore.
  • Fixes RPC fetcher hash decoding (ledger hash is hex, removes incorrect previous-hash decoding) and switches bstream block IDs to hex for filesystem-safe one-block filenames.
  • Refactors RPC fetcher initialization from isMainnet bool to an explicit networkPassphrase string, updating call sites and tests.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
rpc/init_test.go Adds helper to map test RPC endpoint → network passphrase.
rpc/fetcher.go Fixes ledger hash decoding and switches block ID encoding to hex; uses passphrase string for tx extraction.
rpc/fetcher_test.go Updates tests to construct the fetcher with the correct network passphrase.
devel/poller.sh Updates dev polling start block and fetch args.
devel/captive-core.sh Adds dev script to run reader-node using the captive-core fetcher.
cmd/firestellar/tool_compare_fetcher_blocks.go Fixes compare tool to honor --network by selecting the right passphrase.
cmd/firestellar/main.go Registers new fetch rpc and fetch captive-core subcommands.
cmd/firestellar/fetcher.go Renames/updates RPC fetch command; adds passphrase resolution logic and deprecates --is-mainnet.
cmd/firestellar/fetcher_captive_core.go Implements captive-core fetch loop and block conversion to bstream blocks.
.gitignore Ignores runtime data directories created by dev scripts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Comment thread cmd/firestellar/fetcher_captive_core.go
Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Comment thread cmd/firestellar/fetcher.go Outdated
Comment thread devel/poller.sh Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Comment thread cmd/firestellar/fetcher.go
Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Comment thread cmd/firestellar/fetcher_captive_core.go Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

cmd/firestellar/fetcher.go:30

  • The --state-dir flag help text is incorrect (currently says "interval between fetch"), which can confuse operators and tooling. Update the description to reflect that this directory is used for poller state/checkpoint storage.
	cmd.Flags().StringArray("endpoints", []string{}, "List of endpoints to use to fetch different method calls")
	cmd.Flags().String("state-dir", "/data/poller", "interval between fetch")
	cmd.Flags().Duration("interval-between-fetch", 0, "interval between fetch")
	cmd.Flags().Duration("latest-block-retry-interval", time.Second, "interval between fetch")

Comment thread rpc/fetcher.go
Comment thread cmd/firestellar/main.go Outdated
Comment thread rpc/fetcher.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated 6 comments.

Comment thread test/lib/stellar/client.go
Comment thread test/lib/stellar/client.go
Comment thread test/lib/stellar/ops.go Outdated
Comment thread test/lib/xdr/decode.go Outdated
Comment thread Dockerfile
Comment thread test/lib/devstack/devstack.go Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 56 out of 58 changed files in this pull request and generated 2 comments.

Comment thread test/lib/firehose/inprocess_rpc.go
Comment thread test/lib/firehose/inprocess_cc.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 58 out of 60 changed files in this pull request and generated 8 comments.

Comment thread test/lib/stellar/client.go Outdated
Comment thread test/README.md
Comment thread test/scenarios/scenarios_test.go Outdated
Comment thread test/lib/runner/runner.go Outdated
Comment thread test/scripts/dev/configs/derive_pubkey.go
Comment thread test/scripts/dev/configs/stellar-core-validator.cfg
Comment thread cmd/tools/fix/tools_fix_block_hashes.go
Comment thread Dockerfile-rpc-fetcher Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 58 out of 60 changed files in this pull request and generated 3 comments.

Comment thread test/lib/xdr/decode.go
Comment thread test/lib/devstack/devstack.go
Comment thread test/scenarios/main_test.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 59 out of 61 changed files in this pull request and generated 1 comment.

Comment thread cmd/firestellar/tool_compare_merged_blocks.go
- stellar/client.go: route FundAccount through configured httpClient (timeout); SubmitOpsExpectFail now propagates non-horizon errors instead of swallowing all errors.
- stellar/ops.go: ChangeTrust takes txnbuild.CreditAsset (compile-time guard) instead of asserting on txnbuild.Asset at runtime.
- xdr/decode.go: corrected doc on ContractId normalization (encoded as C... strkey, not placeholder).
- devstack/devstack.go: clarified DataRoot default points at <testRoot>/.data.
InProcessRPCFetcher.logger / .networkPassphrase and
InProcessCaptiveCoreFetcher.logger were stored but never read.
- stellar/client.go: drop extra slash in FundAccount URL (/friendbot?addr=)
- Dockerfile-rpc-fetcher: bump build image to golang:1.26 (matches go.mod)
- README + doc comments: correct paths (test/scenarios, test/scripts) and Go version (1.26+)
- tool_compare_merged_blocks: nil-check stellar Header before recovering PreviousLedgerHash
- devstack: rename misnamed repoRoot field to testRoot (actually <repo>/test/)
- xdr/decode: correct doc on what drives per-run drift (deterministic keypairs via AccountSeedScope)
- scenarios/main_test: drop dead '_ = exitCode' from defer
- fix-block-hashes: switch stopBlock comparisons to >= so the range stays
  exclusive on the upper bound, matching the merged-block compare tool.
- tool-compare-merged-blocks: wire logger.Debug per-bundle and drop the
  unused `_ = logger` placeholder.
- xdr/decode.go: clarify normalizeDynamicFields determinism comment; SAC
  IDs and addresses are stable when runner.Config.AccountSeedScope is
  set (the default in the suite).
- README: split fetcher docs into RPC vs captive-core sections; document
  --state-dir / --ignore-cursor resume semantics shared across both.
- .gitignore: cover the data-cc-mainnet runtime dir produced by the
  captive-core dev script.
JSON unmarshal into any/map[string]any coerces all numbers to float64,
losing precision above 2^53. Stellar amounts use stroops (1 XLM = 1e7)
and total supply ~5e17 stroops sits well above that threshold, and
sequence numbers can grow into the same range — so snapshot diffs and
cross-backend equality checks could silently miss real differences.

Switch all four JSON round-trips to json.Decoder.UseNumber():

- xdr/decode.go: roundtrip (driver for all event decode paths).
- snapshot/snapshot.go: Snapshot.normalize (actual side) and Load
  (expected side, so DeepEqual compares like-to-like json.Number).
- snapshot/diff.go: toGeneric (used by DiffViews for backend diffs).

Also stop swallowing roundtrip errors in decode{Diagnostic,Tx,Contract}Events;
surface them via the same {_decodeError: ...} sentinel used for
UnmarshalBinary failures so SDK shape regressions fail loud instead of
dropping fields silently.
…visory)

SDF disclosed a critical network vulnerability in stellar-core; the
fix lives in 26.1.0-3210.427aa3978. Pre-fix builds must not ship.

- Dockerfile: post-install dpkg --compare-versions check fails the
  build when stellar-core is older than STELLAR_CORE_MIN_VERSION (26.1.0
  default). The apt install line still pulls from SDF's stable channel,
  which now ships the patched build.
- test/scripts/dev/docker-compose.yml: pull_policy: always on
  stellar/quickstart so each devstack up grabs the latest image (the
  testing tag floats forward and now bundles the patched stellar-core).
  Override via QUICKSTART_PULL_POLICY=missing for air-gapped runs.
- test/README.md: bump documented minimum stellar-core version and
  point operators at brew/apt upgrade commands.
@GabrielCartier GabrielCartier force-pushed the feature/captive-core branch from 627389f to a9e9db2 Compare May 14, 2026 07:58
The roundtrip()/Load()/toGeneric() change to UseNumber means real
decoded payloads now carry json.Number where they previously held
float64. The dynamic-field / strkey / account-id normalizers were
still type-switching on float64, so SeqNum/BumpTo/OfferId placeholders
and 32-byte strkey collapse silently no-op'd on real input, drifting
snapshots away from their expected shape.

- Add numericByte() and isNumericValue() helpers that accept both
  json.Number and float64 (the latter kept for hand-built fixtures and
  defensive coding).
- Rewrite dynamicFieldPlaceholder, isThirtyTwoByteArray, tryEncodeStrkey,
  and the Type/byte-array assertions in tryEncodeAccountID to use the
  helpers.
- Update decode_test.go fixtures to use json.Number to match the actual
  runtime representation; jsonNumberSlice now lives up to its name. The
  SeqNum case uses 9007199254740993 (2^53 + 1) so the test would have
  failed under the old float64-coerced path — locking in the precision
  fix.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 61 out of 63 changed files in this pull request and generated 2 comments.

Comment thread cmd/firestellar/tool_compare_merged_blocks.go
Comment thread go.mod Outdated
tool-compare-merged-blocks only iterated refStore.Walk, so any bundle
that lived exclusively in the current store was never visited. The
tool reported a clean diff even when the current side had extra
blocks (or an entire range main was missing). Run a second walk over
curStore after the reference pass, skipping filenames already visited,
and report each current-only block as missing-in-reference. Honors
stop-on-first-diff and shares the existing stopErr sentinel.

go.mod: the rebase onto main inadvertently kept go-stellar-sdk v0.4.0
from the feature branch instead of main's v0.5.0 (dependabot bump in
PR #3). Restore v0.5.0 and update the inline comment to reflect that
this version tracks main.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 61 out of 63 changed files in this pull request and generated 2 comments.

Comment thread cursor/cursor.go
Comment thread Dockerfile Outdated
GabrielCartier and others added 3 commits May 14, 2026 16:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
go-version: 1.26.x
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is a config to switch to pull from go.mod defined version, I think it's a better.

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.

4 participants