Skip to content

Commit b08911d

Browse files
metalicjamesadiabat
authored andcommitted
[WIP] Add HTLCs to channels (#251)
* Very preliminary woork * OfferHTLC function and HashSig message * Fewer RTTs by pre-sharing HTLC basepoints * Generating commitment transactions containing HTLC outputs * Generate our HTLCBase and finish building the commitment transaction and HTLC-failure/success transactions for a given state * Sign their txs that they store * Verify our txs that we store * Remove TODO and fix comment * Work so far compiles * HTLC serialisation and deserialisation * Save HTLCs in the database and reload them * Exchange HTLC bases during channel fund * Only add an InProgHTLC to the channel state if it's not nil * Define HTLCBytes * Missed PointRespMsg serialisation * PointResp is 166 bytes * StatCom is now a different size too * More error messages * Be sure to write false if there is no InProgHTLC * Remove debugging prints * Send and verify HTLCSigs with DeltaSig, GapSigRev and SigRev messages * Add HashSigHandler * Add AddHTLC RPC to litrpc and lit-af * Lit knows how to decode a HashSig message now * I guess slices don't work like that... * It's a hashsig not a deltasig * The Amt is 8 bytes * Add some loggin during HTLC output build * Add RHash to HashSigMsg * buffer.Len() gives the unread buffer length * Logging HTLC signatures * Generate new privkey for each HTLC state using elkpoints * Use an elkpoint we actually have * Use elkrem point we have * Print elkpoints being used for signing/verifying HTLC txs * Try next elkpoint * Print pkh output amt * Only subtract HTLC balances if they're incoming * Try calculating amounts this way instead * Only reject REV if there's no inProgHTLC * Those pesky slices again * Work on output value calculation * Give N2HTLCBase after signing * InProgHTLC was already nil when calculating a new HTLCBase was required * Progress on clearing HTLCs * Add clear to litRPC and lit-af * Check for error when building state tx in BuildJusticeSig * Do not fail RevHandler if we're clearing an HTLC * Check we're not clearing in SigRevHandler too * Locktime check was backwards * Only subtract uncleared HTLCs from channel value when calculating balances * Only subtract uncleared HTLCs from channel value when calculating balances here too * Give CTS after non-fatal clear HTLC errors * Refactor balance calculation to its own function * Fix compile * Potentially fix segfault * No need to check these things and fix some messages * Don't allow coop close if there are uncleared HTLCs * Check their close tx signature * Allows connecting to hostnames (again) * Added extra logging, fixed compatibility with master * Detect and ignore HTLC outputs from GetCloseTxos * Store and monitor HTLC Outpoints * Store and monitor HTLC outpoints * Restructuring of HTLC watching * Added code to claim HTLC, work in progress * Work in progress on claim TX * Split generate script and output so we can fetch the script for spending * Fixed witness program verification error * Additional logging * Added claim command to lit-af for testing purposes * Succesful on-chain claim of HTLC Success TX * Claim HTLCs when we see the preimage in an on-chain spend * Add timeout UTXO to the wallit after claiming success htlc after break * Claiming HTLC timeouts onchain * This was good, presig stack was wrong. * Fixes locktime problem * Justice implementation * Fixed refactoring bug * Optimized logging * Justice TXes working for HTLC * Fixes broken unit tests for HTLC * Adds configurable locktime * Fix crashed caused by off-by-one error in argument passing code for HTLCs * Fixes invalid memory address bug * Ignore further processing if no HTLC is found * Added separate boolean for tracking on-chain clearing of HTLC * HLTC byte size is now 1 bigger too * Bugfix in signature verification in PreimageSigHandler * Correct log statements to use log.Printf in stead of fmt * Unlock the channel on error, fetch the Qchan from the right place * Height 0 should show as unconfirmed in lit-af * Handling HashSig-HashSig collisions * Manage DeltaSig-HashSig collisions * Manage HashSig-PreimageSig collisions * Manage PreimageSig-PreimageSig collisions * Manage PreimageSig-DeltaSig collisions * Fixes unit test for GapSigRev * Try autoclearing after preimagesig * Fix deadlock caused by holding RemoteMtx when blocking on OmniOut send * Use the real Qchan when clearing
1 parent 2c301b8 commit b08911d

35 files changed

+3616
-226
lines changed

.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
webui/

cmd/lit-af/chancmds.go

+152
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ var historyCommand = &Command{
9696
ShortDescription: "Show all the metadata for justice txs.\n",
9797
}
9898

99+
var addHTLCCommand = &Command{
100+
Format: fmt.Sprintf("%s%s%s\n", lnutil.White("add"), lnutil.ReqColor("channel idx", "amount", "locktime", "RHash"), lnutil.OptColor("data")),
101+
Description: fmt.Sprintf("%s\n%s\n",
102+
"Add an HTLC of the given amount (in satoshis) to the given channel. Locktime specifies the number of blocks the HTLC stays active before timing out",
103+
"Optionally, the push operation can be associated with a 32 byte value hex encoded."),
104+
ShortDescription: "Add HTLC of the given amount (in satoshis) to the given channel.\n",
105+
}
106+
107+
var clearHTLCCommand = &Command{
108+
Format: fmt.Sprintf("%s%s%s\n", lnutil.White("clear"), lnutil.ReqColor("channel idx", "HTLC idx", "R"), lnutil.OptColor("data")),
109+
Description: fmt.Sprintf("%s\n%s\n%s\n",
110+
"Clear an HTLC of the given index from the given channel.",
111+
"Optionally, the push operation can be associated with a 32 byte value hex encoded.",
112+
"Set R to zero to timeout the HTLC"),
113+
ShortDescription: "Clear HTLC of the given index from the given channel.\n",
114+
}
115+
116+
var claimHTLCCommand = &Command{
117+
Format: fmt.Sprintf("%s%s\n", lnutil.White("claim"), lnutil.ReqColor("R")),
118+
Description: "Claim any on-chain HTLC that matches the given preimage. Use this to claim an HTLC after the channel is broken.\n",
119+
ShortDescription: "Clear HTLC of the given index from the given channel.\n",
120+
}
121+
99122
func (lc *litAfClient) History(textArgs []string) error {
100123
if len(textArgs) > 0 && textArgs[0] == "-h" {
101124
fmt.Fprintf(color.Output, historyCommand.Format)
@@ -436,3 +459,132 @@ func (lc *litAfClient) Watch(textArgs []string) error {
436459

437460
return nil
438461
}
462+
463+
// Add is the shell command which calls AddHTLC
464+
func (lc *litAfClient) AddHTLC(textArgs []string) error {
465+
stopEx, err := CheckHelpCommand(addHTLCCommand, textArgs, 3)
466+
467+
if err != nil || stopEx {
468+
return err
469+
}
470+
471+
args := new(litrpc.AddHTLCArgs)
472+
reply := new(litrpc.AddHTLCReply)
473+
474+
// this stuff is all the same as in cclose, should put into a function...
475+
cIdx, err := strconv.Atoi(textArgs[0])
476+
if err != nil {
477+
return err
478+
}
479+
amt, err := strconv.Atoi(textArgs[1])
480+
if err != nil {
481+
return err
482+
}
483+
locktime, err := strconv.Atoi(textArgs[2])
484+
if err != nil {
485+
return err
486+
}
487+
488+
RHash, err := hex.DecodeString(textArgs[3])
489+
if err != nil {
490+
return err
491+
}
492+
copy(args.RHash[:], RHash[:])
493+
494+
if len(textArgs) > 4 {
495+
data, err := hex.DecodeString(textArgs[4])
496+
if err != nil {
497+
// Wasn't valid hex, copy directly and truncate
498+
copy(args.Data[:], textArgs[4])
499+
} else {
500+
copy(args.Data[:], data[:])
501+
}
502+
}
503+
504+
args.ChanIdx = uint32(cIdx)
505+
args.Amt = int64(amt)
506+
args.LockTime = uint32(locktime)
507+
508+
err = lc.Call("LitRPC.AddHTLC", args, reply)
509+
if err != nil {
510+
return err
511+
}
512+
fmt.Fprintf(color.Output, "Added HTLC %s at state %s idx %s\n", lnutil.SatoshiColor(int64(amt)), lnutil.White(reply.StateIndex), lnutil.White(reply.HTLCIndex))
513+
514+
return nil
515+
}
516+
517+
// Clear is the shell command which calls ClearHTLC
518+
func (lc *litAfClient) ClearHTLC(textArgs []string) error {
519+
stopEx, err := CheckHelpCommand(clearHTLCCommand, textArgs, 3)
520+
if err != nil || stopEx {
521+
return err
522+
}
523+
524+
args := new(litrpc.ClearHTLCArgs)
525+
reply := new(litrpc.ClearHTLCReply)
526+
527+
// this stuff is all the same as in cclose, should put into a function...
528+
cIdx, err := strconv.Atoi(textArgs[0])
529+
if err != nil {
530+
return err
531+
}
532+
HTLCIdx, err := strconv.Atoi(textArgs[1])
533+
if err != nil {
534+
return err
535+
}
536+
537+
R, err := hex.DecodeString(textArgs[2])
538+
if err != nil {
539+
return err
540+
}
541+
copy(args.R[:], R[:])
542+
543+
if len(textArgs) > 3 {
544+
data, err := hex.DecodeString(textArgs[3])
545+
if err != nil {
546+
// Wasn't valid hex, copy directly and truncate
547+
copy(args.Data[:], textArgs[3])
548+
} else {
549+
copy(args.Data[:], data[:])
550+
}
551+
}
552+
553+
args.ChanIdx = uint32(cIdx)
554+
args.HTLCIdx = uint32(HTLCIdx)
555+
556+
err = lc.Call("LitRPC.ClearHTLC", args, reply)
557+
if err != nil {
558+
return err
559+
}
560+
fmt.Fprintf(color.Output, "Cleared HTLC %s at state %s\n", lnutil.White(HTLCIdx), lnutil.White(reply.StateIndex))
561+
562+
return nil
563+
}
564+
565+
// Clear is the shell command which calls ClearHTLC
566+
func (lc *litAfClient) ClaimHTLC(textArgs []string) error {
567+
stopEx, err := CheckHelpCommand(claimHTLCCommand, textArgs, 1)
568+
if err != nil || stopEx {
569+
return err
570+
}
571+
572+
args := new(litrpc.ClaimHTLCArgs)
573+
reply := new(litrpc.TxidsReply)
574+
575+
R, err := hex.DecodeString(textArgs[0])
576+
if err != nil {
577+
return err
578+
}
579+
copy(args.R[:], R[:])
580+
581+
err = lc.Call("LitRPC.ClaimHTLC", args, reply)
582+
if err != nil {
583+
return err
584+
}
585+
for _, txid := range reply.Txids {
586+
fmt.Fprintf(color.Output, "Claimed HTLC with txid %s\n", lnutil.White(txid))
587+
}
588+
589+
return nil
590+
}

cmd/lit-af/shell.go

+18-3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ func (lc *litAfClient) Shellparse(cmdslice []string) error {
108108
return parseErr(err, "push")
109109
}
110110

111+
if cmd == "add" {
112+
err = lc.AddHTLC(args)
113+
return parseErr(err, "add")
114+
}
115+
116+
if cmd == "clear" {
117+
err = lc.ClearHTLC(args)
118+
return parseErr(err, "clear")
119+
}
120+
121+
if cmd == "claim" {
122+
err = lc.ClaimHTLC(args)
123+
return parseErr(err, "claim")
124+
}
125+
111126
if cmd == "con" { // connect to lnd host
112127
err = lc.Connect(args)
113128
return parseErr(err, "con")
@@ -275,7 +290,7 @@ func (lc *litAfClient) Ls(textArgs []string) error {
275290
}
276291

277292
for _, c := range openChannels {
278-
if c.Height == -1 {
293+
if c.Height <= 0 {
279294
c := color.New(color.FgGreen).Add(color.Underline)
280295
c.Printf("Unconfirmed:")
281296
fmt.Fprintf(color.Output, lnutil.Green(" "))
@@ -398,15 +413,15 @@ func printHelp(commands []*Command) {
398413
func printCointypes() {
399414
for k, v := range coinparam.RegisteredNets {
400415
fmt.Fprintf(color.Output, "CoinType: %s\n", strconv.Itoa(int(k)))
401-
fmt.Fprintf(color.Output, "└────── Name: %-13sBech32Prefix: %s\n\n", v.Name + ",", v.Bech32Prefix)
416+
fmt.Fprintf(color.Output, "└────── Name: %-13sBech32Prefix: %s\n\n", v.Name+",", v.Bech32Prefix)
402417
}
403418
}
404419

405420
func (lc *litAfClient) Help(textArgs []string) error {
406421
if len(textArgs) == 0 {
407422

408423
fmt.Fprintf(color.Output, lnutil.Header("Commands:\n"))
409-
listofCommands := []*Command{helpCommand, sayCommand, lsCommand, addressCommand, sendCommand, fanCommand, sweepCommand, lisCommand, conCommand, dlcCommand, fundCommand, dualFundCommand, watchCommand, pushCommand, closeCommand, breakCommand, historyCommand, offCommand, exitCommand}
424+
listofCommands := []*Command{helpCommand, sayCommand, lsCommand, addressCommand, sendCommand, fanCommand, sweepCommand, lisCommand, conCommand, dlcCommand, fundCommand, dualFundCommand, watchCommand, pushCommand, closeCommand, breakCommand, addHTLCCommand, clearHTLCCommand, historyCommand, offCommand, exitCommand}
410425
printHelp(listofCommands)
411426
fmt.Fprintf(color.Output, "\n\n")
412427
fmt.Fprintf(color.Output, lnutil.Header("Coins:\n"))

consts/consts.go

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ const (
1111
MinOutput = 100000 // minOutput is the minimum output amt, post fee. This (plus fees) is also the minimum channel balance
1212
MinSendAmt = 10000 // minimum amount that can be sent through a chan
1313
MaxTxLen = 100000 // maximum number of tx's that can be ingested at once
14+
DefaultLockTime = 500
1415
)

litrpc/chancmds.go

+133
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,136 @@ func (r *LitRPC) DumpPrivs(args NoArgs, reply *DumpReply) error {
450450

451451
return nil
452452
}
453+
454+
// ------------------------- HTLCs
455+
type AddHTLCArgs struct {
456+
ChanIdx uint32
457+
Amt int64
458+
LockTime uint32
459+
RHash [32]byte
460+
Data [32]byte
461+
}
462+
type AddHTLCReply struct {
463+
StateIndex uint64
464+
HTLCIndex uint32
465+
}
466+
467+
func (r *LitRPC) AddHTLC(args AddHTLCArgs, reply *AddHTLCReply) error {
468+
if args.Amt > consts.MaxChanCapacity || args.Amt < consts.MinOutput {
469+
return fmt.Errorf(
470+
"can't add HTLC %d max is 1 coin (100000000), min is %d", args.Amt, consts.MinOutput)
471+
}
472+
473+
log.Printf("add HTLC %d to chan %d with data %x and RHash %x\n", args.Amt, args.ChanIdx, args.Data, args.RHash)
474+
475+
// load the whole channel from disk just to see who the peer is
476+
// (pretty inefficient)
477+
dummyqc, err := r.Node.GetQchanByIdx(args.ChanIdx)
478+
if err != nil {
479+
return err
480+
}
481+
// see if channel is closed and error early
482+
if dummyqc.CloseData.Closed {
483+
return fmt.Errorf("Can't push; channel %d closed", args.ChanIdx)
484+
}
485+
486+
// but we want to reference the qc that's already in ram
487+
// first see if we're connected to that peer
488+
489+
// map read, need mutex...?
490+
r.Node.RemoteMtx.Lock()
491+
peer, ok := r.Node.RemoteCons[dummyqc.Peer()]
492+
r.Node.RemoteMtx.Unlock()
493+
if !ok {
494+
return fmt.Errorf("not connected to peer %d for channel %d",
495+
dummyqc.Peer(), dummyqc.Idx())
496+
}
497+
qc, ok := peer.QCs[dummyqc.Idx()]
498+
if !ok {
499+
return fmt.Errorf("peer %d doesn't have channel %d",
500+
dummyqc.Peer(), dummyqc.Idx())
501+
}
502+
503+
log.Printf("channel %s\n", qc.Op.String())
504+
505+
if qc.CloseData.Closed {
506+
return fmt.Errorf("Channel %d already closed by tx %s",
507+
args.ChanIdx, qc.CloseData.CloseTxid.String())
508+
}
509+
510+
// TODO this is a bad place to put it -- litRPC should be a thin layer
511+
// to the Node.Func() calls. For now though, set the height here...
512+
qc.Height = dummyqc.Height
513+
curHeight := uint32(r.Node.SubWallet[qc.Coin()].CurrentHeight())
514+
curHeight += args.LockTime
515+
516+
err = r.Node.OfferHTLC(qc, uint32(args.Amt), args.RHash, curHeight, args.Data)
517+
if err != nil {
518+
return err
519+
}
520+
521+
reply.StateIndex = qc.State.StateIdx
522+
reply.HTLCIndex = qc.State.HTLCIdx - 1
523+
return nil
524+
}
525+
526+
type ClearHTLCArgs struct {
527+
ChanIdx uint32
528+
HTLCIdx uint32
529+
R [16]byte
530+
Data [32]byte
531+
}
532+
type ClearHTLCReply struct {
533+
StateIndex uint64
534+
}
535+
536+
func (r *LitRPC) ClearHTLC(args ClearHTLCArgs, reply *ClearHTLCReply) error {
537+
log.Printf("clear HTLC %d from chan %d with data %x and preimage %x\n", args.HTLCIdx, args.ChanIdx, args.Data, args.R)
538+
539+
// load the whole channel from disk just to see who the peer is
540+
// (pretty inefficient)
541+
dummyqc, err := r.Node.GetQchanByIdx(args.ChanIdx)
542+
if err != nil {
543+
return err
544+
}
545+
// see if channel is closed and error early
546+
if dummyqc.CloseData.Closed {
547+
return fmt.Errorf("Can't clear; channel %d closed", args.ChanIdx)
548+
}
549+
550+
// but we want to reference the qc that's already in ram
551+
// first see if we're connected to that peer
552+
553+
// map read, need mutex...?
554+
r.Node.RemoteMtx.Lock()
555+
peer, ok := r.Node.RemoteCons[dummyqc.Peer()]
556+
r.Node.RemoteMtx.Unlock()
557+
if !ok {
558+
return fmt.Errorf("not connected to peer %d for channel %d",
559+
dummyqc.Peer(), dummyqc.Idx())
560+
}
561+
qc, ok := peer.QCs[dummyqc.Idx()]
562+
if !ok {
563+
return fmt.Errorf("peer %d doesn't have channel %d",
564+
dummyqc.Peer(), dummyqc.Idx())
565+
}
566+
567+
log.Printf("channel %s\n", qc.Op.String())
568+
569+
if qc.CloseData.Closed {
570+
return fmt.Errorf("Channel %d already closed by tx %s",
571+
args.ChanIdx, qc.CloseData.CloseTxid.String())
572+
}
573+
574+
// TODO this is a bad place to put it -- litRPC should be a thin layer
575+
// to the Node.Func() calls. For now though, set the height here...
576+
qc.Height = dummyqc.Height
577+
578+
err = r.Node.ClearHTLC(qc, args.R, args.HTLCIdx, args.Data)
579+
if err != nil {
580+
return err
581+
}
582+
583+
reply.StateIndex = qc.State.StateIdx
584+
return nil
585+
}

litrpc/walletcmds.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package litrpc
33
import (
44
"fmt"
55
"log"
6+
67
"github.com/mit-dci/lit/bech32"
7-
"github.com/mit-dci/lit/wire"
88
"github.com/mit-dci/lit/consts"
99
"github.com/mit-dci/lit/lnutil"
1010
"github.com/mit-dci/lit/portxo"
11+
"github.com/mit-dci/lit/wire"
1112
)
1213

1314
type TxidsReply struct {
@@ -413,3 +414,21 @@ func (r *LitRPC) Address(args *AddressArgs, reply *AddressReply) error {
413414
// }
414415
// return base58.CheckEncode(pkHash, netID), nil
415416
//}
417+
418+
type ClaimHTLCArgs struct {
419+
R [16]byte
420+
}
421+
422+
func (r *LitRPC) ClaimHTLC(args *ClaimHTLCArgs, reply *TxidsReply) error {
423+
txids, err := r.Node.ClaimHTLC(args.R)
424+
if err != nil {
425+
return err
426+
}
427+
428+
reply.Txids = make([]string, 0)
429+
for _, txid := range txids {
430+
reply.Txids = append(reply.Txids, fmt.Sprintf("%x", txid))
431+
}
432+
433+
return nil
434+
}

0 commit comments

Comments
 (0)