Merge pull request #2432 from jkczyz/2023-07-bolt12-node-signer
[rust-lightning] / lightning-block-sync / src / convert.rs
index 37b2c4323972fb1152fb27b1ee2c195859806104..d6294e1d2a79518c05a674cae5ea86d41fc1faf7 100644 (file)
@@ -1,19 +1,19 @@
-use crate::{BlockHeaderData, BlockSourceError};
 use crate::http::{BinaryResponse, JsonResponse};
 use crate::utils::hex_to_uint256;
+use crate::{BlockHeaderData, BlockSourceError};
 
 use bitcoin::blockdata::block::{Block, BlockHeader};
 use bitcoin::consensus::encode;
-use bitcoin::hash_types::{BlockHash, TxMerkleNode};
-use bitcoin::hashes::hex::{ToHex, FromHex};
-
-use serde::Deserialize;
+use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid};
+use bitcoin::hashes::hex::FromHex;
+use bitcoin::Transaction;
 
 use serde_json;
 
 use std::convert::From;
 use std::convert::TryFrom;
 use std::convert::TryInto;
+use bitcoin::hashes::Hash;
 
 /// Conversion from `std::io::Error` into `BlockSourceError`.
 impl From<std::io::Error> for BlockSourceError {
@@ -44,7 +44,7 @@ impl TryInto<BlockHeaderData> for JsonResponse {
        type Error = std::io::Error;
 
        fn try_into(self) -> std::io::Result<BlockHeaderData> {
-               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 +55,38 @@ impl TryInto<BlockHeaderData> 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::<GetHeaderResponse>(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<serde_json::Value> for BlockHeaderData {
+       type Error = ();
 
-/// Converts from `GetHeaderResponse` to `BlockHeaderData`.
-impl TryFrom<GetHeaderResponse> for BlockHeaderData {
-       type Error = bitcoin::hashes::hex::Error;
+       fn try_from(response: serde_json::Value) -> Result<Self, ()> {
+               macro_rules! get_field { ($name: expr, $ty_access: tt) => {
+                       response.get($name).ok_or(())?.$ty_access().ok_or(())?
+               } }
 
-       fn try_from(response: GetHeaderResponse) -> Result<Self, bitcoin::hashes::hex::Error> {
                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,
+                               version: get_field!("version", as_i64).try_into().map_err(|_| ())?,
+                               prev_blockhash: if let Some(hash_str) = response.get("previousblockhash") {
+                                               BlockHash::from_hex(hash_str.as_str().ok_or(())?).map_err(|_| ())?
+                                       } else { BlockHash::all_zeros() },
+                               merkle_root: TxMerkleNode::from_hex(get_field!("merkleroot", as_str)).map_err(|_| ())?,
+                               time: get_field!("time", as_u64).try_into().map_err(|_| ())?,
+                               bits: 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_uint256(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<Block> for JsonResponse {
        type Error = std::io::Error;
@@ -156,12 +138,103 @@ impl TryInto<(BlockHash, Option<u32>)> for JsonResponse {
        }
 }
 
+impl TryInto<Txid> for JsonResponse {
+       type Error = std::io::Error;
+       fn try_into(self) -> std::io::Result<Txid> {
+               match self.0.as_str() {
+                       None => Err(std::io::Error::new(
+                               std::io::ErrorKind::InvalidData,
+                               "expected JSON string",
+                       )),
+                       Some(hex_data) => match Vec::<u8>::from_hex(hex_data) {
+                               Err(_) => Err(std::io::Error::new(
+                                       std::io::ErrorKind::InvalidData,
+                                       "invalid hex data",
+                               )),
+                               Ok(txid_data) => match encode::deserialize(&txid_data) {
+                                       Err(_) => Err(std::io::Error::new(
+                                               std::io::ErrorKind::InvalidData,
+                                               "invalid txid",
+                                       )),
+                                       Ok(txid) => Ok(txid),
+                               },
+                       },
+               }
+       }
+}
+
+/// Converts a JSON value into a transaction. WATCH OUT! this cannot be used for zero-input transactions
+/// (e.g. createrawtransaction). See <https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/197>
+impl TryInto<Transaction> for JsonResponse {
+       type Error = std::io::Error;
+       fn try_into(self) -> std::io::Result<Transaction> {
+               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::<u8>::from_hex(hex_tx) {
+                       Err(_) => Err(std::io::Error::new(
+                               std::io::ErrorKind::InvalidData,
+                               "invalid hex data",
+                       )),
+                       Ok(tx_data) => match encode::deserialize(&tx_data) {
+                               Err(_) => Err(std::io::Error::new(
+                                       std::io::ErrorKind::InvalidData,
+                                       "invalid transaction",
+                               )),
+                               Ok(tx) => Ok(tx),
+                       },
+               }
+       }
+}
+
 #[cfg(test)]
 pub(crate) mod tests {
        use super::*;
        use bitcoin::blockdata::constants::genesis_block;
-       use bitcoin::consensus::encode;
+       use bitcoin::hashes::Hash;
+       use bitcoin::hashes::hex::ToHex;
        use bitcoin::network::constants::Network;
+       use serde_json::value::Number;
+       use serde_json::Value;
 
        /// Converts from `BlockHeaderData` into a `GetHeaderResponse` JSON value.
        impl From<BlockHeaderData> for serde_json::Value {
@@ -217,7 +290,7 @@ pub(crate) mod tests {
                match TryInto::<BlockHeaderData>::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"),
                }
@@ -469,4 +542,147 @@ pub(crate) mod tests {
                        },
                }
        }
+
+       #[test]
+       fn into_txid_from_json_response_with_unexpected_type() {
+               let response = JsonResponse(serde_json::json!({ "result": "foo" }));
+               match TryInto::<Txid>::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_txid_from_json_response_with_invalid_hex_data() {
+               let response = JsonResponse(serde_json::json!("foobar"));
+               match TryInto::<Txid>::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_txid_from_json_response_with_invalid_txid_data() {
+               let response = JsonResponse(serde_json::json!("abcd"));
+               match TryInto::<Txid>::try_into(response) {
+                       Err(e) => {
+                               assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
+                               assert_eq!(e.get_ref().unwrap().to_string(), "invalid txid");
+                       }
+                       Ok(_) => panic!("Expected error"),
+               }
+       }
+
+       #[test]
+       fn into_txid_from_json_response_with_valid_txid_data() {
+               let target_txid = Txid::from_slice(&[1; 32]).unwrap();
+               let response = JsonResponse(serde_json::json!(encode::serialize_hex(&target_txid)));
+               match TryInto::<Txid>::try_into(response) {
+                       Err(e) => panic!("Unexpected error: {:?}", e),
+                       Ok(txid) => assert_eq!(txid, target_txid),
+               }
+       }
+
+       // TryInto<Transaction> 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::<Transaction>::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::<Transaction>::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::<Transaction>::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::<Transaction>::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::<Transaction>::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::<Transaction>::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::<Transaction>::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"),
+               }
+       }
 }