Skip to main content

nocturne/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use k256::ecdsa::{RecoveryId, Signature as EcdsaSig, SigningKey, VerifyingKey};
4use std::collections::HashSet;
5use tiny_keccak::{Hasher, Keccak};
6
7/// Primitive conversions between Rust, ABI words, and `U256` values.
8pub mod convert;
9pub use convert::*;
10
11/// Checked market and offer construction.
12pub mod builder;
13pub use builder::*;
14
15/// Local offer validation and consumption-cap checks.
16pub mod validate;
17pub use validate::*;
18
19/// Tick, APR, fee, amount, and take-execution simulation.
20pub mod sim;
21pub use sim::*;
22
23/// Typed decoding for Midnight state, calldata, ratifiers, and bundles.
24pub mod decode;
25pub use decode::*;
26
27/// ABI encoding for take, bundle, cancellation, and ratification calls.
28pub mod codec;
29pub use codec::*;
30
31/// Local and external signing backends.
32pub mod signer;
33pub use signer::*;
34
35/// EIP-712 signing for delegated Midnight authorization.
36pub mod authorize;
37pub use authorize::*;
38
39/// Offer capacity and asset-to-unit sizing helpers.
40pub mod sizing;
41pub use sizing::*;
42
43/// Versioned maker-offer mempool payload encoding and decoding.
44pub mod payload;
45pub use payload::*;
46
47/// Wallet-agnostic maker-offer publication transactions.
48pub mod submission;
49pub use submission::*;
50
51/// Wallet-agnostic collateral, take, repayment, redemption, and authorization actions.
52pub mod actions;
53pub use actions::*;
54
55/// Approval and Midnight-authorization requirement planning and discovery.
56pub mod requirements;
57pub use requirements::*;
58
59/// Async Midnight order-book, quote, maker-offer, and validation client.
60pub mod api;
61pub use api::*;
62
63/// Errors from constructing a Merkle tree over offer leaves.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
65pub enum TreeError {
66    /// The leaf set is empty or not a power of two.
67    #[error("leaf count must be a nonzero power of two, got {0}")]
68    NotPowerOfTwo(usize),
69    /// The tree is taller than [`MAX_TREE_HEIGHT`], which `HashLib.offerTreeTypeHash` rejects
70    /// on-chain with `TreeTooHigh`.
71    #[error("tree height must be at most 20, got {0}")]
72    TooHigh(usize),
73    /// The requested leaf is outside the tree's leaf layer.
74    #[error("leaf index {index} is out of range for {leaves} leaves")]
75    LeafIndexOutOfRange { index: usize, leaves: usize },
76}
77
78/// Errors from constructing a canonical offer consumption group.
79#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
80pub enum GroupError {
81    #[error("offer group must not be empty")]
82    Empty,
83    #[error("all offers in a group must use the same maker")]
84    MakerMismatch,
85    #[error("all offers in a group must use the same maker side")]
86    SideMismatch,
87    #[error("all offers in a group must use the same loan token")]
88    LoanTokenMismatch,
89    #[error("all offers in a group must use the same chain id")]
90    ChainIdMismatch,
91    #[error("all offers in a group must use the same Midnight contract")]
92    MidnightMismatch,
93    #[error("every grouped offer must set exactly one non-zero cap")]
94    InvalidCap,
95    #[error("all offers in a group must use the same cap mode and value")]
96    CapMismatch,
97    #[error("buy offers must use the zero maker-seller receiver")]
98    BuyReceiverNotZero,
99}
100
101/// Errors from the high-level grouped and padded offer-tree constructor.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
103pub enum OfferTreeError {
104    #[error(transparent)]
105    Group(#[from] GroupError),
106    #[error(transparent)]
107    Tree(#[from] TreeError),
108    #[error("offer tree must not be empty")]
109    Empty,
110    #[error("offer tree contains a duplicate offer hash")]
111    DuplicateOffer,
112    #[error("all offers in a ratified tree must use the same chain id")]
113    ChainIdMismatch,
114    #[error("all offers in a ratified tree must use the same Midnight contract")]
115    MidnightMismatch,
116    #[error("all offers in a ratified tree must use the same ratifier")]
117    RatifierMismatch,
118}
119
120pub type Word = [u8; 32];
121pub type Address = [u8; 20];
122
123// ---- EIP-712 type strings (order matches HashLib.sol comments) ----
124pub const COLLATERAL_PARAMS_TYPE: &str =
125    "CollateralParams(address token,uint256 lltv,uint256 liquidationCursor,address oracle)";
126pub const MARKET_TYPE: &str = "Market(uint256 chainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)";
127pub const OFFER_TYPE: &str = "Offer(Market market,bool buy,address maker,uint256 start,uint256 expiry,uint256 tick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,address ratifier,bool reduceOnly,uint128 maxUnits,uint128 maxAssets,uint256 continuousFeeCap)";
128pub const EIP712_DOMAIN_TYPE: &str = "EIP712Domain(uint256 chainId,address verifyingContract)";
129
130pub fn keccak(bytes: &[u8]) -> Word {
131    let mut h = Keccak::v256();
132    let mut out = [0u8; 32];
133    h.update(bytes);
134    h.finalize(&mut out);
135    out
136}
137
138#[inline]
139fn addr_word(a: &Address) -> Word {
140    let mut w = [0u8; 32];
141    w[12..].copy_from_slice(a);
142    w
143}
144#[inline]
145fn u128_word(x: u128) -> Word {
146    let mut w = [0u8; 32];
147    w[16..].copy_from_slice(&x.to_be_bytes());
148    w
149}
150#[inline]
151fn bool_word(b: bool) -> Word {
152    let mut w = [0u8; 32];
153    w[31] = b as u8;
154    w
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
158pub struct CollateralParams {
159    pub token: Address,
160    pub lltv: Word,
161    pub liquidation_cursor: Word,
162    pub oracle: Address,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
166pub struct Market {
167    pub chain_id: Word,
168    pub midnight: Address,
169    pub loan_token: Address,
170    pub collateral_params: Vec<CollateralParams>,
171    pub maturity: Word,
172    pub rcf_threshold: Word,
173    pub enter_gate: Address,
174    pub liquidator_gate: Address,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
178pub struct Offer {
179    pub market: Market,
180    pub buy: bool,
181    pub maker: Address,
182    pub start: Word,
183    pub expiry: Word,
184    pub tick: Word,
185    pub group: Word,
186    pub callback: Address,
187    pub callback_data: Vec<u8>,
188    pub receiver_if_maker_is_seller: Address,
189    pub ratifier: Address,
190    pub reduce_only: bool,
191    pub max_units: u128,
192    pub max_assets: u128,
193    pub continuous_fee_cap: Word,
194}
195
196// Typehashes are computed from the type strings; tests assert they equal the on-chain constants.
197pub fn collateral_params_typehash() -> Word {
198    keccak(COLLATERAL_PARAMS_TYPE.as_bytes())
199}
200pub fn market_typehash() -> Word {
201    keccak([MARKET_TYPE, COLLATERAL_PARAMS_TYPE].concat().as_bytes())
202}
203pub fn offer_typehash() -> Word {
204    keccak(
205        [OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE]
206            .concat()
207            .as_bytes(),
208    )
209}
210/// The tallest offer tree `HashLib.offerTreeTypeHash` has a constant for; above this the
211/// contract reverts `TreeTooHigh`, so no taller tree can ever ratify.
212pub const MAX_TREE_HEIGHT: usize = 20;
213
214/// Panics if `height` exceeds [`MAX_TREE_HEIGHT`], where `HashLib.offerTreeTypeHash` reverts
215/// `TreeTooHigh`.
216pub fn offer_tree_typehash(height: usize) -> Word {
217    assert!(
218        height <= MAX_TREE_HEIGHT,
219        "tree height {height} exceeds {MAX_TREE_HEIGHT} (HashLib.offerTreeTypeHash reverts TreeTooHigh)"
220    );
221    let mut field = String::from("OfferTree(Offer");
222    for _ in 0..height {
223        field.push_str("[2]");
224    }
225    field.push_str(" offerTree)");
226    keccak(
227        [
228            field.as_str(),
229            COLLATERAL_PARAMS_TYPE,
230            MARKET_TYPE,
231            OFFER_TYPE,
232        ]
233        .concat()
234        .as_bytes(),
235    )
236}
237
238fn encode(words: &[Word]) -> Vec<u8> {
239    let mut out = Vec::with_capacity(words.len() * 32);
240    for w in words {
241        out.extend_from_slice(w);
242    }
243    out
244}
245
246pub fn hash_collateral_params(cp: &CollateralParams) -> Word {
247    keccak(&encode(&[
248        collateral_params_typehash(),
249        addr_word(&cp.token),
250        cp.lltv,
251        cp.liquidation_cursor,
252        addr_word(&cp.oracle),
253    ]))
254}
255
256/// Return a market with collateral parameters in canonical ascending token order.
257///
258/// Midnight identifies and hashes markets independently of the caller's input order. Keeping
259/// this normalization explicit is also useful before ABI encoding or persistence.
260pub fn canonical_market(market: &Market) -> Market {
261    let mut canonical = market.clone();
262    canonical
263        .collateral_params
264        .sort_by_key(|collateral| collateral.token);
265    canonical
266}
267
268pub fn hash_market(m: &Market) -> Word {
269    // collateralParamsHash = keccak256(abi.encodePacked(hashes))
270    let mut packed = Vec::with_capacity(m.collateral_params.len() * 32);
271    let mut collateral_params: Vec<&CollateralParams> = m.collateral_params.iter().collect();
272    collateral_params.sort_by_key(|collateral| collateral.token);
273    for cp in collateral_params {
274        packed.extend_from_slice(&hash_collateral_params(cp));
275    }
276    let cp_hash = keccak(&packed);
277    keccak(&encode(&[
278        market_typehash(),
279        m.chain_id,
280        addr_word(&m.midnight),
281        addr_word(&m.loan_token),
282        cp_hash,
283        m.maturity,
284        m.rcf_threshold,
285        addr_word(&m.enter_gate),
286        addr_word(&m.liquidator_gate),
287    ]))
288}
289
290/// Compute the deterministic Midnight market id (`IdLib.toId`).
291///
292/// The id is the CREATE2 address word for the SSTORE2 pointer that contains the canonical ABI
293/// encoding of the market parameters. It is returned as the protocol's 32-byte market id rather
294/// than truncated to an Ethereum address.
295pub fn market_id(market: &Market) -> Word {
296    const SSTORE2_PREFIX: [u8; 11] = [
297        0x60, 0x0b, 0x38, 0x03, 0x80, 0x60, 0x0b, 0x5f, 0x39, 0x5f, 0xf3,
298    ];
299
300    let canonical = canonical_market(market);
301    let encoded = encode_market_params(&canonical);
302    let mut init_code = Vec::with_capacity(SSTORE2_PREFIX.len() + encoded.len());
303    init_code.extend_from_slice(&SSTORE2_PREFIX);
304    init_code.extend_from_slice(&encoded);
305    let creation_hash = keccak(&init_code);
306
307    let mut create2 = Vec::with_capacity(1 + 20 + 32 + 32);
308    create2.push(0xff);
309    create2.extend_from_slice(&canonical.midnight);
310    create2.extend_from_slice(&[0u8; 32]);
311    create2.extend_from_slice(&creation_hash);
312    keccak(&create2)
313}
314
315/// EIP-712 struct hash of an Offer - this is the Merkle leaf. Mirrors `HashLib.hashOffer`.
316pub fn hash_offer(o: &Offer) -> Word {
317    keccak(&encode(&[
318        offer_typehash(),
319        hash_market(&o.market),
320        bool_word(o.buy),
321        addr_word(&o.maker),
322        o.start,
323        o.expiry,
324        o.tick,
325        o.group,
326        addr_word(&o.callback),
327        keccak(&o.callback_data),
328        addr_word(&o.receiver_if_maker_is_seller),
329        addr_word(&o.ratifier),
330        bool_word(o.reduce_only),
331        u128_word(o.max_units),
332        u128_word(o.max_assets),
333        o.continuous_fee_cap,
334    ]))
335}
336
337/// Hash offers with `group = 0`, sort those hashes, and hash their concatenation.
338///
339/// This is the deterministic consumption-group id used by the maker-side router.
340pub fn offer_group_id(offers: &[Offer]) -> Result<Word, GroupError> {
341    if offers.is_empty() {
342        return Err(GroupError::Empty);
343    }
344    let mut hashes: Vec<Word> = offers
345        .iter()
346        .map(|offer| {
347            let mut zero_group = offer.clone();
348            zero_group.group = [0u8; 32];
349            hash_offer(&zero_group)
350        })
351        .collect();
352    hashes.sort_unstable();
353    let mut packed = Vec::with_capacity(hashes.len() * 32);
354    for hash in hashes {
355        packed.extend_from_slice(&hash);
356    }
357    Ok(keccak(&packed))
358}
359
360/// A validated set of offers sharing one content-addressed consumption group.
361#[derive(Clone, Debug, PartialEq, Eq)]
362pub struct OfferGroup {
363    pub id: Word,
364    pub offers: Vec<Offer>,
365}
366
367impl OfferGroup {
368    pub fn create(mut offers: Vec<Offer>) -> Result<Self, GroupError> {
369        let first = offers.first().ok_or(GroupError::Empty)?;
370        let maker = first.maker;
371        let buy = first.buy;
372        let loan_token = first.market.loan_token;
373        let chain_id = first.market.chain_id;
374        let midnight = first.market.midnight;
375        let max_units = first.max_units;
376        let max_assets = first.max_assets;
377
378        for offer in &offers {
379            if offer.maker != maker {
380                return Err(GroupError::MakerMismatch);
381            }
382            if offer.buy != buy {
383                return Err(GroupError::SideMismatch);
384            }
385            if offer.market.loan_token != loan_token {
386                return Err(GroupError::LoanTokenMismatch);
387            }
388            if offer.market.chain_id != chain_id {
389                return Err(GroupError::ChainIdMismatch);
390            }
391            if offer.market.midnight != midnight {
392                return Err(GroupError::MidnightMismatch);
393            }
394            if (offer.max_units == 0) == (offer.max_assets == 0) {
395                return Err(GroupError::InvalidCap);
396            }
397            if offer.max_units != max_units || offer.max_assets != max_assets {
398                return Err(GroupError::CapMismatch);
399            }
400            if offer.buy && offer.receiver_if_maker_is_seller != [0u8; 20] {
401                return Err(GroupError::BuyReceiverNotZero);
402            }
403        }
404
405        let id = offer_group_id(&offers)?;
406        for offer in &mut offers {
407            offer.group = id;
408        }
409        Ok(Self { id, offers })
410    }
411}
412
413/// One entry in a canonical offer tree: either a standalone offer or an explicit shared group.
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub enum OfferTreeEntry {
416    Offer(Box<Offer>),
417    Group(OfferGroup),
418}
419
420impl From<Offer> for OfferTreeEntry {
421    fn from(offer: Offer) -> Self {
422        Self::Offer(Box::new(offer))
423    }
424}
425
426impl From<OfferGroup> for OfferTreeEntry {
427    fn from(group: OfferGroup) -> Self {
428        Self::Group(group)
429    }
430}
431
432/// The protocol-zero offer used to pad non-power-of-two Merkle trees.
433pub fn empty_offer() -> Offer {
434    Offer {
435        market: Market {
436            chain_id: [0; 32],
437            midnight: [0; 20],
438            loan_token: [0; 20],
439            collateral_params: Vec::new(),
440            maturity: [0; 32],
441            rcf_threshold: [0; 32],
442            enter_gate: [0; 20],
443            liquidator_gate: [0; 20],
444        },
445        buy: false,
446        maker: [0; 20],
447        start: [0; 32],
448        expiry: [0; 32],
449        tick: [0; 32],
450        group: [0; 32],
451        callback: [0; 20],
452        callback_data: Vec::new(),
453        receiver_if_maker_is_seller: [0; 20],
454        ratifier: [0; 20],
455        reduce_only: false,
456        max_units: 0,
457        max_assets: 0,
458        continuous_fee_cap: [0; 32],
459    }
460}
461
462#[inline]
463pub fn hash_node(left: &Word, right: &Word) -> Word {
464    let mut buf = [0u8; 64];
465    buf[..32].copy_from_slice(left);
466    buf[32..].copy_from_slice(right);
467    keccak(&buf)
468}
469
470/// A perfect binary Merkle tree over offer leaves. `height` = log2(leaves.len()).
471#[derive(Clone, Debug, PartialEq, Eq)]
472pub struct OfferTree {
473    /// `levels[0]` = leaves, `levels[height]` = `[root]`
474    levels: Vec<Vec<Word>>,
475}
476
477/// A canonical tree plus the final offers in leaf order, including zero padding.
478#[derive(Clone, Debug, PartialEq, Eq)]
479pub struct OfferTreeDescriptor {
480    pub offers: Vec<Offer>,
481    pub tree: OfferTree,
482}
483
484impl OfferTree {
485    /// Build a perfect binary tree over `leaves`. Errors unless the count is a nonzero power of
486    /// two of at most `2^MAX_TREE_HEIGHT` - `HashLib.offerTreeTypeHash` reverts `TreeTooHigh`
487    /// above height 20, so a taller tree could never ratify.
488    pub fn build(leaves: Vec<Word>) -> Result<Self, TreeError> {
489        if leaves.is_empty() || !leaves.len().is_power_of_two() {
490            return Err(TreeError::NotPowerOfTwo(leaves.len()));
491        }
492        if leaves.len() > 1 << MAX_TREE_HEIGHT {
493            return Err(TreeError::TooHigh(leaves.len().trailing_zeros() as usize));
494        }
495        let mut levels = vec![leaves];
496        while levels.last().unwrap().len() > 1 {
497            let prev = levels.last().unwrap();
498            let next: Vec<Word> = prev
499                .as_chunks::<2>()
500                .0
501                .iter()
502                .map(|[left, right]| hash_node(left, right))
503                .collect();
504            levels.push(next);
505        }
506        Ok(OfferTree { levels })
507    }
508
509    /// Assign canonical group ids, append protocol-zero padding, and build the Merkle tree.
510    ///
511    /// Standalone offers each receive their own content-addressed group. Explicit
512    /// [`OfferGroup`] entries retain their shared group id.
513    pub fn from_entries<I, E>(entries: I) -> Result<OfferTreeDescriptor, OfferTreeError>
514    where
515        I: IntoIterator<Item = E>,
516        E: Into<OfferTreeEntry>,
517    {
518        let mut offers = Vec::new();
519        for entry in entries {
520            match entry.into() {
521                OfferTreeEntry::Offer(offer) => {
522                    offers.extend(OfferGroup::create(vec![*offer])?.offers);
523                }
524                OfferTreeEntry::Group(group) => {
525                    offers.extend(OfferGroup::create(group.offers)?.offers);
526                }
527            }
528        }
529        if offers.is_empty() {
530            return Err(OfferTreeError::Empty);
531        }
532
533        let first = &offers[0];
534        for offer in &offers[1..] {
535            if offer.market.chain_id != first.market.chain_id {
536                return Err(OfferTreeError::ChainIdMismatch);
537            }
538            if offer.market.midnight != first.market.midnight {
539                return Err(OfferTreeError::MidnightMismatch);
540            }
541            if offer.ratifier != first.ratifier {
542                return Err(OfferTreeError::RatifierMismatch);
543            }
544        }
545
546        let mut seen = HashSet::with_capacity(offers.len());
547        for offer in &offers {
548            if !seen.insert(hash_offer(offer)) {
549                return Err(OfferTreeError::DuplicateOffer);
550            }
551        }
552
553        let padded_len = offers.len().next_power_of_two();
554        offers.resize_with(padded_len, empty_offer);
555        let tree = Self::build(offers.iter().map(hash_offer).collect())?;
556        Ok(OfferTreeDescriptor { offers, tree })
557    }
558
559    pub fn height(&self) -> usize {
560        self.levels.len() - 1
561    }
562    pub fn root(&self) -> Word {
563        self.levels.last().unwrap()[0]
564    }
565
566    /// Merkle proof for the leaf at `index`, sibling per level (matches HashLib.isLeaf order).
567    pub fn proof(&self, index: usize) -> Result<Vec<Word>, TreeError> {
568        let leaves = self.levels[0].len();
569        if index >= leaves {
570            return Err(TreeError::LeafIndexOutOfRange { index, leaves });
571        }
572        let mut proof = Vec::with_capacity(self.height());
573        let mut idx = index;
574        for level in &self.levels[..self.height()] {
575            let sib = idx ^ 1;
576            proof.push(level[sib]);
577            idx >>= 1;
578        }
579        Ok(proof)
580    }
581}
582
583/// Recompute the root from a leaf + proof, exactly as `HashLib.isLeaf` does on-chain -
584/// including its leaf-index range check, whose `LeafIndexOutOfRange` revert is `false` here.
585pub fn verify_leaf(root: &Word, leaf: &Word, leaf_index: usize, proof: &[Word]) -> bool {
586    if proof.len() > MAX_TREE_HEIGHT {
587        return false;
588    }
589    // HashLib.isLeaf requires leafIndex >> proof.length == 0: high bits beyond the proof are
590    // never consumed by the fold, so an index outside [0, 2^len) could otherwise "verify"
591    // off-chain and then revert on-chain. (A shift past usize::BITS - well-defined on the
592    // contract's uint256 - always yields 0, i.e. in range.)
593    if leaf_index.checked_shr(proof.len() as u32).unwrap_or(0) != 0 {
594        return false;
595    }
596    let mut cur = *leaf;
597    for (i, sib) in proof.iter().enumerate() {
598        cur = if leaf_index.checked_shr(i as u32).unwrap_or(0) & 1 == 0 {
599            hash_node(&cur, sib)
600        } else {
601            hash_node(sib, &cur)
602        };
603    }
604    cur == *root
605}
606
607pub fn domain_separator(chain_id: Word, ratifier: &Address) -> Word {
608    keccak(&encode(&[
609        keccak(EIP712_DOMAIN_TYPE.as_bytes()),
610        chain_id,
611        addr_word(ratifier),
612    ]))
613}
614
615/// The digest the maker signs for a whole tree (one signature covers every offer in it).
616///
617/// Panics if `height` exceeds [`MAX_TREE_HEIGHT`] (via [`offer_tree_typehash`]).
618pub fn tree_digest(root: Word, height: usize, chain_id: Word, ratifier: &Address) -> Word {
619    let struct_hash = keccak(&encode(&[offer_tree_typehash(height), root]));
620    let mut buf = Vec::with_capacity(2 + 64);
621    buf.extend_from_slice(&[0x19, 0x01]);
622    buf.extend_from_slice(&domain_separator(chain_id, ratifier));
623    buf.extend_from_slice(&struct_hash);
624    keccak(&buf)
625}
626
627#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
628pub struct Sig {
629    pub r: Word,
630    pub s: Word,
631    pub v: u8,
632}
633
634/// Sign the tree digest with the maker's key (secp256k1, in-process - no wallet round-trip).
635pub fn sign_digest(sk: &SigningKey, digest: &Word) -> Sig {
636    let (sig, rec): (EcdsaSig, RecoveryId) = sk.sign_prehash_recoverable(digest).expect("sign");
637    let b = sig.to_bytes();
638    let mut r = [0u8; 32];
639    let mut s = [0u8; 32];
640    r.copy_from_slice(&b[..32]);
641    s.copy_from_slice(&b[32..]);
642    Sig {
643        r,
644        s,
645        v: 27 + rec.to_byte(),
646    }
647}
648
649/// The Ethereum address of a public key: last 20 bytes of keccak(uncompressed pubkey without the 0x04 tag).
650fn address_of(vk: &VerifyingKey) -> Address {
651    let point = vk.to_encoded_point(false);
652    let h = keccak(&point.as_bytes()[1..]); // drop the 0x04 prefix
653    let mut a = [0u8; 20];
654    a.copy_from_slice(&h[12..]);
655    a
656}
657
658/// The Ethereum address controlled by a signing key - the `maker` address to put in offers.
659pub fn signer_address(sk: &SigningKey) -> Address {
660    address_of(sk.verifying_key())
661}
662
663/// Recover the signer address from a digest and signature, exactly as the `ecrecover` in
664/// `EcrecoverRatifier.isRatified`. Returns `None` for a malformed signature (the on-chain
665/// equivalent of `ecrecover` yielding `address(0)`).
666///
667/// Like the precompile, this accepts a high-`s` (malleable) signature: `ecrecover` takes any
668/// `s` in `[1, n-1]` with no low-`s` guard, so the malleated counterpart `(r, n - s, flipped v)`
669/// of a valid signature recovers the same address. Detect such signatures with [`is_high_s`].
670pub fn recover(digest: &Word, sig: &Sig) -> Option<Address> {
671    // v is 27/28 on-chain (anything else makes ecrecover yield address(0)); RecoveryId wants 0/1.
672    if sig.v != 27 && sig.v != 28 {
673        return None;
674    }
675    let mut rec = RecoveryId::from_byte(sig.v - 27)?;
676    let mut rs = [0u8; 64];
677    rs[..32].copy_from_slice(&sig.r);
678    rs[32..].copy_from_slice(&sig.s);
679    let mut ecdsa = EcdsaSig::from_slice(&rs).ok()?;
680    // `ecrecover` accepts any `s` in [1, n-1], but k256 rejects high-`s`. Substituting `n - s`
681    // negates the signature, which flips the parity of the point `ecrecover` reconstructs from
682    // `r`, so normalizing to low-`s` AND flipping the recovery id recovers exactly the address
683    // the precompile returns for the original `(r, s, v)`.
684    if let Some(low) = ecdsa.normalize_s() {
685        ecdsa = low;
686        rec = RecoveryId::from_byte(rec.to_byte() ^ 1)?;
687    }
688    let vk = VerifyingKey::recover_from_prehash(digest, &ecdsa, rec).ok()?;
689    Some(address_of(&vk))
690}
691
692/// Full off-chain mirror of `EcrecoverRatifier.isRatified` (minus the `isAuthorized` /
693/// `isRootCanceled` lookups, which need chain state). Recomputes the leaf, checks the Merkle
694/// proof, rebuilds the digest, recovers the signer, and confirms it is `expected_maker`.
695///
696/// If this returns `true`, a `take` carrying `(sig, root, leaf_index, proof)` will pass the
697/// ratifier as long as `expected_maker` is (or is authorized by) `offer.maker` on-chain.
698#[allow(clippy::too_many_arguments)]
699pub fn verify(
700    offer: &Offer,
701    root: &Word,
702    leaf_index: usize,
703    proof: &[Word],
704    sig: &Sig,
705    chain_id: Word,
706    ratifier: &Address,
707    expected_maker: &Address,
708) -> bool {
709    // The contract verifies the signature in the offer's own chain/ratifier domain. Reject a
710    // caller-supplied domain that is inconsistent with those signed offer fields.
711    if chain_id != offer.market.chain_id || *ratifier != offer.ratifier {
712        return false;
713    }
714    // The ratifier calls HashLib.offerTreeTypeHash(proof.length), which reverts TreeTooHigh
715    // above MAX_TREE_HEIGHT - such a take can never pass (and there is no digest to rebuild).
716    if proof.len() > MAX_TREE_HEIGHT {
717        return false;
718    }
719    let leaf = hash_offer(offer);
720    if !verify_leaf(root, &leaf, leaf_index, proof) {
721        return false;
722    }
723    let digest = tree_digest(*root, proof.len(), chain_id, ratifier);
724    recover(&digest, sig).as_ref() == Some(expected_maker)
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    fn word_u64(x: u64) -> Word {
732        let mut w = [0u8; 32];
733        w[24..].copy_from_slice(&x.to_be_bytes());
734        w
735    }
736
737    fn tiny_offer(maker: Address, i: u64) -> Offer {
738        let market = Market {
739            chain_id: word_u64(1),
740            midnight: [0x11; 20],
741            loan_token: [0x22; 20],
742            collateral_params: vec![CollateralParams {
743                token: [0x33; 20],
744                lltv: word_u64(860_000_000_000_000_000),
745                liquidation_cursor: word_u64(1),
746                oracle: [0x44; 20],
747            }],
748            maturity: word_u64(1_800_000_000),
749            rcf_threshold: word_u64(1000),
750            enter_gate: [0u8; 20],
751            liquidator_gate: [0u8; 20],
752        };
753        Offer {
754            market,
755            buy: i % 2 == 0,
756            maker,
757            start: word_u64(0),
758            expiry: word_u64(2_000_000_000),
759            tick: word_u64(i % 6744),
760            group: word_u64(i),
761            callback: [0u8; 20],
762            callback_data: Vec::new(),
763            receiver_if_maker_is_seller: [0u8; 20],
764            ratifier: [0xbb; 20],
765            reduce_only: false,
766            max_units: 1_000_000 + i as u128,
767            max_assets: 0,
768            continuous_fee_cap: word_u64(0),
769        }
770    }
771
772    #[test]
773    fn recover_returns_the_signer() {
774        let sk = SigningKey::from_bytes(&[0x42u8; 32].into()).unwrap();
775        let maker = signer_address(&sk);
776        let digest = keccak(b"any 32-byte digest goes here....");
777        let sig = sign_digest(&sk, &digest);
778        assert_eq!(recover(&digest, &sig), Some(maker));
779    }
780
781    #[test]
782    fn verify_accepts_a_valid_offer_signature() {
783        let sk = SigningKey::from_bytes(&[0x07u8; 32].into()).unwrap();
784        let maker = signer_address(&sk);
785        let ratifier = [0xbbu8; 20];
786        let chain_id = word_u64(1);
787
788        let offers: Vec<Offer> = (0..4).map(|i| tiny_offer(maker, i)).collect();
789        let leaves: Vec<Word> = offers.iter().map(hash_offer).collect();
790        let tree = OfferTree::build(leaves).unwrap();
791        let digest = tree_digest(tree.root(), tree.height(), chain_id, &ratifier);
792        let sig = sign_digest(&sk, &digest);
793
794        // Every leaf verifies with its own proof.
795        for (i, offer) in offers.iter().enumerate() {
796            assert!(
797                verify(
798                    offer,
799                    &tree.root(),
800                    i,
801                    &tree.proof(i).unwrap(),
802                    &sig,
803                    chain_id,
804                    &ratifier,
805                    &maker
806                ),
807                "leaf {i} should verify"
808            );
809        }
810    }
811
812    #[test]
813    fn verify_rejects_wrong_maker() {
814        let sk = SigningKey::from_bytes(&[0x07u8; 32].into()).unwrap();
815        let maker = signer_address(&sk);
816        let ratifier = [0xbbu8; 20];
817        let chain_id = word_u64(1);
818
819        let offers: Vec<Offer> = (0..2).map(|i| tiny_offer(maker, i)).collect();
820        let tree = OfferTree::build(offers.iter().map(hash_offer).collect()).unwrap();
821        let digest = tree_digest(tree.root(), tree.height(), chain_id, &ratifier);
822        let sig = sign_digest(&sk, &digest);
823
824        let not_maker = [0x99u8; 20];
825        assert!(!verify(
826            &offers[0],
827            &tree.root(),
828            0,
829            &tree.proof(0).unwrap(),
830            &sig,
831            chain_id,
832            &ratifier,
833            &not_maker
834        ));
835    }
836
837    #[test]
838    fn verify_rejects_tampered_offer_and_proof() {
839        let sk = SigningKey::from_bytes(&[0x07u8; 32].into()).unwrap();
840        let maker = signer_address(&sk);
841        let ratifier = [0xbbu8; 20];
842        let chain_id = word_u64(1);
843
844        let offers: Vec<Offer> = (0..4).map(|i| tiny_offer(maker, i)).collect();
845        let tree = OfferTree::build(offers.iter().map(hash_offer).collect()).unwrap();
846        let digest = tree_digest(tree.root(), tree.height(), chain_id, &ratifier);
847        let sig = sign_digest(&sk, &digest);
848
849        // Tampered offer (different tick) no longer hashes to the signed leaf -> proof fails.
850        let mut tampered = offers[0].clone();
851        tampered.tick = word_u64(999);
852        assert!(!verify(
853            &tampered,
854            &tree.root(),
855            0,
856            &tree.proof(0).unwrap(),
857            &sig,
858            chain_id,
859            &ratifier,
860            &maker
861        ));
862
863        // Right offer, wrong leaf index -> proof fails.
864        assert!(!verify(
865            &offers[0],
866            &tree.root(),
867            1,
868            &tree.proof(1).unwrap(),
869            &sig,
870            chain_id,
871            &ratifier,
872            &maker
873        ));
874
875        // Wrong chain id -> different digest -> recovers a different address.
876        assert!(!verify(
877            &offers[0],
878            &tree.root(),
879            0,
880            &tree.proof(0).unwrap(),
881            &sig,
882            word_u64(999),
883            &ratifier,
884            &maker
885        ));
886    }
887
888    #[test]
889    fn verify_rejects_a_valid_signature_for_a_domain_not_bound_to_the_offer() {
890        let sk = SigningKey::from_bytes(&[0x07u8; 32].into()).unwrap();
891        let maker = signer_address(&sk);
892        let offer = tiny_offer(maker, 0);
893        let tree = OfferTree::build(vec![hash_offer(&offer)]).unwrap();
894
895        let wrong_chain = word_u64(999);
896        let chain_sig = sign_digest(
897            &sk,
898            &tree_digest(tree.root(), tree.height(), wrong_chain, &offer.ratifier),
899        );
900        assert!(!verify(
901            &offer,
902            &tree.root(),
903            0,
904            &[],
905            &chain_sig,
906            wrong_chain,
907            &offer.ratifier,
908            &maker,
909        ));
910
911        let wrong_ratifier = [0xcc; 20];
912        let ratifier_sig = sign_digest(
913            &sk,
914            &tree_digest(
915                tree.root(),
916                tree.height(),
917                offer.market.chain_id,
918                &wrong_ratifier,
919            ),
920        );
921        assert!(!verify(
922            &offer,
923            &tree.root(),
924            0,
925            &[],
926            &ratifier_sig,
927            offer.market.chain_id,
928            &wrong_ratifier,
929            &maker,
930        ));
931    }
932
933    #[test]
934    #[should_panic(expected = "TreeTooHigh")]
935    fn offer_tree_typehash_panics_above_max_height() {
936        // HashLib.offerTreeTypeHash has constants for heights 0-20 only and reverts TreeTooHigh
937        // above that; there is no typehash to compute.
938        offer_tree_typehash(MAX_TREE_HEIGHT + 1);
939    }
940
941    #[test]
942    fn verify_leaf_rejects_out_of_range_leaf_index() {
943        // HashLib.isLeaf reverts LeafIndexOutOfRange unless leafIndex >> proof.length == 0.
944        // Index 2 folds exactly like index 0 on a height-1 tree (bit 1 is never consumed), so
945        // without the range check it would "verify" off-chain and then revert on-chain.
946        let leaves = vec![keccak(b"a"), keccak(b"b")];
947        let tree = OfferTree::build(leaves.clone()).unwrap();
948        let proof = tree.proof(0).unwrap();
949        assert!(verify_leaf(&tree.root(), &leaves[0], 0, &proof));
950        assert!(!verify_leaf(&tree.root(), &leaves[0], 2, &proof));
951
952        // Same on the single-leaf tree (empty proof): any nonzero index is out of range.
953        let one = OfferTree::build(vec![keccak(b"a")]).unwrap();
954        assert!(verify_leaf(&one.root(), &keccak(b"a"), 0, &[]));
955        assert!(!verify_leaf(&one.root(), &keccak(b"a"), 1, &[]));
956    }
957
958    #[test]
959    fn verify_rejects_proofs_taller_than_max_height() {
960        let sk = SigningKey::from_bytes(&[0x07u8; 32].into()).unwrap();
961        let maker = signer_address(&sk);
962        let ratifier = [0xbbu8; 20];
963        let chain_id = word_u64(1);
964        let offer = tiny_offer(maker, 0);
965        let leaf = hash_offer(&offer);
966
967        // Fold the leaf up under zero siblings: a consistent (root, proof) of any height
968        // without materializing a 2^height tree.
969        let fold = |height: usize| {
970            let proof = vec![[0u8; 32]; height];
971            let mut root = leaf;
972            for sib in &proof {
973                root = hash_node(&root, sib);
974            }
975            (root, proof)
976        };
977
978        // Height 20 is the on-chain cap and still verifies end to end.
979        let (root, proof) = fold(MAX_TREE_HEIGHT);
980        let sig = sign_digest(
981            &sk,
982            &tree_digest(root, MAX_TREE_HEIGHT, chain_id, &ratifier),
983        );
984        assert!(verify(
985            &offer, &root, 0, &proof, &sig, chain_id, &ratifier, &maker
986        ));
987
988        // One level higher the ratifier reverts TreeTooHigh, so verify must say false
989        // (whatever the signature - there is no digest a maker could even sign).
990        let (root, proof) = fold(MAX_TREE_HEIGHT + 1);
991        assert!(!verify(
992            &offer, &root, 0, &proof, &sig, chain_id, &ratifier, &maker
993        ));
994    }
995
996    #[test]
997    fn recover_accepts_the_high_s_counterpart() {
998        // ecrecover has no low-s guard: the malleated (r, n - s, flipped v) of a valid
999        // signature recovers the same maker, and `recover` must mirror that.
1000        let sk = SigningKey::from_bytes(&[0x42u8; 32].into()).unwrap();
1001        let maker = signer_address(&sk);
1002        let digest = keccak(b"high-s malleability test digest.");
1003        let low = sign_digest(&sk, &digest);
1004
1005        let high = Sig {
1006            r: low.r,
1007            s: high_s_counterpart(&low.s),
1008            v: if low.v == 27 { 28 } else { 27 },
1009        };
1010        assert!(is_high_s(&high.s), "counterpart must be high-s");
1011        assert_eq!(recover(&digest, &high), Some(maker));
1012
1013        // The low-s original still recovers, unchanged.
1014        assert!(!is_high_s(&low.s));
1015        assert_eq!(recover(&digest, &low), Some(maker));
1016    }
1017
1018    #[test]
1019    fn recover_rejects_malformed_v() {
1020        let sk = SigningKey::from_bytes(&[0x42u8; 32].into()).unwrap();
1021        let digest = keccak(b"another 32-byte test digest....");
1022        let good = sign_digest(&sk, &digest);
1023        // Anything outside 27/28 makes ecrecover yield address(0), i.e. None here.
1024        for v in [0u8, 1, 26, 29, 31, 255] {
1025            let mut sig = good;
1026            sig.v = v;
1027            assert_eq!(recover(&digest, &sig), None, "v = {v} must be rejected");
1028        }
1029    }
1030}