Skip to content

feat(p2p): advertise fee policy in node_status message#943

Merged
icellan merged 4 commits into
bsv-blockchain:mainfrom
icellan:feat/p2p-status-fee-policy
May 26, 2026
Merged

feat(p2p): advertise fee policy in node_status message#943
icellan merged 4 commits into
bsv-blockchain:mainfrom
icellan:feat/p2p-status-fee-policy

Conversation

@icellan

@icellan icellan commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds optional FeePolicy field to NodeStatusMessage so peers can read each other's mining-fee rate, max script/tx size, and max sigops/tx directly from the P2P node_status topic — no out-of-band /v1/policy HTTP call needed.
  • Shape mirrors the ARC /v1/policy response, minus StandardFormatSupported (an ARC transport flag, not a Teranode policy).
  • Wire-compatible: new field uses omitempty; legacy min_mining_tx_fee is unchanged. Older peers and dashboards continue to parse status messages unchanged.

Why

Issue #4639 asks for the broader fee policy to be advertised over P2P. Today only a single MinMiningTxFee scalar is exposed, which is not enough for peers to make informed relay decisions (e.g., skip a node that won't accept a tx's size or sigop count).

Implementation notes

  • FeePolicy is defined in services/p2p/message_types.go (package-local — no shared package introduced; the asset HTTP Policy type is independent and untouched).
  • policyFromSettings() performs the BSV/kB → sat/kB conversion with the same float bounds check used by services/asset/httpimpl/get_policy.go, returning nil on out-of-range / negative configs rather than aborting node_status publication.
  • New field is populated by getNodeStatusMessage, published by handleNodeStatusNotification, and relayed to local WebSocket subscribers by handleNodeStatusTopic.
  • The 64KB pubsub message cap is unaffected (~150 bytes added).

Test plan

  • go build ./... — clean
  • go vet ./services/p2p/... — clean
  • go test -race ./services/p2p/... ./services/asset/httpimpl/... — pass
  • golangci-lint run ./services/p2p/... — no new findings (pre-existing prealloc warning in mock.go untouched)
  • staticcheck ./services/p2p/... — clean
  • New TestGetNodeStatusMessage subtests cover both Policy-set and Policy-nil branches
  • New JSON round-trip + backward-compat tests in services/p2p/fee_policy_test.go verify wire format, omitempty behaviour, and parsing of legacy payloads without fee_policy
  • Manual end-to-end check on a 2-node devnet (optional; consumers of the field are a follow-up)

Closes #4639

Adds an optional FeePolicy field to NodeStatusMessage so peers can learn each
other's mining-fee rate, max script/tx size, and max sigops/tx without an
out-of-band HTTP call to /v1/policy. Shape mirrors the ARC policy response
(minus StandardFormatSupported, which is an ARC transport flag).

Backward compatible: the field uses omitempty and the legacy
min_mining_tx_fee field is unchanged, so older peers and dashboards continue
to parse status messages without changes. policyFromSettings carries the
BSV/kB to sat/kB conversion and bounds check, returning nil on misconfigured
fees rather than aborting status publication.

Closes #4639
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


All previously identified issues have been addressed through iterative commits:

History:

  • ✅ Fixed: Negative value validation added for MaxScriptSizePolicy, MaxTxSizePolicy, MaxTxSigopsCountsPolicy (commit 4823715)
  • ✅ Fixed: NaN/Inf validation added for MinMiningTxFee (commit 4823715)
  • ✅ Fixed: MinMiningTxFee and FeePolicy fields now consistent - both omitted when policy invalid (commit 8bf8571)
  • ✅ Fixed: Invalid policy warning rate-limited using sync.Once to prevent log spam (commit 8bf8571)

Current Review:
No new issues found. The implementation correctly:

  • Validates all policy values before type conversion (message_types.go:38-48)
  • Maintains consistency between legacy and new fields (Server.go:1283-1296)
  • Provides comprehensive test coverage including edge cases (NaN, Inf, negative values)
  • Maintains backward compatibility with older peers via omitempty

Comment thread services/p2p/message_types.go

Copilot AI left a comment

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.

Pull request overview

This PR extends the P2P node_status message to optionally include a richer fee/policy payload (fee_policy) so peers can learn each other’s mining fee rate and key policy limits without requiring an HTTP /v1/policy call.

