utxosnapshot: v2 format — embed full blk0001.dat into snapshot
Per Sami's vision: 'I want to carry over the whole block inside the UTXO.' The snapshot should be self-contained so a fresh node is fully usable — can serve blocks to peers, fully verify the chain, validate txs, and resume syncing forward. Replaces the legacy tri-bootstrap.tar.gz. Format change (UTXO_SNAPSHOT_VERSION 1 → 2): v1 HEADER: magic, version, network, height, blockHash, moneySupply, numHeaders, numUtxos, contentHash (88 bytes) v2 HEADER: same + numBlocks (92 bytes) ← new field v2 CONTENT (after v1's headers + utxos sections): blocks[numBlocks] ← raw blk0001.dat bytes, SHA256 included DumpSnapshot changes: - Walks ALL blocks from pindexBest to pindexGenesisBlock (was: last N=2000). The nHeaders arg is honored only when 0 < nHeaders < chain height for v1-compat diagnostic snapshots. - After writing headers + utxos sections, streams GetDataDir()/blk0001.dat bytes into the snapshot, chunked (64 KB), content-hashed. - Header now writes numBlocks between numUtxos and contentHash. LoadSnapshot changes: - Reads numBlocks after numUtxos when version >= 2. - After the UTXOs section, streams numBlocks bytes from the snapshot into dataDir/blk0001.dat. - v1 snapshots (no numBlocks in header) still load via the partial path: headers + UTXOs only, no blk0001.dat written. The 'block verification skipped for snapshot-sourced chains' hack stays for v1, becomes unnecessary for v2. Total v2 snapshot size: ~1.9 GB (550 MB headers + 1.3 GB blocks + 50 MB UTXOs). Generation on the operator machine: a few minutes. Download on reasonable connection: a few minutes. This commit is format-only — signature verification, auto-rebuild, and the LoadBlockIndex crash fix from PR #8 still apply unchanged.
This commit is contained in:
+112
-14
@@ -42,20 +42,27 @@ bool DumpSnapshot(const fs::path& destPath,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect block index entries (last nHeaders blocks, height ascending)
|
// Collect block index entries.
|
||||||
|
// v2 always includes EVERY block from genesis to tip, so a node
|
||||||
|
// loading this snapshot has full chain index (every block addressable
|
||||||
|
// via mapBlockIndex + readable from blk0001.dat).
|
||||||
|
// The nHeaders argument is honored only for legacy v1 generation.
|
||||||
|
const bool includeAllBlocks = (nHeaders == 0 || nHeaders > (unsigned int)nBestHeight);
|
||||||
std::vector<std::pair<uint256, CDiskBlockIndex>> vHeaders;
|
std::vector<std::pair<uint256, CDiskBlockIndex>> vHeaders;
|
||||||
vHeaders.reserve(nHeaders);
|
|
||||||
{
|
{
|
||||||
CBlockIndex* pindex = pindexBest;
|
CBlockIndex* pindex = pindexBest;
|
||||||
unsigned int nCollected = 0;
|
while (pindex) {
|
||||||
while (pindex && nCollected < nHeaders) {
|
|
||||||
CDiskBlockIndex diskindex(pindex);
|
CDiskBlockIndex diskindex(pindex);
|
||||||
vHeaders.push_back({*pindex->phashBlock, diskindex});
|
vHeaders.push_back({*pindex->phashBlock, diskindex});
|
||||||
pindex = pindex->pprev;
|
pindex = pindex->pprev;
|
||||||
nCollected++;
|
|
||||||
}
|
}
|
||||||
// Reverse to height ascending order
|
// Reverse to height ascending order (genesis first)
|
||||||
std::reverse(vHeaders.begin(), vHeaders.end());
|
std::reverse(vHeaders.begin(), vHeaders.end());
|
||||||
|
|
||||||
|
// Legacy v1 path: trim to last nHeaders if requested and not already all
|
||||||
|
if (!includeAllBlocks && vHeaders.size() > nHeaders) {
|
||||||
|
vHeaders.erase(vHeaders.begin(), vHeaders.begin() + (vHeaders.size() - nHeaders));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the chain DB once and reuse for both the UTXO count and the
|
// Open the chain DB once and reuse for both the UTXO count and the
|
||||||
@@ -91,6 +98,18 @@ bool DumpSnapshot(const fs::path& destPath,
|
|||||||
int64_t moneySupply = pindexBest->nMoneySupply;
|
int64_t moneySupply = pindexBest->nMoneySupply;
|
||||||
unsigned int numHeaders = (unsigned int)vHeaders.size();
|
unsigned int numHeaders = (unsigned int)vHeaders.size();
|
||||||
unsigned int numUtxos = (unsigned int)nUtxoCount;
|
unsigned int numUtxos = (unsigned int)nUtxoCount;
|
||||||
|
// numBlocks = size of raw blk0001.dat content to embed in the snapshot.
|
||||||
|
// We size this up front; the actual write below streams the file.
|
||||||
|
unsigned int numBlocks = 0;
|
||||||
|
{
|
||||||
|
FILE* blkFile = fopen((GetDataDir() / "blk0001.dat").string().c_str(), "rb");
|
||||||
|
if (blkFile) {
|
||||||
|
fseek(blkFile, 0, SEEK_END);
|
||||||
|
long blkSize = ftell(blkFile);
|
||||||
|
fclose(blkFile);
|
||||||
|
if (blkSize > 0) numBlocks = (unsigned int)blkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
uint256 contentHash; // placeholder, filled after writing data
|
uint256 contentHash; // placeholder, filled after writing data
|
||||||
|
|
||||||
fwrite(&magic, sizeof(magic), 1, file);
|
fwrite(&magic, sizeof(magic), 1, file);
|
||||||
@@ -101,6 +120,7 @@ bool DumpSnapshot(const fs::path& destPath,
|
|||||||
fwrite(&moneySupply, sizeof(moneySupply), 1, file);
|
fwrite(&moneySupply, sizeof(moneySupply), 1, file);
|
||||||
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
|
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
|
||||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||||
|
fwrite(&numBlocks, sizeof(numBlocks), 1, file); // v2+: size of embedded blk0001.dat
|
||||||
long contentHashPos = ftell(file);
|
long contentHashPos = ftell(file);
|
||||||
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
|
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
|
||||||
|
|
||||||
@@ -182,6 +202,34 @@ bool DumpSnapshot(const fs::path& destPath,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2: Append the raw blk0001.dat content so a fresh node is fully
|
||||||
|
// self-contained. Streams in chunks; SHA256 covers the bytes.
|
||||||
|
if (numBlocks > 0) {
|
||||||
|
FILE* blkFile = fopen((GetDataDir() / "blk0001.dat").string().c_str(), "rb");
|
||||||
|
if (!blkFile) {
|
||||||
|
fclose(file);
|
||||||
|
strError = "Cannot open blk0001.dat for snapshot embedding";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
printf("UtxoSnapshot: embedding blk0001.dat (%u bytes) into snapshot\n", numBlocks);
|
||||||
|
unsigned char blkBuf[64 * 1024];
|
||||||
|
size_t nLeft = numBlocks;
|
||||||
|
while (nLeft > 0) {
|
||||||
|
size_t nWant = nLeft > sizeof(blkBuf) ? sizeof(blkBuf) : nLeft;
|
||||||
|
size_t nRead = fread(blkBuf, 1, nWant, blkFile);
|
||||||
|
if (nRead != nWant) {
|
||||||
|
fclose(blkFile);
|
||||||
|
fclose(file);
|
||||||
|
strError = "Short read on blk0001.dat during snapshot embed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fwrite(blkBuf, 1, nRead, file);
|
||||||
|
SHA256_Update(&sha256, blkBuf, nRead);
|
||||||
|
nLeft -= nRead;
|
||||||
|
}
|
||||||
|
fclose(blkFile);
|
||||||
|
}
|
||||||
|
|
||||||
// Finalize content hash and write it to the header
|
// Finalize content hash and write it to the header
|
||||||
SHA256_Final((unsigned char*)&contentHash, &sha256);
|
SHA256_Final((unsigned char*)&contentHash, &sha256);
|
||||||
fseek(file, contentHashPos, SEEK_SET);
|
fseek(file, contentHashPos, SEEK_SET);
|
||||||
@@ -189,8 +237,8 @@ bool DumpSnapshot(const fs::path& destPath,
|
|||||||
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, hash=%s)\n",
|
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, %u block bytes, hash=%s)\n",
|
||||||
destPath.string().c_str(), numHeaders, numUtxos,
|
destPath.string().c_str(), numHeaders, numUtxos, numBlocks,
|
||||||
contentHash.ToString().c_str());
|
contentHash.ToString().c_str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -240,7 +288,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
int height;
|
int height;
|
||||||
uint256 blockHash;
|
uint256 blockHash;
|
||||||
int64_t moneySupply;
|
int64_t moneySupply;
|
||||||
unsigned int numHeaders, numUtxos;
|
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0;
|
||||||
uint256 expectedContentHash;
|
uint256 expectedContentHash;
|
||||||
|
|
||||||
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
|
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
|
||||||
@@ -248,12 +296,32 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
fread(&network, sizeof(network), 1, file) != 1 ||
|
fread(&network, sizeof(network), 1, file) != 1 ||
|
||||||
fread(&height, sizeof(height), 1, file) != 1 ||
|
fread(&height, sizeof(height), 1, file) != 1 ||
|
||||||
fread(&blockHash, sizeof(blockHash), 1, file) != 1 ||
|
fread(&blockHash, sizeof(blockHash), 1, file) != 1 ||
|
||||||
fread(&moneySupply, sizeof(moneySupply), 1, file) != 1 ||
|
fread(&moneySupply, sizeof(moneySupply), 1, file) != 1) {
|
||||||
fread(&numHeaders, sizeof(numHeaders), 1, file) != 1 ||
|
|
||||||
fread(&numUtxos, sizeof(numUtxos), 1, file) != 1 ||
|
|
||||||
fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
strError = "Truncated snapshot header";
|
strError = "Truncated snapshot header (common fields)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fread(&numHeaders, sizeof(numHeaders), 1, file) != 1) {
|
||||||
|
fclose(file);
|
||||||
|
strError = "Truncated snapshot header (numHeaders)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fread(&numUtxos, sizeof(numUtxos), 1, file) != 1) {
|
||||||
|
fclose(file);
|
||||||
|
strError = "Truncated snapshot header (numUtxos)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// v2+ has numBlocks between numUtxos and contentHash. v1 stops here.
|
||||||
|
if (version >= 2) {
|
||||||
|
if (fread(&numBlocks, sizeof(numBlocks), 1, file) != 1) {
|
||||||
|
fclose(file);
|
||||||
|
strError = "Truncated snapshot header (numBlocks)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
|
||||||
|
fclose(file);
|
||||||
|
strError = "Truncated snapshot header (contentHash)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +528,36 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2: After UTXOs, extract the raw blk0001.dat content. This makes the
|
||||||
|
// loaded node fully self-contained — no separate bootstrap needed.
|
||||||
|
if (success && version >= 2 && numBlocks > 0) {
|
||||||
|
printf("UtxoSnapshot: extracting %u block bytes to blk0001.dat...\n", numBlocks);
|
||||||
|
fs::path blkOut = dataDir / "blk0001.dat";
|
||||||
|
FILE* blkOutFile = fopen(blkOut.string().c_str(), "wb");
|
||||||
|
if (!blkOutFile) {
|
||||||
|
success = false;
|
||||||
|
strError = "Cannot create blk0001.dat for snapshot extract: " + blkOut.string();
|
||||||
|
} else {
|
||||||
|
unsigned char blkBuf[64 * 1024];
|
||||||
|
size_t nLeft = numBlocks;
|
||||||
|
while (nLeft > 0 && success) {
|
||||||
|
size_t nWant = nLeft > sizeof(blkBuf) ? sizeof(blkBuf) : nLeft;
|
||||||
|
size_t nRead = fread(blkBuf, 1, nWant, file);
|
||||||
|
if (nRead != nWant) {
|
||||||
|
success = false;
|
||||||
|
strError = "Short read on snapshot blocks section";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fwrite(blkBuf, 1, nRead, blkOutFile);
|
||||||
|
SHA256_Update(&sha256, blkBuf, nRead);
|
||||||
|
nLeft -= nRead;
|
||||||
|
}
|
||||||
|
fclose(blkOutFile);
|
||||||
|
if (success)
|
||||||
|
printf("UtxoSnapshot: wrote blk0001.dat (%u bytes)\n", numBlocks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Verify content hash
|
// Verify content hash
|
||||||
if (success) {
|
if (success) {
|
||||||
uint256 actualHash;
|
uint256 actualHash;
|
||||||
|
|||||||
+26
-7
@@ -11,27 +11,46 @@
|
|||||||
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
|
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
|
||||||
|
|
||||||
// UTXO snapshot format version
|
// UTXO snapshot format version
|
||||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 1;
|
//
|
||||||
|
// v1: headers (last N=2000) + UTXOs only. Node still needs blk0001.dat from
|
||||||
|
// somewhere (legacy bootstrap tarball or P2P) to fully verify.
|
||||||
|
// v2: full chain archive. Adds a `blocks` section with raw blk0001.dat content
|
||||||
|
// and writes ALL CDiskBlockIndex entries (every block from genesis to
|
||||||
|
// snapshot tip). After loading a v2 snapshot, the node is fully
|
||||||
|
// self-contained: it can serve any block to peers, fully verify the
|
||||||
|
// chain, validate transactions, and resume syncing from the snapshot
|
||||||
|
// tip forward. Replaces the legacy `tri-bootstrap.tar.gz` artifact.
|
||||||
|
static const unsigned int UTXO_SNAPSHOT_VERSION = 2;
|
||||||
|
|
||||||
// Number of block index entries to include in snapshot (covers difficulty,
|
// Number of block index entries to include in snapshot (v1 only).
|
||||||
// median time, stake modifier, and reorg depth requirements)
|
// v2 always includes ALL headers — this constant is retained for the v1
|
||||||
|
// fallback path and as a CLI override for diagnostic snapshots.
|
||||||
static const unsigned int UTXO_SNAPSHOT_DEFAULT_HEADERS = 2000;
|
static const unsigned int UTXO_SNAPSHOT_DEFAULT_HEADERS = 2000;
|
||||||
|
|
||||||
namespace UtxoSnapshot {
|
namespace UtxoSnapshot {
|
||||||
|
|
||||||
// Create a UTXO snapshot from the current chain state.
|
// Create a UTXO snapshot from the current chain state.
|
||||||
// Writes last nHeaders block index entries + all UTXOs to destPath.
|
// (v2) Writes ALL CDiskBlockIndex entries + the raw blk0001.dat bytes +
|
||||||
|
// all UTXOs to destPath. Output is a complete chain archive at the
|
||||||
|
// current tip — a fresh node loading this is fully usable (can serve
|
||||||
|
// blocks, fully verify, sync forward).
|
||||||
// Returns true on success, sets strError on failure.
|
// Returns true on success, sets strError on failure.
|
||||||
bool DumpSnapshot(const std::filesystem::path& destPath,
|
bool DumpSnapshot(const std::filesystem::path& destPath,
|
||||||
unsigned int nHeaders,
|
unsigned int nHeaders,
|
||||||
std::string& strError);
|
std::string& strError);
|
||||||
|
|
||||||
// Load a UTXO snapshot from a file into a fresh LevelDB.
|
// Load a UTXO snapshot from a file into the data directory.
|
||||||
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
|
// Writes:
|
||||||
// The LevelDB must NOT be open yet (call before LoadBlockIndex).
|
// - all CDiskBlockIndex entries → chain DB
|
||||||
|
// - all UTXOs → chain DB
|
||||||
|
// - raw blk0001.dat bytes → dataDir/blk0001.dat
|
||||||
|
// - hashBestChain, dbformat → chain DB
|
||||||
|
// The chain DB must NOT be open yet (call before LoadBlockIndex).
|
||||||
// `requireCheckpoint` enforces that the snapshot tip is a known
|
// `requireCheckpoint` enforces that the snapshot tip is a known
|
||||||
// checkpoint (for P2P-delivered snapshots). Local loads from a
|
// checkpoint (for P2P-delivered snapshots). Local loads from a
|
||||||
// trusted operator pass false.
|
// trusted operator pass false.
|
||||||
|
// v1 snapshots (headers+UTXOs only) still load via the partial-load
|
||||||
|
// path; the blocks section is simply absent.
|
||||||
// Returns true on success, sets strError on failure.
|
// Returns true on success, sets strError on failure.
|
||||||
bool LoadSnapshot(const std::filesystem::path& snapshotPath,
|
bool LoadSnapshot(const std::filesystem::path& snapshotPath,
|
||||||
const std::filesystem::path& dataDir,
|
const std::filesystem::path& dataDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user