use super::{
prove::Prove,
request::{CharmsFee, ProveRequest},
};
#[cfg(not(feature = "prover"))]
use crate::utils::retry;
use crate::{
cli::{charms_fee_settings, prove_impl},
tx::{bitcoin_tx, cardano_tx},
};
use anyhow::bail;
use charms_client::{
CURRENT_VERSION, NormalizedSpell,
tx::{Chain, Tx, by_txid},
};
use charms_data::util;
use const_format::formatcp;
#[cfg(feature = "prover")]
use redis::AsyncCommands;
#[cfg(feature = "prover")]
use redis_macros::{FromRedisValue, ToRedisArgs};
#[cfg(not(feature = "prover"))]
use reqwest::Client;
#[cfg(feature = "prover")]
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::future::Future;
#[cfg(feature = "prover")]
use std::time::Duration;
pub trait ProveSpellTx: Send + Sync {
fn new(mock: bool) -> Self;
fn prove_spell_tx(
&self,
prove_request: ProveRequest,
) -> impl Future<Output = anyhow::Result<Vec<Tx>>>;
}
pub struct ProveSpellTxImpl {
pub mock: bool,
pub charms_fee_settings: Option<CharmsFee>,
pub charms_prove_api_url: String,
#[cfg(feature = "prover")]
pub cache_client: Option<(redis::Client, rslock::LockManager)>,
pub prover: Box<dyn Prove>,
#[cfg(not(feature = "prover"))]
pub client: Client,
}
const CHARMS_PROVE_API_URL: &'static str =
formatcp!("https://v{CURRENT_VERSION}.charms.dev/spells/prove");
#[cfg(feature = "prover")]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RequestData {
committed_data_hash: [u8; 32],
}
#[cfg(feature = "prover")]
#[derive(Clone, Debug, Serialize, Deserialize, FromRedisValue, ToRedisArgs)]
pub enum ProofState {
Processing {
request_data: RequestData,
},
Done {
request_data: RequestData,
result: Vec<Tx>,
},
}
pub fn committed_data_hash(normalized_spell: &NormalizedSpell) -> anyhow::Result<[u8; 32]> {
let bytes =
util::write(&normalized_spell).context("Failed to serialize normalized spell for hash")?;
Ok(Sha256::digest(&bytes).into())
}
use anyhow::Context;
impl ProveSpellTxImpl {
pub(super) async fn do_prove_spell_tx(
&self,
prove_request: ProveRequest,
app_cycles: u64,
) -> anyhow::Result<Vec<Tx>> {
let total_app_cycles = app_cycles;
let ProveRequest {
spell: norm_spell,
app_private_inputs,
tx_ins_beamed_source_utxos,
binaries,
prev_txs,
change_address,
fee_rate,
chain,
collateral_utxo,
} = prove_request;
if chain == Chain::Cardano && collateral_utxo.is_none() {
bail!("Collateral UTXO is required for Cardano spells");
}
let prev_txs_by_id = by_txid(&prev_txs);
let (truncated_norm_spell, proof, proof_app_cycles) = self.prover.prove(
norm_spell.clone(),
binaries,
app_private_inputs,
prev_txs,
tx_ins_beamed_source_utxos,
)?;
let total_cycles = if !self.mock {
total_app_cycles
} else {
proof_app_cycles };
tracing::info!("proof generated. total app cycles: {}", total_cycles);
let spell_data = util::write(&(&truncated_norm_spell, &proof))?;
let charms_fee = self.charms_fee_settings.clone();
match chain {
Chain::Bitcoin => {
let txs = bitcoin_tx::make_transactions(
&norm_spell,
&change_address,
&prev_txs_by_id,
&spell_data,
fee_rate,
charms_fee,
total_cycles,
)?;
Ok(txs)
}
Chain::Cardano => {
let txs = cardano_tx::make_transactions(
&norm_spell,
&change_address,
&spell_data,
&prev_txs_by_id,
None,
charms_fee,
total_cycles,
collateral_utxo,
)
.await?;
Ok(txs)
}
}
}
}
impl ProveSpellTx for ProveSpellTxImpl {
#[tracing::instrument(level = "debug")]
fn new(mock: bool) -> Self {
let charms_fee_settings = charms_fee_settings();
let charms_prove_api_url = std::env::var("CHARMS_PROVE_API_URL")
.ok()
.unwrap_or(CHARMS_PROVE_API_URL.to_string());
tracing::info!(charms_prove_api_url);
let prover = prove_impl(mock);
#[cfg(feature = "prover")]
let cache_client: Option<(_, _)> = {
std::env::var("REDIS_URL").ok().and_then(|redis_url| {
match redis::Client::open(redis_url) {
Ok(redis_client) => {
let lock_manager =
rslock::LockManager::from_clients(vec![redis_client.clone()]);
Some((redis_client, lock_manager))
}
Err(e) => {
tracing::warn!("Failed to create Redis client, caching disabled: {}", e);
None
}
}
})
};
#[cfg(not(feature = "prover"))]
let client = Client::builder()
.use_rustls_tls() .http2_prior_knowledge()
.http2_adaptive_window(true)
.connect_timeout(std::time::Duration::from_secs(15))
.build()
.expect("HTTP client should be created successfully");
Self {
mock,
charms_fee_settings,
charms_prove_api_url,
#[cfg(feature = "prover")]
cache_client,
prover,
#[cfg(not(feature = "prover"))]
client,
}
}
#[cfg(feature = "prover")]
async fn prove_spell_tx(&self, mut prove_request: ProveRequest) -> anyhow::Result<Vec<Tx>> {
let app_cycles = self.validate_prove_request(&mut prove_request)?;
let norm_spell = &prove_request.spell;
if let Some((cache_client, lock_manager)) = self.cache_client.as_ref() {
let committed_data_hash = committed_data_hash(norm_spell)?;
let request_key = hex::encode(committed_data_hash);
let lock_key = format!("LOCK_{}", request_key.as_str());
let mut con = cache_client.get_multiplexed_async_connection().await?;
match con.get(request_key.as_str()).await? {
Some(ProofState::Done { result, .. }) => Ok(result),
_ => {
const LOCK_TTL: Duration = Duration::from_secs(600);
let lock = lock_manager
.acquire_no_guard(lock_key.as_bytes(), LOCK_TTL)
.await
.map_err(|e| anyhow::anyhow!("failed to acquire lock: {e}"))?;
let result: anyhow::Result<Vec<Tx>> = async {
match con.get(request_key.as_str()).await? {
Some(ProofState::Done { result, .. }) => return Ok(result),
_ => {}
};
let _: () = con
.set(
request_key.as_str(),
ProofState::Processing {
request_data: RequestData {
committed_data_hash,
},
},
)
.await?;
let r: Vec<Tx> =
self.do_prove_spell_tx(prove_request, app_cycles).await?;
let _: () = con
.set(
request_key.as_str(),
ProofState::Done {
request_data: RequestData {
committed_data_hash,
},
result: r.clone(),
},
)
.await?;
Ok(r)
}
.await;
lock_manager.unlock(&lock).await;
Ok(result?)
}
}
} else {
self.do_prove_spell_tx(prove_request, app_cycles).await
}
}
#[cfg(not(feature = "prover"))]
#[tracing::instrument(level = "info", skip_all)]
async fn prove_spell_tx(&self, mut prove_request: ProveRequest) -> anyhow::Result<Vec<Tx>> {
let app_cycles = self.validate_prove_request(&mut prove_request)?;
if self.mock {
return Self::do_prove_spell_tx(self, prove_request, app_cycles).await;
}
let response = retry(0, || async {
let cbor_body = util::write(&prove_request)?;
let response = self
.client
.post(&self.charms_prove_api_url)
.header("Content-Type", "application/cbor")
.body(cbor_body)
.send()
.await?;
if response.status().is_server_error() {
bail!("server error: {}", response.status());
}
Ok(response)
})
.await?;
if response.status().is_client_error() {
let status = response.status();
let body = response.text().await?;
bail!("client error: {}: {}", status, body);
}
let bytes = response.bytes().await?;
let txs: Vec<Tx> = util::read(&bytes[..])?;
Ok(txs)
}
}