1use 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 pub v: CurvePoint,
43 pub h: Scalar,
44 pub s: Scalar,
45 pub h_v: CurvePoint,
47 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
109impl<'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 }, 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 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}