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#[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
32pub const DUMMY_SECRET: [u8; 32] = [0xcd; 32];
34
35impl DummyTxooSource {
36 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
117pub struct DummyPersistentTxooSource {
124 file_source: FileSource,
125 setup: OracleSetup,
126 secret_key: SecretKey,
127}
128
129impl DummyPersistentTxooSource {
130 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 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 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}