mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-02-01 08:51:09 +03:00
349 lines
9.4 KiB
Rust
349 lines
9.4 KiB
Rust
use std::net::SocketAddr;
|
|
use std::net::ToSocketAddrs;
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
use grin_api::LocatedTxKernel;
|
|
use grin_api::{OutputPrintable, OutputType, Tip};
|
|
use grin_core::consensus::COINBASE_MATURITY;
|
|
use grin_core::core::hash::Hash;
|
|
use grin_core::core::{Committed, Input, OutputFeatures, Transaction};
|
|
use grin_util::ToHex;
|
|
use serde_json::json;
|
|
use thiserror::Error;
|
|
|
|
use grin_onion::crypto::secp::Commitment;
|
|
|
|
use crate::http;
|
|
|
|
#[async_trait]
|
|
pub trait GrinNode: Send + Sync {
|
|
/// Retrieves the unspent output with a matching commitment
|
|
async fn async_get_utxo(
|
|
&self,
|
|
output_commit: &Commitment,
|
|
) -> Result<Option<OutputPrintable>, NodeError>;
|
|
|
|
/// Gets the height and hash of the chain tip
|
|
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>;
|
|
|
|
/// Posts a transaction to the grin node
|
|
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>;
|
|
|
|
/// Returns a LocatedTxKernel based on the kernel excess.
|
|
/// The min_height and max_height parameters are both optional.
|
|
/// If not supplied, min_height will be set to 0 and max_height will be set to the head of the chain.
|
|
/// The method will start at the block height max_height and traverse the kernel MMR backwards, until either the kernel is found or min_height is reached.
|
|
async fn async_get_kernel(
|
|
&self,
|
|
excess: &Commitment,
|
|
min_height: Option<u64>,
|
|
max_height: Option<u64>,
|
|
) -> Result<Option<LocatedTxKernel>, NodeError>;
|
|
}
|
|
|
|
/// Error types for interacting with nodes
|
|
#[derive(Error, Debug)]
|
|
pub enum NodeError {
|
|
#[error("Error decoding JSON response: {0:?}")]
|
|
DecodeResponseError(serde_json::Error),
|
|
#[error("JSON-RPC API communication error: {0:?}")]
|
|
ApiCommError(grin_api::Error),
|
|
#[error("Client error: {0:?}")]
|
|
NodeCommError(http::HttpError),
|
|
#[error("Error decoding JSON-RPC response: {0:?}")]
|
|
ResponseParseError(grin_api::json_rpc::Error),
|
|
}
|
|
|
|
/// Checks if a commitment is in the UTXO set
|
|
pub async fn async_is_unspent(
|
|
node: &Arc<dyn GrinNode>,
|
|
commit: &Commitment,
|
|
) -> Result<bool, NodeError> {
|
|
let utxo = node.async_get_utxo(&commit).await?;
|
|
Ok(utxo.is_some())
|
|
}
|
|
|
|
/// Checks whether a commitment is spendable at the block height provided
|
|
pub async fn async_is_spendable(
|
|
node: &Arc<dyn GrinNode>,
|
|
commit: &Commitment,
|
|
next_block_height: u64,
|
|
) -> Result<bool, NodeError> {
|
|
let output = node.async_get_utxo(&commit).await?;
|
|
if let Some(out) = output {
|
|
let is_coinbase = match out.output_type {
|
|
OutputType::Coinbase => true,
|
|
OutputType::Transaction => false,
|
|
};
|
|
|
|
if is_coinbase {
|
|
if let Some(block_height) = out.block_height {
|
|
if block_height + COINBASE_MATURITY < next_block_height {
|
|
return Ok(false);
|
|
}
|
|
} else {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Builds an input for an unspent output commitment
|
|
pub async fn async_build_input(
|
|
node: &Arc<dyn GrinNode>,
|
|
output_commit: &Commitment,
|
|
) -> Result<Option<Input>, NodeError> {
|
|
let output = node.async_get_utxo(&output_commit).await?;
|
|
|
|
if let Some(out) = output {
|
|
let features = match out.output_type {
|
|
OutputType::Coinbase => OutputFeatures::Coinbase,
|
|
OutputType::Transaction => OutputFeatures::Plain,
|
|
};
|
|
|
|
let input = Input::new(features, out.commit);
|
|
return Ok(Some(input));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn async_is_tx_valid(
|
|
node: &Arc<dyn GrinNode>,
|
|
tx: &Transaction,
|
|
) -> Result<bool, NodeError> {
|
|
let next_block_height = node.async_get_chain_tip().await?.0 + 1;
|
|
for input_commit in &tx.inputs_committed() {
|
|
if !async_is_spendable(&node, &input_commit, next_block_height).await? {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
for output_commit in &tx.outputs_committed() {
|
|
if async_is_unspent(&node, &output_commit).await? {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
|
|
#[derive(Clone)]
|
|
pub struct HttpGrinNode {
|
|
node_url: SocketAddr,
|
|
node_api_secret: Option<String>,
|
|
}
|
|
|
|
const ENDPOINT: &str = "/v2/foreign";
|
|
|
|
impl HttpGrinNode {
|
|
pub fn new(node_url: &str, node_api_secret: &Option<String>) -> HttpGrinNode {
|
|
let mut addrs_iter = node_url.to_socket_addrs().unwrap();
|
|
let node_url = addrs_iter.next().unwrap();
|
|
HttpGrinNode {
|
|
node_url,
|
|
node_api_secret: node_api_secret.to_owned(),
|
|
}
|
|
}
|
|
|
|
async fn async_send_request<D: serde::de::DeserializeOwned>(
|
|
&self,
|
|
method: &str,
|
|
params: &serde_json::Value,
|
|
) -> Result<D, NodeError> {
|
|
let url = format!("http://{}{}", self.node_url, ENDPOINT);
|
|
let parsed = http::async_send_json_request(&url, &self.node_api_secret, &method, ¶ms)
|
|
.await
|
|
.map_err(NodeError::NodeCommError)?;
|
|
Ok(parsed)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl GrinNode for HttpGrinNode {
|
|
async fn async_get_utxo(
|
|
&self,
|
|
output_commit: &Commitment,
|
|
) -> Result<Option<OutputPrintable>, NodeError> {
|
|
let commits: Vec<String> = vec![output_commit.to_hex()];
|
|
let start_height: Option<u64> = None;
|
|
let end_height: Option<u64> = None;
|
|
let include_proof: Option<bool> = Some(false);
|
|
let include_merkle_proof: Option<bool> = Some(false);
|
|
|
|
let params = json!([
|
|
Some(commits),
|
|
start_height,
|
|
end_height,
|
|
include_proof,
|
|
include_merkle_proof
|
|
]);
|
|
let outputs = self
|
|
.async_send_request::<Vec<OutputPrintable>>("get_outputs", ¶ms)
|
|
.await?;
|
|
if outputs.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
Ok(Some(outputs[0].clone()))
|
|
}
|
|
|
|
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
|
|
let params = json!([]);
|
|
let tip_json = self
|
|
.async_send_request::<serde_json::Value>("get_tip", ¶ms)
|
|
.await?;
|
|
let tip =
|
|
serde_json::from_value::<Tip>(tip_json).map_err(NodeError::DecodeResponseError)?;
|
|
|
|
Ok((
|
|
tip.height,
|
|
Hash::from_hex(tip.last_block_pushed.as_str()).unwrap(),
|
|
))
|
|
}
|
|
|
|
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
|
let params = json!([tx, true]);
|
|
self.async_send_request::<serde_json::Value>("push_transaction", ¶ms)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn async_get_kernel(
|
|
&self,
|
|
excess: &Commitment,
|
|
min_height: Option<u64>,
|
|
max_height: Option<u64>,
|
|
) -> Result<Option<LocatedTxKernel>, NodeError> {
|
|
let params = json!([excess.0.as_ref().to_hex(), min_height, max_height]);
|
|
let value = self
|
|
.async_send_request::<serde_json::Value>("get_kernel", ¶ms)
|
|
.await?;
|
|
|
|
let contents = format!("{:?}", value);
|
|
if contents.contains("NotFound") {
|
|
return Ok(None);
|
|
}
|
|
|
|
let located_kernel = serde_json::from_value::<LocatedTxKernel>(value)
|
|
.map_err(NodeError::DecodeResponseError)?;
|
|
Ok(Some(located_kernel))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod mock {
|
|
use std::collections::HashMap;
|
|
use std::sync::RwLock;
|
|
|
|
use async_trait::async_trait;
|
|
use grin_api::{LocatedTxKernel, OutputPrintable, OutputType};
|
|
use grin_core::core::hash::Hash;
|
|
use grin_core::core::Transaction;
|
|
|
|
use grin_onion::crypto::secp::Commitment;
|
|
|
|
use super::{GrinNode, NodeError};
|
|
|
|
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
|
|
/// Use only for testing purposes.
|
|
pub struct MockGrinNode {
|
|
utxos: HashMap<Commitment, OutputPrintable>,
|
|
txns_posted: RwLock<Vec<Transaction>>,
|
|
kernels: HashMap<Commitment, LocatedTxKernel>,
|
|
}
|
|
|
|
impl MockGrinNode {
|
|
pub fn new() -> Self {
|
|
MockGrinNode {
|
|
utxos: HashMap::new(),
|
|
txns_posted: RwLock::new(Vec::new()),
|
|
kernels: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new_with_utxos(utxos: &Vec<&Commitment>) -> Self {
|
|
let mut node = MockGrinNode {
|
|
utxos: HashMap::new(),
|
|
txns_posted: RwLock::new(Vec::new()),
|
|
kernels: HashMap::new(),
|
|
};
|
|
for utxo in utxos {
|
|
node.add_default_utxo(utxo);
|
|
}
|
|
node
|
|
}
|
|
|
|
pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) {
|
|
self.utxos.insert(output_commit.clone(), utxo.clone());
|
|
}
|
|
|
|
pub fn add_default_utxo(&mut self, output_commit: &Commitment) {
|
|
let utxo = OutputPrintable {
|
|
output_type: OutputType::Transaction,
|
|
commit: output_commit.to_owned(),
|
|
spent: false,
|
|
proof: None,
|
|
proof_hash: String::from(""),
|
|
block_height: None,
|
|
merkle_proof: None,
|
|
mmr_index: 0,
|
|
};
|
|
|
|
self.add_utxo(&output_commit, &utxo);
|
|
}
|
|
|
|
pub fn get_posted_txns(&self) -> Vec<Transaction> {
|
|
let read = self.txns_posted.read().unwrap();
|
|
read.clone()
|
|
}
|
|
|
|
pub fn add_kernel(&mut self, kernel: &LocatedTxKernel) {
|
|
self.kernels
|
|
.insert(kernel.tx_kernel.excess.clone(), kernel.clone());
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl GrinNode for MockGrinNode {
|
|
async fn async_get_utxo(
|
|
&self,
|
|
output_commit: &Commitment,
|
|
) -> Result<Option<OutputPrintable>, NodeError> {
|
|
if let Some(utxo) = self.utxos.get(&output_commit) {
|
|
return Ok(Some(utxo.clone()));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
|
|
Ok((100, Hash::default()))
|
|
}
|
|
|
|
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
|
|
let mut write = self.txns_posted.write().unwrap();
|
|
write.push(tx.clone());
|
|
Ok(())
|
|
}
|
|
|
|
async fn async_get_kernel(
|
|
&self,
|
|
excess: &Commitment,
|
|
_min_height: Option<u64>,
|
|
_max_height: Option<u64>,
|
|
) -> Result<Option<LocatedTxKernel>, NodeError> {
|
|
if let Some(kernel) = self.kernels.get(&excess) {
|
|
return Ok(Some(kernel.clone()));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|