Skip to content

Commit 1b9c59d

Browse files
committed
Merge dashboard-refinements: Sharechain Transparency Explorer (v0.12.0)
Major dashboard milestone — defragmenter-style sharechain visualization, interactive PPLNS treemaps with LTC+DOGE payouts, animated hover effects, block solution banners (Legendary/Epic), V36 upgrade pressure for miners, modern share detail pages, per-miner hashrate/version/coinbase tooltips.
2 parents 58cd591 + 82cddb6 commit 1b9c59d

17 files changed

Lines changed: 3734 additions & 549 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Changelog
22

3+
## [0.12.0] - 2026-04-07
4+
5+
### Milestone
6+
- **Sharechain Transparency Explorer** — Windows Defragmenter-style canvas visualization of the full sharechain window (8640 mainnet / 400 testnet shares) with interactive PPLNS treemaps, animated hover effects, and V36 upgrade pressure for miners.
7+
8+
### Added
9+
- **Defrag sharechain viewer** — canvas grid where each share is a colored cell. Colors: blue (this node), green (verified), gray (unverified), yellow (orphan), red (dead), purple (pool fee), teal (V36 signaling). Gold/cyan/orange borders for LTC/DOGE/twin block solutions.
10+
- **PPLNS Distribution treemap** — interactive squarified treemap on dashboard and share detail pages. Shows per-miner LTC % and DOGE % with clickable addresses (Blockchair). V35-only miners shown desaturated with "NO DOGE" labels and red borders.
11+
- **Animated hover effects** — share cells enlarge 2.5x with white glow and spring animation. PPLNS treemap cells scale with brightness boost. Chain structure cells (Best Share, Verified Heads, Tails) as colored squares with tooltips.
12+
- **Rich share tooltips** — hash, absolute + relative height, miner address, hashrate, difficulty, age, version + desired version, coinbase tag, stale status, V36 signaling tags, LTC/DOGE block solution badges.
13+
- **Block solution banners** — share detail page shows prominent banners for block solutions: gold (LTC), cyan (DOGE), orange pulsing (LEGENDARY twin). Blockchair links, confirmations, block reward.
14+
- **Modern share.html** — complete redesign matching dashboard dark theme. Card layout with navigation, share data, local data, block header, generation TX, V36 metadata, full PPLNS treemap with DOGE payouts and version tags.
15+
- **`/sharechain/window` enhanced** — compact JSON keys, human-readable miner addresses, `is_block_solution` flag, `desired_version`, coinbase ASCII tag (longest 10+ char run with letters), DOGE discovered blocks.
16+
- **`/web/share/<hash>` endpoint** — full share lookup via `set_share_lookup_fn` callback. Returns p2pool-compatible JSON with block solution detection, DOGE block info, LTC confirmations.
17+
- **V36? column in Current Payouts** — green checkmark (V36), teal arrow (signaling), red X (V35 only, no DOGE).
18+
- **Time axis** — left-side hour labels on defrag grid showing share age per row.
19+
- **WoW quality colors** — Best Share: Epic purple glow. Twin blocks: Legendary orange pulse.
20+
21+
### Changed
22+
- "My Shares" / "Mine" renamed to "This Node" throughout.
23+
- Share explorer section renamed to "Sharechain Transparency Explorer".
24+
- Donation field removed from tooltips (always 0 in V36+).
25+
326
## [0.9.5] - 2026-03-27
427

528
### Milestone

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Original forum thread: <https://bitcointalk.org/index.php?topic=18313>
1212
> **First daemonless DOGE block:** (2026-03-27) — DOGE block accepted on testnet4alpha via embedded SPV P2P, no dogecoind RPC needed
1313
>
1414
> **First V36 Twin Block:** LTC [#3085349](https://blockchair.com/litecoin/block/3085349) + DOGE [#6154761](https://blockchair.com/dogecoin/block/6154761) (2026-04-05) — simultaneous LTC+DOGE block found by v36-signalling nodes running p2pool v36 producing V35 shares with `desired_version=36`; detected and displayed by c2pool's embedded block scanner
15+
>
16+
> **Sharechain Transparency Explorer** (2026-04-07) — defragmenter-style sharechain visualization with interactive PPLNS treemaps, animated hover effects, per-miner LTC+DOGE payout breakdown, V36 upgrade pressure for V35 miners
1517
1618
---
1719

config/c2pool_mainnet.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ port: 9326 # P2P p2pool sharechain port (LTC mainnet default
1212
stratum_port: 9327 # Stratum mining port
1313
web_port: 8080 # Web dashboard / JSON-RPC API port
1414
http_host: "0.0.0.0" # HTTP bind address
15+
external_ip: "voidbind.com" # Public IP/domain for stratum URL display
1516

1617
# ─── Operating Mode ────────────────────────────────────────────────────────
1718
# integrated: true # Full P2P pool (DEFAULT)

src/c2pool/c2pool_refactored.cpp

Lines changed: 720 additions & 100 deletions
Large diffs are not rendered by default.

src/c2pool/merged/coin_broadcaster.hpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,28 @@ class CoinBroadcaster
281281
return static_cast<int>(m_peers.size());
282282
}
283283

