| | | 1 | | namespace NLightning.Domain.Channels.Validators; |
| | | 2 | | |
| | | 3 | | using Bitcoin.Transactions.Constants; |
| | | 4 | | using Constants; |
| | | 5 | | using Domain.Enums; |
| | | 6 | | using Exceptions; |
| | | 7 | | using Interfaces; |
| | | 8 | | using Money; |
| | | 9 | | using Node.Options; |
| | | 10 | | using Parameters; |
| | | 11 | | |
| | | 12 | | public class ChannelOpenValidator : IChannelOpenValidator |
| | | 13 | | { |
| | | 14 | | private readonly NodeOptions _nodeOptions; |
| | | 15 | | |
| | 0 | 16 | | public ChannelOpenValidator(NodeOptions nodeOptions) |
| | | 17 | | { |
| | 0 | 18 | | _nodeOptions = nodeOptions; |
| | 0 | 19 | | } |
| | | 20 | | |
| | | 21 | | /// <inheritdoc/> |
| | | 22 | | public void PerformOptionalChecks(ChannelOpenOptionalValidationParameters parameters) |
| | | 23 | | { |
| | | 24 | | // Check if Funding Satoshis is too small |
| | 0 | 25 | | if (parameters.FundingAmount is not null && parameters.FundingAmount < _nodeOptions.MinimumChannelSize) |
| | 0 | 26 | | throw new ChannelErrorException($"Funding amount is too small: {parameters.FundingAmount}"); |
| | | 27 | | |
| | | 28 | | // Check if we consider htlc_minimum_msat too large. IE. 20% bigger than our htlc minimum amount |
| | 0 | 29 | | if (parameters.HtlcMinimumAmount is not null |
| | 0 | 30 | | && parameters.HtlcMinimumAmount > _nodeOptions.HtlcMinimumAmount * 1.2M) |
| | 0 | 31 | | throw new ChannelErrorException($"Htlc minimum amount is too large: {parameters.HtlcMinimumAmount}"); |
| | | 32 | | |
| | | 33 | | // Check if we consider max_htlc_value_in_flight_msat too small. IE. 20% smaller than our maximum htlc value |
| | 0 | 34 | | if (parameters.FundingAmount is not null && parameters.MaxHtlcValueInFlight is not null) |
| | | 35 | | { |
| | 0 | 36 | | var ourMaxHtlcValueInFlight = |
| | 0 | 37 | | LightningMoney.Satoshis(_nodeOptions.AllowUpToPercentageOfChannelFundsInFlight * |
| | 0 | 38 | | parameters.FundingAmount.Satoshi / 100M); |
| | 0 | 39 | | if (parameters.MaxHtlcValueInFlight < ourMaxHtlcValueInFlight * 0.8M) |
| | 0 | 40 | | throw new ChannelErrorException( |
| | 0 | 41 | | $"Max htlc value in flight is too small: {parameters.MaxHtlcValueInFlight}"); |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | // If the channel amount is too small, we can have the channelReserve smaller than our dust |
| | 0 | 45 | | var ourChannelReserveAmount = parameters.OurChannelReserveAmount; |
| | 0 | 46 | | if (ourChannelReserveAmount < parameters.DustLimitAmount) |
| | 0 | 47 | | ourChannelReserveAmount = parameters.DustLimitAmount; |
| | | 48 | | |
| | | 49 | | // Check if we consider channel_reserve_satoshis too large. IE. 20% bigger than our 1% channel reserve |
| | 0 | 50 | | if (parameters.ChannelReserveAmount > ourChannelReserveAmount * 1.2M) |
| | 0 | 51 | | throw new ChannelErrorException($"Channel reserve amount is too large: {parameters.ChannelReserveAmount}"); |
| | | 52 | | |
| | | 53 | | // Check if we consider max_accepted_htlcs too small. IE. 20% smaller than our max-accepted htlcs |
| | 0 | 54 | | if (parameters.MaxAcceptedHtlcs < (ushort)(_nodeOptions.MaxAcceptedHtlcs * 0.8M)) |
| | 0 | 55 | | throw new ChannelErrorException($"Max accepted htlcs is too small: {parameters.MaxAcceptedHtlcs}"); |
| | | 56 | | |
| | | 57 | | // Check if we consider dust_limit_satoshis too large. IE. 75% bigger than our dust limit |
| | 0 | 58 | | if (parameters.DustLimitAmount > _nodeOptions.DustLimitAmount * 1.75M) |
| | 0 | 59 | | throw new ChannelErrorException($"Dust limit amount is too large: {parameters.DustLimitAmount}"); |
| | 0 | 60 | | } |
| | | 61 | | |
| | | 62 | | /// <inheritdoc/> |
| | | 63 | | public void PerformMandatoryChecks(ChannelOpenMandatoryValidationParameters parameters, |
| | | 64 | | out uint minimumDepth) |
| | | 65 | | { |
| | | 66 | | // Check if ChainHash is compatible |
| | 0 | 67 | | if (parameters.ChainHash is not null && parameters.ChainHash != _nodeOptions.BitcoinNetwork.ChainHash) |
| | 0 | 68 | | throw new ChannelErrorException("ChainHash is not compatible"); |
| | | 69 | | |
| | | 70 | | // Check if we consider to_self_delay unreasonably large. IE. 50% bigger than our to_self_delay |
| | 0 | 71 | | if (parameters.ToSelfDelay > _nodeOptions.ToSelfDelay * 1.5M) |
| | 0 | 72 | | throw new ChannelErrorException($"To self delay is too large: {parameters.ToSelfDelay}"); |
| | | 73 | | |
| | | 74 | | // Check max_accepted_htlcs is too large |
| | 0 | 75 | | if (parameters.MaxAcceptedHtlcs > ChannelConstants.MaxAcceptedHtlcs) |
| | 0 | 76 | | throw new ChannelErrorException($"Max accepted htlcs is too small: {parameters.MaxAcceptedHtlcs}"); |
| | | 77 | | |
| | 0 | 78 | | if (parameters.FeeRatePerKw is not null) |
| | | 79 | | { |
| | | 80 | | // Check if we consider fee_rate_per_kw too large |
| | 0 | 81 | | if (parameters.FeeRatePerKw > ChannelConstants.MaxFeePerKw) |
| | 0 | 82 | | throw new ChannelErrorException($"Fee rate per kw is too large: {parameters.FeeRatePerKw}"); |
| | | 83 | | |
| | | 84 | | // Check if we consider fee_rate_per_kw too small. IE. 20% smaller than our fee rate |
| | 0 | 85 | | if (parameters.FeeRatePerKw < ChannelConstants.MinFeePerKw || |
| | 0 | 86 | | parameters.FeeRatePerKw < parameters.CurrentFeeRatePerKw * 0.8M) |
| | 0 | 87 | | throw new ChannelErrorException( |
| | 0 | 88 | | $"Fee rate per kw is too small: {parameters.FeeRatePerKw}, currentFee{parameters.CurrentFeeRatePerKw |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | // Check if the dust limit is greater than the channel reserve amount |
| | 0 | 92 | | if (parameters.DustLimitAmount > parameters.ChannelReserveAmount) |
| | 0 | 93 | | throw new ChannelErrorException( |
| | 0 | 94 | | $"Dust limit({parameters.DustLimitAmount}) is greater than channel reserve({parameters.ChannelReserveAmo |
| | | 95 | | |
| | | 96 | | // Check if dust_limit_satoshis is too small |
| | 0 | 97 | | if (parameters.DustLimitAmount < ChannelConstants.MinDustLimitAmount) |
| | 0 | 98 | | throw new ChannelErrorException($"Dust limit amount is too small: {parameters.DustLimitAmount}"); |
| | | 99 | | |
| | 0 | 100 | | if (parameters.FundingAmount is not null) |
| | | 101 | | { |
| | | 102 | | // Check if the push amount is too large |
| | 0 | 103 | | if (parameters.PushAmount is not null |
| | 0 | 104 | | && parameters.PushAmount > 1_000 * parameters.FundingAmount) |
| | 0 | 105 | | throw new ChannelErrorException($"Push amount is too large: {parameters.PushAmount}"); |
| | | 106 | | |
| | | 107 | | // Check if there are enough funds to pay for fees |
| | 0 | 108 | | var expectedWeight = parameters.NegotiatedFeatures.OptionAnchors > FeatureSupport.No |
| | 0 | 109 | | ? TransactionConstants.InitialCommitmentTransactionWeightNoAnchor |
| | 0 | 110 | | : TransactionConstants.InitialCommitmentTransactionWeightWithAnchor; |
| | 0 | 111 | | var expectedFee = LightningMoney.Satoshis(expectedWeight * parameters.CurrentFeeRatePerKw.Satoshi / 1000); |
| | 0 | 112 | | if (parameters.FundingAmount < expectedFee + parameters.ChannelReserveAmount) |
| | 0 | 113 | | throw new ChannelErrorException( |
| | 0 | 114 | | $"Funding amount is too small to cover fees: {parameters.FundingAmount}"); |
| | | 115 | | |
| | | 116 | | // Check if this is a large channel and if we support it |
| | 0 | 117 | | if (parameters.FundingAmount >= ChannelConstants.LargeChannelAmount && |
| | 0 | 118 | | parameters.NegotiatedFeatures.LargeChannels == FeatureSupport.No) |
| | 0 | 119 | | throw new ChannelErrorException("We don't support large channels"); |
| | | 120 | | } |
| | | 121 | | |
| | | 122 | | // Check if ChannelType exists |
| | 0 | 123 | | minimumDepth = _nodeOptions.MinimumDepth; |
| | 0 | 124 | | if (parameters.ChannelTypeTlv is null) |
| | 0 | 125 | | throw new ChannelErrorException("ChannelTypeTlv is not present"); |
| | | 126 | | |
| | | 127 | | // Check if OptionStaticRemoteKey is Compulsory |
| | 0 | 128 | | if (!parameters.ChannelTypeTlv.Features.IsFeatureSet(Feature.OptionStaticRemoteKey, true)) |
| | 0 | 129 | | throw new ChannelErrorException("Static remote key feature is compulsory but not set by peer", |
| | 0 | 130 | | "ChannelTypeTlv: Static remote key is compulsory"); |
| | | 131 | | |
| | 0 | 132 | | if (parameters.ChannelTypeTlv.Features.IsFeatureSet(Feature.OptionAnchors, true) |
| | 0 | 133 | | && parameters.NegotiatedFeatures.OptionAnchors == FeatureSupport.No) |
| | 0 | 134 | | throw new ChannelErrorException("Anchor outputs feature is not supported but requested by peer", |
| | 0 | 135 | | "ChannelTypeTlv: We don't support anchor outputs"); |
| | | 136 | | |
| | 0 | 137 | | if (parameters.ChannelTypeTlv.Features.IsFeatureSet(Feature.OptionScidAlias, true)) |
| | | 138 | | { |
| | 0 | 139 | | if (parameters.ChannelFlags is not null && parameters.ChannelFlags.Value.AnnounceChannel) |
| | 0 | 140 | | throw new ChannelErrorException("Invalid channel flags for OPTION_SCID_ALIAS", |
| | 0 | 141 | | "ChannelTypeTlv: We want to announce this channel"); |
| | | 142 | | } |
| | | 143 | | |
| | | 144 | | // Check for ZeroConf feature |
| | 0 | 145 | | if (parameters.ChannelTypeTlv.Features.IsFeatureSet(Feature.OptionZeroconf, true)) |
| | | 146 | | { |
| | 0 | 147 | | if (_nodeOptions.Features.ZeroConf == FeatureSupport.No) |
| | 0 | 148 | | throw new ChannelErrorException("ZeroConf feature not supported but requested by peer", |
| | 0 | 149 | | "ChannelTypeTlv: We don't support ZeroConf with you"); |
| | | 150 | | |
| | 0 | 151 | | minimumDepth = 0U; |
| | | 152 | | } |
| | 0 | 153 | | } |
| | | 154 | | } |