Skip to content

Commit cd326fa

Browse files
committed
Adds basic Carrier
Adapts other actors to fit the Carrier in. Temporarily uses both rust-lightnings rpc and bitcoincore-rpc rpc clients. Waiting for both to be merged. Related to: rust-bitcoin/rust-bitcoincore-rpc#166
1 parent 9442246 commit cd326fa

File tree

9 files changed

+455
-17
lines changed

9 files changed

+455
-17
lines changed

teos/Cargo.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ serde_json = "1.0"
1818
tokio = { version = "1.5", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] }
1919

2020
bitcoin = "0.27"
21-
lightning-block-sync = {version = "0.0.99", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main", features = [ "rpc-client" ] }
22-
lightning-net-tokio = { version = "0.0.99", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" }
23-
lightning = { version = "0.0.99", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" }
21+
lightning-block-sync = {version = "0.0.100", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main", features = [ "rpc-client" ] }
22+
lightning-net-tokio = { version = "0.0.100", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" }
23+
lightning = { version = "0.0.100", git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" }
24+
bitcoincore-rpc = "0.13.0"
25+
26+
2427

2528
teos-common = {path = "../teos-common"}
2629

2730

2831
[dev-dependencies]
2932
rand = "0.8.4"
30-
chunked_transfer = "1.4"
33+
chunked_transfer = "1.4"
34+
httpmock = "0.6"

teos/src/carrier.rs

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
4+
use crate::errors;
5+
use crate::rpc_errors;
6+
7+
use bitcoincore_rpc::bitcoin::{Transaction, Txid};
8+
use bitcoincore_rpc::{
9+
jsonrpc::error::Error::Rpc as RpcError, Client as BitcoindClient,
10+
Error::JsonRpc as JsonRpcError, RpcApi,
11+
};
12+
13+
#[derive(Clone, Debug)]
14+
pub struct Receipt {
15+
delivered: bool,
16+
confirmations: Option<u32>,
17+
reason: Option<i32>,
18+
}
19+
20+
impl Receipt {
21+
fn new(delivered: bool, confirmations: Option<u32>, reason: Option<i32>) -> Self {
22+
Receipt {
23+
delivered,
24+
confirmations,
25+
reason,
26+
}
27+
}
28+
}
29+
30+
pub struct Carrier {
31+
bitcoin_cli: Arc<BitcoindClient>,
32+
issued_receipts: HashMap<Txid, Receipt>,
33+
}
34+
35+
impl Carrier {
36+
pub fn new(bitcoin_cli: Arc<BitcoindClient>) -> Self {
37+
Carrier {
38+
bitcoin_cli,
39+
issued_receipts: HashMap::new(),
40+
}
41+
}
42+
43+
pub async fn send_transaction(&mut self, tx: &Transaction) -> Receipt {
44+
log::info!("Pushing transaction to the network: {}", tx.txid());
45+
let receipt: Receipt;
46+
47+
match self.bitcoin_cli.send_raw_transaction(tx) {
48+
Ok(_) => {
49+
log::info!("Transaction successfully delivered: {}", tx.txid());
50+
receipt = Receipt::new(true, Some(0), None);
51+
}
52+
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
53+
// Since we're pushing a raw transaction to the network we can face several rejections
54+
rpc_errors::RPC_VERIFY_REJECTED => {
55+
log::error!("Transaction couldn't be broadcast. {:?}", rpcerr);
56+
receipt = Receipt::new(false, Some(0), Some(rpc_errors::RPC_VERIFY_REJECTED))
57+
}
58+
rpc_errors::RPC_VERIFY_ERROR => {
59+
log::error!("Transaction couldn't be broadcast. {:?}", rpcerr);
60+
receipt = Receipt::new(false, Some(0), Some(rpc_errors::RPC_VERIFY_ERROR))
61+
}
62+
rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => {
63+
log::info!(
64+
"Transaction is already in the blockchain: {}. Getting confirmation count",
65+
tx.txid()
66+
);
67+
68+
// TODO: Get confirmation count from bitcoind. Currently `get_transaction` builds a transaction but it has no confirmation
69+
// field. Another method may be needed for this.
70+
// FIXME: Update the confirmation count here
71+
receipt = Receipt::new(true, Some(0), None)
72+
}
73+
rpc_errors::RPC_DESERIALIZATION_ERROR => {
74+
// Adding this here just for completeness. We should never end up here. The Carrier only sends txs handed by the Responder,
75+
// who receives them from the Watcher, who checks that the tx can be properly deserialized.
76+
log::info!("Transaction cannot be deserialized: {}", tx.txid());
77+
receipt = Receipt::new(false, None, Some(rpc_errors::RPC_DESERIALIZATION_ERROR))
78+
}
79+
_ => {
80+
// If something else happens (unlikely but possible) log it so we can treat it in future releases
81+
log::error!(
82+
"Unexpected rpc error when calling sendrawtransaction: {:?}",
83+
rpcerr
84+
);
85+
receipt = Receipt::new(false, None, Some(errors::UNKNOWN_JSON_RPC_EXCEPTION))
86+
}
87+
},
88+
Err(e) => {
89+
{
90+
// FIXME: Only logging for now. This needs finer catching. e.g. Connection errors need to be handled here
91+
log::error!("Unexpected error when calling sendrawtransaction: {:?}", e);
92+
receipt = Receipt::new(false, None, None)
93+
}
94+
}
95+
}
96+
97+
self.issued_receipts.insert(tx.txid(), receipt.clone());
98+
receipt
99+
}
100+
101+
pub async fn get_transaction(&self, txid: &Txid) -> Option<Transaction> {
102+
match self.bitcoin_cli.get_raw_transaction(txid, None) {
103+
Ok(tx) => Some(tx),
104+
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
105+
rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => {
106+
log::info!("Transaction not found in mempool nor blockchain: {}", txid);
107+
None
108+
}
109+
e => {
110+
log::error!(
111+
"Unexpected error code when calling getrawtransaction: {}",
112+
e
113+
);
114+
None
115+
}
116+
},
117+
// TODO: This needs finer catching. e.g. Connection errors need to be handled here
118+
Err(e) => {
119+
log::error!(
120+
"Unexpected JSONRPCError when calling getrawtransaction: {}",
121+
e
122+
);
123+
None
124+
}
125+
}
126+
}
127+
}
128+
129+
// FIXME: This needs fixing. Tests make sense but the response has to contain the seed sent by the request, otherwise the rpc client rejects the response.
130+
// Already contacted the httpmock devs regarding this to see if there is any solution.
131+
#[cfg(test)]
132+
mod tests {
133+
use super::*;
134+
use crate::test_utils::{TXID_HEX, TX_HEX};
135+
use bitcoincore_rpc::bitcoin::consensus::{deserialize, serialize};
136+
use bitcoincore_rpc::bitcoin::hashes::hex::FromHex;
137+
use bitcoincore_rpc::Auth;
138+
use httpmock::prelude::*;
139+
use serde_json;
140+
use std::sync::Arc;
141+
142+
#[tokio::test]
143+
async fn test_send_transaction_ok() {
144+
let server = MockServer::start();
145+
let txid_mock = server.mock(|when, then| {
146+
when.method(POST);
147+
then.status(200)
148+
.header("content-type", "application/json")
149+
.body(serde_json::json!({ "id": TXID_HEX }).to_string());
150+
});
151+
152+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
153+
let mut carrier = Carrier::new(bitcoin_cli);
154+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
155+
let r = carrier.send_transaction(&tx).await;
156+
157+
txid_mock.assert();
158+
assert!(r.delivered);
159+
assert_eq!(r.confirmations, Some(0));
160+
assert_eq!(r.reason, None);
161+
}
162+
163+
#[tokio::test]
164+
async fn test_send_transaction_verify_rejected() {
165+
let server = MockServer::start();
166+
let txid_mock = server.mock(|when, then| {
167+
when.method(POST);
168+
then.status(200)
169+
.header("content-type", "application/json")
170+
.body(serde_json::json!({ "error": rpc_errors::RPC_VERIFY_REJECTED }).to_string());
171+
});
172+
173+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
174+
let mut carrier = Carrier::new(bitcoin_cli);
175+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
176+
let r = carrier.send_transaction(&tx).await;
177+
178+
txid_mock.assert();
179+
assert!(!r.delivered);
180+
assert_eq!(r.confirmations, None);
181+
assert_eq!(r.reason, Some(rpc_errors::RPC_VERIFY_REJECTED));
182+
}
183+
184+
#[tokio::test]
185+
async fn test_send_transaction_verify_error() {
186+
let server = MockServer::start();
187+
let txid_mock = server.mock(|when, then| {
188+
when.method(POST);
189+
then.status(200)
190+
.header("content-type", "application/json")
191+
.body(serde_json::json!({ "error": rpc_errors::RPC_VERIFY_ERROR }).to_string());
192+
});
193+
194+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
195+
let mut carrier = Carrier::new(bitcoin_cli);
196+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
197+
let r = carrier.send_transaction(&tx).await;
198+
199+
txid_mock.assert();
200+
assert!(!r.delivered);
201+
assert_eq!(r.confirmations, None);
202+
assert_eq!(r.reason, Some(rpc_errors::RPC_VERIFY_ERROR));
203+
}
204+
205+
#[tokio::test]
206+
async fn test_send_transaction_verify_already_in_chain() {
207+
let server = MockServer::start();
208+
let txid_mock = server.mock(|when, then| {
209+
when.method(POST);
210+
then.status(200)
211+
.header("content-type", "application/json")
212+
.body(
213+
serde_json::json!({ "error": rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN })
214+
.to_string(),
215+
);
216+
});
217+
218+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
219+
let mut carrier = Carrier::new(bitcoin_cli);
220+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
221+
let r = carrier.send_transaction(&tx).await;
222+
223+
txid_mock.assert();
224+
assert!(r.delivered);
225+
// FIXME: This is temporary. Confirmations need to be properly set.
226+
assert_eq!(r.confirmations, Some(0));
227+
assert_eq!(r.reason, None);
228+
}
229+
230+
#[tokio::test]
231+
async fn test_send_transaction_unexpected_error() {
232+
let server = MockServer::start();
233+
234+
// Reply with an unexpected rpc error (any of the non accounted for should do)
235+
let txid_mock = server.mock(|when, then| {
236+
when.method(POST);
237+
then.status(200)
238+
.header("content-type", "application/json")
239+
.body(serde_json::json!({ "error": rpc_errors::RPC_MISC_ERROR }).to_string());
240+
});
241+
242+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
243+
let mut carrier = Carrier::new(bitcoin_cli);
244+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
245+
let r = carrier.send_transaction(&tx).await;
246+
247+
txid_mock.assert();
248+
assert!(!r.delivered);
249+
assert_eq!(r.confirmations, None);
250+
assert_eq!(r.reason, Some(errors::UNKNOWN_JSON_RPC_EXCEPTION));
251+
}
252+
253+
#[tokio::test]
254+
async fn test_send_transaction_connection_error() {
255+
MockServer::start();
256+
257+
// Try to connect to an nonexisting server.
258+
let bitcoin_cli =
259+
Arc::new(BitcoindClient::new("http://localhost:1234".to_string(), Auth::None).unwrap());
260+
let mut carrier = Carrier::new(bitcoin_cli);
261+
let tx: Transaction = deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
262+
let r = carrier.send_transaction(&tx).await;
263+
264+
assert!(!r.delivered);
265+
assert_eq!(r.confirmations, None);
266+
assert_eq!(r.reason, None);
267+
}
268+
269+
#[tokio::test]
270+
async fn get_transaction_ok() {
271+
let server = MockServer::start();
272+
let txid_mock = server.mock(|when, then| {
273+
when.method(POST);
274+
then.status(200)
275+
.header("content-type", "application/json")
276+
.body(serde_json::json!({ "result": TX_HEX }).to_string());
277+
});
278+
279+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
280+
let carrier = Carrier::new(bitcoin_cli);
281+
let txid = Txid::from_hex(TXID_HEX).unwrap();
282+
let r = carrier.get_transaction(&txid).await;
283+
284+
txid_mock.assert();
285+
assert_eq!(serialize(&r.unwrap()), Vec::from_hex(TX_HEX).unwrap());
286+
}
287+
288+
#[tokio::test]
289+
async fn get_transaction_not_found() {
290+
let server = MockServer::start();
291+
let txid_mock = server.mock(|when, then| {
292+
when.method(POST);
293+
then.status(200)
294+
.header("content-type", "application/json")
295+
.body(
296+
serde_json::json!({ "error": rpc_errors::RPC_INVALID_ADDRESS_OR_KEY })
297+
.to_string(),
298+
);
299+
});
300+
301+
let bitcoin_cli = Arc::new(BitcoindClient::new(server.base_url(), Auth::None).unwrap());
302+
let carrier = Carrier::new(bitcoin_cli);
303+
let txid = Txid::from_hex(TXID_HEX).unwrap();
304+
let r = carrier.get_transaction(&txid).await;
305+
306+
txid_mock.assert();
307+
assert_eq!(r, None);
308+
}
309+
310+
#[tokio::test]
311+
async fn get_transaction_connection_error() {
312+
MockServer::start();
313+
314+
// Try to connect to an nonexisting server.
315+
let bitcoin_cli =
316+
Arc::new(BitcoindClient::new("http://localhost:1234".to_string(), Auth::None).unwrap());
317+
let carrier = Carrier::new(bitcoin_cli);
318+
let txid = Txid::from_hex(TXID_HEX).unwrap();
319+
let r = carrier.get_transaction(&txid).await;
320+
321+
assert_eq!(r, None);
322+
}
323+
}

teos/src/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Custom RPC errors [255+]
2+
pub const RPC_TX_REORGED_AFTER_BROADCAST: i32 = -256;
3+
// UNHANDLED
4+
pub const UNKNOWN_JSON_RPC_EXCEPTION: i32 = -257;

teos/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
pub mod bitcoin_cli;
2+
pub mod carrier;
23
pub mod chain_monitor;
34
mod convert;
5+
mod errors;
46
mod extended_appointment;
57
pub mod gatekeeper;
68
pub mod responder;
9+
mod rpc_errors;
710
pub mod watcher;
811

912
#[cfg(test)]

0 commit comments

Comments
 (0)