use std::path::PathBuf;
use crate::rpc;
use anyhow::anyhow;
use parity_scale_codec::{Decode, Encode};
use sled::Db;
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, Hash},
OpaqueExtrinsic,
};
use tuxedo_core::{
dynamic_typing::UtxoData,
types::Transaction,
types::{Input, OpaqueBlock, OutputRef},
ConstraintChecker,
};
use jsonrpsee::http_client::HttpClient;
use runtime::{money::Coin, timestamp::Timestamp, Block, OuterVerifier};
const BLOCKS: &str = "blocks";
const BLOCK_HASHES: &str = "block_hashes";
const UNSPENT: &str = "unspent";
const SPENT: &str = "spent";
pub(crate) fn open_db(
db_path: PathBuf,
expected_genesis_hash: H256,
expected_genesis_block: OpaqueBlock,
) -> anyhow::Result<Db> {
let db = sled::open(db_path)?;
let wallet_block_hashes_tree = db.open_tree(BLOCK_HASHES)?;
let wallet_blocks_tree = db.open_tree("blocks")?;
if height(&db)?.is_some() {
let wallet_genesis_ivec = wallet_block_hashes_tree
.get(0.encode())?
.expect("We know there are some blocks, so there should be a 0th block.");
let wallet_genesis_hash = H256::decode(&mut &wallet_genesis_ivec[..])?;
log::debug!("Found existing database.");
if expected_genesis_hash != wallet_genesis_hash {
log::error!("Wallet's genesis does not match expected. Aborting database opening.");
return Err(anyhow!("Node reports a different genesis block than wallet. Wallet: {wallet_genesis_hash:?}. Expected: {expected_genesis_hash:?}. Aborting all operations"));
}
return Ok(db);
}
log::info!(
"Initializing fresh sync from genesis {:?}",
expected_genesis_hash
);
wallet_block_hashes_tree.insert(0u32.encode(), expected_genesis_hash.encode())?;
wallet_blocks_tree.insert(
expected_genesis_hash.encode(),
expected_genesis_block.encode(),
)?;
Ok(db)
}
pub(crate) async fn synchronize<F: Fn(&OuterVerifier) -> bool>(
parachain: bool,
db: &Db,
client: &HttpClient,
filter: &F,
) -> anyhow::Result<()> {
if parachain {
synchronize_helper::<F, crate::ParachainConstraintChecker>(db, client, filter).await
} else {
synchronize_helper::<F, crate::OuterConstraintChecker>(db, client, filter).await
}
}
pub(crate) async fn synchronize_helper<F: Fn(&OuterVerifier) -> bool, C: ConstraintChecker>(
db: &Db,
client: &HttpClient,
filter: &F,
) -> anyhow::Result<()> {
log::debug!("Synchronizing wallet with node.");
let mut height: u32 = height(db)?.ok_or(anyhow!("tried to sync an uninitialized database"))?;
let mut wallet_hash = get_block_hash(db, height)?
.expect("Local database should have a block hash at the height reported as best");
let mut node_hash: Option<H256> = rpc::node_get_block_hash(height, client).await?;
while Some(wallet_hash) != node_hash {
log::debug!("Divergence at height {height}. Node reports block: {node_hash:?}. Reverting wallet block: {wallet_hash:?}.");
unapply_highest_block::<C>(db).await?;
height -= 1;
wallet_hash = get_block_hash(db, height)?
.expect("Local database should have a block hash at the height reported as best");
node_hash = rpc::node_get_block_hash(height, client).await?;
}
log::debug!("Resyncing from common ancestor {node_hash:?} - {wallet_hash:?}");
height += 1;
node_hash = rpc::node_get_block_hash(height, client).await?;
while let Some(hash) = node_hash {
log::debug!("Forward syncing height {height}, hash {hash:?}");
let block = rpc::node_get_block(hash, client)
.await?
.expect("Node should be able to return a block whose hash it already returned");
apply_block::<F, C>(db, block, hash, filter).await?;
height += 1;
node_hash = rpc::node_get_block_hash(height, client).await?;
}
log::debug!("Done with forward sync up to {}", height - 1);
Ok(())
}
pub(crate) fn get_unspent(db: &Db, output_ref: &OutputRef) -> anyhow::Result<Option<(H256, u128)>> {
let wallet_unspent_tree = db.open_tree(UNSPENT)?;
let Some(ivec) = wallet_unspent_tree.get(output_ref.encode())? else {
return Ok(None);
};
Ok(Some(<(H256, u128)>::decode(&mut &ivec[..])?))
}
pub(crate) fn get_arbitrary_unspent_set(
db: &Db,
target: u128,
) -> anyhow::Result<Option<Vec<OutputRef>>> {
let wallet_unspent_tree = db.open_tree(UNSPENT)?;
let mut total = 0u128;
let mut keepers = Vec::new();
let mut unspent_iter = wallet_unspent_tree.iter();
while total < target {
let Some(pair) = unspent_iter.next() else {
return Ok(None);
};
let (output_ref_ivec, owner_amount_ivec) = pair?;
let output_ref = OutputRef::decode(&mut &output_ref_ivec[..])?;
let (_owner_pubkey, amount) = <(H256, u128)>::decode(&mut &owner_amount_ivec[..])?;
total += amount;
keepers.push(output_ref);
}
Ok(Some(keepers))
}
pub(crate) fn get_block_hash(db: &Db, height: u32) -> anyhow::Result<Option<H256>> {
let wallet_block_hashes_tree = db.open_tree(BLOCK_HASHES)?;
let Some(ivec) = wallet_block_hashes_tree.get(height.encode())? else {
return Ok(None);
};
let hash = H256::decode(&mut &ivec[..])?;
Ok(Some(hash))
}
#[allow(dead_code)]
pub(crate) fn get_block(db: &Db, hash: H256) -> anyhow::Result<Option<Block>> {
let wallet_blocks_tree = db.open_tree(BLOCKS)?;
let Some(ivec) = wallet_blocks_tree.get(hash.encode())? else {
return Ok(None);
};
let block = Block::decode(&mut &ivec[..])?;
Ok(Some(block))
}
pub(crate) async fn apply_block<F: Fn(&OuterVerifier) -> bool, C: ConstraintChecker>(
db: &Db,
b: OpaqueBlock,
block_hash: H256,
filter: &F,
) -> anyhow::Result<()> {
log::debug!("Applying Block {:?}, Block_Hash {:?}", b, block_hash);
let wallet_block_hashes_tree = db.open_tree(BLOCK_HASHES)?;
wallet_block_hashes_tree.insert(b.header.number.encode(), block_hash.encode())?;
let wallet_blocks_tree = db.open_tree(BLOCKS)?;
wallet_blocks_tree.insert(block_hash.encode(), b.encode())?;
for tx in b.extrinsics {
apply_transaction::<F, C>(db, tx, filter).await?;
}
Ok(())
}
async fn apply_transaction<F: Fn(&OuterVerifier) -> bool, C: ConstraintChecker>(
db: &Db,
opaque_tx: OpaqueExtrinsic,
filter: &F,
) -> anyhow::Result<()> {
let encoded_extrinsic = opaque_tx.encode();
let tx_hash = BlakeTwo256::hash_of(&encoded_extrinsic);
log::debug!("syncing transaction {tx_hash:?}");
let tx = Transaction::<OuterVerifier, C>::decode(&mut &encoded_extrinsic[..])?;
for (index, output) in tx.outputs.iter().enumerate() {
match output.payload.type_id {
Coin::<0>::TYPE_ID => {
if filter(&output.verifier) {
crate::money::apply_transaction(db, tx_hash, index as u32, output)?;
}
}
Timestamp::TYPE_ID => {
crate::timestamp::apply_transaction(db, output)?;
}
_ => continue,
}
}
log::debug!("about to spend all inputs");
for Input { output_ref, .. } in tx.inputs {
spend_output(db, &output_ref)?;
}
Ok(())
}
pub(crate) fn add_unspent_output(
db: &Db,
output_ref: &OutputRef,
owner_pubkey: &H256,
amount: &u128,
) -> anyhow::Result<()> {
let unspent_tree = db.open_tree(UNSPENT)?;
unspent_tree.insert(output_ref.encode(), (owner_pubkey, amount).encode())?;
Ok(())
}
fn remove_unspent_output(db: &Db, output_ref: &OutputRef) -> anyhow::Result<()> {
let unspent_tree = db.open_tree(UNSPENT)?;
unspent_tree.remove(output_ref.encode())?;
Ok(())
}
fn spend_output(db: &Db, output_ref: &OutputRef) -> anyhow::Result<()> {
let unspent_tree = db.open_tree(UNSPENT)?;
let spent_tree = db.open_tree(SPENT)?;
let Some(ivec) = unspent_tree.remove(output_ref.encode())? else {
return Ok(());
};
let (owner, amount) = <(H256, u128)>::decode(&mut &ivec[..])?;
spent_tree.insert(output_ref.encode(), (owner, amount).encode())?;
Ok(())
}
fn unspend_output(db: &Db, output_ref: &OutputRef) -> anyhow::Result<()> {
let unspent_tree = db.open_tree(UNSPENT)?;
let spent_tree = db.open_tree(SPENT)?;
let Some(ivec) = spent_tree.remove(output_ref.encode())? else {
return Ok(());
};
let (owner, amount) = <(H256, u128)>::decode(&mut &ivec[..])?;
unspent_tree.insert(output_ref.encode(), (owner, amount).encode())?;
Ok(())
}
fn unapply_transaction<C: ConstraintChecker>(db: &Db, tx: &OpaqueExtrinsic) -> anyhow::Result<()> {
let tx = Transaction::<OuterVerifier, C>::decode(&mut &tx.encode()[..])?;
for Input { output_ref, .. } in &tx.inputs {
unspend_output(db, output_ref)?;
}
let tx_hash = BlakeTwo256::hash_of(&tx.encode());
for i in 0..tx.outputs.len() {
let output_ref = OutputRef {
tx_hash,
index: i as u32,
};
remove_unspent_output(db, &output_ref)?;
}
Ok(())
}
pub(crate) async fn unapply_highest_block<C: ConstraintChecker>(db: &Db) -> anyhow::Result<()> {
let wallet_blocks_tree = db.open_tree(BLOCKS)?;
let wallet_block_hashes_tree = db.open_tree(BLOCK_HASHES)?;
let height = height(db)?.ok_or(anyhow!("Cannot unapply block from uninitialized database"))?;
let Some(ivec) = wallet_block_hashes_tree.remove(height.encode())? else {
return Err(anyhow!(
"No block hash found at height reported as best. DB is inconsistent."
));
};
let hash = H256::decode(&mut &ivec[..])?;
let Some(ivec) = wallet_blocks_tree.remove(hash.encode())? else {
return Err(anyhow!(
"Block was not present in db but block hash was. DB is corrupted."
));
};
let block = OpaqueBlock::decode(&mut &ivec[..])?;
for tx in block.extrinsics.iter().rev() {
unapply_transaction::<C>(db, tx)?;
}
Ok(())
}
pub(crate) fn height(db: &Db) -> anyhow::Result<Option<u32>> {
let wallet_block_hashes_tree = db.open_tree(BLOCK_HASHES)?;
let num_blocks = wallet_block_hashes_tree.len();
Ok(if num_blocks == 0 {
None
} else {
Some(num_blocks as u32 - 1)
})
}
#[allow(dead_code)]
pub(crate) fn print_block_hashes_tree(db: &Db) -> anyhow::Result<()> {
for height in 0..height(db)?.unwrap() {
let hash = get_block_hash(db, height)?;
println!("height: {height}, hash: {hash:?}");
}
Ok(())
}
pub(crate) fn print_unspent_tree(db: &Db) -> anyhow::Result<()> {
let wallet_unspent_tree = db.open_tree(UNSPENT)?;
for x in wallet_unspent_tree.iter() {
let (output_ref_ivec, owner_amount_ivec) = x?;
let output_ref = hex::encode(output_ref_ivec);
let (owner_pubkey, amount) = <(H256, u128)>::decode(&mut &owner_amount_ivec[..])?;
println!("{output_ref}: owner {owner_pubkey:?}, amount {amount}");
}
Ok(())
}
pub(crate) fn get_balances(db: &Db) -> anyhow::Result<impl Iterator<Item = (H256, u128)>> {
let mut balances = std::collections::HashMap::<H256, u128>::new();
let wallet_unspent_tree = db.open_tree(UNSPENT)?;
for raw_data in wallet_unspent_tree.iter() {
let (_output_ref_ivec, owner_amount_ivec) = raw_data?;
let (owner, amount) = <(H256, u128)>::decode(&mut &owner_amount_ivec[..])?;
balances
.entry(owner)
.and_modify(|old| *old += amount)
.or_insert(amount);
}
Ok(balances.into_iter())
}