284+
/// Return per-peer info JSON array (daemon-style getpeerinfo).
285+
nlohmann::json get_peer_info() const
286+
{
287+
std::lock_guard<std::mutex> lock(m_mutex);
288+
nlohmann::json arr = nlohmann::json::array();
289+
for (const auto& [key, peer] : m_peers) {
290+
arr.push_back({
291+
{"addr", key},
292+
{"version", peer->node_p2p.peer_version()},
293+
{"subver", peer->node_p2p.peer_subver()},
294+
{"services", peer->node_p2p.peer_services()},
295+
{"startingheight", peer->node_p2p.peer_start_height()},
296+
{"conntime", peer->node_p2p.peer_uptime_sec()},
297+
{"inbound", false}, // broadcaster only makes outbound connections
298+
{"connected", peer->node_p2p.peer_version() > 0}
299+
});
300+
}
301+
return arr;
302+
}
303+
304+
const std::string& symbol() const { return m_symbol; }
305+
284306
/// Request a full block (MSG_MWEB_BLOCK) from all peers via getdata.
285307
/// Used after a chain reorg to re-fetch MWEB state for the new tip.
286308
void request_full_block(const uint256& block_hash)
@@ -299,6 +321,24 @@ class CoinBroadcaster
299321
}
300322
}
301323

324+
/// Request a block via plain MSG_BLOCK (0x02) from all peers.
325+
/// Works for any block in the chain regardless of MWEB/witness support.
326+
void request_block_plain(const uint256& block_hash)
327+
{
328+
std::lock_guard<std::mutex> lock(m_mutex);
329+
int sent = 0;
330+
for (auto& [key, peer] : m_peers) {
331+
try {
332+
peer->node_p2p.request_block(block_hash);
333+
++sent;
334+
} catch (...) {}
335+
}
336+
if (sent > 0) {
337+
LOG_INFO << "[" << m_symbol << "] Block (MSG_BLOCK) requested from "
338+
<< sent << " peer(s): " << block_hash.GetHex().substr(0, 16) << "...";
339+
}
340+
}
341+
302342
/// Send getheaders to all connected peers using the supplied block locator.
303343
/// Used by the embedded node to request initial header sync after seeding genesis.
304344
void request_headers(const std::vector<uint256>& locator, const uint256& stop_hash)
@@ -321,7 +361,6 @@ class CoinBroadcaster
321361
<< (locator.empty() ? "" : " tip=" + locator.front().GetHex().substr(0, 16) + "...");
322362
}
323363

324-
const std::string& symbol() const { return m_symbol; }
325364
CoinPeerManager& peer_manager() { return m_peer_manager; }
326365

327366
private:

src/c2pool/merged/merged_mining.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,6 +1658,7 @@ void MergedMiningManager::record_discovered_block(
16581658
std::chrono::system_clock::now().time_since_epoch()).count();
16591659
blk.accepted = accepted;
16601660
blk.coinbase_value = chain.current_work.coinbase_value;
1661+
blk.is_local = true; // our own submission
16611662

16621663
m_discovered_blocks.push_back(std::move(blk));
16631664
m_blocks_per_chain[chain.config.chain_id]++;
@@ -1722,6 +1723,20 @@ void MergedMiningManager::add_discovered_block(const DiscoveredMergedBlock& blk)
17221723
<< " hash=" << blk.block_hash.substr(0, 16);
17231724
}
17241725

