Skip to content

fix(blockchain/peer-registry): post-#832 follow-ups#988

Merged
oskarszoon merged 4 commits into
bsv-blockchain:mainfrom
oskarszoon:feat/peer-registry-cleanup
Jun 2, 2026
Merged

fix(blockchain/peer-registry): post-#832 follow-ups#988
oskarszoon merged 4 commits into
bsv-blockchain:mainfrom
oskarszoon:feat/peer-registry-cleanup

Conversation

@oskarszoon

Copy link
Copy Markdown
Contributor

Summary

Six small follow-ups deferred from #832, all touching the same files while context is fresh:

# Item Source
1 bytesToBlockHash returns (Hash, error); protoToPeerInfo takes a ulogger.Logger; PeerRegistryClient.SetLogger plumbed via daemon_stores Siggi #6 (os.Stderr bypasses logger)
2 StartPeriodicSave moves the save loop onto the registry's WaitGroup so Close() drains it Siggi #3 (periodic save not joined)
3 persistedRegistry.Version is enforced — reject Version > persistedRegistryVersion Siggi #4 (Version parsed but unused)
4 AddBanScore param renamed pointsdefaultPoints; comment clarifies "config overrides caller" Siggi #5 (misleading comment)
5 SetLogger / log() use atomic.Value instead of mismatched lock discipline Copilot nit 1
6 Sidecar archive key uses UnixNano instead of Unix Copilot nit 2

#5 (cleanup goroutine driver from the original plan) was already shipped in #832StartCleanup is wired in services/blockchain/Server.go.

Test plan

  • go vet ./services/blockchain/... ./services/p2p/... ./daemon/... — clean
  • go test -race ./services/blockchain/... ./services/p2p/... ./daemon/... — 1162 pass
  • New tests:
    • TestPersistence_RejectsFutureVersion
    • TestRegistry_SetLoggerNoRace
    • TestRegistry_SetLoggerNilIsNoop
    • TestPersistence_CorruptBlobArchivedToSidecar (updated for nanosecond)
    • TestBlockchain_SavePeerRegistryPeriodically (rewritten around StartPeriodicSave + Close drain)
    • TestGRPC_BlockHashBytes* (updated for new error return)

Refs #832.

Six small follow-ups from Siggi's re-review (bsv-blockchain#832) and Copilot nits left
explicitly deferred for a follow-up PR.

1. bytesToBlockHash no longer writes to os.Stderr (peer_registry_grpc.go).
   Signature is now (Hash, error); protoToPeerInfo accepts a ulogger.Logger
   and surfaces the error via the structured logger. PeerRegistryClient
   gains a SetLogger method mirroring the registry's, plumbed from
   daemon_stores so production decoder warnings route through ulogger.

2. StartPeriodicSave moves the savePeerRegistryPeriodically loop onto
   the registry's own WaitGroup (peer_registry.go). Close() now drains
   it alongside ban-decay and TTL cleanup; tickers after Stop are
   impossible. The Server.go wrapper is gone.

3. persistedRegistry.Version is enforced
   (peer_registry_persistence.go). A blob whose Version is newer than
   persistedRegistryVersion is rejected with a clear error rather than
   silently dropping unknown fields on downgrade.

4. AddBanScore renames the param points -> defaultPoints
   (peer_registry.go). The comment now states explicitly that
   banConfig.ReasonPoints overrides the caller's value for configured
   reasons. Interface and gRPC client follow.

5. SetLogger / log() use atomic.Value
   (peer_registry.go). Previously the field was written under r.mu and
   read without; production flow happens to be single-call-then-many-
   reads, but the asymmetry was a footgun if SetLogger ever becomes
   runtime-reconfigurable. Switched to atomic.Value via a loggerHolder
   wrapper so concurrent log() readers are race-free without taking
   r.mu.

6. Sidecar archive key uses UnixNano
   (peer_registry_persistence.go). Second granularity made two
   corruption events in the same second collide via
   WithAllowOverwrite=true.

Tests:
- TestPersistence_RejectsFutureVersion (Version gate)
- TestRegistry_SetLoggerNoRace / _NilIsNoop (atomic logger)
- TestPersistence_CorruptBlobArchivedToSidecar updated for nanosecond
- TestBlockchain_SavePeerRegistryPeriodically rewritten around
  StartPeriodicSave + Close drain
- TestGRPC_BlockHashBytes* now check the new (h, err) shape

go vet clean. 1162 tests pass with -race across blockchain, p2p, daemon.
@oskarszoon oskarszoon requested review from galt-tr, icellan and ordishs May 29, 2026 16:24
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Summary