Changes:

  • Adds FeePolicy (ARC-shaped) to NodeStatusMessage and the internal notificationMsg, wiring it through P2P publish/receive and local WebSocket fanout.
  • Introduces policyFromSettings() to build the advertised policy object from local settings.PolicySettings.
  • Adds unit tests for policyFromSettings(), JSON wire compatibility/omitempty, and getNodeStatusMessage population behavior.

Reviewed changes

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

Show a summary per file
File Description
services/p2p/Server.go Populates and forwards the new FeePolicy field through node status publication and WebSocket notification paths.
services/p2p/server_handler_test.go Adds getNodeStatusMessage tests covering FeePolicy present/absent scenarios.
services/p2p/message_types.go Defines FeePolicy/FeeAmount, adds policyFromSettings(), and extends NodeStatusMessage with fee_policy.
services/p2p/HandleWebsocket.go Extends WebSocket notification payload with fee_policy.
services/p2p/fee_policy_test.go Adds tests for conversion logic and JSON backward/forward compatibility for the new field.

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

Comment thread services/p2p/message_types.go
Comment thread services/p2p/message_types.go Outdated
Comment thread services/p2p/Server.go Outdated
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-943 (6eb87d8)

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.581µ 1.579µ ~ 0.400
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.29n 71.78n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.55n 71.11n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 71.11n 71.00n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.06n 31.99n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 55.36n 54.12n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 125.8n 127.9n ~ 0.100
MiningCandidate_Stringify_Short-4 220.3n 216.5n ~ 0.100
MiningCandidate_Stringify_Long-4 1.607µ 1.600µ ~ 0.300
MiningSolution_Stringify-4 846.6n 840.3n ~ 0.100
BlockInfo_MarshalJSON-4 1.709µ 1.713µ ~ 0.300
NewFromBytes-4 94.48n 94.14n ~ 1.000
AddTxBatchColumnar_Validation-4 1.842µ 1.981µ ~ 0.100
OffsetValidationLoop-4 556.4n 561.0n ~ 0.700
Mine_EasyDifficulty-4 60.14µ 60.53µ ~ 0.100
Mine_WithAddress-4 7.259µ 6.635µ ~ 0.100
BlockAssembler_AddTx-4 0.02731n 0.02729n ~ 0.700
AddNode-4 11.29 10.79 ~ 0.400
AddNodeWithMap-4 12.97 11.87 ~ 0.700
DirectSubtreeAdd/4_per_subtree-4 56.63n 57.90n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 28.61n 28.77n ~ 1.000
DirectSubtreeAdd/256_per_subtree-4 27.17n 27.60n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 26.19n 26.09n ~ 0.700
DirectSubtreeAdd/2048_per_subtree-4 25.93n 25.70n ~ 0.200
SubtreeProcessorAdd/4_per_subtree-4 283.0n 285.6n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 281.2n 280.6n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 280.1n 280.7n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 270.6n 273.6n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 270.9n 274.7n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 276.9n 280.1n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 276.2n 281.0n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 275.1n 275.2n ~ 1.000
SubtreeProcessorRotate/1024_per_subtree-4 272.5n 273.4n ~ 0.200
SubtreeNodeAddOnly/4_per_subtree-4 54.59n 54.41n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 34.45n 34.40n ~ 0.700
SubtreeNodeAddOnly/256_per_subtree-4 33.37n 33.43n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 32.69n 32.58n ~ 0.200
SubtreeCreationOnly/4_per_subtree-4 114.8n 114.1n ~ 0.700
SubtreeCreationOnly/64_per_subtree-4 402.5n 397.2n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.344µ 1.354µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.466µ 4.454µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 8.120µ 8.247µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 272.4n 274.3n ~ 0.200
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 273.8n 274.4n ~ 0.200
ParallelGetAndSetIfNotExists/1k_nodes-4 2.186m 2.182m ~ 1.000
ParallelGetAndSetIfNotExists/10k_nodes-4 5.257m 5.288m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.134m 7.472m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 9.665m 10.095m ~ 0.200
SequentialGetAndSetIfNotExists/1k_nodes-4 1.939m 1.965m ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 4.420m 4.355m ~ 0.400
SequentialGetAndSetIfNotExists/50k_nodes-4 13.16m 12.83m ~ 0.400
SequentialGetAndSetIfNotExists/100k_nodes-4 23.44m 22.61m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.240m 2.239m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.107m 8.141m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.08m 13.01m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.982m 2.001m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.388m 7.548m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 40.15m 40.98m ~ 0.200
DiskTxMap_SetIfNotExists-4 5.648µ 5.641µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 5.099µ 5.070µ ~ 1.000
DiskTxMap_ExistenceOnly-4 472.7n 471.1n ~ 0.400
Queue-4 221.9n 226.0n ~ 0.200
AtomicPointer-4 8.161n 8.164n ~ 0.300
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 987.3µ 991.8µ ~ 0.700
ReorgOptimizations/DedupFilterPipeline/New/10K-4 930.0µ 905.9µ ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/10K-4 141.5µ 136.9µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 58.48µ 58.61µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 75.43µ 67.57µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.86µ 11.85µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 6.899µ 6.184µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 2.660µ 2.077µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 19.56m 19.95m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 18.49m 19.21m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.420m 1.462m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 730.1µ 735.3µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 758.7µ 786.2µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/100K-4 361.4µ 341.6µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 54.80µ 56.58µ ~ 0.200
ReorgOptimizations/NodeFlags/New/100K-4 18.48µ 19.00µ ~ 0.100
TxMapSetIfNotExists-4 52.98n 52.19n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 48.23n 48.35n ~ 0.200
ChannelSendReceive-4 701.5n 707.2n ~ 0.100
CalcBlockWork-4 456.1n 463.4n ~ 0.100
CalculateWork-4 618.4n 647.6n ~ 0.400
BuildBlockLocatorString_Helpers/Size_10-4 1.648µ 1.653µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_100-4 12.86µ 12.99µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 125.5µ 126.6µ ~ 0.100
CatchupWithHeaderCache-4 103.8m 103.9m ~ 0.400
_prepareTxsPerLevel-4 406.8m 415.7m ~ 0.100
_prepareTxsPerLevelOrdered-4 3.522m 3.813m ~ 0.400
_prepareTxsPerLevel_Comparison/Original-4 407.3m 416.2m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 3.402m 3.712m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.350m 1.353m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 318.6µ 323.0µ ~ 0.400
SubtreeSizes/10k_tx_64_per_subtree-4 75.36µ 77.40µ ~ 0.400
SubtreeSizes/10k_tx_256_per_subtree-4 18.71µ 19.03µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.351µ 9.374µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.616µ 4.722µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.285µ 2.329µ ~ 0.400
BlockSizeScaling/10k_tx_64_per_subtree-4 74.26µ 75.65µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 19.16µ 18.86µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.721µ 4.700µ ~ 0.400
BlockSizeScaling/50k_tx_64_per_subtree-4 401.1µ 402.6µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 94.12µ 94.95µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.90µ 23.08µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 160.2µ 156.1µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 164.4µ 164.1µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 321.0µ 321.1µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.294µ 9.156µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.632µ 9.537µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 18.68µ 18.63µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.222µ 2.211µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.328µ 2.354µ ~ 0.200
SubtreeAllocations/large_subtrees_full_validation-4 4.657µ 4.742µ ~ 0.100
_BufferPoolAllocation/16KB-4 3.668µ 3.835µ ~ 0.100
_BufferPoolAllocation/32KB-4 7.117µ 10.528µ ~ 0.100
_BufferPoolAllocation/64KB-4 17.96µ 16.85µ ~ 0.700
_BufferPoolAllocation/128KB-4 24.19µ 29.79µ ~ 0.100
_BufferPoolAllocation/512KB-4 93.85µ 88.88µ ~ 0.700
_BufferPoolConcurrent/32KB-4 17.60µ 18.83µ ~ 0.200
_BufferPoolConcurrent/64KB-4 27.81µ 30.59µ ~ 0.100
_BufferPoolConcurrent/512KB-4 138.8µ 152.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 693.5µ 618.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 683.3µ 613.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 664.8µ 605.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 584.1µ 594.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 591.7µ 605.1µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.54m 37.16m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.19m 36.58m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.33m 36.58m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.23m 36.29m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.76m 35.76m ~ 0.700
_PooledVsNonPooled/Pooled-4 834.2n 831.3n ~ 0.100
_PooledVsNonPooled/NonPooled-4 7.035µ 8.050µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.901µ 7.334µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.181µ 9.396µ ~ 0.400
_MemoryFootprint/Alternative_64KB_32concurrent-4 8.769µ 9.305µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 344.8µ 337.5µ ~ 0.200
StoreBlock_Sequential/AboveCSVHeight-4 337.1µ 336.6µ ~ 1.000
GetUtxoHashes-4 255.8n 261.4n ~ 0.600
GetUtxoHashes_ManyOutputs-4 42.66µ 42.65µ ~ 0.700
_NewMetaDataFromBytes-4 227.2n 228.1n ~ 0.100
_Bytes-4 402.3n 397.1n ~ 0.100
_MetaBytes-4 137.6n 138.0n ~ 0.500

