bwk_electrum/electrum/
types.rs

1// from https://github.com/romanz/electrs/blob/master/src/types.rs
2use miniscript::bitcoin::{
3    blockdata::block::Header as BlockHeader,
4    consensus::encode::{deserialize, Decodable, Encodable},
5    hashes::{hash_newtype, sha256, Hash},
6    OutPoint, Script, Txid,
7};
8use miniscript::serde::{Deserialize, Serialize};
9use std::convert::TryFrom;
10
11macro_rules! impl_consensus_encoding {
12    ($thing:ident, $($field:ident),+) => (
13        impl Encodable for $thing {
14            #[inline]
15            fn consensus_encode<S: miniscript::bitcoin::io::Write + ?Sized>(
16                &self,
17                s: &mut S,
18            ) -> Result<usize, miniscript::bitcoin::io::Error> {
19                let mut len = 0;
20                $(len += self.$field.consensus_encode(s)?;)+
21                Ok(len)
22            }
23        }
24
25        impl Decodable for $thing {
26            #[inline]
27            fn consensus_decode<D: miniscript::bitcoin::io::Read + ?Sized>(
28                d: &mut D,
29            ) -> Result<$thing, miniscript::bitcoin::consensus::encode::Error> {
30                Ok($thing {
31                    $($field: Decodable::consensus_decode(d)?),+
32                })
33            }
34        }
35    );
36}
37
38pub const HASH_PREFIX_LEN: usize = 8;
39const HEIGHT_SIZE: usize = 4;
40
41pub(crate) type HashPrefix = [u8; HASH_PREFIX_LEN];
42pub(crate) type SerializedHashPrefixRow = [u8; HASH_PREFIX_ROW_SIZE];
43type Height = u32;
44pub(crate) type SerBlock = Vec<u8>;
45
46#[derive(Debug, Serialize, Deserialize, PartialEq)]
47pub(crate) struct HashPrefixRow {
48    prefix: HashPrefix,
49    height: Height, // transaction confirmed height
50}
51
52pub const HASH_PREFIX_ROW_SIZE: usize = HASH_PREFIX_LEN + HEIGHT_SIZE;
53
54impl HashPrefixRow {
55    pub(crate) fn to_db_row(&self) -> SerializedHashPrefixRow {
56        let mut row = [0; HASH_PREFIX_ROW_SIZE];
57        let len = self
58            .consensus_encode(&mut (&mut row as &mut [u8]))
59            .expect("in-memory writers don't error");
60        debug_assert_eq!(len, HASH_PREFIX_ROW_SIZE);
61        row
62    }
63
64    pub(crate) fn from_db_row(row: SerializedHashPrefixRow) -> Self {
65        deserialize(&row).expect("bad HashPrefixRow")
66    }
67
68    pub fn height(&self) -> usize {
69        usize::try_from(self.height).expect("invalid height")
70    }
71}
72
73impl_consensus_encoding!(HashPrefixRow, prefix, height);
74
75hash_newtype! {
76    /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#script-hashes
77    #[hash_newtype(backward)]
78    pub struct ScriptHash(sha256::Hash);
79}
80
81impl ScriptHash {
82    pub fn new(script: &Script) -> Self {
83        ScriptHash::hash(script.as_bytes())
84    }
85
86    fn prefix(&self) -> HashPrefix {
87        let mut prefix = HashPrefix::default();
88        prefix.copy_from_slice(&self.0[..HASH_PREFIX_LEN]);
89        prefix
90    }
91}
92
93pub(crate) struct ScriptHashRow;
94
95impl ScriptHashRow {
96    pub(crate) fn scan_prefix(scripthash: ScriptHash) -> HashPrefix {
97        scripthash.0[..HASH_PREFIX_LEN].try_into().unwrap()
98    }
99
100    pub(crate) fn row(scripthash: ScriptHash, height: usize) -> HashPrefixRow {
101        HashPrefixRow {
102            prefix: scripthash.prefix(),
103            height: Height::try_from(height).expect("invalid height"),
104        }
105    }
106}
107
108// ***************************************************************************
109
110hash_newtype! {
111    /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status
112    pub struct StatusHash(sha256::Hash);
113}
114
115// ***************************************************************************
116
117fn spending_prefix(prev: OutPoint) -> HashPrefix {
118    let txid_prefix = HashPrefix::try_from(&prev.txid[..HASH_PREFIX_LEN]).unwrap();
119    let value = u64::from_be_bytes(txid_prefix);
120    let value = value.wrapping_add(prev.vout.into());
121    value.to_be_bytes()
122}
123
124pub(crate) struct SpendingPrefixRow;
125
126impl SpendingPrefixRow {
127    pub(crate) fn scan_prefix(outpoint: OutPoint) -> HashPrefix {
128        spending_prefix(outpoint)
129    }
130
131    pub(crate) fn row(outpoint: OutPoint, height: usize) -> HashPrefixRow {
132        HashPrefixRow {
133            prefix: spending_prefix(outpoint),
134            height: Height::try_from(height).expect("invalid height"),
135        }
136    }
137}
138
139// ***************************************************************************
140
141fn txid_prefix(txid: &Txid) -> HashPrefix {
142    let mut prefix = [0u8; HASH_PREFIX_LEN];
143    prefix.copy_from_slice(&txid[..HASH_PREFIX_LEN]);
144    prefix
145}
146
147pub(crate) struct TxidRow;
148
149impl TxidRow {
150    pub(crate) fn scan_prefix(txid: Txid) -> HashPrefix {
151        txid_prefix(&txid)
152    }
153
154    pub(crate) fn row(txid: Txid, height: usize) -> HashPrefixRow {
155        HashPrefixRow {
156            prefix: txid_prefix(&txid),
157            height: Height::try_from(height).expect("invalid height"),
158        }
159    }
160}
161
162// ***************************************************************************
163
164pub(crate) type SerializedHeaderRow = [u8; HEADER_ROW_SIZE];
165
166#[derive(Debug, Serialize, Deserialize)]
167pub(crate) struct HeaderRow {
168    pub(crate) header: BlockHeader,
169}
170
171pub const HEADER_ROW_SIZE: usize = 80;
172
173impl_consensus_encoding!(HeaderRow, header);
174
175impl HeaderRow {
176    pub(crate) fn new(header: BlockHeader) -> Self {
177        Self { header }
178    }
179
180    pub(crate) fn to_db_row(&self) -> SerializedHeaderRow {
181        let mut row = [0; HEADER_ROW_SIZE];
182        let len = self
183            .consensus_encode(&mut (&mut row as &mut [u8]))
184            .expect("in-memory writers don't error");
185        debug_assert_eq!(len, HEADER_ROW_SIZE);
186        row
187    }
188
189    pub(crate) fn from_db_row(row: SerializedHeaderRow) -> Self {
190        deserialize(&row).expect("bad HeaderRow")
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use crate::electrum::types::{
197        spending_prefix, HashPrefixRow, ScriptHash, ScriptHashRow, TxidRow,
198    };
199    use hex_lit::hex;
200    use miniscript::bitcoin::{Address, OutPoint, Txid};
201    use serde_json::{from_str, json};
202
203    use std::str::FromStr;
204
205    #[test]
206    fn test_scripthash_serde() {
207        let hex = "\"4b3d912c1523ece4615e91bf0d27381ca72169dbf6b1c2ffcc9f92381d4984a3\"";
208        let scripthash: ScriptHash = from_str(hex).unwrap();
209        assert_eq!(format!("\"{}\"", scripthash), hex);
210        assert_eq!(json!(scripthash).to_string(), hex);
211    }
212
213    #[test]
214    fn test_scripthash_row() {
215        let hex = "\"4b3d912c1523ece4615e91bf0d27381ca72169dbf6b1c2ffcc9f92381d4984a3\"";
216        let scripthash: ScriptHash = from_str(hex).unwrap();
217        let row1 = ScriptHashRow::row(scripthash, 123456);
218        let db_row = row1.to_db_row();
219        assert_eq!(db_row, hex!("a384491d38929fcc40e20100"));
220        let row2 = HashPrefixRow::from_db_row(db_row);
221        assert_eq!(row1, row2);
222    }
223
224    #[test]
225    fn test_scripthash() {
226        let addr = Address::from_str("1KVNjD3AAnQ3gTMqoTKcWFeqSFujq9gTBT")
227            .unwrap()
228            .assume_checked();
229        let scripthash = ScriptHash::new(&addr.script_pubkey());
230        assert_eq!(
231            scripthash,
232            "00dfb264221d07712a144bda338e89237d1abd2db4086057573895ea2659766a"
233                .parse()
234                .unwrap()
235        );
236    }
237
238    #[test]
239    fn test_txid1_prefix() {
240        // duplicate txids from BIP-30
241        let hex = "d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599";
242        let txid = Txid::from_str(hex).unwrap();
243
244        let row1 = TxidRow::row(txid, 91812);
245        let row2 = TxidRow::row(txid, 91842);
246
247        assert_eq!(row1.to_db_row(), hex!("9985d82954e10f22a4660100"));
248        assert_eq!(row2.to_db_row(), hex!("9985d82954e10f22c2660100"));
249    }
250
251    #[test]
252    fn test_txid2_prefix() {
253        // duplicate txids from BIP-30
254        let hex = "e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468";
255        let txid = Txid::from_str(hex).unwrap();
256
257        let row1 = TxidRow::row(txid, 91722);
258        let row2 = TxidRow::row(txid, 91880);
259
260        // low-endian encoding => rows should be sorted according to block height
261        assert_eq!(row1.to_db_row(), hex!("68b45f58b674e94e4a660100"));
262        assert_eq!(row2.to_db_row(), hex!("68b45f58b674e94ee8660100"));
263    }
264
265    #[test]
266    fn test_spending_prefix() {
267        let txid = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
268            .parse()
269            .unwrap();
270
271        assert_eq!(
272            spending_prefix(OutPoint { txid, vout: 0 }),
273            [31, 30, 29, 28, 27, 26, 25, 24]
274        );
275        assert_eq!(
276            spending_prefix(OutPoint { txid, vout: 10 }),
277            [31, 30, 29, 28, 27, 26, 25, 34]
278        );
279        assert_eq!(
280            spending_prefix(OutPoint { txid, vout: 255 }),
281            [31, 30, 29, 28, 27, 26, 26, 23]
282        );
283        assert_eq!(
284            spending_prefix(OutPoint { txid, vout: 256 }),
285            [31, 30, 29, 28, 27, 26, 26, 24]
286        );
287    }
288}