use bitcoin::secp256k1::{Keypair, SecretKey, XOnlyPublicKey};
use bitcoin::Transaction;
use serde::{Deserialize, Serialize};
use sidestr_core::block::secp;
mod chain;
mod machine;
mod opening;
mod snapshot;
mod wire;
pub use chain::{ChainSpend, ClaimKind, ClaimRecord, FollowedOutput, OutputLookup, SpendOutcome};
pub use machine::{
AckOutcome, AwaitingRevoke, Bound, ChannelMachine, DroppedUpdate, FinalisedUpdate,
FundingSpend, PendingUpdate, ProtocolState, ReceiveUpdate, SignedAlternative, SyncOutcome,
TickAction, TickOptions,
};
pub use opening::{
AcceptPolicy, AcceptedFunder, FunderOpening, FunderSync, OpenParams, OpeningKeys,
ReceiverOpening,
};
pub use snapshot::ChannelSnapshot;
pub use wire::{
AcceptMessage, AcceptTag, AckMessage, AckTag, Bytes32, ChannelId, CloseMessage, CloseTag,
CommitMessage, CommitTag, FundingOffer, NodeId, OfferedHtlc, OpenMessage, OpenTag, PeerMessage,
ProtocolHtlc, ReadyMessage, ReadyTag, RejectMessage, RejectTag, RevocationProofs,
RevokeMessage, RevokeTag, Route, SyncMessage, SyncTag, Update, UpdateMessage, UpdateTag,
WireSide,
};
pub const KIND: u16 = 23_600;
pub const MIN_OPEN: u64 = 10_000;
pub const MIN_HTLC: u64 = 1_000;
pub const EXPIRY_MARGIN: u32 = 12;
pub const CLAIM_MARGIN: u32 = 3;
pub const CLOSE_DEPTH: u32 = 6;
pub const MEMO_MAX: usize = 140;
pub const FUNDING_TIMEOUT: u64 = 24 * 3_600;
pub const PROPOSAL_TIMEOUT: u64 = 6 * 3_600;
pub const UNFUNDED_TIMEOUT: u64 = 7 * 24 * 3_600;
pub const MIN_CONF: u32 = 2;
pub const DEFAULT_PENDING_TIMEOUT: u64 = 90;
pub const MAX_PENDING_BACKOFF: u64 = 600;
pub const MAX_SATS: u64 = 2_100_000_000_000_000;
pub const MAX_HUB_FEE: u64 = 100_000;
const MAX_SYNC_ITEMS: usize = 64;
const MAX_STATUS_LEN: usize = 32;
const AWAITING_RESYNC: u64 = 60;
const RESYNC_INTERVAL: u64 = 30;
const CLOSE_REBROADCAST: u32 = 3;
const CLAIM_REBROADCAST: u32 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Context {
pub height: u32,
pub now: u64,
pub aux: [u8; 32],
pub stale: bool,
}
impl Context {
pub fn new(height: u32, now: u64, aux: [u8; 32]) -> Self {
Self {
height,
now,
aux,
stale: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Broadcast {
pub label: String,
pub tx: Transaction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChannelStatus {
Funding,
Open,
ClosingAsked,
Closing,
ForceClosing,
ClosedMine,
ClosedTheirs,
ClosedTheirsAlt,
Punishing,
Settling,
ClosedCoop,
Closed,
Punished,
SpentUnknown,
}
impl ChannelStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Funding => "funding",
Self::Open => "open",
Self::ClosingAsked => "closing-asked",
Self::Closing => "closing",
Self::ForceClosing => "force-closing",
Self::ClosedMine => "closed-mine",
Self::ClosedTheirs => "closed-theirs",
Self::ClosedTheirsAlt => "closed-theirs-alt",
Self::Punishing => "punishing",
Self::Settling => "settling",
Self::ClosedCoop => "closed-coop",
Self::Closed => "closed",
Self::Punished => "punished",
Self::SpentUnknown => "spent-unknown",
}
}
pub fn is_live(self) -> bool {
matches!(self, Self::Funding | Self::Open | Self::ClosingAsked)
}
pub fn is_watched(self) -> bool {
!self.is_terminal()
}
pub fn is_following(self) -> bool {
matches!(
self,
Self::ClosedMine
| Self::ClosedTheirs
| Self::ClosedTheirsAlt
| Self::Punishing
| Self::Settling
| Self::ClosedCoop
)
}
pub fn is_terminal(self) -> bool {
matches!(self, Self::Closed | Self::Punished | Self::SpentUnknown)
}
pub fn is_closed_out(self) -> bool {
matches!(
self,
Self::ForceClosing | Self::Closing | Self::ClosingAsked
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentOutcome {
InFlight,
Sent,
Failed,
NotMade,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChannelEvent {
Dropped(DroppedUpdate),
Preimage {
hash: Bytes32,
preimage: Bytes32,
},
Payment {
hash: Bytes32,
outcome: PaymentOutcome,
reason: Option<String>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("malformed message: {0}")]
Malformed(&'static str),
#[error("message is for another channel")]
WrongChannel,
#[error("channel key does not match this peer's role")]
WrongChannelKey,
#[error("unsupported channel snapshot version {0}")]
SnapshotVersion(u8),
#[error("corrupt channel snapshot: {0}")]
CorruptSnapshot(&'static str),
#[error("funding output index exceeds 65535")]
FundingVout,
#[error("funding value is below {MIN_OPEN} sats or above 21 million coins")]
FundingValue,
#[error("the two channel peers must differ")]
SamePeer,
#[error("push must leave the funder its fee and dust")]
BadPush,
#[error("opening message peer does not match its authenticated envelope")]
WrongPeer,
#[error("channel delay is below the receiver's minimum")]
DelayBelowPolicy,
#[error("hub fee exceeds 100000 sats")]
HubFee,
#[error("a revocation point comes without proof of its secret")]
MissingProof,
#[error("the channel is {0}")]
Status(&'static str),
#[error("state {got}, expected {expected}")]
UnexpectedState {
got: u64,
expected: u64,
},
#[error("an update is already pending")]
Pending,
#[error("no matching update is pending")]
NoPending,
#[error("waiting for their revocation of the previous state")]
AwaitingRevocation,
#[error("an update is in flight; try again when it settles")]
InFlight,
#[error("their revocation secret for state {0} is missing; resyncing first")]
MissingReveal(u64),
#[error("revocation key for state {0} is missing")]
MissingRevocationKey(u64),
#[error("state {0} is missing")]
MissingState(u64),
#[error("state {0} was never signed by them")]
MissingSignature(u64),
#[error("signature on the commitment does not verify")]
BadSignature,
#[error("revealed secret is not the announced revocation key")]
BadRevocation,
#[error("my view of the chain is stale; HTLCs wait until it returns")]
StaleChain,
#[error("that payment hash is still bound on this channel ({0})")]
HashBound(Bound),
#[error("at most {0} sat")]
TooMuch(u64),
#[error("payment amount must be positive")]
ZeroAmount,
#[error("a memo is at most {MEMO_MAX} characters")]
MemoTooLong,
#[error("HTLC amount is below {MIN_HTLC} sats")]
HtlcTooSmall,
#[error("bad HTLC expiry")]
BadExpiry,
#[error("bad htlc id")]
BadHtlcId,
#[error("the htlc would expire too soon")]
ExpiryTooSoon,
#[error("an htlc with that hash is already in flight")]
DuplicatePaymentHash,
#[error("a balance would go negative")]
NegativeBalance,
#[error("balance overflow")]
BalanceOverflow,
#[error("the funder must keep its fee")]
FunderFee,
#[error("no such htlc")]
NoSuchHtlc,
#[error("the offerer cannot settle")]
OffererCannotSettle,
#[error("wrong preimage")]
WrongPreimage,
#[error("the htlc has expired; it settles on the chain or not at all")]
HtlcExpired,
#[error("the offerer may fail only after the expiry")]
OffererFailBeforeExpiry,
#[error("I hold the preimage: it settles, on the chain if need be")]
KnownPreimage,
#[error("the funding is not confirmed; there is nothing to close yet")]
Unfunded,
#[error(transparent)]
Channel(#[from] crate::Error),
}
impl ProtocolError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::Pending | Self::AwaitingRevocation | Self::MissingReveal(_)
)
}
}
pub(crate) fn public(secret: &SecretKey) -> XOnlyPublicKey {
Keypair::from_secret_key(secp(), secret)
.x_only_public_key()
.0
}
pub(crate) fn secret(bytes: Bytes32) -> Result<SecretKey, ProtocolError> {
SecretKey::from_slice(&bytes.0).map_err(|_| ProtocolError::BadRevocation)
}
pub(crate) fn sha(preimage: &[u8; 32]) -> [u8; 32] {
use bitcoin::hashes::{sha256, Hash};
sha256::Hash::hash(preimage).to_byte_array()
}
pub(crate) fn node_less(a: XOnlyPublicKey, b: XOnlyPublicKey) -> bool {
a.serialize() < b.serialize()
}
pub(crate) fn encode_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
output.push(HEX[usize::from(byte >> 4)] as char);
output.push(HEX[usize::from(byte & 0x0f)] as char);
}
output
}
pub(crate) fn decode_hex(text: &str, output: &mut [u8]) -> Option<()> {
if text.len() != output.len() * 2 {
return None;
}
for (target, pair) in output.iter_mut().zip(text.as_bytes().chunks_exact(2)) {
*target = (nibble(pair[0])? << 4) | nibble(pair[1])?;
}
Some(())
}
fn nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}
pub(crate) fn pop_context(id: ChannelId, owner: WireSide, place: PopPlace) -> String {
let role = match owner {
WireSide::A => "a",
WireSide::B => "b",
};
match place {
PopPlace::Base => format!("{id}/{role}/base"),
PopPlace::State(n) => format!("{id}/{role}/{n}"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PopPlace {
Base,
State(u64),
}
#[cfg(test)]
mod tests;