Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c1c85c5
Add support for std::unordered_{map,set} to memusage.h
sipa Apr 21, 2017
4f0dd59
Switch CCoinsMap from boost to std unordered_map
sipa Apr 21, 2017
8c2ed80
Add SizeEstimate to CDBBatch
sipa Apr 25, 2017
3f59bef
error() in disconnect for disk corruption, not inconsistency
sipa Apr 25, 2017
8f1caba
Introduce CHashVerifier to hash read data
sipa Apr 25, 2017
ebb2d36
Add specialization of SipHash for 256 + 32 bit data
sipa Apr 25, 2017
a1e51f4
Remove/ignore tx version in utxo and undo
sipa Apr 25, 2017
fc752c4
Report on-disk size in gettxoutsetinfo
sipa May 12, 2017
8798688
Store/allow tx metadata in all undo records
sipa Apr 25, 2017
246da0f
Introduce Coin, a single unspent output
sipa Apr 25, 2017
af9be44
Replace CTxInUndo with Coin
sipa Apr 25, 2017
bc6957a
Optimization: Coin&& to ApplyTxInUndo
sipa Apr 25, 2017
2f09e11
Introduce new per-txout CCoinsViewCache functions
sipa Apr 25, 2017
31a15d3
Switch from per-tx to per-txout CCoinsViewCache methods in some places
sipa Apr 25, 2017
7d65b9b
Only pass things committed to by tx's witness hash to CScriptCheck
TheBlueMatt Apr 27, 2017
f9ce8af
Switch CScriptCheck to use Coin instead of CCoins
sipa Apr 25, 2017
5db4293
Switch tests from ModifyCoins to AddCoin/SpendCoin
sipa Apr 25, 2017
b91f50d
Remove ModifyCoins/ModifyNewCoins
sipa Apr 25, 2017
01250ab
Replace CCoins-based CTxMemPool::pruneSpent with isSpent
sipa Apr 25, 2017
546c1a1
Refactor GetUTXOStats in preparation for per-COutPoint iteration
sipa Apr 26, 2017
20dddd7
Switch CCoinsView and chainstate db from per-txid to per-txout
sipa Apr 25, 2017
1c9a91f
Backport Session 2 per-txout migration commits with Dogecoin compatib…
edtubbs Jun 13, 2026
b2ed44a
Session 3: fix coins_tests.cpp for per-txout and 21180ff73 semantics
edtubbs Jun 13, 2026
2dddcc3
add unused SnapshotMetadata class
jamesob Mar 28, 2019
911c750
coinstats: add coins_count
jamesob Mar 28, 2019
316c7fc
rpc: add dumptxoutset
jamesob Mar 28, 2019
fd5f643
session 4: add dumptxoutset QA test
edtubbs Jun 22, 2026
5e2aa8b
session 5: enhance dumptxoutset with snapshot magic/version, txoutset…
edtubbs Jun 23, 2026
8989647
session 6: add adapted loadtxoutset RPC and test scaffolding
edtubbs Jun 23, 2026
2f4066c
session 7: fix setBlockIndexCandidates invariant in RewindChainstateT…
edtubbs Jun 23, 2026
b91c683
session 8: add snapshot RPC QA and docs
edtubbs Jun 23, 2026
7c79b9e
Remove invalid PoW check in LoadBlockIndexGuts (scrypt/auxpow)
edtubbs Jun 24, 2026
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 doc/rpc-maturity.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Core RPC. Maturity is expressed over 3 stages:
| decoderawtransaction | STABLE | |
| decodescript | STABLE | |
| disconnectnode | STABLE | |
| dumptxoutset | UNSTABLE | New in 1.14.99 |
| dumpprivkey | STABLE | |
| dumpwallet | UNSTABLE | Breaking change since 1.14.6 |
| encryptwallet | STABLE | |
Expand Down Expand Up @@ -97,6 +98,7 @@ Core RPC. Maturity is expressed over 3 stages:
| listtransactions | STABLE | |
| listunspent | STABLE | |
| lockunspent | STABLE | |
| loadtxoutset | UNSTABLE | New in 1.14.99 |
| move | DEPRECATED | Deprecated since 1.14.0 |
| ping | STABLE | |
| preciousblock | STABLE | |
Expand Down
2 changes: 2 additions & 0 deletions qa/pull-tester/rpc-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
'nodehandling.py',
'decodescript.py',
'blockchain.py',
'dumptxoutset.py',
'loadtxoutset.py',
'disablewallet.py',
'keypool.py',
'p2p-mempool.py',
Expand Down
4 changes: 2 additions & 2 deletions qa/rpc-tests/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ def _test_gettxoutsetinfo(self):
assert_equal(res['transactions'], 120)
assert_equal(res['height'], 120)
assert_equal(res['txouts'], 120)
assert_equal(res['bytes_serialized'], 8520),
assert_greater_than(res['disk_size'], 0)
assert_equal(len(res['bestblock']), 64)
assert_equal(len(res['hash_serialized']), 64)
assert_equal(len(res['hash_serialized_2']), 64)

