diff --git a/fundingmanager.go b/fundingmanager.go index 8a9f94a2..eb269c35 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 41ba46e2..eea887b0 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 bba6837f..a7bcc453 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 c782258d..c3cf907e 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 e57739bc..8aaa7539 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 279e90e9..2f19b2f2 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