use crate::BlockSource;
use async_trait::async_trait;
use bitcoin::{Block, BlockHash, OutPoint, Txid};
use bitcoin::blockdata::block::Header as BlockHeader;
use txoo::spv::SpvProof;
#[derive(Debug)]
pub enum Error {
SourceError(String),
}
impl From<crate::Error> for Error {
fn from(e: crate::Error) -> Error {
Error::SourceError(e.to_string())
}
}
#[derive(PartialEq)]
pub enum FollowAction {
None,
BlockAdded(Block),
BlockReorged(Block, BlockHeader),
}
pub struct SourceFollower {
source: Box<dyn BlockSource>,
}
impl SourceFollower {
pub fn new(source: Box<dyn BlockSource>) -> Self {
SourceFollower { source }
}
}
impl SourceFollower {
pub async fn follow(
&self,
current_height: u32,
current_hash: BlockHash,
) -> Result<FollowAction, Error> {
match self.source.get_block_hash(current_height + 1).await? {
None => {
match self.source.get_block_hash(current_height).await? {
None => {
let current_block = self.source.get_block(¤t_hash).await?;
let prev_block_header = self
.source
.get_header(¤t_block.header.prev_blockhash)
.await?
.header;
Ok(FollowAction::BlockReorged(current_block, prev_block_header))
}
Some(check_hash) => {
if check_hash == current_hash {
Ok(FollowAction::None)
} else {
let current_block = self.source.get_block(¤t_hash).await?;
let prev_block_header = self
.source
.get_header(¤t_block.header.prev_blockhash)
.await?
.header;
Ok(FollowAction::BlockReorged(current_block, prev_block_header))
}
}
}
}
Some(new_hash) => {
let block = self.source.get_block(&new_hash).await?;
if block.header.prev_blockhash == current_hash {
Ok(FollowAction::BlockAdded(block))
} else {
let current_block = self.source.get_block(¤t_hash).await?;
let prev_block_header = self
.source
.get_header(¤t_block.header.prev_blockhash)
.await?
.header;
Ok(FollowAction::BlockReorged(current_block, prev_block_header))
}
}
}
}
}
pub enum FollowWithProofAction {
None,
BlockAdded(Block, SpvProof),
BlockReorged(Block, SpvProof, BlockHeader),
}
#[async_trait]
pub trait Tracker {
async fn forward_watches(&self) -> (Vec<Txid>, Vec<OutPoint>);
async fn reverse_watches(&self) -> (Vec<Txid>, Vec<OutPoint>);
}
pub struct SourceWithProofFollower(SourceFollower);
impl SourceWithProofFollower {
pub fn new(source: Box<dyn BlockSource>) -> Self {
SourceWithProofFollower(SourceFollower::new(source))
}
pub async fn follow_with_proof(
&self,
current_height: u32,
current_hash: BlockHash,
tracker: &impl Tracker,
) -> Result<FollowWithProofAction, Error> {
match self.0.follow(current_height, current_hash).await? {
FollowAction::None => Ok(FollowWithProofAction::None),
FollowAction::BlockAdded(block) => {
let (txids, outpoints) = tracker.forward_watches().await;
let proof = SpvProof::build(&block, &txids, &outpoints).0;
Ok(FollowWithProofAction::BlockAdded(block, proof))
}
FollowAction::BlockReorged(block, prev_block_header) => {
let (txids, outpoints) = tracker.reverse_watches().await;
let proof = SpvProof::build(&block, &txids, &outpoints).0;
Ok(FollowWithProofAction::BlockReorged(
block,
proof,
prev_block_header,
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{DummyBlockSource, DummyTracker};
use crate::BlockSource;
use bitcoin::BlockHash;
#[tokio::test]
async fn dummy_test() {
let mut source = DummyBlockSource::new();
source.add();
source.add();
source.add();
let tip = source.get_best_block().await.unwrap();
assert_eq!(tip.1, 3);
let tip_block = source.get_block(&tip.0).await.unwrap();
source.remove();
let prev_tip = source.get_best_block().await.unwrap();
assert_eq!(prev_tip.1, 2);
assert_eq!(tip_block.header.prev_blockhash, prev_tip.0);
}
#[tokio::test]
async fn follow_test() {
let mut source = DummyBlockSource::new();
let follower = SourceFollower::new(Box::new(source.clone()));
let genesis_hash = source.genesis_hash();
assert!(follower.follow(0, genesis_hash).await.unwrap() == FollowAction::None);
source.add();
let hash1 = assert_add(follower.follow(0, genesis_hash).await.unwrap());
source.add();
let hash2 = assert_add(follower.follow(1, hash1).await.unwrap());
assert!(follower.follow(2, hash2).await.unwrap() == FollowAction::None);
source.remove();
source.remove();
let action = follower.follow(2, hash2).await.unwrap();
if let FollowAction::BlockReorged(block, _prev_block_header) = action {
assert_eq!(block.block_hash(), hash2);
} else {
panic!("expected reorg");
}
let action = follower.follow(1, hash1).await.unwrap();
if let FollowAction::BlockReorged(block, _prev_block_header) = action {
assert_eq!(block.block_hash(), hash1);
} else {
panic!("expected reorg");
}
source.add();
let hash1a = assert_add(follower.follow(0, genesis_hash).await.unwrap());
assert_eq!(hash1a, hash1);
assert!(follower.follow(1, hash1).await.unwrap() == FollowAction::None);
}
#[tokio::test]
async fn follow_with_proof_test() {
let tracker = DummyTracker();
let mut source = DummyBlockSource::new();
let follower = SourceWithProofFollower::new(Box::new(source.clone()));
let genesis_hash = source.genesis_hash();
if let FollowWithProofAction::None = follower
.follow_with_proof(0, genesis_hash, &tracker)
.await
.unwrap()
{
} else {
panic!("expected None");
}
source.add();
let hash1 = match follower
.follow_with_proof(0, genesis_hash, &tracker)
.await
.unwrap()
{
FollowWithProofAction::BlockAdded(block, proof) => {
assert!(proof.proof.is_none());
block.block_hash()
}
_ => panic!("expected block added with proof"),
};
if let FollowWithProofAction::None = follower
.follow_with_proof(1, hash1, &tracker)
.await
.unwrap()
{
} else {
panic!("expected None");
}
source.remove();
let action = follower
.follow_with_proof(1, hash1, &tracker)
.await
.unwrap();
if let FollowWithProofAction::BlockReorged(block, proof, _prev_block_header) = action {
assert!(proof.proof.is_none());
assert_eq!(block.block_hash(), hash1);
} else {
panic!("expected reorg");
}
}
fn assert_add(action: FollowAction) -> BlockHash {
if let FollowAction::BlockAdded(block) = action {
block.block_hash()
} else {
panic!("wrong follow action");
}
}
}