Skip to content

Commit df5fbc7

Browse files
canndrewknocte
authored andcommitted
Implement revocation (#11)
This commit adds the following: * getFundsFromForceClosingTransaction This function takes an on-chain transaction which spends the channel funds and tries to extract spendable utxos out of it. If successful, it returns a TransactionBuilder with txins added for each spendable output of the closing transaction and with the necessary keys and BuilderExtension added. To recover funds from a broadcast commitment transaction you just need to call this function, add outputs to the returned TransactionBuilder to send the money where you want it to go, then broadcast the transaction. * CommitmentToLocalBuilderExtension This is an NBitcoin BuilderExtension that tells TransactionBuilder how to recognise and sign lightning commitment transaction to_local outputs. getFundsFromForceClosingTransaction adds this extension to the TransactionBuilder it returns if it's needed to sign the transaction. * SeqConsumer A computation expression which makes it easy to write code that consumes a sequence one element at a time. * OptionCE A computation expression for creation options. Similar to the result computation expression.
1 parent 151599c commit df5fbc7

File tree

8 files changed

+444
-1
lines changed

8 files changed

+444
-1
lines changed

src/DotNetLightning.Core/Channel/Channel.fs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,6 @@ module Channel =
574574
RemoteCommit = theirNextCommit
575575
RemoteNextCommitInfo = RemoteNextCommitInfo.Revoked msg.NextPerCommitmentPoint
576576
RemotePerCommitmentSecrets = remotePerCommitmentSecrets }
577-
Console.WriteLine("WARNING: revocation is not implemented yet")
578577
Ok [ WeAcceptedRevokeAndACK(commitments1) ]
579578