This PR successfully implements 6 post-#832 follow-ups with solid test coverage and careful attention to concurrency correctness.

Key changes verified:

  1. bytesToBlockHash now returns (Hash, error); protoToPeerInfo logs invalid hashes
  2. StartPeriodicSave properly tracked on registry WaitGroup
  3. Version enforcement added with saveDisabled protection
  4. AddBanScore parameter renamed points → defaultPoints with clear docs
  5. Logger uses atomic.Value for race-free concurrent access
  6. Sidecar key uses UnixNano for collision resistance

Test coverage: 1162 tests pass under -race, including new tests for all major changes.

No issues found. The implementation is clean, well-documented, and follows project conventions.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-988 (d38534c)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.928µ 1.619µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.42n 71.15n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.24n 71.44n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 71.22n 71.21n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.58n 33.15n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 55.60n 55.80n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 126.6n 127.4n ~ 0.700
MiningCandidate_Stringify_Short-4 218.2n 218.0n ~ 0.700
MiningCandidate_Stringify_Long-4 1.629µ 1.624µ ~ 0.400
MiningSolution_Stringify-4 843.4n 845.9n ~ 0.700
BlockInfo_MarshalJSON-4 1.715µ 1.720µ ~ 1.000
NewFromBytes-4 124.7n 123.9n ~ 0.400
AddTxBatchColumnar_Validation-4 2.484µ 2.385µ ~ 0.100
OffsetValidationLoop-4 546.8n 720.8n ~ 0.100
Mine_EasyDifficulty-4 67.19µ 66.97µ ~ 0.200
Mine_WithAddress-4 7.075µ 6.993µ ~ 0.700
BlockAssembler_AddTx-4 0.01675n 0.01658n ~ 0.700
AddNode-4 7.636 7.889 ~ 0.700
AddNodeWithMap-4 7.478 7.482 ~ 0.700
DirectSubtreeAdd/4_per_subtree-4 59.52n 60.21n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.81n 28.97n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.75n 27.63n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 26.46n 26.49n ~ 0.600
DirectSubtreeAdd/2048_per_subtree-4 26.10n 26.07n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 293.1n 288.1n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 286.6n 285.2n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 289.2n 288.3n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 282.4n 280.7n ~ 0.700
SubtreeProcessorAdd/2048_per_subtree-4 283.9n 280.5n ~ 0.200
SubtreeProcessorRotate/4_per_subtree-4 285.6n 284.4n ~ 0.700
SubtreeProcessorRotate/64_per_subtree-4 281.1n 279.2n ~ 0.800
SubtreeProcessorRotate/256_per_subtree-4 276.8n 279.8n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 277.0n 276.9n ~ 1.000
SubtreeNodeAddOnly/4_per_subtree-4 54.79n 54.76n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 35.95n 35.97n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 35.02n 34.90n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 34.41n 34.42n ~ 0.700
SubtreeCreationOnly/4_per_subtree-4 109.6n 108.3n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 344.1n 343.4n ~ 0.100
SubtreeCreationOnly/256_per_subtree-4 1.218µ 1.206µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 3.766µ 3.773µ ~ 0.800
SubtreeCreationOnly/2048_per_subtree-4 6.738µ 6.704µ ~ 0.200
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 277.7n 281.0n ~ 0.400
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 275.6n 281.3n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 1.990m 1.980m ~ 0.400
ParallelGetAndSetIfNotExists/10k_nodes-4 5.173m 5.216m ~ 0.200
ParallelGetAndSetIfNotExists/50k_nodes-4 7.074m 7.206m ~ 0.200
ParallelGetAndSetIfNotExists/100k_nodes-4 9.534m 9.561m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 1.769m 1.788m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 4.409m 4.384m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 13.47m 13.59m ~ 0.400
SequentialGetAndSetIfNotExists/100k_nodes-4 24.77m 24.83m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.075m 2.073m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.440m 8.338m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.16m 13.14m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.790m 1.826m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.962m 8.002m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 42.88m 42.99m ~ 0.400
DiskTxMap_SetIfNotExists-4 4.485µ 4.624µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 4.002µ 3.953µ ~ 1.000
DiskTxMap_ExistenceOnly-4 411.8n 421.7n ~ 0.200
Queue-4 206.0n 204.4n ~ 0.200
AtomicPointer-4 3.646n 3.650n ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 925.9µ 954.7µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 849.2µ 848.9µ ~ 1.000
ReorgOptimizations/AllMarkFalse/Old/10K-4 130.3µ 120.8µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 64.88µ 64.11µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 65.48µ 69.77µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/10K-4 11.44µ 11.01µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 5.327µ 5.298µ ~ 0.400
ReorgOptimizations/NodeFlags/New/10K-4 2.450µ 2.104µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 11.532m 9.972m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 11.11m 10.29m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.169m 1.153m ~ 0.400
ReorgOptimizations/AllMarkFalse/New/100K-4 709.7µ 707.3µ ~ 1.000
ReorgOptimizations/HashSlicePool/Old/100K-4 575.3µ 612.2µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 212.5µ 217.1µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/100K-4 50.69µ 50.70µ ~ 1.000
ReorgOptimizations/NodeFlags/New/100K-4 17.70µ 17.60µ ~ 1.000
TxMapSetIfNotExists-4 49.72n 49.96n ~ 0.200
TxMapSetIfNotExistsDuplicate-4 41.57n 41.27n ~ 0.300
ChannelSendReceive-4 610.9n 649.0n ~ 0.100
CalcBlockWork-4 508.0n 506.0n ~ 0.700
CalculateWork-4 695.1n 692.7n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.551µ 1.711µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 14.08µ 20.42µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 145.0µ 155.1µ ~ 0.700
CatchupWithHeaderCache-4 105.4m 105.7m ~ 0.400
_BufferPoolAllocation/16KB-4 4.178µ 4.702µ ~ 0.700
_BufferPoolAllocation/32KB-4 10.390µ 8.847µ ~ 0.400
_BufferPoolAllocation/64KB-4 19.37µ 17.03µ ~ 0.100
_BufferPoolAllocation/128KB-4 39.36µ 33.52µ ~ 0.100
_BufferPoolAllocation/512KB-4 137.9µ 125.5µ ~ 0.100
_BufferPoolConcurrent/32KB-4 24.21µ 20.18µ ~ 0.100
_BufferPoolConcurrent/64KB-4 36.72µ 33.75µ ~ 0.700
_BufferPoolConcurrent/512KB-4 155.6µ 160.7µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/16KB-4 645.0µ 744.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 667.7µ 735.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 660.2µ 713.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 651.5µ 723.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 673.0µ 682.9µ ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.49m 38.13m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.23m 37.97m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.44m 38.04m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.36m 38.17m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 37.58m 37.75m ~ 0.400
_PooledVsNonPooled/Pooled-4 743.8n 744.3n ~ 0.700
_PooledVsNonPooled/NonPooled-4 9.150µ 8.600µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.713µ 7.061µ ~ 0.200
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.974µ 11.856µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.637µ 10.198µ ~ 0.100
_prepareTxsPerLevel-4 402.0m 400.3m ~ 1.000
_prepareTxsPerLevelOrdered-4 4.217m 3.982m ~ 0.400
_prepareTxsPerLevel_Comparison/Original-4 403.0m 405.3m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 4.193m 3.743m ~ 0.700
SubtreeSizes/10k_tx_4_per_subtree-4 1.299m 1.287m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 308.2µ 302.2µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 73.31µ 72.41µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 18.03µ 18.09µ ~ 0.400
SubtreeSizes/10k_tx_512_per_subtree-4 8.989µ 8.912µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.439µ 4.444µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.195µ 2.195µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 70.64µ 70.34µ ~ 0.700
BlockSizeScaling/10k_tx_256_per_subtree-4 17.81µ 17.73µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.458µ 4.405µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 373.8µ 372.5µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 88.69µ 87.60µ ~ 0.200
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.73µ 21.84µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 151.4µ 150.4µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 160.2µ 159.8µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 310.0µ 313.4µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 8.889µ 8.858µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.338µ 9.477µ ~ 0.200
SubtreeAllocations/medium_subtrees_full_validation-4 17.69µ 17.74µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.086µ 2.083µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.289µ 2.210µ ~ 0.200
SubtreeAllocations/large_subtrees_full_validation-4 4.454µ 4.380µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 168.4µ 168.5µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 169.7µ 170.8µ ~ 0.700
GetUtxoHashes-4 210.1n 210.5n ~ 1.000
GetUtxoHashes_ManyOutputs-4 36.69µ 36.64µ ~ 0.700
_NewMetaDataFromBytes-4 165.9n 170.5n ~ 0.100
_Bytes-4 309.3n 314.0n ~ 0.400
_MetaBytes-4 106.6n 109.1n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-06-01 16:15 UTC

