Page MenuHomePhabricator

D17043.diff
No OneTemporary

D17043.diff

diff --git a/chronik/chronik-http/src/handlers.rs b/chronik/chronik-http/src/handlers.rs
--- a/chronik/chronik-http/src/handlers.rs
+++ b/chronik/chronik-http/src/handlers.rs
@@ -169,9 +169,9 @@
payload: &str,
indexer: &ChronikIndexer,
) -> Result<proto::ScriptUtxos> {
- let script_variant = parse_script_variant_hex(script_type, payload)?;
let script_utxos = indexer.script_utxos()?;
- let script = script_variant.to_script();
+ let member = get_group_member(script_type, payload)?;
+ let script = script_utxos.script(member, indexer.decompress_script_fn)?;
let utxos = script_utxos.utxos(&script)?;
Ok(proto::ScriptUtxos {
script: script.bytecode().to_vec(),
diff --git a/chronik/chronik-indexer/src/indexer.rs b/chronik/chronik-indexer/src/indexer.rs
--- a/chronik/chronik-indexer/src/indexer.rs
+++ b/chronik/chronik-indexer/src/indexer.rs
@@ -56,6 +56,9 @@
const CURRENT_INDEXER_VERSION: SchemaVersion = 13;
const LAST_UPGRADABLE_VERSION: SchemaVersion = 10;
+/// Function ptr to decompress script scripts
+pub type DecompressScriptFn = fn(&[u8]) -> Result<Vec<u8>>;
+
/// Params for setting up a [`ChronikIndexer`] instance.
#[derive(Clone)]
pub struct ChronikIndexerParams {
@@ -77,6 +80,8 @@
pub plugin_ctx: Arc<PluginContext>,
/// Settings for script history indexing
pub script_history: GroupHistorySettings,
+ /// Function to decompress scripts
+ pub decompress_script_fn: DecompressScriptFn,
}
/// Struct for indexing blocks and txs. Maintains db handles and mempool.
@@ -99,6 +104,10 @@
plugin_ctx: Arc<PluginContext>,
plugin_name_map: PluginNameMap,
block_merkle_tree: Mutex<BlockMerkleTree>,
+ /// Function that can decompress the compressed scripts used as keys in
+ /// the script db. We inject it via the indexer struct to avoid
+ /// introducing a dependency on chronik_bridge in other crates.
+ pub decompress_script_fn: DecompressScriptFn,
}
/// Access to the bitcoind node.
@@ -318,6 +327,7 @@
plugin_ctx: params.plugin_ctx,
plugin_name_map,
block_merkle_tree: Mutex::new(BlockMerkleTree::new()),
+ decompress_script_fn: params.decompress_script_fn,
})
}
@@ -513,9 +523,10 @@
script_history_writer.wipe_member_hash(&mut batch);
self.db.write_batch(batch)?;
- script_history_writer.reindex_member_hash(decompress_script, || {
- bridge.shutdown_requested()
- })?;
+ script_history_writer
+ .reindex_member_hash(self.decompress_script_fn, || {
+ bridge.shutdown_requested()
+ })?;
let mut batch = WriteBatch::default();
// If the user requested a shutdown, it is very unlikely that the
@@ -926,6 +937,7 @@
group: self.script_group.clone(),
utxo_mapper: UtxoProtobufValue,
is_token_index_enabled: self.is_token_index_enabled,
+ is_scripthash_index_enabled: self.is_scripthash_index_enabled,
plugin_name_map: &self.plugin_name_map,
})
}
@@ -961,6 +973,7 @@
group: TokenIdGroup,
utxo_mapper: UtxoProtobufOutput,
is_token_index_enabled: self.is_token_index_enabled,
+ is_scripthash_index_enabled: self.is_scripthash_index_enabled,
plugin_name_map: &self.plugin_name_map,
}
}
@@ -1470,10 +1483,6 @@
Ok(min_tx_num)
}
-fn decompress_script(script: &[u8]) -> Result<Vec<u8>> {
- Ok(chronik_bridge::ffi::decompress_script(script)?)
-}
-
impl Node {
/// If `result` is [`Err`], logs and aborts the node.
pub fn ok_or_abort<T>(&self, func_name: &str, result: Result<T>) {
@@ -1514,6 +1523,11 @@
ChronikIndexerError, ChronikIndexerParams, CURRENT_INDEXER_VERSION,
};
+ /// A mock "decompression" that just prefixes with "DECOMPRESS:".
+ fn mock_decompress(script: &[u8]) -> Result<Vec<u8>> {
+ Ok([b"DECOMPRESS:".as_ref(), script.as_ref()].concat())
+ }
+
#[test]
fn test_indexer() -> Result<()> {
use bitcoinsuite_core::tx::{Tx, TxId, TxMut};
@@ -1533,6 +1547,7 @@
tx_num_cache: Default::default(),
plugin_ctx: Default::default(),
script_history: Default::default(),
+ decompress_script_fn: mock_decompress,
};
// regtest folder doesn't exist yet -> error
assert_eq!(
@@ -1616,6 +1631,7 @@
tx_num_cache: Default::default(),
plugin_ctx: Default::default(),
script_history: Default::default(),
+ decompress_script_fn: mock_decompress,
};
// Setting up DB first time sets the schema version
diff --git a/chronik/chronik-indexer/src/query/group_utxos.rs b/chronik/chronik-indexer/src/query/group_utxos.rs
--- a/chronik/chronik-indexer/src/query/group_utxos.rs
+++ b/chronik/chronik-indexer/src/query/group_utxos.rs
@@ -7,8 +7,15 @@
use std::collections::{BTreeSet, HashMap};
use abc_rust_error::Result;
-use bitcoinsuite_core::tx::{OutPoint, TxId};
+use bitcoinsuite_core::{
+ hash::Hashed,
+ script::Script,
+ tx::{OutPoint, TxId},
+};
use bitcoinsuite_slp::verify::SpentToken;
+use bytes::Bytes;
+use chronik_db::group::GroupMember;
+use chronik_db::io::GroupHistoryReader;
use chronik_db::{
db::Db,
group::{Group, UtxoData, UtxoDataOutput, UtxoDataValue},
@@ -51,6 +58,8 @@
pub utxo_mapper: U,
/// Whether the SLP/ALP token index is enabled
pub is_token_index_enabled: bool,
+ /// Whether the script hash index is enabled
+ pub is_scripthash_index_enabled: bool,
/// Map plugin name <-> plugin idx of all loaded plugins
pub plugin_name_map: &'a PluginNameMap,
}
@@ -149,6 +158,14 @@
but the output doesn't"
)]
MempoolTxOutputsOutOfBounds(OutPoint),
+
+ /// Script hash not found
+ #[error("404: Script hash {0:?} not found")]
+ ScriptHashNotFound(String),
+
+ /// Script hash index not enabled
+ #[error("400: Script hash index disabled")]
+ ScriptHashIndexDisabled,
}
impl<'a, G, U> QueryGroupUtxos<'a, G, U>
@@ -156,6 +173,42 @@
G: Group,
U: UtxoProtobuf<UtxoData = G::UtxoData>,
{
+ /// Return a script given a script or a script hash. This should only be
+ /// called when G is ScriptGroup
+ pub fn script(
+ &self,
+ member: GroupMember<Script>,
+ decompress_script_fn: fn(&[u8]) -> Result<Vec<u8>>,
+ ) -> Result<Script> {
+ let history_reader: GroupHistoryReader<'_, G> =
+ GroupHistoryReader::new(self.db)?;
+ match member {
+ GroupMember::Member(member) => Ok(member),
+ GroupMember::MemberHash(member_hash) => {
+ if !self.is_scripthash_index_enabled {
+ return Err(ScriptHashIndexDisabled.into());
+ }
+ let mempool_script_ser = self
+ .mempool
+ .script_history()
+ .member_ser_by_member_hash(member_hash);
+ let script_ser_db;
+ let script_ser = match mempool_script_ser {
+ Some(script_ser) => script_ser,
+ None => {
+ script_ser_db = history_reader
+ .member_ser_by_member_hash(member_hash)?
+ .ok_or_else(|| {
+ ScriptHashNotFound(member_hash.hex_be())
+ })?;
+ &script_ser_db
+ }
+ };
+ Ok(Script::new(Bytes::from(decompress_script_fn(script_ser)?)))
+ }
+ }
+ }
+
/// Return the UTXOs of the given member, from both DB and mempool.
///
/// UTXOs are sorted this way:
diff --git a/chronik/chronik-indexer/src/query/plugins.rs b/chronik/chronik-indexer/src/query/plugins.rs
--- a/chronik/chronik-indexer/src/query/plugins.rs
+++ b/chronik/chronik-indexer/src/query/plugins.rs
@@ -99,6 +99,7 @@
group: PluginsGroup,
utxo_mapper: UtxoProtobufOutput,
is_token_index_enabled: self.is_token_index_enabled,
+ is_scripthash_index_enabled: false,
plugin_name_map: self.plugin_name_map,
};
utxos.utxos(PluginMember { plugin_idx, group }.ser())
diff --git a/chronik/chronik-lib/src/bridge.rs b/chronik/chronik-lib/src/bridge.rs
--- a/chronik/chronik-lib/src/bridge.rs
+++ b/chronik/chronik-lib/src/bridge.rs
@@ -108,6 +108,7 @@
script_history: GroupHistorySettings {
is_member_hash_index_enabled: params.enable_scripthash_index,
},
+ decompress_script_fn: decompress_script,
},
|file_num, data_pos, undo_pos| {
Ok(Tx::from(bridge_ref.load_tx(file_num, data_pos, undo_pos)?))
@@ -169,6 +170,10 @@
Ok(SocketAddr::new(ip_addr, default_port))
}
+fn decompress_script(script: &[u8]) -> Result<Vec<u8>> {
+ Ok(chronik_bridge::ffi::decompress_script(script)?)
+}
+
/// Contains all db, runtime, tpc, etc. handles needed by Chronik.
/// This makes it so when this struct is dropped, all handles are relased
/// cleanly.
diff --git a/test/functional/chronik_scripthash.py b/test/functional/chronik_scripthash.py
--- a/test/functional/chronik_scripthash.py
+++ b/test/functional/chronik_scripthash.py
@@ -13,11 +13,12 @@
from test_framework.blocktools import (
GENESIS_CB_PK,
GENESIS_CB_SCRIPT_PUBKEY,
+ GENESIS_CB_TXID,
create_block,
make_conform_to_ctor,
)
from test_framework.hash import hex_be_sha256
-from test_framework.messages import CTransaction, FromHex, ToHex
+from test_framework.messages import XEC, CTransaction, FromHex, ToHex
from test_framework.p2p import P2PDataStore
from test_framework.script import CScript
from test_framework.test_framework import BitcoinTestFramework
@@ -69,6 +70,10 @@
.msg,
err_msg,
)
+ assert_equal(
+ self.chronik.script("scripthash", payload).utxos().err(400).msg,
+ err_msg,
+ )
# Potentially valid sha256 hash, but unlikely to collide with any existing
# scripthash
@@ -83,16 +88,13 @@
)
assert_equal(
self.chronik.script("scripthash", valid_payload)
- .confirmed_txs()
+ .unconfirmed_txs()
.err(404)
.msg,
err_msg,
)
assert_equal(
- self.chronik.script("scripthash", valid_payload)
- .confirmed_txs()
- .err(404)
- .msg,
+ self.chronik.script("scripthash", valid_payload).utxos().err(404).msg,
err_msg,
)
@@ -113,6 +115,24 @@
self.chronik.script("scripthash", GENESIS_CB_SCRIPTHASH).history().ok(),
expected_cb_history,
)
+ assert_equal(
+ self.chronik.script("scripthash", GENESIS_CB_SCRIPTHASH).utxos().ok(),
+ pb.ScriptUtxos(
+ script=bytes.fromhex(f"41{GENESIS_CB_PK}ac"),
+ utxos=[
+ pb.ScriptUtxo(
+ outpoint=pb.OutPoint(
+ txid=bytes.fromhex(GENESIS_CB_TXID)[::-1],
+ out_idx=0,
+ ),
+ block_height=0,
+ is_coinbase=True,
+ value=50_000_000 * XEC,
+ is_final=False,
+ )
+ ],
+ ),
+ )
# No txs in mempool for the genesis pubkey
assert_equal(
self.chronik.script("scripthash", GENESIS_CB_SCRIPTHASH)
@@ -121,7 +141,7 @@
pb.TxHistoryPage(num_pages=0, num_txs=0),
)
- def check_num_txs(num_block_txs, num_mempool_txs):
+ def check_num_txs(num_block_txs, num_mempool_txs, num_utxos):
page_size = 200
page_num = 0
script_conf_txs = (
@@ -142,14 +162,28 @@
.ok()
)
assert_equal(script_unconf_txs.num_txs, num_mempool_txs)
+ script_utxos = (
+ self.chronik.script("scripthash", SCRIPTHASH_P2SH_OP_TRUE_HEX)
+ .utxos()
+ .ok()
+ )
+ assert_equal(len(script_utxos.utxos), num_utxos)
# Generate blocks to some address and verify the history
blockhashes = self.generatetoaddress(self.node, 10, ADDRESS_ECREG_P2SH_OP_TRUE)
- check_num_txs(num_block_txs=len(blockhashes), num_mempool_txs=0)
+ check_num_txs(
+ num_block_txs=len(blockhashes),
+ num_mempool_txs=0,
+ num_utxos=len(blockhashes),
+ )
# Undo last block & check history
self.node.invalidateblock(blockhashes[-1])
- check_num_txs(num_block_txs=len(blockhashes) - 1, num_mempool_txs=0)
+ check_num_txs(
+ num_block_txs=len(blockhashes) - 1,
+ num_mempool_txs=0,
+ num_utxos=len(blockhashes) - 1,
+ )
# Create a replacement block (use a different destination address to ensure it
# has a hash different from the invalidated one)
@@ -161,22 +195,33 @@
blockhashes += self.generatetoaddress(
self.node, 101, ADDRESS_ECREG_P2SH_OP_TRUE
)
- check_num_txs(num_block_txs=len(blockhashes) - 1, num_mempool_txs=0)
+ check_num_txs(
+ num_block_txs=len(blockhashes) - 1,
+ num_mempool_txs=0,
+ num_utxos=len(blockhashes) - 1,
+ )
# Add mempool txs
self.op_true_wallet.rescan_utxos()
num_mempool_txs = 0
+ # the number of utxos remains constant throughout the loop because we
+ # spend one to create another one
+ num_utxos = len(blockhashes) - 1
for _ in range(10):
self.op_true_wallet.send_self_transfer(from_node=self.node)
num_mempool_txs += 1
check_num_txs(
- num_block_txs=len(blockhashes) - 1, num_mempool_txs=num_mempool_txs
+ num_block_txs=len(blockhashes) - 1,
+ num_mempool_txs=num_mempool_txs,
+ num_utxos=num_utxos,
)
# Mine mempool txs, now they're in confirmed-txs
blockhashes += self.generatetoaddress(self.node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)
check_num_txs(
- num_block_txs=len(blockhashes) + num_mempool_txs - 1, num_mempool_txs=0
+ num_block_txs=len(blockhashes) + num_mempool_txs - 1,
+ num_mempool_txs=0,
+ num_utxos=num_utxos + 1,
)
self.log.info(
@@ -200,6 +245,10 @@
.msg,
f'404: Script hash "{scripthash_hex}" not found',
)
+ assert_equal(
+ self.chronik.script("scripthash", scripthash_hex).utxos().err(404).msg,
+ f'404: Script hash "{scripthash_hex}" not found',
+ )
txid = self.op_true_wallet.send_to(
from_node=self.node, scriptPubKey=script, amount=1337
@@ -216,6 +265,11 @@
assert_equal(proto.num_txs, 1)
assert_equal(proto.txs[0].txid, bytes.fromhex(txid)[::-1])
+ proto = self.chronik.script("scripthash", scripthash_hex).utxos().ok()
+ assert_equal(len(proto.utxos), 1)
+ assert_equal(proto.utxos[0].block_height, -1)
+ assert_equal(proto.utxos[0].value, 1337)
+
def test_conflicts(self):
self.log.info("A mempool transaction is replaced by a mined transaction")
@@ -232,6 +286,10 @@
.msg,
f'404: Script hash "{scripthash_hex}" not found',
)
+ assert_equal(
+ self.chronik.script("scripthash", scripthash_hex).utxos().err(404).msg,
+ f'404: Script hash "{scripthash_hex}" not found',
+ )
assert_404(scripthash_hex1)
@@ -255,8 +313,18 @@
def is_txid_in_history(txid: str, history_page) -> bool:
return any(tx.txid[::-1].hex() == txid for tx in history_page.txs)
+ def is_utxo_in_utxos(utxo: dict, script_utxos) -> bool:
+ return any(
+ txo.outpoint.txid[::-1].hex() == utxo["txid"]
+ and txo.outpoint.out_idx == utxo["vout"]
+ for txo in script_utxos.utxos
+ )
+
def check_history(
- scripthash_hex: str, conf_txids: list[str], unconf_txids: list[str]
+ scripthash_hex: str,
+ conf_txids: list[str],
+ unconf_txids: list[str],
+ utxos=None,
):
unconf_txs = (
self.chronik.script("scripthash", scripthash_hex).unconfirmed_txs().ok()
@@ -266,10 +334,15 @@
.confirmed_txs(page_size=200)
.ok()
)
+ script_utxos = (
+ self.chronik.script("scripthash", scripthash_hex).utxos().ok()
+ )
assert_equal(conf_txs.num_txs, len(conf_txids))
assert_equal(unconf_txs.num_txs, len(unconf_txids))
+ assert_equal(len(script_utxos.utxos), len(utxos))
assert all(is_txid_in_history(txid, conf_txs) for txid in conf_txids)
assert all(is_txid_in_history(txid, unconf_txs) for txid in unconf_txids)
+ assert all(is_utxo_in_utxos(utxo, script_utxos) for utxo in utxos)
# Consistency check: None of the txids should be duplicated
all_txids = conf_txids + unconf_txids
@@ -279,6 +352,7 @@
scripthash_hex1,
conf_txids=[utxo_to_spend1["txid"], utxo_to_spend2["txid"]],
unconf_txids=[],
+ utxos=[utxo_to_spend1, utxo_to_spend2],
)
# Create 2 mempool txs, one of which will later conflict with a block tx.
@@ -295,6 +369,7 @@
mempool_tx_to_be_replaced["txid"],
other_mempool_tx["txid"],
],
+ utxos=[mempool_tx_to_be_replaced["new_utxo"], other_mempool_tx["new_utxo"]],
)
replacement_tx = wallet.create_self_transfer(utxo_to_spend=utxo_to_spend1)
@@ -318,6 +393,7 @@
replacement_tx["txid"],
],
unconf_txids=[other_mempool_tx["txid"]],
+ utxos=[other_mempool_tx["new_utxo"], replacement_tx["new_utxo"]],
)
self.log.info(
@@ -327,13 +403,14 @@
script_pubkey = b"\x21\x03" + 32 * b"\xff" + b"\xac"
scripthash_hex2 = hex_be_sha256(script_pubkey)
- funding_txid, _ = self.op_true_wallet.send_to(
+ funding_txid, funding_out_idx = self.op_true_wallet.send_to(
from_node=self.node, scriptPubKey=script_pubkey, amount=50_000_000
)
check_history(
scripthash_hex2,
conf_txids=[],
unconf_txids=[funding_txid],
+ utxos=[{"txid": funding_txid, "vout": funding_out_idx}],
)
# Mine a tx spending the same input to a different output.
@@ -366,6 +443,13 @@
.msg,
"400: Script hash index disabled",
)
+ assert_equal(
+ self.chronik.script("scripthash", GENESIS_CB_SCRIPTHASH)
+ .utxos()
+ .err(400)
+ .msg,
+ "400: Script hash index disabled",
+ )
self.log.info("Restarting with chronikscripthashindex=1 restores the index")
self.restart_node(0, ["-chronik", "-chronikscripthashindex=1"])

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 26, 10:09 (1 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573202
Default Alt Text
D17043.diff (19 KB)

Event Timeline