580579
| ChannelState.Normal state, ChannelCommand.Close cmd ->
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
namespace DotNetLightning.Channel
2+
3+
open NBitcoin
4+
open NBitcoin.BuilderExtensions
5+
open DotNetLightning.Utils
6+
open DotNetLightning.Utils.SeqConsumer
7+
open DotNetLightning.Crypto
8+
9+
type CommitmentToLocalParameters = {
10+
RevocationPubKey: RevocationPubKey
11+
ToSelfDelay: BlockHeightOffset16
12+
LocalDelayedPubKey: DelayedPaymentPubKey
13+
}
14+
with
15+
static member TryExtractParameters (scriptPubKey: Script): Option<CommitmentToLocalParameters> =
16+
let ops =
17+
scriptPubKey.ToOps()
18+
// we have to collect it into a list and convert back to a seq
19+
// because the IEnumerable that NBitcoin gives us is internally
20+
// mutable.
21+
|> List.ofSeq
22+
|> Seq.ofList
23+
let checkOpCode(opcodeType: OpcodeType) = seqConsumer<Op> {
24+
let! op = NextInSeq()
25+
if op.Code = opcodeType then
26+
return ()
27+
else
28+
return! AbortSeqConsumer()
29+
}
30+
let consumeAllResult =
31+
SeqConsumer.ConsumeAll ops <| seqConsumer {
32+
do! checkOpCode OpcodeType.OP_IF
33+
let! opRevocationPubKey = NextInSeq()
34+
let! revocationPubKey = seqConsumer {
35+
match opRevocationPubKey.PushData with
36+
| null -> return! AbortSeqConsumer()
37+
| bytes -> return RevocationPubKey.FromBytes bytes // FIXME: catch exception
38+
}
39+
do! checkOpCode OpcodeType.OP_ELSE
40+
let! opToSelfDelay = NextInSeq()
41+
let! toSelfDelay = seqConsumer {
42+
let nullableToSelfDelay = opToSelfDelay.GetLong()
43+
if nullableToSelfDelay.HasValue then
44+
// FIXME: catch exception
45+
return BlockHeightOffset16 (uint16 nullableToSelfDelay.Value)
46+
else
47+
return! AbortSeqConsumer()
48+
}
49+
do! checkOpCode OpcodeType.OP_CHECKSEQUENCEVERIFY
50+
do! checkOpCode OpcodeType.OP_DROP
51+
let! opLocalDelayedPubKey = NextInSeq()
52+
let! localDelayedPubKey = seqConsumer {
53+
match opLocalDelayedPubKey.PushData with
54+
| null -> return! AbortSeqConsumer()
55+
| bytes -> return DelayedPaymentPubKey.FromBytes bytes // FIXME: catch exception
56+
}
57+
do! checkOpCode OpcodeType.OP_ENDIF
58+
do! checkOpCode OpcodeType.OP_CHECKSIG
59+
return {
60+
RevocationPubKey = revocationPubKey
61+
ToSelfDelay = toSelfDelay
62+
LocalDelayedPubKey = localDelayedPubKey
63+
}
64+
}
65+
match consumeAllResult with
66+
| Ok data -> Some data
67+
| Error _consumeAllError -> None
68+
69+
type internal CommitmentToLocalExtension() =
70+
inherit BuilderExtension()
71+
override self.CanGenerateScriptSig (scriptPubKey: Script): bool =
72+
(CommitmentToLocalParameters.TryExtractParameters scriptPubKey).IsSome
73+
74+
override self.GenerateScriptSig(scriptPubKey: Script, keyRepo: IKeyRepository, signer: ISigner): Script =
75+
let parameters =
76+
match (CommitmentToLocalParameters.TryExtractParameters scriptPubKey) with
77+
| Some parameters -> parameters
78+
| None ->
79+
failwith
80+
"NBitcoin should not call this unless CanGenerateScriptSig returns true"
81+
let pubKey = keyRepo.FindKey scriptPubKey
82+
// FindKey will return null if it can't find a key for
83+
// scriptPubKey. If we can't find a valid key then this method
84+
// should return null, indicating to NBitcoin that the sigScript
85+
// could not be generated.
86+
match pubKey with
87+
| null -> null
88+
| _ when pubKey = parameters.RevocationPubKey.RawPubKey() ->
89+
let revocationSig = signer.Sign (parameters.RevocationPubKey.RawPubKey())
90+
Script [
91+
Op.GetPushOp (revocationSig.ToBytes())
92+
Op.op_Implicit OpcodeType.OP_TRUE
93+
]
94+
| _ when pubKey = parameters.LocalDelayedPubKey.RawPubKey() ->
95+
let localDelayedSig = signer.Sign (parameters.LocalDelayedPubKey.RawPubKey())
96+
Script [
97+
Op.GetPushOp (localDelayedSig.ToBytes())
98+
Op.op_Implicit OpcodeType.OP_FALSE
99+
]
100+
| _ -> null
101+
102+
override self.CanDeduceScriptPubKey(_scriptSig: Script): bool =
103+
false
104+
105+
override self.DeduceScriptPubKey(_scriptSig: Script): Script =
106+
raise <| System.NotSupportedException()
107+
108+
override self.CanEstimateScriptSigSize(_scriptPubKey: Script): bool =
109+
false
110+
111+
override self.EstimateScriptSigSize(_scriptPubKey: Script): int =
112+
raise <| System.NotSupportedException()
113+
114+
override self.CanCombineScriptSig(_scriptPubKey: Script, _a: Script, _b: Script): bool =
115+
false
116+
117+
override self.CombineScriptSig(_scriptPubKey: Script, _a: Script, _b: Script): Script =
118+
raise <| System.NotSupportedException()
119+
120+
override self.IsCompatibleKey(pubKey: PubKey, scriptPubKey: Script): bool =
121+
match CommitmentToLocalParameters.TryExtractParameters scriptPubKey with
122+
| None -> false
123+
| Some parameters ->
124+
parameters.RevocationPubKey.RawPubKey() = pubKey
125+
|| parameters.LocalDelayedPubKey.RawPubKey() = pubKey
126+
127+

