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

View File

@ -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()

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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