Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,27 @@ boundary are implemented, along with configurable, inventory-aware firm-quote
construction for exact-in and exact-out trades. The quote engine applies exact
integer pricing, deterministically selects fresh available inventory, reserves
its exact outpoints, and durably replays the same symbolic transaction
contribution for an idempotent request. Its `FirmQuote` is an internal,
unauthenticated artifact, not yet a provider-signed network quote. A production
wallet/RPC/HSM backend, market-data pricing source, transaction validator,
signer adapter, authenticated remote protocol, and relay remain future work.
contribution for an idempotent request. The provider now also validates a
concrete final Liquid PSET before the irreversible signing transition: it binds
the persisted RFQ leg inside a venue-neutral transaction, checks authoritative
unspent prevouts, finalized taker P2TR `SIGHASH_ALL` signatures, confidential
disclosures/proofs/balance and provider output recovery, and derives fee and
weight facts with the missing provider witnesses projected. Its `FirmQuote` is
still an internal, unauthenticated artifact, not yet a provider-signed network
quote. A production wallet/RPC/HSM backend, market-data pricing source, signer
adapter, authenticated remote protocol, and relay remain future work.
This initial validator accepts ordinary finalized tree-less P2TR
`SIGHASH_ALL` inputs outside the current RFQ leg; Simplicity covenant inputs
and a second interactive RFQ signer need a later authenticated venue/script
verification seam.
The eventual service must derive market assets from chain-validated canonical
parameters and add authenticated-owner rate limits plus bounded history
retention; the library's live-quote quotas only cap concurrent reservations.
Until the validator and signer adapter land, the safety-critical commit and
signed-result transitions are intentionally crate-internal. The RFQ provider
remains separate from `deadcat-node`; future AMM and DLOB protocols are not
implemented by this repository today.
The safety-critical commit is reachable only by consuming the validator's
opaque one-shot intent; the signed-result transition remains crate-internal
until the signer adapter lands. The RFQ provider remains separate from
`deadcat-node`; future AMM and DLOB protocols are not implemented by this
repository today.

The RFQ provider database is still clean-slate preproduction state. Its schema
and private record-layout versions intentionally remain `1` while the provider
Expand Down
23 changes: 15 additions & 8 deletions crates/deadcat-rfq-provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
//!
//! Wallet discovery admits only confidential tree-less P2TR outputs and quote
//! eligibility is the intersection of a fresh complete scan with durable
//! unallocated state. Concrete wallet/RPC/HSM implementations remain outside
//! this crate. The commit and signed-result transitions also remain private
//! until the concrete transaction validator and signer adapter can be their
//! only producers.
//! unallocated state. The final-PSET validator is the sole production path to
//! the commit transition: it rechecks the durable quote, authoritative
//! prevouts, complete taker signatures, confidential proofs and openings, and
//! exact fee/weight facts before it emits a one-shot signing capability.
//! Concrete wallet/RPC/HSM implementations remain outside this crate. The
//! signed-result transition remains private until a concrete signer adapter
//! can be its only producer.

mod inventory;
mod model;
Expand Down Expand Up @@ -50,12 +53,16 @@ pub use quote::{
StaticRateRule, StaticRationalPricing,
};
pub use store::{
CommitOutcome, MAX_EXPIRATION_BATCH, ProviderError, ReservationBook, SCHEMA_VERSION,
SignedOutcome,
AuthoritativePrevout, CommitOutcome, DEFAULT_MAX_SETTLEMENT_INPUTS,
DEFAULT_MAX_SETTLEMENT_OUTPUTS, MAX_EXPIRATION_BATCH, ProviderError,
ProviderSettlementValidator, ReservationBook, SCHEMA_VERSION, SettlementChainSource,
SettlementInputPlacement, SettlementLayout, SettlementLayoutError, SettlementLimitsError,
SettlementOutputPlacement, SettlementValidationError, SettlementValidationLimits,
SignedOutcome, ValidatedSigningIntent,
};
pub use wallet::{
ConfidentialDestination, DestinationPurpose, DestinationSource, InventorySnapshot,
InventorySnapshotCommitment, InventorySource, P2TR_SIGHASH_ALL_SCRIPT_WITNESS_BYTES,
P2TR_SIGHASH_ALL_SIGNATURE_BYTES, ProviderInputSignature, ProviderSigner, SigningResponse,
WalletBoundaryError, WalletOwnedOutput, WalletScanAnchor,
P2TR_SIGHASH_ALL_SIGNATURE_BYTES, ProviderInputSignature, ProviderOutputRecovery,
ProviderSigner, SigningResponse, WalletBoundaryError, WalletOwnedOutput, WalletScanAnchor,
};
192 changes: 180 additions & 12 deletions crates/deadcat-rfq-provider/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ use crate::model::{
SigningTarget, TransactionFee, UnixMillis, WalletKeyLocator,
};
use crate::quote::{
FirmQuote, FirmQuoteDraft, FirmQuoteOutcome, FirmQuoteRequest, PricingDecision,
QuoteContribution, QuoteEnginePolicy, QuoteExecution, QuoteOutputRole, QuoteSnapshotEvidence,
QuotedProviderInput, finalize_quote, quote_from_stored_parts, quote_outcome,
recompute_quote_commitment, recovery_metadata_commitment,
DestinationRecovery, FirmQuote, FirmQuoteDraft, FirmQuoteOutcome, FirmQuoteRequest,
PricingDecision, QuoteContribution, QuoteEnginePolicy, QuoteExecution, QuoteOutputRole,
QuoteSnapshotEvidence, QuotedProviderInput, finalize_quote, quote_from_stored_parts,
quote_outcome, recompute_quote_commitment, recovery_metadata_commitment,
};
use crate::wallet::recompute_inventory_binding;