src/DotNetLightning.Core/Channel/CommitmentsModule.fs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,179 @@ module internal Commitments =
414414
OriginChannels = originChannels1 }
415415
return [ WeAcceptedCommitmentSigned(nextMsg, nextCommitments) ;]
416416
}
417+
418+
module RemoteForceClose =
419+
// The lightning spec specifies that commitment txs use version 2 bitcoin transactions.
420+
let TxVersionNumberOfCommitmentTxs = 2u
421+
422+
let check(thing: bool): Option<unit> =
423+
if thing then
424+
Some ()
425+
else
426+
None
427+
428+
let tryGetObscuredCommitmentNumber (fundingOutPoint: OutPoint)
429+
(transaction: Transaction)
430+
: Option<ObscuredCommitmentNumber> = option {
431+
do! check (transaction.Version = TxVersionNumberOfCommitmentTxs)
432+
let! txIn = Seq.tryExactlyOne transaction.Inputs
433+
do! check (fundingOutPoint = txIn.PrevOut)
434+
let! obscuredCommitmentNumber =
435+
ObscuredCommitmentNumber.TryFromLockTimeAndSequence transaction.LockTime txIn.Sequence
436+
return obscuredCommitmentNumber
437+
}
438+
439+
let tryGetFundsFromRemoteCommitmentTx (commitments: Commitments)
440+
(localChannelPrivKeys: ChannelPrivKeys)
441+
(network: Network)
442+
(transaction: Transaction)
443+
: Option<TransactionBuilder> = option {
444+
let! obscuredCommitmentNumber =
445+
tryGetObscuredCommitmentNumber
446+
commitments.FundingScriptCoin.Outpoint
447+
transaction
448+
let localChannelPubKeys = commitments.LocalParams.ChannelPubKeys
449+
let remoteChannelPubKeys = commitments.RemoteParams.ChannelPubKeys
450+
let commitmentNumber =
451+
obscuredCommitmentNumber.Unobscure
452+
false
453+
localChannelPubKeys.PaymentBasepoint
454+
remoteChannelPubKeys.PaymentBasepoint
455+
let perCommitmentSecretOpt =
456+
commitments.RemotePerCommitmentSecrets.GetPerCommitmentSecret commitmentNumber
457+
let! perCommitmentPoint =
458+
match perCommitmentSecretOpt with
459+
| Some perCommitmentSecret -> Some <| perCommitmentSecret.PerCommitmentPoint()
460+
| None ->
461+
if commitments.RemoteCommit.Index = commitmentNumber then
462+
Some commitments.RemoteCommit.RemotePerCommitmentPoint
463+
else
464+
None
465+
466+
let localCommitmentPubKeys =
467+
perCommitmentPoint.DeriveCommitmentPubKeys localChannelPubKeys
468+
let remoteCommitmentPubKeys =
469+
perCommitmentPoint.DeriveCommitmentPubKeys remoteChannelPubKeys
470+
471+
let transactionBuilder = network.CreateTransactionBuilder()
472+
473+
let toRemoteScriptPubKey =
474+
localCommitmentPubKeys.PaymentPubKey.RawPubKey().WitHash.ScriptPubKey
475+
let toRemoteIndexOpt =
476+
Seq.tryFindIndex
477+
(fun (txOut: TxOut) -> txOut.ScriptPubKey = toRemoteScriptPubKey)
478+
transaction.Outputs
479+
let spentToRemoteOutput =
480+
match toRemoteIndexOpt with
481+
| None -> false
482+
| Some toRemoteIndex ->
483+
let localPaymentPrivKey =
484+
perCommitmentPoint.DerivePaymentPrivKey
485+
localChannelPrivKeys.PaymentBasepointSecret
486+
(transactionBuilder.AddKeys (localPaymentPrivKey.RawKey()))
487+
.AddCoins (Coin(transaction, uint32 toRemoteIndex))
488+
|> ignore
489+
true
490+
491+
let spentToLocalOutput =
492+
match perCommitmentSecretOpt with
493+
| None -> false
494+
| Some perCommitmentSecret ->
495+
let toLocalScriptPubKey =
496+
Scripts.toLocalDelayed
497+
localCommitmentPubKeys.RevocationPubKey
498+
commitments.RemoteParams.ToSelfDelay
499+
remoteCommitmentPubKeys.DelayedPaymentPubKey
500+
let toLocalIndexOpt =
501+
let toLocalWitScriptPubKey = toLocalScriptPubKey.WitHash.ScriptPubKey
502+
Seq.tryFindIndex
503+
(fun (txOut: TxOut) -> txOut.ScriptPubKey = toLocalWitScriptPubKey)
504+
transaction.Outputs
505+
match toLocalIndexOpt with
506+
| None -> false
507+
| Some toLocalIndex ->
508+
let revocationPrivKey =
509+
perCommitmentSecret.DeriveRevocationPrivKey
510+
localChannelPrivKeys.RevocationBasepointSecret
511+
transactionBuilder.Extensions.Add (CommitmentToLocalExtension())
512+
(transactionBuilder.AddKeys (revocationPrivKey.RawKey()))
513+
.AddCoins
514+
(ScriptCoin(transaction, uint32 toLocalIndex, toLocalScriptPubKey))
515+
|> ignore
516+
true
517+
518+
do! check (spentToRemoteOutput || spentToLocalOutput)
519+
520+
return transactionBuilder
521+
}
522+
523+
let tryGetFundsFromLocalCommitmentTx (commitments: Commitments)
524+
(localChannelPrivKeys: ChannelPrivKeys)
525+
(network: Network)
526+
(transaction: Transaction)
527+
: Option<TransactionBuilder> = option {
528+
let! obscuredCommitmentNumber =
529+
tryGetObscuredCommitmentNumber
530+
commitments.FundingScriptCoin.Outpoint
531+
transaction
532+
let localChannelPubKeys = commitments.LocalParams.ChannelPubKeys
533+
let remoteChannelPubKeys = commitments.RemoteParams.ChannelPubKeys
534+
let commitmentNumber =
535+
obscuredCommitmentNumber.Unobscure
536+
true
537+
localChannelPubKeys.PaymentBasepoint
538+
remoteChannelPubKeys.PaymentBasepoint
539+
540+
let perCommitmentPoint =
541+
localChannelPrivKeys.CommitmentSeed.DerivePerCommitmentPoint commitmentNumber
542+
let localCommitmentPubKeys =
543+
perCommitmentPoint.DeriveCommitmentPubKeys localChannelPubKeys
544+
let remoteCommitmentPubKeys =
545+
perCommitmentPoint.DeriveCommitmentPubKeys remoteChannelPubKeys
546+
547+
let transactionBuilder = network.CreateTransactionBuilder()
548+
549+
let toLocalScriptPubKey =
550+
Scripts.toLocalDelayed
551+
remoteCommitmentPubKeys.RevocationPubKey
552+
commitments.LocalParams.ToSelfDelay
553+
localCommitmentPubKeys.DelayedPaymentPubKey
554+
let! toLocalIndex =
555+
let toLocalWitScriptPubKey = toLocalScriptPubKey.WitHash.ScriptPubKey
556+
Seq.tryFindIndex
557+
(fun (txOut: TxOut) -> txOut.ScriptPubKey = toLocalWitScriptPubKey)
558+
transaction.Outputs
559+
560+
let delayedPaymentPrivKey =
561+
perCommitmentPoint.DeriveDelayedPaymentPrivKey
562+
localChannelPrivKeys.DelayedPaymentBasepointSecret
563+
transactionBuilder.Extensions.Add (CommitmentToLocalExtension())
564+
(transactionBuilder.AddKeys (delayedPaymentPrivKey.RawKey()))
565+
.AddCoins
566+
(ScriptCoin(transaction, uint32 toLocalIndex, toLocalScriptPubKey))
567+
|> ignore
568+
569+
return transactionBuilder
570+
}
571+
572+
let getFundsFromForceClosingTransaction (commitments: Commitments)
573+
(localChannelPrivKeys: ChannelPrivKeys)
574+
(network: Network)
575+
(transaction: Transaction)
576+
: Option<TransactionBuilder> =
577+
let attemptsSeq = seq {
578+
yield
579+
tryGetFundsFromRemoteCommitmentTx
580+
commitments
581+
localChannelPrivKeys
582+
network
583+
transaction
584+
yield
585+
tryGetFundsFromLocalCommitmentTx
586+
commitments
587+
localChannelPrivKeys
588+
network
589+
transaction
590+
}
591+
Seq.tryPick (fun opt -> opt) attemptsSeq
592+

