Skip to main content

paralock_core/
evm.rs

1//! The EVM leg: `ParalockHTLC` and `ParalockAdaptorEscrow` swap structs, their ids and commitments
2//! (`keccak256(abi.encode(swap))`), the adaptor escrow's claim digest, hashlocks, canonical `(v, r, s)` claim
3//! signatures and EVM addresses. A port of `@paralock/evm`'s pure helpers with hand-written ABI encoding (every field
4//! is a static 32-byte word).
5
6use core::cmp::Ordering;
7use core::fmt;
8use core::str::FromStr;
9
10use sha2::{Digest, Sha256};
11use sha3::Keccak256;
12
13use crate::adaptor;
14use crate::encoding::{self, EncodingError};
15use crate::keys::{CURVE_ORDER_HALF, KeyError, PublicKey, Signature, is_low};
16
17/// `keccak256("ParalockAdaptorClaim")`: the claim digest's domain tag (`CLAIM_TYPEHASH` in
18/// `ParalockAdaptorEscrow.sol`).
19pub const CLAIM_TYPEHASH: [u8; 32] = [
20    0x76, 0x49, 0x65, 0x52, 0xa5, 0xd8, 0x92, 0xc5, 0xce, 0x0b, 0x63, 0xbf, 0x3f, 0x56, 0x93, 0x57, 0xdd, 0x16, 0xc2,
21    0x40, 0x0a, 0xc0, 0x5d, 0x3c, 0x20, 0xbc, 0xab, 0xb4, 0x5b, 0x6a, 0x64, 0xca,
22];
23
24/// `ParalockHTLC.SETTLED`: the state word of a claimed or refunded hashlock.
25pub const SETTLED_STATE: [u8; 32] = {
26    let mut word = [0u8; 32];
27    word[31] = 1;
28    word
29};
30/// The state word of a hashlock that was never used.
31pub const EMPTY_STATE: [u8; 32] = [0u8; 32];
32
33/// An EVM encoding or signature error.
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35#[non_exhaustive]
36pub enum Error {
37    /// Malformed hex or length.
38    #[error(transparent)]
39    Encoding(#[from] EncodingError),
40    /// An invalid key or signature scalar.
41    #[error(transparent)]
42    Key(#[from] KeyError),
43    /// Not `0x` followed by 40 hex digits.
44    #[error("invalid EVM address")]
45    InvalidAddress,
46    /// A mixed-case address whose EIP-55 checksum is wrong.
47    #[error("invalid EVM address checksum")]
48    InvalidChecksum,
49    /// A decimal string that is not a 256-bit unsigned integer.
50    #[error("invalid uint256")]
51    InvalidUint,
52    /// `v` / `yParity` is not 0, 1, 27 or 28.
53    #[error("invalid signature recovery value {0}")]
54    InvalidRecoveryValue(u64),
55    /// `s` is zero or not below the curve order.
56    #[error("signature s is out of range")]
57    SignatureOutOfRange,
58    /// High `s` or `v` not in {27, 28}: the contract reverts with `MalleableSignature`.
59    #[error("signature is not canonical (high s or v not 27/28)")]
60    MalleableSignature,
61    /// The signature does not recover to a key (`ecrecover` would return the zero address).
62    #[error("signature does not recover to a public key")]
63    InvalidSignature,
64    /// An escrow status outside 0, 1, 2.
65    #[error("unknown adaptor escrow status {0}")]
66    UnknownStatus(u8),
67}
68
69/// `keccak256(data)`.
70pub fn keccak256(data: &[u8]) -> [u8; 32] {
71    Keccak256::digest(data).into()
72}
73
74/// `sha256(preimage)`: the hashlock of a 32-byte secret. Matches the contract and the Zcash script's `OP_SHA256`.
75pub fn hashlock_of(preimage: &[u8; 32]) -> [u8; 32] {
76    Sha256::digest(preimage).into()
77}
78
79// ---------------------------------------------------------------------------------------------------------------
80// Addresses
81
82/// A 20-byte EVM address.
83#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
84pub struct EvmAddress(pub [u8; 20]);
85
86impl EvmAddress {
87    /// The zero address.
88    pub const ZERO: Self = Self([0u8; 20]);
89
90    /// Parses `0x` + 40 hex digits. All-lowercase and all-uppercase digits are taken as they are; mixed case must be
91    /// a valid EIP-55 checksum ([`Error::InvalidChecksum`] otherwise), so a mistyped checksummed address is caught.
92    /// Use this (or `str::parse`) for addresses a person entered or configured: recipients, tokens, escrows.
93    pub fn parse(s: &str) -> Result<Self, Error> {
94        let address = Self::parse_any_case(s)?;
95        let digits = s.get(2..).unwrap_or("");
96        let mixed = digits.bytes().any(|b| b.is_ascii_uppercase()) && digits.bytes().any(|b| b.is_ascii_lowercase());
97        if mixed && address.to_checksum() != s {
98            return Err(Error::InvalidChecksum);
99        }
100        Ok(address)
101    }
102
103    /// Parses `0x` + 40 hex digits in any letter case, **without** checking an EIP-55 checksum, like the TypeScript
104    /// `isEvmAddress`. For values received in protocol messages, which the reference accepts in any case; prefer
105    /// [`Self::parse`] for anything a person typed.
106    pub fn parse_any_case(s: &str) -> Result<Self, Error> {
107        let digits = s.strip_prefix("0x").ok_or(Error::InvalidAddress)?;
108        if digits.len() != 40 || !digits.bytes().all(|b| b.is_ascii_hexdigit()) {
109            return Err(Error::InvalidAddress);
110        }
111        Ok(Self(encoding::decode_hex_array(digits, "address").map_err(|_| Error::InvalidAddress)?))
112    }
113
114    /// The address of a secp256k1 public key: the last 20 bytes of `keccak256(x ‖ y)` (`evmAddressOf`).
115    pub fn from_public_key(public_key: &PublicKey) -> Self {
116        let uncompressed = public_key.to_uncompressed();
117        let hash = keccak256(&uncompressed[1..]);
118        let mut out = [0u8; 20];
119        out.copy_from_slice(&hash[12..]);
120        Self(out)
121    }
122
123    /// True for the zero address.
124    pub fn is_zero(&self) -> bool {
125        self.0 == [0u8; 20]
126    }
127
128    /// Lowercase `0x` hex.
129    pub fn to_hex(&self) -> String {
130        encoding::to_hex_0x(&self.0)
131    }
132
133    /// EIP-55 mixed-case checksum encoding.
134    pub fn to_checksum(&self) -> String {
135        let lower = encoding::to_hex(&self.0);
136        let hash = keccak256(lower.as_bytes());
137        let mut out = String::with_capacity(42);
138        out.push_str("0x");
139        for (i, c) in lower.chars().enumerate() {
140            let nibble = (hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 })) & 0x0f;
141            out.push(if c.is_ascii_alphabetic() && nibble >= 8 { c.to_ascii_uppercase() } else { c });
142        }
143        out
144    }
145
146    /// The address as an ABI word (left-padded to 32 bytes).
147    pub fn to_word(&self) -> [u8; 32] {
148        let mut word = [0u8; 32];
149        word[12..].copy_from_slice(&self.0);
150        word
151    }
152}
153
154impl fmt::Display for EvmAddress {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.write_str(&self.to_hex())
157    }
158}
159
160impl fmt::Debug for EvmAddress {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "EvmAddress({})", self.to_hex())
163    }
164}
165
166impl FromStr for EvmAddress {
167    type Err = Error;
168
169    /// [`EvmAddress::parse`]: mixed-case input must carry a valid EIP-55 checksum.
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Self::parse(s)
172    }
173}
174
175/// `evmAddressOf`: the EVM address of a public key.
176pub fn evm_address_of(public_key: &PublicKey) -> EvmAddress {
177    EvmAddress::from_public_key(public_key)
178}
179
180// ---------------------------------------------------------------------------------------------------------------
181// uint256
182
183/// A 256-bit unsigned integer (an ABI `uint256`), stored big-endian. Token amounts and chain ids use it.
184#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
185pub struct U256([u8; 32]);
186
187impl U256 {
188    /// Zero.
189    pub const ZERO: Self = Self([0u8; 32]);
190
191    /// From big-endian bytes.
192    pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
193        Self(bytes)
194    }
195
196    /// Big-endian bytes (the ABI word).
197    pub const fn to_be_bytes(&self) -> [u8; 32] {
198        self.0
199    }
200
201    /// From a `u128`.
202    pub fn from_u128(n: u128) -> Self {
203        let mut out = [0u8; 32];
204        out[16..].copy_from_slice(&n.to_be_bytes());
205        Self(out)
206    }
207
208    /// The value as a `u128`, if it fits.
209    pub fn to_u128(&self) -> Option<u128> {
210        if self.0[..16].iter().any(|&b| b != 0) {
211            return None;
212        }
213        let mut low = [0u8; 16];
214        low.copy_from_slice(&self.0[16..]);
215        Some(u128::from_be_bytes(low))
216    }
217
218    /// True for zero.
219    pub fn is_zero(&self) -> bool {
220        self.0 == [0u8; 32]
221    }
222
223    fn limbs(&self) -> [u64; 4] {
224        let mut limbs = [0u64; 4];
225        for (i, limb) in limbs.iter_mut().enumerate() {
226            let mut b = [0u8; 8];
227            b.copy_from_slice(&self.0[24 - 8 * i..32 - 8 * i]);
228            *limb = u64::from_be_bytes(b);
229        }
230        limbs
231    }
232
233    fn from_limbs(limbs: [u64; 4]) -> Self {
234        let mut out = [0u8; 32];
235        for (i, limb) in limbs.iter().enumerate() {
236            out[24 - 8 * i..32 - 8 * i].copy_from_slice(&limb.to_be_bytes());
237        }
238        Self(out)
239    }
240
241    /// Parses a decimal string of 1 to 78 digits (no sign, no separators).
242    pub fn from_dec_str(s: &str) -> Result<Self, Error> {
243        if s.is_empty() || s.len() > 78 || !s.bytes().all(|b| b.is_ascii_digit()) {
244            return Err(Error::InvalidUint);
245        }
246        let mut limbs = [0u64; 4];
247        for digit in s.bytes() {
248            let mut carry = u128::from(digit - b'0');
249            for limb in &mut limbs {
250                let v = u128::from(*limb) * 10 + carry;
251                *limb = v as u64;
252                carry = v >> 64;
253            }
254            if carry != 0 {
255                return Err(Error::InvalidUint);
256            }
257        }
258        Ok(Self::from_limbs(limbs))
259    }
260
261    /// The decimal string.
262    pub fn to_dec_string(&self) -> String {
263        const CHUNK: u64 = 10_000_000_000_000_000_000; // 10^19
264        let mut limbs = self.limbs();
265        let mut chunks = Vec::new();
266        loop {
267            let mut rem: u128 = 0;
268            for limb in limbs.iter_mut().rev() {
269                let cur = (rem << 64) | u128::from(*limb);
270                *limb = (cur / u128::from(CHUNK)) as u64;
271                rem = cur % u128::from(CHUNK);
272            }
273            chunks.push(rem as u64);
274            if limbs.iter().all(|&l| l == 0) {
275                break;
276            }
277        }
278        let mut out = String::new();
279        for (i, chunk) in chunks.iter().rev().enumerate() {
280            if i == 0 {
281                out.push_str(&chunk.to_string());
282            } else {
283                out.push_str(&format!("{chunk:019}"));
284            }
285        }
286        out
287    }
288}
289
290impl From<u128> for U256 {
291    fn from(n: u128) -> Self {
292        Self::from_u128(n)
293    }
294}
295
296impl From<u64> for U256 {
297    fn from(n: u64) -> Self {
298        Self::from_u128(u128::from(n))
299    }
300}
301
302impl PartialOrd for U256 {
303    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
304        Some(self.cmp(other))
305    }
306}
307
308impl Ord for U256 {
309    fn cmp(&self, other: &Self) -> Ordering {
310        self.0.cmp(&other.0)
311    }
312}
313
314impl fmt::Display for U256 {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        f.write_str(&self.to_dec_string())
317    }
318}
319
320impl fmt::Debug for U256 {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        write!(f, "U256({})", self.to_dec_string())
323    }
324}
325
326impl FromStr for U256 {
327    type Err = Error;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        Self::from_dec_str(s)
331    }
332}
333
334fn u64_word(n: u64) -> [u8; 32] {
335    let mut word = [0u8; 32];
336    word[24..].copy_from_slice(&n.to_be_bytes());
337    word
338}
339
340// ---------------------------------------------------------------------------------------------------------------
341// Swaps
342
343/// On-chain parameters of a `ParalockHTLC` swap (`ParalockHTLC.Swap`).
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
345pub struct HtlcSwap {
346    /// sha256 of the 32-byte secret; also the Zcash HTLC's hashlock.
347    pub hashlock: [u8; 32],
348    /// Token held in escrow.
349    pub token: EvmAddress,
350    /// Locks the funds; refunded from `expiry` on (the maker).
351    pub sender: EvmAddress,
352    /// Paid on claim (the taker).
353    pub recipient: EvmAddress,
354    /// Token base units.
355    pub amount: U256,
356    /// Unix seconds from which the swap can only be refunded.
357    pub expiry: u64,
358}
359
360impl HtlcSwap {
361    /// `abi.encode(swap)`: six 32-byte words.
362    pub fn abi_encode(&self) -> [u8; 192] {
363        let mut out = [0u8; 192];
364        for (i, word) in [
365            self.hashlock,
366            self.token.to_word(),
367            self.sender.to_word(),
368            self.recipient.to_word(),
369            self.amount.to_be_bytes(),
370            u64_word(self.expiry),
371        ]
372        .iter()
373        .enumerate()
374        {
375            out[32 * i..32 * (i + 1)].copy_from_slice(word);
376        }
377        out
378    }
379
380    /// `keccak256(abi.encode(swap))`: the value the contract stores while the swap is active (`commitmentOf`).
381    pub fn commitment(&self) -> [u8; 32] {
382        keccak256(&self.abi_encode())
383    }
384}
385
386/// On-chain parameters of a `ParalockAdaptorEscrow` swap (`ParalockAdaptorEscrow.Swap`).
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
388pub struct AdaptorSwap {
389    /// Token held in escrow.
390    pub token: EvmAddress,
391    /// Locks the funds; refunded from `expiry` on (the maker).
392    pub sender: EvmAddress,
393    /// Paid on claim (the taker).
394    pub recipient: EvmAddress,
395    /// Address whose signature over `claim_digest(id)` releases the funds.
396    pub signer: EvmAddress,
397    /// Token base units.
398    pub amount: U256,
399    /// Unix seconds from which the swap can only be refunded.
400    pub expiry: u64,
401    /// Random 32 bytes that make the id unique and unlinkable.
402    pub salt: [u8; 32],
403}
404
405impl AdaptorSwap {
406    /// `abi.encode(swap)`: seven 32-byte words.
407    pub fn abi_encode(&self) -> [u8; 224] {
408        let mut out = [0u8; 224];
409        for (i, word) in [
410            self.token.to_word(),
411            self.sender.to_word(),
412            self.recipient.to_word(),
413            self.signer.to_word(),
414            self.amount.to_be_bytes(),
415            u64_word(self.expiry),
416            self.salt,
417        ]
418        .iter()
419        .enumerate()
420        {
421            out[32 * i..32 * (i + 1)].copy_from_slice(word);
422        }
423        out
424    }
425
426    /// `keccak256(abi.encode(swap))`: the swap id, exactly as the contract's `idOf` computes it.
427    pub fn id(&self) -> [u8; 32] {
428        keccak256(&self.abi_encode())
429    }
430}
431
432/// The swap id of an adaptor escrow swap (`adaptorSwapId` / `idOf`).
433pub fn adaptor_swap_id(swap: &AdaptorSwap) -> [u8; 32] {
434    swap.id()
435}
436
437/// The raw 32-byte digest the swap's signer signs (no EIP-191 prefix):
438/// `keccak256(abi.encode(CLAIM_TYPEHASH, chainId, contract, id))`. Bound to one chain and one contract.
439pub fn claim_digest(chain_id: u64, contract: &EvmAddress, id: &[u8; 32]) -> [u8; 32] {
440    keccak256(&claim_digest_preimage(chain_id, contract, id))
441}
442
443/// `abi.encode(CLAIM_TYPEHASH, chainId, contract, id)`: the 128 bytes [`claim_digest`] hashes.
444pub fn claim_digest_preimage(chain_id: u64, contract: &EvmAddress, id: &[u8; 32]) -> [u8; 128] {
445    let mut data = [0u8; 128];
446    data[..32].copy_from_slice(&CLAIM_TYPEHASH);
447    data[32..64].copy_from_slice(&u64_word(chain_id));
448    data[64..96].copy_from_slice(&contract.to_word());
449    data[96..].copy_from_slice(id);
450    data
451}
452
453// ---------------------------------------------------------------------------------------------------------------
454// Claim signatures
455
456/// A secp256k1 signature as `ParalockAdaptorEscrow.claim` takes it: `v` is 27 or 28 and `s` is low.
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
458pub struct ClaimSignature {
459    /// 27 or 28.
460    pub v: u8,
461    /// `r`, big-endian.
462    pub r: [u8; 32],
463    /// `s`, big-endian, at most `n/2`.
464    pub s: [u8; 32],
465}
466
467impl ClaimSignature {
468    /// The `(r, s)` pair as a signature (for [`adaptor::extract`]). Fails if a scalar is out of range.
469    pub fn signature(&self) -> Result<Signature, Error> {
470        Ok(Signature::from_scalars(self.r, self.s)?)
471    }
472}
473
474/// Normalizes a signature to the canonical form the contract accepts (`normalizeClaimSignature`): `v` in {27, 28} and
475/// low `s` (a high `s` becomes `n − s` with the recovery bit flipped, the same signature's other encoding).
476/// `v_or_parity` may be 0, 1, 27 or 28.
477pub fn normalize_claim_signature(r: [u8; 32], s: [u8; 32], v_or_parity: u64) -> Result<ClaimSignature, Error> {
478    let mut parity = if v_or_parity >= 27 { v_or_parity - 27 } else { v_or_parity };
479    if parity > 1 {
480        return Err(Error::InvalidRecoveryValue(v_or_parity));
481    }
482    // r is not range-checked here (the contract doesn't either); ecrecover rejects it.
483    let sig_s = Signature::from_scalars([1u8; 32], s).map_err(|_| Error::SignatureOutOfRange)?;
484    let low = sig_s.normalize_s();
485    if low.s() != s {
486        parity ^= 1;
487    }
488    Ok(ClaimSignature { v: 27 + parity as u8, r, s: low.s() })
489}
490
491/// The canonical claim signature of a (low-S) signature by `signer` on `digest`: `v = 27 + recovery id`.
492pub fn claim_signature(signature: &Signature, signer: &PublicKey, digest: &[u8; 32]) -> Result<ClaimSignature, Error> {
493    let sig = signature.normalize_s();
494    let id = adaptor::recovery_id(signer, digest, &sig).map_err(|_| Error::InvalidSignature)?;
495    Ok(ClaimSignature { v: 27 + id, r: sig.r(), s: sig.s() })
496}
497
498/// What `ParalockAdaptorEscrow.claim` checks before `ecrecover`, then `ecrecover` itself: the address that signed
499/// `digest`. Fails with [`Error::MalleableSignature`] exactly when the contract reverts with `MalleableSignature`.
500pub fn recover_signer(digest: &[u8; 32], sig: &ClaimSignature) -> Result<EvmAddress, Error> {
501    if !is_low(&sig.s) || (sig.v != 27 && sig.v != 28) {
502        return Err(Error::MalleableSignature);
503    }
504    let signature = Signature::from_scalars(sig.r, sig.s).map_err(|_| Error::InvalidSignature)?;
505    let key = adaptor::recover_public_key(digest, &signature, sig.v - 27).map_err(|_| Error::InvalidSignature)?;
506    Ok(EvmAddress::from_public_key(&key))
507}
508
509/// `n / 2`: the largest `s` the escrow accepts.
510pub const SECP256K1_N_HALF: [u8; 32] = CURVE_ORDER_HALF;
511
512// ---------------------------------------------------------------------------------------------------------------
513// Status
514
515/// Lifecycle of a hashlock (`ParalockHTLC`) or swap id (`ParalockAdaptorEscrow`). Never reusable once settled.
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
517pub enum EscrowStatus {
518    /// Never used.
519    Empty,
520    /// Locked, not yet claimed or refunded.
521    Active,
522    /// Claimed or refunded.
523    Settled,
524}
525
526/// Maps a raw `ParalockHTLC.stateOf` word to a status (0 empty, 1 settled, anything else active).
527pub fn htlc_status_of(raw: &[u8; 32]) -> EscrowStatus {
528    if *raw == EMPTY_STATE {
529        EscrowStatus::Empty
530    } else if *raw == SETTLED_STATE {
531        EscrowStatus::Settled
532    } else {
533        EscrowStatus::Active
534    }
535}
536
537/// Maps `ParalockAdaptorEscrow.statusOf` (0, 1, 2) to a status.
538pub fn adaptor_status_of(raw: u8) -> Result<EscrowStatus, Error> {
539    match raw {
540        0 => Ok(EscrowStatus::Empty),
541        1 => Ok(EscrowStatus::Active),
542        2 => Ok(EscrowStatus::Settled),
543        other => Err(Error::UnknownStatus(other)),
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
550    use super::*;
551
552    #[test]
553    fn claim_typehash_is_keccak_of_the_tag() {
554        assert_eq!(keccak256(b"ParalockAdaptorClaim"), CLAIM_TYPEHASH);
555    }
556
557    #[test]
558    fn u256_decimal_round_trips() {
559        for s in ["0", "1", "18446744073709551616", "1000000000000000000000000000000", &"9".repeat(77)] {
560            assert_eq!(U256::from_dec_str(s).unwrap().to_dec_string(), s);
561        }
562        let max = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
563        assert_eq!(U256::from_dec_str(max).unwrap().to_be_bytes(), [0xff; 32]);
564        assert_eq!(
565            U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639936"),
566            Err(Error::InvalidUint)
567        );
568        assert!(U256::from_dec_str("").is_err());
569        assert!(U256::from_dec_str("-1").is_err());
570        assert_eq!(U256::from(u128::MAX).to_u128(), Some(u128::MAX));
571    }
572
573    #[test]
574    fn addresses_parse_and_checksum() {
575        // EIP-55 test vectors.
576        for good in [
577            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
578            "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
579            "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
580            "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
581        ] {
582            let a = EvmAddress::parse(good).unwrap();
583            assert_eq!(a.to_checksum(), good);
584            assert_eq!(good.parse::<EvmAddress>().unwrap(), a);
585        }
586        // A mistyped checksum is refused by `parse`, accepted by `parse_any_case`.
587        let typo = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD";
588        assert_eq!(EvmAddress::parse(typo), Err(Error::InvalidChecksum));
589        assert_eq!(typo.parse::<EvmAddress>(), Err(Error::InvalidChecksum));
590        assert!(EvmAddress::parse_any_case(typo).is_ok());
591        // All-lowercase and all-uppercase digits carry no checksum and are accepted.
592        assert!(EvmAddress::parse("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").is_ok());
593        assert!(EvmAddress::parse("0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED").is_ok());
594        assert!(EvmAddress::parse("5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").is_err());
595        assert!(EvmAddress::parse("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beae").is_err());
596    }
597
598    #[test]
599    fn normalizes_claim_signatures() {
600        let key = crate::keys::KeyPair::random(&mut crate::testutil::TestRng);
601        let digest = keccak256(b"claim");
602        let sig = key.private_key().sign_prehash(&digest).unwrap();
603        let canonical = claim_signature(&sig, &key.public_key(), &digest).unwrap();
604        assert_eq!(recover_signer(&digest, &canonical).unwrap(), EvmAddress::from_public_key(&key.public_key()));
605        // The high-S twin normalizes back to the canonical signature.
606        let s = crate::keys::scalar_from_bytes(&sig.s()).unwrap();
607        let high_s = crate::keys::scalar_to_bytes(&-s);
608        let flipped = 27 + (1 - (canonical.v - 27)) as u64;
609        assert_eq!(normalize_claim_signature(sig.r(), high_s, flipped).unwrap(), canonical);
610        assert_eq!(normalize_claim_signature(sig.r(), sig.s(), u64::from(canonical.v) - 27).unwrap(), canonical);
611        assert_eq!(recover_signer(&digest, &ClaimSignature { s: high_s, ..canonical }), Err(Error::MalleableSignature));
612        assert_eq!(recover_signer(&digest, &ClaimSignature { v: 29, ..canonical }), Err(Error::MalleableSignature));
613        assert_eq!(normalize_claim_signature(sig.r(), sig.s(), 2), Err(Error::InvalidRecoveryValue(2)));
614        assert_eq!(normalize_claim_signature(sig.r(), [0; 32], 27), Err(Error::SignatureOutOfRange));
615    }
616
617    #[test]
618    fn maps_statuses() {
619        assert_eq!(htlc_status_of(&EMPTY_STATE), EscrowStatus::Empty);
620        assert_eq!(htlc_status_of(&SETTLED_STATE), EscrowStatus::Settled);
621        assert_eq!(htlc_status_of(&[7; 32]), EscrowStatus::Active);
622        assert_eq!(adaptor_status_of(3), Err(Error::UnknownStatus(3)));
623    }
624}