Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
test_utxo \
v37_test \
-j$(nproc)

- name: Run tests
Expand Down Expand Up @@ -192,6 +193,7 @@ jobs:
test_address_resolution test_compute_share_target \
test_utxo \
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
-j$(nproc)

- name: Run tests under sanitizers
Expand Down
26 changes: 25 additions & 1 deletion src/c2pool/main_btc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ static void print_usage()
" --stratum [H:]P stratum TCP listener for miners (B4-stratum)\n"
" e.g. --stratum 9332 (binds 0.0.0.0:9332)\n"
" --stratum 127.0.0.1:9332 (loopback only)\n"
" Omit to disable stratum listener.\n";
" Omit to disable stratum listener.\n"
" --network-id ID c2pool sharechain IDENTIFIER (hex, <=8 bytes) for a\n"
" private/custom p2pool network. Omit for public BTC.\n"
" --prefix HEX c2pool sharechain PREFIX (hex, <=8 bytes), an\n"
" INDEPENDENT per-network constant (no algebraic tie to\n"
" IDENTIFIER). Supply with --network-id to join a custom\n"
" p2pool chain; omit to use the compiled default prefix.\n";
}

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

for (int i = 1; i < argc; ++i)
{
Expand Down Expand Up @@ -165,6 +173,14 @@ int main(int argc, char* argv[])
stratum_port = static_cast<uint16_t>(std::stoi(ep.substr(colon + 1)));
}
}
else if (arg == "--network-id" && i + 1 < argc)
{
network_id_hex = argv[++i];
}
else if (arg == "--prefix" && i + 1 < argc)
{
prefix_hex = argv[++i];
}
else
{
std::cerr << "unknown arg: " << arg << "\n";
Expand All @@ -181,6 +197,14 @@ int main(int argc, char* argv[])

btc::PoolConfig::is_testnet = testnet;

// B2-net: apply optional private-chain overrides. IDENTIFIER and PREFIX are
// two INDEPENDENT per-network constants — set_network_id never derives one
// from the other (commit 9034b59d). A bare --network-id keeps the compiled
// network-default prefix; supply --prefix to join a custom p2pool chain.
if (!prefix_hex.empty() && network_id_hex.empty())
std::cerr << "[BTC] warning: --prefix ignored without --network-id\n";
btc::PoolConfig::set_network_id(network_id_hex, prefix_hex);

auto chain_params = testnet4
? btc::coin::BTCChainParams::testnet4()
: (testnet ? btc::coin::BTCChainParams::testnet()
Expand Down
54 changes: 28 additions & 26 deletions src/impl/btc/config_pool.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,33 +157,35 @@ class PoolConfig : protected core::Fileconfig
static inline std::string override_identifier_hex;
static inline std::string override_prefix_hex;

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

// Pad to 16 hex chars (8 bytes) if shorter
std::string padded = network_id_hex;
while (padded.size() < 16) padded = "0" + padded;
if (padded.size() > 16) padded = padded.substr(0, 16);

override_identifier_hex = padded;

// Derive PREFIX from IDENTIFIER using simple XOR mixing
// PREFIX = IDENTIFIER bytes XOR-rotated (fast, deterministic, non-reversible enough
// for transport framing — the real security is in IDENTIFIER via ref_hash)
auto id_bytes = ParseHex(padded);
static const char* HEX = "0123456789abcdef";
override_prefix_hex.clear();
override_prefix_hex.reserve(16);
for (size_t i = 0; i < 8 && i < id_bytes.size(); ++i) {
// XOR with rotated byte + constant to ensure PREFIX != IDENTIFIER
uint8_t b = id_bytes[i] ^ id_bytes[(i + 3) % id_bytes.size()] ^ 0x5A;
override_prefix_hex += HEX[b >> 4];
override_prefix_hex += HEX[b & 0x0f];
}
// Normalize a hex string to exactly 16 hex chars (8 bytes).
auto to8 = [](std::string h) {
while (h.size() < 16) h = "0" + h;
if (h.size() > 16) h = h.substr(0, 16);
return h;
};

override_identifier_hex = to8(network_id_hex);

// PREFIX is an INDEPENDENT transport constant — set it directly from the
// override and NEVER derive it from IDENTIFIER. The old XOR-rotate
// derivation has no p2pool analog and structurally prevented c2pool from
// joining any p2pool custom network (derived prefix != p2pool prefix).
// When no prefix override is given, leave override_prefix_hex empty so
// prefix_hex() falls back to the compiled network default.
if (!prefix_hex_override.empty())
override_prefix_hex = to8(prefix_hex_override);
}

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

auto pfx_bytes = ParseHex(override_prefix_hex);
auto id_bytes = ParseHex(override_identifier_hex);
auto pfx_bytes = ParseHex(prefix_hex());
auto id_bytes = ParseHex(identifier_hex());
std::vector<unsigned char> preimage;
preimage.reserve(pfx_bytes.size() + id_bytes.size());
preimage.insert(preimage.end(), pfx_bytes.begin(), pfx_bytes.end());
Expand Down
3 changes: 2 additions & 1 deletion src/sharechain/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ set(SHARECHAIN_SOURCE
add_library(sharechain OBJECT ${SHARECHAIN_SOURCE})
target_link_libraries(sharechain core)

add_subdirectory(test)
add_subdirectory(test)
add_subdirectory(v37/test)
99 changes: 99 additions & 0 deletions src/sharechain/v37/IMPLEMENTATION-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# V37 MRR Roundabout Round-Buffer — implementation notes (WIP for review)

Branch: `v37/mrr-roundabout-buffer`. Spec: `c2pool-v37-mrr-roundabout-buffer.md` v1.0
(all OQ/S decisions resolved). Module: `src/sharechain/v37/`, header-only,
`namespace v37`, stdlib-only — compiles and tests standalone with
`g++ -std=c++20`, no conan/boost/btclibs dependency.

## Done (implemented + tested on this VM)

| Component (brief) | Where | Status |
|---|---|---|
| 1. Lane storage, bucket leaves (F-1 fields) | `v37_lane.hpp` | done |
| 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 |
| 3. Epoch-scaled incremental decay + OQ-2 exact rebuild | `v37_lane.hpp` (`push`, `epoch_rebuild`) | done |
| 4. PayoutDescriptor v1 (OQ-3, S-1/S-2/S-3) | `v37_descriptor.hpp` | done |
| 5. Quantized window (OQ-1) + reorg journal D=64 (OQ-7) | `v37_lane.hpp` (`evict_oldest_bucket`, `rewind`) | done |
| 6. Lane digest (§8.5, OQ-4) | `v37_lane.hpp` (`digest`) | done |
| 7. Fixed-point model + slow reference + bit-exact gate | `v37_fixed.hpp` + `test/v37_test.cpp` (`ReferenceLane`) | done |
| Multichain container + miner intern | `v37_roundabout.hpp` | done |

Test run (g++ 13.3, -O2 and again under `-fsanitize=address,undefined`):
**100,338 checks, 0 failures.** The consensus gate is `ReferenceLane` — an
independent implementation whose per-miner weights are recomputed by full
scan of the durable records after EVERY push — compared bit-exact against the
incremental accumulators, across two geometries (small stress geometry, 5+
epochs; ratified OQ-5 default geometry across two epoch rebuilds, ~9.5k
pushes, full-u64-range weights). Also covered: digest determinism +
sensitivity, rewind bit-exact restoration, window quantization, raw-work
conservation (F-1), descriptor canon vectors for all five template kinds +
kind-255 fallback, S-1 identity distinctness, aux/attribution validity rules,
runtime lane add/remove, cross-lane identity intern.

## Range pinning + spec errata (flag for the spec's next revision)

1. **Q62, not Q64.** §8.2 says "widens to 64 fractional bits"; §8.1 delegates
exact range pinning here. Pinned at FRAC_BITS = 62 so that: every table
entry (decay ≤ 1.0, inverse ≤ 2^1.9) fits u64; w_raw keeps the FULL u64
range; every w_raw × table product fits native unsigned __int128;
accumulators fit 256 bits. Still 2^22 finer than V36's Q40. Suggest spec
erratum: "62 fractional bits" with this rationale.
2. **Wider storage than the §3 struct sketch.** Spec sketches comp w_scaled
as q64 and bucket scaled_sum as q128; with full-range u64 raw work those
overflow. Implementation uses u128 (L0 scaled) and U256 (sums, comp
scaled). Spec §5 footprint numbers shift accordingly; tightening back
down requires capping w_raw (a consensus parameter decision — operator).
3. **Journal does not cross epoch rebuilds.** `rewind()` refuses if the span
crosses a rebuild (journal is cleared there); caller takes the full lane
rebuild path — the same escape hatch as the >D case (§6.2). Affects ~D/E
≈ 1.6% of max-depth reorgs at default geometry. Suggest folding this rule
into §6.2 explicitly.
4. **Level sums for levels ≥ 1.** Buckets in one level can carry different
epoch tags (immutability rule), so a single stored per-level scaled sum is
frame-mixed; the per-band view weight is assembled per-bucket with the
epoch shift (O(buckets/level) ≤ 568) instead of a maintained O(1) field.
L0 sums are maintained O(1) as specced.
5. **No residual dust at L = 2 (stronger than spec).** §4.2 tolerates
deterministic truncation residuals between rebuilds; with the
rebuild-from-raw design and default L = 2 the incremental state equals the
full-scan reference EXACTLY at every operation (proven by the gate). The
only residual source is the L≥3 cascade fold (children re-framed at fold);
at L≥3 the gate holds at rebuild points, per spec. L≥3 needs its own
rebuild-point-gated test before any chain uses it.

## Stubbed / deferred (not blocking review)

- **L1 view projection** (`RingFrame`/`Level` serialization for
`/pplns/rings`): not implemented — view-layer concern; all backing queries
exist (`payout_map`, `raw_work_in_span`, `levels()` accessor, digest).
- **SoA / arena / power-of-two mask storage**: test-grade impl uses
std::deque/std::vector with identical semantics; the §5 memory-layout
optimizations (contiguous SoA rings, bump-allocated comps, SIMD renorm
pass) are an integration-phase change that cannot alter results (same op
sequence, same arithmetic).
- **THE state-root hookup**: `Lane::digest()` produces the leaf; committing
it into the coinbase OP_RETURN root is wiring in the existing THE
commitment path, out of scope for the standalone module.
- **Adaptive W (OQ-6)**: headroom only, per the resolution — W is a
LaneParams constant; no formula.

## Needs CI / integrator attention

1. **Weight adapter**: V36 `att` is `uint288` (`target_to_average_attempts`).
The lane takes `w_raw : u64`. For sharechain share targets this fits
easily (att ≈ share_difficulty × 2^32), but the adapter MUST assert/clamp
att ≤ u64::max — define the rule (reject share vs clamp) as a consensus
parameter before wiring. Flagged rather than decided here.
2. **Hashers**: `v37_hash.hpp` is a self-contained FIPS-conformant SHA-256
(known-vector tested, bit-equal to core's CSHA256). Integration may swap
to `core/hash.hpp` for one less implementation; output identical.
3. **Full-tree build**: `src/sharechain/CMakeLists.txt` gained
`add_subdirectory(v37/test)`; the test target is dependency-free and
gated on BUILD_TESTING. Not exercised against the full conan build on
this VM (by design) — ci-steward/btc-heap-opt to verify.
4. **gtest port**: the suite uses a standalone CHECK harness so it runs
without GTest; trivially portable to the repo's gtest idiom if preferred.
5. **Caller migration**: `HeadPPLNS`/`think()` integration (replacing
`DensePPLNSRing` per the spec's "subsumes" map) is intentionally not
started — the module is per-coin-agnostic and caller-shaped for it
(push/rewind/payout_map mirror slide/rebuild/compute_v36_weights).
9 changes: 9 additions & 0 deletions src/sharechain/v37/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# V37 MRR roundabout — standalone unit tests.
# Header-only module, stdlib-only: no gtest, no core link needed. The suite
# is its own harness (returns nonzero on failure) so it runs anywhere the
# toolchain exists, including outside the conan/boost dependency tree.
if (BUILD_TESTING)
add_executable(v37_test v37_test.cpp)
target_compile_features(v37_test PRIVATE cxx_std_20)
add_test(NAME v37_test COMMAND v37_test)
endif()
Loading
Loading