Threshold: >10% with p < 0.05 | Generated: 2026-05-26 10:40 UTC

Tightens policyFromSettings to reject:
- NaN or +/-Inf MinMiningTxFee (the previous < 0 / > MaxUint64 check is
  always false for NaN, so a NaN fee silently cast to a wrong uint64).
- Negative MaxScriptSizePolicy / MaxTxSizePolicy / MaxTxSigopsCountsPolicy
  (signed-to-uint64 cast would wrap to a huge advertised value).

Also harmonises the legacy min_mining_tx_fee field with fee_policy in
getNodeStatusMessage: when policyFromSettings rejects the policy, both
fields are omitted, so consumers can never see an invalid scalar fee
alongside an absent fee_policy.

Addresses review feedback on PR bsv-blockchain#943.

Copilot AI left a comment

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.

Pull request overview

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

Comment thread services/p2p/Server.go
@icellan icellan requested a review from galt-tr May 26, 2026 10:02
icellan and others added 2 commits May 26, 2026 12:19
The status publisher fires every ~10s, so the previous Warnf for an
invalid policy config would flood the logs on a misconfigured node.
Gate the warning behind sync.Once so it's emitted at most once per
process, and demote subsequent occurrences to Debugf. Also broaden
the message to cover the out-of-uint64-range MinMiningTxFee branch
in policyFromSettings.

