Commit 18e181e
Add better processing behavior and tests (#134)
* feat(block-status): track header → BLOCK_PROCESSED → BUMP-built per block
Adds a `block_processing` table populated by three writers — chaintracks
header subscription, the merkle-service callback handler, and the
bump-builder consumer — so we can answer "which blocks reached which
milestone?" without scanning transactions or compound BUMPs.
- Models: `BlockProcessingStatus` with active/orphaned status and
pointer-time milestones so JSON cleanly distinguishes "not yet" from zero.
- Store: 6 new methods (Upsert/MarkProcessed/MarkBUMPBuilt/MarkOrphaned/
Get/List) implemented across Postgres (column-level ON CONFLICT),
Pebble (inverted-uint64 height index for descending scans), and
Aerospike (per-bin Operate to avoid clobbering concurrent writers).
Drops dead `setProcessedBlocks` helpers in Aerospike along the way.
- Writers: chaintracks tip + reorg subscription in api-server,
`MarkBlockProcessed` in `handleBlockProcessed` before Kafka publish
(log-and-continue on store error), `MarkBlockBUMPBuilt` in bump-builder
after `InsertBUMP`.
- API: `GET /api/v1/blocks/processing-status` (paginated, descending
height) and `GET /api/v1/blocks/processing-status/:blockHash`.
- Tests: 8 Pebble + 7 Postgres backend tests + 7 api-server handler
tests + 2 bump-builder integration tests; existing test mocks extended
to satisfy the larger interface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(app): hoist Bootstrap and BuildServices out of cmd/arcade
`buildServices` and the dependency wiring around it lived as private
helpers inside `cmd/arcade/main.go`, which meant test harnesses that
want to boot arcade in-process either had to launch the binary or
duplicate every kafka/store/teranode/merkle-client setup line.
Moves both into a new top-level `app` package with two entry points:
`app.Bootstrap(ctx, cfg, logger)` returns a `*Deps` plus a cleanup
func (closing publisher → teranode client → store → producer in
reverse order), and `app.BuildServices(deps)` returns the slice of
services to run for the configured mode. `cmd/arcade/main.go`
becomes a thin supervisor that calls the two and handles signals.
No behavior change. The full default test suite passes; this lands
ahead of the e2e harness which will reuse the same boot path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): add container layer for arcade ↔ merkle-service smoke tests
Lays the foundation for a reusable end-to-end test harness that boots
real backing services and runs against `ghcr.io/bsv-blockchain/
merkle-service:latest` instead of mocks.
- Adds testcontainers-go (postgres + redpanda modules) to go.mod.
- New `tests/e2e/harness` package, gated behind the `e2e` build tag
so the default `go test ./...` run is unaffected.
- `harness.New(t, ...Option)` brings up Postgres, Redpanda, and the
merkle-service container on a shared user-defined bridge network.
merkle-service is wired with `STORE_BACKEND=sql` (Postgres),
`KAFKA_BROKERS` (Redpanda alias), `BLOB_STORE_URL=memory:`, and
the SSRF/private-IP guards relaxed so callbacks back to
`host.docker.internal` succeed. Wait strategy accepts both 200
(healthy) and 503 (degraded — peer count zero before libp2p host
attaches) on `/health`.
- Container teardown handles partial-start failures and runs via
`t.Cleanup`. Helpers expose Postgres DSN / Kafka broker addresses
and a `WaitForMerkleLogLine` poll that reads container stdout —
used by later steps to assert deterministic events without poking
at internal state.
- `TestContainers_BootAndTearDown` skips when no container runtime
is reachable (so devs without docker/podman aren't blocked) and
otherwise takes ~16s warm to boot the stack and verify /health.
Verified against rootless podman with
`DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): in-process libp2p host as bootstrap peer for merkle-service
The smoke test needs to drive synthetic SubtreeMessage / BlockMessage
announcements that merkle-service consumes via libp2p. Adds a
`LibP2PHost` that wraps go-p2p-message-bus and:
- Listens on a random TCP port and advertises
`/dns4/host.docker.internal/tcp/<port>` so the merkle-service
container can dial back via the host gateway.
- Subscribes to the regtest block + subtree topics so gossipsub
forms mesh links with merkle-service (publish-only peers don't get
picked up by the mesh). A 250ms grace after Subscribe avoids a
msgbus close-of-closed race when tests tear down quickly.
- Exposes `BootstrapMultiaddr()` for feeding to merkle-service via
`P2P_BOOTSTRAP_PEERS`, and `PublishBlock` / `PublishSubtree` that
JSON-encode `teranode.BlockMessage` / `teranode.SubtreeMessage` and
send on the right pubsub topic.
`TestLibP2PHost_MerkleServicePeersWithHost` brings up the harness
host plus the full container stack and asserts merkle-service logs
`[CONNECTED] Topic peer <harness-peer-id>` within 90s. Using the
container log line as the peering signal — rather than
`msgbus.GetPeers()` — because msgbus's peerTracker only counts peers
we've received messages from, which stays empty when merkle-service
is silent.
Round-trip peering verified end-to-end against rootless podman in
~5s warm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): in-process datahub fake + synthetic block/subtree builder
The smoke test needs to feed arcade and merkle-service the same block +
subtree binaries that they would normally pull from a teranode datahub.
Adds two pieces:
- `harness.Datahub` is an httptest.Server bound on 0.0.0.0:auto-port
exposing GET /block/<hash> and GET /subtree/<hash>. Tests Stage*
payloads keyed by hash; the merkle-service container reaches the
server via the host.docker.internal URL.
- `txbuilder.go` mints synthetic transactions (using LockTime to vary
txids), packs them into the concatenated-32-byte-hash subtree binary
merkle-service expects, and constructs full model.Block.Bytes()
payloads via teranode's `model` package. Bitcoin merkle-tree helpers
(SubtreeRoot, MerkleRootFromCoinbaseAndSubtree) compose a header-
valid layout for callers that need ValidateCompoundRoot to pass
downstream.
`TestDatahub_StageAndServe` round-trips a synthetic block through
arcade's `bump.FetchBlockDataForBUMP`, which exercises the same
parseBlockBinary path the bump-builder runs. This verifies our format
is wire-compatible with both arcade (unchanged) and merkle-service
(model.NewBlockFromBytes uses identical byte layout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): in-process arcade runtime + poll helpers
The harness now boots arcade via the same `app.Bootstrap` →
`app.BuildServices` path the production binary uses, with config
overridden in-memory:
- regtest network (chaintracks auto-disabled)
- pebble store on a t.TempDir()
- in-process memory kafka broker
- merkle_service.url = merkle-service container's host-mapped URL
- callback_url = host.docker.internal so the container can reach back
- datahub_urls seeded with the harness datahub fake
- p2p bootstrap_peers = harness libp2p multiaddr (datahub_discovery off)
- callback.allow_private_ips = true (RFC1918 loopback by definition)
`StartArcade(t, opts)` returns an ArcadeRuntime exposing the bound
port, host-side base URL, and merkle-service-reachable host URL. All
services start in goroutines bound to the test context; t.Cleanup
cancels and waits with a 15s bound.
`poll.go` adds the helpers later steps lean on:
- BroadcastTx: POST /tx with raw bytes; returns server-side txid.
- GetTxStatus / WaitForMined: poll /tx/:txid until every supplied
txid reaches MINED with a non-empty merklePath, or fail with the
first stuck txid's last-observed status.
`TestArcadeRuntime_BootsAndServesHealth` brings up the full stack —
containers + libp2p + datahub + arcade — and asserts /health returns
200 with the harness datahub URL listed as healthy. Verified against
rootless podman in ~5s warm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): smoke test verifies tx → arcade → merkle-service /watch
The first cross-service smoke scenario. It boots the full harness
(Postgres + Redpanda + merkle-service container, plus in-process
libp2p host, datahub fake, and arcade), submits a single transaction
to arcade via POST /tx, and asserts the txid lands in
merkle-service's registration store via GET /api/lookup/<txid>.
That walks every piece of wiring this PR adds:
- arcade's tx-validator parses + structurally validates the tx
- propagation calls merkleClient.Register over HTTP
- merkle-service inside its container reaches Postgres and persists
the (txid, callbackUrl, callbackToken) entry
Adds harness.BuildValidatableTxs which produces the minimum-valid
transactions arcade's structural validator accepts (1 input + 1
output + non-data scripts + LockTime-driven txid uniqueness).
TxBuilder unit tests confirm the validator path on these synthetic
txs without any container involvement.
Round-trips cleanly against rootless podman in ~5s warm. Full MINED
status (which requires merkle-tree-valid block construction so
arcade's ValidateCompoundRoot accepts the compound BUMP) is the
next step — tracked as a gap in tests/e2e/MERKLE_SERVICE_GAPS.md
in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(e2e): wire smoke suite as required PR gate, document harness
Adds .github/workflows/e2e-smoke.yml — pre-pulls the merkle-service
image so registry latency stays out of the test's wait budgets,
disables Ryuk (flaky on rootless container runtimes), and runs the
full e2e suite under the e2e build tag with a 20-min hard timeout.
Triggers on every PR and push to main; configured to be the required
gate via repo branch-protection settings.
tests/e2e/README.md documents the local-run recipe for both Docker
and rootless podman (DOCKER_HOST socket override), the file layout,
and how to add a new scenario.
tests/e2e/MERKLE_SERVICE_GAPS.md captures six friction points the
harness work surfaced — backend-import drift, mandatory external
Kafka, private-IP guard defaults, health-endpoint signal aliasing,
no deterministic block-replay, and missing private-network docs —
each with a suggested fix and a note about how the harness works
around it today. Worth filing upstream against /git/merkle-service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix linting
* Lint and test fixes
* docs(e2e): document rootless podman + pasta networking limitation
The harness now reaches the host from inside containers via the
docker bridge gateway IP (auto-discovered) with host.docker.internal
as a fallback. README updates to reflect the new flow:
- Document the gateway-IP-first announce strategy and the
host.docker.internal fallback.
- Call out the rootless-podman + pasta limitation: when pasta runs
with --no-map-gw (the security-conscious default on recent
Fedora/Ubuntu), the host is not reachable from inside containers
via either the gateway IP or host.docker.internal. Tests that
require merkle-service to dial back into the harness will time
out under this configuration. CI uses Docker so the required PR
gate is unaffected.
- Add a one-liner reachability check developers can run before
trying the libp2p-peering tests locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Run go mod tidy
* Potential fix for pull request finding 'CodeQL / Incorrect conversion between integer types'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>1 parent 9fb01f2 commit 18e181e
42 files changed
Lines changed: 4506 additions & 275 deletions
File tree
- .github/workflows
- app
- cmd/arcade
- models
- services
- api_server
- bump_builder
- webhook
- store
- aerospike
- pebble
- postgres
- tests/e2e
- harness
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
62 | | - | |
| 61 | + | |
63 | 62 | | |
64 | 63 | | |
65 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
0 commit comments