1726+
void MergedMiningManager::update_block_coinbase(const std::string& block_hash, uint64_t coinbase_value)
1727+
{
1728+
std::lock_guard<std::recursive_mutex> lock(m_mutex);
1729+
for (auto& blk : m_discovered_blocks) {
1730+
if (blk.block_hash == block_hash) {
1731+
blk.coinbase_value = coinbase_value;
1732+
LOG_INFO << "[MM:" << blk.symbol << "] Coinbase updated: height=" << blk.height
1733+
<< " value=" << coinbase_value << " (" << (coinbase_value / 1e8) << " "
1734+
<< blk.symbol << ")";
1735+
return;
1736+
}
1737+
}
1738+
}
1739+
17251740
std::vector<MergedMiningManager::ChainInfo> MergedMiningManager::get_chain_infos() const
17261741
{
17271742
std::lock_guard<std::recursive_mutex> lock(m_mutex);

src/c2pool/merged/merged_mining.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ struct DiscoveredMergedBlock
153153
int64_t timestamp{0}; // unix epoch seconds
154154
bool accepted{true}; // RPC returned success
155155
uint64_t coinbase_value{0}; // aux block reward in satoshis
156+
bool is_local{false}; // true = our own submission, false = detected from peer share
157+
uint32_t parent_height{0}; // parent (LTC) block height (absheight)
158+
std::string miner; // miner address who found the block
156159
};
157160

158161
// ─── Integrated Merged Mining Manager ────────────────────────────────────────
@@ -243,6 +246,8 @@ class MergedMiningManager
243246
uint64_t get_chain_block_count(uint32_t chain_id) const;
244247
// Add externally discovered block (e.g. from peer share scan)
245248
void add_discovered_block(const DiscoveredMergedBlock& blk);
249+
// Update coinbase_value on a discovered block (after fetching full block from P2P)
250+
void update_block_coinbase(const std::string& block_hash, uint64_t coinbase_value);
246251

247252
// Per-chain config snapshots (for REST)
248253
struct ChainInfo {

src/c2pool/storage/found_block_store.hpp

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,154 @@ class FoundBlockStore {
198198
}
199199
};
200200

