Skip to content

Commit c38b37d

Browse files
authored
Merge pull request #96 from frstrtr/btc/v36-parity-network-prefix-independence
btc/v36: network-id/prefix independence (#95 BTC mirror)
2 parents ee906e8 + 3bc7ec9 commit c38b37d

12 files changed

Lines changed: 1616 additions & 28 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
test_mweb_builder \
6868
test_address_resolution test_compute_share_target \
6969
test_utxo \
70+
v37_test \
7071
-j$(nproc)
7172
7273
- name: Run tests
@@ -192,6 +193,7 @@ jobs:
192193
test_address_resolution test_compute_share_target \
193194
test_utxo \
194195
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
196+
v37_test \
195197
-j$(nproc)
196198
197199
- name: Run tests under sanitizers

src/c2pool/main_btc.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ static void print_usage()
8585
" --stratum [H:]P stratum TCP listener for miners (B4-stratum)\n"
8686
" e.g. --stratum 9332 (binds 0.0.0.0:9332)\n"
8787
" --stratum 127.0.0.1:9332 (loopback only)\n"
88-
" Omit to disable stratum listener.\n";
88+
" Omit to disable stratum listener.\n"
89+
" --network-id ID c2pool sharechain IDENTIFIER (hex, <=8 bytes) for a\n"
90+
" private/custom p2pool network. Omit for public BTC.\n"
91+
" --prefix HEX c2pool sharechain PREFIX (hex, <=8 bytes), an\n"
92+
" INDEPENDENT per-network constant (no algebraic tie to\n"
93+
" IDENTIFIER). Supply with --network-id to join a custom\n"
94+
" p2pool chain; omit to use the compiled default prefix.\n";
8995
}
9096

9197
/// BTC wire-protocol magic bytes per network (pchMessageStart).
@@ -111,6 +117,8 @@ int main(int argc, char* argv[])
111117
uint16_t p2pool_port = 0;
112118
std::string stratum_addr = "0.0.0.0"; // listen all interfaces by default
113119
uint16_t stratum_port = 0; // 0 disables stratum; --stratum sets it
120+
std::string network_id_hex; // --network-id: c2pool IDENTIFIER override (empty = public net)
121+
std::string prefix_hex; // --prefix: c2pool PREFIX override (empty = compiled default)
114122

