lnwallet: integrate obfuscated state hints into funding workflow

This commit finalizes the implementation of #58 by integrating passing
around the obfuscate state hints into the funding workflow of the
wallet, and also the daemon’s funding manager.

In order to amend the tests, the functions to set and receive the state
hints are now publicly exported.
This commit is contained in:
Olaoluwa Osuntokun 2016-11-16 12:54:27 -08:00
parent 3010412bbc
commit 22074eb737
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
6 changed files with 147 additions and 34 deletions

@ -103,13 +103,14 @@ type fundingErrorMsg struct {
type pendingChannels map[uint64]*reservationWithCtx type pendingChannels map[uint64]*reservationWithCtx
// fundingManager acts as an orchestrator/bridge between the wallet's // fundingManager acts as an orchestrator/bridge between the wallet's
// 'ChannelReservation' workflow, and the wire protocl's funding initiation // 'ChannelReservation' workflow, and the wire protocol's funding initiation
// messages. Any requests to initaite the funding workflow for a channel, either // messages. Any requests to initiate the funding workflow for a channel,
// kicked-off locally, or remotely is handled by the funding manager. Once a // either kicked-off locally, or remotely is handled by the funding manager.
// channels's funding workflow has been completed, any local callers, the local // Once a channel's funding workflow has been completed, any local callers, the
// peer, and possibly the remote peer are notified of the completion of the // local peer, and possibly the remote peer are notified of the completion of
// channel workflow. Additionally, any temporary or permanent access controls // the channel workflow. Additionally, any temporary or permanent access
// between the wallet and remote peers are enforced via the funding manager. // controls between the wallet and remote peers are enforced via the funding
// manager.
type fundingManager struct { type fundingManager struct {
// MUST be used atomically. // MUST be used atomically.
started int32 started int32
@ -464,8 +465,10 @@ func (f *fundingManager) handleFundingResponse(fmsg *fundingResponseMsg) {
outPoint, msg.ChannelID) outPoint, msg.ChannelID)
revocationKey := resCtx.reservation.OurContribution().RevocationKey revocationKey := resCtx.reservation.OurContribution().RevocationKey
obsfucator := resCtx.reservation.StateNumObfuscator()
fundingComplete := lnwire.NewSingleFundingComplete(msg.ChannelID, fundingComplete := lnwire.NewSingleFundingComplete(msg.ChannelID,
outPoint, commitSig, revocationKey) outPoint, commitSig, revocationKey, obsfucator)
sourcePeer.queueMsg(fundingComplete, nil) sourcePeer.queueMsg(fundingComplete, nil)
} }
@ -491,16 +494,20 @@ func (f *fundingManager) handleFundingComplete(fmsg *fundingCompleteMsg) {
// TODO(roasbeef): make case (p vs P) consistent throughout // TODO(roasbeef): make case (p vs P) consistent throughout
fundingOut := fmsg.msg.FundingOutPoint fundingOut := fmsg.msg.FundingOutPoint
chanID := fmsg.msg.ChannelID chanID := fmsg.msg.ChannelID
commitSig := fmsg.msg.CommitSignature.Serialize()
fndgLog.Infof("completing pendingID(%v) with ChannelPoint(%v)", fndgLog.Infof("completing pendingID(%v) with ChannelPoint(%v)",
fmsg.msg.ChannelID, fundingOut, chanID, fundingOut,
) )
// Append a sighash type of SigHashAll to the signature as it's the
// sighash type used implicitly within this type of channel for
// commitment transactions.
revokeKey := fmsg.msg.RevocationKey revokeKey := fmsg.msg.RevocationKey
if err := resCtx.reservation.CompleteReservationSingle(revokeKey, fundingOut, commitSig); err != nil { obsfucator := fmsg.msg.StateHintObsfucator
commitSig := fmsg.msg.CommitSignature.Serialize()
// With all the necessary data available, attempt to advance the
// funding workflow to the next stage. If this succeeds then the
// funding transaction will broadcast after our next message.
err := resCtx.reservation.CompleteReservationSingle(revokeKey,
fundingOut, commitSig, obsfucator)
if err != nil {
// TODO(roasbeef): better error logging: peerID, channelID, etc. // TODO(roasbeef): better error logging: peerID, channelID, etc.
fndgLog.Errorf("unable to complete single reservation: %v", err) fndgLog.Errorf("unable to complete single reservation: %v", err)
fmsg.peer.Disconnect() fmsg.peer.Disconnect()

@ -108,6 +108,8 @@ type bobNode struct {
delay uint32 delay uint32
id *btcec.PublicKey id *btcec.PublicKey
obsfucator [lnwallet.StateHintSize]byte
availableOutputs []*wire.TxIn availableOutputs []*wire.TxIn
changeOutputs []*wire.TxOut changeOutputs []*wire.TxOut
fundingAmt btcutil.Amount fundingAmt btcutil.Amount
@ -241,6 +243,9 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) {
copy(revocation[:], bobsPrivKey) copy(revocation[:], bobsPrivKey)
revocation[0] = 0xff revocation[0] = 0xff
var obsfucator [lnwallet.StateHintSize]byte
copy(obsfucator[:], revocation[:])
// His ID is just as creative... // His ID is just as creative...
var id [wire.HashSize]byte var id [wire.HashSize]byte
id[0] = 0xff id[0] = 0xff
@ -364,9 +369,9 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, wallet *lnwallet
} }
// The channel reservation should now be populated with a multi-sig key // The channel reservation should now be populated with a multi-sig key
// from our HD chain, a change output with 3 BTC, and 2 outputs selected // from our HD chain, a change output with 3 BTC, and 2 outputs
// of 4 BTC each. Additionally, the rest of the items needed to fufill a // selected of 4 BTC each. Additionally, the rest of the items needed
// funding contribution should also have been filled in. // to fulfill a funding contribution should also have been filled in.
ourContribution := chanReservation.OurContribution() ourContribution := chanReservation.OurContribution()
if len(ourContribution.Inputs) != 2 { if len(ourContribution.Inputs) != 2 {
t.Fatalf("outputs for funding tx not properly selected, have %v "+ t.Fatalf("outputs for funding tx not properly selected, have %v "+
@ -582,7 +587,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness,
// outpoints, will let us fail early/fast instead of querying and // outpoints, will let us fail early/fast instead of querying and
// attempting coin selection. // attempting coin selection.
// Request to fund a new channel should now succeeed. // Request to fund a new channel should now succeed.
_, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount,
testPub, bobAddr, numReqConfs, 4) testPub, bobAddr, numReqConfs, 4)
if err != nil { if err != nil {
@ -733,6 +738,13 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness,
hex.EncodeToString(channels[0].FundingOutpoint.Hash[:]), hex.EncodeToString(channels[0].FundingOutpoint.Hash[:]),
hex.EncodeToString(fundingSha[:])) hex.EncodeToString(fundingSha[:]))
} }
if !channels[0].IsInitiator {
t.Fatalf("alice not detected as channel initiator")
}
if channels[0].ChanType != channeldb.SingleFunder {
t.Fatalf("channel type is incorrect, expected %v instead got %v",
channeldb.SingleFunder, channels[0].ChanType)
}
assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan()) assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan())
} }
@ -848,15 +860,23 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness,
fundingTxID := fundingTx.TxSha() fundingTxID := fundingTx.TxSha()
_, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript) _, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript)
fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex)
bobObsfucator := bobNode.obsfucator
// Next, manually create Alice's commitment transaction, signing the
// fully sorted and state hinted transaction.
fundingTxIn := wire.NewTxIn(fundingOutpoint, nil, nil) fundingTxIn := wire.NewTxIn(fundingOutpoint, nil, nil)
aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, ourContribution.CommitKey, aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn,
bobContribution.CommitKey, ourContribution.RevocationKey, ourContribution.CommitKey, bobContribution.CommitKey,
ourContribution.CsvDelay, 0, capacity) ourContribution.RevocationKey, ourContribution.CsvDelay, 0,
capacity)
if err != nil { if err != nil {
t.Fatalf("unable to create alice's commit tx: %v", err) t.Fatalf("unable to create alice's commit tx: %v", err)
} }
txsort.InPlaceSort(aliceCommitTx) txsort.InPlaceSort(aliceCommitTx)
err = lnwallet.SetStateNumHint(aliceCommitTx, 0, bobObsfucator)
if err != nil {
t.Fatalf("unable to set state hint: %v", err)
}
bobCommitSig, err := bobNode.signCommitTx(aliceCommitTx, bobCommitSig, err := bobNode.signCommitTx(aliceCommitTx,
// TODO(roasbeef): account for hard-coded fee, remove bob node // TODO(roasbeef): account for hard-coded fee, remove bob node
fundingRedeemScript, int64(capacity)+5000) fundingRedeemScript, int64(capacity)+5000)
@ -866,8 +886,9 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness,
// With this stage complete, Alice can now complete the reservation. // With this stage complete, Alice can now complete the reservation.
bobRevokeKey := bobContribution.RevocationKey bobRevokeKey := bobContribution.RevocationKey
if err := chanReservation.CompleteReservationSingle(bobRevokeKey, err = chanReservation.CompleteReservationSingle(bobRevokeKey,
fundingOutpoint, bobCommitSig); err != nil { fundingOutpoint, bobCommitSig, bobObsfucator)
if err != nil {
t.Fatalf("unable to complete reservation: %v", err) t.Fatalf("unable to complete reservation: %v", err)
} }
@ -1146,10 +1167,10 @@ func TestLightningWallet(t *testing.T) {
// up this node with a chain length of 125, so we have plentyyy of BTC // up this node with a chain length of 125, so we have plentyyy of BTC
// to play around with. // to play around with.
miningNode, err := rpctest.New(netParams, nil, nil) miningNode, err := rpctest.New(netParams, nil, nil)
defer miningNode.TearDown()
if err != nil { if err != nil {
t.Fatalf("unable to create mining node: %v", err) t.Fatalf("unable to create mining node: %v", err)
} }
defer miningNode.TearDown()
if err := miningNode.SetUp(true, 25); err != nil { if err := miningNode.SetUp(true, 25); err != nil {
t.Fatalf("unable to set up mining node: %v", err) t.Fatalf("unable to set up mining node: %v", err)
} }

@ -324,8 +324,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*InputScr
// the .OurSignatures() method. As this method should only be called as a // the .OurSignatures() method. As this method should only be called as a
// response to a single funder channel, only a commitment signature will be // response to a single funder channel, only a commitment signature will be
// populated. // populated.
func (r *ChannelReservation) CompleteReservationSingle(revocationKey *btcec.PublicKey, func (r *ChannelReservation) CompleteReservationSingle(
fundingPoint *wire.OutPoint, commitSig []byte) error { revocationKey *btcec.PublicKey, fundingPoint *wire.OutPoint,
commitSig []byte, obsfucator [StateHintSize]byte) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
r.wallet.msgChan <- &addSingleFunderSigsMsg{ r.wallet.msgChan <- &addSingleFunderSigsMsg{
@ -333,6 +335,7 @@ func (r *ChannelReservation) CompleteReservationSingle(revocationKey *btcec.Publ
revokeKey: revocationKey, revokeKey: revocationKey,
fundingOutpoint: fundingPoint, fundingOutpoint: fundingPoint,
theirCommitmentSig: commitSig, theirCommitmentSig: commitSig,
obsfucator: obsfucator,
err: errChan, err: errChan,
} }
@ -395,6 +398,19 @@ func (r *ChannelReservation) FundingOutpoint() *wire.OutPoint {
return r.partialState.FundingOutpoint return r.partialState.FundingOutpoint
} }
// StateNumObfuscator returns the bytes to be used to obsfucate the state
// number hints for all future states of the commitment transaction for this
// workflow.
//
// NOTE: This value will only be available for a single funder workflow after
// the CompleteReservation or CompleteReservationSingle methods have been
// successfully executed.
func (r *ChannelReservation) StateNumObfuscator() [StateHintSize]byte {
r.RLock()
defer r.RUnlock()
return r.partialState.StateHintObsfucator
}
// Cancel abandons this channel reservation. This method should be called in // Cancel abandons this channel reservation. This method should be called in
// the scenario that communications with the counterparty break down. Upon // the scenario that communications with the counterparty break down. Upon
// cancellation, all resources previously reserved for this pending payment // cancellation, all resources previously reserved for this pending payment

@ -772,14 +772,15 @@ func deriveElkremRoot(elkremDerivationRoot *btcec.PrivateKey,
return elkremRoot return elkremRoot
} }
// setStateNumHint encodes the current state number within the passed // SetStateNumHint encodes the current state number within the passed
// commitment transaction by re-purposing the sequence fields in the input of // commitment transaction by re-purposing the sequence fields in the input of
// the commitment transaction to encode the obfuscated state number. The state // the commitment transaction to encode the obfuscated state number. The state
// number is encoded using 31-bits of the sequence number, with the top bit set // number is encoded using 31-bits of the sequence number, with the top bit set
// in order to disable BIP0068 (sequence locks) semantics. Finally before // in order to disable BIP0068 (sequence locks) semantics. Finally before
// encoding, the obfuscater is XOR'd against the state number in order to hide // encoding, the obfuscater is XOR'd against the state number in order to hide
// the exact state number from the PoV of outside parties. // the exact state number from the PoV of outside parties.
func setStateNumHint(commitTx *wire.MsgTx, stateNum uint32, // TODO(roasbeef): unexport function after bobNode is gone
func SetStateNumHint(commitTx *wire.MsgTx, stateNum uint32,
obsfucator [StateHintSize]byte) error { obsfucator [StateHintSize]byte) error {
// With the current schema we are only able able to encode state num // With the current schema we are only able able to encode state num
@ -809,13 +810,13 @@ func setStateNumHint(commitTx *wire.MsgTx, stateNum uint32,
return nil return nil
} }
// getStateNumHint recovers the current state number given a commitment // GetStateNumHint recovers the current state number given a commitment
// transaction which has previously had the state number encoded within it via // transaction which has previously had the state number encoded within it via
// setStateNumHint and a shared obsfucator. // setStateNumHint and a shared obsfucator.
// //
// See setStateNumHint for further details w.r.t exactly how the state-hints // See setStateNumHint for further details w.r.t exactly how the state-hints
// are encoded. // are encoded.
func getStateNumHint(commitTx *wire.MsgTx, obsfucator [StateHintSize]byte) uint32 { func GetStateNumHint(commitTx *wire.MsgTx, obsfucator [StateHintSize]byte) uint32 {
// Convert the obfuscater into a uint32, this will be used to // Convert the obfuscater into a uint32, this will be used to
// de-obfuscate the final recovered state number. // de-obfuscate the final recovered state number.
xorInt := binary.BigEndian.Uint32(obsfucator[:]) & (^wire.SequenceLockTimeDisabled) xorInt := binary.BigEndian.Uint32(obsfucator[:]) & (^wire.SequenceLockTimeDisabled)

@ -547,12 +547,12 @@ func TestCommitTxStateHint(t *testing.T) {
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
stateNum := uint32(i) stateNum := uint32(i)
err := setStateNumHint(commitTx, stateNum, obsfucator) err := SetStateNumHint(commitTx, stateNum, obsfucator)
if err != nil { if err != nil {
t.Fatalf("unable to set state num %v: %v", i, err) t.Fatalf("unable to set state num %v: %v", i, err)
} }
extractedStateNum := getStateNumHint(commitTx, obsfucator) extractedStateNum := GetStateNumHint(commitTx, obsfucator)
if extractedStateNum != stateNum { if extractedStateNum != stateNum {
t.Fatalf("state number mismatched, expected %v, got %v", t.Fatalf("state number mismatched, expected %v, got %v",
stateNum, extractedStateNum) stateNum, extractedStateNum)

@ -6,6 +6,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/btcsuite/fastsha256"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -197,10 +198,14 @@ type addSingleFunderSigsMsg struct {
// construct for them. // construct for them.
revokeKey *btcec.PublicKey revokeKey *btcec.PublicKey
// This should be 1/2 of the signatures needed to succesfully spend our // theirCommitmentSig are the 1/2 of the signatures needed to
// version of the commitment transaction. // succesfully spend our version of the commitment transaction.
theirCommitmentSig []byte theirCommitmentSig []byte
// obsfucator is the bytes to be used to obsfucate the state hints on
// the commitment transaction.
obsfucator [StateHintSize]byte
// NOTE: In order to avoid deadlocks, this channel MUST be buffered. // NOTE: In order to avoid deadlocks, this channel MUST be buffered.
err chan error err chan error
} }
@ -782,6 +787,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
return return
} }
// With both commitment transactions constructed, generate the state
// obsfucator then use it to encode the current state number withi both
// commitment transactions.
// TODO(roasbeef): define obsfucator scheme for dual funder
var stateObsfucator [StateHintSize]byte
if pendingReservation.partialState.IsInitiator {
stateObsfucator, err = deriveStateHintObsfucator(elkremSender)
if err != nil {
req.err <- err
return
}
}
err = initStateHints(ourCommitTx, theirCommitTx, stateObsfucator)
if err != nil {
req.err <- err
return
}
// Sort both transactions according to the agreed upon cannonical // Sort both transactions according to the agreed upon cannonical
// ordering. This lets us skip sending the entire transaction over, // ordering. This lets us skip sending the entire transaction over,
// instead we'll just send signatures. // instead we'll just send signatures.
@ -801,6 +824,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
pendingReservation.partialState.TheirCommitKey = theirCommitKey pendingReservation.partialState.TheirCommitKey = theirCommitKey
pendingReservation.partialState.TheirMultiSigKey = theirContribution.MultiSigKey pendingReservation.partialState.TheirMultiSigKey = theirContribution.MultiSigKey
pendingReservation.partialState.OurCommitTx = ourCommitTx pendingReservation.partialState.OurCommitTx = ourCommitTx
pendingReservation.partialState.StateHintObsfucator = stateObsfucator
pendingReservation.ourContribution.RevocationKey = ourRevokeKey pendingReservation.ourContribution.RevocationKey = ourRevokeKey
// Generate a signature for their version of the initial commitment // Generate a signature for their version of the initial commitment
@ -1047,6 +1071,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
pendingReservation.partialState.FundingOutpoint = req.fundingOutpoint pendingReservation.partialState.FundingOutpoint = req.fundingOutpoint
pendingReservation.partialState.TheirCurrentRevocation = req.revokeKey pendingReservation.partialState.TheirCurrentRevocation = req.revokeKey
pendingReservation.partialState.ChanID = req.fundingOutpoint pendingReservation.partialState.ChanID = req.fundingOutpoint
pendingReservation.partialState.StateHintObsfucator = req.obsfucator
fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil)
// Now that we have the funding outpoint, we can generate both versions // Now that we have the funding outpoint, we can generate both versions
@ -1071,6 +1096,15 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return return
} }
// With both commitment transactions constructed, generate the state
// obsfucator then use it to encode the current state number within
// both commitment transactions.
err = initStateHints(ourCommitTx, theirCommitTx, req.obsfucator)
if err != nil {
req.err <- err
return
}
// Sort both transactions according to the agreed upon cannonical // Sort both transactions according to the agreed upon cannonical
// ordering. This ensures that both parties sign the same sighash // ordering. This ensures that both parties sign the same sighash
// without further synchronization. // without further synchronization.
@ -1291,6 +1325,40 @@ func (l *LightningWallet) deriveMasterElkremRoot() (*btcec.PrivateKey, error) {
return masterElkremRoot.ECPrivKey() return masterElkremRoot.ECPrivKey()
} }
// deriveStateHintObsfucator derives the bytes to be used for obsfucatating the
// state hints from the elkerem root to be used for a new channel. The
// obsfucator is generated by performing an additional sha256 hash of the first
// child derived from the elkrem root. The leading 4 bytes are used for the
// obsfucator.
func deriveStateHintObsfucator(elkremRoot *elkrem.ElkremSender) ([StateHintSize]byte, error) {
var obsfucator [StateHintSize]byte
firstChild, err := elkremRoot.AtIndex(0)
if err != nil {
return obsfucator, err
}
grandChild := fastsha256.Sum256(firstChild[:])
copy(obsfucator[:], grandChild[:])
return obsfucator, nil
}
// initStateHints properly sets the obsfucated state ints ob both commitment
// transactions using the passed obsfucator.
func initStateHints(commit1, commit2 *wire.MsgTx,
obsfucator [StateHintSize]byte) error {
if err := SetStateNumHint(commit1, 0, obsfucator); err != nil {
return err
}
if err := SetStateNumHint(commit2, 0, obsfucator); err != nil {
return err
}
return nil
}
// selectInputs selects a slice of inputs necessary to meet the specified // selectInputs selects a slice of inputs necessary to meet the specified
// selection amount. If input selection is unable to suceed to to insuffcient // selection amount. If input selection is unable to suceed to to insuffcient
// funds, a non-nil error is returned. Additionally, the total amount of the // funds, a non-nil error is returned. Additionally, the total amount of the