mod rpc;
use async_trait::async_trait;
use paralock_core::protocol::{self, NetworkUpgrade, UpgradeCheck};
pub use rpc::{BlockchainInfo, Consensus, UpgradeInfo, ZcashRpc, ZcashRpcOptions};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressUtxo {
pub address: String,
pub txid: String,
pub vout: u32,
pub value: u64,
pub script_pubkey: String,
pub height: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct NodeInput {
pub txid: Option<String>,
pub vout: Option<u32>,
pub coinbase: Option<String>,
pub script_sig: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeTransaction {
pub txid: String,
pub hex: String,
pub height: Option<u32>,
pub confirmations: u32,
pub vin: Vec<NodeInput>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeTxOut {
pub confirmations: u32,
pub value: u64,
pub script_pubkey: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpgradeSchedule {
pub tip: u32,
pub upgrades: Vec<NetworkUpgrade>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum NodeError {
#[error("{method}: {message}")]
Transport {
method: String,
message: String,
},
#[error("{method}: {message} ({code})")]
Rpc {
method: String,
code: i64,
message: String,
},
#[error("{method}: invalid response: {message}")]
InvalidResponse {
method: String,
message: String,
},
#[error("{0} is not supported by this Zcash backend")]
Unsupported(&'static str),
}
impl NodeError {
pub fn is_rejection(&self) -> bool {
matches!(self, Self::Rpc { .. })
}
pub fn already_known(&self) -> bool {
match self {
Self::Rpc { message, .. } => already_known_message(message),
_ => false,
}
}
}
pub(crate) fn already_known_message(message: &str) -> bool {
let m = message.to_ascii_lowercase();
m.contains("already in mempool")
|| m.contains("already in the mempool")
|| m.contains("already have transaction")
|| m.contains("txn-already-known")
|| m.contains("txn-already-in-mempool")
|| m.contains("transaction already in block chain")
|| m.contains("committed to the best chain")
|| m.contains("already exists")
}
#[async_trait]
pub trait ZcashNode: Send + Sync {
async fn get_block_count(&self) -> Result<u32, NodeError>;
async fn get_next_branch_id(&self) -> Result<u32, NodeError>;
async fn get_address_utxos(&self, addresses: &[String]) -> Result<Vec<AddressUtxo>, NodeError>;
async fn get_address_txids(&self, addresses: &[String]) -> Result<Vec<String>, NodeError>;
async fn get_raw_transaction(&self, txid: &str) -> Result<NodeTransaction, NodeError>;
async fn send_raw_transaction(&self, hex: &str) -> Result<String, NodeError>;
async fn find_spend(&self, address: &str, txid: &str, vout: u32) -> Result<Option<NodeTransaction>, NodeError>;
async fn get_tx_out(&self, txid: &str, vout: u32, include_mempool: bool) -> Result<Option<NodeTxOut>, NodeError> {
let _ = (txid, vout, include_mempool);
Err(NodeError::Unsupported("getTxOut"))
}
async fn network_upgrades(&self) -> Result<UpgradeSchedule, NodeError> {
Err(NodeError::Unsupported("networkUpgrades"))
}
async fn generate(&self, blocks: u32) -> Result<Vec<String>, NodeError> {
let _ = blocks;
Err(NodeError::Unsupported("generate"))
}
}
pub async fn is_unspent(node: &dyn ZcashNode, address: &str, txid: &str, vout: u32) -> Result<bool, NodeError> {
match node.get_tx_out(txid, vout, true).await {
Ok(out) => Ok(out.is_some()),
Err(NodeError::Unsupported(_)) => {
let utxos = node.get_address_utxos(&[address.to_owned()]).await?;
Ok(utxos.iter().any(|u| u.txid.eq_ignore_ascii_case(txid) && u.vout == vout))
}
Err(e) => Err(e),
}
}
pub(crate) async fn upgrade_schedule(node: &dyn ZcashNode) -> crate::Result<Option<UpgradeSchedule>> {
match node.network_upgrades().await {
Ok(schedule) => Ok(Some(schedule)),
Err(NodeError::Unsupported(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub(crate) fn upgrade_rule(
schedule: Option<&UpgradeSchedule>,
refund_height: u32,
network: paralock_core::zcash::Network,
allow_unknown: bool,
) -> crate::Result<UpgradeCheck> {
match schedule {
Some(schedule) => Ok(UpgradeCheck::Checked(protocol::assert_no_upgrade_before(
&schedule.upgrades,
schedule.tip,
refund_height,
)?)),
None if network == paralock_core::zcash::Network::Mainnet && !allow_unknown => Err(crate::Error::protocol(
protocol::ParalockErrorCode::Unavailable,
"the Zcash backend can't report network upgrades, which private swaps on mainnet need (an upgrade before \
the refund height would invalidate the pre-signed refund); use a full node, or set allow_unknown_upgrades",
)),
None => {
tracing::warn!(
"this Zcash backend can't report network upgrades; skipping the upgrade check for pre-signed spends"
);
Ok(UpgradeCheck::BackendCannotReport)
}
}
}
pub(crate) async fn upgrade_check(
node: &dyn ZcashNode,
refund_height: u32,
network: paralock_core::zcash::Network,
allow_unknown: bool,
) -> crate::Result<UpgradeCheck> {
upgrade_rule(upgrade_schedule(node).await?.as_ref(), refund_height, network, allow_unknown)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use paralock_core::protocol::ParalockErrorCode;
use paralock_core::zcash::Network;
#[test]
fn refuses_an_unknown_upgrade_schedule_for_mainnet_private_swaps_unless_allowed() {
let e = upgrade_rule(None, 1_000, Network::Mainnet, false).unwrap_err();
assert_eq!(e.code(), ParalockErrorCode::Unavailable);
assert!(e.to_string().contains("allow_unknown_upgrades"), "{e}");
assert_eq!(upgrade_rule(None, 1_000, Network::Mainnet, true).unwrap(), UpgradeCheck::BackendCannotReport);
assert_eq!(upgrade_rule(None, 1_000, Network::Testnet, false).unwrap(), UpgradeCheck::BackendCannotReport);
let schedule = UpgradeSchedule {
tip: 900,
upgrades: vec![NetworkUpgrade { name: "NU7".into(), branch_id: 0x7719_0ad9, activation_height: 950 }],
};
let e = upgrade_rule(Some(&schedule), 1_000, Network::Mainnet, false).unwrap_err();
assert_eq!(e.code(), ParalockErrorCode::UnsafeTimeouts);
assert!(matches!(upgrade_rule(Some(&schedule), 940, Network::Mainnet, false), Ok(UpgradeCheck::Checked(_))));
}
#[test]
fn recognizes_already_known_broadcasts() {
for m in [
"transaction already in block chain",
"txn-already-in-mempool",
"Transaction already in the mempool",
"already have transaction",
"Transaction is already committed to the best chain",
"transaction already exists",
] {
let e = NodeError::Rpc { method: "sendrawtransaction".into(), code: -27, message: m.into() };
assert!(e.already_known(), "{m}");
}
let e =
NodeError::Rpc { method: "sendrawtransaction".into(), code: -26, message: "bad-txns-inputs-spent".into() };
assert!(!e.already_known());
assert!(e.is_rejection());
let t = NodeError::Transport { method: "x".into(), message: "already exists".into() };
assert!(!t.already_known() && !t.is_rejection());
}
}