Addresses follow-up review feedback on PR bsv-blockchain#943.
@sonarqubecloud

Copy link
Copy Markdown

@galt-tr galt-tr left a comment

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.

LGTM

@icellan icellan merged commit bb7e4f0 into bsv-blockchain:main May 26, 2026
25 checks passed
@ordishs

ordishs commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Now that this is merged, a few non-blocking cleanup notes from the review for a follow-up PR if anyone picks this up:

1. Consistency comment is partially true (services/p2p/Server.go:1281)

The new code says "Keep legacy and new fields consistent: only advertise the scalar fee when the full policy is valid." That holds inside the s.settings.Policy != nil branch, but the outer else (Policy entirely absent) still sets minMiningTxFee = &0 while feePolicy stays nil — so the invariant doesn't hold there. Either scope the comment ("when policy is present but invalid") or drop the legacy &0 default to make it actually consistent. The current FeePolicy is nil when policy settings are absent test doesn't assert anything about MinMiningTxFee, so the gap is uncovered.

2. Missing MaxUint64 boundary test (services/p2p/fee_policy_test.go)

policyFromSettings guards feeInSatoshis > float64(math.MaxUint64) but no test exercises it. Trivial to add with MinMiningTxFee: 1e20.

3. Type name is narrow (services/p2p/message_types.go:18)

FeePolicy carries MaxScriptSizePolicy, MaxTxSizePolicy, and MaxTxSigopsCountsPolicy in addition to the fee — not strictly a fee policy. ARC calls this just Policy. Since httpimpl already owns a Policy type, something like NodePolicy or RelayPolicy would read more accurately.

4. Document the "0 = unlimited" convention

For MaxTxSigopsCountsPolicy (and the size policies if zeroed), the default 0 means unlimited per settings/policy_settings.go:13. A peer receiving maxtxsigopscountspolicy: 0 has no way to disambiguate "reject all" from "unlimited" without prior knowledge. Worth a one-liner on the struct documenting this — matches ARC semantics, but explicit is better.

5. Document the mixed JSON-key convention

NodeStatusMessage uses snake_case keys; the nested FeePolicy uses ARC's lowercase concatenated form (maxscriptsizepolicy, miningFee). Worth a comment so future contributors don't "fix" the inconsistency.

6. Conversion-logic duplication

policyFromSettings duplicates the BSV/kB → sat/kB conversion from services/asset/httpimpl/get_policy.go:47-51. Fine for two callers; if a third appears, hoist to settings.PolicySettings.MiningFeeAsSatPerKB() (uint64, error).

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.

5 participants