hopr_internal_types/
legacy.rs

1//! This module should be removed in 3.0. It introduces the missing binary format separation
2//! from business objects (AcknowledgedTicket), to ensure backwards compatibility of
3//! Ticket Aggregation in 2.x releases
4
5use ethers::core::k256::AffinePoint;
6use ethers::prelude::k256::elliptic_curve::sec1::FromEncodedPoint;
7use ethers::prelude::k256::Scalar;
8use serde::de::Visitor;
9use serde::{de, Deserialize, Deserializer, Serialize};
10
11use hopr_primitive_types::prelude::{BytesEncodable, GeneralError};
12
13#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
14pub struct Address {
15    addr: [u8; 20],
16}
17
18#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, PartialOrd, Ord, std::hash::Hash)]
19pub struct Hash {
20    hash: [u8; 32],
21}
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Response {
25    response: [u8; 32],
26}
27
28#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
29pub struct CurvePoint {
30    affine: AffinePoint,
31}
32
33impl From<AffinePoint> for CurvePoint {
34    fn from(value: AffinePoint) -> Self {
35        Self { affine: value }
36    }
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
40pub struct VrfParameters {
41    /// the pseudo-random point
42    pub v: CurvePoint,
43    pub h: Scalar,
44    pub s: Scalar,
45    /// helper value for smart contract
46    pub h_v: CurvePoint,
47    /// helper value for smart contract
48    pub s_b: CurvePoint,
49}
50
51impl From<VrfParameters> for hopr_crypto_types::vrf::VrfParameters {
52    fn from(value: VrfParameters) -> Self {
53        Self {
54            V: value.v.affine.into(),
55            h: value.h,
56            s: value.s,
57        }
58    }
59}
60
61#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
62pub enum AcknowledgedTicketStatus {
63    #[default]
64    Untouched,
65    BeingRedeemed {
66        tx_hash: Hash,
67    },
68    BeingAggregated {
69        start: u64,
70        end: u64,
71    },
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct Ticket(pub crate::tickets::Ticket);
76
77impl Serialize for Ticket {
78    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
79    where
80        S: serde::Serializer,
81    {
82        serializer.serialize_bytes(&self.0.clone().into_encoded())
83    }
84}
85
86struct TicketVisitor {}
87
88impl Visitor<'_> for TicketVisitor {
89    type Value = Ticket;
90
91    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
92        formatter.write_fmt(format_args!(
93            "a byte-array with {} elements",
94            crate::tickets::Ticket::SIZE
95        ))
96    }
97
98    fn visit_bytes<E>(self, v: &[u8]) -> std::result::Result<Self::Value, E>
99    where
100        E: de::Error,
101    {
102        Ok(Ticket(
103            v.try_into()
104                .map_err(|e: GeneralError| de::Error::custom(e.to_string()))?,
105        ))
106    }
107}
108
109// Use compact deserialization for tickets as they are used very often
110impl<'de> Deserialize<'de> for Ticket {
111    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
112    where
113        D: Deserializer<'de>,
114    {
115        deserializer.deserialize_bytes(TicketVisitor {})
116    }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120pub struct AcknowledgedTicket {
121    #[serde(default)]
122    pub status: AcknowledgedTicketStatus,
123    pub ticket: Ticket,
124    pub response: Response,
125    pub vrf_params: VrfParameters,
126    pub signer: Address,
127}
128
129impl AcknowledgedTicket {
130    pub fn new(
131        value: crate::tickets::TransferableWinningTicket,
132        me: &hopr_primitive_types::primitives::Address,
133        domain_separator: &hopr_crypto_types::types::Hash,
134    ) -> Self {
135        let mut response = Response::default();
136        response.response.copy_from_slice(value.response.as_ref());
137
138        let mut signer = Address::default();
139        signer.addr.copy_from_slice(value.signer.as_ref());
140
141        let vrf_params = VrfParameters {
142            v: AffinePoint::from_encoded_point(value.vrf_params.V.as_compressed())
143                .expect("invalid vrf params")
144                .into(),
145            h: value.vrf_params.h,
146            s: value.vrf_params.s,
147            h_v: AffinePoint::from_encoded_point(&value.vrf_params.get_h_v_witness())
148                .expect("invalid vrf params")
149                .into(),
150            s_b: AffinePoint::from_encoded_point(
151                &value
152                    .vrf_params
153                    .get_s_b_witness(
154                        me,
155                        &value.ticket.get_hash(domain_separator).into(),
156                        domain_separator.as_ref(),
157                    )
158                    .expect("invalid vrf params"),
159            )
160            .expect("invalid vrf params")
161            .into(),
162        };
163
164        Self {
165            status: AcknowledgedTicketStatus::BeingAggregated { start: 0, end: 0 }, // values not  used
166            ticket: Ticket(value.ticket),
167            response,
168            vrf_params,
169            signer,
170        }
171    }
172}
173
174impl From<AcknowledgedTicket> for crate::tickets::TransferableWinningTicket {
175    fn from(value: AcknowledgedTicket) -> Self {
176        Self {
177            ticket: value.ticket.0,
178            response: value.response.response.into(),
179            vrf_params: value.vrf_params.into(),
180            signer: hopr_primitive_types::primitives::Address::new(&value.signer.addr),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::legacy::{Ticket, VrfParameters};
188    use crate::tickets::{TicketBuilder, TransferableWinningTicket};
189    use ethers::utils::hex;
190    use hex_literal::hex;
191    use hopr_crypto_types::prelude::{ChainKeypair, Challenge, CurvePoint, HalfKey, Hash, Keypair};
192    use hopr_primitive_types::prelude::{BalanceType, EthereumChallenge};
193    use hopr_primitive_types::prelude::{IntoEndian, U256};
194    use hopr_primitive_types::primitives::Address;
195    use lazy_static::lazy_static;
196
197    lazy_static! {
198        static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!(
199            "14d2d952715a51aadbd4cc6bfac9aa9927182040da7b336d37d5bb7247aa7566"
200        ))
201        .expect("lazy static keypair should be constructible");
202        static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!(
203            "48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c"
204        ))
205        .expect("lazy static keypair should be constructible");
206        static ref DESTINATION: [u8; 20] = hex!("345ae204774ff2b3e8d4cac884dad3d1603b5917");
207        static ref CHANNEL_DST: [u8; 32] = hex!("57dc754bb522f2fe7799e471fd6efd0b6139a2120198f15b92f4a78cb882af35");
208        static ref ETHEREUM_CHALLENGE: [u8; 20] = hex!("4162339a4204a1cedf43c92049875a19cb09dd20");
209        static ref RESPONSE: [u8; 32] = hex!("83c841f72b270440b7c8cd7b4f7d806a84f40ead5b04edccbb9a4c8936b91436");
210    }
211
212    fn default_affine_point() -> crate::legacy::AffinePoint {
213        hopr_crypto_types::types::CurvePoint::from_exponent(&U256::one().to_be_bytes())
214            .expect("curve point should exist")
215            .into()
216    }
217
218    #[test]
219    fn ticket_binary_compatibility_with_the_v2_format() -> anyhow::Result<()> {
220        let kp = ChainKeypair::from_secret(&hex!(
221            "492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775"
222        ))?;
223
224        let ticket = TicketBuilder::default()
225            .addresses(
226                kp.public().to_address(),
227                hex!("fb6916095ca1df60bb79ce92ce3ea74c37c5d359"),
228            )
229            .balance(BalanceType::HOPR.balance(100))
230            .index_offset(1)
231            .index(1)
232            .win_prob(1.0)
233            .challenge(EthereumChallenge::new(&hex!(
234                "045a4d76d0bfc0d84f6ff946b5934b7ea6a2958f"
235            )))
236            .channel_epoch(1)
237            .build_signed(
238                &kp,
239                &Hash::from(hex!("51d3003d908045a4d76d0bfc0d84f6ff946b5934b7ea6a2958faf02fead4567a")),
240            )?
241            .leak();
242
243        let buf = Vec::new();
244        let serialized = cbor4ii::serde::to_vec(buf, &Ticket(ticket))?;
245
246        const EXPECTED_V2_BINARY_REPRESENTATION_CBOR_HEX: [u8; 150] = hex!("5894abc401a1657346845964c4f318e28e94a20c447eceaed5aa024f5b7345e5f46400000000000000000000006400000000000100000001000001ffffffffffffff045a4d76d0bfc0d84f6ff946b5934b7ea6a2958f103acd610d4728745bafb0c4e1fd3928d0f8a9d61b555f89e41598fa4aba60fc700eaea5f974dadd8c6517e7e3654851814c38d9d33b1cc39fbcdd1764057cfb");
247
248        assert_eq!(&serialized, &EXPECTED_V2_BINARY_REPRESENTATION_CBOR_HEX);
249
250        Ok(())
251    }
252
253    #[test]
254    fn test_legacy_binary_compatibility_with_2_0_8() -> anyhow::Result<()> {
255        let domain_separator = Hash::from(*CHANNEL_DST);
256
257        let ticket = TicketBuilder::default()
258            .direction(&ALICE.public().to_address(), &Address::new(DESTINATION.as_ref()))
259            .amount(1000000_u64)
260            .index(10)
261            .index_offset(2)
262            .win_prob(1.0)
263            .channel_epoch(2)
264            .challenge(EthereumChallenge::new(ETHEREUM_CHALLENGE.as_ref()))
265            .build_signed(&ALICE, &domain_separator)?;
266
267        let mut signer = crate::legacy::Address::default();
268        signer.addr.copy_from_slice(ALICE.public().to_address().as_ref());
269
270        let ack_ticket = crate::legacy::AcknowledgedTicket {
271            status: crate::legacy::AcknowledgedTicketStatus::BeingAggregated { start: 0, end: 0 },
272            ticket: Ticket(ticket.verified_ticket().clone()),
273            response: crate::legacy::Response { response: *RESPONSE },
274            vrf_params: VrfParameters {
275                v: default_affine_point().into(),
276                h: Default::default(),
277                s: Default::default(),
278                h_v: default_affine_point().into(),
279                s_b: default_affine_point().into(),
280            },
281            signer,
282        };
283
284        let serialized = cbor4ii::serde::to_vec(Vec::with_capacity(300), &ack_ticket)?;
285        let hex_encoded = hex::encode(serialized);
286
287        // This is the serialized output from 2.0.8 with the same inputs
288        let expected = "a566737461747573a16f4265696e6741676772656761746564a26573746172740063656e6400667469636b65745894cff6549a8f770afcc2ff07ff0d947178a7fb935539ecb2316ebeabff3f1740040000000000000000000f424000000000000a00000002000002ffffffffffffff4162339a4204a1cedf43c92049875a19cb09dd20b33432df13bb26810abc14161b514fb15a2027b05288ad9d8c3befd73831fce3d64808cc3c386fbf1a33263342a32434f24ea0a78b2cb6d503133a10f528378268726573706f6e7365a168726573706f6e73659820188318c8184118f7182b182704184018b718c818cd187b184f187d1880186a188418f40e18ad185b0418ed18cc18bb189a184c1889183618b91418366a7672665f706172616d73a56176a166616666696e65982102187918be1866187e18f918dc18bb18ac185518a01862189518ce18870b0702189b18fc18db182d18ce182818d9185918f21881185b1618f817189861689820000000000000000000000000000000000000000000000000000000000000000061739820000000000000000000000000000000000000000000000000000000000000000063685f76a166616666696e65982102187918be1866187e18f918dc18bb18ac185518a01862189518ce18870b0702189b18fc18db182d18ce182818d9185918f21881185b1618f817189863735f62a166616666696e65982102187918be1866187e18f918dc18bb18ac185518a01862189518ce18870b0702189b18fc18db182d18ce182818d9185918f21881185b1618f8171898667369676e6572a1646164647294182018ab183c18ad184e186c18d718c518c818da184518cd1889189218d8184518f4189c189200";
289        assert_eq!(expected, hex_encoded);
290
291        Ok(())
292    }
293
294    #[test]
295    fn test_legacy_must_serialize_deserialize_correctly() -> anyhow::Result<()> {
296        let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
297        let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
298
299        let cp1: CurvePoint = hk1.to_challenge().try_into()?;
300        let cp2: CurvePoint = hk2.to_challenge().try_into()?;
301        let cp_sum = CurvePoint::combine(&[&cp1, &cp2]);
302
303        let domain_separator = Hash::try_from(CHANNEL_DST.as_ref())?;
304
305        let ticket = TicketBuilder::default()
306            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
307            .amount(1000000_u64)
308            .index(10)
309            .index_offset(2)
310            .win_prob(1.0)
311            .channel_epoch(2)
312            .challenge(Challenge::from(cp_sum).to_ethereum_challenge())
313            .build_signed(&ALICE, &domain_separator)?;
314
315        let unack = ticket.into_unacknowledged(hk1);
316        let acked = unack.acknowledge(&hk2)?;
317
318        let transferable = acked.into_transferable(&BOB, &domain_separator)?;
319
320        transferable
321            .clone()
322            .into_redeemable(&ALICE.public().to_address(), &domain_separator)?;
323
324        let serialized =
325            crate::legacy::AcknowledgedTicket::new(transferable, &BOB.public().to_address(), &domain_separator);
326
327        let deserialized = TransferableWinningTicket::from(serialized);
328
329        deserialized.into_redeemable(&ALICE.public().to_address(), &domain_separator)?;
330
331        Ok(())
332    }
333}