use bitcoin::secp256k1::{SecretKey, XOnlyPublicKey};
use bitcoin::{Amount, OutPoint};
use sidestr_core::sighash::SighashRules;
use super::machine::{ChannelMachine, Keys, ProtocolState};
use super::{
pop_context, public, AcceptMessage, AcceptTag, ChannelId, CommitMessage, CommitTag,
FundingOffer, NodeId, OpenMessage, OpenTag, PopPlace, ProtocolError, ReadyMessage, ReadyTag,
RevocationProofs, SyncMessage, SyncTag, WireSide, MAX_HUB_FEE, MAX_SATS, MIN_OPEN,
};
use crate::{
pop_sign, pop_verify, revocation_pub, Balances, Channel, ChannelState, RevocationKeys, Side,
DUST,
};
pub struct OpeningKeys {
pub channel: SecretKey,
pub revocation_base: SecretKey,
pub revocation: [SecretKey; 2],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpenParams {
pub funding: OutPoint,
pub funding_value: Amount,
pub push: Amount,
pub delay: u16,
pub fee: Amount,
pub hub_fee: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AcceptPolicy {
pub min_delay: u16,
pub hub_fee: Option<u64>,
}
impl Default for AcceptPolicy {
fn default() -> Self {
Self {
min_delay: crate::MIN_DELAY,
hub_fee: None,
}
}
}
#[derive(Clone)]
pub struct FunderOpening {
id: ChannelId,
channel: Channel,
keys: Keys,
initial_state: ProtocolState,
message: OpenMessage,
rules: SighashRules,
}
#[derive(Clone)]
pub struct AcceptedFunder {
opening: FunderOpening,
their_revocation: [XOnlyPublicKey; 2],
their_revocation_base: XOnlyPublicKey,
their_signature_zero: crate::LeafSignature,
peer_hub_fee: Option<u64>,
commit: CommitMessage,
}
#[derive(Clone)]
pub struct ReceiverOpening {
id: ChannelId,
channel: Channel,
keys: Keys,
initial_state: ProtocolState,
their_revocation: [XOnlyPublicKey; 2],
their_revocation_base: XOnlyPublicKey,
peer_hub_fee: Option<u64>,
message: AcceptMessage,
rules: SighashRules,
}
pub enum FunderSync {
ResendCommit(CommitMessage),
Ready(Box<ChannelMachine>),
Waiting,
}
fn proofs(id: ChannelId, role: WireSide, keys: &Keys, aux: &[u8; 32]) -> RevocationProofs {
RevocationProofs {
base: pop_sign(
&keys.revocation_base,
&pop_context(id, role, PopPlace::Base),
aux,
),
rev: [
pop_sign(
&keys.revocation[0],
&pop_context(id, role, PopPlace::State(0)),
aux,
),
pop_sign(
&keys.revocation[1],
&pop_context(id, role, PopPlace::State(1)),
aux,
),
],
}
}
fn proofs_hold(
id: ChannelId,
role: WireSide,
base: &NodeId,
rev: &[NodeId; 2],
pop: &RevocationProofs,
) -> bool {
pop_verify(&base.0, &pop.base, &pop_context(id, role, PopPlace::Base))
&& pop_verify(
&rev[0].0,
&pop.rev[0],
&pop_context(id, role, PopPlace::State(0)),
)
&& pop_verify(
&rev[1].0,
&pop.rev[1],
&pop_context(id, role, PopPlace::State(1)),
)
}
fn opening_state(
state: &ProtocolState,
a_view: XOnlyPublicKey,
b_view: XOnlyPublicKey,
) -> ChannelState {
ChannelState {
balances: Balances {
a: Amount::from_sat(state.balance_a),
b: Amount::from_sat(state.balance_b),
},
revocation: RevocationKeys {
a: a_view,
b: b_view,
},
htlcs: vec![],
}
}
impl FunderOpening {
pub fn propose(
params: OpenParams,
keys: OpeningKeys,
peer: XOnlyPublicKey,
rules: SighashRules,
aux: &[u8; 32],
) -> Result<(Self, OpenMessage), ProtocolError> {
validate_hub_fee(params.hub_fee)?;
if params.funding.vout > u32::from(u16::MAX) {
return Err(ProtocolError::FundingVout);
}
if !(MIN_OPEN..=MAX_SATS).contains(¶ms.funding_value.to_sat()) {
return Err(ProtocolError::FundingValue);
}
let mine = public(&keys.channel);
if mine == peer {
return Err(ProtocolError::SamePeer);
}
validate_push(params.funding_value, params.push, params.fee)?;
let channel = Channel::new(
mine,
peer,
params.funding,
params.funding_value,
params.fee,
params.delay,
)?;
let id = ChannelId::from_txid(params.funding.txid);
let initial_state = initial_state(params.funding_value, params.push)?;
let keys = Keys::from(keys);
let message = OpenMessage {
t: OpenTag::Open,
id,
funding: FundingOffer {
txid: params.funding.txid,
vout: params.funding.vout,
value: params.funding_value.to_sat(),
},
push: params.push.to_sat(),
delay: params.delay,
fee: params.fee.to_sat(),
a: NodeId(mine),
b: NodeId(peer),
rev: [
NodeId(public(&keys.revocation[0])),
NodeId(public(&keys.revocation[1])),
],
rev_base: NodeId(public(&keys.revocation_base)),
pop: proofs(id, WireSide::A, &keys, aux),
hub_fee: params.hub_fee,
};
Ok((
Self {
id,
channel,
keys,
initial_state,
message: message.clone(),
rules,
},
message,
))
}
pub fn id(&self) -> ChannelId {
self.id
}
pub fn message(&self) -> &OpenMessage {
&self.message
}
pub fn accept(
&self,
message: AcceptMessage,
aux: &[u8; 32],
) -> Result<(AcceptedFunder, CommitMessage), ProtocolError> {
message.well_formed()?;
if message.id != self.id {
return Err(ProtocolError::WrongChannel);
}
if !proofs_hold(
self.id,
WireSide::B,
&message.rev_base,
&message.rev,
&message.pop,
) {
return Err(ProtocolError::MissingProof);
}
let their_base = message.rev_base.0;
let theirs = [message.rev[0].0, message.rev[1].0];
let state = opening_state(
&self.initial_state,
revocation_pub(their_base, &self.keys.revocation[0])?,
revocation_pub(theirs[0], &self.keys.revocation_base)?,
);
let mine = self.channel.commitment(0, Side::A, &state)?;
if !self.channel.verify_funding(
&mine.tx,
&self.channel.key(Side::B),
&message.sig,
self.rules,
) {
return Err(ProtocolError::BadSignature);
}
let theirs_commitment = self.channel.commitment(0, Side::B, &state)?;
let signature = self.channel.sign_funding(
&theirs_commitment.tx,
&self.keys.channel,
self.rules,
aux,
)?;
let commit = CommitMessage {
t: CommitTag::Commit,
id: self.id,
sig: signature,
};
Ok((
AcceptedFunder {
opening: self.clone(),
their_revocation: theirs,
their_revocation_base: their_base,
their_signature_zero: message.sig,
peer_hub_fee: message.hub_fee,
commit: commit.clone(),
},
commit,
))
}
}
impl AcceptedFunder {
pub fn id(&self) -> ChannelId {
self.opening.id
}
pub fn commit_message(&self) -> &CommitMessage {
&self.commit
}
pub fn sync_message(&self) -> SyncMessage {
SyncMessage {
t: SyncTag::Sync,
id: self.opening.id,
n: 0,
status: Some(super::ChannelStatus::Funding.as_str().into()),
pending_n: None,
reveal: None,
missing: vec![],
reveals: None,
}
}
pub fn ready(&self, message: ReadyMessage) -> Result<ChannelMachine, ProtocolError> {
if message.id != self.opening.id {
return Err(ProtocolError::WrongChannel);
}
self.machine()
}
pub fn receive_sync(&self, message: &SyncMessage) -> Result<FunderSync, ProtocolError> {
message.well_formed()?;
if message.id != self.opening.id {
return Err(ProtocolError::WrongChannel);
}
match message.status.as_deref() {
Some("accepted") => Ok(FunderSync::ResendCommit(self.commit.clone())),
Some("funding" | "open") => Ok(FunderSync::Ready(Box::new(self.machine()?))),
_ => Ok(FunderSync::Waiting),
}
}
fn machine(&self) -> Result<ChannelMachine, ProtocolError> {
ChannelMachine::new(
self.opening.id,
Side::A,
self.opening.channel.clone(),
self.opening.initial_state.clone(),
self.opening.keys.clone(),
self.their_revocation_base,
self.their_revocation,
self.their_signature_zero,
self.peer_hub_fee,
self.opening.rules,
)
}
}
impl ReceiverOpening {
pub fn accept(
message: OpenMessage,
authenticated_sender: XOnlyPublicKey,
keys: OpeningKeys,
rules: SighashRules,
policy: AcceptPolicy,
aux: &[u8; 32],
) -> Result<(Self, AcceptMessage), ProtocolError> {
validate_open(
&message,
authenticated_sender,
public(&keys.channel),
policy,
)?;
let id = message.id;
if !proofs_hold(
id,
WireSide::A,
&message.rev_base,
&message.rev,
&message.pop,
) {
return Err(ProtocolError::MissingProof);
}
let outpoint = OutPoint {
txid: message.funding.txid,
vout: message.funding.vout,
};
let value = Amount::from_sat(message.funding.value);
let channel = Channel::new(
message.a.0,
message.b.0,
outpoint,
value,
Amount::from_sat(message.fee),
message.delay,
)?;
let state = initial_state(value, Amount::from_sat(message.push))?;
let keys = Keys::from(keys);
let their_base = message.rev_base.0;
let theirs = [message.rev[0].0, message.rev[1].0];
let full = opening_state(
&state,
revocation_pub(theirs[0], &keys.revocation_base)?,
revocation_pub(their_base, &keys.revocation[0])?,
);
let commitment = channel.commitment(0, Side::A, &full)?;
let signature = channel.sign_funding(&commitment.tx, &keys.channel, rules, aux)?;
let response = AcceptMessage {
t: AcceptTag::Accept,
id,
rev: [
NodeId(public(&keys.revocation[0])),
NodeId(public(&keys.revocation[1])),
],
rev_base: NodeId(public(&keys.revocation_base)),
pop: proofs(id, WireSide::B, &keys, aux),
sig: signature,
hub_fee: policy.hub_fee,
};
Ok((
Self {
id,
channel,
keys,
initial_state: state,
their_revocation: theirs,
their_revocation_base: their_base,
peer_hub_fee: message.hub_fee,
message: response.clone(),
rules,
},
response,
))
}
pub fn id(&self) -> ChannelId {
self.id
}
pub fn message(&self) -> &AcceptMessage {
&self.message
}
pub fn commit(
&self,
message: CommitMessage,
) -> Result<(ChannelMachine, ReadyMessage), ProtocolError> {
if message.id != self.id {
return Err(ProtocolError::WrongChannel);
}
let ready = ReadyMessage {
t: ReadyTag::Ready,
id: self.id,
};
let machine = ChannelMachine::new(
self.id,
Side::B,
self.channel.clone(),
self.initial_state.clone(),
self.keys.clone(),
self.their_revocation_base,
self.their_revocation,
message.sig,
self.peer_hub_fee,
self.rules,
)?;
Ok((machine, ready))
}
}
fn validate_open(
message: &OpenMessage,
sender: XOnlyPublicKey,
receiver: XOnlyPublicKey,
policy: AcceptPolicy,
) -> Result<(), ProtocolError> {
message.well_formed()?;
if message.a.0 != sender || message.b.0 != receiver {
return Err(ProtocolError::WrongPeer);
}
if message.delay < policy.min_delay {
return Err(ProtocolError::DelayBelowPolicy);
}
validate_hub_fee(policy.hub_fee)?;
validate_push(
Amount::from_sat(message.funding.value),
Amount::from_sat(message.push),
Amount::from_sat(message.fee),
)
}
fn validate_hub_fee(fee: Option<u64>) -> Result<(), ProtocolError> {
if fee.is_some_and(|value| value > MAX_HUB_FEE) {
return Err(ProtocolError::HubFee);
}
Ok(())
}
fn validate_push(funding: Amount, push: Amount, fee: Amount) -> Result<(), ProtocolError> {
let minimum = fee
.to_sat()
.checked_add(DUST)
.ok_or(ProtocolError::BadPush)?;
let maximum = funding
.to_sat()
.checked_sub(minimum)
.ok_or(ProtocolError::BadPush)?;
if push.to_sat() > maximum {
return Err(ProtocolError::BadPush);
}
Ok(())
}
fn initial_state(funding: Amount, push: Amount) -> Result<ProtocolState, ProtocolError> {
let balance_a = funding
.to_sat()
.checked_sub(push.to_sat())
.ok_or(ProtocolError::BadPush)?;
Ok(ProtocolState {
balance_a,
balance_b: push.to_sat(),
htlcs: vec![],
})
}