前两篇介绍了UXTO表示以及CCoinViewCache
的使用:Bitcoin UTXO代码分析(一):UTXO的相关表示和Bitcoin UTXO代码分析(二):CCoinsViewCache,这篇文章主要介绍UTXO和其他模块的交互:新块被激活的时候如何更新UTXO,内存池中的交易和UTXO如何交互,以及UTXO的存储。
Blockchain激活
Blockchain发生变化时UTXO相关的逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36bool CChainState::ConnectTip(CValidationState& state, const CChainParams& chainparams, CBlockIndex* pindexNew, const std::shared_ptr<const CBlock>& pblock, ConnectTrace& connectTrace, DisconnectedBlockTransactions &disconnectpool)
{
....
{
CCoinsViewCache view(pcoinsTip.get());
bool rv = ConnectBlock(blockConnecting, state, pindexNew, view, chainparams);
GetMainSignals().BlockChecked(blockConnecting, state);
if (!rv) {
if (state.IsInvalid())
InvalidBlockFound(pindexNew, state);
return error("ConnectTip(): ConnectBlock %s failed", pindexNew->GetBlockHash().ToString());
}
nTime3 = GetTimeMicros(); nTimeConnectTotal += nTime3 - nTime2;
LogPrint(BCLog::BENCH, " - Connect total: %.2fms [%.2fs (%.2fms/blk)]\n", (nTime3 - nTime2) * MILLI, nTimeConnectTotal * MICRO, nTimeConnectTotal * MILLI / nBlocksTotal);
bool flushed = view.Flush();
assert(flushed);
}
.....
}
bool CChainState::DisconnectTip(CValidationState& state, const CChainParams& chainparams, DisconnectedBlockTransactions *disconnectpool)
{
...
{
CCoinsViewCache view(pcoinsTip.get());
assert(view.GetBestBlock() == pindexDelete->GetBlockHash());
if (DisconnectBlock(block, pindexDelete, view) != DISCONNECT_OK)
return error("DisconnectTip(): DisconnectBlock %s failed", pindexDelete->GetBlockHash().ToString());
bool flushed = view.Flush();
assert(flushed);
}
...
}
在blockchain结构重新组织,当前activeChain
发生变化时,某些block会链接上activeChain, 某些block会断掉跟activeChain的链接,内部会调用CChainState::ConnectTip
和CChainState::DisconnectTip
,这里会生成临时的CCoinsViewCache对象,后端连接上全局的另一个CCoinsViewCache实例pcoinsTip
, 在调用ConnectBlock,DisconnectBlock后,更新自己的状态到pcoinsTip
。
Mempool相关
有一个对象专门用来处理Mempool相关的UTXO,对象为CCoinsViewMemPool
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CCoinsViewMemPool : public CCoinsViewBacked
{
protected:
const CTxMemPool& mempool;
public:
CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn);
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
};
{
...
CCoinsView viewDummy;
CCoinsViewCache view(&viewDummy);
{
LOCK(mempool.cs);
CCoinsViewCache &viewChain = *pcoinsTip;
CCoinsViewMemPool viewMempool(&viewChain, mempool);
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
for (const CTxIn& txin : mtx.vin) {
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
}
view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long
}
...
}
在rpc请求时,比如:signrawtransaction,combinerawtransaction,构造了临时viewcache对象, 临时viewMempool对象CCoinsViewMemPool
,CCoinsViewMemPool
被设为view的后端,这样确保mtx交易的父交易,数据来源包括当前activeChain的内存部分,磁盘部分,还有mempool。gettxout
请求:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16UniValue gettxout(const JSONRPCRequest& request)
{
...
if (fMempool) {
LOCK(mempool.cs);
CCoinsViewMemPool view(pcoinsTip.get(), mempool);
if (!view.GetCoin(out, coin) || mempool.isSpent(out)) {
return NullUniValue;
}
} else {
if (!pcoinsTip->GetCoin(out, coin)) {
return NullUniValue;
}
}
...
}
rpc请求gettxout处理时,如果include_mempool
为true,会构造临时ViewMemPool,方便外部用户查询utxo时,引入mempool中的utxo。
检查timelock交易的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24bool CheckSequenceLocks(const CTransaction &tx, int flags, LockPoints* lp, bool useExistingLockPoints)
{
...
{
CCoinsViewMemPool viewMemPool(pcoinsTip.get(), mempool);
std::vector<int> prevheights;
prevheights.resize(tx.vin.size());
for (size_t txinIndex = 0; txinIndex < tx.vin.size(); txinIndex++) {
const CTxIn& txin = tx.vin[txinIndex];
Coin coin;
if (!viewMemPool.GetCoin(txin.prevout, coin)) {
return error("%s: Missing input", __func__);
}
if (coin.nHeight == MEMPOOL_HEIGHT) {
// Assume all mempool transaction confirm in the next block
prevheights[txinIndex] = tip->nHeight + 1;
} else {
prevheights[txinIndex] = coin.nHeight;
}
}
}
...
}
评估使用timelock
的交易时,需要再次构造临时ViewMemPool,查询当前blockchain tip对应的coin集合与内存池的coin集合。
有交易进入内存池时:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool& pool, CValidationState& state, const CTransactionRef& ptx,
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
bool bypass_limits, const CAmount& nAbsurdFee, std::vector<COutPoint>& coins_to_uncache)
{
.....
CCoinsView dummy;
CCoinsViewCache view(&dummy);
LockPoints lp;
{
LOCK(pool.cs);
CCoinsViewMemPool viewMemPool(pcoinsTip.get(), pool);
view.SetBackend(viewMemPool);
// do all inputs exist?
for (const CTxIn txin : tx.vin) {
if (!pcoinsTip->HaveCoinInCache(txin.prevout)) {
coins_to_uncache.push_back(txin.prevout);
}
if (!view.HaveCoin(txin.prevout)) {
// Are inputs missing because we already have the tx?
for (size_t out = 0; out < tx.vout.size(); out++) {
// Optimistically just do efficient check of cache for outputs
if (pcoinsTip->HaveCoinInCache(COutPoint(hash, out))) {
return state.Invalid(false, REJECT_DUPLICATE, "txn-already-known");
}
}
// Otherwise assume this might be an orphan tx for which we just haven't seen parents yet
if (pfMissingInputs) {
*pfMissingInputs = true;
}
return false; // fMissingInputs and !state.IsInvalid() is used to detect this condition, don't set state.Invalid()
}
}
// Bring the best block into scope
view.GetBestBlock();
view.SetBackend(dummy);
......
}
在AcceptToMemoryPoolWorker
中,构造了临时ccoinsviewcache对象view,
临时ccoinsviewMemPool对象viewmempool,view 后端数据来源是viewmempool。
UTXO的存储
utxo 的磁盘存储使用了leveldb 键值对数据库, 键的序列化格式:
C[32 bytes of outpoint->hash][varint(outpoint->n]
1 | struct CoinEntry { |
value 对应的存储格式是就是coin 对象的序列化形式:
VARINT((coinbase?1: 0) | (height <<1))
CTxout 的序列化格式(使用CTxOutCompressor 类定制特殊压缩方式)。
1 | template<typename Stream> |
在每一个CTxout 对象本身之前, 加上了一个变长编码的code = nHeight * 2 + fCoinBase
数字,接着就是txcout 本身:
1 | class CTxOutCompressor |
接着是被压缩的数目, 使用CompressAmount
成员函数中的算法:
1 | uint64_t CTxOutCompressor::CompressAmount(uint64_t n) |
CTxout 中的锁定脚本使用CScriptCompressor 对象压缩存储:1
2
3
4
5
6
7
8
9
10
11
12
13
14class CScriptCompressor
{
template<typename Stream>
void Serialize(Stream &s) const {
std::vector<unsigned char> compr;
if (Compress(compr)) {
s << CFlatData(compr);
return;
}
unsigned int nSize = script.size() + nSpecialScripts;
s << VARINT(nSize);
s << CFlatData(script);
}
}
基本思路, 对于p2pkh 类型的脚本, 比如:
OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG,
压缩成:
[0x00][ab68025513c3dbd2f7b92a94e0581f5d50f654e7]
总共21 字节。
p2sh 类型脚本, 比如:
OP_HASH160 20 0x620a6eeaf538ec9eb89b6ae83f2ed8ef98566a03 OP_EQUAL
压缩成:
[0x01][620a6eeaf538ec9eb89b6ae83f2ed8ef98566a03]
总共21字节。
p2pk 类型脚本, 比如:
33 0x022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da OP_CHECKSIG
压缩成:
[0x2][2df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da]
总共33 字节, 未压缩的p2pk脚本, 首字节是0x04|(pubkey[64]&0x01), 接着是32字节的pubkey的x 坐标。
对于其它不能被压缩的脚本, 如segwit 的scriptPubKey,采用下面方法:1
2
3unsigned int nSize = script.size() + nSpecialScripts;
s << VARINT(nSize);
s << CFlatData(script);
本文由 Copernicus团队
喻建写作,转载无需授权