use std::{
cmp::Ordering,
fmt::{Display, Formatter},
str::FromStr,
};
use crate::crypto::prelude::*;
use crate::primitive::prelude::*;
use hex_literal::hex;
use tracing::{error, instrument};
use crate::internal::prelude::ChannelBuilder;
use crate::internal::{
errors,
errors::CoreTypesError,
prelude::{ChannelId, CoreTypesError::InvalidInputData, generate_channel_id},
};
const ENCODED_WIN_PROB_LENGTH: usize = 7;
pub const REDEEM_CALL_SELECTOR: [u8; 4] = [101, 227, 250, 114];
pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
#[derive(Clone, Copy, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WinningProbability(
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] EncodedWinProb,
);
impl WinningProbability {
pub const ALWAYS: Self = Self([0xff; ENCODED_WIN_PROB_LENGTH]);
pub const EPSILON: f64 = 0.00000001;
pub const NEVER: Self = Self([0u8; ENCODED_WIN_PROB_LENGTH]);
pub fn as_luck(&self) -> u64 {
let mut tmp = [0u8; 8];
tmp[1..].copy_from_slice(&self.0);
u64::from_be_bytes(tmp)
}
pub fn as_encoded(&self) -> EncodedWinProb {
self.0
}
pub fn as_f64(&self) -> f64 {
if self.0.eq(&Self::NEVER.0) {
return 0.0;
}
if self.0.eq(&Self::ALWAYS.0) {
return 1.0;
}
let mut tmp = [0u8; 8];
tmp[1..].copy_from_slice(&self.0);
let tmp = u64::from_be_bytes(tmp);
let significand: u64 = tmp + 1;
f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
}
pub fn try_from_f64(win_prob: f64) -> errors::Result<Self> {
if !(0.0..=1.0).contains(&win_prob) {
return Err(InvalidInputData(
"winning probability must be in [0.0, 1.0]".into(),
));
}
if f64_approx_eq(0.0, win_prob, Self::EPSILON) {
return Ok(Self::NEVER);
}
if f64_approx_eq(1.0, win_prob, Self::EPSILON) {
return Ok(Self::ALWAYS);
}
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(Self(res))
}
pub fn approx_cmp(&self, other: &Self) -> Ordering {
let a = self.as_f64();
let b = other.as_f64();
if !f64_approx_eq(a, b, Self::EPSILON) {
a.partial_cmp(&b)
.expect("finite non-NaN f64 comparison cannot fail")
} else {
Ordering::Equal
}
}
pub fn approx_eq(&self, other: &Self) -> bool {
self.approx_cmp(other).is_eq()
}
pub fn lex_cmp(&self, other: &Self) -> Ordering {
self.as_luck().cmp(&other.as_luck())
}
pub fn lex_eq(&self, other: &Self) -> bool {
self.lex_cmp(other).is_eq()
}
pub fn min(&self, other: &Self) -> Self {
if self.approx_cmp(other) == Ordering::Less {
*self
} else {
*other
}
}
pub fn max(&self, other: &Self) -> Self {
if self.approx_cmp(other) == Ordering::Greater {
*self
} else {
*other
}
}
}
impl Default for WinningProbability {
fn default() -> Self {
Self::ALWAYS
}
}
impl Display for WinningProbability {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.8}", self.as_f64())
}
}
impl FromStr for WinningProbability {
type Err = CoreTypesError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
f64::from_str(s)
.map_err(|e| {
CoreTypesError::ParseError(format!("failed to parse winning probability: {e}"))
})
.and_then(|v| v.try_into())
}
}
impl From<EncodedWinProb> for WinningProbability {
fn from(value: EncodedWinProb) -> Self {
Self(value)
}
}
impl<'a> From<&'a EncodedWinProb> for WinningProbability {
fn from(value: &'a EncodedWinProb) -> Self {
Self(*value)
}
}
impl From<WinningProbability> for EncodedWinProb {
fn from(value: WinningProbability) -> Self {
value.0
}
}
impl From<u64> for WinningProbability {
fn from(value: u64) -> Self {
let mut ret = Self::default();
ret.0.copy_from_slice(&value.to_be_bytes()[1..]);
ret
}
}
impl TryFrom<f64> for WinningProbability {
type Error = CoreTypesError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
Self::try_from_f64(value)
}
}
impl From<WinningProbability> for f64 {
fn from(value: WinningProbability) -> Self {
value.as_f64()
}
}
impl PartialEq<f64> for WinningProbability {
fn eq(&self, other: &f64) -> bool {
f64_approx_eq(self.as_f64(), *other, Self::EPSILON)
}
}
impl PartialEq<WinningProbability> for f64 {
fn eq(&self, other: &WinningProbability) -> bool {
f64_approx_eq(*self, other.as_f64(), WinningProbability::EPSILON)
}
}
impl PartialEq<EncodedWinProb> for WinningProbability {
fn eq(&self, other: &EncodedWinProb) -> bool {
self.0.eq(other)
}
}
impl AsRef<[u8]> for WinningProbability {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl<'a> TryFrom<&'a [u8]> for WinningProbability {
type Error = GeneralError;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
value
.try_into()
.map(Self)
.map_err(|_| GeneralError::ParseError("WinningProbability".into()))
}
}
impl BytesRepresentable for WinningProbability {
const SIZE: usize = ENCODED_WIN_PROB_LENGTH;
}
pub(crate) fn check_ticket_win(
ticket_hash: &Hash,
ticket_signature: &Signature,
win_prob: &WinningProbability,
response: &Response,
vrf_params: &VrfParameters,
) -> bool {
let mut computed_ticket_luck = [0u8; 8];
computed_ticket_luck[1..].copy_from_slice(
&Hash::create(&[
ticket_hash.as_ref(),
&vrf_params.get_v_encoded_point().as_bytes()[1..], response.as_ref(),
ticket_signature.as_ref(),
])
.as_ref()[0..7],
);
u64::from_be_bytes(computed_ticket_luck) <= win_prob.as_luck()
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TicketId {
pub id: ChannelId,
pub epoch: u32,
pub index: u64,
}
impl Display for TicketId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ticket #{}, epoch {} in channel {}",
self.index, self.epoch, self.id
)
}
}
impl From<&VerifiedTicket> for TicketId {
fn from(value: &VerifiedTicket) -> Self {
Self {
id: value.channel_id,
epoch: value.ticket.channel_epoch,
index: value.ticket.index,
}
}
}
#[derive(Debug, Copy, Clone, smart_default::SmartDefault)]
pub struct TicketBuilder {
counterparty: Option<Address>,
amount: Option<U256>,
balance: Option<HoprBalance>,
#[default = 0]
index: u64,
#[default = 1]
channel_epoch: u32,
win_prob: WinningProbability,
challenge: Option<EthereumChallenge>,
signature: Option<Signature>,
}
impl TicketBuilder {
pub const MAX_TICKET_AMOUNT: u128 = ChannelBuilder::MAX_FUNDING_AMOUNT;
pub const MAX_TICKET_INDEX: u64 = (1_u64 << 48) - 1;
pub const MAX_CHANNEL_EPOCH: u32 = (1_u32 << 24) - 1;
#[must_use]
pub fn zero_hop() -> Self {
Self {
index: 0,
amount: Some(U256::zero()),
win_prob: WinningProbability::NEVER,
channel_epoch: 0,
..Default::default()
}
}
#[must_use]
pub fn counterparty<A: Into<Address>>(mut self, counterparty: A) -> Self {
self.counterparty = Some(counterparty.into());
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: HoprBalance) -> 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 channel_epoch(mut self, channel_epoch: u32) -> Self {
self.channel_epoch = channel_epoch;
self
}
#[must_use]
pub fn win_prob(mut self, win_prob: WinningProbability) -> Self {
self.win_prob = win_prob;
self
}
#[must_use]
pub fn challenge(mut self, challenge: Challenge) -> Self {
self.challenge = Some(challenge.to_ethereum_challenge());
self
}
pub fn eth_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(&Self::MAX_TICKET_AMOUNT.into()) => {
HoprBalance::from(amount)
}
(None, Some(balance)) if balance.amount().lt(&Self::MAX_TICKET_AMOUNT.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 > Self::MAX_TICKET_INDEX {
return Err(InvalidInputData(
"cannot hold ticket indices larger than 2^48 - 1".into(),
));
}
if self.channel_epoch > Self::MAX_CHANNEL_EPOCH {
return Err(InvalidInputData(
"cannot hold channel epoch larger than 2^24 - 1".into(),
));
}
Ok(Ticket {
counterparty: self
.counterparty
.ok_or(InvalidInputData("missing channel id".into()))?,
amount,
index: self.index,
encoded_win_prob: self.win_prob.into(),
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 = signature.recover_from_hash(&hash)?.to_address();
let ticket = self.build()?;
Ok(VerifiedTicket {
hash,
issuer,
channel_id: generate_channel_id(&issuer, &ticket.counterparty),
ticket,
})
} else {
Err(InvalidInputData("signature is missing".into()))
}
}
}
impl From<&Ticket> for TicketBuilder {
fn from(value: &Ticket) -> Self {
Self {
counterparty: Some(value.counterparty),
amount: None,
balance: Some(value.amount),
index: value.index,
channel_epoch: value.channel_epoch,
win_prob: value.encoded_win_prob.into(),
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, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ticket {
pub counterparty: Address,
pub amount: HoprBalance, pub index: u64, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
pub signature: Option<Signature>,
}
impl Display for Ticket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ticket #{}, amount {}, epoch {} with {}",
self.index, self.amount, self.channel_epoch, self.counterparty
)
}
}
impl Ticket {
#[must_use]
pub fn builder() -> TicketBuilder {
TicketBuilder::default()
}
fn encode_tail_without_signature(&self) -> [u8; Self::SIZE - Address::SIZE - Signature::SIZE] {
let mut ret = [0u8; Self::SIZE - Address::SIZE - Signature::SIZE];
let mut offset = 0;
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 + 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
}
fn encode_for_transfer(&self) -> [u8; Self::SIZE - Signature::SIZE] {
let mut ret = [0u8; Self::SIZE - Signature::SIZE];
let mut offset = 0;
ret[offset..offset + Address::SIZE].copy_from_slice(self.counterparty.as_ref());
offset += Address::SIZE;
ret[offset..].copy_from_slice(&self.encode_tail_without_signature());
ret
}
fn encode_for_signing(
&self,
issuer: &Address,
) -> (ChannelId, [u8; ON_CHAIN_TICKET_SIZE - Signature::SIZE]) {
let mut ret = [0u8; ON_CHAIN_TICKET_SIZE - Signature::SIZE];
let mut offset = 0;
let channel_id = generate_channel_id(issuer, &self.counterparty);
ret[offset..offset + Hash::SIZE].copy_from_slice(channel_id.as_ref());
offset += Hash::SIZE;
ret[offset..].copy_from_slice(&self.encode_tail_without_signature());
(channel_id, ret)
}
fn get_hash(&self, issuer: &Address, domain_separator: &Hash) -> (ChannelId, Hash) {
let (channel_id, hash_struct) = self.encode_for_signing(issuer);
let ticket_hash = Hash::create(&[hash_struct.as_ref()]); let hash_struct = Hash::create(&[&REDEEM_CALL_SELECTOR, &[0u8; 28], ticket_hash.as_ref()]);
(
channel_id,
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 (channel_id, ticket_hash) =
self.get_hash(signing_key.public().as_ref(), domain_separator);
self.signature = Some(Signature::sign_hash(&ticket_hash, signing_key));
VerifiedTicket {
ticket: self,
hash: ticket_hash,
issuer: signing_key.public().to_address(),
channel_id,
}
}
#[instrument(level = "trace", skip_all, err)]
pub fn verify(
self,
issuer: &Address,
domain_separator: &Hash,
) -> Result<VerifiedTicket, Box<Ticket>> {
let (channel_id, ticket_hash) = self.get_hash(issuer, domain_separator);
if let Some(signature) = &self.signature {
match signature.recover_from_hash(&ticket_hash) {
Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket {
ticket: self,
hash: ticket_hash,
issuer: *issuer,
channel_id,
}),
Err(e) => {
error!("failed to verify ticket signature: {e}");
Err(self.into())
}
_ => Err(self.into()),
}
} else {
Err(self.into())
}
}
#[inline]
pub fn win_prob(&self) -> WinningProbability {
WinningProbability(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_for_transfer().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 counterparty = Address::try_from(&value[offset..offset + Address::SIZE])?;
offset += Address::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 channel_epoch = [0u8; 4];
channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
offset += 3;
let win_prob =
WinningProbability::try_from(&value[offset..offset + WinningProbability::SIZE])?;
offset += WinningProbability::SIZE;
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()
.counterparty(counterparty)
.amount(U256::from_big_endian(&amount))
.index(u64::from_be_bytes(index))
.channel_epoch(u32::from_be_bytes(channel_epoch))
.win_prob(win_prob)
.eth_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 = 48 + EthereumChallenge::SIZE + Signature::SIZE;
const ON_CHAIN_TICKET_SIZE: usize = 60 + EthereumChallenge::SIZE + Signature::SIZE;
impl BytesEncodable<TICKET_SIZE> for Ticket {}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VerifiedTicket {
ticket: Ticket,
hash: Hash,
issuer: Address,
channel_id: ChannelId,
}
impl VerifiedTicket {
#[inline]
pub fn win_prob(&self) -> WinningProbability {
self.ticket.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.hash, chain_keypair, domain_separator.as_ref())
{
check_ticket_win(
&self.hash,
self.ticket
.signature
.as_ref()
.expect("verified ticket have always a signature"),
&self.ticket.win_prob(),
response,
&vrf_params,
)
} else {
error!("cannot derive vrf parameters for {self}");
false
}
}
#[inline]
pub fn verified_ticket(&self) -> &Ticket {
&self.ticket
}
#[inline]
pub fn verified_hash(&self) -> &Hash {
&self.hash
}
#[inline]
pub fn verified_issuer(&self) -> &Address {
&self.issuer
}
#[inline]
pub fn channel_id(&self) -> &ChannelId {
&self.channel_id
}
pub fn verified_signature(&self) -> &Signature {
self.ticket
.signature
.as_ref()
.expect("verified ticket always has a signature")
}
#[inline]
pub fn leak(self) -> Ticket {
self.ticket
}
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 {} in channel {}", self.ticket, self.channel_id)
}
}
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 {
TicketId::from(self).cmp(&TicketId::from(other))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::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::internal::errors::Result<AcknowledgedTicket> {
let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
tracing::trace!(ticket = %self.ticket, response = response.to_hex(), "acknowledging ticket using response");
if self.ticket.verified_ticket().challenge
== response.to_challenge()?.to_ethereum_challenge()
{
Ok(self.ticket.into_acknowledged(response))
} else {
Err(CryptoError::InvalidChallenge.into())
}
}
}
impl Display for UnacknowledgedTicket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "unacknowledged {}", self.ticket)
}
}
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialEq,
strum::Display,
strum::EnumString,
num_enum::IntoPrimitive,
num_enum::TryFromPrimitive,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[strum(serialize_all = "PascalCase")]
pub enum AcknowledgedTicketStatus {
#[default]
Untouched = 0,
BeingRedeemed = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AcknowledgedTicket {
#[cfg_attr(feature = "serde", 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,
) -> 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(),
)?;
if !check_ticket_win(
self.ticket.verified_hash(),
self.ticket.verified_signature(),
&self.ticket.win_prob(),
&self.response,
&vrf_params,
) {
return Err(CoreTypesError::TicketNotWinning);
}
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, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::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()
}
#[inline]
pub fn ticket_id(&self) -> TicketId {
TicketId::from(&self.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 Eq for RedeemableTicket {}
impl PartialOrd for RedeemableTicket {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RedeemableTicket {
fn cmp(&self, other: &Self) -> Ordering {
self.ticket.cmp(&other.ticket)
}
}
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, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::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(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().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(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 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 crate::crypto::{
keypairs::{ChainKeypair, Keypair},
types::{HalfKey, Hash, Response},
};
use crate::crypto_random::Randomizable;
use crate::primitive::{
prelude::UnitaryFloatOps,
primitives::{Address, EthereumChallenge, U256},
};
use hex_literal::hex;
use super::*;
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() -> anyhow::Result<()> {
assert_eq!(0.0f64, WinningProbability::NEVER.as_f64());
assert_eq!(1.0f64, WinningProbability::ALWAYS.as_f64());
let mut test_bit_string = [0xffu8; 7];
test_bit_string[0] = 0x7f;
assert_eq!(0.5f64, WinningProbability::from(&test_bit_string).as_f64());
test_bit_string[0] = 0x3f;
assert_eq!(0.25f64, WinningProbability::from(&test_bit_string).as_f64());
test_bit_string[0] = 0x1f;
assert_eq!(
0.125f64,
WinningProbability::from(&test_bit_string).as_f64()
);
Ok(())
}
#[test]
pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
assert_eq!([0u8; 7], WinningProbability::try_from(0.0f64)?.as_encoded());
let mut test_bit_string = [0xffu8; 7];
assert_eq!(
test_bit_string,
WinningProbability::try_from(1.0f64)?.as_encoded()
);
test_bit_string[0] = 0x7f;
assert_eq!(
test_bit_string,
WinningProbability::try_from(0.5f64)?.as_encoded()
);
test_bit_string[0] = 0x3f;
assert_eq!(
test_bit_string,
WinningProbability::try_from(0.25f64)?.as_encoded()
);
test_bit_string[0] = 0x1f;
assert_eq!(
test_bit_string,
WinningProbability::try_from(0.125f64)?.as_encoded()
);
Ok(())
}
#[test]
pub fn test_win_prob_approx_eq() -> anyhow::Result<()> {
let wp_0 = WinningProbability(hex!("0020C49BBFFFFF"));
let wp_1 = WinningProbability(hex!("0020C49BA5E34F"));
assert_ne!(wp_0, wp_1.as_encoded());
assert!(!wp_0.lex_eq(&wp_1));
assert_eq!(wp_0, wp_1.as_f64());
assert!(wp_0.approx_eq(&wp_1));
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 - WinningProbability::try_from_f64(float)?.as_f64()).abs() < f64::EPSILON
);
}
Ok(())
}
#[test]
pub fn test_win_prob_must_be_correctly_approx_ordered() {
let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
assert!(prev.approx_cmp(&next).is_lt());
prev = next;
}
}
#[test]
pub fn test_win_prob_must_be_correctly_lex_ordered() {
let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
assert!(prev.lex_cmp(&next).is_lt());
prev = next;
}
}
#[test]
pub fn test_win_prob_epsilon_must_be_never() -> anyhow::Result<()> {
assert!(
WinningProbability::NEVER.approx_eq(&WinningProbability::try_from_f64(
WinningProbability::EPSILON
)?)
);
assert!(
WinningProbability::NEVER.lex_eq(&WinningProbability::try_from_f64(
WinningProbability::EPSILON
)?)
);
Ok(())
}
#[test]
pub fn test_win_prob_bounds_must_be_approx_eq() -> anyhow::Result<()> {
let bound = 0.1 + WinningProbability::EPSILON;
let other = 0.1;
assert!(
WinningProbability::try_from_f64(bound)?
.approx_eq(&WinningProbability::try_from_f64(other)?)
);
Ok(())
}
#[test]
pub fn test_win_prob_bounds_must_not_be_approx_eq_when_differ_by_more_then_epsilon()
-> anyhow::Result<()> {
let bound = 0.1 + 1.1 * WinningProbability::EPSILON;
let other = 0.1;
assert!(
!WinningProbability::try_from_f64(bound)?
.approx_eq(&WinningProbability::try_from_f64(other)?)
);
Ok(())
}
#[test]
pub fn test_win_prob_bounds_must_not_be_lex_eq() -> anyhow::Result<()> {
let bound = 0.1 + WinningProbability::EPSILON;
let other = 0.1;
assert!(
!WinningProbability::try_from_f64(bound)?
.lex_eq(&WinningProbability::try_from_f64(other)?)
);
Ok(())
}
#[test]
pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
let ticket = TicketBuilder::zero_hop()
.counterparty(&*BOB)
.eth_challenge(Default::default())
.build()?;
assert_eq!(0, ticket.index);
assert_eq!(0.0, ticket.win_prob().as_f64());
assert_eq!(0, ticket.channel_epoch);
Ok(())
}
#[test]
pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.counterparty(&*BOB)
.balance(1.into())
.index(0)
.win_prob(1.0.try_into()?)
.channel_epoch(1)
.eth_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()).into();
assert_eq!(
initial_ticket.verified_ticket(),
&Ticket::try_from(ticket_bytes.as_ref())?
);
Ok(())
}
#[test]
#[cfg(feature = "serde")]
pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.counterparty(&*BOB)
.balance(1.into())
.index(0)
.win_prob(1.0.try_into()?)
.channel_epoch(1)
.eth_challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert_eq!(
initial_ticket,
postcard::from_bytes(&postcard::to_allocvec(&initial_ticket)?)?
);
Ok(())
}
#[test]
pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
let initial_ticket = TicketBuilder::default()
.counterparty(&*BOB)
.balance(1.into())
.index(0)
.win_prob(1.0.try_into()?)
.channel_epoch(1)
.eth_challenge(Default::default())
.build_signed(&ALICE, &Default::default())?;
assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
assert_eq!(
initial_ticket.channel_id(),
&generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address())
);
let ticket = initial_ticket.leak();
assert!(
ticket
.verify(&ALICE.public().to_address(), &Default::default())
.is_ok()
);
Ok(())
}
#[test]
pub fn test_zero_hop() -> anyhow::Result<()> {
let ticket = TicketBuilder::zero_hop()
.counterparty(&*BOB)
.eth_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()
.counterparty(*counterparty)
.amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
.index(0)
.win_prob(1.0.try_into()?)
.channel_epoch(4)
.eth_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 challenge = Response::from_half_keys(&hk1, &hk2)?.to_challenge()?;
let dst = Hash::default();
let ack = mock_ticket(
&ALICE,
&BOB.public().to_address(),
Some(dst),
Some(challenge.to_ethereum_challenge()),
)?
.into_unacknowledged(hk1)
.acknowledge(&hk2)?;
assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
Ok(())
}
#[test]
#[cfg(feature = "serde")]
fn test_acknowledged_ticket_serde() -> 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()?.to_ethereum_challenge()),
)?;
let acked_ticket = ticket.into_acknowledged(response);
let mut deserialized_ticket = postcard::from_bytes(&postcard::to_allocvec(&acked_ticket)?)?;
assert_eq!(acked_ticket, deserialized_ticket);
assert!(deserialized_ticket.is_winning(&BOB, &dst));
deserialized_ticket.status = AcknowledgedTicketStatus::BeingRedeemed;
assert_eq!(
deserialized_ticket,
postcard::from_bytes(&postcard::to_allocvec(&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()
.counterparty(&*BOB)
.balance(1.into())
.index(0)
.win_prob(1.0.try_into()?)
.channel_epoch(1)
.challenge(resp.to_challenge()?)
.build_signed(&ALICE, &Default::default())?;
let unack = verified.into_unacknowledged(hk1);
let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
let redeemable_1 = acknowledged.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(())
}
}