201+
/// Persistent record for a discovered merged block (DOGE etc).
202+
/// Uses key prefix "mblk:" — never pruned.
203+
struct MergedBlockRecord {
204+
uint32_t chain_id{0};
205+
std::string symbol;
206+
int height{0};
207+
std::string block_hash;
208+
std::string parent_hash;
209+
int64_t timestamp{0};
210+
bool accepted{true};
211+
uint64_t coinbase_value{0};
212+
bool is_local{false};
213+
uint32_t parent_height{0};
214+
std::string miner;
215+
216+
std::vector<uint8_t> serialize() const
217+
{
218+
PackStream ps;
219+
uint8_t version = 1;
220+
ps << version;
221+
ps << chain_id;
222+
uint8_t sym_len = static_cast<uint8_t>(std::min(symbol.size(), size_t(31)));
223+
ps << sym_len;
224+
ps.write(std::span<const std::byte>(
225+
reinterpret_cast<const std::byte*>(symbol.data()), sym_len));
226+
ps << static_cast<uint32_t>(height);
227+
uint8_t bh_len = static_cast<uint8_t>(std::min(block_hash.size(), size_t(255)));
228+
ps << bh_len;
229+
ps.write(std::span<const std::byte>(
230+
reinterpret_cast<const std::byte*>(block_hash.data()), bh_len));
231+
uint8_t ph_len = static_cast<uint8_t>(std::min(parent_hash.size(), size_t(255)));
232+
ps << ph_len;
233+
ps.write(std::span<const std::byte>(
234+
reinterpret_cast<const std::byte*>(parent_hash.data()), ph_len));
235+
ps << static_cast<uint64_t>(timestamp);
236+
uint8_t flags = (accepted ? 1 : 0) | (is_local ? 2 : 0);
237+
ps << flags;
238+
ps << coinbase_value;
239+
ps << parent_height;
240+
uint8_t mn_len = static_cast<uint8_t>(std::min(miner.size(), size_t(255)));
241+
ps << mn_len;
242+
if (mn_len > 0)
243+
ps.write(std::span<const std::byte>(
244+
reinterpret_cast<const std::byte*>(miner.data()), mn_len));
245+
246+
auto span = ps.get_span();
247+
return {reinterpret_cast<const uint8_t*>(span.data()),
248+
reinterpret_cast<const uint8_t*>(span.data()) + span.size()};
249+
}
250+
251+
static MergedBlockRecord deserialize(const std::vector<uint8_t>& data)
252+
{
253+
MergedBlockRecord rec;
254+
if (data.size() < 4) return rec;
255+
size_t pos = 0;
256+
auto read_u8 = [&]() -> uint8_t { return pos < data.size() ? data[pos++] : 0; };
257+
auto read_u32 = [&]() -> uint32_t {
258+
if (pos + 4 > data.size()) return 0;
259+
uint32_t v; std::memcpy(&v, &data[pos], 4); pos += 4; return v;
260+
};
261+
auto read_u64 = [&]() -> uint64_t {
262+
if (pos + 8 > data.size()) return 0;
263+
uint64_t v; std::memcpy(&v, &data[pos], 8); pos += 8; return v;
264+
};
265+
auto read_str = [&](uint8_t len) -> std::string {
266+
if (pos + len > data.size()) { pos = data.size(); return ""; }
267+
std::string s(reinterpret_cast<const char*>(&data[pos]), len);
268+
pos += len; return s;
269+
};
270+
271+
uint8_t version = read_u8();
272+
if (version != 1) return rec;
273+
rec.chain_id = read_u32();
274+
rec.symbol = read_str(read_u8());
275+
rec.height = static_cast<int>(read_u32());
276+
rec.block_hash = read_str(read_u8());
277+
rec.parent_hash = read_str(read_u8());
278+
rec.timestamp = static_cast<int64_t>(read_u64());
279+
uint8_t flags = read_u8();
280+
rec.accepted = (flags & 1) != 0;
281+
rec.is_local = (flags & 2) != 0;
282+
rec.coinbase_value = read_u64();
283+
rec.parent_height = read_u32();
284+
rec.miner = read_str(read_u8());
285+
return rec;
286+
}
287+
};
288+
289+
/// LevelDB-backed persistent store for discovered merged blocks.
290+
/// Uses key prefix "mblk:" — shares the same LevelDB as found blocks.
291+
class MergedBlockStore {
292+
public:
293+
explicit MergedBlockStore(core::LevelDBStore& store) : m_store(store) {}
294+
295+
bool store(const MergedBlockRecord& rec)
296+
{
297+
auto key = make_key(rec.chain_id, rec.height, rec.block_hash);
298+
return m_store.put(key, rec.serialize());
299+
}
300+
301+
bool update_coinbase(const std::string& block_hash, uint32_t chain_id, uint64_t coinbase_value)
302+
{
303+
auto keys = m_store.list_keys("mblk:", 1000);
304+
for (const auto& key : keys) {
305+
std::vector<uint8_t> data;
306+
if (!m_store.get(key, data)) continue;
307+
auto rec = MergedBlockRecord::deserialize(data);
308+
if (rec.block_hash == block_hash && rec.chain_id == chain_id) {
309+
rec.coinbase_value = coinbase_value;
310+
return m_store.put(key, rec.serialize());
311+
}
312+
}
313+
return false;
314+
}
315+
316+
std::vector<MergedBlockRecord> load_all()
317+
{
318+
std::vector<MergedBlockRecord> result;
319+
auto keys = m_store.list_keys("mblk:", 10000);
320+
for (const auto& key : keys) {
321+
std::vector<uint8_t> data;
322+
if (m_store.get(key, data)) {
323+
auto rec = MergedBlockRecord::deserialize(data);
324+
if (!rec.block_hash.empty())
325+
result.push_back(std::move(rec));
326+
}
327+
}
328+
std::sort(result.begin(), result.end(),
329+
[](const MergedBlockRecord& a, const MergedBlockRecord& b) {
330+
return a.timestamp > b.timestamp;
331+
});
332+
return result;
333+
}
334+
335+
size_t count() { return m_store.list_keys("mblk:", 10000).size(); }
336+
337+
private:
338+
core::LevelDBStore& m_store;
339+
340+
static std::string make_key(uint32_t chain_id, int height, const std::string& block_hash)
341+
{
342+
std::ostringstream oss;
343+
oss << "mblk:" << chain_id << ":"
344+
<< std::setfill('0') << std::setw(12) << height << ":"
345+
<< block_hash.substr(0, 16);
346+
return oss.str();
347+
}
348+
};
349+
201350
} // namespace storage
202351
} // namespace c2pool

0 commit comments

Comments
 (0)