use std::fmt::Debug;
use std::ops::Range;
#[cfg(feature = "serde")]
use amplify::ToYamlString;
use lnp2p::bolt::{AcceptChannel, ChannelType, OpenChannel};
pub const BOLT3_MAX_ACCEPTED_HTLC_LIMIT: u16 = 483;
pub const BOLT3_DUST_LIMIT: u64 = 354;
#[derive(
Clone,
Copy,
PartialEq,
Eq,
Hash,
Debug,
Display,
Error,
StrictEncode,
StrictDecode
)]
#[display(doc_comments)]
pub enum PolicyError {
ToSelfDelayUnreasonablyLarge { proposed: u16, allowed_maximum: u16 },
MaxAcceptedHtlcLimitExceeded(u16),
FeeRateUnreasonable {
proposed: u32,
lowest_accepted: u32,
highest_accepted: u32,
},
ChannelReserveLessDust { reserve: u64, dust_limit: u64 },
DustLimitTooSmall(u64),
ChannelFundingTooSmall {
proposed: u64,
required_minimum: u64,
},
HtlcMinimumTooLarge { proposed: u64, allowed_maximum: u64 },
HtlcInFlightMaximumTooSmall {
proposed: u64,
required_minimum: u64,
},
ChannelReserveTooLarge { proposed: u64, allowed_maximum: u64 },
MaxAcceptedHtlcsTooSmall {
proposed: u16,
required_minimum: u16,
},
DustLimitTooLarge { proposed: u64, allowed_maximum: u64 },
UnreasonableMinDepth { proposed: u32, allowed_maximum: u32 },
LocalDustExceedsRemoteReserve {
channel_reserve: u64,
dust_limit: u64,
},
RemoteDustExceedsLocalReserve {
channel_reserve: u64,
dust_limit: u64,
},
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, StrictEncode, StrictDecode)]
#[cfg_attr(
feature = "serde",
derive(Display, Serialize, Deserialize),
serde(crate = "serde_crate"),
display(Policy::to_yaml_string)
)]
pub struct Policy {
pub to_self_delay_max: u16,
pub feerate_per_kw_range: Range<u32>,
pub minimum_depth: u32,
pub maximum_depth: Option<u32>,
pub funding_satoshis_min: Option<u64>,
pub htlc_minimum_msat_max: Option<u64>,
pub max_htlc_value_in_flight_msat_min: Option<u64>,
pub channel_reserve_satoshis_max_abs: Option<u64>,
pub channel_reserve_satoshis_max_percent: Option<u8>,
pub max_accepted_htlcs_min: Option<u16>,
pub dust_limit_satoshis_max: Option<u64>,
}
#[cfg(feature = "serde")]
impl ToYamlString for Policy {}
impl Default for Policy {
fn default() -> Policy {
Policy {
to_self_delay_max: 250,
feerate_per_kw_range: 1..100,
minimum_depth: 3,
maximum_depth: Some(6),
funding_satoshis_min: Some(10000),
htlc_minimum_msat_max: None,
max_htlc_value_in_flight_msat_min: Some(10000),
max_accepted_htlcs_min: Some(10),
channel_reserve_satoshis_max_abs: None,
channel_reserve_satoshis_max_percent: Some(10),
dust_limit_satoshis_max: Some(1000),
}
}
}
impl Policy {
pub fn with_clightning_defaults() -> Policy {
Policy {
to_self_delay_max: 14 * 24 * 6,
feerate_per_kw_range: 1..1000,
minimum_depth: 3,
maximum_depth: Some(6),
funding_satoshis_min: Some(10000),
htlc_minimum_msat_max: None,
max_htlc_value_in_flight_msat_min: Some(10000),
max_accepted_htlcs_min: Some(10),
channel_reserve_satoshis_max_abs: None,
channel_reserve_satoshis_max_percent: Some(10),
dust_limit_satoshis_max: Some(546),
}
}
pub fn with_lnd_defaults() -> Policy {
Policy {
to_self_delay_max: 14 * 24 * 6,
feerate_per_kw_range: 1..1000,
minimum_depth: 3,
maximum_depth: Some(6),
funding_satoshis_min: Some(20000),
htlc_minimum_msat_max: None,
max_htlc_value_in_flight_msat_min: Some(10000),
max_accepted_htlcs_min: Some(10),
channel_reserve_satoshis_max_abs: None,
channel_reserve_satoshis_max_percent: Some(1),
dust_limit_satoshis_max: Some(546),
}
}
pub fn with_eclair_defaults() -> Policy {
Policy {
to_self_delay_max: 14 * 24 * 6,
feerate_per_kw_range: 1..1000,
minimum_depth: 3,
maximum_depth: Some(6),
funding_satoshis_min: Some(100000),
htlc_minimum_msat_max: None,
max_htlc_value_in_flight_msat_min: Some(10000),
max_accepted_htlcs_min: Some(10),
channel_reserve_satoshis_max_abs: None,
channel_reserve_satoshis_max_percent: Some(5),
dust_limit_satoshis_max: Some(546),
}
}
fn validate_peer_params(
&self,
params: PeerParams,
) -> Result<(), PolicyError> {
if params.to_self_delay > self.to_self_delay_max {
return Err(PolicyError::ToSelfDelayUnreasonablyLarge {
proposed: params.to_self_delay,
allowed_maximum: self.to_self_delay_max,
});
}
if params.max_accepted_htlcs > BOLT3_MAX_ACCEPTED_HTLC_LIMIT {
return Err(PolicyError::MaxAcceptedHtlcLimitExceeded(
params.max_accepted_htlcs,
));
}
if params.dust_limit_satoshis > params.channel_reserve_satoshis {
return Err(PolicyError::ChannelReserveLessDust {
reserve: params.channel_reserve_satoshis,
dust_limit: params.dust_limit_satoshis,
});
}
if params.dust_limit_satoshis < BOLT3_DUST_LIMIT {
return Err(PolicyError::DustLimitTooSmall(
params.dust_limit_satoshis,
));
}
if let Some(limit) = self.htlc_minimum_msat_max {
if params.htlc_minimum_msat > limit {
return Err(PolicyError::HtlcMinimumTooLarge {
proposed: params.htlc_minimum_msat,
allowed_maximum: limit,
});
}
}
if let Some(limit) = self.max_htlc_value_in_flight_msat_min {
if params.max_htlc_value_in_flight_msat < limit {
return Err(PolicyError::HtlcInFlightMaximumTooSmall {
proposed: params.max_htlc_value_in_flight_msat,
required_minimum: limit,
});
}
}
if let Some(limit) = self.channel_reserve_satoshis_max_abs {
if params.channel_reserve_satoshis > limit {
return Err(PolicyError::ChannelReserveTooLarge {
proposed: params.channel_reserve_satoshis,
allowed_maximum: limit,
});
}
}
if let Some(limit) = self.max_accepted_htlcs_min {
if params.max_accepted_htlcs < limit {
return Err(PolicyError::MaxAcceptedHtlcsTooSmall {
proposed: params.max_accepted_htlcs,
required_minimum: limit,
});
}
}
if let Some(limit) = self.dust_limit_satoshis_max {
if params.dust_limit_satoshis > limit {
return Err(PolicyError::DustLimitTooLarge {
proposed: params.dust_limit_satoshis,
allowed_maximum: limit,
});
}
}
Ok(())
}
pub fn validate_inbound(
&self,
open_channel: &OpenChannel,
) -> Result<PeerParams, PolicyError> {
if !self
.feerate_per_kw_range
.contains(&open_channel.feerate_per_kw)
{
return Err(PolicyError::FeeRateUnreasonable {
proposed: open_channel.feerate_per_kw,
lowest_accepted: self.feerate_per_kw_range.start,
highest_accepted: self.feerate_per_kw_range.end,
});
}
if let Some(limit) = self.funding_satoshis_min {
if open_channel.funding_satoshis < limit {
return Err(PolicyError::ChannelFundingTooSmall {
proposed: open_channel.funding_satoshis,
required_minimum: limit,
});
}
}
if let Some(percents) = self.channel_reserve_satoshis_max_percent {
let limit =
open_channel.funding_satoshis as f32 * (percents as f32 / 100.);
let limit = limit as u64;
if open_channel.channel_reserve_satoshis > limit {
return Err(PolicyError::ChannelReserveTooLarge {
proposed: open_channel.channel_reserve_satoshis,
allowed_maximum: limit,
});
}
}
let peer_params = PeerParams::from(open_channel);
self.validate_peer_params(peer_params)?;
Ok(peer_params)
}
pub fn confirm_outbound(
&self,
our_params: PeerParams,
accept_channel: &AcceptChannel,
) -> Result<PeerParams, PolicyError> {
if let Some(limit) = self.maximum_depth {
if accept_channel.minimum_depth > limit {
return Err(PolicyError::UnreasonableMinDepth {
proposed: accept_channel.minimum_depth,
allowed_maximum: limit,
});
}
}
if accept_channel.channel_reserve_satoshis
< our_params.dust_limit_satoshis
{
return Err(PolicyError::LocalDustExceedsRemoteReserve {
channel_reserve: accept_channel.channel_reserve_satoshis,
dust_limit: our_params.dust_limit_satoshis,
});
}
if our_params.channel_reserve_satoshis
< accept_channel.dust_limit_satoshis
{
return Err(PolicyError::RemoteDustExceedsLocalReserve {
channel_reserve: our_params.channel_reserve_satoshis,
dust_limit: accept_channel.dust_limit_satoshis,
});
}
let peer_params = PeerParams::from(accept_channel);
self.validate_peer_params(peer_params)?;
Ok(peer_params)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, StrictEncode, StrictDecode)]
#[cfg_attr(
feature = "serde",
derive(Display, Serialize, Deserialize),
serde(crate = "serde_crate"),
display(CommonParams::to_yaml_string)
)]
pub struct CommonParams {
pub minimum_depth: u32,
pub feerate_per_kw: u32,
pub announce_channel: bool,
pub channel_type: ChannelType,
}
#[cfg(feature = "serde")]
impl ToYamlString for CommonParams {}
impl Default for CommonParams {
fn default() -> Self {
CommonParams {
minimum_depth: 3,
feerate_per_kw: 256,
announce_channel: true,
channel_type: ChannelType::default(),
}
}
}
impl CommonParams {
#[inline]
pub fn with(open_channel: &OpenChannel, minimum_depth: u32) -> Self {
CommonParams {
minimum_depth,
feerate_per_kw: open_channel.feerate_per_kw,
announce_channel: open_channel.should_announce_channel(),
channel_type: open_channel.channel_type.unwrap_or_default(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, StrictEncode, StrictDecode)]
#[cfg_attr(
feature = "serde",
derive(Display, Serialize, Deserialize),
serde(crate = "serde_crate"),
display(PeerParams::to_yaml_string)
)]
pub struct PeerParams {
pub dust_limit_satoshis: u64,
pub to_self_delay: u16,
pub htlc_minimum_msat: u64,
pub max_htlc_value_in_flight_msat: u64,
pub channel_reserve_satoshis: u64,
pub max_accepted_htlcs: u16,
}
#[cfg(feature = "serde")]
impl ToYamlString for PeerParams {}
impl Default for PeerParams {
fn default() -> Self {
PeerParams {
dust_limit_satoshis: BOLT3_DUST_LIMIT,
to_self_delay: 3,
htlc_minimum_msat: 1,
max_htlc_value_in_flight_msat: 1_000_000_000,
channel_reserve_satoshis: 10000,
max_accepted_htlcs: BOLT3_MAX_ACCEPTED_HTLC_LIMIT,
}
}
}
impl From<&OpenChannel> for PeerParams {
#[inline]
fn from(open_channel: &OpenChannel) -> Self {
PeerParams {
dust_limit_satoshis: open_channel.dust_limit_satoshis,
to_self_delay: open_channel.to_self_delay,
htlc_minimum_msat: open_channel.htlc_minimum_msat,
max_htlc_value_in_flight_msat: open_channel
.max_htlc_value_in_flight_msat,
channel_reserve_satoshis: open_channel.channel_reserve_satoshis,
max_accepted_htlcs: open_channel.max_accepted_htlcs,
}
}
}
impl From<&AcceptChannel> for PeerParams {
#[inline]
fn from(accept_channel: &AcceptChannel) -> Self {
PeerParams {
dust_limit_satoshis: accept_channel.dust_limit_satoshis,
to_self_delay: accept_channel.to_self_delay,
htlc_minimum_msat: accept_channel.htlc_minimum_msat,
max_htlc_value_in_flight_msat: accept_channel
.max_htlc_value_in_flight_msat,
channel_reserve_satoshis: accept_channel.channel_reserve_satoshis,
max_accepted_htlcs: accept_channel.max_accepted_htlcs,
}
}
}
#[cfg(test)]
mod test {
use amplify::DumbDefault;
use p2p::bolt::OpenChannel;
use super::*;
fn get_open_channel() -> OpenChannel {
let mut open_channel = OpenChannel::dumb_default();
open_channel.to_self_delay = 250;
open_channel.max_accepted_htlcs = BOLT3_MAX_ACCEPTED_HTLC_LIMIT;
open_channel.channel_reserve_satoshis = 10000;
open_channel.max_htlc_value_in_flight_msat = 10000;
open_channel.dust_limit_satoshis = BOLT3_DUST_LIMIT;
open_channel.htlc_minimum_msat = 10;
open_channel.feerate_per_kw = 1;
open_channel
}
fn get_accept_channel() -> AcceptChannel {
let mut accept_channel = AcceptChannel::dumb_default();
accept_channel.to_self_delay = 250;
accept_channel.max_accepted_htlcs = BOLT3_MAX_ACCEPTED_HTLC_LIMIT;
accept_channel.channel_reserve_satoshis = 10000;
accept_channel.max_htlc_value_in_flight_msat = 10000;
accept_channel.dust_limit_satoshis = BOLT3_DUST_LIMIT;
accept_channel.htlc_minimum_msat = 10;
accept_channel
}
#[test]
fn test_to_self_delay_too_large() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.to_self_delay = policy.to_self_delay_max + 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::ToSelfDelayUnreasonablyLarge {
proposed: params.to_self_delay,
allowed_maximum: policy.to_self_delay_max,
})
);
}
#[test]
fn test_max_accepted_htlc_limit_exceeded() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.max_accepted_htlcs = BOLT3_MAX_ACCEPTED_HTLC_LIMIT + 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::MaxAcceptedHtlcLimitExceeded(
params.max_accepted_htlcs,
))
);
}
#[test]
fn test_channel_reserve_less_than_dust_limit() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.channel_reserve_satoshis =
open_channel.dust_limit_satoshis - 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::ChannelReserveLessDust {
dust_limit: params.dust_limit_satoshis,
reserve: params.channel_reserve_satoshis,
})
);
}
#[test]
fn test_dust_limit_is_too_small() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.dust_limit_satoshis = BOLT3_DUST_LIMIT - 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::DustLimitTooSmall(params.dust_limit_satoshis,))
);
}
#[test]
fn test_htlc_min_too_large() {
let mut policy = Policy::default();
let open_channel = get_open_channel();
let htlc_minimum_msat_max = open_channel.htlc_minimum_msat - 1;
policy.htlc_minimum_msat_max = Some(htlc_minimum_msat_max);
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::HtlcMinimumTooLarge {
proposed: params.htlc_minimum_msat,
allowed_maximum: htlc_minimum_msat_max,
})
);
}
#[test]
fn test_htlc_in_flight_max_too_small() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
let max_htlc_value_in_flight_msat_min =
policy.max_htlc_value_in_flight_msat_min.unwrap();
open_channel.max_htlc_value_in_flight_msat =
max_htlc_value_in_flight_msat_min - 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::HtlcInFlightMaximumTooSmall {
proposed: params.max_htlc_value_in_flight_msat,
required_minimum: max_htlc_value_in_flight_msat_min,
})
);
}
#[test]
fn test_channel_reserve_too_large_abs() {
let mut policy = Policy::default();
let open_channel = get_open_channel();
let channel_reserve_satoshis_max =
open_channel.channel_reserve_satoshis - 1;
policy.channel_reserve_satoshis_max_abs =
Some(channel_reserve_satoshis_max);
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::ChannelReserveTooLarge {
proposed: params.channel_reserve_satoshis,
allowed_maximum: channel_reserve_satoshis_max,
})
);
}
#[test]
fn test_max_accepted_htlc_too_small() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
let max_accepted_htlcs_min = policy.max_accepted_htlcs_min.unwrap();
open_channel.max_accepted_htlcs = max_accepted_htlcs_min - 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::MaxAcceptedHtlcsTooSmall {
proposed: params.max_accepted_htlcs,
required_minimum: max_accepted_htlcs_min,
})
);
}
#[test]
fn test_dust_limit_too_large() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
let dust_limit_satoshis_max = policy.dust_limit_satoshis_max.unwrap();
open_channel.dust_limit_satoshis = dust_limit_satoshis_max + 1;
let params = PeerParams::from(&open_channel);
let error = policy.validate_peer_params(params);
assert_eq!(
error,
Err(PolicyError::DustLimitTooLarge {
proposed: params.dust_limit_satoshis,
allowed_maximum: dust_limit_satoshis_max,
})
);
}
#[test]
fn test_unreasonable_feerate_range_on_inbound() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.feerate_per_kw = policy.feerate_per_kw_range.end + 1;
let error = policy.validate_inbound(&open_channel);
assert_eq!(
error,
Err(PolicyError::FeeRateUnreasonable {
proposed: open_channel.feerate_per_kw,
lowest_accepted: policy.feerate_per_kw_range.start,
highest_accepted: policy.feerate_per_kw_range.end,
})
);
}
#[test]
fn test_channel_funding_too_small() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
let funding_satoshis_min = policy.funding_satoshis_min.unwrap();
open_channel.funding_satoshis = funding_satoshis_min - 1;
let error = policy.validate_inbound(&open_channel);
assert_eq!(
error,
Err(PolicyError::ChannelFundingTooSmall {
proposed: open_channel.funding_satoshis,
required_minimum: funding_satoshis_min,
})
);
}
#[test]
fn test_channel_reserve_too_large_percent() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
open_channel.funding_satoshis = 20000;
let percents = policy.channel_reserve_satoshis_max_percent.unwrap();
let channel_reserve_satoshis_max =
open_channel.funding_satoshis as f32 * (percents as f32 / 100.);
let channel_reserve_satoshis_max = channel_reserve_satoshis_max as u64;
let error = policy.validate_inbound(&open_channel);
assert_eq!(
error,
Err(PolicyError::ChannelReserveTooLarge {
proposed: open_channel.channel_reserve_satoshis,
allowed_maximum: channel_reserve_satoshis_max,
})
);
}
#[test]
fn test_unreasonable_min_depth() {
let policy = Policy::default();
let open_channel = get_open_channel();
let mut accept_channel = get_accept_channel();
let maximum_depth = policy.maximum_depth.unwrap();
accept_channel.minimum_depth = maximum_depth + 1;
let params = PeerParams::from(&open_channel);
let error = policy.confirm_outbound(params, &accept_channel);
assert_eq!(
error,
Err(PolicyError::UnreasonableMinDepth {
proposed: accept_channel.minimum_depth,
allowed_maximum: maximum_depth,
})
);
}
#[test]
fn test_local_dust_limit_exeeds_remote_reserve() {
let policy = Policy::default();
let open_channel = get_open_channel();
let mut accept_channel = get_accept_channel();
accept_channel.channel_reserve_satoshis =
open_channel.dust_limit_satoshis - 1;
let params = PeerParams::from(&open_channel);
let error = policy.confirm_outbound(params, &accept_channel);
assert_eq!(
error,
Err(PolicyError::LocalDustExceedsRemoteReserve {
channel_reserve: accept_channel.channel_reserve_satoshis,
dust_limit: params.dust_limit_satoshis,
})
);
}
#[test]
fn test_remote_dust_limit_exceeds_local_reserve() {
let policy = Policy::default();
let mut open_channel = get_open_channel();
let accept_channel = get_accept_channel();
open_channel.channel_reserve_satoshis =
accept_channel.dust_limit_satoshis - 1;
let params = PeerParams::from(&open_channel);
let error = policy.confirm_outbound(params, &accept_channel);
assert_eq!(
error,
Err(PolicyError::RemoteDustExceedsLocalReserve {
channel_reserve: params.channel_reserve_satoshis,
dust_limit: accept_channel.dust_limit_satoshis,
})
);
}
}