Skip to content

BIP352 index (for light wallets and consistency check) #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: bip352-pr-28122
Choose a base branch
from
Open
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 src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
httpserver.cpp
i2p.cpp
index/base.cpp
index/bip352.cpp
index/blockfilterindex.cpp
index/coinstatsindex.cpp
index/txindex.cpp
Expand Down Expand Up @@ -325,6 +326,7 @@ target_link_libraries(bitcoin_node
$<TARGET_NAME_IF_EXISTS:bitcoin_zmq>
leveldb
minisketch
secp256k1
univalue
Boost::headers
$<TARGET_NAME_IF_EXISTS:libevent::core>
Expand Down
12 changes: 12 additions & 0 deletions src/common/bip352.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ std::optional<PublicData> CreateInputPubkeysTweak(
return public_data;
}

bool MaybeSilentPayment(const CTransactionRef &tx) {
if (tx->IsCoinBase()) return false;

if (std::none_of(tx->vout.begin(), tx->vout.end(), [](const CTxOut& txout) {
return txout.scriptPubKey.IsPayToTaproot();
})) {
return false;
}

return true;
}

std::optional<PublicData> GetSilentPaymentsPublicData(const std::vector<CTxIn>& vin, const std::map<COutPoint, Coin>& coins)
{
// Extract the keys from the inputs
Expand Down
11 changes: 11 additions & 0 deletions src/common/bip352.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ std::pair<CPubKey, uint256> CreateLabelTweak(const CKey& scan_key, const int m);
*/
V0SilentPaymentDestination GenerateSilentPaymentLabeledAddress(const V0SilentPaymentDestination& recipient, const uint256& label);

/**
* @brief: Check if a transaction could be a silent payment.
*
* A coinbase transaction can't be a silent payment.
* A transaction with no Taproot outputs can't be a silent payment.
*
* @param tx The transaction to check
* @return false if a transaction can't be a silent payment, true otherwise.
*/
bool MaybeSilentPayment(const CTransactionRef &tx);

/**
* @brief Get silent payment public data from transaction inputs.
*
Expand Down
20 changes: 15 additions & 5 deletions src/index/base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ void BaseIndex::DB::WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator
batch.Write(DB_BEST_BLOCK, locator);
}

BaseIndex::BaseIndex(std::unique_ptr<interfaces::Chain> chain, std::string name)
: m_chain{std::move(chain)}, m_name{std::move(name)} {}
BaseIndex::BaseIndex(std::unique_ptr<interfaces::Chain> chain, std::string name, int start_height)
: m_chain{std::move(chain)}, m_name{std::move(name)}, m_start_height{std::move(start_height)} {}

BaseIndex::~BaseIndex()
{
Expand Down Expand Up @@ -131,11 +131,16 @@ bool BaseIndex::Init()
return true;
}

static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain& chain, int start_height) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
{
AssertLockHeld(cs_main);

if (!pindex_prev) {
// pindex_prev is null, we are starting our sync. Return the genesis block
// or start from the specified start_height.
if (start_height > 0) {
return chain[start_height];
}
return chain.Genesis();
}

Expand Down Expand Up @@ -200,7 +205,7 @@ void BaseIndex::Sync()
return;
}

const CBlockIndex* pindex_next = WITH_LOCK(cs_main, return NextSyncBlock(pindex, m_chainstate->m_chain));
const CBlockIndex* pindex_next = WITH_LOCK(cs_main, return NextSyncBlock(pindex, m_chainstate->m_chain, m_start_height));
// If pindex_next is null, it means pindex is the chain tip, so
// commit data indexed so far.
if (!pindex_next) {
Expand All @@ -214,12 +219,17 @@ void BaseIndex::Sync()
// attached while m_synced is still false, and it would not be
// indexed.
LOCK(::cs_main);
pindex_next = NextSyncBlock(pindex, m_chainstate->m_chain);
if (!pindex_next) {
m_synced = true;
break;
}
}
// If pindex_next is our first block and we are starting from a custom height,
// set pindex to be the previous block. This ensures we test that we can still rewind
// from our custom start height in the event of a reorg.
if (pindex_next->nHeight == m_start_height && m_start_height > 0) {
pindex = pindex_next->pprev;
}
if (pindex_next->pprev != pindex && !Rewind(pindex, pindex_next->pprev)) {
FatalErrorf("Failed to rewind %s to a previous chain tip", GetName());
return;
Expand Down
3 changes: 2 additions & 1 deletion src/index/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class BaseIndex : public CValidationInterface
std::unique_ptr<interfaces::Chain> m_chain;
Chainstate* m_chainstate{nullptr};
const std::string m_name;
const int m_start_height{0};

void BlockConnected(ChainstateRole role, const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) override;

Expand Down Expand Up @@ -131,7 +132,7 @@ class BaseIndex : public CValidationInterface
void SetBestBlockIndex(const CBlockIndex* block);

public:
BaseIndex(std::unique_ptr<interfaces::Chain> chain, std::string name);
BaseIndex(std::unique_ptr<interfaces::Chain> chain, std::string name, int start_height = 0);
/// Destructor interrupts sync thread if running and blocks until it exits.
virtual ~BaseIndex();

Expand Down
144 changes: 144 additions & 0 deletions src/index/bip352.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <index/bip352.h>

#include <common/bip352.h>
#include <chainparams.h>
#include <coins.h>
#include <common/args.h>
#include <index/disktxpos.h>
#include <node/blockstorage.h>
#include <pubkey.h>
#include <primitives/transaction.h>

#include <undo.h>
#include <util/fs.h>
#include <validation.h>

#include <dbwrapper.h>
#include <hash.h>

constexpr uint8_t DB_SILENT_PAYMENT_INDEX{'s'};
/* Save space on mainnet by starting the index at Taproot activation.
* Copying the height here assuming DEPLOYMENT_TAPROOT will be dropped:
* https://github.com/bitcoin/bitcoin/pull/26201/
* Only apply this storage optimization on mainnet.
*/
const int TAPROOT_MAINNET_ACTIVATION_HEIGHT{709632};

std::unique_ptr<BIP352Index> g_bip352_index;
std::unique_ptr<BIP352Index> g_bip352_ct_index;

/** Access to the silent payment index database (indexes/bip352/) */
class BIP352Index::DB : public BaseIndex::DB
{
public:
explicit DB(fs::path file_name, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);

bool WriteSilentPayments(const std::pair<uint256, tweak_index_entry>& kv);
};

BIP352Index::DB::DB(fs::path file_name, size_t n_cache_size, bool f_memory, bool f_wipe) :
BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / file_name, n_cache_size, f_memory, f_wipe)
{}

bool BIP352Index::DB::WriteSilentPayments(const std::pair<uint256, tweak_index_entry>& kv)
{
CDBBatch batch(*this);
batch.Write(std::make_pair(DB_SILENT_PAYMENT_INDEX, kv.first), kv.second);
return WriteBatch(batch);
}

BIP352Index::BIP352Index(bool cut_through, std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
: BaseIndex(std::move(chain), strprintf("bip352 %sindex", cut_through ? "cut-through " : ""), /*start_height=*/Params().IsTestChain() ? 0 : TAPROOT_MAINNET_ACTIVATION_HEIGHT), m_db(std::make_unique<BIP352Index::DB>(fs::u8path(strprintf("bip352%s", cut_through ? "ct" : "")), n_cache_size, f_memory, f_wipe))
{
m_cut_through = cut_through;
}

BIP352Index::~BIP352Index() = default;

bool BIP352Index::GetSilentPaymentKeys(const std::vector<CTransactionRef>& txs, const CBlockUndo& block_undo, tweak_index_entry& index_entry) const
{
assert(txs.size() - 1 == block_undo.vtxundo.size());

for (size_t i=0; i < txs.size(); i++) {
auto& tx = txs.at(i);

if (!bip352::MaybeSilentPayment(tx)) continue;

// -1 as blockundo does not have coinbase tx
CTxUndo undoTX{block_undo.vtxundo.at(i - 1)};
std::map<COutPoint, Coin> coins;

for (size_t j = 0; j < tx->vin.size(); j++) {
coins[tx->vin.at(j).prevout] = undoTX.vprevout.at(j);
}

std::optional<CPubKey> tweaked_pk = bip352::GetSerializedSilentPaymentsPublicData(tx->vin, coins);
if (tweaked_pk) {
// Used to filter dust. To keep the index small we use only one byte
// and measure in hexasats.
uint8_t max_output_hsat = 0;
for (const CTxOut& txout : tx->vout) {
if (!txout.scriptPubKey.IsPayToTaproot()) continue;
uint8_t output_hsat = txout.nValue > max_dust_threshold ? UINT8_MAX : txout.nValue >> dust_shift;
max_output_hsat = std::max(output_hsat, max_output_hsat);
}

if (m_cut_through) {
// Skip entry if all outputs have been spent.
// This is only effective when the index is generated while
// the tip is far ahead.
//
// This is done after calculating the tweak in order to minimize
// the number of UTXO lookups.
LOCK(cs_main);
const CCoinsViewCache& coins_cache = m_chainstate->CoinsTip();

uint32_t spent{0};
for (size_t j{0}; j < tx->vout.size(); j++) {
COutPoint outpoint(tx->GetHash(), j);
// Many new blocks may be processed while generating the index,
// in between HaveCoin calls. This is not a problem, because
// the cut-through index can safely have false positives.
if (!coins_cache.HaveCoin(outpoint)) spent++;
}
if (spent == tx->vout.size()) continue;
}
index_entry.emplace_back(std::make_pair(tweaked_pk.value(), max_output_hsat));
}
}

return true;
}

interfaces::Chain::NotifyOptions BIP352Index::CustomOptions()
{
interfaces::Chain::NotifyOptions options;
options.connect_undo_data = true;
return options;
}

bool BIP352Index::CustomAppend(const interfaces::BlockInfo& block)
{
// Exclude genesis block transaction because outputs are not spendable. This
// is needed on non-mainnet chains because m_start_height is 0 by default.
if (block.height == 0) return true;

// Exclude pre-taproot
if (block.height < m_start_height) return true;

tweak_index_entry index_entry;
GetSilentPaymentKeys(Assert(block.data)->vtx, *Assert(block.undo_data), index_entry);

return m_db->WriteSilentPayments(make_pair(block.hash, index_entry));
}

bool BIP352Index::FindSilentPayment(const uint256& block_hash, tweak_index_entry& index_entry) const
{
return m_db->Read(std::make_pair(DB_SILENT_PAYMENT_INDEX, block_hash), index_entry);
}

BaseIndex::DB& BIP352Index::GetDB() const { return *m_db; }
83 changes: 83 additions & 0 deletions src/index/bip352.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_INDEX_BIP352_H
#define BITCOIN_INDEX_BIP352_H

#include <coins.h>
#include <index/base.h>
#include <pubkey.h>

class COutPoint;
class CBlockUndo;

static constexpr bool DEFAULT_BIP352_INDEX{false};
static constexpr bool DEFAULT_BIP352_CT_INDEX{false};

/**
* This index is used to look up the tweaked sum of eligible public keys for a
* given transaction hash. See BIP352.
*
* Currently only silent payments v0 exists. Future versions may expand the
* existing index or create a (perhaps overlapping) new one.
*/
class BIP352Index final : public BaseIndex
{
public:
/**
* For each block hash we store and array of transactions. For each
* transaction we store:
* - the tweaked pubkey sum
* - the highest output amount (for dust filtering)
*/
using tweak_index_entry = std::vector<std::pair<CPubKey, uint8_t>>;
static constexpr uint8_t dust_shift{4}; // Hexasat
/* Maximum dust threshold that we can filter */
static constexpr CAmount max_dust_threshold{CAmount(UINT8_MAX - 1) << dust_shift};

class DB;

private:
const std::unique_ptr<DB> m_db;

/** Whether this index has transaction cut-through enabled */
bool m_cut_through{false};

bool AllowPrune() const override { return false; }

/**
* Derive the silent payment tweaked public key for every block transaction.
*
*
* @param[in] txs all block transactions
* @param[in] block_undo block undo data
* @param[out] index_entry the tweaked public keys, only for transactions that have one
* @return false if something went wrong
*/
bool GetSilentPaymentKeys(const std::vector<CTransactionRef>& txs, const CBlockUndo& block_undo, tweak_index_entry& index_entry) const;

protected:
interfaces::Chain::NotifyOptions CustomOptions() override;

bool CustomAppend(const interfaces::BlockInfo& block) override;

BaseIndex::DB& GetDB() const override;
public:

explicit BIP352Index(bool cut_through, std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);

// Destructor is declared because this class contains a unique_ptr to an incomplete type.
virtual ~BIP352Index() override;

bool FindSilentPayment(const uint256& block_hash, tweak_index_entry& index_entry) const;
};

/// The global BIP325 index. May be null.
extern std::unique_ptr<BIP352Index> g_bip352_index;

/// The global BIP325 with transaction cut-through index. May be null.
extern std::unique_ptr<BIP352Index> g_bip352_ct_index;


#endif // BITCOIN_INDEX_BIP352_H
Loading