def _test_getblockheader(self):
node = self.nodes[0]
Expand Down
96 changes: 96 additions & 0 deletions qa/rpc-tests/dumptxoutset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# Copyright (c) 2019-2022 The Bitcoin Core developers
# Copyright (c) 2024 The Dogecoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

"""
Test the dumptxoutset RPC call.
"""

import os

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_raises_jsonrpc,
start_node,
)


class DumptxoutsetTest(BitcoinTestFramework):

def __init__(self):
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 1

def setup_network(self, split=False):
self.nodes = []
self.nodes.append(start_node(0, self.options.tmpdir, []))
self.is_network_split = False

def run_test(self):
"""Test basic dumptxoutset functionality."""
node = self.nodes[0]

# Mine some blocks so there are UTXOs to dump
node.generate(110)

# Determine the output path inside the node's datadir
datadir = os.path.join(self.options.tmpdir, "node0")
out_path = os.path.join(datadir, "utxos.dat")
temp_path = out_path + ".incomplete"

# Call dumptxoutset
result = node.dumptxoutset(out_path)

# Validate return fields
assert_equal(result['base_height'], 110)
assert_equal(len(result['base_hash']), 64)
assert_equal(result['path'], out_path)
assert_greater_than(result['coins_written'], 0)
# 110 coinbase transactions = 110 UTXOs (one per block)
assert_equal(result['coins_written'], 110)
# New fields added in session 5
assert_equal(len(result['txoutset_hash']), 64)
assert_greater_than(result['nchaintx'], 0)

# The final file should exist
assert os.path.exists(out_path), "snapshot file was not created"

# The temporary (.incomplete) file should have been renamed away
assert not os.path.exists(temp_path), ".incomplete file still present"

# File must be non-empty
assert_greater_than(os.path.getsize(out_path), 0)

# Calling again with the same path should fail
assert_raises_jsonrpc(
-8,
"already exists",
node.dumptxoutset,
out_path,
)

# Passing a relative path returns an absolute path in the result
result2 = node.dumptxoutset("utxos2.dat")
assert os.path.isabs(result2['path'])
assert result2['path'].endswith("utxos2.dat")
assert os.path.exists(result2['path'])

# Invalid output directories should fail cleanly
invalid_path = os.path.join(datadir, "missing", "utxos3.dat")
assert_raises_jsonrpc(
-8,
"Couldn't open file",
node.dumptxoutset,
invalid_path,
)

print("dumptxoutset tests passed")


if __name__ == '__main__':
DumptxoutsetTest().main()
108 changes: 108 additions & 0 deletions qa/rpc-tests/loadtxoutset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) 2024 The Dogecoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

"""
Test the loadtxoutset RPC call.
"""

import os

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_jsonrpc,
start_node,
)


class LoadtxoutsetTest(BitcoinTestFramework):

def __init__(self):
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 1

def setup_network(self, split=False):
self.nodes = []
self.nodes.append(start_node(0, self.options.tmpdir, []))
self.is_network_split = False

def run_test(self):
node = self.nodes[0]

node.generate(110)
datadir = os.path.join(self.options.tmpdir, "node0")
snapshot_path = os.path.join(datadir, "utxos-load.dat")

missing_snapshot = os.path.join(datadir, "does-not-exist.dat")
assert_raises_jsonrpc(
-8,
"Couldn't open file",
node.loadtxoutset,
missing_snapshot,
"00" * 32,
)

dump_result = node.dumptxoutset(snapshot_path)
snapshot_info = node.gettxoutsetinfo()

assert_equal(dump_result["base_height"], 110)
assert_equal(snapshot_info["hash_serialized_2"], dump_result["txoutset_hash"])

bad_magic_path = snapshot_path + ".badmagic"
with open(snapshot_path, "rb") as snapshot_file:
snapshot_bytes = bytearray(snapshot_file.read())
snapshot_bytes[0] ^= 0x01
with open(bad_magic_path, "wb") as snapshot_file:
snapshot_file.write(snapshot_bytes)
assert_raises_jsonrpc(
-22,
"Unable to parse metadata: Invalid UTXO snapshot magic bytes",
node.loadtxoutset,
bad_magic_path,
dump_result["txoutset_hash"],
)

bad_network_path = snapshot_path + ".badnet"
with open(snapshot_path, "rb") as snapshot_file:
snapshot_bytes = bytearray(snapshot_file.read())
snapshot_bytes[7:11] = b"\x00\x00\x00\x00"
with open(bad_network_path, "wb") as snapshot_file:
snapshot_file.write(snapshot_bytes)
assert_raises_jsonrpc(
-8,
"Snapshot network does not match this node",
node.loadtxoutset,
bad_network_path,
dump_result["txoutset_hash"],
)

node.generate(5)
assert_equal(node.getblockcount(), 115)

assert_raises_jsonrpc(
-8,
"Snapshot content hash mismatch",
node.loadtxoutset,
snapshot_path,
"00" * 32,
)

load_result = node.loadtxoutset(snapshot_path, dump_result["txoutset_hash"])