Comment thread services/blockchain/peer_registry_client.go
ordishs

This comment was marked as outdated.

…ockers

Two blockers from the PR bsv-blockchain#988 review, plus the two non-blocking nits
called out alongside.

## Blocker 1 — Sidecar test was platform-dependent

The previous TestPersistence_CorruptBlobArchivedToSidecar probed the
keyspace at 1µs steps to find the archived blob. macOS's UnixNano() is
microsecond-aligned (mod 1000 == 0), so the loop happened to land on the
real key. Linux returns true-ns timestamps, which means the loop misses
~99.9% of the time. The test would have failed in CI.

Fix: surface the archive key from the registry. CentralizedPeerRegistry
now records the most recent sidecar key in an atomic.Pointer[string],
exposed via LastCorruptArchiveKey(). The test reads the key directly
instead of guessing it from wall-clock bounds — no more clock-resolution
dependency. Also useful for operator post-mortems (the key can be
exposed via gRPC / diagnostics later).

## Blocker 2 — Version rejection was undone by the next save

Load returned an error on future-version blobs but Server.go only
logged Warnf and continued. StartPeriodicSave still launched. On the
first tick (or the final shutdown Save), an empty Version=1 envelope
overwrote the operator's future-version data via
WithAllowOverwrite=true. The version check was therefore documentation,
not protection.

