From 22074eb7372954d514e05433c3eff68099ffd942 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 16 Nov 2016 12:54:27 -0800 Subject: [PATCH] lnwallet: integrate obfuscated state hints into funding workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- fundingmanager.go | 35 ++++++++++------- lnwallet/interface_test.go | 41 +++++++++++++++----- lnwallet/reservation.go | 20 +++++++++- lnwallet/script_utils.go | 9 +++-- lnwallet/script_utils_test.go | 4 +- lnwallet/wallet.go | 72 ++++++++++++++++++++++++++++++++++- 6 files changed, 147 insertions(+), 34 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 8a9f94a28..eb269c35a 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -103,13 +103,14 @@ type fundingErrorMsg struct { type pendingChannels map[uint64]*reservationWithCtx // fundingManager acts as an orchestrator/bridge between the wallet's -// 'ChannelReservation' workflow, and the wire protocl's funding initiation -// messages. Any requests to initaite the funding workflow for a channel, either -// kicked-off locally, or remotely is handled by the funding manager. Once a -// channels's funding workflow has been completed, any local callers, the local -// peer, and possibly the remote peer are notified of the completion of the -// channel workflow. Additionally, any temporary or permanent access controls -// between the wallet and remote peers are enforced via the funding manager. +// 'ChannelReservation' workflow, and the wire protocol's funding initiation +// messages. Any requests to initiate the funding workflow for a channel, +// either kicked-off locally, or remotely is handled by the funding manager. +// Once a channel's funding workflow has been completed, any local callers, the +// local peer, and possibly the remote peer are notified of the completion of +// the channel workflow. Additionally, any temporary or permanent access +// controls between the wallet and remote peers are enforced via the funding +// manager. type fundingManager struct { // MUST be used atomically. started int32 @@ -464,8 +465,10 @@ func (f *fundingManager) handleFundingResponse(fmsg *fundingResponseMsg) { outPoint, msg.ChannelID) revocationKey := resCtx.reservation.OurContribution().RevocationKey + obsfucator := resCtx.reservation.StateNumObfuscator() + fundingComplete := lnwire.NewSingleFundingComplete(msg.ChannelID, - outPoint, commitSig, revocationKey) + outPoint, commitSig, revocationKey, obsfucator) sourcePeer.queueMsg(fundingComplete, nil) } @@ -491,16 +494,20 @@ func (f *fundingManager) handleFundingComplete(fmsg *fundingCompleteMsg) { // TODO(roasbeef): make case (p vs P) consistent throughout fundingOut := fmsg.msg.FundingOutPoint chanID := fmsg.msg.ChannelID - commitSig := fmsg.msg.CommitSignature.Serialize() 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 - 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. fndgLog.Errorf("unable to complete single reservation: %v", err) fmsg.peer.Disconnect() diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 41ba46e27..eea887b00 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -108,6 +108,8 @@ type bobNode struct { delay uint32 id *btcec.PublicKey + obsfucator [lnwallet.StateHintSize]byte + availableOutputs []*wire.TxIn changeOutputs []*wire.TxOut fundingAmt btcutil.Amount @@ -241,6 +243,9 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) { copy(revocation[:], bobsPrivKey) revocation[0] = 0xff + var obsfucator [lnwallet.StateHintSize]byte + copy(obsfucator[:], revocation[:]) + // His ID is just as creative... var id [wire.HashSize]byte 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 - // from our HD chain, a change output with 3 BTC, and 2 outputs selected - // of 4 BTC each. Additionally, the rest of the items needed to fufill a - // funding contribution should also have been filled in. + // from our HD chain, a change output with 3 BTC, and 2 outputs + // selected of 4 BTC each. Additionally, the rest of the items needed + // to fulfill a funding contribution should also have been filled in. ourContribution := chanReservation.OurContribution() if len(ourContribution.Inputs) != 2 { 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 // 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, testPub, bobAddr, numReqConfs, 4) if err != nil { @@ -733,6 +738,13 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, hex.EncodeToString(channels[0].FundingOutpoint.Hash[:]), 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()) } @@ -848,15 +860,23 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, fundingTxID := fundingTx.TxSha() _, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript) 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) - aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, ourContribution.CommitKey, - bobContribution.CommitKey, ourContribution.RevocationKey, - ourContribution.CsvDelay, 0, capacity) + aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, + ourContribution.CommitKey, bobContribution.CommitKey, + ourContribution.RevocationKey, ourContribution.CsvDelay, 0, + capacity) if err != nil { t.Fatalf("unable to create alice's commit tx: %v", err) } 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, // TODO(roasbeef): account for hard-coded fee, remove bob node fundingRedeemScript, int64(capacity)+5000) @@ -866,8 +886,9 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, // With this stage complete, Alice can now complete the reservation. bobRevokeKey := bobContribution.RevocationKey - if err := chanReservation.CompleteReservationSingle(bobRevokeKey, - fundingOutpoint, bobCommitSig); err != nil { + err = chanReservation.CompleteReservationSingle(bobRevokeKey, + fundingOutpoint, bobCommitSig, bobObsfucator) + if err != nil { 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 // to play around with. miningNode, err := rpctest.New(netParams, nil, nil) - defer miningNode.TearDown() if err != nil { t.Fatalf("unable to create mining node: %v", err) } + defer miningNode.TearDown() if err := miningNode.SetUp(true, 25); err != nil { t.Fatalf("unable to set up mining node: %v", err) } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index bba6837f2..a7bcc4535 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -324,8 +324,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*InputScr // the .OurSignatures() method. As this method should only be called as a // response to a single funder channel, only a commitment signature will be // populated. -func (r *ChannelReservation) CompleteReservationSingle(revocationKey *btcec.PublicKey, - fundingPoint *wire.OutPoint, commitSig []byte) error { +func (r *ChannelReservation) CompleteReservationSingle( + revocationKey *btcec.PublicKey, fundingPoint *wire.OutPoint, + commitSig []byte, obsfucator [StateHintSize]byte) error { + errChan := make(chan error, 1) r.wallet.msgChan <- &addSingleFunderSigsMsg{ @@ -333,6 +335,7 @@ func (r *ChannelReservation) CompleteReservationSingle(revocationKey *btcec.Publ revokeKey: revocationKey, fundingOutpoint: fundingPoint, theirCommitmentSig: commitSig, + obsfucator: obsfucator, err: errChan, } @@ -395,6 +398,19 @@ func (r *ChannelReservation) FundingOutpoint() *wire.OutPoint { 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 // the scenario that communications with the counterparty break down. Upon // cancellation, all resources previously reserved for this pending payment diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index c782258d5..c3cf907e9 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -772,14 +772,15 @@ func deriveElkremRoot(elkremDerivationRoot *btcec.PrivateKey, 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 // 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 // in order to disable BIP0068 (sequence locks) semantics. Finally before // encoding, the obfuscater is XOR'd against the state number in order to hide // 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 { // 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 } -// 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 // setStateNumHint and a shared obsfucator. // // See setStateNumHint for further details w.r.t exactly how the state-hints // 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 // de-obfuscate the final recovered state number. xorInt := binary.BigEndian.Uint32(obsfucator[:]) & (^wire.SequenceLockTimeDisabled) diff --git a/lnwallet/script_utils_test.go b/lnwallet/script_utils_test.go index e57739bc8..8aaa75393 100644 --- a/lnwallet/script_utils_test.go +++ b/lnwallet/script_utils_test.go @@ -547,12 +547,12 @@ func TestCommitTxStateHint(t *testing.T) { for i := 0; i < 10000; i++ { stateNum := uint32(i) - err := setStateNumHint(commitTx, stateNum, obsfucator) + err := SetStateNumHint(commitTx, stateNum, obsfucator) if err != nil { t.Fatalf("unable to set state num %v: %v", i, err) } - extractedStateNum := getStateNumHint(commitTx, obsfucator) + extractedStateNum := GetStateNumHint(commitTx, obsfucator) if extractedStateNum != stateNum { t.Fatalf("state number mismatched, expected %v, got %v", stateNum, extractedStateNum) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 279e90e95..2f19b2f2e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -6,6 +6,7 @@ import ( "sync" "sync/atomic" + "github.com/btcsuite/fastsha256" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" @@ -197,10 +198,14 @@ type addSingleFunderSigsMsg struct { // construct for them. revokeKey *btcec.PublicKey - // This should be 1/2 of the signatures needed to succesfully spend our - // version of the commitment transaction. + // theirCommitmentSig are the 1/2 of the signatures needed to + // succesfully spend our version of the commitment transaction. 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. err chan error } @@ -782,6 +787,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { 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 // ordering. This lets us skip sending the entire transaction over, // instead we'll just send signatures. @@ -801,6 +824,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { pendingReservation.partialState.TheirCommitKey = theirCommitKey pendingReservation.partialState.TheirMultiSigKey = theirContribution.MultiSigKey pendingReservation.partialState.OurCommitTx = ourCommitTx + pendingReservation.partialState.StateHintObsfucator = stateObsfucator pendingReservation.ourContribution.RevocationKey = ourRevokeKey // 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.TheirCurrentRevocation = req.revokeKey pendingReservation.partialState.ChanID = req.fundingOutpoint + pendingReservation.partialState.StateHintObsfucator = req.obsfucator fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) // Now that we have the funding outpoint, we can generate both versions @@ -1071,6 +1096,15 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { 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 // ordering. This ensures that both parties sign the same sighash // without further synchronization. @@ -1291,6 +1325,40 @@ func (l *LightningWallet) deriveMasterElkremRoot() (*btcec.PrivateKey, error) { 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 // 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