diff --git a/src/DotNetLightning.Core/Channel/Channel.fs b/src/DotNetLightning.Core/Channel/Channel.fs index cd60df8df..4f7f2711b 100644 --- a/src/DotNetLightning.Core/Channel/Channel.fs +++ b/src/DotNetLightning.Core/Channel/Channel.fs @@ -583,6 +583,58 @@ and Channel = { return! Error <| OnceConfirmedFundingTxHasBecomeUnconfirmed(height, depth) } + member self.MonoHopUnidirectionalPayment (amount: LNMoney) + : Result = result { + if self.NegotiatingState.HasEnteredShutdown() then + return! + sprintf + "Could not send mono-hop unidirectional payment of amount %A since shutdown is already \ + in progress." amount + |> apiMisuse + else + let payment: MonoHopUnidirectionalPaymentMsg = { + ChannelId = self.SavedChannelState.StaticChannelConfig.ChannelId() + Amount = amount + } + let commitments1 = self.Commitments.AddLocalProposal(payment) + + let! remoteNextCommitInfo = + self.RemoteNextCommitInfoIfFundingLockedNormal "MonoHopUniDirectionalPayment" + let remoteCommit1 = + match remoteNextCommitInfo with + | Waiting nextRemoteCommit -> nextRemoteCommit + | Revoked _info -> self.SavedChannelState.RemoteCommit + let! reduced = remoteCommit1.Spec.Reduce(self.SavedChannelState.RemoteChanges.ACKed, commitments1.ProposedLocalChanges) |> expectTransactionError + do! + Validation.checkOurMonoHopUnidirectionalPaymentIsAcceptableWithCurrentSpec + reduced + self.SavedChannelState.StaticChannelConfig + payment + let channel = { + self with + Commitments = commitments1 + } + return channel, payment + } + + member self.ApplyMonoHopUnidirectionalPayment (msg: MonoHopUnidirectionalPaymentMsg) + : Result = result { + let commitments1 = self.Commitments.AddRemoteProposal(msg) + let! reduced = + self.SavedChannelState.LocalCommit.Spec.Reduce + (self.SavedChannelState.LocalChanges.ACKed, commitments1.ProposedRemoteChanges) + |> expectTransactionError + do! + Validation.checkTheirMonoHopUnidirectionalPaymentIsAcceptableWithCurrentSpec + reduced + self.SavedChannelState.StaticChannelConfig + msg + return { + self with + Commitments = commitments1 + } + } + member self.AddHTLC (op: OperationAddHTLC) : Result = result { if self.NegotiatingState.HasEnteredShutdown() then @@ -1116,6 +1168,45 @@ and Channel = { (not self.SavedChannelState.LocalChanges.ACKed.IsEmpty) || (not self.Commitments.ProposedRemoteChanges.IsEmpty) + member self.SpendableBalance (): LNMoney = + let remoteParams = self.SavedChannelState.StaticChannelConfig.RemoteParams + let remoteCommit = + match self.RemoteNextCommitInfo with + | Some (RemoteNextCommitInfo.Waiting nextRemoteCommit) -> nextRemoteCommit + | Some (RemoteNextCommitInfo.Revoked _info) -> self.SavedChannelState.RemoteCommit + // TODO: This could return a proper error, or report the full balance + | None -> failwith "funding is not locked" + let reducedRes = + remoteCommit.Spec.Reduce( + self.SavedChannelState.RemoteChanges.ACKed, + self.Commitments.ProposedLocalChanges + ) + let reduced = + match reducedRes with + | Error err -> + failwithf + "reducing commit failed even though we have not proposed any changes\ + error: %A" + err + | Ok reduced -> reduced + let fees = + if self.SavedChannelState.StaticChannelConfig.IsFunder then + Transactions.commitTxFee remoteParams.DustLimitSatoshis reduced + |> LNMoney.FromMoney + else + LNMoney.Zero + let channelReserve = + remoteParams.ChannelReserveSatoshis + |> LNMoney.FromMoney + let totalBalance = reduced.ToRemote + let untrimmedSpendableBalance = totalBalance - channelReserve - fees + let dustLimit = + remoteParams.DustLimitSatoshis + |> LNMoney.FromMoney + let untrimmedMax = LNMoney.Min(untrimmedSpendableBalance, dustLimit) + let spendableBalance = LNMoney.Max(untrimmedMax, untrimmedSpendableBalance) + spendableBalance + member private self.sendCommit (remoteNextCommitInfo: RemoteNextCommitInfo) : Result = let channelPrivKeys = self.ChannelPrivKeys diff --git a/src/DotNetLightning.Core/Channel/ChannelError.fs b/src/DotNetLightning.Core/Channel/ChannelError.fs index 1db5cd9f1..4c67f30dd 100644 --- a/src/DotNetLightning.Core/Channel/ChannelError.fs +++ b/src/DotNetLightning.Core/Channel/ChannelError.fs @@ -39,6 +39,7 @@ type ChannelError = | InvalidFundingLocked of InvalidFundingLockedError | InvalidOpenChannel of InvalidOpenChannelError | InvalidAcceptChannel of InvalidAcceptChannelError + | InvalidMonoHopUnidirectionalPayment of InvalidMonoHopUnidirectionalPaymentError | InvalidUpdateAddHTLC of InvalidUpdateAddHTLCError | InvalidRevokeAndACK of InvalidRevokeAndACKError | InvalidUpdateFee of InvalidUpdateFeeError @@ -76,6 +77,7 @@ type ChannelError = | InvalidFundingLocked _ -> DistrustPeer | InvalidOpenChannel _ -> DistrustPeer | InvalidAcceptChannel _ -> DistrustPeer + | InvalidMonoHopUnidirectionalPayment _ -> Close | InvalidUpdateAddHTLC _ -> Close | InvalidRevokeAndACK _ -> Close | InvalidUpdateFee _ -> Close @@ -143,6 +145,8 @@ type ChannelError = "Received commitment signed when we have not pending changes" | InvalidAcceptChannel invalidAcceptChannelError -> sprintf "Invalid accept_channel msg: %s" invalidAcceptChannelError.Message + | InvalidMonoHopUnidirectionalPayment invalidMonohopUnidirectionalPaymentError -> + sprintf "Invalid mono hop unidrectional payment: %s" invalidMonohopUnidirectionalPaymentError.Message | InvalidUpdateAddHTLC invalidUpdateAddHTLCError -> sprintf "Invalid udpate_add_htlc msg: %s" invalidUpdateAddHTLCError.Message | InvalidRevokeAndACK invalidRevokeAndACKError -> @@ -207,7 +211,18 @@ and InvalidAcceptChannelError = { } member this.Message = String.concat "; " this.Errors - +and InvalidMonoHopUnidirectionalPaymentError = { + NetworkMsg: MonoHopUnidirectionalPaymentMsg + Errors: string list +} + with + static member Create msg e = { + NetworkMsg = msg + Errors = e + } + member this.Message = + String.concat "; " this.Errors + and InvalidUpdateAddHTLCError = { NetworkMsg: UpdateAddHTLCMsg Errors: string list @@ -554,7 +569,23 @@ module internal AcceptChannelMsgValidation = let check7 = check (msg.MinimumDepth.Value) (>) (config.MaxMinimumDepth.Value |> uint32) "We consider the minimum depth (%A) to be unreasonably large. Our max minimum depth is (%A)" (check1 |> Validation.ofResult) *^> check2 *^> check3 *^> check4 *^> check5 *^> check6 *^> check7 - + +module UpdateMonoHopUnidirectionalPaymentWithContext = + let internal checkWeHaveSufficientFunds (staticChannelConfig: StaticChannelConfig) (currentSpec) = + let fees = + if staticChannelConfig.IsFunder then + Transactions.commitTxFee staticChannelConfig.RemoteParams.DustLimitSatoshis currentSpec + else + Money.Zero + let missing = currentSpec.ToRemote.ToMoney() - staticChannelConfig.RemoteParams.ChannelReserveSatoshis - fees + if missing < Money.Zero then + sprintf "We don't have sufficient funds to send mono-hop unidirectional payment. current to_remote amount is: %A. Remote Channel Reserve is: %A. and fee is %A" + (currentSpec.ToRemote.ToMoney()) + staticChannelConfig.RemoteParams.ChannelReserveSatoshis + fees + |> Error + else + Ok() module UpdateAddHTLCValidation = let internal checkExpiryIsNotPast (current: BlockHeight) (expiry) = @@ -569,7 +600,23 @@ module UpdateAddHTLCValidation = let internal checkAmountIsLargerThanMinimum (htlcMinimum: LNMoney) (amount) = check (amount) (<) (htlcMinimum) "htlc value (%A) is too small. must be greater or equal to %A" - +module internal MonoHopUnidirectionalPaymentValidationWithContext = + let checkWeHaveSufficientFunds (staticChannelConfig: StaticChannelConfig) (currentSpec) = + let fees = + if staticChannelConfig.IsFunder then + Transactions.commitTxFee staticChannelConfig.RemoteParams.DustLimitSatoshis currentSpec + else + Money.Zero + let missing = currentSpec.ToRemote.ToMoney() - staticChannelConfig.RemoteParams.ChannelReserveSatoshis - fees + if missing < Money.Zero then + sprintf "We don't have sufficient funds to send mono-hop unidirectional payment. current to_remote amount is: %A. Remote Channel Reserve is: %A. and fee is %A" + (currentSpec.ToRemote.ToMoney()) + staticChannelConfig.RemoteParams.ChannelReserveSatoshis + fees + |> Error + else + Ok() + module internal UpdateAddHTLCValidationWithContext = let checkLessThanHTLCValueInFlightLimit (currentSpec: CommitmentSpec) (limit) (add: UpdateAddHTLCMsg) = let outgoingValue = diff --git a/src/DotNetLightning.Core/Channel/ChannelValidation.fs b/src/DotNetLightning.Core/Channel/ChannelValidation.fs index ab5330cd3..8b5fbba45 100644 --- a/src/DotNetLightning.Core/Channel/ChannelValidation.fs +++ b/src/DotNetLightning.Core/Channel/ChannelValidation.fs @@ -214,6 +214,23 @@ module internal Validation = *> AcceptChannelMsgValidation.checkConfigPermits channelHandshakeLimits acceptChannelMsg |> Result.mapError(InvalidAcceptChannelError.Create acceptChannelMsg >> InvalidAcceptChannel) + let checkOurMonoHopUnidirectionalPaymentIsAcceptableWithCurrentSpec (currentSpec) + (staticChannelConfig: StaticChannelConfig) + (payment: MonoHopUnidirectionalPaymentMsg) = + MonoHopUnidirectionalPaymentValidationWithContext.checkWeHaveSufficientFunds + staticChannelConfig + currentSpec + |> Validation.ofResult + |> Result.mapError(fun errs -> InvalidMonoHopUnidirectionalPayment { NetworkMsg = payment; Errors = errs }) + + let checkTheirMonoHopUnidirectionalPaymentIsAcceptableWithCurrentSpec (currentSpec) + (staticChannelConfig: StaticChannelConfig) + (payment: MonoHopUnidirectionalPaymentMsg) = + MonoHopUnidirectionalPaymentValidationWithContext.checkWeHaveSufficientFunds + staticChannelConfig + currentSpec + |> Validation.ofResult + |> Result.mapError(fun errs -> InvalidMonoHopUnidirectionalPayment { NetworkMsg = payment; Errors = errs }) let checkOperationAddHTLC (remoteParams: RemoteParams) (op: OperationAddHTLC) = Validation.ofResult(UpdateAddHTLCValidation.checkExpiryIsNotPast op.CurrentHeight op.Expiry) diff --git a/src/DotNetLightning.Core/Crypto/ShaChain.fs b/src/DotNetLightning.Core/Crypto/ShaChain.fs index 3a027ddb0..457f50e49 100644 --- a/src/DotNetLightning.Core/Crypto/ShaChain.fs +++ b/src/DotNetLightning.Core/Crypto/ShaChain.fs @@ -1,5 +1,7 @@ namespace DotNetLightning.Crypto +open System + type Node = { Value: byte[] Height: int32 @@ -16,8 +18,9 @@ module ShaChain = let flip (_input: byte[]) (_index: uint64): byte[] = failwith "Not implemented: ShaChain::flip" - let addHash (_receiver: ShaChain) (_hash: byte[]) (_index: uint64) = - failwith "Not implemented: ShaChain::addHash" + let addHash (receiver: ShaChain) (_hash: byte[]) (_index: uint64) = + Console.WriteLine("WARNING: Not implemented: ShaChain::addHash") + receiver let getHash (_receiver: ShaChain)(_index: uint64) = failwith "Not implemented: ShaChain::getHash" diff --git a/src/DotNetLightning.Core/Serialization/Msgs/Msgs.fs b/src/DotNetLightning.Core/Serialization/Msgs/Msgs.fs index cc8714484..4548ab357 100644 --- a/src/DotNetLightning.Core/Serialization/Msgs/Msgs.fs +++ b/src/DotNetLightning.Core/Serialization/Msgs/Msgs.fs @@ -104,6 +104,8 @@ module internal TypeFlag = let ReplyChannelRange = 264us [] let GossipTimestampFilter = 265us + [] + let MonoHopUnidirectionalPayment = 42198us type ILightningMsg = interface end type ISetupMsg = inherit ILightningMsg @@ -195,6 +197,8 @@ module ILightningSerializable = deserialize(ls) :> ILightningMsg | TypeFlag.GossipTimestampFilter -> deserialize(ls) :> ILightningMsg + | TypeFlag.MonoHopUnidirectionalPayment -> + deserialize(ls) :> ILightningMsg | x -> raise <| FormatException(sprintf "Unknown message type %d" x) let serializeWithFlags (ls: LightningWriterStream) (data: ILightningMsg) = @@ -283,6 +287,9 @@ module ILightningSerializable = | :? GossipTimestampFilterMsg as d -> ls.Write(TypeFlag.GossipTimestampFilter, false) (d :> ILightningSerializable).Serialize(ls) + | :? MonoHopUnidirectionalPaymentMsg as d -> + ls.Write(TypeFlag.MonoHopUnidirectionalPayment, false) + (d :> ILightningSerializable).Serialize(ls) | x -> failwithf "%A is not known lightning message. This should never happen" x module LightningMsg = @@ -1592,4 +1599,19 @@ type GossipTimestampFilterMsg = { ls.Write(this.ChainHash, true) ls.Write(this.FirstTimestamp, false) ls.Write(this.TimestampRange, false) - + +[] +type MonoHopUnidirectionalPaymentMsg = { + mutable ChannelId: ChannelId + mutable Amount: LNMoney +} +with + interface IHTLCMsg + interface IUpdateMsg + interface ILightningSerializable with + member this.Deserialize(ls) = + this.ChannelId <- ls.ReadUInt256(true) |> ChannelId + this.Amount <- ls.ReadUInt64(false) |> LNMoney.MilliSatoshis + member this.Serialize(ls) = + ls.Write(this.ChannelId.Value.ToBytes()) + ls.Write(this.Amount.MilliSatoshi, false) diff --git a/src/DotNetLightning.Core/Transactions/CommitmentSpec.fs b/src/DotNetLightning.Core/Transactions/CommitmentSpec.fs index 4bca46369..5749e6d77 100644 --- a/src/DotNetLightning.Core/Transactions/CommitmentSpec.fs +++ b/src/DotNetLightning.Core/Transactions/CommitmentSpec.fs @@ -34,6 +34,12 @@ type CommitmentSpec = { (fun cs -> cs.ToRemote), (fun v cs -> { cs with ToRemote = v }) + member internal this.AddOutgoingMonoHopUnidirectionalPayment(update: MonoHopUnidirectionalPaymentMsg) = + { this with ToLocal = this.ToLocal - update.Amount; ToRemote = this.ToRemote + update.Amount } + + member internal this.AddIncomingMonoHopUnidirectionalPayment(update: MonoHopUnidirectionalPaymentMsg) = + { this with ToLocal = this.ToLocal + update.Amount; ToRemote = this.ToRemote - update.Amount } + member internal this.AddOutgoingHTLC(update: UpdateAddHTLCMsg) = { this with ToLocal = (this.ToLocal - update.Amount); OutgoingHTLCs = this.OutgoingHTLCs.Add(update.HTLCId, update)} @@ -73,6 +79,24 @@ type CommitmentSpec = { UnknownHTLC htlcId |> Error member internal this.Reduce(localChanges: #IUpdateMsg list, remoteChanges: #IUpdateMsg list) = + let specMonoHopUnidirectionalPaymentLocal = + localChanges + |> List.fold(fun (acc: CommitmentSpec) updateMsg -> + match box updateMsg with + | :? MonoHopUnidirectionalPaymentMsg as u -> acc.AddOutgoingMonoHopUnidirectionalPayment u + | _ -> acc + ) + this + + let specMonoHopUnidirectionalPaymentRemote = + remoteChanges + |> List.fold(fun (acc: CommitmentSpec) updateMsg -> + match box updateMsg with + | :? MonoHopUnidirectionalPaymentMsg as u -> acc.AddIncomingMonoHopUnidirectionalPayment u + | _ -> acc + ) + specMonoHopUnidirectionalPaymentLocal + let spec1 = localChanges |> List.fold(fun (acc: CommitmentSpec) updateMsg -> @@ -80,7 +104,7 @@ type CommitmentSpec = { | :? UpdateAddHTLCMsg as u -> acc.AddOutgoingHTLC u | _ -> acc ) - this + specMonoHopUnidirectionalPaymentRemote let spec2 = remoteChanges diff --git a/src/DotNetLightning.Core/Utils/LNMoney.fs b/src/DotNetLightning.Core/Utils/LNMoney.fs index 9ecdfe80b..dfcef90ae 100644 --- a/src/DotNetLightning.Core/Utils/LNMoney.fs +++ b/src/DotNetLightning.Core/Utils/LNMoney.fs @@ -39,6 +39,8 @@ type LNMoney = | LNMoney of int64 with let satoshi = Checked.op_Multiply (amount) (decimal lnUnit) LNMoney(Checked.int64 satoshi) + static member FromMoney (money: Money) = + LNMoney.Satoshis money.Satoshi static member Coins(coins: decimal) = LNMoney.FromUnit(coins * (decimal LNMoneyUnit.BTC), LNMoneyUnit.MilliSatoshi)