Persist immutable private send recovery records with guarded transitions

This commit is contained in:
archipelago
2026-10-06 16:29:34 -04:00
parent a4ede20204
commit d4b359dbae
5 changed files with 707 additions and 70 deletions
+95 -64
View File
@@ -82,6 +82,100 @@ impl std::fmt::Debug for PreparedSwap {
}
}
impl PreparedSwap {
pub(super) fn validate_for_mint(&self, mint_url: &str) -> Result<()> {
anyhow::ensure!(
self.mint_url == mint_url,
"Prepared swap belongs to a different mint"
);
anyhow::ensure!(
!self.inputs.is_empty()
&& !self.outputs.is_empty()
&& self.outputs.len() == self.blinding.len(),
"Invalid prepared swap structure"
);
let commitments: std::collections::HashSet<_> = self
.outputs
.iter()
.map(|output| output.b_prime.as_str())
.collect();
anyhow::ensure!(
commitments.len() == self.outputs.len(),
"Prepared swap contains duplicate outputs"
);
let input_secrets: std::collections::HashSet<_> = self
.inputs
.iter()
.map(|proof| proof.secret.as_str())
.collect();
anyhow::ensure!(
input_secrets.len() == self.inputs.len(),
"Prepared swap contains duplicate inputs"
);
anyhow::ensure!(
self.keyset.unit == "sat",
"Prepared swap is not denominated in sats"
);
let input_total = self
.inputs
.iter()
.try_fold(0u64, |sum, proof| sum.checked_add(proof.amount))
.context("Prepared input amount overflow")?;
let output_total = self
.outputs
.iter()
.try_fold(0u64, |sum, output| sum.checked_add(output.amount))
.context("Prepared output amount overflow")?;
anyhow::ensure!(
output_total <= input_total,
"Prepared swap output value exceeds its inputs"
);
for (output, secret) in self.outputs.iter().zip(&self.blinding) {
anyhow::ensure!(
output.id == self.keyset.id
&& output.amount == secret.amount
&& output.amount.is_power_of_two(),
"Prepared swap output metadata changed"
);
let factor = secp256k1::SecretKey::from_slice(&secret.factor)
.context("Invalid prepared blinding factor")?;
let blinded = bdhke::blind_message(&secret.secret, &factor)?;
anyhow::ensure!(
output.b_prime == hex::encode(blinded.b_prime.serialize()),
"Prepared swap output commitment changed"
);
std::str::from_utf8(&secret.secret).context("Invalid prepared secret encoding")?;
self.keyset.key_for_amount(output.amount)?;
}
Ok(())
}
pub(super) fn validate_result_proofs(&self, proofs: &[Proof]) -> Result<()> {
anyhow::ensure!(
proofs.len() == self.blinding.len(),
"Payment result has missing or extra proofs"
);
let mut expected: std::collections::HashMap<_, _> = self
.blinding
.iter()
.map(|output| Ok((std::str::from_utf8(&output.secret)?, output.amount)))
.collect::<Result<_>>()?;
for proof in proofs {
anyhow::ensure!(
proof.id == self.keyset.id
&& expected.remove(proof.secret.as_str()) == Some(proof.amount),
"Payment result does not match prepared outputs"
);
proof.c_as_pubkey()?;
}
anyhow::ensure!(
expected.is_empty(),
"Payment result omitted prepared outputs"
);
Ok(())
}
}
/// Result of a mint operation.
pub struct MintResult {
pub proofs: Vec<Proof>,
@@ -656,70 +750,7 @@ impl MintClient {
}
fn validate_prepared_swap(&self, prepared: &PreparedSwap) -> Result<()> {
anyhow::ensure!(
prepared.mint_url == self.url,
"Prepared swap belongs to a different mint"
);
anyhow::ensure!(
!prepared.inputs.is_empty()
&& !prepared.outputs.is_empty()
&& prepared.outputs.len() == prepared.blinding.len(),
"Invalid prepared swap structure"
);
let commitments: std::collections::HashSet<_> = prepared
.outputs
.iter()
.map(|output| output.b_prime.as_str())
.collect();
anyhow::ensure!(
commitments.len() == prepared.outputs.len(),
"Prepared swap contains duplicate outputs"
);
let input_secrets: std::collections::HashSet<_> = prepared
.inputs
.iter()
.map(|proof| proof.secret.as_str())
.collect();
anyhow::ensure!(
input_secrets.len() == prepared.inputs.len(),
"Prepared swap contains duplicate inputs"
);
anyhow::ensure!(
prepared.keyset.unit == "sat",
"Prepared swap is not denominated in sats"
);
let input_total = prepared
.inputs
.iter()
.try_fold(0u64, |sum, proof| sum.checked_add(proof.amount))
.context("Prepared input amount overflow")?;
let output_total = prepared
.outputs
.iter()
.try_fold(0u64, |sum, output| sum.checked_add(output.amount))
.context("Prepared output amount overflow")?;
anyhow::ensure!(
output_total <= input_total,
"Prepared swap output value exceeds its inputs"
);
for (output, secret) in prepared.outputs.iter().zip(&prepared.blinding) {
anyhow::ensure!(
output.id == prepared.keyset.id
&& output.amount == secret.amount
&& output.amount.is_power_of_two(),
"Prepared swap output metadata changed"
);
let factor = secp256k1::SecretKey::from_slice(&secret.factor)
.context("Invalid prepared blinding factor")?;
let blinded = bdhke::blind_message(&secret.secret, &factor)?;
anyhow::ensure!(
output.b_prime == hex::encode(blinded.b_prime.serialize()),
"Prepared swap output commitment changed"
);
std::str::from_utf8(&secret.secret).context("Invalid prepared secret encoding")?;
prepared.keyset.key_for_amount(output.amount)?;
}
Ok(())
prepared.validate_for_mint(&self.url)
}
/// Execute only an already prepared request. Callers implementing recovery