diff --git a/src/NLightning.Common/BitUtils/BitWriter.cs b/src/NLightning.Common/BitUtils/BitWriter.cs index 1157d53..b320e36 100644 --- a/src/NLightning.Common/BitUtils/BitWriter.cs +++ b/src/NLightning.Common/BitUtils/BitWriter.cs @@ -1,9 +1,10 @@ +using System.Buffers; using System.Buffers.Binary; using System.Runtime.CompilerServices; namespace NLightning.Common.BitUtils; -public class BitWriter +public class BitWriter : IDisposable { private int _bitOffset; private byte[] _buffer; @@ -17,7 +18,7 @@ public BitWriter(int totalBits) TotalBits = totalBits; var totalBytes = (totalBits + 7) / 8; - _buffer = new byte[totalBytes]; + _buffer = ArrayPool.Shared.Rent(totalBytes); } public void GrowByBits(int additionalBits) @@ -226,4 +227,9 @@ public byte[] ToArray() return bytes; } + + public void Dispose() + { + ArrayPool.Shared.Return(_buffer); + } } \ No newline at end of file diff --git a/src/NLightning.Common/Factories/MessageFactory.cs b/src/NLightning.Common/Factories/MessageFactory.cs index 91c7207..49ed2e2 100644 --- a/src/NLightning.Common/Factories/MessageFactory.cs +++ b/src/NLightning.Common/Factories/MessageFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.Crypto; +using NLightning.Common.Managers; namespace NLightning.Common.Factories; @@ -395,6 +396,50 @@ public IMessage CreateClosingSignedMessage(ChannelId channelId, ulong feeSatoshi return new ClosingSignedMessage(payload, new FeeRangeTlv(minFeeSatoshis, maxFeeSatoshis)); } + /// + /// Create a OpenChannel1 message. + /// + /// The temporary channel id. + /// The amount of satoshis we're adding to the channel. + /// The amount of satoshis we're pushing to the other side. + /// The channel reserve amount. + /// The fee rate per kw. + /// The max accepted htlcs. + /// The revocation pubkey. + /// The payment pubkey. + /// The delayed payment pubkey. + /// The htlc pubkey. + /// The first per commitment pubkey. + /// The flags for the channel. + /// The upfront shutdown script tlv. + /// The channel type tlv. + /// The OpenChannel1 message. + /// + /// + /// + /// + /// + /// + public IMessage CreateOpenChannel1Message(ChannelId temporaryChannelId, LightningMoney fundingAmount, + LightningMoney pushAmount, LightningMoney channelReserveAmount, + LightningMoney feeRatePerKw, ushort maxAcceptedHtlcs, + PubKey revocationBasepoint, PubKey paymentBasepoint, + PubKey delayedPaymentBasepoint, PubKey htlcBasepoint, + PubKey firstPerCommitmentPoint, ChannelFlags channelFlags, + UpfrontShutdownScriptTlv? upfrontShutdownScriptTlv, + ChannelTypeTlv? channelTypeTlv) + { + var payload = new OpenChannel1Payload(_nodeOptions.Network.ChainHash, temporaryChannelId, fundingAmount, + pushAmount, _nodeOptions.DustLimitAmount, + _nodeOptions.MaxHtlcValueInFlight, channelReserveAmount, + _nodeOptions.HtlcMinimumAmount, feeRatePerKw, _nodeOptions.ToSelfDelay, + maxAcceptedHtlcs, SecureKeyManager.GetPrivateKey().PubKey, + revocationBasepoint, paymentBasepoint, delayedPaymentBasepoint, + htlcBasepoint, firstPerCommitmentPoint, channelFlags); + + return new OpenChannel1Message(payload, upfrontShutdownScriptTlv, channelTypeTlv); + } + /// /// Create a OpenChannel2 message. /// @@ -447,6 +492,45 @@ channelType is null ? requireConfirmedInputs ? new RequireConfirmedInputsTlv() : null); } + /// + /// Create a AcceptChannel1 message. + /// + /// The temporary channel id. + /// The channel reserve amount. + /// The minimum depth. + /// The max accepted htlcs. + /// The revocation pubkey. + /// The payment pubkey. + /// The delayed payment pubkey. + /// The htlc pubkey. + /// The first per commitment pubkey. + /// The upfront shutdown script tlv. + /// The channel type tlv. + /// The AcceptChannel1 message. + /// + /// + /// + /// + /// + /// + public IMessage CreateAcceptChannel1Message(ChannelId temporaryChannelId, LightningMoney channelReserveAmount, + uint minimumDepth, ushort maxAcceptedHtlcs, + PubKey revocationBasepoint, PubKey paymentBasepoint, + PubKey delayedPaymentBasepoint, PubKey htlcBasepoint, + PubKey firstPerCommitmentPoint, + UpfrontShutdownScriptTlv? upfrontShutdownScriptTlv, + ChannelTypeTlv? channelTypeTlv) + { + var payload = new AcceptChannel1Payload(temporaryChannelId, _nodeOptions.DustLimitAmount, + _nodeOptions.MaxHtlcValueInFlight, channelReserveAmount, + _nodeOptions.HtlcMinimumAmount, minimumDepth, _nodeOptions.ToSelfDelay, + maxAcceptedHtlcs, SecureKeyManager.GetPrivateKey().PubKey, + revocationBasepoint, paymentBasepoint, delayedPaymentBasepoint, + htlcBasepoint, firstPerCommitmentPoint); + + return new AcceptChannel1Message(payload, upfrontShutdownScriptTlv, channelTypeTlv); + } + /// /// Create a AcceptChannel2 message. /// @@ -489,6 +573,43 @@ channelType is null ? : new ChannelTypeTlv(channelType), requireConfirmedInputs ? new RequireConfirmedInputsTlv() : null); } + + /// + /// Create a FundingCreated message. + /// + /// The temporary channel id. + /// The funding transaction id. + /// The funding output index. + /// The signature for the funding transaction. + /// The FundingCreated message. + /// + /// + /// + /// + public IMessage CreatedFundingCreatedMessage(ChannelId temporaryChannelId, ReadOnlySpan fundingTxId, + ushort fundingOutputIndex, ECDSASignature signature) + { + var payload = new FundingCreatedPayload(temporaryChannelId, fundingTxId, fundingOutputIndex, signature); + + return new FundingCreatedMessage(payload); + } + + /// + /// Create a FundingSigned message. + /// + /// The channel id. + /// + /// The FundingSigned message. + /// + /// + /// + /// + public IMessage CreatedFundingSignedMessage(ChannelId channelId, ECDSASignature signature) + { + var payload = new FundingSignedPayload(channelId, signature); + + return new FundingSignedMessage(payload); + } #endregion #region Commitment @@ -671,6 +792,14 @@ public IMessage CreateChannelReestablishMessage(ChannelId channelId, ulong nextC return await PingMessage.DeserializeAsync(stream); // 18 -> 0x12 case MessageTypes.PONG: return await PongMessage.DeserializeAsync(stream); // 19 -> 0x13 + case MessageTypes.OPEN_CHANNEL: + return await OpenChannel1Message.DeserializeAsync(stream); // 32 -> 0x20 + case MessageTypes.ACCEPT_CHANNEL: + return await AcceptChannel1Message.DeserializeAsync(stream); // 33 -> 0x21 + case MessageTypes.FUNDING_CREATED: + return await FundingCreatedMessage.DeserializeAsync(stream); // 34 -> 0x22 + case MessageTypes.FUNDING_SIGNED: + return await FundingSignedMessage.DeserializeAsync(stream); // 35 -> 0x23 case MessageTypes.CHANNEL_READY: return await ChannelReadyMessage.DeserializeAsync(stream); // 36 -> 0x24 case MessageTypes.SHUTDOWN: @@ -716,12 +845,6 @@ public IMessage CreateChannelReestablishMessage(ChannelId channelId, ulong nextC case MessageTypes.CHANNEL_REESTABLISH: return await ChannelReestablishMessage.DeserializeAsync(stream); // 136 -> 0x88 - case MessageTypes.OPEN_CHANNEL: - case MessageTypes.ACCEPT_CHANNEL: - case MessageTypes.FUNDING_CREATED: - case MessageTypes.FUNDING_SIGNED: - throw new InvalidMessageException("You must use OpenChannel2 flow"); - default: { // If type is unknown and even, throw exception diff --git a/src/NLightning.Common/Messages/AcceptChannel1Message.cs b/src/NLightning.Common/Messages/AcceptChannel1Message.cs new file mode 100644 index 0000000..1653df5 --- /dev/null +++ b/src/NLightning.Common/Messages/AcceptChannel1Message.cs @@ -0,0 +1,85 @@ +using System.Runtime.Serialization; + +namespace NLightning.Common.Messages; + +using Constants; +using Exceptions; +using Payloads; +using TLVs; +using Types; + +/// +/// Represents an open_channel message. +/// +/// +/// The accept_channel message is sent to the initiator in order to accept the channel opening. +/// The message type is 33. +/// +public sealed class AcceptChannel1Message : BaseMessage +{ + /// + /// The payload of the message. + /// + public new AcceptChannel1Payload Payload { get => (AcceptChannel1Payload)base.Payload; } + + /// + /// Optional UpfrontShutdownScriptTlv + /// + public UpfrontShutdownScriptTlv? UpfrontShutdownScriptTlv { get; } + + /// + /// Optional ChannelTypeTlv + /// + public ChannelTypeTlv? ChannelTypeTlv { get; } + + public AcceptChannel1Message(AcceptChannel1Payload payload, + UpfrontShutdownScriptTlv? upfrontShutdownScriptTlv = null, + ChannelTypeTlv? channelTypeTlv = null) + : base(MessageTypes.ACCEPT_CHANNEL_2, payload) + { + UpfrontShutdownScriptTlv = upfrontShutdownScriptTlv; + ChannelTypeTlv = channelTypeTlv; + + if (UpfrontShutdownScriptTlv is not null || ChannelTypeTlv is not null) + { + Extension = new TlvStream(); + Extension.Add(UpfrontShutdownScriptTlv, ChannelTypeTlv); + } + } + + /// + /// Deserialize a OpenChannel1Message from a stream. + /// + /// The stream to deserialize from. + /// The deserialized AcceptChannel1Message. + /// Error deserializing OpenChannel1Message + public static async Task DeserializeAsync(Stream stream) + { + try + { + // Deserialize payload + var payload = await AcceptChannel1Payload.DeserializeAsync(stream); + + // Deserialize extension + var extension = await TlvStream.DeserializeAsync(stream); + if (extension is null) + { + return new AcceptChannel1Message(payload); + } + + var upfrontShutdownScriptTlv = extension.TryGetTlv(TlvConstants.UPFRONT_SHUTDOWN_SCRIPT, out var tlv) + ? UpfrontShutdownScriptTlv.FromTlv(tlv!) + : null; + + var channelTypeTlv = extension.TryGetTlv(TlvConstants.CHANNEL_TYPE, out tlv) + ? ChannelTypeTlv.FromTlv(tlv!) + : null; + + return new AcceptChannel1Message(payload, upfrontShutdownScriptTlv, channelTypeTlv); + } + catch (SerializationException e) + { + throw new MessageSerializationException("Error deserializing AcceptChannel1Message", e); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/FundingCreatedMessage.cs b/src/NLightning.Common/Messages/FundingCreatedMessage.cs new file mode 100644 index 0000000..3870df3 --- /dev/null +++ b/src/NLightning.Common/Messages/FundingCreatedMessage.cs @@ -0,0 +1,46 @@ +using System.Runtime.Serialization; + +namespace NLightning.Common.Messages; + +using Constants; +using Exceptions; +using Payloads; + +/// +/// Represents a funding_created message. +/// +/// +/// The funding_created message is sent by the funder to the fundee after the funding transaction has been created. +/// The message type is 34. +/// +public sealed class FundingCreatedMessage : BaseMessage +{ + /// + /// The payload of the message. + /// + public new FundingCreatedPayload Payload { get => (FundingCreatedPayload)base.Payload; } + + public FundingCreatedMessage(FundingCreatedPayload payload) : base(MessageTypes.ACCEPT_CHANNEL_2, payload) + { } + + /// + /// Deserialize a FundingCreatedMessage from a stream. + /// + /// The stream to deserialize from. + /// The deserialized FundingCreatedMessage. + /// Error deserializing FundingCreatedMessage + public static async Task DeserializeAsync(Stream stream) + { + try + { + // Deserialize payload + var payload = await FundingCreatedPayload.DeserializeAsync(stream); + + return new FundingCreatedMessage(payload); + } + catch (SerializationException e) + { + throw new MessageSerializationException("Error deserializing FundingCreatedMessage", e); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/FundingSignedMessage.cs b/src/NLightning.Common/Messages/FundingSignedMessage.cs new file mode 100644 index 0000000..eebd298 --- /dev/null +++ b/src/NLightning.Common/Messages/FundingSignedMessage.cs @@ -0,0 +1,46 @@ +using System.Runtime.Serialization; + +namespace NLightning.Common.Messages; + +using Constants; +using Exceptions; +using Payloads; + +/// +/// Represents a funding_signed message. +/// +/// +/// The funding_signed message is sent by the funder to the fundee after the funding transaction has been created. +/// The message type is 35. +/// +public sealed class FundingSignedMessage : BaseMessage +{ + /// + /// The payload of the message. + /// + public new FundingSignedPayload Payload { get => (FundingSignedPayload)base.Payload; } + + public FundingSignedMessage(FundingSignedPayload payload) : base(MessageTypes.ACCEPT_CHANNEL_2, payload) + { } + + /// + /// Deserialize a FundingSignedMessage from a stream. + /// + /// The stream to deserialize from. + /// The deserialized FundingSignedMessage. + /// Error deserializing FundingSignedMessage + public static async Task DeserializeAsync(Stream stream) + { + try + { + // Deserialize payload + var payload = await FundingSignedPayload.DeserializeAsync(stream); + + return new FundingSignedMessage(payload); + } + catch (SerializationException e) + { + throw new MessageSerializationException("Error deserializing FundingCreatedMessage", e); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/OpenChannel1Message.cs b/src/NLightning.Common/Messages/OpenChannel1Message.cs new file mode 100644 index 0000000..caf0c52 --- /dev/null +++ b/src/NLightning.Common/Messages/OpenChannel1Message.cs @@ -0,0 +1,77 @@ +using System.Runtime.Serialization; + +namespace NLightning.Common.Messages; + +using Constants; +using Exceptions; +using Payloads; +using TLVs; +using Types; + +/// +/// Represents an open_channel2 message. +/// +/// +/// The open_channel message is sent to another peer in order to start the channel negotiation. +/// The message type is 32. +/// +public sealed class OpenChannel1Message : BaseMessage +{ + /// + /// The payload of the message. + /// + public new OpenChannel1Payload Payload { get => (OpenChannel1Payload)base.Payload; } + + public UpfrontShutdownScriptTlv? UpfrontShutdownScriptTlv { get; } + public ChannelTypeTlv? ChannelTypeTlv { get; } + + public OpenChannel1Message(OpenChannel1Payload payload, UpfrontShutdownScriptTlv? upfrontShutdownScriptTlv = null, + ChannelTypeTlv? channelTypeTlv = null) + : base(MessageTypes.OPEN_CHANNEL_2, payload) + { + UpfrontShutdownScriptTlv = upfrontShutdownScriptTlv; + ChannelTypeTlv = channelTypeTlv; + + if (UpfrontShutdownScriptTlv is not null || ChannelTypeTlv is not null) + { + Extension = new TlvStream(); + Extension.Add(UpfrontShutdownScriptTlv, ChannelTypeTlv); + } + } + + /// + /// Deserialize a OpenChannel1Message from a stream. + /// + /// The stream to deserialize from. + /// The deserialized OpenChannel2Message. + /// Error deserializing OpenChannel2Message + public static async Task DeserializeAsync(Stream stream) + { + try + { + // Deserialize payload + var payload = await OpenChannel1Payload.DeserializeAsync(stream); + + // Deserialize extension + var extension = await TlvStream.DeserializeAsync(stream); + if (extension is null) + { + return new OpenChannel1Message(payload); + } + + var upfrontShutdownScriptTlv = extension.TryGetTlv(TlvConstants.UPFRONT_SHUTDOWN_SCRIPT, out var tlv) + ? UpfrontShutdownScriptTlv.FromTlv(tlv!) + : null; + + var channelTypeTlv = extension.TryGetTlv(TlvConstants.CHANNEL_TYPE, out tlv) + ? ChannelTypeTlv.FromTlv(tlv!) + : null; + + return new OpenChannel1Message(payload, upfrontShutdownScriptTlv, channelTypeTlv); + } + catch (SerializationException e) + { + throw new MessageSerializationException("Error deserializing OpenChannel1Message", e); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/Payloads/AcceptChannel1Payload.cs b/src/NLightning.Common/Messages/Payloads/AcceptChannel1Payload.cs new file mode 100644 index 0000000..4d36fb9 --- /dev/null +++ b/src/NLightning.Common/Messages/Payloads/AcceptChannel1Payload.cs @@ -0,0 +1,208 @@ +using System.Buffers; +using NBitcoin; + +namespace NLightning.Common.Messages.Payloads; + +using BitUtils; +using Exceptions; +using Interfaces; +using Types; + +/// +/// Represents the payload for the accept_channel message. +/// +/// +/// Initializes a new instance of the AcceptChannel1Payload class. +/// +public class AcceptChannel1Payload : IMessagePayload +{ + /// + /// The temporary_channel_id is used to identify this channel on a per-peer basis until the funding transaction + /// is established, at which point it is replaced by the channel_id, which is derived from the funding transaction. + /// + public ChannelId TemporaryChannelId { get; set; } + + /// + /// dust_limit_satoshis is the threshold below which outputs should not be generated for this node's commitment or + /// HTLC transactions + /// + public LightningMoney DustLimitAmount { get; } + + /// + /// max_htlc_value_in_flight_msat is a cap on total value of outstanding HTLCs offered by the remote node, which + /// allows the local node to limit its exposure to HTLCs + /// + public LightningMoney MaxHtlcValueInFlightAmount { get; } + + /// + /// channel_reserve_satoshis is the amount the acceptor is reserving for the channel, which is not available for + /// spending + /// + public LightningMoney ChannelReserveAmount { get; set; } + + /// + /// htlc_minimum_msat indicates the smallest value HTLC this node will accept. + /// + public LightningMoney HtlcMinimumAmount { get; } + + /// + /// minimum_depth is the number of blocks we consider reasonable to avoid double-spending of the funding transaction. + /// In case channel_type includes option_zeroconf this MUST be 0 + /// + public uint MinimumDepth { get; set; } + + /// + /// to_self_delay is how long (in blocks) the other node will have to wait in case of breakdown before redeeming + /// its own funds. + /// + public ushort ToSelfDelay { get; } + + /// + /// max_accepted_htlcs limits the number of outstanding HTLCs the remote node can offer. + /// + public ushort MaxAcceptedHtlcs { get; } + + /// + /// funding_pubkey is the public key in the 2-of-2 multisig script of the funding transaction output. + /// + public PubKey FundingPubKey { get; set; } + + /// + /// revocation_basepoint is used to regenerate the scripts required for the penalty transaction + /// + public PubKey RevocationBasepoint { get; set; } + + /// + /// payment_basepoint is used to produce payment signatures for the protocol + /// + public PubKey PaymentBasepoint { get; set; } + + /// + /// delayed_payment_basepoint is used to regenerate the scripts required for the penalty transaction + /// + public PubKey DelayedPaymentBasepoint { get; set; } + + /// + /// htlc_basepoint is used to produce HTLC signatures for the protocol + /// + public PubKey HtlcBasepoint { get; set; } + + /// + /// first_per_commitment_point is the per-commitment point used for the first commitment transaction + /// + public PubKey FirstPerCommitmentPoint { get; set; } + + public AcceptChannel1Payload(ChannelId temporaryChannelId, LightningMoney dustLimitAmount, + LightningMoney maxHtlcValueInFlight, LightningMoney channelReserveAmount, + LightningMoney htlcMinimumAmount, uint minimumDepth, ushort toSelfDelay, + ushort maxAcceptedHtlcs, PubKey fundingPubKey, PubKey revocationBasepoint, + PubKey paymentBasepoint, PubKey delayedPaymentBasepoint, PubKey htlcBasepoint, + PubKey firstPerCommitmentPoint) + { + TemporaryChannelId = temporaryChannelId; + DustLimitAmount = dustLimitAmount; + MaxHtlcValueInFlightAmount = maxHtlcValueInFlight; + ChannelReserveAmount = channelReserveAmount; + HtlcMinimumAmount = htlcMinimumAmount; + MinimumDepth = minimumDepth; + ToSelfDelay = toSelfDelay; + MaxAcceptedHtlcs = maxAcceptedHtlcs; + FundingPubKey = fundingPubKey; + RevocationBasepoint = revocationBasepoint; + PaymentBasepoint = paymentBasepoint; + DelayedPaymentBasepoint = delayedPaymentBasepoint; + HtlcBasepoint = htlcBasepoint; + FirstPerCommitmentPoint = firstPerCommitmentPoint; + } + + /// + public async Task SerializeAsync(Stream stream) + { + await TemporaryChannelId.SerializeAsync(stream); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian((ulong)DustLimitAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(MaxHtlcValueInFlightAmount.MilliSatoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian((ulong)ChannelReserveAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(HtlcMinimumAmount.MilliSatoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(MinimumDepth)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(ToSelfDelay)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(MaxAcceptedHtlcs)); + await stream.WriteAsync(FundingPubKey.ToBytes()); + await stream.WriteAsync(RevocationBasepoint.ToBytes()); + await stream.WriteAsync(PaymentBasepoint.ToBytes()); + await stream.WriteAsync(DelayedPaymentBasepoint.ToBytes()); + await stream.WriteAsync(HtlcBasepoint.ToBytes()); + await stream.WriteAsync(FirstPerCommitmentPoint.ToBytes()); + } + + /// + /// Deserializes the payload from a stream. + /// + /// The stream to deserialize from. + /// The deserialized payload. + /// Error deserializing Payload + public static async Task DeserializeAsync(Stream stream) + { + var bytes = ArrayPool.Shared.Rent(33); + + try + { + var temporaryChannelId = await ChannelId.DeserializeAsync(stream); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var dustLimitAmount = LightningMoney + .Satoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var maxHtlcValueInFlight = LightningMoney + .MilliSatoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var channelReserveAmount = + LightningMoney.Satoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var htlcMinimumAmount = LightningMoney + .MilliSatoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(uint)]); + var minimumDepth = EndianBitConverter.ToUInt32BigEndian(bytes[..sizeof(uint)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ushort)]); + var toSelfDelay = EndianBitConverter.ToUInt16BigEndian(bytes[..sizeof(ushort)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ushort)]); + var maxAcceptedHtlcs = EndianBitConverter.ToUInt16BigEndian(bytes[..sizeof(ushort)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var fundingPubKey = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var revocationBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var paymentBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var delayedPaymentBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var htlcBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var firstPerCommitmentPoint = new PubKey(bytes[..33]); + + return new AcceptChannel1Payload(temporaryChannelId, dustLimitAmount, maxHtlcValueInFlight, + channelReserveAmount, htlcMinimumAmount, minimumDepth, toSelfDelay, + maxAcceptedHtlcs, fundingPubKey, revocationBasepoint, paymentBasepoint, + delayedPaymentBasepoint, htlcBasepoint, firstPerCommitmentPoint); + } + catch (Exception e) + { + throw new PayloadSerializationException("Error deserializing AcceptChannel1Payload", e); + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/Payloads/FundingCreatedPayload.cs b/src/NLightning.Common/Messages/Payloads/FundingCreatedPayload.cs new file mode 100644 index 0000000..cfc3f8f --- /dev/null +++ b/src/NLightning.Common/Messages/Payloads/FundingCreatedPayload.cs @@ -0,0 +1,96 @@ +using System.Buffers; +using NBitcoin.Crypto; + +namespace NLightning.Common.Messages.Payloads; + +using BitUtils; +using Constants; +using Exceptions; +using Interfaces; +using Types; + +/// +/// Represents the payload for the funding_created message. +/// +/// +/// Initializes a new instance of the FundingCreatedPayload class. +/// +public class FundingCreatedPayload : IMessagePayload +{ + /// + /// The temporary_channel_id is used to identify this channel on a per-peer basis until the funding transaction + /// is established, at which point it is replaced by the channel_id, which is derived from the funding transaction. + /// + public ChannelId TemporaryChannelId { get; set; } + + /// + /// The funding transaction id. + /// + public ReadOnlyMemory FundingTxId { get; set; } + + /// + /// The funding transaction output index. + /// + public ushort FundingOutputIndex { get; set; } + + /// + /// The signature of the funding transaction. + /// + public ECDSASignature Signature { get; set; } + + public FundingCreatedPayload(ChannelId temporaryChannelId, ReadOnlySpan fundingTxId, ushort fundingOutputIndex, + ECDSASignature signature) + { + TemporaryChannelId = temporaryChannelId; + FundingTxId = fundingTxId.ToArray(); + FundingOutputIndex = fundingOutputIndex; + Signature = signature; + } + + /// + public async Task SerializeAsync(Stream stream) + { + await TemporaryChannelId.SerializeAsync(stream); + await stream.WriteAsync(FundingTxId); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(FundingOutputIndex)); + await stream.WriteAsync(Signature.ToCompact()); + } + + /// + /// Deserializes the payload from a stream. + /// + /// The stream to deserialize from. + /// The deserialized payload. + /// Error deserializing Payload + public static async Task DeserializeAsync(Stream stream) + { + var bytes = ArrayPool.Shared.Rent(64); + + try + { + var temporaryChannelId = await ChannelId.DeserializeAsync(stream); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..HashConstants.SHA256_HASH_LEN]); + var fundingTxId = bytes[..HashConstants.SHA256_HASH_LEN]; + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ushort)]); + var fundingOutputIndex = EndianBitConverter.ToUInt16BigEndian(bytes[..sizeof(ushort)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..64]); + if (!ECDSASignature.TryParseFromCompact(bytes[..64], out var signature)) + { + throw new Exception("Unable to parse signature"); + } + + return new FundingCreatedPayload(temporaryChannelId, fundingTxId, fundingOutputIndex, signature); + } + catch (Exception e) + { + throw new PayloadSerializationException("Error deserializing FundingCreatedPayload", e); + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/Payloads/FundingSignedPayload.cs b/src/NLightning.Common/Messages/Payloads/FundingSignedPayload.cs new file mode 100644 index 0000000..61003f6 --- /dev/null +++ b/src/NLightning.Common/Messages/Payloads/FundingSignedPayload.cs @@ -0,0 +1,73 @@ +using System.Buffers; +using NBitcoin.Crypto; + +namespace NLightning.Common.Messages.Payloads; + +using Exceptions; +using Interfaces; +using Types; + +/// +/// Represents the payload for the funding_created message. +/// +/// +/// Initializes a new instance of the FundingCreatedPayload class. +/// +public class FundingSignedPayload : IMessagePayload +{ + /// + /// The channel_id is used to identify this channel on a per-peer basis until the funding transaction + /// is established, at which point it is replaced by the channel_id, which is derived from the funding transaction. + /// + public ChannelId ChannelId { get; set; } + + /// + /// The signature of the funding transaction. + /// + public ECDSASignature Signature { get; set; } + + public FundingSignedPayload(ChannelId channelId, ECDSASignature signature) + { + ChannelId = channelId; + Signature = signature; + } + + /// + public async Task SerializeAsync(Stream stream) + { + await ChannelId.SerializeAsync(stream); + await stream.WriteAsync(Signature.ToCompact()); + } + + /// + /// Deserializes the payload from a stream. + /// + /// The stream to deserialize from. + /// The deserialized payload. + /// Error deserializing Payload + public static async Task DeserializeAsync(Stream stream) + { + var bytes = ArrayPool.Shared.Rent(64); + + try + { + var temporaryChannelId = await ChannelId.DeserializeAsync(stream); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..64]); + if (!ECDSASignature.TryParseFromCompact(bytes[..64], out var signature)) + { + throw new Exception("Unable to parse signature"); + } + + return new FundingSignedPayload(temporaryChannelId, signature); + } + catch (Exception e) + { + throw new PayloadSerializationException("Error deserializing FundingSignedPayload", e); + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Messages/Payloads/OpenChannel1Payload.cs b/src/NLightning.Common/Messages/Payloads/OpenChannel1Payload.cs new file mode 100644 index 0000000..81e5f37 --- /dev/null +++ b/src/NLightning.Common/Messages/Payloads/OpenChannel1Payload.cs @@ -0,0 +1,250 @@ +using System.Buffers; +using NBitcoin; + +namespace NLightning.Common.Messages.Payloads; + +using BitUtils; +using Exceptions; +using Interfaces; +using Types; + +/// +/// Represents the payload for the open_channel message. +/// +/// +/// Initializes a new instance of the OpenChannel1Payload class. +/// +public class OpenChannel1Payload : IMessagePayload +{ + /// + /// The chain_hash value denotes the exact blockchain that the opened channel will reside within. + /// + public ChainHash ChainHash { get; } + + /// + /// The temporary_channel_id is used to identify this channel on a per-peer basis until the funding transaction + /// is established, at which point it is replaced by the channel_id, which is derived from the funding transaction. + /// + public ChannelId TemporaryChannelId { get; set; } + + /// + /// funding_satoshis is the amount the sender is putting into the channel. + /// + /// Amount is used in Satoshis + public LightningMoney FundingAmount { get; set; } + + /// + /// push_msat is the amount the sender is pushing to the receiver of the channel. + /// + /// Amount is used in Millisatoshis + public LightningMoney PushAmount { get; set; } + + /// + /// dust_limit_satoshis is the threshold below which outputs should not be generated for this node's commitment or + /// HTLC transactions + /// + public LightningMoney DustLimitAmount { get; } + + /// + /// max_htlc_value_in_flight_msat is a cap on total value of outstanding HTLCs offered by the remote node, which + /// allows the local node to limit its exposure to HTLCs + /// + public LightningMoney MaxHtlcValueInFlight { get; } + + /// + /// channel_reserve_satoshis is the amount that must remain in the channel after a commitment transaction + /// + public LightningMoney ChannelReserveAmount { get; set; } + + /// + /// htlc_minimum_msat indicates the smallest value HTLC this node will accept. + /// + public LightningMoney HtlcMinimumAmount { get; } + + /// + /// feerate_per_kw is the fee rate that will be paid for the commitment transaction in + /// satoshi per 1000-weight + /// + public LightningMoney FeeRatePerKw { get; set; } + + /// + /// to_self_delay is how long (in blocks) the other node will have to wait in case of breakdown before redeeming + /// its own funds. + /// + public ushort ToSelfDelay { get; } + + /// + /// max_accepted_htlcs limits the number of outstanding HTLCs the remote node can offer. + /// + public ushort MaxAcceptedHtlcs { get; } + + /// + /// funding_pubkey is the public key in the 2-of-2 multisig script of the funding transaction output. + /// + public PubKey FundingPubKey { get; set; } + + /// + /// revocation_basepoint is used to regenerate the scripts required for the penalty transaction + /// + public PubKey RevocationBasepoint { get; set; } + + /// + /// payment_basepoint is used to produce payment signatures for the protocol + /// + public PubKey PaymentBasepoint { get; set; } + + /// + /// delayed_payment_basepoint is used to regenerate the scripts required for the penalty transaction + /// + public PubKey DelayedPaymentBasepoint { get; set; } + + /// + /// htlc_basepoint is used to produce HTLC signatures for the protocol + /// + public PubKey HtlcBasepoint { get; set; } + + /// + /// first_per_commitment_point is the per-commitment point used for the first commitment transaction + /// + public PubKey FirstPerCommitmentPoint { get; set; } + + /// + /// Only the least-significant bit of channel_flags is currently defined: announce_channel. This indicates whether + /// the initiator of the funding flow wishes to advertise this channel publicly to the network + /// + public ChannelFlags ChannelFlags { get; set; } + + public OpenChannel1Payload(ChainHash chainHash, ChannelId temporaryChannelId, LightningMoney fundingAmount, + LightningMoney pushAmount, LightningMoney dustLimitAmount, + LightningMoney maxHtlcValueInFlight, LightningMoney channelReserveAmount, + LightningMoney htlcMinimumAmount, LightningMoney feeRatePerKw, ushort toSelfDelay, + ushort maxAcceptedHtlcs, PubKey fundingPubKey, PubKey revocationBasepoint, + PubKey paymentBasepoint, PubKey delayedPaymentBasepoint, PubKey htlcBasepoint, + PubKey firstPerCommitmentPoint, ChannelFlags channelFlags) + { + ChainHash = chainHash; + TemporaryChannelId = temporaryChannelId; + FundingAmount = fundingAmount; + PushAmount = pushAmount; + DustLimitAmount = dustLimitAmount; + MaxHtlcValueInFlight = maxHtlcValueInFlight; + ChannelReserveAmount = channelReserveAmount; + HtlcMinimumAmount = htlcMinimumAmount; + FeeRatePerKw = feeRatePerKw; + ToSelfDelay = toSelfDelay; + MaxAcceptedHtlcs = maxAcceptedHtlcs; + FundingPubKey = fundingPubKey; + RevocationBasepoint = revocationBasepoint; + PaymentBasepoint = paymentBasepoint; + DelayedPaymentBasepoint = delayedPaymentBasepoint; + HtlcBasepoint = htlcBasepoint; + FirstPerCommitmentPoint = firstPerCommitmentPoint; + ChannelFlags = channelFlags; + } + + /// + public async Task SerializeAsync(Stream stream) + { + await ChainHash.SerializeAsync(stream); + await TemporaryChannelId.SerializeAsync(stream); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(FundingAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(PushAmount.MilliSatoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(FundingAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(DustLimitAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(MaxHtlcValueInFlight.MilliSatoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(ChannelReserveAmount.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(HtlcMinimumAmount.MilliSatoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(FeeRatePerKw.Satoshi)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(ToSelfDelay)); + await stream.WriteAsync(EndianBitConverter.GetBytesBigEndian(MaxAcceptedHtlcs)); + await stream.WriteAsync(FundingPubKey.ToBytes()); + await stream.WriteAsync(RevocationBasepoint.ToBytes()); + await stream.WriteAsync(PaymentBasepoint.ToBytes()); + await stream.WriteAsync(DelayedPaymentBasepoint.ToBytes()); + await stream.WriteAsync(HtlcBasepoint.ToBytes()); + await stream.WriteAsync(FirstPerCommitmentPoint.ToBytes()); + await ChannelFlags.SerializeAsync(stream); + } + + /// + /// Deserializes the payload from a stream. + /// + /// The stream to deserialize from. + /// The deserialized payload. + /// Error deserializing Payload + public static async Task DeserializeAsync(Stream stream) + { + var bytes = ArrayPool.Shared.Rent(33); + + try + { + var chainHash = await ChainHash.DeserializeAsync(stream); + var temporaryChannelId = await ChannelId.DeserializeAsync(stream); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var fundingSatoshis = LightningMoney.Satoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var pushAmount = LightningMoney.MilliSatoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var dustLimitSatoshis = LightningMoney + .Satoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var maxHtlcValueInFlight = LightningMoney + .MilliSatoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + var channelReserveAmount = LightningMoney + .Satoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ulong)]); + var htlcMinimumAmount = LightningMoney + .MilliSatoshis(EndianBitConverter.ToUInt64BigEndian(bytes[..sizeof(ulong)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(uint)]); + var feeRatePerKw = LightningMoney.Satoshis(EndianBitConverter.ToUInt32BigEndian(bytes[..sizeof(uint)])); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ushort)]); + var toSelfDelay = EndianBitConverter.ToUInt16BigEndian(bytes[..sizeof(ushort)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..sizeof(ushort)]); + var maxAcceptedHtlcs = EndianBitConverter.ToUInt16BigEndian(bytes[..sizeof(ushort)]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var fundingPubKey = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var revocationBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var paymentBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var delayedPaymentBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var htlcBasepoint = new PubKey(bytes[..33]); + + await stream.ReadExactlyAsync(bytes.AsMemory()[..33]); + var firstPerCommitmentPoint = new PubKey(bytes[..33]); + + var channelFlags = await ChannelFlags.DeserializeAsync(stream); + + return new OpenChannel1Payload(chainHash, temporaryChannelId, fundingSatoshis, pushAmount, + dustLimitSatoshis, maxHtlcValueInFlight, channelReserveAmount, + htlcMinimumAmount, feeRatePerKw, toSelfDelay, maxAcceptedHtlcs, + fundingPubKey, revocationBasepoint, paymentBasepoint, + delayedPaymentBasepoint, htlcBasepoint, firstPerCommitmentPoint, + channelFlags); + } + catch (Exception e) + { + throw new PayloadSerializationException("Error deserializing OpenChannel2Payload", e); + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } +} \ No newline at end of file diff --git a/src/NLightning.Common/Options/NodeOptions.cs b/src/NLightning.Common/Options/NodeOptions.cs index a9b9c60..0a73c98 100644 --- a/src/NLightning.Common/Options/NodeOptions.cs +++ b/src/NLightning.Common/Options/NodeOptions.cs @@ -1,15 +1,15 @@ -using NLightning.Common.Types; - namespace NLightning.Common.Options; +using Constants; using Models; +using Types; public class NodeOptions { /// /// The network to connect to. Can be "mainnet", "testnet", or "regtest" /// - public string Network { get; set; } = "mainnet"; + public Network Network { get; set; } = NetworkConstants.MAINNET; /// /// True if NLTG should run in Daemon mode (background)