Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711020
D17043.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Subscribers
None
D17043.diff
View Options
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
Details
Attached
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)
Attached To
D17043: [chronik] implement scripthash type for utxos endpoints
Event Timeline
Log In to Comment