bitcoind_client/
dummy.rs

1use async_trait::async_trait;
2use bitcoin::hashes::Hash;
3use bitcoin::secp256k1::{All, PublicKey, Secp256k1, SecretKey};
4use bitcoin::{Block, BlockHash, Network};
5use bitcoin::hash_types::FilterHeader;
6use bitcoin::key::Keypair;
7use log::info;
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12use txoo::filter::BlockSpendFilter;
13use txoo::source::Error as TxooSourceError;
14use txoo::source::{attestation_path, write_yaml_to_file, FileSource, Source};
15use txoo::util::sign_attestation;
16use txoo::{Attestation, OracleSetup, SignedAttestation};
17
18/// A dummy TXOO source that can be used for testing.
19/// Uses DUMMY_SECRET as the oracle secret key.
20///
21/// Note that the [`TxooFollower`] will automatically provide blocks to the source
22/// via the [`on_new_block`] method, except for the genesis block that must be provided
23/// by the caller.
24#[derive(Clone)]
25pub struct DummyTxooSource {
26    setup: OracleSetup,
27    secret_key: SecretKey,
28    attestations: Arc<Mutex<HashMap<BlockHash, SignedAttestation>>>,
29    secp: Secp256k1<All>,
30}
31
32/// A dummy oracle secret
33pub const DUMMY_SECRET: [u8; 32] = [0xcd; 32];
34
35impl DummyTxooSource {
36    /// Create a new source
37    pub fn new() -> Self {
38        let secp = Secp256k1::new();
39        let secret_key =
40            SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
41        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
42        Self {
43            setup: OracleSetup {
44                network: Network::Bitcoin,
45                start_block: 0,
46                public_key,
47            },
48            secret_key,
49            attestations: Arc::new(Mutex::new(HashMap::new())),
50            secp,
51        }
52    }
53}
54
55#[async_trait]
56impl Source for DummyTxooSource {
57    async fn get_unchecked(
58        &self,
59        block_height: u32,
60        block_hash: &BlockHash,
61    ) -> Result<SignedAttestation, TxooSourceError> {
62        let attestations = self.attestations.lock().unwrap();
63        attestations
64            .get(block_hash)
65            .cloned()
66            .map(|a| {
67                if a.attestation.block_height != block_height {
68                    panic!(
69                        "wrong height {} {}",
70                        a.attestation.block_height, block_height
71                    );
72                } else {
73                    a
74                }
75            })
76            .ok_or(TxooSourceError::NotExists)
77    }
78
79    async fn oracle_setup(&self) -> &OracleSetup {
80        &self.setup
81    }
82
83    fn secp(&self) -> &Secp256k1<All> {
84        &self.secp
85    }
86
87    async fn on_new_block(&self, block_height: u32, block: &Block) {
88        let mut attestations = self.attestations.lock().unwrap();
89        if attestations.len() != block_height as usize {
90            panic!(
91                "wrong height to DummyTxooSource::on_new_block stored {} called with {}",
92                attestations.len(),
93                block_height as usize
94            );
95        }
96        let prev_block_hash = block.header.prev_blockhash;
97        let filter_header = if !attestations.is_empty() {
98            let prev_attestation = attestations.get(&prev_block_hash).unwrap();
99            let prev_filter_header = prev_attestation.attestation.filter_header;
100            let filter = BlockSpendFilter::from_block(&block);
101            filter.filter_header(&prev_filter_header)
102        } else {
103            FilterHeader::all_zeros()
104        };
105        let attestation = Attestation {
106            block_hash: block.block_hash(),
107            block_height,
108            filter_header,
109            time: 0,
110        };
111        let keypair = Keypair::from_secret_key(&self.secp, &self.secret_key);
112        let signed_attestation = sign_attestation(attestation, &keypair, &self.secp);
113        attestations.insert(block.block_hash(), signed_attestation);
114    }
115}
116
117/// A dummy TXOO source that can be used for testing.
118/// Uses DUMMY_SECRET as the oracle secret key.
119///
120/// Note that the [`TxooFollower`] will automatically provide blocks to the source
121/// via the [`on_new_block`] method, except for the genesis block that must be provided
122/// by the caller.
123pub struct DummyPersistentTxooSource {
124    file_source: FileSource,
125    setup: OracleSetup,
126    secret_key: SecretKey,
127}
128
129impl DummyPersistentTxooSource {
130    /// Create a new source
131    pub fn new(
132        datadir: PathBuf,
133        network: Network,
134        start_block: u32,
135        block: &Block,
136        prev_filter_header: &FilterHeader,
137    ) -> Self {
138        let secp = Secp256k1::new();
139        let secret_key =
140            SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
141        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
142        let config = OracleSetup {
143            network,
144            start_block,
145            public_key,
146        };
147
148        fs::create_dir_all(datadir.join("public")).expect("create datadir/public");
149
150        write_yaml_to_file(&datadir, "public/config", &config);
151        let file_source = FileSource::new(datadir);
152        let signed_attestation = make_signed_attestation_from_block(
153            &secret_key,
154            start_block,
155            &block,
156            prev_filter_header,
157            &secp,
158        );
159        do_write_attestation(file_source.datadir(), &signed_attestation);
160
161        info!(
162            "dummy persistent source, start block {}, datadir {}",
163            start_block,
164            file_source.datadir().display()
165        );
166
167        Self {
168            file_source,
169            setup: OracleSetup {
170                network,
171                start_block,
172                public_key,
173            },
174            secret_key,
175        }
176    }
177
178    /// Create a new source at a checkpoint, where the filter_header is known
179    pub fn from_checkpoint(
180        datadir: PathBuf,
181        network: Network,
182        start_block: u32,
183        block_hash: BlockHash,
184        filter_header: FilterHeader,
185    ) -> Self {
186        let secp = Secp256k1::new();
187        let secret_key =
188            SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
189        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
190        let config = OracleSetup {
191            network,
192            start_block,
193            public_key,
194        };
195
196        fs::create_dir_all(datadir.join("public")).expect("create datadir/public");
197
198        write_yaml_to_file(&datadir, "public/config", &config);
199        let file_source = FileSource::new(datadir);
200        let signed_attestation =
201            make_signed_attestation(&secret_key, start_block, block_hash, filter_header, &secp);
202        do_write_attestation(file_source.datadir(), &signed_attestation);
203
204        info!(
205            "dummy persistent source, start block {}, datadir {}",
206            start_block,
207            file_source.datadir().display()
208        );
209
210        Self {
211            file_source,
212            setup: OracleSetup {
213                network,
214                start_block,
215                public_key,
216            },
217            secret_key,
218        }
219    }
220}
221
222fn make_signed_attestation_from_block(
223    secret_key: &SecretKey,
224    block_height: u32,
225    block: &Block,
226    prev_filter_header: &FilterHeader,
227    secp: &Secp256k1<All>,
228) -> SignedAttestation {
229    let filter = BlockSpendFilter::from_block(&block);
230    let filter_header = filter.filter_header(&prev_filter_header);
231    let block_hash = block.block_hash();
232    make_signed_attestation(secret_key, block_height, block_hash, filter_header, secp)
233}
234
235fn make_signed_attestation(
236    secret_key: &SecretKey,
237    block_height: u32,
238    block_hash: BlockHash,
239    filter_header: FilterHeader,
240    secp: &Secp256k1<All>,
241) -> SignedAttestation {
242    let attestation = Attestation {
243        block_hash,
244        block_height,
245        filter_header,
246        time: 0,
247    };
248    let keypair = Keypair::from_secret_key(&secp, secret_key);
249    sign_attestation(attestation, &keypair, &secp)
250}
251
252fn do_write_attestation(datadir: &PathBuf, signed_attestation: &SignedAttestation) {
253    let attestation = &signed_attestation.attestation;
254    write_yaml_to_file(
255        datadir,
256        &attestation_path(attestation.block_height, &attestation.block_hash),
257        &signed_attestation,
258    )
259}
260
261#[async_trait]
262impl Source for DummyPersistentTxooSource {
263    async fn get_unchecked(
264        &self,
265        block_height: u32,
266        block_hash: &BlockHash,
267    ) -> Result<SignedAttestation, TxooSourceError> {
268        self.file_source
269            .get_unchecked(block_height, block_hash)
270            .await
271    }
272
273    async fn oracle_setup(&self) -> &OracleSetup {
274        &self.setup
275    }
276
277    fn secp(&self) -> &Secp256k1<All> {
278        self.file_source.secp()
279    }
280
281    async fn on_new_block(&self, block_height: u32, block: &Block) {
282        info!("new block {}-{}", block_height, block.block_hash());
283        let prev_block_hash = block.header.prev_blockhash;
284        let prev_attestation = self
285            .file_source
286            .get_unchecked(block_height - 1, &prev_block_hash)
287            .await
288            .unwrap_or_else(|e| {
289                panic!(
290                    "could not get attestation for prev {}-{}: {:?}",
291                    block_height - 1,
292                    prev_block_hash,
293                    e
294                )
295            });
296        let prev_filter_header = prev_attestation.attestation.filter_header;
297        let signed_attestation = make_signed_attestation_from_block(
298            &self.secret_key,
299            block_height,
300            block,
301            &prev_filter_header,
302            self.secp(),
303        );
304        do_write_attestation(&self.file_source.datadir(), &signed_attestation);
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use txoo::source::Error;
312    use bitcoin::block::Version;
313    use bitcoin::blockdata::constants::genesis_block;
314    use bitcoin::blockdata::block::Header as BlockHeader;
315    use bitcoin::hash_types::TxMerkleNode;
316    use bitcoin::CompactTarget;
317
318    #[tokio::test]
319    async fn dummy_source_test() {
320        let tmpdir = tempfile::tempdir().unwrap();
321        let network = Network::Regtest;
322        let block = genesis_block(network);
323        let source = DummyPersistentTxooSource::new(
324            tmpdir.path().to_path_buf(),
325            network,
326            0,
327            &block,
328            &FilterHeader::all_zeros(),
329        );
330        let attestation = source
331            .get_unchecked(0, &block.block_hash())
332            .await
333            .expect("attestation exists");
334        assert_eq!(attestation.attestation.block_height, 0);
335        assert_eq!(attestation.attestation.block_hash, block.block_hash());
336
337        let block1 = Block {
338            header: BlockHeader {
339                version: Version::from_consensus(0),
340                prev_blockhash: block.block_hash(),
341                merkle_root: TxMerkleNode::all_zeros(),
342                time: 0,
343                bits: CompactTarget::from_consensus(0),
344                nonce: 0,
345            },
346            txdata: vec![],
347        };
348        source.on_new_block(1, &block1).await;
349        let attestation = source
350            .get_unchecked(1, &block1.block_hash())
351            .await
352            .expect("attestation exists");
353        assert_eq!(attestation.attestation.block_height, 1);
354
355        source.get(1, &block1).await.expect("get 1");
356    }
357
358    #[tokio::test]
359    async fn dummy_source_genesis_test() {
360        let tmpdir = tempfile::tempdir().unwrap();
361        let network = Network::Regtest;
362        let block0 = genesis_block(network);
363        let block1 = Block {
364            header: BlockHeader {
365                version: Version::ONE,
366                prev_blockhash: block0.block_hash(),
367                merkle_root: TxMerkleNode::all_zeros(),
368                time: 0,
369                bits: CompactTarget::from_consensus(0),
370                nonce: 0,
371            },
372            txdata: vec![],
373        };
374        // create source from block height 1, genesis block attestation wasn't created
375        let source = DummyPersistentTxooSource::new(
376            tmpdir.path().to_path_buf(),
377            network,
378            1,
379            &block1,
380            &FilterHeader::all_zeros(),
381        );
382
383        let genesis_result = source.get_unchecked(0, &block0.block_hash()).await;
384        assert!(genesis_result.is_err());
385        assert_eq!(genesis_result.err().unwrap(), Error::NotExists);
386
387        let (_, prev_filter_header) = source.get(1, &block1).await.expect("get 1");
388        assert_eq!(prev_filter_header, FilterHeader::all_zeros());
389    }
390}