assert_equal(load_result["coins_loaded"], dump_result["coins_written"])
assert_equal(load_result["base_height"], dump_result["base_height"])
assert_equal(load_result["base_hash"], dump_result["base_hash"])
assert_equal(load_result["path"], snapshot_path)

restored_info = node.gettxoutsetinfo()
assert_equal(node.getblockcount(), 110)
assert_equal(restored_info["bestblock"], snapshot_info["bestblock"])
assert_equal(restored_info["transactions"], snapshot_info["transactions"])
assert_equal(restored_info["txouts"], snapshot_info["txouts"])
assert_equal(restored_info["hash_serialized_2"], snapshot_info["hash_serialized_2"])

if __name__ == '__main__':
LoadtxoutsetTest().main()
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ BITCOIN_CORE_H = \
netaddress.h \
netbase.h \
netmessagemaker.h \
node/utxo_snapshot.h \
noui.h \
policy/fees.h \
policy/policy.h \
Expand Down
4 changes: 2 additions & 2 deletions src/bench/ccoins_caching.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ SetupDummyInputs(CBasicKeyStore& keystoreRet, CCoinsViewCache& coinsRet)
dummyTransactions[0].vout[0].scriptPubKey << ToByteVector(key[0].GetPubKey()) << OP_CHECKSIG;
dummyTransactions[0].vout[1].nValue = 50 * CENT;
dummyTransactions[0].vout[1].scriptPubKey << ToByteVector(key[1].GetPubKey()) << OP_CHECKSIG;
coinsRet.ModifyCoins(dummyTransactions[0].GetHash())->FromTx(dummyTransactions[0], 0);
AddCoins(coinsRet, dummyTransactions[0], 0);

dummyTransactions[1].vout.resize(2);
dummyTransactions[1].vout[0].nValue = 21 * CENT;
dummyTransactions[1].vout[0].scriptPubKey = GetScriptForDestination(key[2].GetPubKey().GetID());
dummyTransactions[1].vout[1].nValue = 22 * CENT;
dummyTransactions[1].vout[1].scriptPubKey = GetScriptForDestination(key[3].GetPubKey().GetID());
coinsRet.ModifyCoins(dummyTransactions[1].GetHash())->FromTx(dummyTransactions[1], 0);
AddCoins(coinsRet, dummyTransactions[1], 0);

return dummyTransactions;
}
Expand Down
26 changes: 14 additions & 12 deletions src/bitcoin-tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -576,24 +576,26 @@ static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr)
if (nOut < 0)
throw std::runtime_error("vout must be positive");

COutPoint out(txid, nOut);
std::vector<unsigned char> pkData(ParseHexUV(prevOut["scriptPubKey"], "scriptPubKey"));
CScript scriptPubKey(pkData.begin(), pkData.end());

{
CCoinsModifier coins = view.ModifyCoins(txid);
if (coins->IsAvailable(nOut) && coins->vout[nOut].scriptPubKey != scriptPubKey) {
const Coin& coin = view.AccessCoin(out);
if (!coin.IsSpent() && coin.out.scriptPubKey != scriptPubKey) {
std::string err("Previous output scriptPubKey mismatch:\n");
err = err + ScriptToAsmStr(coins->vout[nOut].scriptPubKey) + "\nvs:\n"+
err = err + ScriptToAsmStr(coin.out.scriptPubKey) + "\nvs:\n"+
ScriptToAsmStr(scriptPubKey);
throw std::runtime_error(err);
}
if ((unsigned int)nOut >= coins->vout.size())
coins->vout.resize(nOut+1);
coins->vout[nOut].scriptPubKey = scriptPubKey;
coins->vout[nOut].nValue = 0;
Coin newcoin;
newcoin.out.scriptPubKey = scriptPubKey;
newcoin.out.nValue = 0;
if (prevOut.exists("amount")) {
coins->vout[nOut].nValue = AmountFromValue(prevOut["amount"]);
newcoin.out.nValue = AmountFromValue(prevOut["amount"]);
}
newcoin.nHeight = 1;
view.AddCoin(out, std::move(newcoin), true);
}

// if redeemScript given and private keys given,
Expand All @@ -615,13 +617,13 @@ static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr)
// Sign what we can:
for (unsigned int i = 0; i < mergedTx.vin.size(); i++) {
CTxIn& txin = mergedTx.vin[i];
const CCoins* coins = view.AccessCoins(txin.prevout.hash);
if (!coins || !coins->IsAvailable(txin.prevout.n)) {
const Coin& coin = view.AccessCoin(txin.prevout);
if (coin.IsSpent()) {
fComplete = false;
continue;
}
const CScript& prevPubKey = coins->vout[txin.prevout.n].scriptPubKey;
const CAmount& amount = coins->vout[txin.prevout.n].nValue;
const CScript& prevPubKey = coin.out.scriptPubKey;
const CAmount& amount = coin.out.nValue;

SignatureData sigdata;
// Only sign SIGHASH_SINGLE if there's a corresponding output:
Expand Down
Loading