use ethers::contract::EthCall;
use hex_literal::hex;
use hopr_bindings::hopr_channels::RedeemTicketCall;
use hopr_crypto_types::prelude::*;
use hopr_primitive_types::prelude::*;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use tracing::{debug, error};
use crate::errors;
use crate::errors::CoreTypesError;
use crate::prelude::CoreTypesError::InvalidInputData;
use crate::prelude::{generate_channel_id, DEFAULT_OUTGOING_TICKET_WIN_PROB};
const ENCODED_TICKET_LENGTH: usize = 64;
const ENCODED_WIN_PROB_LENGTH: usize = 7;
pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
pub const ALWAYS_WINNING: EncodedWinProb = hex!("ffffffffffffff");
pub const NEVER_WINNING: EncodedWinProb = hex!("00000000000000");
pub(crate) fn check_ticket_win(
ticket_hash: &Hash,
ticket_signature: &Signature,
win_prob: &EncodedWinProb,
response: &Response,
vrf_params: &VrfParameters,
) -> bool {
let mut signed_ticket_luck = [0u8; 8];
signed_ticket_luck[1..].copy_from_slice(win_prob);
let mut computed_ticket_luck = [0u8; 8];
computed_ticket_luck[1..].copy_from_slice(
&Hash::create(&[
ticket_hash.as_ref(),
&vrf_params.V.as_uncompressed().as_bytes()[1..], response.as_ref(),
ticket_signature.as_ref(),
])
.as_ref()[0..7],
);
u64::from_be_bytes(computed_ticket_luck) <= u64::from_be_bytes(signed_ticket_luck)
}
#[derive(Debug, Clone, smart_default::SmartDefault)]
pub struct TicketBuilder {
channel_id: Option<Hash>,
amount: Option<U256>,
balance: Option<Balance>,
#[default = 0]
index: u64,
#[default = 1]
index_offset: u32,
#[default = 1]
channel_epoch: u32,
#[default(Some(DEFAULT_OUTGOING_TICKET_WIN_PROB))]
win_prob: Option<f64>,
win_prob_enc: Option<EncodedWinProb>,
challenge: Option<EthereumChallenge>,
signature: Option<Signature>,
}
impl TicketBuilder {
#[must_use]
pub fn zero_hop() -> Self {
Self {
index: 0,
amount: Some(U256::zero()),
index_offset: 1,
win_prob: Some(0.0),
channel_epoch: 0,
..Default::default()
}
}
#[must_use]
pub fn direction(mut self, source: &Address, destination: &Address) -> Self {
self.channel_id = Some(generate_channel_id(source, destination));
self
}
#[must_use]
pub fn addresses<T: Into<Address>, U: Into<Address>>(mut self, source: T, destination: U) -> Self {
self.channel_id = Some(generate_channel_id(&source.into(), &destination.into()));
self
}
#[must_use]
pub fn channel_id(mut self, channel_id: Hash) -> Self {
self.channel_id = Some(channel_id);
self
}
#[must_use]
pub fn amount<T: Into<U256>>(mut self, amount: T) -> Self {
self.amount = Some(amount.into());
self.balance = None;
self
}
#[must_use]
pub fn balance(mut self, balance: Balance) -> Self {
self.balance = Some(balance);
self.amount = None;
self
}
#[must_use]
pub fn index(mut self, index: u64) -> Self {
self.index = index;
self
}
#[must_use]
pub fn index_offset(mut self, index_offset: u32) -> Self {
self.index_offset = index_offset;
self
}
#[must_use]
pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
self.channel_epoch = channel_epoch;
self
}
#[must_use]
pub fn win_prob(mut self, win_prob: f64) -> Self {
self.win_prob = Some(win_prob);
self.win_prob_enc = None;
self
}
#[must_use]
pub fn win_prob_encoded(mut self, win_prob: EncodedWinProb) -> Self {
self.win_prob = None;
self.win_prob_enc = Some(win_prob);
self
}
#[must_use]
pub fn challenge(mut self, challenge: EthereumChallenge) -> Self {
self.challenge = Some(challenge);
self
}
#[must_use]
pub fn signature(mut self, signature: Signature) -> Self {
self.signature = Some(signature);
self
}
pub fn build(self) -> errors::Result<Ticket> {
let amount = match (self.amount, self.balance) {
(Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => BalanceType::HOPR.balance(amount),
(None, Some(balance))
if balance.balance_type() == BalanceType::HOPR && balance.amount().lt(&10_u128.pow(25).into()) =>
{
balance
}
(None, None) => return Err(InvalidInputData("missing ticket amount".into())),
(Some(_), Some(_)) => {
return Err(InvalidInputData(
"either amount or balance must be set but not both".into(),
))
}
_ => {
return Err(InvalidInputData(
"tickets may not have more than 1% of total supply".into(),
))
}
};
if self.index > (1_u64 << 48) {
return Err(InvalidInputData("cannot hold ticket indices larger than 2^48".into()));
}
if self.channel_epoch > (1_u32 << 24) {
return Err(InvalidInputData("cannot hold channel epoch larger than 2^24".into()));
}
let encoded_win_prob = match (self.win_prob, self.win_prob_enc) {
(Some(win_prob), None) => f64_to_win_prob(win_prob)?,
(None, Some(win_prob)) => win_prob,
(Some(_), Some(_)) => return Err(InvalidInputData("conflicting winning probabilities".into())),
(None, None) => return Err(InvalidInputData("missing ticket winning probability".into())),
};
if self.index_offset < 1 {
return Err(InvalidInputData(
"ticket index offset must be greater or equal to 1".into(),
));
}
Ok(Ticket {
channel_id: self.channel_id.ok_or(InvalidInputData("missing channel id".into()))?,
amount,
index: self.index,
index_offset: self.index_offset,
encoded_win_prob,
channel_epoch: self.channel_epoch,
challenge: self
.challenge
.ok_or(InvalidInputData("missing ticket challenge".into()))?,
signature: self.signature,
})
}
pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
if self.signature.is_none() {
Ok(self.build()?.sign(signer, domain_separator))
} else {
Err(InvalidInputData("signature already set".into()))
}
}
pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
if let Some(signature) = self.signature {
let issuer = PublicKey::from_signature_hash(hash.as_ref(), &signature)?.to_address();
Ok(VerifiedTicket(self.build()?, hash, issuer))
} else {
Err(InvalidInputData("signature is missing".into()))
}
}
}
impl From<&Ticket> for TicketBuilder {
fn from(value: &Ticket) -> Self {
Self {
channel_id: Some(value.channel_id),
amount: None,
balance: Some(value.amount),
index: value.index,
index_offset: value.index_offset,
channel_epoch: value.channel_epoch,
win_prob: None,
win_prob_enc: Some(value.encoded_win_prob),
challenge: Some(value.challenge),
signature: None,
}
}
}
impl From<Ticket> for TicketBuilder {
fn from(value: Ticket) -> Self {
Self::from(&value)
}
}
#[cfg_attr(doc, aquamarine::aquamarine)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ticket {
pub channel_id: Hash,
pub amount: Balance, pub index: u64, pub index_offset: u32, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
pub signature: Option<Signature>,
}
impl PartialOrd for Ticket {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Ticket {
fn cmp(&self, other: &Self) -> Ordering {
match self.channel_id.cmp(&other.channel_id) {
Ordering::Equal => match self.channel_epoch.cmp(&other.channel_epoch) {
Ordering::Equal => self.index.cmp(&other.index),
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
},
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
}
}
}
impl Display for Ticket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ticket #{}, offset {}, epoch {} in channel {}",
self.index, self.index_offset, self.channel_epoch, self.channel_id
)
}
}
impl Ticket {
fn encode_without_signature(&self) -> [u8; Self::SIZE - Signature::SIZE] {
let mut ret = [0u8; Self::SIZE - Signature::SIZE];
let mut offset = 0;
ret[offset..offset + Hash::SIZE].copy_from_slice(self.channel_id.as_ref());
offset += Hash::SIZE;
ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
offset += 12;
ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
offset += 6;
ret[offset..offset + 4].copy_from_slice(&self.index_offset.to_be_bytes());
offset += 4;
ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
offset += 3;
ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
offset += ENCODED_WIN_PROB_LENGTH;
ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
ret
}
pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
let ticket_hash = Hash::create(&[self.encode_without_signature().as_ref()]); let hash_struct = Hash::create(&[&RedeemTicketCall::selector(), &[0u8; 28], ticket_hash.as_ref()]);
Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()])
}
pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
let ticket_hash = self.get_hash(domain_separator);
self.signature = Some(Signature::sign_hash(ticket_hash.as_ref(), signing_key));
VerifiedTicket(self, ticket_hash, signing_key.public().to_address())
}
pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
let ticket_hash = self.get_hash(domain_separator);
if let Some(signature) = &self.signature {
match PublicKey::from_signature_hash(ticket_hash.as_ref(), signature) {
Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket(self, ticket_hash, *issuer)),
Err(e) => {
error!("failed to verify ticket signature: {e}");
Err(self.into())
}
_ => Err(self.into()),
}
} else {
Err(self.into())
}
}
pub fn is_aggregated(&self) -> bool {
self.index_offset > 1
}
pub fn win_prob(&self) -> f64 {
win_prob_to_f64(&self.encoded_win_prob)
}
}
impl From<Ticket> for [u8; TICKET_SIZE] {
fn from(value: Ticket) -> Self {
let mut ret = [0u8; TICKET_SIZE];
ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_without_signature().as_ref());
ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
value
.signature
.expect("cannot serialize ticket without signature")
.as_ref(),
);
ret
}
}
impl TryFrom<&[u8]> for Ticket {
type Error = GeneralError;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
if value.len() == Self::SIZE {
let mut offset = 0;
let channel_id = Hash::try_from(&value[offset..offset + Hash::SIZE])?;
offset += Hash::SIZE;
let mut amount = [0u8; 32];
amount[20..32].copy_from_slice(&value[offset..offset + 12]);
offset += 12;
let mut index = [0u8; 8];
index[2..8].copy_from_slice(&value[offset..offset + 6]);
offset += 6;
let mut index_offset = [0u8; 4];
index_offset.copy_from_slice(&value[offset..offset + 4]);
offset += 4;
let mut channel_epoch = [0u8; 4];
channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
offset += 3;
let mut encoded_win_prob = [0u8; 7];
encoded_win_prob.copy_from_slice(&value[offset..offset + 7]);
offset += 7;
debug_assert_eq!(offset, ENCODED_TICKET_LENGTH);
let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
offset += EthereumChallenge::SIZE;
let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
TicketBuilder::default()
.channel_id(channel_id)
.amount(amount)
.index(u64::from_be_bytes(index))
.index_offset(u32::from_be_bytes(index_offset))
.channel_epoch(u32::from_be_bytes(channel_epoch))
.win_prob_encoded(encoded_win_prob)
.challenge(challenge)
.signature(signature)
.build()
.map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
} else {
Err(GeneralError::ParseError("Ticket".into()))
}
}
}
const TICKET_SIZE: usize = ENCODED_TICKET_LENGTH + EthereumChallenge::SIZE + Signature::SIZE;
impl BytesEncodable<TICKET_SIZE> for Ticket {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedTicket(Ticket, Hash, Address);
impl VerifiedTicket {
pub fn win_prob(&self) -> f64 {
self.0.win_prob()
}
pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
if let Ok(vrf_params) = derive_vrf_parameters(self.1, chain_keypair, domain_separator.as_ref()) {
check_ticket_win(
&self.1,
self.0
.signature
.as_ref()
.expect("verified ticket have always a signature"),
&self.0.encoded_win_prob,
response,
&vrf_params,
)
} else {
error!("cannot derive vrf parameters for {self}");
false
}
}
pub fn get_path_position(&self, price_per_packet: U256) -> errors::Result<u8> {
let pos = self.0.amount.amount() / price_per_packet.div_f64(self.win_prob())?;
pos.as_u64()
.try_into() .map_err(|_| CoreTypesError::ArithmeticError(format!("Cannot convert {pos} to u8")))
}
pub fn verified_ticket(&self) -> &Ticket {
&self.0
}
pub fn verified_hash(&self) -> &Hash {
&self.1
}
pub fn verified_issuer(&self) -> &Address {
&self.2
}
pub fn verified_signature(&self) -> &Signature {
self.0
.signature
.as_ref()
.expect("verified ticket always has a signature")
}
pub fn leak(self) -> Ticket {
self.0
}
pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
UnacknowledgedTicket { ticket: self, own_key }
}
pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
AcknowledgedTicket {
status: AcknowledgedTicketStatus::Untouched,
ticket: self,
response,
}
}
}
impl Display for VerifiedTicket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "verified {}", self.0)
}
}
impl PartialOrd for VerifiedTicket {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VerifiedTicket {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
pub fn win_prob_to_f64(encoded_win_prob: &EncodedWinProb) -> f64 {
if encoded_win_prob.eq(&NEVER_WINNING) {
return 0.0;
}
if encoded_win_prob.eq(&ALWAYS_WINNING) {
return 1.0;
}
let mut tmp = [0u8; 8];
tmp[1..].copy_from_slice(encoded_win_prob);
let tmp = u64::from_be_bytes(tmp);
let significand: u64 = tmp + 1;
f64::from_bits(1023u64 << 52 | significand >> 4) - 1.0
}
pub fn f64_to_win_prob(win_prob: f64) -> errors::Result<EncodedWinProb> {
if !(0.0..=1.0).contains(&win_prob) {
return Err(CoreTypesError::InvalidInputData(
"Winning probability must be in [0.0, 1.0]".into(),
));
}
if win_prob == 0.0 {
return Ok(NEVER_WINNING);
}
if win_prob == 1.0 {
return Ok(ALWAYS_WINNING);
}
let tmp: u64 = (win_prob + 1.0).to_bits();
let significand: u64 = tmp & 0x000fffffffffffffu64;
let encoded = ((significand - 1) << 4) | 0x000000000000000fu64;
let mut res = [0u8; 7];
res.copy_from_slice(&encoded.to_be_bytes()[1..]);
Ok(res)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnacknowledgedTicket {
pub ticket: VerifiedTicket,
pub(crate) own_key: HalfKey,
}
impl UnacknowledgedTicket {
#[inline]
pub fn verified_ticket(&self) -> &Ticket {
self.ticket.verified_ticket()
}
pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
debug!("acknowledging ticket using response {}", response.to_hex());
if self.ticket.verified_ticket().challenge == response.to_challenge().into() {
Ok(self.ticket.into_acknowledged(response))
} else {
Err(CryptoError::InvalidChallenge.into())
}
}
}
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialEq,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
num_enum::IntoPrimitive,
num_enum::TryFromPrimitive,
)]
#[strum(serialize_all = "PascalCase")]
pub enum AcknowledgedTicketStatus {
#[default]
Untouched = 0,
BeingRedeemed = 1,
BeingAggregated = 2,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AcknowledgedTicket {
#[serde(default)]
pub status: AcknowledgedTicketStatus,
pub ticket: VerifiedTicket,
pub response: Response,
}
impl PartialOrd for AcknowledgedTicket {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AcknowledgedTicket {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.ticket.cmp(&other.ticket)
}
}
impl AcknowledgedTicket {
#[inline]
pub fn verified_ticket(&self) -> &Ticket {
self.ticket.verified_ticket()
}
pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
}
pub fn into_redeemable(
self,
chain_keypair: &ChainKeypair,
domain_separator: &Hash,
) -> crate::errors::Result<RedeemableTicket> {
if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
return Err(errors::CoreTypesError::LoopbackTicket);
}
let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
Ok(RedeemableTicket {
ticket: self.ticket,
response: self.response,
vrf_params,
channel_dst: *domain_separator,
})
}
pub fn into_transferable(
self,
chain_keypair: &ChainKeypair,
domain_separator: &Hash,
) -> errors::Result<TransferableWinningTicket> {
self.into_redeemable(chain_keypair, domain_separator)
.map(TransferableWinningTicket::from)
}
}
impl Display for AcknowledgedTicket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RedeemableTicket {
pub ticket: VerifiedTicket,
pub response: Response,
pub vrf_params: VrfParameters,
pub channel_dst: Hash,
}
impl RedeemableTicket {
#[inline]
pub fn verified_ticket(&self) -> &Ticket {
self.ticket.verified_ticket()
}
}
impl PartialEq for RedeemableTicket {
fn eq(&self, other: &Self) -> bool {
self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
}
}
impl Display for RedeemableTicket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "redeemable {}", self.ticket)
}
}
impl From<RedeemableTicket> for AcknowledgedTicket {
fn from(value: RedeemableTicket) -> Self {
Self {
status: AcknowledgedTicketStatus::Untouched,
ticket: value.ticket,
response: value.response,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferableWinningTicket {
pub ticket: Ticket,
pub response: Response,
pub vrf_params: VrfParameters,
pub signer: Address,
}
impl TransferableWinningTicket {
pub fn into_redeemable(
self,
expected_issuer: &Address,
domain_separator: &Hash,
) -> errors::Result<RedeemableTicket> {
if !self.signer.eq(expected_issuer) {
return Err(crate::errors::CoreTypesError::InvalidInputData(
"invalid ticket issuer".into(),
));
}
let verified_ticket = self
.ticket
.verify(&self.signer, domain_separator)
.map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
if check_ticket_win(
verified_ticket.verified_hash(),
verified_ticket.verified_signature(),
&verified_ticket.verified_ticket().encoded_win_prob,
&self.response,
&self.vrf_params,
) {
Ok(RedeemableTicket {
ticket: verified_ticket,
response: self.response,
vrf_params: self.vrf_params,
channel_dst: *domain_separator,
})
} else {
Err(crate::errors::CoreTypesError::InvalidInputData(
"ticket is not a win".into(),
))
}
}
}
impl PartialEq for TransferableWinningTicket {
fn eq(&self, other: &Self) -> bool {
self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
}
}
impl PartialOrd<Self> for TransferableWinningTicket {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.ticket.cmp(&other.ticket))
}
}
impl From<RedeemableTicket> for TransferableWinningTicket {
fn from(value: RedeemableTicket) -> Self {
Self {
response: value.response,
vrf_params: value.vrf_params,
signer: *value.ticket.verified_issuer(),
ticket: value.ticket.leak(),
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::tickets::AcknowledgedTicket;
use hex_literal::hex;
use hopr_crypto_types::{
keypairs::{ChainKeypair, Keypair},
types::{Challenge, CurvePoint, HalfKey, Hash, Response},
};
use hopr_primitive_types::prelude::UnitaryFloatOps;
use hopr_primitive_types::primitives::{Address, BalanceType, EthereumChallenge, U256};
lazy_static::lazy_static! {
static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
}
#[test]
pub fn test_win_prob_to_f64() {
let mut test_bit_string = [0xffu8; 7];
assert_eq!(0.0f64, super::win_prob_to_f64(&[0u8; 7]));
assert_eq!(1.0f64, super::win_prob_to_f64(&test_bit_string));
test_bit_string[0] = 0x7f;
assert_eq!(0.5f64, super::win_prob_to_f64(&test_bit_string));
test_bit_string[0] = 0x3f;
assert_eq!(0.25f64, super::win_prob_to_f64(&test_bit_string));
test_bit_string[0] = 0x1f;
assert_eq!(0.125f64, super::win_prob_to_f64(&test_bit_string));
}
#[test]
pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
let mut test_bit_string = [0xffu8; 7];
assert_eq!([0u8; 7], super::f64_to_win_prob(0.0f64)?);
assert_eq!(test_bit_string, super::f64_to_win_prob(1.0f64)?);
test_bit_string[0] = 0x7f;
assert_eq!(test_bit_string, super::f64_to_win_prob(0.5f64)?);
test_bit_string[0] = 0x3f;
assert_eq!(test_bit_string, super::f64_to_win_prob(0.25f64)?);
test_bit_string[0] = 0x1f;
assert_eq!(test_bit_string, super::f64_to_win_prob(0.125f64)?);
Ok(())
}
#[test]
pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
assert!((float - super::win_prob_to_f64(&super::f64_to_win_prob(float)?)).abs() < f64::EPSILON);
}
Ok(())
}
#[test]
pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
let ticket = TicketBuilder::zero_hop()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.challenge(Default::default())
.build()?;
assert_eq!(0, ticket.index);
assert_eq!(0.0, ticket.win_prob());
assert_eq!(0, ticket.channel_epoch);
assert_eq!(
generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address()),
ticket.channel_id
);
Ok(())
}
#[test]
pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.balance(BalanceType::HOPR.one())
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
assert_eq!(
initial_ticket.verified_ticket(),
&Ticket::try_from(ticket_bytes.as_ref())?
);
Ok(())
}
#[test]
pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.balance(BalanceType::HOPR.one())
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert_eq!(
initial_ticket,
bincode::deserialize(&bincode::serialize(&initial_ticket)?)?
);
Ok(())
}
#[test]
pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.balance(BalanceType::HOPR.one())
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
let ticket = initial_ticket.leak();
assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
Ok(())
}
#[test]
pub fn test_path_position() -> anyhow::Result<()> {
let builder = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.balance(BalanceType::HOPR.one())
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(Default::default());
let ticket = builder.clone().build_signed(&ALICE, &Default::default())?;
assert_eq!(1u8, ticket.get_path_position(1_u32.into())?);
let ticket = builder
.clone()
.amount(34_u64)
.build_signed(&ALICE, &Default::default())?;
assert_eq!(2u8, ticket.get_path_position(17_u64.into())?);
let ticket = builder
.clone()
.amount(30_u64)
.win_prob(0.2)
.build_signed(&ALICE, &Default::default())?;
assert_eq!(2u8, ticket.get_path_position(3_u64.into())?);
Ok(())
}
#[test]
pub fn test_path_position_mismatch() -> anyhow::Result<()> {
let ticket = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.amount(256)
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert!(ticket.get_path_position(1_u64.into()).is_err());
Ok(())
}
#[test]
pub fn test_zero_hop() -> anyhow::Result<()> {
let ticket = TicketBuilder::zero_hop()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert!(ticket
.leak()
.verify(&ALICE.public().to_address(), &Hash::default())
.is_ok());
Ok(())
}
fn mock_ticket(
pk: &ChainKeypair,
counterparty: &Address,
domain_separator: Option<Hash>,
challenge: Option<EthereumChallenge>,
) -> anyhow::Result<VerifiedTicket> {
let win_prob = 1.0f64; let price_per_packet: U256 = 10000000000000000u128.into(); let path_pos = 5u64;
Ok(TicketBuilder::default()
.direction(&pk.public().to_address(), counterparty)
.amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(4)
.challenge(challenge.unwrap_or_default())
.build_signed(pk, &domain_separator.unwrap_or_default())?)
}
#[test]
fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
let cp1: CurvePoint = hk1.to_challenge().try_into()?;
let cp2: CurvePoint = hk2.to_challenge().try_into()?;
let cp_sum = CurvePoint::combine(&[&cp1, &cp2]);
let dst = Hash::default();
let ack = mock_ticket(
&ALICE,
&BOB.public().to_address(),
Some(dst),
Some(Challenge::from(cp_sum).to_ethereum_challenge()),
)?
.into_unacknowledged(hk1)
.acknowledge(&hk2)?;
assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
Ok(())
}
#[test]
fn test_acknowledged_ticket() -> anyhow::Result<()> {
let response =
Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
let dst = Hash::default();
let ticket = mock_ticket(
&ALICE,
&BOB.public().to_address(),
Some(dst),
Some(response.to_challenge().into()),
)?;
let acked_ticket = ticket.into_acknowledged(response);
let mut deserialized_ticket = bincode::deserialize::<AcknowledgedTicket>(&bincode::serialize(&acked_ticket)?)?;
assert_eq!(acked_ticket, deserialized_ticket);
assert!(deserialized_ticket.is_winning(&BOB, &dst));
deserialized_ticket.status = super::AcknowledgedTicketStatus::BeingAggregated;
assert_eq!(
deserialized_ticket,
bincode::deserialize(&bincode::serialize(&deserialized_ticket)?)?
);
Ok(())
}
#[test]
fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
let hk1 = HalfKey::random();
let hk2 = HalfKey::random();
let resp = Response::from_half_keys(&hk1, &hk2)?;
let verified = TicketBuilder::default()
.direction(&ALICE.public().to_address(), &BOB.public().to_address())
.balance(BalanceType::HOPR.one())
.index(0)
.index_offset(1)
.win_prob(1.0)
.channel_epoch(1)
.challenge(resp.to_challenge().to_ethereum_challenge())
.build_signed(&ALICE, &Default::default())?;
let unack = verified.into_unacknowledged(hk1);
let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
assert_eq!(redeemable_1, redeemable_2);
assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
Ok(())
}
}