Skip to content

Commit

Permalink
Set up FCMP++ validation
Browse files Browse the repository at this point in the history
CRITICAL FIXME's:
- sum of inputs == sum of outputs (requires using an updated
rerandomized output and blinded C Blind at tx construction time)
- serialize tree root for rct_ver_cache_t (planning to use the
compressed tree root and then de-compress at verification time)

Planning to rebase onto monero-project#9135
  • Loading branch information
j-berman committed Feb 6, 2025
1 parent 94235d1 commit b5b1492
Show file tree
Hide file tree
Showing 20 changed files with 400 additions and 111 deletions.
1 change: 1 addition & 0 deletions src/blockchain_db/blockchain_db.h
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,7 @@ class BlockchainDB
virtual uint64_t get_n_leaf_tuples() const = 0;
virtual uint64_t get_block_n_leaf_tuples(const uint64_t block_idx) const = 0;
virtual crypto::ec_point get_tree_root() const = 0;
virtual std::size_t get_tree_root_at_blk_idx(const uint64_t blk_idx, uint8_t *&tree_root_out) const = 0;

/**
* @brief return custom timelocked outputs after the provided block idx
Expand Down
34 changes: 31 additions & 3 deletions src/blockchain_db/lmdb/db_lmdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ void BlockchainLMDB::add_block(const block& blk, size_t block_weight, uint64_t l
}
bi.bi_long_term_block_weight = long_term_block_weight;
bi.bi_n_leaf_tuples = this->get_n_leaf_tuples();
bi.bi_tree_root = this->get_tree_root();
bi.bi_tree_root = this->get_tree_root(); // TODO: grow_tree should return new root and n leaf tuples

MDB_val_set(val, bi);
result = mdb_cursor_put(m_cur_block_info, (MDB_val *)&zerokval, &val, MDB_APPENDDUP);
Expand Down Expand Up @@ -1969,6 +1969,7 @@ uint64_t BlockchainLMDB::get_block_n_leaf_tuples(const uint64_t block_idx) const
return n_leaf_tuples;
}

// TODO: remove this function for cleanup. Can be replaced by having grow_tree return new root
crypto::ec_point BlockchainLMDB::get_tree_root() const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
Expand All @@ -1977,7 +1978,7 @@ crypto::ec_point BlockchainLMDB::get_tree_root() const
TXN_PREFIX_RDONLY();
RCURSOR(layers)

crypto::ec_point root;
crypto::ec_point root{};

{
MDB_val k, v;
Expand All @@ -1988,14 +1989,41 @@ crypto::ec_point BlockchainLMDB::get_tree_root() const
root = std::move(lv->child_chunk_hash);
}
else if (result != MDB_NOTFOUND)
throw0(DB_ERROR(lmdb_error("Failed to get last leaf: ", result).c_str()));
throw0(DB_ERROR(lmdb_error("Failed to get last layer elem (the expected root): ", result).c_str()));
}

TXN_POSTFIX_RDONLY();

return root;
}

// TODO: don't return de-compressed data anywhere from DB interface. DB interface should return what the DB knows
std::size_t BlockchainLMDB::get_tree_root_at_blk_idx(const uint64_t blk_idx, uint8_t *&tree_root_out) const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
check_open();

TXN_PREFIX_RDONLY();
RCURSOR(block_info);

MDB_val_set(result, blk_idx);
auto get_result = mdb_cursor_get(m_cur_block_info, (MDB_val *)&zerokval, &result, MDB_GET_BOTH);
if (get_result == MDB_NOTFOUND)
throw0(BLOCK_DNE(std::string("Attempt to get tree root from blk idx ").append(boost::lexical_cast<std::string>(blk_idx)).append(" failed -- tree root not in db").c_str()));
else if (get_result)
throw0(DB_ERROR("Error attempting to retrieve a tree root from the db"));

mdb_block_info *bi = (mdb_block_info *)result.mv_data;
const crypto::ec_point root = bi->bi_tree_root;
const std::size_t n_tree_layers = m_curve_trees->n_layers(bi->bi_n_leaf_tuples);
tree_root_out = m_curve_trees->get_tree_root_from_bytes(n_tree_layers, root);
if (tree_root_out == nullptr)
throw0(DB_ERROR("Error reading tree root from bytes"));
TXN_POSTFIX_RDONLY();

return n_tree_layers;
}

fcmp_pp::curve_trees::CurveTreesV1::LastHashes BlockchainLMDB::get_tree_last_hashes() const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
Expand Down
1 change: 1 addition & 0 deletions src/blockchain_db/lmdb/db_lmdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ class BlockchainLMDB : public BlockchainDB
uint64_t get_block_n_leaf_tuples(uint64_t block_idx) const;

virtual crypto::ec_point get_tree_root() const;
virtual std::size_t get_tree_root_at_blk_idx(const uint64_t blk_idx, uint8_t *&tree_root_out) const;

fcmp_pp::curve_trees::CurveTreesV1::LastHashes get_tree_last_hashes() const;

Expand Down
1 change: 1 addition & 0 deletions src/blockchain_db/testdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class BaseTestDB: public cryptonote::BlockchainDB {
virtual void trim_tree(const uint64_t new_n_leaf_tuples, const uint64_t trim_block_id) override {};
virtual bool audit_tree(const uint64_t expected_n_leaf_tuples) const override { return false; };
virtual crypto::ec_point get_tree_root() const override { return {}; };
virtual std::size_t get_tree_root_at_blk_idx(const uint64_t blk_idx, uint8_t *&tree_root_out) const override { return {}; };
virtual uint64_t get_n_leaf_tuples() const override { return 0; };
virtual uint64_t get_block_n_leaf_tuples(const uint64_t block_idx) const override { return 0; };
virtual fcmp_pp::curve_trees::OutputsByLastLockedBlock get_custom_timelocked_outputs(uint64_t start_block_idx) const override { return {{}}; };
Expand Down
7 changes: 7 additions & 0 deletions src/cryptonote_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@
#define BULLETPROOF_MAX_OUTPUTS 16
#define BULLETPROOF_PLUS_MAX_OUTPUTS 16

// TODO: settle on figures here
// https://gist.github.com/kayabaNerve/dbbadf1f2b0f4e04732fc5ac559745b7
// https://gist.github.com/Rucknium/784b243d75184333144a92b3258788f6
#define FCMP_PLUS_PLUS_MAX_INPUTS 8
#define FCMP_PLUS_PLUS_MAX_OUTPUTS 8

#define CRYPTONOTE_PRUNING_STRIPE_SIZE 4096 // the smaller, the smoother the increase
#define CRYPTONOTE_PRUNING_LOG_STRIPES 3 // the higher, the more space saved
#define CRYPTONOTE_PRUNING_TIP_BLOCKS 5500 // the smaller, the more space saved
Expand Down Expand Up @@ -258,6 +264,7 @@ namespace config
const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS_SEED[] = "multisig_tx_privkeys_seed";
const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS[] = "multisig_tx_privkeys";
const constexpr char HASH_KEY_TXHASH_AND_MIXRING[] = "txhash_and_mixring";
const constexpr char HASH_KEY_TXHASH_AND_TREE_ROOT[] = "txhash_and_tree_root";

// Multisig
const uint32_t MULTISIG_MAX_SIGNERS{16};
Expand Down
151 changes: 121 additions & 30 deletions src/cryptonote_core/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,28 @@ static bool fill(BlockchainDB *db, const crypto::hash &tx_hash, tx_blob_entry &t
return true;
}
//------------------------------------------------------------------
static bool get_fcmp_tx_tree_root(BlockchainDB *db, const cryptonote::transaction &tx, uint8_t *&tree_root_out)
{
tree_root_out = nullptr;
if (!rct::is_rct_fcmp(tx.rct_signatures.type))
return true;

// Make sure reference block exists in the chain
CHECK_AND_ASSERT_MES(tx.rct_signatures.p.reference_block < db->height(), false,
"tx included reference block that was too high");

// Get the tree root and n tree layers at provided block
const std::size_t n_tree_layers = db->get_tree_root_at_blk_idx(tx.rct_signatures.p.reference_block, tree_root_out);
CHECK_AND_ASSERT_MES(tree_root_out != nullptr, false, "tree root was not set");

// Make sure the provided n tree layers matches expected
static_assert(sizeof(std::size_t) >= sizeof(uint8_t), "unexpected size of size_t");
CHECK_AND_ASSERT_MES((std::size_t)tx.rct_signatures.p.n_tree_layers == n_tree_layers, false,
"tx included incorrect number of tree layers");

return true;
}
//------------------------------------------------------------------
//TODO: return type should be void, throw on exception
// alternatively, return true only if no transactions missed
bool Blockchain::get_transactions_blobs(const std::vector<crypto::hash>& txs_ids, std::vector<cryptonote::blobdata>& txs, std::vector<crypto::hash>& missed_txs, bool pruned) const
Expand Down Expand Up @@ -3212,7 +3234,7 @@ bool Blockchain::have_tx_keyimges_as_spent(const transaction &tx) const
}
return false;
}
bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector<std::vector<rct::ctkey>> &pubkeys)
bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector<std::vector<rct::ctkey>> &pubkeys, uint8_t *tree_root)
{
PERF_TIMER(expand_transaction_2);
CHECK_AND_ASSERT_MES(tx.version == 2, false, "Transaction version is not 2");
Expand Down Expand Up @@ -3251,6 +3273,11 @@ bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_pr
}
}
}
else if (rv.type == rct::RCTTypeFcmpPlusPlus)
{
CHECK_AND_ASSERT_MES(pubkeys.empty(), false, "non-empty pubkeys");
CHECK_AND_ASSERT_MES(rv.mixRing.empty(), false, "non-empty mixRing");
}
else
{
CHECK_AND_ASSERT_MES(false, false, "Unsupported rct tx type: " + boost::lexical_cast<std::string>(rv.type));
Expand Down Expand Up @@ -3290,6 +3317,20 @@ bool Blockchain::expand_transaction_2(transaction &tx, const crypto::hash &tx_pr
}
}
}
else if (rv.type == rct::RCTTypeFcmpPlusPlus)
{
if (!tx.pruned)
{
CHECK_AND_ASSERT_MES(tree_root != nullptr, false, "tree_root is null");
rv.p.fcmp_ver_helper_data.tree_root = tree_root;
rv.p.fcmp_ver_helper_data.key_images.reserve(tx.vin.size());
for (size_t n = 0; n < tx.vin.size(); ++n)
{
crypto::key_image ki = boost::get<txin_to_key>(tx.vin[n]).k_image;
rv.p.fcmp_ver_helper_data.key_images.emplace_back(std::move(ki));
}
}
}
else
{
CHECK_AND_ASSERT_MES(false, false, "Unsupported rct tx type: " + boost::lexical_cast<std::string>(rv.type));
Expand Down Expand Up @@ -3334,11 +3375,33 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
}
}

// from hard fork 2, we require mixin at least 2 unless one output cannot mix with 2 others
// if one output cannot mix with 2 others, we accept at most 1 output that can mix
if (hf_version >= 2)
size_t n_unmixable = 0;

// after FCMP++ hard fork, require all inputs have 0 mixin
if (hf_version > HF_VERSION_FCMP_PLUS_PLUS)
{
size_t n_unmixable = 0, n_mixable = 0;
for (const auto& txin : tx.vin)
{
if (txin.type() == typeid(txin_to_key))
{
const txin_to_key& in_to_key = boost::get<txin_to_key>(txin);
if (!in_to_key.key_offsets.empty())
{
MERROR_VER("Tx " << get_transaction_hash(tx) << " has non-empty ring after FCMP++ fork");
tvc.m_invalid_input = true;
return false;
}
}
}
}
else if (hf_version >= 2)
{
// At HF_VERSION_FCMP_PLUS_PLUS, temporarily allow either 0 ring size or
// 16 to allow the transition to FCMP++.
// from hard fork 2 to HF_VERSION_FCMP_PLUS_PLUS, we require mixin at least
// 2 unless one output cannot mix with 2 others if one output cannot mix
// with 2 others, we accept at most 1 output that can mix
size_t n_mixable = 0;
size_t min_actual_mixin = std::numeric_limits<size_t>::max();
size_t max_actual_mixin = 0;
const size_t min_mixin = hf_version >= HF_VERSION_MIN_MIXIN_15 ? 15 : hf_version >= HF_VERSION_MIN_MIXIN_10 ? 10 : hf_version >= HF_VERSION_MIN_MIXIN_6 ? 6 : hf_version >= HF_VERSION_MIN_MIXIN_4 ? 4 : 2;
Expand All @@ -3348,6 +3411,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
if (txin.type() == typeid(txin_to_key))
{
const txin_to_key& in_to_key = boost::get<txin_to_key>(txin);
if (in_to_key.key_offsets.empty())
{
min_actual_mixin = 0;
continue;
}

if (in_to_key.amount == 0)
{
// always consider rct inputs mixable. Even if there's not enough rct
Expand Down Expand Up @@ -3384,11 +3453,16 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
}
}

// The only circumstance where ring sizes less than expected are
// allowed is when spending unmixable non-RCT outputs in the chain.
// Caveat: at HF_VERSION_MIN_MIXIN_15, temporarily allow ring sizes
// Before HF_VERSION_FCMP_PLUS_PLUS, the only circumstance where ring sizes
// less than expected are allowed is when spending unmixable non-RCT outputs
// in the chain.
// At HF_VERSION_MIN_MIXIN_15, temporarily allow ring sizes
// of 11 to allow a grace period in the transition to larger ring size.
if (min_actual_mixin < min_mixin && !(hf_version == HF_VERSION_MIN_MIXIN_15 && min_actual_mixin == 10))
if (hf_version >= HF_VERSION_FCMP_PLUS_PLUS && min_actual_mixin == 0 && max_actual_mixin == 0)
{
// 0 ring size is allowed at HF_VERSION_FCMP_PLUS_PLUS
}
else if (min_actual_mixin < min_mixin && !(hf_version == HF_VERSION_MIN_MIXIN_15 && min_actual_mixin == 10))
{
if (n_unmixable == 0)
{
Expand All @@ -3412,22 +3486,23 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
tvc.m_low_mixin = true;
return false;
}
}

// min/max tx version based on HF, and we accept v1 txes if having a non mixable
const size_t max_tx_version = (hf_version <= 3) ? 1 : 2;
if (tx.version > max_tx_version)
{
MERROR_VER("transaction version " << (unsigned)tx.version << " is higher than max accepted version " << max_tx_version);
tvc.m_verifivation_failed = true;
return false;
}
const size_t min_tx_version = (n_unmixable > 0 ? 1 : (hf_version >= HF_VERSION_ENFORCE_RCT) ? 2 : 1);
if (tx.version < min_tx_version)
{
MERROR_VER("transaction version " << (unsigned)tx.version << " is lower than min accepted version " << min_tx_version);
tvc.m_verifivation_failed = true;
return false;
}
// min/max tx version based on HF, and we accept v1 txes if having a non mixable
// TODO: double check sync from genesis
const size_t max_tx_version = (hf_version <= 3) ? 1 : 2;
if (tx.version > max_tx_version)
{
MERROR_VER("transaction version " << (unsigned)tx.version << " is higher than max accepted version " << max_tx_version);
tvc.m_verifivation_failed = true;
return false;
}
const size_t min_tx_version = (n_unmixable > 0 ? 1 : (hf_version >= HF_VERSION_ENFORCE_RCT) ? 2 : 1);
if (tx.version < min_tx_version)
{
MERROR_VER("transaction version " << (unsigned)tx.version << " is lower than min accepted version " << min_tx_version);
tvc.m_verifivation_failed = true;
return false;
}

// from v7, sorted ins
Expand Down Expand Up @@ -3468,22 +3543,33 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
CHECK_AND_ASSERT_MES(txin.type() == typeid(txin_to_key), false, "wrong type id in tx input at Blockchain::check_tx_inputs");
const txin_to_key& in_to_key = boost::get<txin_to_key>(txin);

// make sure tx output has key offset(s) (is signed to be used)
CHECK_AND_ASSERT_MES(in_to_key.key_offsets.size(), false, "empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx));

if(have_tx_keyimg_as_spent(in_to_key.k_image))
{
MERROR_VER("Key image already spent in blockchain: " << epee::string_tools::pod_to_hex(in_to_key.k_image));
tvc.m_double_spend = true;
return false;
}

if (rct::is_rct_fcmp(tx.rct_signatures.type))
{
// No need to check ring signature members for FCMP txs
CHECK_AND_ASSERT_MES(in_to_key.key_offsets.empty(), false, "non-empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx));
// No referenced pubkeys, key_offsets should all be empty
pubkeys.clear();
// IMPORTANT: continue so that key image spend check still executes for all key images
continue;
}

// The rest of this function concerns ring signature validation
if (tx.version == 1)
{
// basically, make sure number of inputs == number of signatures
CHECK_AND_ASSERT_MES(sig_index < tx.signatures.size(), false, "wrong transaction: not signature entry for input with index= " << sig_index);
}

// make sure tx output has key offset(s) (is signed to be used)
CHECK_AND_ASSERT_MES(in_to_key.key_offsets.size(), false, "empty in_to_key.key_offsets in transaction with id " << get_transaction_hash(tx));

// make sure that output being spent matches up correctly with the
// signature spending it.
if (!check_tx_input(tx.version, in_to_key, tx_prefix_hash, tx.version == 1 ? tx.signatures[sig_index] : std::vector<crypto::signature>(), tx.rct_signatures, pubkeys[sig_index], pmax_used_block_height, hf_version))
Expand Down Expand Up @@ -3535,8 +3621,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
false, "Transaction spends at least one output which is too young");
}

// TODO: use byte represenation, decompress the tree root when verifying
uint8_t *ref_tree_root = nullptr;
CHECK_AND_ASSERT_MES(get_fcmp_tx_tree_root(m_db, tx, ref_tree_root), false, "failed to get tree root");

// Warn that new RCT types are present, and thus the cache is not being used effectively
static constexpr const std::uint8_t RCT_CACHE_TYPE = rct::RCTTypeBulletproofPlus;
static constexpr const std::uint8_t RCT_CACHE_TYPE = rct::RCTTypeFcmpPlusPlus;
if (tx.rct_signatures.type > RCT_CACHE_TYPE)
{
MWARNING("RCT cache is not caching new verification results. Please update RCT_CACHE_TYPE!");
Expand Down Expand Up @@ -3579,8 +3669,9 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
case rct::RCTTypeBulletproof2:
case rct::RCTTypeCLSAG:
case rct::RCTTypeBulletproofPlus:
case rct::RCTTypeFcmpPlusPlus:
{
if (!ver_rct_non_semantics_simple_cached(tx, pubkeys, m_rct_ver_cache, RCT_CACHE_TYPE))
if (!ver_rct_non_semantics_simple_cached(tx, pubkeys, ref_tree_root, m_rct_ver_cache, RCT_CACHE_TYPE))
{
MERROR_VER("Failed to check ringct signatures!");
return false;
Expand All @@ -3589,7 +3680,7 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
}
case rct::RCTTypeFull:
{
if (!expand_transaction_2(tx, tx_prefix_hash, pubkeys))
if (!expand_transaction_2(tx, tx_prefix_hash, pubkeys, nullptr))
{
MERROR_VER("Failed to expand rct signatures!");
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/cryptonote_core/blockchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ namespace cryptonote
* can be reconstituted by the receiver. This function expands
* that implicit data.
*/
static bool expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector<std::vector<rct::ctkey>> &pubkeys);
static bool expand_transaction_2(transaction &tx, const crypto::hash &tx_prefix_hash, const std::vector<std::vector<rct::ctkey>> &pubkeys, uint8_t *tree_root);

/**
* @brief validates a transaction's inputs
Expand Down
Loading

0 comments on commit b5b1492

Please sign in to comment.