mod settlement;

pub use settlement::{
AuthoritativePrevout, DEFAULT_MAX_SETTLEMENT_INPUTS, DEFAULT_MAX_SETTLEMENT_OUTPUTS,
ProviderSettlementValidator, SettlementChainSource, SettlementInputPlacement, SettlementLayout,
SettlementLayoutError, SettlementLimitsError, SettlementOutputPlacement,
SettlementValidationError, SettlementValidationLimits, ValidatedSigningIntent,
};

pub const SCHEMA_VERSION: u32 = 1;
/// Maximum number of unrelated expirations one explicit sweep may mutate in a
/// single immediate-durability transaction.
Expand Down Expand Up @@ -788,6 +797,102 @@ impl ReservationBook {
.transpose()
}

/// Load the authenticated, durable inputs needed to validate a final
/// settlement. The returned quote and recovery metadata are reconstructed
/// from the reservation record rather than accepted from the submitter.
///
/// Committed and signed records include their exact durable signing job so
/// the settlement layer can recognize retries without consulting live
/// wallet or chain state. Released reservations are terminal and therefore
/// do not produce a validation context.
pub(super) fn settlement_context(
&self,
access: ReservationAccess,
) -> Result<SettlementContext, ProviderError> {
self.ensure_healthy()?;
let read = self.database.begin_read()?;
let reservations = read.open_table(RESERVATIONS)?;
let record = reservations
.get(access.reservation_id().to_bytes().as_slice())?
.map(|value| decode_record::<StoredReservation>(value.value()))
.transpose()?
.ok_or(ProviderError::ReservationNotFound(access.reservation_id()))?;
if record.id() != access.reservation_id() {
return Err(ProviderError::CorruptState(
"reservation key and record ID disagree".to_owned(),
));
}
record.validate()?;
if record.owner != access.owner().to_bytes() {
return Err(ProviderError::ReservationOwnerMismatch(
access.reservation_id(),
));
}
let stored_quote = record.quote.as_ref().ok_or_else(|| {
ProviderError::CorruptState(
"settlement validation requires a firm-quote reservation".to_owned(),
)
})?;
let quote = stored_quote.to_domain(self.identity, &record)?;
let provider_receive_recovery = DestinationRecovery {
internal_key: stored_quote.provider_receive_internal_key,
wallet_locator: stored_quote.provider_receive_wallet_locator,
};
let provider_change_recovery = match (
stored_quote.provider_change_internal_key,
stored_quote.provider_change_wallet_locator,
) {
(Some(internal_key), Some(wallet_locator)) => Some(DestinationRecovery {
internal_key,
wallet_locator,
}),
(None, None) => None,
(Some(_), None) | (None, Some(_)) => {
return Err(ProviderError::CorruptState(
"persisted provider change recovery is incomplete".to_owned(),
));
}
};
let inventory = read.open_table(INVENTORY)?;
let mut provider_targets = Vec::with_capacity(record.outpoints.len());
for outpoint in &record.outpoints {
let stored = inventory
.get(outpoint_key(*outpoint).as_slice())?
.map(|value| decode_record::<StoredInventoryItem>(value.value()))
.transpose()?
.ok_or_else(|| {
ProviderError::CorruptState(format!(
"reserved outpoint {outpoint:?} has no inventory metadata"
))
})?;
stored.to_domain()?;
provider_targets.push(StoredSigningTarget::from_inventory(stored).to_domain()?);
}
let state = match &record.state {
StoredReservationState::Reserved => SettlementContextState::Reserved,
StoredReservationState::Released { .. } => {
return Err(ProviderError::ReservationAlreadyReleased(record.id()));
}
StoredReservationState::Committed { intent } => {
SettlementContextState::Committed(intent.to_job(record.id())?)
}
StoredReservationState::Signed { intent, artifact } => {
let job = intent.to_job(record.id())?;
artifact.to_domain(record.id(), job.commitment())?;
SettlementContextState::Signed(job)
}
};
Ok(SettlementContext {
provider: self.identity,
access,
quote,
provider_receive_recovery,
provider_change_recovery,
provider_targets,
state,
})
}

