electrs/src/elements/asset.rs
2026-01-31 22:04:39 +09:00

663 lines
21 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock, RwLockReadGuard};
use bitcoin::hashes::{sha256, Hash};
use elements::confidential::{Asset, Value};
use elements::encode::{deserialize, serialize};
use elements::secp256k1_zkp::ZERO_TWEAK;
use elements::{issuance::ContractHash, AssetId, AssetIssuance, OutPoint, Transaction, TxIn};
use crate::chain::{BNetwork, BlockHash, Network, Txid};
use crate::elements::peg::{get_pegin_data, get_pegout_data, PeginInfo, PegoutInfo};
use crate::elements::registry::{AssetMeta, AssetRegistry};
use crate::errors::*;
use crate::new_index::schema::{Operation, TxHistoryInfo, TxHistoryKey, TxHistoryRow};
use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query};
use crate::util::{
bincode_util, full_hash, Bytes, FullHash, IsProvablyUnspendable, TransactionStatus, TxInput,
};
lazy_static! {
pub static ref NATIVE_ASSET_ID: AssetId =
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
.parse()
.unwrap();
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
"144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
.parse()
.unwrap();
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
.parse()
.unwrap();
}
fn parse_asset_id(sl: &[u8]) -> AssetId {
AssetId::from_slice(sl).expect("failed to parse AssetId")
}
#[derive(Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum LiquidAsset {
Issued(IssuedAsset),
Native(PeggedAsset),
}
#[derive(Serialize)]
pub struct PeggedAsset {
pub asset_id: AssetId,
pub chain_stats: PeggedAssetStats,
pub mempool_stats: PeggedAssetStats,
}
#[derive(Serialize)]
pub struct IssuedAsset {
pub asset_id: AssetId,
pub issuance_txin: TxInput,
#[serde(serialize_with = "crate::util::serialize_outpoint")]
pub issuance_prevout: OutPoint,
pub reissuance_token: AssetId,
#[serde(skip_serializing_if = "Option::is_none")]
pub contract_hash: Option<ContractHash>,
// the confirmation status of the initial issuance transaction
pub status: TransactionStatus,
pub chain_stats: IssuedAssetStats,
pub mempool_stats: IssuedAssetStats,
// optional metadata from registry
#[serde(flatten)]
pub meta: Option<AssetMeta>,
}
// DB representation (issued assets only)
#[derive(Serialize, Deserialize, Debug)]
pub struct AssetRow {
pub issuance_txid: FullHash,
pub issuance_vin: u32,
pub prev_txid: FullHash,
pub prev_vout: u32,
pub issuance: Bytes, // bincode does not like dealing with AssetIssuance, deserialization fails with "invalid type: sequence, expected a struct"
pub reissuance_token: FullHash,
}
impl IssuedAsset {
pub fn new(
asset_id: &AssetId,
asset: &AssetRow,
(chain_stats, mempool_stats): (IssuedAssetStats, IssuedAssetStats),
meta: Option<AssetMeta>,
status: TransactionStatus,
) -> Self {
let issuance: AssetIssuance =
deserialize(&asset.issuance).expect("failed parsing AssetIssuance");
let reissuance_token = parse_asset_id(&asset.reissuance_token);
let contract_hash = if issuance.asset_entropy != [0u8; 32] {
Some(ContractHash::from_byte_array(issuance.asset_entropy))
} else {
None
};
Self {
asset_id: *asset_id,
issuance_txin: TxInput {
txid: deserialize(&asset.issuance_txid).unwrap(),
vin: asset.issuance_vin,
},
issuance_prevout: OutPoint {
txid: deserialize(&asset.prev_txid).unwrap(),
vout: asset.prev_vout,
},
contract_hash,
reissuance_token,
status,
chain_stats,
mempool_stats,
meta,
}
}
}
impl LiquidAsset {
pub fn supply(&self) -> Option<u64> {
match self {
LiquidAsset::Native(asset) => Some(
asset.chain_stats.peg_in_amount
- asset.chain_stats.peg_out_amount
- asset.chain_stats.burned_amount
+ asset.mempool_stats.peg_in_amount
- asset.mempool_stats.peg_out_amount
- asset.mempool_stats.burned_amount,
),
LiquidAsset::Issued(asset) => {
if asset.chain_stats.has_blinded_issuances
|| asset.mempool_stats.has_blinded_issuances
{
None
} else {
Some(
asset.chain_stats.issued_amount - asset.chain_stats.burned_amount
+ asset.mempool_stats.issued_amount
- asset.mempool_stats.burned_amount,
)
}
}
}
}
pub fn precision(&self) -> u8 {
match self {
LiquidAsset::Native(_) => 8,
LiquidAsset::Issued(asset) => asset.meta.as_ref().map_or(0, |m| m.precision),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct IssuingInfo {
pub txid: FullHash,
pub vin: u32,
pub is_reissuance: bool,
// None for blinded issuances
pub issued_amount: Option<u64>,
pub token_amount: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct BurningInfo {
pub txid: FullHash,
pub vout: u32,
pub value: u64,
}
// Index confirmed transaction issuances and save as db rows
pub fn index_confirmed_tx_assets(
tx: &Transaction,
confirmed_height: u32,
tx_position: u16,
network: Network,
parent_network: BNetwork,
rows: &mut Vec<DBRow>,
op: &Operation,
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
rows.extend(history.into_iter().map(|(asset_id, info)| {
let history_row = asset_history_row(&asset_id, confirmed_height, tx_position, info);
if let Operation::DeleteBlocksWithHistory(tx) = op {
tx.send(history_row.key.hash)
.expect("unbounded channel won't fail");
}
history_row.into_row()
}));
// the initial issuance is kept twice: once in the history index under I<asset><height><txid:vin>,
// and once separately under i<asset> for asset lookup with some more associated metadata.
// reissuances are only kept under the history index.
rows.extend(issuances.into_iter().map(|(asset_id, asset_row)| DBRow {
key: [b"i", &asset_id.into_inner()[..]].concat(),
value: bincode_util::serialize_little(&asset_row).unwrap(),
}));
}
// Index mempool transaction issuances and save to in-memory store
pub fn index_mempool_tx_assets(
tx: &Transaction,
network: Network,
parent_network: BNetwork,
asset_history: &mut HashMap<AssetId, Vec<TxHistoryInfo>>,
asset_issuance: &mut HashMap<AssetId, AssetRow>,
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
for (asset_id, info) in history {
asset_history.entry(asset_id).or_default().push(info);
}
for (asset_id, issuance) in issuances {
asset_issuance.insert(asset_id, issuance);
}
}
// Remove mempool transaction issuances from in-memory store
pub fn remove_mempool_tx_assets(
to_remove: &HashSet<&Txid>,
asset_history: &mut HashMap<AssetId, Vec<TxHistoryInfo>>,
asset_issuance: &mut HashMap<AssetId, AssetRow>,
) {
// TODO optimize
asset_history.retain(|_assethash, entries| {
entries.retain(|entry| !to_remove.contains(&entry.get_txid()));
!entries.is_empty()
});
asset_issuance.retain(|_assethash, issuance| {
let txid: Txid = deserialize(&issuance.issuance_txid).unwrap();
!to_remove.contains(&txid)
});
}
type HistoryAndIssuances = (Vec<(AssetId, TxHistoryInfo)>, Vec<(AssetId, AssetRow)>);
// Internal utility function, index a transaction and return its history entries and issuances
fn index_tx_assets(
tx: &Transaction,
network: Network,
parent_network: BNetwork,
) -> HistoryAndIssuances {
let mut history = vec![];
let mut issuances = vec![];
let txid = full_hash(&tx.txid()[..]);
for (txo_index, txo) in tx.output.iter().enumerate() {
if let Some(pegout) = get_pegout_data(txo, network, parent_network) {
history.push((
pegout.asset.explicit().unwrap(),
TxHistoryInfo::Pegout(PegoutInfo {
txid,
vout: txo_index as u32,
value: pegout.value,
}),
));
} else if txo.script_pubkey.is_provably_unspendable_() && !txo.is_fee() {
if let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) {
if value > 0 {
history.push((
asset_id,
TxHistoryInfo::Burning(BurningInfo {
txid,
vout: txo_index as u32,
value,
}),
));
}
}
}
}
for (txi_index, txi) in tx.input.iter().enumerate() {
if let Some(pegin) = get_pegin_data(txi, network) {
history.push((
pegin.asset,
TxHistoryInfo::Pegin(PeginInfo {
txid,
vin: txi_index as u32,
value: pegin.value,
}),
));
} else if txi.has_issuance() {
let is_reissuance = txi.asset_issuance.asset_blinding_nonce != ZERO_TWEAK;
let asset_entropy = get_issuance_entropy(txi).expect("invalid issuance");
let asset_id = AssetId::from_entropy(asset_entropy);
let issued_amount = match txi.asset_issuance.amount {
Value::Explicit(amount) => Some(amount),
Value::Null => Some(0),
_ => None,
};
let token_amount = match txi.asset_issuance.inflation_keys {
Value::Explicit(amount) => Some(amount),
Value::Null => Some(0),
_ => None,
};
history.push((
asset_id,
TxHistoryInfo::Issuing(IssuingInfo {
txid,
vin: txi_index as u32,
is_reissuance,
issued_amount,
token_amount,
}),
));
if !is_reissuance {
let is_confidential =
matches!(txi.asset_issuance.inflation_keys, Value::Confidential(..));
let reissuance_token =
AssetId::reissuance_token_from_entropy(asset_entropy, is_confidential);
issuances.push((
asset_id,
AssetRow {
issuance_txid: txid,
issuance_vin: txi_index as u32,
prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout,
issuance: serialize(&txi.asset_issuance),
reissuance_token: full_hash(&reissuance_token.into_inner()[..]),
},
));
}
}
}
(history, issuances)
}
fn asset_history_row(
asset_id: &AssetId,
confirmed_height: u32,
tx_position: u16,
txinfo: TxHistoryInfo,
) -> TxHistoryRow {
let key = TxHistoryKey {
code: b'I',
hash: full_hash(&asset_id.into_inner()[..]),
confirmed_height,
tx_position,
txinfo,
};
TxHistoryRow { key }
}
pub enum AssetRegistryLock<'a> {
RwLock(&'a Arc<RwLock<AssetRegistry>>),
RwLockReadGuard(&'a RwLockReadGuard<'a, AssetRegistry>),
}
pub fn lookup_asset(
query: &Query,
registry: Option<AssetRegistryLock>,
asset_id: &AssetId,
meta: Option<&AssetMeta>, // may optionally be provided if already known
) -> Result<Option<LiquidAsset>> {
if query.network().pegged_asset() == Some(asset_id) {
let (chain_stats, mempool_stats) = pegged_asset_stats(query, asset_id);
return Ok(Some(LiquidAsset::Native(PeggedAsset {
asset_id: *asset_id,
chain_stats,
mempool_stats,
})));
}
let history_db = query.chain().store().history_db();
let mempool = query.mempool();
let mempool_issuances = &mempool.asset_issuance;
let chain_row = history_db
.get(&[b"i", &asset_id.into_inner()[..]].concat())
.map(|row| {
bincode_util::deserialize_little::<AssetRow>(&row).expect("failed parsing AssetRow")
});
let row = chain_row
.as_ref()
.or_else(|| mempool_issuances.get(asset_id));
Ok(if let Some(row) = row {
let reissuance_token = parse_asset_id(&row.reissuance_token);
let meta = meta.cloned().or_else(|| match registry {
Some(AssetRegistryLock::RwLock(rwlock)) => {
rwlock.read().unwrap().get(asset_id).cloned()
}
Some(AssetRegistryLock::RwLockReadGuard(guard)) => guard.get(asset_id).cloned(),
None => None,
});
let stats = issued_asset_stats(query.chain(), &mempool, asset_id, &reissuance_token);
let status = query.get_tx_status(&deserialize(&row.issuance_txid).unwrap());
let asset = IssuedAsset::new(asset_id, row, stats, meta, status);
Some(LiquidAsset::Issued(asset))
} else {
None
})
}
pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> {
if !txin.has_issuance() {
bail!("input has no issuance");
}
let is_reissuance = txin.asset_issuance.asset_blinding_nonce != ZERO_TWEAK;
Ok(if !is_reissuance {
let contract_hash = ContractHash::from_slice(&txin.asset_issuance.asset_entropy)
.chain_err(|| "invalid entropy (contract hash)")?;
AssetId::generate_asset_entropy(txin.previous_output, contract_hash)
} else {
sha256::Midstate::from_slice(&txin.asset_issuance.asset_entropy)
.chain_err(|| "invalid entropy (reissuance)")?
})
}
//
// Asset stats
//
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct IssuedAssetStats {
pub tx_count: usize,
pub issuance_count: usize,
pub issued_amount: u64,
pub burned_amount: u64,
pub has_blinded_issuances: bool,
pub reissuance_tokens: Option<u64>, // none if confidential
pub burned_reissuance_tokens: u64,
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct PeggedAssetStats {
pub tx_count: usize,
pub peg_in_count: usize,
pub peg_in_amount: u64,
pub peg_out_count: usize,
pub peg_out_amount: u64,
pub burn_count: usize,
pub burned_amount: u64,
}
type AssetStatApplyFn<T> = fn(&TxHistoryInfo, &mut T, &mut HashSet<Txid>);
fn asset_cache_key(asset_id: &AssetId) -> Bytes {
[b"z", &asset_id.into_inner()[..]].concat()
}
fn asset_cache_row<T>(asset_id: &AssetId, stats: &T, blockhash: &BlockHash) -> DBRow
where
T: serde::Serialize,
{
DBRow {
key: asset_cache_key(asset_id),
value: bincode_util::serialize_little(&(stats, blockhash)).unwrap(),
}
}
// Get stats for the network's pegged asset
fn pegged_asset_stats(query: &Query, asset_id: &AssetId) -> (PeggedAssetStats, PeggedAssetStats) {
(
chain_asset_stats(query.chain(), asset_id, apply_pegged_asset_stats),
mempool_asset_stats(&query.mempool(), asset_id, apply_pegged_asset_stats),
)
}
// Get stats for issued assets
fn issued_asset_stats(
chain: &ChainQuery,
mempool: &Mempool,
asset_id: &AssetId,
reissuance_token: &AssetId,
) -> (IssuedAssetStats, IssuedAssetStats) {
let afn = apply_issued_asset_stats;
let mut chain_stats = chain_asset_stats(chain, asset_id, afn);
chain_stats.burned_reissuance_tokens =
chain_asset_stats(chain, reissuance_token, afn).burned_amount;
let mut mempool_stats = mempool_asset_stats(mempool, asset_id, afn);
mempool_stats.burned_reissuance_tokens =
mempool_asset_stats(mempool, reissuance_token, afn).burned_amount;
(chain_stats, mempool_stats)
}
// Get on-chain confirmed asset stats (issued or the pegged asset)
fn chain_asset_stats<T>(chain: &ChainQuery, asset_id: &AssetId, apply_fn: AssetStatApplyFn<T>) -> T
where
T: Default + serde::Serialize + serde::de::DeserializeOwned,
{
// get the last known stats and the blockhash they are updated for.
// invalidates the cache if the block was orphaned.
let cache: Option<(T, usize)> = chain
.store()
.cache_db()
.get(&asset_cache_key(asset_id))
.map(|c| bincode_util::deserialize_little(&c).unwrap())
.and_then(|(stats, blockhash)| {
chain
.height_by_hash(&blockhash)
.map(|height| (stats, height))
});
// update stats with new transactions since
let (newstats, lastblock) = cache.map_or_else(
|| chain_asset_stats_delta(chain, asset_id, T::default(), 0, apply_fn),
|(oldstats, blockheight)| {
chain_asset_stats_delta(chain, asset_id, oldstats, blockheight + 1, apply_fn)
},
);
// save updated stats to cache
if let Some(lastblock) = lastblock {
chain.store().cache_db().write(
vec![asset_cache_row(asset_id, &newstats, &lastblock)],
DBFlush::Enable,
);
}
newstats
}
// Update the asset stats with the delta of confirmed txs since start_height
fn chain_asset_stats_delta<T>(
chain: &ChainQuery,
asset_id: &AssetId,
init_stats: T,
start_height: usize,
apply_fn: AssetStatApplyFn<T>,
) -> (T, Option<BlockHash>) {
let history_iter = chain
.history_iter_scan(b'I', &asset_id.into_inner()[..], start_height)
.map(TxHistoryRow::from_row)
.filter_map(|history| {
chain
.tx_confirming_block(&history.get_txid())
.map(|blockid| (history, blockid))
});
let mut stats = init_stats;
let mut seen_txids = HashSet::new();
let mut lastblock = None;
for (row, blockid) in history_iter {
if lastblock != Some(blockid.hash) {
seen_txids.clear();
}
apply_fn(&row.key.txinfo, &mut stats, &mut seen_txids);
lastblock = Some(blockid.hash);
}
(stats, lastblock)
}
// Get mempool asset stats (issued or the pegged asset)
pub fn mempool_asset_stats<T>(
mempool: &Mempool,
asset_id: &AssetId,
apply_fn: AssetStatApplyFn<T>,
) -> T
where
T: Default,
{
let mut stats = T::default();
if let Some(history) = mempool.asset_history.get(asset_id) {
let mut seen_txids = HashSet::new();
for info in history {
apply_fn(info, &mut stats, &mut seen_txids)
}
}
stats
}
fn apply_issued_asset_stats(
info: &TxHistoryInfo,
stats: &mut IssuedAssetStats,
seen_txids: &mut HashSet<Txid>,
) {
if seen_txids.insert(info.get_txid()) {
stats.tx_count += 1;
}
match info {
TxHistoryInfo::Issuing(issuance) => {
stats.issuance_count += 1;
match issuance.issued_amount {
Some(amount) => stats.issued_amount += amount,
None => stats.has_blinded_issuances = true,
}
if !issuance.is_reissuance {
stats.reissuance_tokens = issuance.token_amount;
}
}
TxHistoryInfo::Burning(info) => {
stats.burned_amount += info.value;
}
TxHistoryInfo::Funding(_) | TxHistoryInfo::Spending(_) => {
// we don't keep funding/spending entries for assets
unreachable!();
}
TxHistoryInfo::Pegin(_) | TxHistoryInfo::Pegout(_) => {
// issued assets cannot have pegins/pegouts
unreachable!();
}
}
}
fn apply_pegged_asset_stats(
info: &TxHistoryInfo,
stats: &mut PeggedAssetStats,
seen_txids: &mut HashSet<Txid>,
) {
if seen_txids.insert(info.get_txid()) {
stats.tx_count += 1;
}
match info {
TxHistoryInfo::Pegin(info) => {
stats.peg_in_count += 1;
stats.peg_in_amount += info.value;
}
TxHistoryInfo::Pegout(info) => {
stats.peg_out_count += 1;
stats.peg_out_amount += info.value;
}
TxHistoryInfo::Burning(info) => {
stats.burn_count += 1;
stats.burned_amount += info.value;
}
TxHistoryInfo::Issuing(_) => {
warn!("encountered issuance of native asset, ignoring (possibly freeinitialcoins?)");
}
TxHistoryInfo::Funding(_) | TxHistoryInfo::Spending(_) => {
// these history entries variants are never kept for native assets
unreachable!();
}
}
}