Fix: registry-level saveDisabled atomic.Bool. Load detects
errFutureRegistryVersion and latches the flag before returning. Save
becomes a no-op when saveDisabled is set; StartPeriodicSave refuses to
start. saveDisabled is one-way — clearing it requires constructing a
fresh registry, which is the correct contract (the operator must
explicitly decide whether to roll forward or restore a backup).

TestPersistence_FutureVersionDisablesPersistence verifies the end-to-end
behaviour: the original bytes on disk are unchanged after Load, Save,
and StartPeriodicSave + Close.

## Nit 1 — PeerRegistryClient.logger now atomic.Value

Matches the hardening applied to CentralizedPeerRegistry in bsv-blockchain#988. log()
runs on every GetPeer / ListPeers RPC, so a plain field + post-
construction SetLogger was the same race shape that motivated the
registry change. Same loggerHolder wrapper, same nil-no-op semantics,
plus TestPeerRegistryClient_SetLoggerNoRace as a -race guardrail.

## Nit 2 — GetPeerRegistryClient guarded by sync.Once

Pre-existing race in the singleton init: nil-check + assign without a
mutex meant two concurrent first-callers could each create a client
(and only one would be retained). Now serialized by Stores.peerRegistryClientOnce.
Error path stored on Stores.peerRegistryClientErr so the Once contract
is preserved (the function is called once; the error is replayed). Other
singleton getters in this file have the same shape but are out of scope
for this PR.

Tests:
- TestPersistence_CorruptBlobArchivedToSidecar rewritten around
  LastCorruptArchiveKey (Linux-safe).
- TestPersistence_FutureVersionDisablesPersistence: end-to-end blob
  protection.
- TestPeerRegistryClient_SetLoggerNoRace and _NilIsNoop.

go vet clean. Tests pass with -race on services/blockchain,
services/p2p, daemon.
@oskarszoon oskarszoon requested a review from ordishs May 29, 2026 18:55

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve with minor comments.

Disciplined, well-tested cleanup — minimal diffs, every change paired with a test, build is clean. The version-gating (saveDisabled one-way latch) closes a real data-loss hole, and the UnixNano + LastCorruptArchiveKey() test seam is the right fix for the macOS-vs-Linux clock flakiness.

A few minor things to consider (none blocking):

  1. sync.Once latches the init error permanently (daemon/daemon_stores.go). The rewrite caches peerRegistryClientErr for the process lifetime; the old code retried NewPeerRegistryClient on every call after a failure. Risk is low since GetGRPCClient builds a lazy gRPC conn (fails only on permanent conditions like a malformed address), but if it can ever fail transiently during a startup race, that's a changed recovery semantic. Worth a one-line comment noting the intentional permanent caching, or confirming GetGRPCClient never fails transiently.

  2. protoToPeerInfo nil-logger tolerance (peer_registry_grpc.go). The err != nil && logger != nil guard means an invalid BlockHash arriving via a nil-logger path is silently dropped — the same stderr-bypass problem this PR fixes, just relocated. Not a regression (prod callers pass real loggers); consider passing ulogger.TestLogger{} in tests and dropping the nil tolerance so it's impossible by construction.

  3. Nit: TestPersistence_RejectsFutureVersion is a subset of TestPersistence_FutureVersionDisablesPersistence. Both are cheap and the narrower one localizes failures, so keeping both is fine — just noting the overlap.

@sonarqubecloud

sonarqubecloud Bot commented Jun 1, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
78.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@oskarszoon oskarszoon merged commit 2b49c99 into bsv-blockchain:main Jun 2, 2026
34 checks passed
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.

3 participants