src/DotNetLightning.Core/DotNetLightning.Core.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
</ItemGroup>
2828
<ItemGroup>
2929
<Compile Include="AssemblyInfo.fs" />
30+
<Compile Include="Utils/SeqConsumer.fs" />
3031
<Compile Include="Utils/LNMoney.fs" />
3132
<Compile Include="Utils\Extensions.fs" />
3233
<Compile Include="Utils/UInt48.fs" />
@@ -68,6 +69,7 @@
6869
<Compile Include="Peer\PeerChannelEncryptor.fs" />
6970
<Compile Include="Peer\PeerTypes.fs" />
7071
<Compile Include="Peer\Peer.fs" />
72+
<Compile Include="Channel\CommitmentToLocalExtension.fs" />
7173
<Compile Include="Channel\HTLCChannelType.fs" />
7274
<Compile Include="Channel\ChannelConstants.fs" />
7375
<Compile Include="Channel\ChannelOperations.fs" />

src/DotNetLightning.Core/Utils/Keys.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ type RevocationPubKey =
5959
let (RevocationPubKey pubKey) = this
6060
pubKey
6161

62+
static member FromBytes(bytes: array<byte>): RevocationPubKey =
63+
RevocationPubKey <| PubKey bytes
64+
6265
member this.ToBytes(): array<byte> =
6366
this.RawPubKey().ToBytes()
6467

@@ -151,6 +154,9 @@ type DelayedPaymentPubKey =
151154
let (DelayedPaymentPubKey pubKey) = this
152155
pubKey
153156

157+
static member FromBytes(bytes: array<byte>): DelayedPaymentPubKey =
158+
DelayedPaymentPubKey <| PubKey bytes
159+
154160
member this.ToBytes(): array<byte> =
155161
this.RawPubKey().ToBytes()
156162

0 commit comments

Comments
 (0)