lnwallet: add logic for adding/removing HTLCs
* Updates to the channel are made atomic, and consistent via a proxy object: “ChannelUpdate” which encapsulates an update transaction. Only one update transaction may be outstanding at any time. * Update transactions are initiated via AddHTLC or SettleHTLC. * Once a transaction has been begun, in order to complete the update the transaction must first be presented with a signature from the counter-party for our new version of the commitment tx (VerifyNewCommitmentSigs), and finally to atomically commit the transaction, the counterparty’s pre-image to their previous revocation hash must be validate (Commit).
This commit is contained in:
parent
895d70e79f
commit
27744a3582
@ -1,6 +1,8 @@
|
||||
package lnwallet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"li.lan/labs/plasma/chainntfs"
|
||||
@ -18,6 +20,9 @@ const (
|
||||
MaxPendingPayments = 10
|
||||
)
|
||||
|
||||
// PaymentHash presents the hash160 of a random value. This hash is used to
|
||||
// uniquely track incoming/outgoing payments within this channel, as well as
|
||||
// payments requested by the wallet/daemon.
|
||||
type PaymentHash [20]byte
|
||||
|
||||
// LightningChannel...
|
||||
@ -36,14 +41,13 @@ type LightningChannel struct {
|
||||
stateMtx sync.RWMutex
|
||||
channelState channeldb.OpenChannel
|
||||
|
||||
// Payment's which we've requested.
|
||||
unfufilledPayments map[PaymentHash]*PaymentRequest
|
||||
updateTotem chan struct{}
|
||||
|
||||
// Uncleared HTLC's.
|
||||
pendingPayments map[PaymentHash]*PaymentDescriptor
|
||||
|
||||
ourPendingCommitTx *wire.MsgTx
|
||||
theirPendingCommitTx *wire.MsgTx
|
||||
// Payment's which we've requested.
|
||||
unfufilledPayments map[PaymentHash]*PaymentRequest
|
||||
|
||||
fundingTxIn *wire.TxIn
|
||||
fundingP2SH []byte
|
||||
@ -61,10 +65,13 @@ func newLightningChannel(wallet *LightningWallet, events *chainntnfs.ChainNotifi
|
||||
chanDB *channeldb.DB, state channeldb.OpenChannel) (*LightningChannel, error) {
|
||||
|
||||
lc := &LightningChannel{
|
||||
lnwallet: wallet,
|
||||
channelEvents: events,
|
||||
channelDB: chanDB,
|
||||
channelState: state,
|
||||
lnwallet: wallet,
|
||||
channelEvents: events,
|
||||
channelState: state,
|
||||
channelDB: chanDB,
|
||||
updateTotem: make(chan struct{}, 1),
|
||||
pendingPayments: make(map[PaymentHash]*PaymentDescriptor),
|
||||
unfufilledPayments: make(map[PaymentHash]*PaymentRequest),
|
||||
}
|
||||
|
||||
fundingTxId := state.FundingTx.TxSha()
|
||||
@ -81,15 +88,173 @@ func newLightningChannel(wallet *LightningWallet, events *chainntnfs.ChainNotifi
|
||||
|
||||
// PaymentDescriptor...
|
||||
type PaymentDescriptor struct {
|
||||
RHash [20]byte
|
||||
RHash [20]byte
|
||||
Timeout uint32
|
||||
Value btcutil.Amount
|
||||
|
||||
OurRevocation [20]byte // TODO(roasbeef): don't need these?
|
||||
TheirRevocation [20]byte
|
||||
|
||||
Timeout uint32
|
||||
Value btcutil.Amount
|
||||
PayToUs bool
|
||||
}
|
||||
|
||||
lnchannel *LightningChannel
|
||||
// ChannelUpdate...
|
||||
type ChannelUpdate struct {
|
||||
pendingDesc *PaymentDescriptor
|
||||
deletion bool
|
||||
|
||||
currentUpdateNum uint64
|
||||
pendingUpdateNum uint64
|
||||
|
||||
ourPendingCommitTx *wire.MsgTx
|
||||
theirPendingCommitTx *wire.MsgTx
|
||||
|
||||
pendingRevocation [20]byte
|
||||
sigTheirNewCommit []byte
|
||||
|
||||
// TODO(roasbeef): some enum to track current state in lifetime?
|
||||
// state UpdateStag
|
||||
|
||||
lnChannel *LightningChannel
|
||||
}
|
||||
|
||||
// RevocationHash...
|
||||
func (c *ChannelUpdate) RevocationHash() ([]byte, error) {
|
||||
c.lnChannel.stateMtx.RLock()
|
||||
defer c.lnChannel.stateMtx.RUnlock()
|
||||
|
||||
shachain := c.lnChannel.channelState.OurShaChain
|
||||
nextPreimage, err := shachain.GetHash(c.pendingUpdateNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return btcutil.Hash160(nextPreimage[:]), nil
|
||||
}
|
||||
|
||||
// SignCounterPartyCommitment...
|
||||
func (c *ChannelUpdate) SignCounterPartyCommitment() ([]byte, error) {
|
||||
c.lnChannel.stateMtx.RLock()
|
||||
defer c.lnChannel.stateMtx.RUnlock()
|
||||
|
||||
if c.sigTheirNewCommit != nil {
|
||||
return c.sigTheirNewCommit, nil
|
||||
}
|
||||
|
||||
// Sign their version of the commitment transaction.
|
||||
sig, err := txscript.RawTxInSignature(c.theirPendingCommitTx, 0,
|
||||
c.lnChannel.channelState.FundingRedeemScript, txscript.SigHashAll,
|
||||
c.lnChannel.channelState.MultiSigKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.sigTheirNewCommit = sig
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// PreviousRevocationPreImage...
|
||||
func (c *ChannelUpdate) PreviousRevocationPreImage() ([]byte, error) {
|
||||
c.lnChannel.stateMtx.RLock()
|
||||
defer c.lnChannel.stateMtx.RUnlock()
|
||||
|
||||
// Retrieve the pre-image to the revocation hash our current commitment
|
||||
// transaction.
|
||||
shachain := c.lnChannel.channelState.OurShaChain
|
||||
revokePreImage, err := shachain.GetHash(c.currentUpdateNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return revokePreImage[:], nil
|
||||
}
|
||||
|
||||
// VerifyNewCommitmentSigs...
|
||||
func (c *ChannelUpdate) VerifyNewCommitmentSigs(ourSig, theirSig []byte) error {
|
||||
c.lnChannel.stateMtx.RLock()
|
||||
defer c.lnChannel.stateMtx.RUnlock()
|
||||
|
||||
var err error
|
||||
var scriptSig []byte
|
||||
channelState := c.lnChannel.channelState
|
||||
|
||||
// When initially generating the redeemScript, we sorted the serialized
|
||||
// public keys in descending order. So we do a quick comparison in order
|
||||
// ensure the signatures appear on the Script Virual Machine stack in
|
||||
// the correct order.
|
||||
// TODO(roasbeef): func
|
||||
redeemScript := channelState.FundingRedeemScript
|
||||
ourKey := channelState.OurCommitKey.PubKey().SerializeCompressed()
|
||||
theirKey := channelState.TheirCommitKey.SerializeCompressed()
|
||||
if bytes.Compare(ourKey, theirKey) == -1 {
|
||||
scriptSig, err = spendMultiSig(redeemScript, theirSig, ourSig)
|
||||
} else {
|
||||
scriptSig, err = spendMultiSig(redeemScript, ourSig, theirSig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attach the scriptSig to our commitment transaction's only input,
|
||||
// then validate that the scriptSig executes correctly.
|
||||
commitTx := c.ourPendingCommitTx
|
||||
commitTx.TxIn[0].SignatureScript = scriptSig
|
||||
vm, err := txscript.NewEngine(c.lnChannel.fundingP2SH, commitTx, 0,
|
||||
txscript.StandardVerifyFlags, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return vm.Execute()
|
||||
}
|
||||
|
||||
// Commit...
|
||||
func (c *ChannelUpdate) Commit(pastRevokePreimage []byte) error {
|
||||
c.lnChannel.stateMtx.Lock()
|
||||
defer c.lnChannel.stateMtx.Unlock()
|
||||
|
||||
// First, ensure that the pre-image properly links into the shachain.
|
||||
theirShaChain := c.lnChannel.channelState.TheirShaChain
|
||||
var preImage [32]byte
|
||||
copy(preImage[:], pastRevokePreimage)
|
||||
if err := theirShaChain.AddNextHash(preImage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelState := c.lnChannel.channelState
|
||||
|
||||
// Finally, verify that that this is indeed the pre-image to the
|
||||
// revocation hash we were given earlier.
|
||||
if !bytes.Equal(btcutil.Hash160(pastRevokePreimage),
|
||||
channelState.TheirCurrentRevocation[:]) {
|
||||
return fmt.Errorf("pre-image hash does not match revocation")
|
||||
}
|
||||
|
||||
// Store this current revocation in the channel state so we can
|
||||
// verify future channel updates.
|
||||
channelState.TheirCurrentRevocation = c.pendingRevocation
|
||||
|
||||
// The channel update is now complete, roll over to the newest commitment
|
||||
// transaction.
|
||||
channelState.OurCommitTx = c.ourPendingCommitTx
|
||||
channelState.TheirCommitTx = c.theirPendingCommitTx
|
||||
channelState.NumUpdates = c.pendingUpdateNum
|
||||
|
||||
// If this channel update involved deleting an HTLC, remove it from the
|
||||
// set of pending payments.
|
||||
if c.deletion {
|
||||
delete(c.lnChannel.pendingPayments, c.pendingDesc.RHash)
|
||||
}
|
||||
|
||||
// TODO(roasbeef): db writes, checkpoints, and such
|
||||
|
||||
// Return the updateTotem, allowing another update to be created now
|
||||
// that this pending update has been commited, and finalized.
|
||||
c.lnChannel.updateTotem <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddHTLC...
|
||||
@ -99,41 +264,44 @@ type PaymentDescriptor struct {
|
||||
// * value
|
||||
// * r_hash
|
||||
// * next revocation hash
|
||||
// Can build our new commitment tx at this point
|
||||
// 3. they accept
|
||||
// * their next revocation hash
|
||||
// * their sig for our new commitment tx (verify correctness)
|
||||
// Can buld both new commitment txns at this point
|
||||
// 4. we give sigs
|
||||
// * our sigs for their new commitment tx
|
||||
// * the pre-image to our old commitment tx
|
||||
// 5. they complete
|
||||
// * the pre-image to their old commitment tx (verify is part of their chain, is pre-image)
|
||||
func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount,
|
||||
rHash, revocation PaymentHash, payToUs bool) (*PaymentDescriptor, []byte, error) {
|
||||
rHash, revocation PaymentHash, payToUs bool) (*ChannelUpdate, error) {
|
||||
|
||||
lc.stateMtx.Lock()
|
||||
defer lc.stateMtx.Unlock()
|
||||
// Grab the updateTotem, this acts as a barrier upholding the invariant
|
||||
// that only one channel update transaction should exist at any moment.
|
||||
// This aides in ensuring the channel updates are atomic, and consistent.
|
||||
<-lc.updateTotem
|
||||
|
||||
paymentDetails := &PaymentDescriptor{
|
||||
RHash: rHash,
|
||||
TheirRevocation: revocation,
|
||||
Timeout: timeout,
|
||||
Value: value,
|
||||
PayToUs: payToUs,
|
||||
lnchannel: lc,
|
||||
chanUpdate := &ChannelUpdate{
|
||||
pendingDesc: &PaymentDescriptor{
|
||||
RHash: rHash,
|
||||
TheirRevocation: revocation,
|
||||
Timeout: timeout,
|
||||
Value: value,
|
||||
PayToUs: payToUs,
|
||||
},
|
||||
pendingRevocation: revocation,
|
||||
lnChannel: lc,
|
||||
}
|
||||
|
||||
lc.channelState.TheirCurrentRevocation = revocation
|
||||
|
||||
// Get next revocation hash, updating the number of updates in the
|
||||
// channel as a result.
|
||||
updateNum := lc.channelState.NumUpdates + 1
|
||||
nextPreimage, err := lc.channelState.OurShaChain.GetHash(updateNum)
|
||||
chanUpdate.currentUpdateNum = lc.channelState.NumUpdates
|
||||
chanUpdate.pendingUpdateNum = lc.channelState.NumUpdates + 1
|
||||
nextPreimage, err := lc.channelState.OurShaChain.GetHash(chanUpdate.pendingUpdateNum)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
copy(paymentDetails.OurRevocation[:], btcutil.Hash160(nextPreimage[:]))
|
||||
lc.channelState.NumUpdates = updateNum
|
||||
copy(chanUpdate.pendingDesc.OurRevocation[:], btcutil.Hash160(nextPreimage[:]))
|
||||
|
||||
// Re-calculate the amount of cleared funds for each side.
|
||||
var amountToUs, amountToThem btcutil.Amount
|
||||
@ -146,33 +314,25 @@ func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount,
|
||||
}
|
||||
|
||||
// Re-create copies of the current commitment transactions to be updated.
|
||||
ourNewCommitTx, err := createCommitTx(lc.fundingTxIn,
|
||||
lc.channelState.OurCommitKey.PubKey(), lc.channelState.TheirCommitKey,
|
||||
paymentDetails.OurRevocation[:], lc.channelState.CsvDelay,
|
||||
amountToUs, amountToThem)
|
||||
ourNewCommitTx, theirNewCommitTx, err := createNewCommitmentTxns(
|
||||
lc.fundingTxIn, &lc.channelState, chanUpdate, amountToUs, amountToThem,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
theirNewCommitTx, err := createCommitTx(lc.fundingTxIn,
|
||||
lc.channelState.TheirCommitKey, lc.channelState.OurCommitKey.PubKey(),
|
||||
paymentDetails.TheirRevocation[:], lc.channelState.CsvDelay,
|
||||
amountToThem, amountToUs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, re-add all the old HTLCs.
|
||||
for _, paymentDesc := range lc.pendingPayments {
|
||||
if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDesc); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Then add this new HTLC.
|
||||
if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDetails); err != nil {
|
||||
return nil, nil, err
|
||||
if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, chanUpdate.pendingDesc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lc.pendingPayments[rHash] = paymentDetails // TODO(roasbeef): check for dups?
|
||||
lc.pendingPayments[rHash] = chanUpdate.pendingDesc // TODO(roasbeef): check for dups?
|
||||
|
||||
// Sort both transactions according to the agreed upon cannonical
|
||||
// ordering. This lets us skip sending the entire transaction over,
|
||||
@ -182,16 +342,12 @@ func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount,
|
||||
|
||||
// TODO(roasbeef): locktimes/sequence set
|
||||
|
||||
// Sign their version of the commitment transaction.
|
||||
sigTheirCommit, err := txscript.RawTxInSignature(theirNewCommitTx, 0,
|
||||
lc.channelState.FundingRedeemScript, txscript.SigHashAll,
|
||||
lc.channelState.MultiSigKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO(roasbeef): write checkpoint here...
|
||||
return paymentDetails, sigTheirCommit, nil
|
||||
|
||||
chanUpdate.ourPendingCommitTx = ourNewCommitTx
|
||||
chanUpdate.theirPendingCommitTx = theirNewCommitTx
|
||||
|
||||
return chanUpdate, nil
|
||||
}
|
||||
|
||||
// addHTLC...
|
||||
@ -258,8 +414,106 @@ func (lc *LightningChannel) addHTLC(ourCommitTx, theirCommitTx *wire.MsgTx,
|
||||
// SettleHTLC...
|
||||
// R-VALUE, NEW REVOKE HASH
|
||||
// accept, sig
|
||||
func (lc *LightningChannel) SettleHTLC(rValue []byte) error {
|
||||
return nil
|
||||
func (lc *LightningChannel) SettleHTLC(rValue [20]byte, newRevocation [20]byte) (*ChannelUpdate, error) {
|
||||
// Grab the updateTotem, this acts as a barrier upholding the invariant
|
||||
// that only one channel update transaction should exist at any moment.
|
||||
// This aides in ensuring the channel updates are atomic, and consistent.
|
||||
<-lc.updateTotem
|
||||
|
||||
// Find the matching payment descriptor, bailing out early if it
|
||||
// doesn't exist.
|
||||
var rHash PaymentHash
|
||||
copy(rHash[:], btcutil.Hash160(rValue[:]))
|
||||
payDesc, ok := lc.pendingPayments[rHash]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("r-hash for preimage not found")
|
||||
}
|
||||
|
||||
chanUpdate := &ChannelUpdate{
|
||||
pendingDesc: payDesc,
|
||||
deletion: true,
|
||||
pendingRevocation: newRevocation,
|
||||
lnChannel: lc,
|
||||
}
|
||||
|
||||
// TODO(roasbeef): such copy pasta, make into func
|
||||
// Get next revocation hash, updating the number of updates in the
|
||||
// channel as a result.
|
||||
chanUpdate.currentUpdateNum = lc.channelState.NumUpdates
|
||||
chanUpdate.pendingUpdateNum = lc.channelState.NumUpdates + 1
|
||||
nextPreimage, err := lc.channelState.OurShaChain.GetHash(chanUpdate.pendingUpdateNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(chanUpdate.pendingDesc.OurRevocation[:], btcutil.Hash160(nextPreimage[:]))
|
||||
|
||||
// Re-calculate the amount of cleared funds for each side.
|
||||
var amountToUs, amountToThem btcutil.Amount
|
||||
if payDesc.PayToUs {
|
||||
amountToUs = lc.channelState.OurBalance + payDesc.Value
|
||||
amountToThem = lc.channelState.TheirBalance
|
||||
} else {
|
||||
amountToUs = lc.channelState.OurBalance
|
||||
amountToThem = lc.channelState.TheirBalance + payDesc.Value
|
||||
}
|
||||
|
||||
// Create new commitment transactions that reflect the settlement of
|
||||
// this pending HTLC.
|
||||
ourNewCommitTx, theirNewCommitTx, err := createNewCommitmentTxns(
|
||||
lc.fundingTxIn, &lc.channelState, chanUpdate, amountToUs, amountToThem,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Re-add all the HTLC's skipping over this newly settled payment.
|
||||
for paymentHash, paymentDesc := range lc.pendingPayments {
|
||||
if bytes.Equal(paymentHash[:], rHash[:]) {
|
||||
continue
|
||||
}
|
||||
if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDesc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sort both transactions according to the agreed upon cannonical
|
||||
// ordering. This lets us skip sending the entire transaction over,
|
||||
// instead we'll just send signatures.
|
||||
txsort.InPlaceSort(ourNewCommitTx)
|
||||
txsort.InPlaceSort(theirNewCommitTx)
|
||||
|
||||
// TODO(roasbeef): locktimes/sequence set
|
||||
|
||||
// TODO(roasbeef): write checkpoint here...
|
||||
|
||||
chanUpdate.ourPendingCommitTx = ourNewCommitTx
|
||||
chanUpdate.theirPendingCommitTx = theirNewCommitTx
|
||||
|
||||
return chanUpdate, nil
|
||||
}
|
||||
|
||||
// createNewCommitmentTxns....
|
||||
// NOTE: This MUST be called with stateMtx held.
|
||||
func createNewCommitmentTxns(fundingTxIn *wire.TxIn, state *channeldb.OpenChannel,
|
||||
chanUpdate *ChannelUpdate, amountToUs, amountToThem btcutil.Amount) (*wire.MsgTx, *wire.MsgTx, error) {
|
||||
|
||||
ourNewCommitTx, err := createCommitTx(fundingTxIn,
|
||||
state.OurCommitKey.PubKey(), state.TheirCommitKey,
|
||||
chanUpdate.pendingDesc.OurRevocation[:], state.CsvDelay,
|
||||
amountToUs, amountToThem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
theirNewCommitTx, err := createCommitTx(fundingTxIn,
|
||||
state.TheirCommitKey, state.OurCommitKey.PubKey(),
|
||||
chanUpdate.pendingDesc.TheirRevocation[:], state.CsvDelay,
|
||||
amountToThem, amountToUs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ourNewCommitTx, theirNewCommitTx, nil
|
||||
}
|
||||
|
||||
// CancelHTLC...
|
||||
|
Loading…
Reference in New Issue
Block a user