use async_trait::async_trait;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{All, PublicKey, Secp256k1, SecretKey};
use bitcoin::{Block, BlockHash, Network};
use bitcoin::hash_types::FilterHeader;
use bitcoin::key::KeyPair;
use log::info;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use txoo::filter::BlockSpendFilter;
use txoo::source::Error as TxooSourceError;
use txoo::source::{attestation_path, write_yaml_to_file, FileSource, Source};
use txoo::util::sign_attestation;
use txoo::{Attestation, OracleSetup, SignedAttestation};
#[derive(Clone)]
pub struct DummyTxooSource {
setup: OracleSetup,
secret_key: SecretKey,
attestations: Arc<Mutex<HashMap<BlockHash, SignedAttestation>>>,
secp: Secp256k1<All>,
}
pub const DUMMY_SECRET: [u8; 32] = [0xcd; 32];
impl DummyTxooSource {
pub fn new() -> Self {
let secp = Secp256k1::new();
let secret_key =
SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
Self {
setup: OracleSetup {
network: Network::Bitcoin,
start_block: 0,
public_key,
},
secret_key,
attestations: Arc::new(Mutex::new(HashMap::new())),
secp,
}
}
}
#[async_trait]
impl Source for DummyTxooSource {
async fn get_unchecked(
&self,
block_height: u32,
block_hash: &BlockHash,
) -> Result<SignedAttestation, TxooSourceError> {
let attestations = self.attestations.lock().unwrap();
attestations
.get(block_hash)
.cloned()
.map(|a| {
if a.attestation.block_height != block_height {
panic!(
"wrong height {} {}",
a.attestation.block_height, block_height
);
} else {
a
}
})
.ok_or(TxooSourceError::NotExists)
}
async fn oracle_setup(&self) -> &OracleSetup {
&self.setup
}
fn secp(&self) -> &Secp256k1<All> {
&self.secp
}
async fn on_new_block(&self, block_height: u32, block: &Block) {
let mut attestations = self.attestations.lock().unwrap();
if attestations.len() != block_height as usize {
panic!(
"wrong height to DummyTxooSource::on_new_block stored {} called with {}",
attestations.len(),
block_height as usize
);
}
let prev_block_hash = block.header.prev_blockhash;
let filter_header = if !attestations.is_empty() {
let prev_attestation = attestations.get(&prev_block_hash).unwrap();
let prev_filter_header = prev_attestation.attestation.filter_header;
let filter = BlockSpendFilter::from_block(&block);
filter.filter_header(&prev_filter_header)
} else {
FilterHeader::all_zeros()
};
let attestation = Attestation {
block_hash: block.block_hash(),
block_height,
filter_header,
time: 0,
};
let keypair = KeyPair::from_secret_key(&self.secp, &self.secret_key);
let signed_attestation = sign_attestation(attestation, &keypair, &self.secp);
attestations.insert(block.block_hash(), signed_attestation);
}
}
pub struct DummyPersistentTxooSource {
file_source: FileSource,
setup: OracleSetup,
secret_key: SecretKey,
}
impl DummyPersistentTxooSource {
pub fn new(
datadir: PathBuf,
network: Network,
start_block: u32,
block: &Block,
prev_filter_header: &FilterHeader,
) -> Self {
let secp = Secp256k1::new();
let secret_key =
SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let config = OracleSetup {
network,
start_block,
public_key,
};
fs::create_dir_all(datadir.join("public")).expect("create datadir/public");
write_yaml_to_file(&datadir, "public/config", &config);
let file_source = FileSource::new(datadir);
let signed_attestation = make_signed_attestation_from_block(
&secret_key,
start_block,
&block,
prev_filter_header,
&secp,
);
do_write_attestation(file_source.datadir(), &signed_attestation);
info!(
"dummy persistent source, start block {}, datadir {}",
start_block,
file_source.datadir().display()
);
Self {
file_source,
setup: OracleSetup {
network,
start_block,
public_key,
},
secret_key,
}
}
pub fn from_checkpoint(
datadir: PathBuf,
network: Network,
start_block: u32,
block_hash: BlockHash,
filter_header: FilterHeader,
) -> Self {
let secp = Secp256k1::new();
let secret_key =
SecretKey::from_slice(&DUMMY_SECRET).expect("32 bytes, within curve order");
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let config = OracleSetup {
network,
start_block,
public_key,
};
fs::create_dir_all(datadir.join("public")).expect("create datadir/public");
write_yaml_to_file(&datadir, "public/config", &config);
let file_source = FileSource::new(datadir);
let signed_attestation =
make_signed_attestation(&secret_key, start_block, block_hash, filter_header, &secp);
do_write_attestation(file_source.datadir(), &signed_attestation);
info!(
"dummy persistent source, start block {}, datadir {}",
start_block,
file_source.datadir().display()
);
Self {
file_source,
setup: OracleSetup {
network,
start_block,
public_key,
},
secret_key,
}
}
}
fn make_signed_attestation_from_block(
secret_key: &SecretKey,
block_height: u32,
block: &Block,
prev_filter_header: &FilterHeader,
secp: &Secp256k1<All>,
) -> SignedAttestation {
let filter = BlockSpendFilter::from_block(&block);
let filter_header = filter.filter_header(&prev_filter_header);
let block_hash = block.block_hash();
make_signed_attestation(secret_key, block_height, block_hash, filter_header, secp)
}
fn make_signed_attestation(
secret_key: &SecretKey,
block_height: u32,
block_hash: BlockHash,
filter_header: FilterHeader,
secp: &Secp256k1<All>,
) -> SignedAttestation {
let attestation = Attestation {
block_hash,
block_height,
filter_header,
time: 0,
};
let keypair = KeyPair::from_secret_key(&secp, secret_key);
sign_attestation(attestation, &keypair, &secp)
}
fn do_write_attestation(datadir: &PathBuf, signed_attestation: &SignedAttestation) {
let attestation = &signed_attestation.attestation;
write_yaml_to_file(
datadir,
&attestation_path(attestation.block_height, &attestation.block_hash),
&signed_attestation,
)
}
#[async_trait]
impl Source for DummyPersistentTxooSource {
async fn get_unchecked(
&self,
block_height: u32,
block_hash: &BlockHash,
) -> Result<SignedAttestation, TxooSourceError> {
self.file_source
.get_unchecked(block_height, block_hash)
.await
}
async fn oracle_setup(&self) -> &OracleSetup {
&self.setup
}
fn secp(&self) -> &Secp256k1<All> {
self.file_source.secp()
}
async fn on_new_block(&self, block_height: u32, block: &Block) {
info!("new block {}-{}", block_height, block.block_hash());
let prev_block_hash = block.header.prev_blockhash;
let prev_attestation = self
.file_source
.get_unchecked(block_height - 1, &prev_block_hash)
.await
.unwrap_or_else(|e| {
panic!(
"could not get attestation for prev {}-{}: {:?}",
block_height - 1,
prev_block_hash,
e
)
});
let prev_filter_header = prev_attestation.attestation.filter_header;
let signed_attestation = make_signed_attestation_from_block(
&self.secret_key,
block_height,
block,
&prev_filter_header,
self.secp(),
);
do_write_attestation(&self.file_source.datadir(), &signed_attestation);
}
}
#[cfg(test)]
mod tests {
use super::*;
use txoo::source::Error;
use bitcoin::block::Version;
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::blockdata::block::Header as BlockHeader;
use bitcoin::hash_types::TxMerkleNode;
use bitcoin::CompactTarget;
#[tokio::test]
async fn dummy_source_test() {
let tmpdir = tempfile::tempdir().unwrap();
let network = Network::Regtest;
let block = genesis_block(network);
let source = DummyPersistentTxooSource::new(
tmpdir.path().to_path_buf(),
network,
0,
&block,
&FilterHeader::all_zeros(),
);
let attestation = source
.get_unchecked(0, &block.block_hash())
.await
.expect("attestation exists");
assert_eq!(attestation.attestation.block_height, 0);
assert_eq!(attestation.attestation.block_hash, block.block_hash());
let block1 = Block {
header: BlockHeader {
version: Version::from_consensus(0),
prev_blockhash: block.block_hash(),
merkle_root: TxMerkleNode::all_zeros(),
time: 0,
bits: CompactTarget::from_consensus(0),
nonce: 0,
},
txdata: vec![],
};
source.on_new_block(1, &block1).await;
let attestation = source
.get_unchecked(1, &block1.block_hash())
.await
.expect("attestation exists");
assert_eq!(attestation.attestation.block_height, 1);
source.get(1, &block1).await.expect("get 1");
}
#[tokio::test]
async fn dummy_source_genesis_test() {
let tmpdir = tempfile::tempdir().unwrap();
let network = Network::Regtest;
let block0 = genesis_block(network);
let block1 = Block {
header: BlockHeader {
version: Version::ONE,
prev_blockhash: block0.block_hash(),
merkle_root: TxMerkleNode::all_zeros(),
time: 0,
bits: CompactTarget::from_consensus(0),
nonce: 0,
},
txdata: vec![],
};
let source = DummyPersistentTxooSource::new(
tmpdir.path().to_path_buf(),
network,
1,
&block1,
&FilterHeader::all_zeros(),
);
let genesis_result = source.get_unchecked(0, &block0.block_hash()).await;
assert!(genesis_result.is_err());
assert_eq!(genesis_result.err().unwrap(), Error::NotExists);
let (_, prev_filter_header) = source.get(1, &block1).await.expect("get 1");
assert_eq!(prev_filter_header, FilterHeader::all_zeros());
}
}