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
7pub mod convert;
9pub use convert::*;
10
11pub mod builder;
13pub use builder::*;
14
15pub mod validate;
17pub use validate::*;
18
19pub mod sim;
21pub use sim::*;
22
23pub mod decode;
25pub use decode::*;
26
27pub mod codec;
29pub use codec::*;
30
31pub mod signer;
33pub use signer::*;
34
35pub mod authorize;
37pub use authorize::*;
38
39pub mod sizing;
41pub use sizing::*;
42
43pub mod payload;
45pub use payload::*;
46
47pub mod submission;
49pub use submission::*;
50
51pub mod actions;
53pub use actions::*;
54
55pub mod requirements;
57pub use requirements::*;
58
59pub mod api;
61pub use api::*;
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
65pub enum TreeError {
66 #[error("leaf count must be a nonzero power of two, got {0}")]
68 NotPowerOfTwo(usize),
69 #[error("tree height must be at most 20, got {0}")]
72 TooHigh(usize),
73 #[error("leaf index {index} is out of range for {leaves} leaves")]
75 LeafIndexOutOfRange { index: usize, leaves: usize },
76}
77
78#[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#[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
123pub 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
196pub 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}
210pub const MAX_TREE_HEIGHT: usize = 20;
213
214pub 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
256pub 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 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
290pub 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
315pub 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
337pub 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#[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#[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
432pub 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#[derive(Clone, Debug, PartialEq, Eq)]
472pub struct OfferTree {
473 levels: Vec<Vec<Word>>,
475}
476
477#[derive(Clone, Debug, PartialEq, Eq)]
479pub struct OfferTreeDescriptor {
480 pub offers: Vec<Offer>,
481 pub tree: OfferTree,
482}
483
484impl OfferTree {
485 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 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 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
583pub 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 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
615pub 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
634pub 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
649fn address_of(vk: &VerifyingKey) -> Address {
651 let point = vk.to_encoded_point(false);
652 let h = keccak(&point.as_bytes()[1..]); let mut a = [0u8; 20];
654 a.copy_from_slice(&h[12..]);
655 a
656}
657
658pub fn signer_address(sk: &SigningKey) -> Address {
660 address_of(sk.verifying_key())
661}
662
663pub fn recover(digest: &Word, sig: &Sig) -> Option<Address> {
671 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 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#[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 if chain_id != offer.market.chain_id || *ratifier != offer.ratifier {
712 return false;
713 }
714 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 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 ¬_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 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 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 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 offer_tree_typehash(MAX_TREE_HEIGHT + 1);
939 }
940
941 #[test]
942 fn verify_leaf_rejects_out_of_range_leaf_index() {
943 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 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 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 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 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 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 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 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}