diff --git a/chronik/chronik-indexer/src/query/util.rs b/chronik/chronik-indexer/src/query/util.rs index 9296c169d..396e6241f 100644 --- a/chronik/chronik-indexer/src/query/util.rs +++ b/chronik/chronik-indexer/src/query/util.rs @@ -1,167 +1,171 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. use std::collections::{hash_map::Entry, BTreeMap, HashMap}; use abc_rust_error::Result; -use bitcoinsuite_core::tx::{OutPoint, SpentBy, Tx, TxId}; +use bitcoinsuite_core::{ + ser::BitcoinSer, + tx::{OutPoint, SpentBy, Tx, TxId}, +}; use chronik_db::io::{DbBlock, SpentByEntry, SpentByReader, TxNum, TxReader}; use chronik_proto::proto; use thiserror::Error; use crate::avalanche::Avalanche; /// Errors indicating something went wrong with reading txs. #[derive(Debug, Error, PartialEq)] pub enum QueryUtilError { /// DB contains a spent-by entry whose referenced tx_num cannot be found. #[error( "500: Inconsistent DB: tx_num = {tx_num} has spent entry {entry:?} in \ the DB, but the tx cannot be not found in the index" )] SpendingTxNotFound { /// Which tx has an output spent by an unknown tx. tx_num: TxNum, /// The offending entry in the DB that references an unknown tx. entry: SpentByEntry, }, } use self::QueryUtilError::*; /// Make a [`proto::Tx`]. pub(crate) fn make_tx_proto( tx: &Tx, outputs_spent: &OutputsSpent<'_>, time_first_seen: i64, is_coinbase: bool, block: Option<&DbBlock>, avalanche: &Avalanche, ) -> proto::Tx { proto::Tx { txid: tx.txid().to_vec(), version: tx.version, inputs: tx .inputs .iter() .map(|input| { let coin = input.coin.as_ref(); let (output_script, value) = coin .map(|coin| { (coin.output.script.to_vec(), coin.output.value) }) .unwrap_or_default(); proto::TxInput { prev_out: Some(make_outpoint_proto(&input.prev_out)), input_script: input.script.to_vec(), output_script, value, sequence_no: input.sequence, } }) .collect(), outputs: tx .outputs .iter() .enumerate() .map(|(output_idx, output)| proto::TxOutput { value: output.value, output_script: output.script.to_vec(), spent_by: outputs_spent .spent_by(output_idx as u32) .map(|spent_by| make_spent_by_proto(&spent_by)), }) .collect(), lock_time: tx.locktime, block: block.map(|block| proto::BlockMetadata { hash: block.hash.to_vec(), height: block.height, timestamp: block.timestamp, is_final: avalanche.is_final_height(block.height), }), time_first_seen, + size: tx.ser_len() as u32, is_coinbase, } } pub(crate) fn make_outpoint_proto(outpoint: &OutPoint) -> proto::OutPoint { proto::OutPoint { txid: outpoint.txid.to_vec(), out_idx: outpoint.out_idx, } } fn make_spent_by_proto(spent_by: &SpentBy) -> proto::SpentBy { proto::SpentBy { txid: spent_by.txid.to_vec(), input_idx: spent_by.input_idx, } } /// Helper struct for querying which tx outputs have been spent by DB or mempool /// txs. pub(crate) struct OutputsSpent<'a> { spent_by_mempool: Option<&'a BTreeMap>, spent_by_blocks: Vec, txid_by_num: HashMap, } impl<'a> OutputsSpent<'a> { pub(crate) fn new_mempool( spent_by_mempool: Option<&'a BTreeMap>, ) -> Self { OutputsSpent { spent_by_mempool, spent_by_blocks: vec![], txid_by_num: HashMap::new(), } } pub(crate) fn query( spent_by_reader: &SpentByReader<'_>, tx_reader: &TxReader<'_>, spent_by_mempool: Option<&'a BTreeMap>, tx_num: TxNum, ) -> Result { let spent_by_blocks = spent_by_reader.by_tx_num(tx_num)?.unwrap_or_default(); let mut txid_by_num = HashMap::::new(); for spent_by in &spent_by_blocks { if let Entry::Vacant(entry) = txid_by_num.entry(spent_by.tx_num) { let txid = tx_reader .txid_by_tx_num(spent_by.tx_num)? .ok_or_else(|| SpendingTxNotFound { tx_num, entry: spent_by.clone(), })?; entry.insert(txid); } } Ok(OutputsSpent { spent_by_mempool, spent_by_blocks, txid_by_num, }) } pub(crate) fn spent_by(&self, output_idx: u32) -> Option { if let Some(spent_by_mempool) = self.spent_by_mempool { if let Some(outpoint) = spent_by_mempool.get(&output_idx) { return Some(*outpoint); } } let search_idx = self .spent_by_blocks .binary_search_by_key(&output_idx, |entry| entry.out_idx); let entry = match search_idx { Ok(found_idx) => &self.spent_by_blocks[found_idx], Err(_) => return None, }; let txid = self.txid_by_num.get(&entry.tx_num).unwrap(); Some(SpentBy { txid: *txid, input_idx: entry.input_idx, }) } } diff --git a/chronik/chronik-proto/proto/chronik.proto b/chronik/chronik-proto/proto/chronik.proto index ae1d0a3db..1db390a33 100644 --- a/chronik/chronik-proto/proto/chronik.proto +++ b/chronik/chronik-proto/proto/chronik.proto @@ -1,258 +1,260 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. syntax = "proto3"; package chronik; // Block on the blockchain message Block { // Info about the block BlockInfo block_info = 1; } // Range of blocks message Blocks { // Queried blocks repeated BlockInfo blocks = 1; } // Info about the state of the blockchain. message BlockchainInfo { // Hash (little-endian) of the current tip bytes tip_hash = 1; // Height of the current tip (genesis has height = 0) int32 tip_height = 2; } // Info about a block message BlockInfo { // Hash (little-endian) bytes hash = 1; // Hash of the previous block (little-endian) bytes prev_hash = 2; // Height in the chain int32 height = 3; // nBits field encoding the target uint32 n_bits = 4; // Timestamp field of the block int64 timestamp = 5; // Whether the block has been finalized by Avalanche bool is_final = 14; // Block size of this block in bytes (including headers etc.) uint64 block_size = 6; // Number of txs in this block uint64 num_txs = 7; // Total number of tx inputs in block (including coinbase) uint64 num_inputs = 8; // Total number of tx output in block (including coinbase) uint64 num_outputs = 9; // Total number of satoshis spent by tx inputs int64 sum_input_sats = 10; // Block reward for this block int64 sum_coinbase_output_sats = 11; // Total number of satoshis in non-coinbase tx outputs int64 sum_normal_output_sats = 12; // Total number of satoshis burned using OP_RETURN int64 sum_burned_sats = 13; } // Details about a transaction message Tx { // TxId (little-endian) of the tx bytes txid = 1; // nVersion int32 version = 2; // Inputs of the tx (aka. `vin`) repeated TxInput inputs = 3; // Outputs of the tx (aka. `vout`) repeated TxOutput outputs = 4; // nLockTime uint32 lock_time = 5; // Which block this tx is in, or None, if in the mempool BlockMetadata block = 8; // Time this tx has first been added to the mempool, or 0 if unknown int64 time_first_seen = 9; + // Serialized size of the tx + uint32 size = 11; // Whether this tx is a coinbase tx bool is_coinbase = 12; } // UTXO of a script. message ScriptUtxo { // txid and out_idx of the unspent output. OutPoint outpoint = 1; // Block height of the UTXO, or -1 if in mempool. int32 block_height = 2; // Whether the UTXO has been created in a coinbase tx. bool is_coinbase = 3; // Value of the output, in satoshis. int64 value = 5; // Whether the UTXO has been finalized by Avalanche. bool is_final = 10; } // COutPoint, points to a coin being spent by an input. message OutPoint { // TxId of the tx of the output being spent. bytes txid = 1; // Index of the output spent within the transaction. uint32 out_idx = 2; } // Points to an input spending a coin. message SpentBy { // TxId of the tx with the input. bytes txid = 1; // Index in the inputs of the tx. uint32 input_idx = 2; } // CTxIn, spends a coin. message TxInput { // Reference to the coin being spent. OutPoint prev_out = 1; // scriptSig, script unlocking the coin. bytes input_script = 2; // scriptPubKey, script of the output locking the coin. bytes output_script = 3; // value of the output being spent, in satoshis. int64 value = 4; // nSequence of the input. uint32 sequence_no = 5; } // CTxOut, creates a new coin. message TxOutput { // Value of the coin, in satoshis. int64 value = 1; // scriptPubKey, script locking the output. bytes output_script = 2; // Which tx and input spent this output, if any. SpentBy spent_by = 4; } // Data about a block which a Tx is in. message BlockMetadata { // Height of the block the tx is in. int32 height = 1; // Hash of the block the tx is in. bytes hash = 2; // nTime of the block the tx is in. int64 timestamp = 3; // Whether the block has been finalized by Avalanche. bool is_final = 4; } // Page with txs message TxHistoryPage { // Txs of the page repeated Tx txs = 1; // How many pages there are total uint32 num_pages = 2; // How many txs there are total uint32 num_txs = 3; } // List of UTXOs of a script message ScriptUtxos { // The serialized script of the UTXOs bytes script = 1; // UTXOs of the script. repeated ScriptUtxo utxos = 2; } // Raw serialized tx. message RawTx { // Bytes of the serialized tx. bytes raw_tx = 1; } // Subscription to WebSocket updates. message WsSub { // Set this to `true` to unsubscribe from the event. bool is_unsub = 1; // What kind of updates to subscribe to. oneof sub_type { // Subscription to block updates WsSubBlocks blocks = 2; // Subscription to a script WsSubScript script = 3; } } // Subscription to blocks. They will be sent any time a block got connected, // disconnected or finalized. message WsSubBlocks {} // Subscription to a script. They will be send every time a tx spending the // given script or sending to the given script has been added to/removed from // the mempool, or confirmed in a block. message WsSubScript { // Script type to subscribe to ("p2pkh", "p2sh", "p2pk", "other"). string script_type = 1; // Payload for the given script type: // - 20-byte hash for "p2pkh" and "p2sh" // - 33-byte or 65-byte pubkey for "p2pk" // - Serialized script for "other" bytes payload = 2; } // Message coming from the WebSocket message WsMsg { // Kind of message oneof msg_type { // Error, e.g. when a bad message has been sent into the WebSocket. Error error = 1; // Block got connected, disconnected, finalized, etc. MsgBlock block = 2; // Tx got added to/removed from the mempool, or confirmed in a block. MsgTx tx = 3; } } // Block got connected, disconnected, finalized, etc. message MsgBlock { // What happened to the block BlockMsgType msg_type = 1; // Hash of the block (little-endian) bytes block_hash = 2; // Height of the block int32 block_height = 3; } // Type of message for the block enum BlockMsgType { // Block connected to the blockchain BLK_CONNECTED = 0; // Block disconnected from the blockchain BLK_DISCONNECTED = 1; // Block has been finalized by Avalanche BLK_FINALIZED = 2; } // Tx got added to/removed from mempool, or confirmed in a block, etc. message MsgTx { // What happened to the tx TxMsgType msg_type = 1; // Txid of the tx (little-endian) bytes txid = 2; } // Type of message for a tx enum TxMsgType { // Tx added to the mempool TX_ADDED_TO_MEMPOOL = 0; // Tx removed from the mempool TX_REMOVED_FROM_MEMPOOL = 1; // Tx confirmed in a block TX_CONFIRMED = 2; // Tx finalized by Avalanche TX_FINALIZED = 3; } // Error message returned from our APIs. message Error { // 2, as legacy chronik uses this for the message so we're still compatible. string msg = 2; } diff --git a/test/functional/chronik_script_unconfirmed_txs.py b/test/functional/chronik_script_unconfirmed_txs.py index f578cc837..ae6a3d831 100644 --- a/test/functional/chronik_script_unconfirmed_txs.py +++ b/test/functional/chronik_script_unconfirmed_txs.py @@ -1,191 +1,192 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test Chronik's /script/:type/:payload/unconfirmed-txs endpoint. """ from test_framework.address import ( ADDRESS_ECREG_P2SH_OP_TRUE, P2SH_OP_TRUE, SCRIPTSIG_OP_TRUE, ) from test_framework.blocktools import ( GENESIS_CB_PK, create_block, create_coinbase, make_conform_to_ctor, ) from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut from test_framework.p2p import P2PDataStore from test_framework.script import OP_RETURN, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class ChronikScriptUnconfirmedTxsTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-chronik']] self.rpc_timeout = 240 def skip_test_if_missing_module(self): self.skip_if_no_chronik() def run_test(self): from test_framework.chronik.client import ChronikClient, pb node = self.nodes[0] chronik = ChronikClient('127.0.0.1', node.chronik_port) peer = node.add_p2p_connection(P2PDataStore()) mocktime = 1300000000 node.setmocktime(mocktime) assert_equal( chronik.script('', '').unconfirmed_txs().err(400).msg, '400: Unknown script type: ') assert_equal( chronik.script('foo', '', ).unconfirmed_txs().err(400).msg, '400: Unknown script type: foo') assert_equal( chronik.script('p2pkh', 'LILALI').unconfirmed_txs().err(400).msg, "400: Invalid hex: Invalid character 'L' at position 0") assert_equal( chronik.script('other', 'LILALI').unconfirmed_txs().err(400).msg, "400: Invalid hex: Invalid character 'L' at position 0") assert_equal( chronik.script('p2pkh', '', ).unconfirmed_txs().err(400).msg, '400: Invalid payload for P2PKH: Invalid length, ' + 'expected 20 bytes but got 0 bytes') assert_equal( chronik.script('p2pkh', 'aA').unconfirmed_txs().err(400).msg, '400: Invalid payload for P2PKH: Invalid length, ' + 'expected 20 bytes but got 1 bytes') assert_equal( chronik.script('p2sh', 'aaBB').unconfirmed_txs().err(400).msg, '400: Invalid payload for P2SH: Invalid length, ' + 'expected 20 bytes but got 2 bytes') assert_equal( chronik.script('p2pk', 'aaBBcc').unconfirmed_txs().err(400).msg, '400: Invalid payload for P2PK: Invalid length, ' + 'expected one of [33, 65] but got 3 bytes') # No txs in mempool for the genesis pubkey assert_equal( chronik.script('p2pk', GENESIS_CB_PK).unconfirmed_txs().ok(), pb.TxHistoryPage(num_pages=0, num_txs=0)) script_type = 'p2sh' payload_hex = P2SH_OP_TRUE[2:-1].hex() # Generate 110 blocks to some address blockhashes = self.generatetoaddress(node, 110, ADDRESS_ECREG_P2SH_OP_TRUE) # No txs in mempool for that address assert_equal( chronik.script(script_type, payload_hex).unconfirmed_txs().ok(), pb.TxHistoryPage(num_pages=0, num_txs=0)) coinvalue = 5000000000 cointxids = [] for coinblockhash in blockhashes[:10]: coinblock = node.getblock(coinblockhash) cointxids.append(coinblock['tx'][0]) mempool_txs = [] mempool_proto_txs = [] # Send 10 mempool txs, each with their own mocktime mocktime_offsets = [0, 10, 10, 5, 0, 0, 12, 12, 10, 5] for mocktime_offset in mocktime_offsets: cointxid = cointxids.pop(0) time_first_seen = mocktime + mocktime_offset pad_script = CScript([OP_RETURN, bytes(100)]) tx = CTransaction() tx.nVersion = 1 tx.vin = [CTxIn(outpoint=COutPoint(int(cointxid, 16), 0), scriptSig=SCRIPTSIG_OP_TRUE, nSequence=0xffffffff)] tx.vout = [ CTxOut(coinvalue - 1000, P2SH_OP_TRUE), CTxOut(0, pad_script), ] tx.nLockTime = 1 node.setmocktime(time_first_seen) txid = node.sendrawtransaction(tx.serialize().hex()) mempool_txs.append(tx) mempool_proto_txs.append(pb.Tx( txid=bytes.fromhex(txid)[::-1], version=1, inputs=[pb.TxInput( prev_out=pb.OutPoint( txid=bytes.fromhex(cointxid)[::-1], out_idx=0, ), input_script=bytes(SCRIPTSIG_OP_TRUE), output_script=bytes(P2SH_OP_TRUE), value=coinvalue, sequence_no=0xffffffff, )], outputs=[ pb.TxOutput( value=coinvalue - 1000, output_script=bytes(P2SH_OP_TRUE), ), pb.TxOutput( value=0, output_script=bytes(pad_script), ), ], lock_time=1, + size=len(tx.serialize()), time_first_seen=time_first_seen, )) # Sort txs by time_first_seen and then by txid def sorted_txs(txs): return sorted(txs, key=lambda tx: (tx.time_first_seen, tx.txid[::-1])) assert_equal( chronik.script(script_type, payload_hex).unconfirmed_txs().ok(), pb.TxHistoryPage(txs=sorted_txs(mempool_proto_txs), num_pages=1, num_txs=len(mempool_txs))) # Mine 5 transactions, with 2 conflicts, leave 5 others unconfirmed mine_txs = mempool_txs[:3] mine_proto_txs = mempool_proto_txs[:3] for conflict_tx, conflict_proto_tx in zip( mempool_txs[3:5], mempool_proto_txs[3:5]): conflict_tx.nLockTime = 2 conflict_tx.rehash() mine_txs.append(conflict_tx) conflict_proto_tx.txid = bytes.fromhex(conflict_tx.hash)[::-1] conflict_proto_tx.lock_time = 2 mine_proto_txs.append(conflict_proto_tx) height = 111 coinbase_tx = create_coinbase(height) coinbase_tx.vout[0].scriptPubKey = P2SH_OP_TRUE coinbase_tx.rehash() block = create_block(int(blockhashes[-1], 16), coinbase_tx, mocktime + 1100) block.vtx += mine_txs make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() block.solve() peer.send_blocks_and_test([block], node) # Only unconfirmed txs remain, conflict txs are removed assert_equal( chronik.script(script_type, payload_hex).unconfirmed_txs().ok(), pb.TxHistoryPage(txs=sorted_txs(mempool_proto_txs[5:]), num_pages=1, num_txs=5)) if __name__ == '__main__': ChronikScriptUnconfirmedTxsTest().main() diff --git a/test/functional/chronik_tx.py b/test/functional/chronik_tx.py index f1cdfe4a8..9e727d16d 100644 --- a/test/functional/chronik_tx.py +++ b/test/functional/chronik_tx.py @@ -1,185 +1,187 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test Chronik's /tx endpoint. """ from test_framework.address import ( ADDRESS_ECREG_P2SH_OP_TRUE, ADDRESS_ECREG_UNSPENDABLE, P2SH_OP_TRUE, SCRIPTSIG_OP_TRUE, ) from test_framework.blocktools import GENESIS_CB_TXID, create_block, create_coinbase from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut from test_framework.p2p import P2PDataStore from test_framework.script import OP_EQUAL, OP_HASH160, CScript, hash160 from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class ChronikTxTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-chronik']] self.rpc_timeout = 240 def skip_test_if_missing_module(self): self.skip_if_no_chronik() def run_test(self): from test_framework.chronik.client import ChronikClient, pb from test_framework.chronik.test_data import genesis_cb_tx node = self.nodes[0] chronik = ChronikClient('127.0.0.1', node.chronik_port) peer = node.add_p2p_connection(P2PDataStore()) node.setmocktime(1333333337) assert_equal(chronik.tx('0').err(400).msg, '400: Not a txid: 0') assert_equal(chronik.tx('123').err(400).msg, '400: Not a txid: 123') assert_equal(chronik.tx('1234f').err(400).msg, '400: Not a txid: 1234f') assert_equal(chronik.tx('00' * 31).err(400).msg, f'400: Not a txid: {"00"*31}') assert_equal(chronik.tx('01').err(400).msg, '400: Not a txid: 01') assert_equal(chronik.tx('12345678901').err(400).msg, '400: Not a txid: 12345678901') assert_equal(chronik.tx('00' * 32).err(404).msg, f'404: Transaction {"00"*32} not found in the index') # Verify queried genesis tx matches assert_equal(chronik.tx(GENESIS_CB_TXID).ok(), genesis_cb_tx()) coinblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0] coinblock = node.getblock(coinblockhash) cointx = coinblock['tx'][0] self.generatetoaddress(node, 100, ADDRESS_ECREG_UNSPENDABLE) coinvalue = 5000000000 send_values = [coinvalue - 10000, 1000, 2000, 3000] send_redeem_scripts = [bytes([i + 0x52]) for i in range(len(send_values))] send_scripts = [CScript([OP_HASH160, hash160(redeem_script), OP_EQUAL]) for redeem_script in send_redeem_scripts] tx = CTransaction() tx.nVersion = 2 tx.vin = [CTxIn(outpoint=COutPoint(int(cointx, 16), 0), scriptSig=SCRIPTSIG_OP_TRUE, nSequence=0xfffffffe)] tx.vout = [CTxOut(value, script) for (value, script) in zip(send_values, send_scripts)] tx.nLockTime = 1234567890 # Submit tx to mempool txid = node.sendrawtransaction(tx.serialize().hex()) proto_tx = pb.Tx( txid=bytes.fromhex(txid)[::-1], version=tx.nVersion, inputs=[pb.TxInput( prev_out=pb.OutPoint(txid=bytes.fromhex(cointx)[::-1], out_idx=0), input_script=bytes(tx.vin[0].scriptSig), output_script=bytes(P2SH_OP_TRUE), value=coinvalue, sequence_no=0xfffffffe, )], outputs=[pb.TxOutput( value=value, output_script=bytes(script), ) for value, script in zip(send_values, send_scripts)], lock_time=1234567890, block=None, time_first_seen=1333333337, + size=len(tx.serialize()), is_coinbase=False, ) assert_equal(chronik.tx(txid).ok(), proto_tx) # If we mine the block, querying will gives us all the tx details + block txblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[0] # Set the `block` field, now that we mined it proto_tx.block.CopyFrom(pb.BlockMetadata( hash=bytes.fromhex(txblockhash)[::-1], height=102, timestamp=1333333355, )) assert_equal(chronik.tx(txid).ok(), proto_tx) node.setmocktime(1333333338) tx2 = CTransaction() tx2.nVersion = 2 tx2.vin = [CTxIn(outpoint=COutPoint(int(txid, 16), i), scriptSig=CScript([redeem_script]), nSequence=0xfffffff0 + i) for i, redeem_script in enumerate(send_redeem_scripts)] tx2.vout = [CTxOut(coinvalue - 20000, send_scripts[0])] tx2.nLockTime = 12 # Submit tx to mempool txid2 = node.sendrawtransaction(tx2.serialize().hex()) proto_tx2 = pb.Tx( txid=bytes.fromhex(txid2)[::-1], version=tx2.nVersion, inputs=[ pb.TxInput( prev_out=pb.OutPoint(txid=bytes.fromhex(txid)[::-1], out_idx=i), input_script=bytes(tx2.vin[i].scriptSig), output_script=bytes(script), value=value, sequence_no=0xfffffff0 + i, ) for i, (value, script) in enumerate(zip(send_values, send_scripts)) ], outputs=[pb.TxOutput( value=tx2.vout[0].nValue, output_script=bytes(tx2.vout[0].scriptPubKey), )], lock_time=12, block=None, time_first_seen=1333333338, + size=len(tx2.serialize()), is_coinbase=False, ) assert_equal(chronik.tx(txid2).ok(), proto_tx2) # Mine tx tx2blockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[0] # Invalidate block node.invalidateblock(tx2blockhash) # Tx back in mempool assert_equal(chronik.tx(txid2).ok(), proto_tx2) # Mine conflicting tx conflict_tx = CTransaction(tx2) conflict_tx.nLockTime = 13 block = create_block(int(txblockhash, 16), create_coinbase(103, b'\x03' * 33), 1333333500) block.vtx += [conflict_tx] block.hashMerkleRoot = block.calc_merkle_root() block.solve() peer.send_blocks_and_test([block], node) assert_equal(chronik.tx(txid2).err(404).msg, f'404: Transaction {txid2} not found in the index') proto_tx2.txid = bytes.fromhex(conflict_tx.hash)[::-1] proto_tx2.lock_time = 13 proto_tx2.time_first_seen = 0 proto_tx2.block.CopyFrom(pb.BlockMetadata( hash=bytes.fromhex(block.hash)[::-1], height=103, timestamp=1333333500, )) assert_equal(chronik.tx(conflict_tx.hash).ok(), proto_tx2) if __name__ == '__main__': ChronikTxTest().main() diff --git a/test/functional/test_framework/chronik/test_data.py b/test/functional/test_framework/chronik/test_data.py index 80aacd12e..ad6a1e592 100644 --- a/test/functional/test_framework/chronik/test_data.py +++ b/test/functional/test_framework/chronik/test_data.py @@ -1,37 +1,38 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test_framework.blocktools import ( GENESIS_BLOCK_HASH, GENESIS_CB_SCRIPT_PUBKEY, GENESIS_CB_SCRIPT_SIG, GENESIS_CB_TXID, TIME_GENESIS_BLOCK, ) from test_framework.chronik.client import pb def genesis_cb_tx(): return pb.Tx( txid=bytes.fromhex(GENESIS_CB_TXID)[::-1], version=1, inputs=[pb.TxInput( prev_out=pb.OutPoint(txid=bytes(32), out_idx=0xffffffff), input_script=bytes(GENESIS_CB_SCRIPT_SIG), sequence_no=0xffffffff, )], outputs=[pb.TxOutput( value=5000000000, output_script=bytes(GENESIS_CB_SCRIPT_PUBKEY), )], lock_time=0, block=pb.BlockMetadata( hash=bytes.fromhex(GENESIS_BLOCK_HASH)[::-1], height=0, timestamp=TIME_GENESIS_BLOCK, ), time_first_seen=0, + size=204, is_coinbase=True, )