test_bitcoincore_rpc/
lib.rs

1use {
2  bitcoin::{
3    blockdata::constants::COIN_VALUE, blockdata::script, consensus::encode::serialize,
4    hash_types::BlockHash, hashes::Hash, Amount, Block, BlockHeader, Network, OutPoint,
5    PackedLockTime, Script, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
6  },
7  bitcoincore_rpc_json::{GetRawTransactionResult, ListUnspentResultEntry},
8  jsonrpc_core::{IoHandler, Value},
9  jsonrpc_http_server::{CloseHandle, ServerBuilder},
10  std::collections::BTreeMap,
11  std::{
12    sync::{Arc, Mutex},
13    thread,
14  },
15};
16
17pub fn spawn() -> Handle {
18  let server = Server::new();
19  let state = server.state.clone();
20  let mut io = IoHandler::default();
21  io.extend_with(server.to_delegate());
22
23  let rpc_server = ServerBuilder::new(io)
24    .threads(1)
25    .start_http(&"127.0.0.1:0".parse().unwrap())
26    .unwrap();
27
28  let close_handle = rpc_server.close_handle();
29  let port = rpc_server.address().port();
30
31  thread::spawn(|| rpc_server.wait());
32
33  Handle {
34    close_handle: Some(close_handle),
35    port,
36    state,
37  }
38}
39
40pub struct TransactionTemplate<'a> {
41  pub input_slots: &'a [(usize, usize, usize)],
42  pub output_count: usize,
43  pub fee: u64,
44}
45
46struct State {
47  blocks: BTreeMap<BlockHash, Block>,
48  hashes: Vec<BlockHash>,
49  mempool: Vec<Transaction>,
50  nonce: u32,
51  transactions: BTreeMap<Txid, Transaction>,
52}
53
54impl State {
55  fn new() -> Self {
56    let mut hashes = Vec::new();
57    let mut blocks = BTreeMap::new();
58
59    let genesis_block = bitcoin::blockdata::constants::genesis_block(Network::Bitcoin);
60    let genesis_block_hash = genesis_block.block_hash();
61    hashes.push(genesis_block_hash);
62    blocks.insert(genesis_block_hash, genesis_block);
63
64    Self {
65      blocks,
66      hashes,
67      mempool: Vec::new(),
68      nonce: 0,
69      transactions: BTreeMap::new(),
70    }
71  }
72
73  fn push_block(&mut self) -> Block {
74    let coinbase = Transaction {
75      version: 0,
76      lock_time: PackedLockTime(0),
77      input: vec![TxIn {
78        previous_output: OutPoint::null(),
79        script_sig: script::Builder::new()
80          .push_scriptint(self.blocks.len().try_into().unwrap())
81          .into_script(),
82        sequence: Sequence(0),
83        witness: Witness::new(),
84      }],
85      output: vec![TxOut {
86        value: 50 * COIN_VALUE
87          + self
88            .mempool
89            .iter()
90            .map(|tx| {
91              tx.input
92                .iter()
93                .map(|txin| {
94                  self.transactions[&txin.previous_output.txid].output
95                    [txin.previous_output.vout as usize]
96                    .value
97                })
98                .sum::<u64>()
99                - tx.output.iter().map(|txout| txout.value).sum::<u64>()
100            })
101            .sum::<u64>(),
102        script_pubkey: Script::new(),
103      }],
104    };
105
106    let block = Block {
107      header: BlockHeader {
108        version: 0,
109        prev_blockhash: *self.hashes.last().unwrap(),
110        merkle_root: TxMerkleNode::all_zeros(),
111        time: 0,
112        bits: 0,
113        nonce: self.nonce,
114      },
115      txdata: std::iter::once(coinbase)
116        .chain(self.mempool.drain(0..))
117        .collect(),
118    };
119
120    for tx in &block.txdata {
121      self.transactions.insert(tx.txid(), tx.clone());
122    }
123    self.blocks.insert(block.block_hash(), block.clone());
124    self.hashes.push(block.block_hash());
125    self.nonce += 1;
126
127    block
128  }
129
130  fn pop_block(&mut self) -> BlockHash {
131    let blockhash = self.hashes.pop().unwrap();
132    self.blocks.remove(&blockhash);
133
134    blockhash
135  }
136
137  fn broadcast_tx(&mut self, options: TransactionTemplate) -> Txid {
138    let mut total_value = 0;
139    let mut input = Vec::new();
140    for (height, tx, vout) in options.input_slots {
141      let tx = &self.blocks.get(&self.hashes[*height]).unwrap().txdata[*tx];
142      total_value += tx.output[*vout].value;
143      input.push(TxIn {
144        previous_output: OutPoint::new(tx.txid(), *vout as u32),
145        script_sig: Script::new(),
146        sequence: Sequence(0),
147        witness: Witness::new(),
148      });
149    }
150
151    let value_per_output = (total_value - options.fee) / options.output_count as u64;
152    assert_eq!(
153      value_per_output * options.output_count as u64 + options.fee,
154      total_value
155    );
156
157    let tx = Transaction {
158      version: 0,
159      lock_time: PackedLockTime(0),
160      input,
161      output: (0..options.output_count)
162        .map(|_| TxOut {
163          value: value_per_output,
164          script_pubkey: script::Builder::new().into_script(),
165        })
166        .collect(),
167    };
168    self.mempool.push(tx.clone());
169
170    tx.txid()
171  }
172}
173
174pub struct Server {
175  state: Arc<Mutex<State>>,
176}
177
178impl Server {
179  fn new() -> Self {
180    Self {
181      state: Arc::new(Mutex::new(State::new())),
182    }
183  }
184}
185
186#[jsonrpc_derive::rpc]
187pub trait Api {
188  #[rpc(name = "getblockhash")]
189  fn getblockhash(&self, height: usize) -> Result<BlockHash, jsonrpc_core::Error>;
190
191  #[rpc(name = "getblockheader")]
192  fn getblockheader(
193    &self,
194    blockhash: BlockHash,
195    verbose: bool,
196  ) -> Result<String, jsonrpc_core::Error>;
197
198  #[rpc(name = "getblock")]
199  fn getblock(&self, blockhash: BlockHash, verbosity: u64) -> Result<String, jsonrpc_core::Error>;
200
201  #[rpc(name = "getrawtransaction")]
202  fn get_raw_transaction(
203    &self,
204    txid: Txid,
205    verbose: bool,
206    blockhash: Option<BlockHash>,
207  ) -> Result<Value, jsonrpc_core::Error>;
208
209  #[rpc(name = "listunspent")]
210  fn list_unspent(
211    &self,
212    minconf: Option<usize>,
213    maxconf: Option<usize>,
214    address: Option<bitcoin::Address>,
215    include_unsafe: Option<bool>,
216    query_options: Option<String>,
217  ) -> Result<Vec<ListUnspentResultEntry>, jsonrpc_core::Error>;
218}
219
220impl Api for Server {
221  fn getblockhash(&self, height: usize) -> Result<BlockHash, jsonrpc_core::Error> {
222    match self.state.lock().unwrap().hashes.get(height) {
223      Some(block_hash) => Ok(*block_hash),
224      None => Err(jsonrpc_core::Error::new(
225        jsonrpc_core::types::error::ErrorCode::ServerError(-8),
226      )),
227    }
228  }
229
230  fn getblockheader(
231    &self,
232    block_hash: BlockHash,
233    verbose: bool,
234  ) -> Result<String, jsonrpc_core::Error> {
235    assert!(!verbose);
236    match self.state.lock().unwrap().blocks.get(&block_hash) {
237      Some(block) => Ok(hex::encode(serialize(&block.header))),
238      None => Err(jsonrpc_core::Error::new(
239        jsonrpc_core::types::error::ErrorCode::ServerError(-8),
240      )),
241    }
242  }
243
244  fn getblock(&self, block_hash: BlockHash, verbosity: u64) -> Result<String, jsonrpc_core::Error> {
245    assert_eq!(verbosity, 0, "Verbosity level {verbosity} is unsupported");
246    match self.state.lock().unwrap().blocks.get(&block_hash) {
247      Some(block) => Ok(hex::encode(serialize(block))),
248      None => Err(jsonrpc_core::Error::new(
249        jsonrpc_core::types::error::ErrorCode::ServerError(-8),
250      )),
251    }
252  }
253
254  fn get_raw_transaction(
255    &self,
256    txid: Txid,
257    verbose: bool,
258    blockhash: Option<BlockHash>,
259  ) -> Result<Value, jsonrpc_core::Error> {
260    assert_eq!(blockhash, None, "Blockhash param is unsupported");
261    if verbose {
262      match self.state.lock().unwrap().transactions.get(&txid) {
263        Some(_) => Ok(
264          serde_json::to_value(GetRawTransactionResult {
265            in_active_chain: None,
266            hex: Vec::new(),
267            txid: Txid::all_zeros(),
268            hash: Wtxid::all_zeros(),
269            size: 0,
270            vsize: 0,
271            version: 0,
272            locktime: 0,
273            vin: Vec::new(),
274            vout: Vec::new(),
275            blockhash: None,
276            confirmations: Some(1),
277            time: None,
278            blocktime: None,
279          })
280          .unwrap(),
281        ),
282        None => Err(jsonrpc_core::Error::new(
283          jsonrpc_core::types::error::ErrorCode::ServerError(-8),
284        )),
285      }
286    } else {
287      match self.state.lock().unwrap().transactions.get(&txid) {
288        Some(tx) => Ok(Value::String(hex::encode(serialize(tx)))),
289        None => Err(jsonrpc_core::Error::new(
290          jsonrpc_core::types::error::ErrorCode::ServerError(-8),
291        )),
292      }
293    }
294  }
295
296  fn list_unspent(
297    &self,
298    minconf: Option<usize>,
299    maxconf: Option<usize>,
300    address: Option<bitcoin::Address>,
301    include_unsafe: Option<bool>,
302    query_options: Option<String>,
303  ) -> Result<Vec<ListUnspentResultEntry>, jsonrpc_core::Error> {
304    assert_eq!(minconf, None, "minconf param not supported");
305    assert_eq!(maxconf, None, "maxconf param not supported");
306    assert_eq!(address, None, "address param not supported");
307    assert_eq!(include_unsafe, None, "include_unsafe param not supported");
308    assert_eq!(query_options, None, "query_options param not supported");
309    Ok(
310      self
311        .state
312        .lock()
313        .unwrap()
314        .transactions
315        .iter()
316        .flat_map(|(txid, tx)| {
317          (0..tx.output.len()).map(|vout| ListUnspentResultEntry {
318            txid: *txid,
319            vout: vout as u32,
320            address: None,
321            label: None,
322            redeem_script: None,
323            witness_script: None,
324            script_pub_key: Script::new(),
325            amount: Amount::default(),
326            confirmations: 0,
327            spendable: true,
328            solvable: true,
329            descriptor: None,
330            safe: true,
331          })
332        })
333        .collect(),
334    )
335  }
336}
337
338pub struct Handle {
339  close_handle: Option<CloseHandle>,
340  port: u16,
341  state: Arc<Mutex<State>>,
342}
343
344impl Handle {
345  pub fn url(&self) -> String {
346    format!("http://127.0.0.1:{}", self.port)
347  }
348
349  pub fn mine_blocks(&self, num: u64) -> Vec<Block> {
350    let mut bitcoin_rpc_data = self.state.lock().unwrap();
351    (0..num).map(|_| bitcoin_rpc_data.push_block()).collect()
352  }
353
354  pub fn broadcast_tx(&self, options: TransactionTemplate) -> Txid {
355    self.state.lock().unwrap().broadcast_tx(options)
356  }
357
358  pub fn invalidate_tip(&self) -> BlockHash {
359    self.state.lock().unwrap().pop_block()
360  }
361
362  pub fn tx(&self, bi: usize, ti: usize) -> Transaction {
363    let state = self.state.lock().unwrap();
364    state.blocks[&state.hashes[bi]].txdata[ti].clone()
365  }
366}
367
368impl Drop for Handle {
369  fn drop(&mut self) {
370    self.close_handle.take().unwrap().close();
371  }
372}