115123
for (int i = 1; i < argc; ++i)
116124
{
@@ -167,6 +175,14 @@ int main(int argc, char* argv[])
167175
stratum_port = static_cast<uint16_t>(std::stoi(ep.substr(colon + 1)));
168176
}
169177
}
178+
else if (arg == "--network-id" && i + 1 < argc)
179+
{
180+
network_id_hex = argv[++i];
181+
}
182+
else if (arg == "--prefix" && i + 1 < argc)
183+
{
184+
prefix_hex = argv[++i];
185+
}
170186
else
171187
{
172188
std::cerr << "unknown arg: " << arg << "\n";
@@ -183,6 +199,14 @@ int main(int argc, char* argv[])
183199

184200
btc::PoolConfig::is_testnet = testnet;
185201

202+
// B2-net: apply optional private-chain overrides. IDENTIFIER and PREFIX are
203+
// two INDEPENDENT per-network constants — set_network_id never derives one
204+
// from the other (commit 9034b59d). A bare --network-id keeps the compiled
205+
// network-default prefix; supply --prefix to join a custom p2pool chain.
206+
if (!prefix_hex.empty() && network_id_hex.empty())
207+
std::cerr << "[BTC] warning: --prefix ignored without --network-id\n";
208+
btc::PoolConfig::set_network_id(network_id_hex, prefix_hex);
209+
186210
auto chain_params = testnet4
187211
? btc::coin::BTCChainParams::testnet4()
188212
: (testnet ? btc::coin::BTCChainParams::testnet()

src/impl/btc/config_pool.hpp

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -157,33 +157,35 @@ class PoolConfig : protected core::Fileconfig
157157
static inline std::string override_identifier_hex;
158158
static inline std::string override_prefix_hex;
159159

160-
/// Set private network identity. IDENTIFIER is the consensus secret
161-
/// (hashed into ref_hash). PREFIX is derived from it for transport framing.
162-
/// Call once at startup before any P2P or share operations.
163-
static void set_network_id(const std::string& network_id_hex) {
160+
/// Set private network identity. IDENTIFIER and PREFIX are TWO INDEPENDENT
161+
/// per-network constants (p2pool model) — there is NO algebraic relationship
162+
/// between them, so PREFIX is never derived from IDENTIFIER. To join a custom
163+
/// p2pool sharechain, supply BOTH the network id and its prefix (each a
164+
/// separate per-network constant). If the prefix override is omitted, the
165+
/// compiled network-default prefix is used. Call once at startup before any
166+
/// P2P or share operations.
167+
static void set_network_id(const std::string& network_id_hex,
168+
const std::string& prefix_hex_override = "") {
164169
if (network_id_hex.empty() || network_id_hex == "0" || network_id_hex == "00000000")
165170
return; // public network, use defaults
166171

167-
// Pad to 16 hex chars (8 bytes) if shorter
168-
std::string padded = network_id_hex;
169-
while (padded.size() < 16) padded = "0" + padded;
170-
if (padded.size() > 16) padded = padded.substr(0, 16);
171-
172-
override_identifier_hex = padded;
173-
174-
// Derive PREFIX from IDENTIFIER using simple XOR mixing
175-
// PREFIX = IDENTIFIER bytes XOR-rotated (fast, deterministic, non-reversible enough
176-
// for transport framing — the real security is in IDENTIFIER via ref_hash)
177-
auto id_bytes = ParseHex(padded);
178-
static const char* HEX = "0123456789abcdef";
179-
override_prefix_hex.clear();
180-
override_prefix_hex.reserve(16);
181-
for (size_t i = 0; i < 8 && i < id_bytes.size(); ++i) {
182-
// XOR with rotated byte + constant to ensure PREFIX != IDENTIFIER
183-
uint8_t b = id_bytes[i] ^ id_bytes[(i + 3) % id_bytes.size()] ^ 0x5A;
184-
override_prefix_hex += HEX[b >> 4];
185-
override_prefix_hex += HEX[b & 0x0f];
186-
}
172+
// Normalize a hex string to exactly 16 hex chars (8 bytes).
173+
auto to8 = [](std::string h) {
174+
while (h.size() < 16) h = "0" + h;
175+
if (h.size() > 16) h = h.substr(0, 16);
176+
return h;
177+
};
178+
179+
override_identifier_hex = to8(network_id_hex);
180+
181+
// PREFIX is an INDEPENDENT transport constant — set it directly from the
182+
// override and NEVER derive it from IDENTIFIER. The old XOR-rotate
183+
// derivation has no p2pool analog and structurally prevented c2pool from
184+
// joining any p2pool custom network (derived prefix != p2pool prefix).
185+
// When no prefix override is given, leave override_prefix_hex empty so
186+
// prefix_hex() falls back to the compiled network default.
187+
if (!prefix_hex_override.empty())
188+
override_prefix_hex = to8(prefix_hex_override);
187189
}
188190

189191
static const std::string& identifier_hex() {
@@ -207,8 +209,8 @@ class PoolConfig : protected core::Fileconfig
207209
if (override_identifier_hex.empty())
208210
return 0; // public network
209211

210-
auto pfx_bytes = ParseHex(override_prefix_hex);
211-
auto id_bytes = ParseHex(override_identifier_hex);
212+
auto pfx_bytes = ParseHex(prefix_hex());
213+
auto id_bytes = ParseHex(identifier_hex());
212214
std::vector<unsigned char> preimage;
213215
preimage.reserve(pfx_bytes.size() + id_bytes.size());
214216
preimage.insert(preimage.end(), pfx_bytes.begin(), pfx_bytes.end());

src/sharechain/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ set(SHARECHAIN_SOURCE
1111
add_library(sharechain OBJECT ${SHARECHAIN_SOURCE})
1212
target_link_libraries(sharechain core)
1313

14-
add_subdirectory(test)
14+
add_subdirectory(test)
15+
add_subdirectory(v37/test)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# V37 MRR Roundabout Round-Buffer — implementation notes (WIP for review)
2+
3+
Branch: `v37/mrr-roundabout-buffer`. Spec: `c2pool-v37-mrr-roundabout-buffer.md` v1.0
4+
(all OQ/S decisions resolved). Module: `src/sharechain/v37/`, header-only,
5+
`namespace v37`, stdlib-only — compiles and tests standalone with
6+
`g++ -std=c++20`, no conan/boost/btclibs dependency.
7+
8+
## Done (implemented + tested on this VM)
9+
10+
| Component (brief) | Where | Status |
11+
|---|---|---|
12+
| 1. Lane storage, bucket leaves (F-1 fields) | `v37_lane.hpp` | done |
13+
| 2. MRR roll-up pyramid (OQ-5 geometry) | `v37_lane.hpp` (`fold_l0`, `cascade_folds`) | done; L=2 default fully tested, L≥3 see notes |
14+
| 3. Epoch-scaled incremental decay + OQ-2 exact rebuild | `v37_lane.hpp` (`push`, `epoch_rebuild`) | done |
15+
| 4. PayoutDescriptor v1 (OQ-3, S-1/S-2/S-3) | `v37_descriptor.hpp` | done |
16+
| 5. Quantized window (OQ-1) + reorg journal D=64 (OQ-7) | `v37_lane.hpp` (`evict_oldest_bucket`, `rewind`) | done |
17+
| 6. Lane digest (§8.5, OQ-4) | `v37_lane.hpp` (`digest`) | done |
18+
| 7. Fixed-point model + slow reference + bit-exact gate | `v37_fixed.hpp` + `test/v37_test.cpp` (`ReferenceLane`) | done |
19+
| Multichain container + miner intern | `v37_roundabout.hpp` | done |
20+
21+
Test run (g++ 13.3, -O2 and again under `-fsanitize=address,undefined`):
22+
**100,338 checks, 0 failures.** The consensus gate is `ReferenceLane` — an
23+
independent implementation whose per-miner weights are recomputed by full
24+
scan of the durable records after EVERY push — compared bit-exact against the
25+
incremental accumulators, across two geometries (small stress geometry, 5+
26+
epochs; ratified OQ-5 default geometry across two epoch rebuilds, ~9.5k
27+
pushes, full-u64-range weights). Also covered: digest determinism +
28+
sensitivity, rewind bit-exact restoration, window quantization, raw-work
29+
conservation (F-1), descriptor canon vectors for all five template kinds +
30+
kind-255 fallback, S-1 identity distinctness, aux/attribution validity rules,
31+
runtime lane add/remove, cross-lane identity intern.
32+
33+
## Range pinning + spec errata (flag for the spec's next revision)
34+
35+
1. **Q62, not Q64.** §8.2 says "widens to 64 fractional bits"; §8.1 delegates
36+
exact range pinning here. Pinned at FRAC_BITS = 62 so that: every table
37+
entry (decay ≤ 1.0, inverse ≤ 2^1.9) fits u64; w_raw keeps the FULL u64
38+
range; every w_raw × table product fits native unsigned __int128;
39+
accumulators fit 256 bits. Still 2^22 finer than V36's Q40. Suggest spec
40+
erratum: "62 fractional bits" with this rationale.
41+
2. **Wider storage than the §3 struct sketch.** Spec sketches comp w_scaled
42+
as q64 and bucket scaled_sum as q128; with full-range u64 raw work those
43+
overflow. Implementation uses u128 (L0 scaled) and U256 (sums, comp
44+
scaled). Spec §5 footprint numbers shift accordingly; tightening back
45+
down requires capping w_raw (a consensus parameter decision — operator).
46+
3. **Journal does not cross epoch rebuilds.** `rewind()` refuses if the span
47+
crosses a rebuild (journal is cleared there); caller takes the full lane
48+
rebuild path — the same escape hatch as the >D case (§6.2). Affects ~D/E
49+
≈ 1.6% of max-depth reorgs at default geometry. Suggest folding this rule
50+
into §6.2 explicitly.
51+
4. **Level sums for levels ≥ 1.** Buckets in one level can carry different
52+
epoch tags (immutability rule), so a single stored per-level scaled sum is
53+
frame-mixed; the per-band view weight is assembled per-bucket with the
54+
epoch shift (O(buckets/level) ≤ 568) instead of a maintained O(1) field.
55+
L0 sums are maintained O(1) as specced.
56+
5. **No residual dust at L = 2 (stronger than spec).** §4.2 tolerates
57+
deterministic truncation residuals between rebuilds; with the
58+
rebuild-from-raw design and default L = 2 the incremental state equals the
59+
full-scan reference EXACTLY at every operation (proven by the gate). The
60+
only residual source is the L≥3 cascade fold (children re-framed at fold);
61+
at L≥3 the gate holds at rebuild points, per spec. L≥3 needs its own
62+
rebuild-point-gated test before any chain uses it.
63+
64+
## Stubbed / deferred (not blocking review)
65+
66+
- **L1 view projection** (`RingFrame`/`Level` serialization for
67+
`/pplns/rings`): not implemented — view-layer concern; all backing queries
68+
exist (`payout_map`, `raw_work_in_span`, `levels()` accessor, digest).
69+
- **SoA / arena / power-of-two mask storage**: test-grade impl uses
70+
std::deque/std::vector with identical semantics; the §5 memory-layout
71+
optimizations (contiguous SoA rings, bump-allocated comps, SIMD renorm
72+
pass) are an integration-phase change that cannot alter results (same op
73+
sequence, same arithmetic).
74+
- **THE state-root hookup**: `Lane::digest()` produces the leaf; committing
75+
it into the coinbase OP_RETURN root is wiring in the existing THE
76+
commitment path, out of scope for the standalone module.
77+
- **Adaptive W (OQ-6)**: headroom only, per the resolution — W is a
78+
LaneParams constant; no formula.
79+
80+
## Needs CI / integrator attention
81+
82+
1. **Weight adapter**: V36 `att` is `uint288` (`target_to_average_attempts`).
83+
The lane takes `w_raw : u64`. For sharechain share targets this fits
84+
easily (att ≈ share_difficulty × 2^32), but the adapter MUST assert/clamp
85+
att ≤ u64::max — define the rule (reject share vs clamp) as a consensus
86+
parameter before wiring. Flagged rather than decided here.
87+
2. **Hashers**: `v37_hash.hpp` is a self-contained FIPS-conformant SHA-256
88+
(known-vector tested, bit-equal to core's CSHA256). Integration may swap
89+
to `core/hash.hpp` for one less implementation; output identical.
90+
3. **Full-tree build**: `src/sharechain/CMakeLists.txt` gained
91+
`add_subdirectory(v37/test)`; the test target is dependency-free and
92+
gated on BUILD_TESTING. Not exercised against the full conan build on
93+
this VM (by design) — ci-steward/btc-heap-opt to verify.
94+
4. **gtest port**: the suite uses a standalone CHECK harness so it runs
95+
without GTest; trivially portable to the repo's gtest idiom if preferred.
96+
5. **Caller migration**: `HeadPPLNS`/`think()` integration (replacing
97+
`DensePPLNSRing` per the spec's "subsumes" map) is intentionally not
98+
started — the module is per-coin-agnostic and caller-shaped for it
99+
(push/rewind/payout_map mirror slide/rebuild/compute_v36_weights).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# V37 MRR roundabout — standalone unit tests.
2+
# Header-only module, stdlib-only: no gtest, no core link needed. The suite
3+
# is its own harness (returns nonzero on failure) so it runs anywhere the
4+
# toolchain exists, including outside the conan/boost dependency tree.
5+
if (BUILD_TESTING)
6+
add_executable(v37_test v37_test.cpp)
7+
target_compile_features(v37_test PRIVATE cxx_std_20)
8+
add_test(NAME v37_test COMMAND v37_test)
9+
endif()

0 commit comments

Comments
 (0)