/// Cancel a reservation only while it remains before the signing point of
/// no return. Cancellation at or after the deadline is recorded as expiry.
pub fn cancel<C: Clock>(
Expand Down Expand Up @@ -874,23 +979,52 @@ impl ReservationBook {
/// before any wallet or HSM signer is invoked.
///
/// `pre_sign_payload` must be the complete immutable provider signing
/// transcript produced by the later settlement validator, including the
/// finalized transaction body, proofs, authoritative prevouts, existing
/// user witnesses, approved sighash profile, and quote economics.
// The concrete validator added in the next provider layer will be this
// method's only production caller. Keeping the transition crate-private
// prevents detached fee assertions from crossing the trust boundary.
#[allow(dead_code)]
pub(crate) fn commit_before_sign<C: Clock>(
/// transcript produced by the settlement validator, including the
/// finalized transaction body, proofs, authoritatively checked PSET
/// prevouts, existing user witnesses, approved sighash profile, and quote
/// economics.
// The concrete validator is this method's only production caller. Keeping
// the transition private prevents detached fee assertions from crossing
// the trust boundary.
fn commit_validated_before_sign<C: Clock>(
&self,
expected_provider: ProviderIdentity,
access: ReservationAccess,
expected_quote_commitment: QuoteCommitment,
pre_sign_payload: Vec<u8>,
fee: TransactionFee,
clock: &C,
) -> Result<CommitOutcome, ProviderError> {
if self.identity != expected_provider {
return Err(ProviderError::ValidatedIntentBindingMismatch(
access.reservation_id(),
));
}
self.commit_before_sign_inner(
access,
Some(expected_quote_commitment),
pre_sign_payload,
fee,
clock,
)
}

fn commit_before_sign_inner<C: Clock>(
&self,
access: ReservationAccess,
expected_quote_commitment: Option<QuoteCommitment>,
pre_sign_payload: Vec<u8>,
fee: TransactionFee,
clock: &C,
) -> Result<CommitOutcome, ProviderError> {
validate_settlement_bytes(&pre_sign_payload)?;
let (_operation_guard, write, now) = self.begin_timed_write(clock)?;
let mut record = require_authorized_reservation(&write, access)?;
if expected_quote_commitment.is_some_and(|expected| {
record.quote.is_none() || record.quote_commitment != expected.to_bytes()
}) {
return Err(ProviderError::ValidatedIntentBindingMismatch(record.id()));
}

match &record.state {
StoredReservationState::Committed { intent } => {
Expand Down Expand Up @@ -1013,6 +1147,19 @@ impl ReservationBook {
Ok(CommitOutcome::NewlyCommitted(job))
}

/// Legacy state-machine test seam. Production code can cross this boundary
/// only through [`ValidatedSigningIntent`](settlement::ValidatedSigningIntent).
#[cfg(test)]
pub(crate) fn commit_before_sign<C: Clock>(
&self,
access: ReservationAccess,
pre_sign_payload: Vec<u8>,
fee: TransactionFee,
clock: &C,
) -> Result<CommitOutcome, ProviderError> {
self.commit_before_sign_inner(access, None, pre_sign_payload, fee, clock)
}

/// Persist exact signed bytes before they can be returned or relayed.
// The signer adapter added in the next provider layer will verify and
// canonicalize its result before invoking this crate-private transition.
Expand Down Expand Up @@ -1276,6 +1423,25 @@ impl ReservationBook {
}
}

/// Authenticated durable state from which the settlement validator derives
/// one opaque signing intent.
pub(super) struct SettlementContext {
pub(super) provider: ProviderIdentity,
pub(super) access: ReservationAccess,
pub(super) quote: FirmQuote,
pub(super) provider_receive_recovery: DestinationRecovery,
pub(super) provider_change_recovery: Option<DestinationRecovery>,
pub(super) provider_targets: Vec<SigningTarget>,
pub(super) state: SettlementContextState,
}

/// Durable replay state paired with a settlement context.
pub(super) enum SettlementContextState {
Reserved,
Committed(SigningJob),
Signed(SigningJob),
}

#[cfg(test)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ReserveOutcome {
Expand Down Expand Up @@ -3929,6 +4095,8 @@ pub enum ProviderError {
ReservationAlreadyReleased(ReservationId),
#[error("reservation crossed the irreversible signing point: {0:?}")]
PointOfNoReturn(ReservationId),
#[error("validated settlement does not match the provider or firm quote for reservation {0:?}")]
ValidatedIntentBindingMismatch(ReservationId),
#[error("reservation is already committed to a different signing intent: {0:?}")]
DifferentSigningIntent(ReservationId),
#[error("settlement payload must not be empty")]
Expand Down
Loading
Loading