mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-08 20:09:54 +00:00
Document Bitcoin RPC and make it more robust
This commit is contained in:
parent
9b47ad56bb
commit
0525ba2f62
8 changed files with 64 additions and 13 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -600,6 +600,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.6",
|
"sha2 0.10.6",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -30,3 +30,5 @@ reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.6", features = ["tests"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.6", features = ["tests"] }
|
||||||
|
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
|
@ -4,7 +4,7 @@ pub mod crypto;
|
||||||
pub mod algorithm;
|
pub mod algorithm;
|
||||||
/// Wallet functionality to create transactions.
|
/// Wallet functionality to create transactions.
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
/// A minimal async RPC.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -16,11 +16,12 @@ use bitcoin::{
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub(crate) enum RpcResponse<T> {
|
enum RpcResponse<T> {
|
||||||
Ok { result: T },
|
Ok { result: T },
|
||||||
Err { error: String },
|
Err { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Rpc(String);
|
pub struct Rpc(String);
|
||||||
|
|
||||||
|
@ -35,10 +36,14 @@ pub enum RpcError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rpc {
|
impl Rpc {
|
||||||
pub fn new(url: String) -> Rpc {
|
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
||||||
Rpc(url)
|
let rpc = Rpc(url);
|
||||||
|
// Make an RPC request to verify the node is reachable and sane
|
||||||
|
rpc.get_latest_block_number().await?;
|
||||||
|
Ok(rpc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform an arbitrary RPC call.
|
||||||
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
@ -63,17 +68,21 @@ impl Rpc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the latest block's number.
|
||||||
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
|
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
|
||||||
self.rpc_call("getblockcount", json!([])).await
|
self.rpc_call("getblockcount", json!([])).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the hash of a block by the block's number.
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||||
let mut hash =
|
let mut hash =
|
||||||
self.rpc_call::<BlockHash>("getblockhash", json!([number])).await?.as_hash().into_inner();
|
self.rpc_call::<BlockHash>("getblockhash", json!([number])).await?.as_hash().into_inner();
|
||||||
|
// bitcoin stores the inner bytes in reverse order.
|
||||||
hash.reverse();
|
hash.reverse();
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a block's number by its hash.
|
||||||
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Number {
|
struct Number {
|
||||||
|
@ -82,19 +91,42 @@ impl Rpc {
|
||||||
Ok(self.rpc_call::<Number>("getblockheader", json!([hash.to_hex()])).await?.height)
|
Ok(self.rpc_call::<Number>("getblockheader", json!([hash.to_hex()])).await?.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a block by its hash.
|
||||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getblock", json!([hash.to_hex(), 0])).await?;
|
let hex = self.rpc_call::<String>("getblock", json!([hash.to_hex(), 0])).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)
|
let block: Block = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
|
|
||||||
|
let mut block_hash = block.block_hash().as_hash().into_inner();
|
||||||
|
block_hash.reverse();
|
||||||
|
if hash != &block_hash {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Publish a transaction.
|
||||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
||||||
self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await
|
let txid = self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await?;
|
||||||
|
if txid != tx.txid() {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
Ok(txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a transaction by its hash.
|
||||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hash.to_hex()])).await?;
|
let hex = self.rpc_call::<String>("getrawtransaction", json!([hash.to_hex()])).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)
|
let tx: Transaction = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
|
|
||||||
|
let mut tx_hash = tx.txid().as_hash().into_inner();
|
||||||
|
tx_hash.reverse();
|
||||||
|
if hash != &tx_hash {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,11 @@ use frost::{
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
tests::{algorithm_machines, key_gen, sign},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{crypto::{x_only, make_even}, algorithm::Schnorr};
|
use crate::{
|
||||||
|
crypto::{x_only, make_even},
|
||||||
|
algorithm::Schnorr,
|
||||||
|
rpc::Rpc,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_algorithm() {
|
fn test_algorithm() {
|
||||||
|
@ -25,7 +29,8 @@ fn test_algorithm() {
|
||||||
*keys = keys.offset(Scalar::from(offset));
|
*keys = keys.offset(Scalar::from(offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
let algo = Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
let algo =
|
||||||
|
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algo.clone(),
|
algo.clone(),
|
||||||
|
@ -42,3 +47,14 @@ fn test_algorithm() {
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rpc() {
|
||||||
|
let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await.unwrap();
|
||||||
|
|
||||||
|
let latest = rpc.get_latest_block_number().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
rpc.get_block_number(&rpc.get_block_hash(latest).await.unwrap()).await.unwrap(),
|
||||||
|
latest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -215,8 +215,8 @@ impl PartialEq for Bitcoin {
|
||||||
impl Eq for Bitcoin {}
|
impl Eq for Bitcoin {}
|
||||||
|
|
||||||
impl Bitcoin {
|
impl Bitcoin {
|
||||||
pub fn new(url: String) -> Bitcoin {
|
pub async fn new(url: String) -> Bitcoin {
|
||||||
Bitcoin { rpc: Rpc::new(url) }
|
Bitcoin { rpc: Rpc::new(url).await.expect("couldn't create a Bitcoin RPC") }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -450,7 +450,7 @@ async fn main() {
|
||||||
let url = env::var("COIN_RPC").expect("coin rpc wasn't specified as an env var");
|
let url = env::var("COIN_RPC").expect("coin rpc wasn't specified as an env var");
|
||||||
match env::var("COIN").expect("coin wasn't specified as an env var").as_str() {
|
match env::var("COIN").expect("coin wasn't specified as an env var").as_str() {
|
||||||
#[cfg(feature = "bitcoin")]
|
#[cfg(feature = "bitcoin")]
|
||||||
"bitcoin" => run(db, Bitcoin::new(url), coordinator).await,
|
"bitcoin" => run(db, Bitcoin::new(url).await, coordinator).await,
|
||||||
#[cfg(feature = "monero")]
|
#[cfg(feature = "monero")]
|
||||||
"monero" => run(db, Monero::new(url), coordinator).await,
|
"monero" => run(db, Monero::new(url), coordinator).await,
|
||||||
_ => panic!("unrecognized coin"),
|
_ => panic!("unrecognized coin"),
|
||||||
|
|
|
@ -3,7 +3,7 @@ mod bitcoin {
|
||||||
use crate::coins::Bitcoin;
|
use crate::coins::Bitcoin;
|
||||||
|
|
||||||
async fn bitcoin() -> Bitcoin {
|
async fn bitcoin() -> Bitcoin {
|
||||||
let bitcoin = Bitcoin::new("http://serai:seraidex@127.0.0.1:18443".to_string());
|
let bitcoin = Bitcoin::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await;
|
||||||
bitcoin.fresh_chain().await;
|
bitcoin.fresh_chain().await;
|
||||||
bitcoin
|
bitcoin
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue