X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-block-sync%2Fsrc%2Fconvert.rs;h=ed811d2cc0c3f629e16a450937c58d8287f35689;hb=19bcb1c62f100dba5dfaf70e8899ae904e6d7f75;hp=e8e1427bdb654ffa432d069faf37457fc5a74cea;hpb=f65d05c23db8174d49771fd1b6b8dd0efa72ddd4;p=rust-lightning diff --git a/lightning-block-sync/src/convert.rs b/lightning-block-sync/src/convert.rs index e8e1427b..ed811d2c 100644 --- a/lightning-block-sync/src/convert.rs +++ b/lightning-block-sync/src/convert.rs @@ -1,19 +1,25 @@ -use crate::{BlockHeaderData, BlockSourceError}; use crate::http::{BinaryResponse, JsonResponse}; -use crate::utils::hex_to_uint256; +use crate::utils::hex_to_work; +use crate::{BlockHeaderData, BlockSourceError}; -use bitcoin::blockdata::block::{Block, BlockHeader}; +use bitcoin::blockdata::block::{Block, Header}; use bitcoin::consensus::encode; use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid}; -use bitcoin::hashes::hex::{ToHex, FromHex}; - -use serde::Deserialize; +use bitcoin::hashes::hex::FromHex; +use bitcoin::Transaction; use serde_json; use std::convert::From; use std::convert::TryFrom; use std::convert::TryInto; +use std::str::FromStr; +use bitcoin::hashes::Hash; + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> Result { Ok(self.0) } +} /// Conversion from `std::io::Error` into `BlockSourceError`. impl From for BlockSourceError { @@ -38,13 +44,24 @@ impl TryInto for BinaryResponse { } } +/// Parses binary data as a block hash. +impl TryInto for BinaryResponse { + type Error = std::io::Error; + + fn try_into(self) -> std::io::Result { + BlockHash::from_slice(&self.0).map_err(|_| + std::io::Error::new(std::io::ErrorKind::InvalidData, "bad block hash length") + ) + } +} + /// Converts a JSON value into block header data. The JSON value may be an object representing a /// block header or an array of such objects. In the latter case, the first object is converted. impl TryInto for JsonResponse { type Error = std::io::Error; fn try_into(self) -> std::io::Result { - let mut header = match self.0 { + let header = match self.0 { serde_json::Value::Array(mut array) if !array.is_empty() => array.drain(..).next().unwrap(), serde_json::Value::Object(_) => self.0, _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected JSON type")), @@ -55,56 +72,42 @@ impl TryInto for JsonResponse { } // Add an empty previousblockhash for the genesis block. - if let None = header.get("previousblockhash") { - let hash: BlockHash = Default::default(); - header.as_object_mut().unwrap().insert("previousblockhash".to_string(), serde_json::json!(hash.to_hex())); - } - - match serde_json::from_value::(header) { - Err(_) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid header response")), - Ok(response) => match response.try_into() { - Err(_) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid header data")), - Ok(header) => Ok(header), - }, + match header.try_into() { + Err(_) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid header data")), + Ok(header) => Ok(header), } } } -/// Response data from `getblockheader` RPC and `headers` REST requests. -#[derive(Deserialize)] -struct GetHeaderResponse { - pub version: i32, - pub merkleroot: String, - pub time: u32, - pub nonce: u32, - pub bits: String, - pub previousblockhash: String, - - pub chainwork: String, - pub height: u32, -} +impl TryFrom for BlockHeaderData { + type Error = (); -/// Converts from `GetHeaderResponse` to `BlockHeaderData`. -impl TryFrom for BlockHeaderData { - type Error = bitcoin::hashes::hex::Error; + fn try_from(response: serde_json::Value) -> Result { + macro_rules! get_field { ($name: expr, $ty_access: tt) => { + response.get($name).ok_or(())?.$ty_access().ok_or(())? + } } - fn try_from(response: GetHeaderResponse) -> Result { Ok(BlockHeaderData { - header: BlockHeader { - version: response.version, - prev_blockhash: BlockHash::from_hex(&response.previousblockhash)?, - merkle_root: TxMerkleNode::from_hex(&response.merkleroot)?, - time: response.time, - bits: u32::from_be_bytes(<[u8; 4]>::from_hex(&response.bits)?), - nonce: response.nonce, + header: Header { + version: bitcoin::blockdata::block::Version::from_consensus( + get_field!("version", as_i64).try_into().map_err(|_| ())? + ), + prev_blockhash: if let Some(hash_str) = response.get("previousblockhash") { + BlockHash::from_str(hash_str.as_str().ok_or(())?).map_err(|_| ())? + } else { BlockHash::all_zeros() }, + merkle_root: TxMerkleNode::from_str(get_field!("merkleroot", as_str)).map_err(|_| ())?, + time: get_field!("time", as_u64).try_into().map_err(|_| ())?, + bits: bitcoin::CompactTarget::from_consensus( + u32::from_be_bytes(<[u8; 4]>::from_hex(get_field!("bits", as_str)).map_err(|_| ())?) + ), + nonce: get_field!("nonce", as_u64).try_into().map_err(|_| ())?, }, - chainwork: hex_to_uint256(&response.chainwork)?, - height: response.height, + chainwork: hex_to_work(get_field!("chainwork", as_str)).map_err(|_| ())?, + height: get_field!("height", as_u64).try_into().map_err(|_| ())?, }) } } - /// Converts a JSON value into a block. Assumes the block is hex-encoded in a JSON string. impl TryInto for JsonResponse { type Error = std::io::Error; @@ -133,7 +136,7 @@ impl TryInto<(BlockHash, Option)> for JsonResponse { } let hash = match &self.0["bestblockhash"] { - serde_json::Value::String(hex_data) => match BlockHash::from_hex(&hex_data) { + serde_json::Value::String(hex_data) => match BlockHash::from_str(&hex_data) { Err(_) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid hex data")), Ok(block_hash) => block_hash, }, @@ -159,49 +162,139 @@ impl TryInto<(BlockHash, Option)> for JsonResponse { impl TryInto for JsonResponse { type Error = std::io::Error; fn try_into(self) -> std::io::Result { - match self.0.as_str() { - None => Err(std::io::Error::new( + let hex_data = self.0.as_str().ok_or(Self::Error::new(std::io::ErrorKind::InvalidData, "expected JSON string" ))?; + Txid::from_str(hex_data).map_err(|err|Self::Error::new(std::io::ErrorKind::InvalidData, err.to_string() )) + } +} + +/// Converts a JSON value into a transaction. WATCH OUT! this cannot be used for zero-input transactions +/// (e.g. createrawtransaction). See +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let hex_tx = if self.0.is_object() { + // result is json encoded + match &self.0["hex"] { + // result has hex field + serde_json::Value::String(hex_data) => match self.0["complete"] { + // result may or may not be signed (e.g. signrawtransactionwithwallet) + serde_json::Value::Bool(x) => { + if x == false { + let reason = match &self.0["errors"][0]["error"] { + serde_json::Value::String(x) => x.as_str(), + _ => "Unknown error", + }; + + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("transaction couldn't be signed. {}", reason), + )); + } else { + hex_data + } + } + // result is a complete transaction (e.g. getrawtranaction verbose) + _ => hex_data, + }, + _ => return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "expected JSON string", + )), + } + } else { + // result is plain text (e.g. getrawtransaction no verbose) + match self.0.as_str() { + Some(hex_tx) => hex_tx, + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "expected JSON string", + )) + } + } + }; + + match Vec::::from_hex(hex_tx) { + Err(_) => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, - "expected JSON string", + "invalid hex data", )), - Some(hex_data) => match Vec::::from_hex(hex_data) { + Ok(tx_data) => match encode::deserialize(&tx_data) { Err(_) => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, - "invalid hex data", + "invalid transaction", )), - Ok(txid_data) => match encode::deserialize(&txid_data) { - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid txid", - )), - Ok(txid) => Ok(txid), - }, + Ok(tx) => Ok(tx), }, } } } +impl TryInto for JsonResponse { + type Error = std::io::Error; + + fn try_into(self) -> std::io::Result { + match self.0.as_str() { + None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON string")), + Some(hex_data) if hex_data.len() != 64 => + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid hash length")), + Some(hex_data) => BlockHash::from_str(hex_data) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid hex data")), + } + } +} + +/// The REST `getutxos` endpoint retuns a whole pile of data we don't care about and one bit we do +/// - whether the `hit bitmap` field had any entries. Thus we condense the result down into only +/// that. +#[cfg(feature = "rest-client")] +pub(crate) struct GetUtxosResponse { + pub(crate) hit_bitmap_nonempty: bool +} + +#[cfg(feature = "rest-client")] +impl TryInto for JsonResponse { + type Error = std::io::Error; + + fn try_into(self) -> std::io::Result { + let bitmap_str = + self.0.as_object().ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected an object"))? + .get("bitmap").ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "missing bitmap field"))? + .as_str().ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "bitmap should be an str"))?; + let mut hit_bitmap_nonempty = false; + for c in bitmap_str.chars() { + if c < '0' || c > '9' { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid byte")); + } + if c > '0' { hit_bitmap_nonempty = true; } + } + Ok(GetUtxosResponse { hit_bitmap_nonempty }) + } +} + #[cfg(test)] pub(crate) mod tests { use super::*; use bitcoin::blockdata::constants::genesis_block; - use bitcoin::consensus::encode; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; + use hex::DisplayHex; + use serde_json::value::Number; + use serde_json::Value; /// Converts from `BlockHeaderData` into a `GetHeaderResponse` JSON value. impl From for serde_json::Value { fn from(data: BlockHeaderData) -> Self { let BlockHeaderData { chainwork, height, header } = data; serde_json::json!({ - "chainwork": chainwork.to_string()["0x".len()..], + "chainwork": chainwork.to_be_bytes().as_hex().to_string(), "height": height, - "version": header.version, - "merkleroot": header.merkle_root.to_hex(), + "version": header.version.to_consensus(), + "merkleroot": header.merkle_root.to_string(), "time": header.time, "nonce": header.nonce, - "bits": header.bits.to_hex(), - "previousblockhash": header.prev_blockhash.to_hex(), + "bits": header.bits.to_consensus().to_be_bytes().as_hex().to_string(), + "previousblockhash": header.prev_blockhash.to_string(), }) } } @@ -243,7 +336,7 @@ pub(crate) mod tests { match TryInto::::try_into(response) { Err(e) => { assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid header response"); + assert_eq!(e.get_ref().unwrap().to_string(), "invalid header data"); }, Ok(_) => panic!("Expected error"), } @@ -290,7 +383,7 @@ pub(crate) mod tests { #[test] fn into_block_header_from_json_response_with_valid_header_array() { let genesis_block = genesis_block(Network::Bitcoin); - let best_block_header = BlockHeader { + let best_block_header = Header { prev_blockhash: genesis_block.block_hash(), ..genesis_block.header }; @@ -437,7 +530,7 @@ pub(crate) mod tests { fn into_block_hash_from_json_response_without_height() { let block = genesis_block(Network::Bitcoin); let response = JsonResponse(serde_json::json!({ - "bestblockhash": block.block_hash().to_hex(), + "bestblockhash": block.block_hash().to_string(), })); match TryInto::<(BlockHash, Option)>::try_into(response) { Err(e) => panic!("Unexpected error: {:?}", e), @@ -452,7 +545,7 @@ pub(crate) mod tests { fn into_block_hash_from_json_response_with_unexpected_blocks_type() { let block = genesis_block(Network::Bitcoin); let response = JsonResponse(serde_json::json!({ - "bestblockhash": block.block_hash().to_hex(), + "bestblockhash": block.block_hash().to_string(), "blocks": "foo", })); match TryInto::<(BlockHash, Option)>::try_into(response) { @@ -468,7 +561,7 @@ pub(crate) mod tests { fn into_block_hash_from_json_response_with_invalid_height() { let block = genesis_block(Network::Bitcoin); let response = JsonResponse(serde_json::json!({ - "bestblockhash": block.block_hash().to_hex(), + "bestblockhash": block.block_hash().to_string(), "blocks": std::u64::MAX, })); match TryInto::<(BlockHash, Option)>::try_into(response) { @@ -484,7 +577,7 @@ pub(crate) mod tests { fn into_block_hash_from_json_response_with_height() { let block = genesis_block(Network::Bitcoin); let response = JsonResponse(serde_json::json!({ - "bestblockhash": block.block_hash().to_hex(), + "bestblockhash": block.block_hash().to_string(), "blocks": 1, })); match TryInto::<(BlockHash, Option)>::try_into(response) { @@ -514,7 +607,7 @@ pub(crate) mod tests { match TryInto::::try_into(response) { Err(e) => { assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data"); + assert_eq!(e.get_ref().unwrap().to_string(), "bad hex string length 6 (expected 64)"); } Ok(_) => panic!("Expected error"), } @@ -526,7 +619,7 @@ pub(crate) mod tests { match TryInto::::try_into(response) { Err(e) => { assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid txid"); + assert_eq!(e.get_ref().unwrap().to_string(), "bad hex string length 4 (expected 64)"); } Ok(_) => panic!("Expected error"), } @@ -541,4 +634,115 @@ pub(crate) mod tests { Ok(txid) => assert_eq!(txid, target_txid), } } + + #[test] + fn into_txid_from_bitcoind_rpc_json_response() { + let mut rpc_response = serde_json::json!( + {"error": "", "id": "770", "result": "7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906"} + + ); + let r: std::io::Result = JsonResponse(rpc_response.get_mut("result").unwrap().take()) + .try_into(); + assert_eq!( + r.unwrap().to_string(), + "7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906" + ); + } + + // TryInto can be used in two ways, first with plain hex response where data is + // the hex encoded transaction (e.g. as a result of getrawtransaction) or as a JSON object + // where the hex encoded transaction can be found in the hex field of the object (if present) + // (e.g. as a result of signrawtransactionwithwallet). + + // plain hex transaction + + #[test] + fn into_tx_from_json_response_with_invalid_hex_data() { + let response = JsonResponse(serde_json::json!("foobar")); + match TryInto::::try_into(response) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); + assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data"); + } + Ok(_) => panic!("Expected error"), + } + } + + #[test] + fn into_tx_from_json_response_with_invalid_data_type() { + let response = JsonResponse(Value::Number(Number::from_f64(1.0).unwrap())); + match TryInto::::try_into(response) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); + assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + } + Ok(_) => panic!("Expected error"), + } + } + + #[test] + fn into_tx_from_json_response_with_invalid_tx_data() { + let response = JsonResponse(serde_json::json!("abcd")); + match TryInto::::try_into(response) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); + assert_eq!(e.get_ref().unwrap().to_string(), "invalid transaction"); + } + Ok(_) => panic!("Expected error"), + } + } + + #[test] + fn into_tx_from_json_response_with_valid_tx_data_plain() { + let genesis_block = genesis_block(Network::Bitcoin); + let target_tx = genesis_block.txdata.get(0).unwrap(); + let response = JsonResponse(serde_json::json!(encode::serialize_hex(&target_tx))); + match TryInto::::try_into(response) { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tx) => assert_eq!(&tx, target_tx), + } + } + + #[test] + fn into_tx_from_json_response_with_valid_tx_data_hex_field() { + let genesis_block = genesis_block(Network::Bitcoin); + let target_tx = genesis_block.txdata.get(0).unwrap(); + let response = JsonResponse(serde_json::json!({"hex": encode::serialize_hex(&target_tx)})); + match TryInto::::try_into(response) { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tx) => assert_eq!(&tx, target_tx), + } + } + + // transaction in hex field of JSON object + + #[test] + fn into_tx_from_json_response_with_no_hex_field() { + let response = JsonResponse(serde_json::json!({ "error": "foo" })); + match TryInto::::try_into(response) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); + assert_eq!( + e.get_ref().unwrap().to_string(), + "expected JSON string" + ); + } + Ok(_) => panic!("Expected error"), + } + } + + #[test] + fn into_tx_from_json_response_not_signed() { + let response = JsonResponse(serde_json::json!({ "hex": "foo", "complete": false })); + match TryInto::::try_into(response) { + Err(e) => { + assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); + assert!( + e.get_ref().unwrap().to_string().contains( + "transaction couldn't be signed") + ); + } + Ok(_) => panic!("Expected error"), + } + } }