watchtower+htlcswitch: update client tower logic to recognize safu commitments

In this commit, we update the tower+link logic to tag a commitment as
the new (tweakless) format if it applies. In order to do this, the
BackupTask method has gained an additional parameter to indicate the
type of commitment that we're attempting to upload. This new tweakless
bool is then threaded through all the way to back up task creation to
ensure that we make the proper input.Input.

Finally, we've added a new test case for each existing test case to test
each case w/ and w/o the tweakless modifier.
This commit is contained in:
Olaoluwa Osuntokun 2019-08-07 19:49:59 -07:00
parent d22f2a1936
commit 4b65aea306
No known key found for this signature in database
GPG Key ID: BC13F65E2DC84465
8 changed files with 219 additions and 162 deletions

@ -175,6 +175,8 @@ type TowerClient interface {
// state. If the method returns nil, the backup is guaranteed to be
// successful unless the tower is unavailable and client is force quit,
// or the justice transaction would create dust outputs when trying to
// abide by the negotiated policy.
BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution) error
// abide by the negotiated policy. If the channel we're trying to back
// up doesn't have a tweak for the remote party's output, then
// isTweakless should be true.
BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, bool) error
}

@ -954,7 +954,7 @@ func (l *channelLink) htlcManager() {
l.fail(
LinkFailureError{
code: ErrSyncError,
code: ErrRecoveryError,
ForceClose: false,
},
"unable to synchronize channel "+
@ -1844,8 +1844,13 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
return
}
chanType := l.channel.State().ChanType
isTweakless := chanType == channeldb.SingleFunderTweakless
chanID := l.ChanID()
err = l.cfg.TowerClient.BackupState(&chanID, breachInfo)
err = l.cfg.TowerClient.BackupState(
&chanID, breachInfo, isTweakless,
)
if err != nil {
l.fail(LinkFailureError{code: ErrInternalError},
"unable to queue breach backup: %v", err)

@ -34,6 +34,11 @@ const (
// ErrInvalidRevocation indicates that the remote peer send us an
// invalid revocation message.
ErrInvalidRevocation
// ErrRecoveryError the channel was unable to be resumed, we need the
// remote party to force close the channel out on chain now as a
// result.
ErrRecoveryError
)
// LinkFailureError encapsulates an error that will make us fail the current
@ -74,6 +79,8 @@ func (e LinkFailureError) Error() string {
return "invalid commitment"
case ErrInvalidRevocation:
return "invalid revocation"
case ErrRecoveryError:
return "unable to resume channel, recovery required"
default:
return "unknown error"
}

@ -254,7 +254,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// DER-encoded signature under the to-remote pubkey. The sighash flag is
// also present, so we trim it.
toRemoteWitness, err := input.CommitSpendNoDelay(
signer, toRemoteSignDesc, justiceTxn,
signer, toRemoteSignDesc, justiceTxn, false,
)
if err != nil {
t.Fatalf("unable to sign to-remote input: %v", err)

@ -54,7 +54,7 @@ type backupTask struct {
// variables.
func newBackupTask(chanID *lnwire.ChannelID,
breachInfo *lnwallet.BreachRetribution,
sweepPkScript []byte) *backupTask {
sweepPkScript []byte, isTweakless bool) *backupTask {
// Parse the non-dust outputs from the breach transaction,
// simultaneously computing the total amount contained in the inputs
@ -85,12 +85,18 @@ func newBackupTask(chanID *lnwire.ChannelID,
totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
}
if breachInfo.LocalOutputSignDesc != nil {
witnessType := input.CommitmentNoDelay
if isTweakless {
witnessType = input.CommitSpendNoDelayTweakless
}
toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint,
input.CommitmentNoDelay,
witnessType,
breachInfo.LocalOutputSignDesc,
0,
)
totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
}
@ -271,6 +277,8 @@ func (t *backupTask) craftSessionPayload(
case input.CommitmentRevoke:
copy(justiceKit.CommitToLocalSig[:], signature[:])
case input.CommitSpendNoDelayTweakless:
fallthrough
case input.CommitmentNoDelay:
copy(justiceKit.CommitToRemoteSig[:], signature[:])
}

@ -73,6 +73,7 @@ type backupTaskTest struct {
bindErr error
expSweepScript []byte
signer input.Signer
tweakless bool
}
// genTaskTest creates a instance of a backupTaskTest using the passed
@ -89,7 +90,8 @@ func genTaskTest(
rewardScript []byte,
expSweepAmt int64,
expRewardAmt int64,
bindErr error) backupTaskTest {
bindErr error,
tweakless bool) backupTaskTest {
// Parse the key pairs for all keys used in the test.
revSK, revPK := btcec.PrivKeyFromBytes(
@ -188,9 +190,15 @@ func genTaskTest(
Hash: txid,
Index: index,
}
witnessType := input.CommitmentNoDelay
if tweakless {
witnessType = input.CommitSpendNoDelayTweakless
}
toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint,
input.CommitmentNoDelay,
witnessType,
breachInfo.LocalOutputSignDesc,
0,
)
@ -218,6 +226,7 @@ func genTaskTest(
bindErr: bindErr,
expSweepScript: makeAddrSlice(22),
signer: signer,
tweakless: tweakless,
}
}
@ -233,7 +242,19 @@ var (
addrScript, _ = txscript.PayToAddrScript(addr)
)
var backupTaskTests = []backupTaskTest{
// TestBackupTaskBind tests the initialization and binding of a backupTask to a
// ClientSession. After a successful bind, all parameters of the justice
// transaction should be solidified, so we assert there correctness. In an
// unsuccessful bind, the session-dependent parameters should be unmodified so
// that the backup task can be rescheduled if necessary. Finally, we assert that
// the backup task is able to encrypt a valid justice kit, and that we can
// decrypt it using the breach txid.
func TestBackupTask(t *testing.T) {
t.Parallel()
var backupTaskTests []backupTaskTest
for _, tweakless := range []bool{true, false} {
backupTaskTests = append(backupTaskTests, []backupTaskTest{
genTaskTest(
"commit no-reward, both outputs",
100, // stateNum
@ -245,6 +266,7 @@ var backupTaskTests = []backupTaskTest{
299241, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit no-reward, to-local output only",
@ -257,6 +279,7 @@ var backupTaskTests = []backupTaskTest{
199514, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit no-reward, to-remote output only",
@ -269,6 +292,7 @@ var backupTaskTests = []backupTaskTest{
99561, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit no-reward, to-remote output only, creates dust",
@ -281,6 +305,7 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
tweakless,
),
genTaskTest(
"commit no-reward, no outputs, fee rate exceeds inputs",
@ -293,6 +318,7 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr
tweakless,
),
genTaskTest(
"commit no-reward, no outputs, fee rate of 0 creates dust",
@ -305,6 +331,7 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
tweakless,
),
genTaskTest(
"commit reward, both outputs",
@ -317,6 +344,7 @@ var backupTaskTests = []backupTaskTest{
296117, // expSweepAmt
3000, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit reward, to-local output only",
@ -329,6 +357,7 @@ var backupTaskTests = []backupTaskTest{
197390, // expSweepAmt
2000, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit reward, to-remote output only",
@ -341,6 +370,7 @@ var backupTaskTests = []backupTaskTest{
98437, // expSweepAmt
1000, // expRewardAmt
nil, // bindErr
tweakless,
),
genTaskTest(
"commit reward, to-remote output only, creates dust",
@ -353,6 +383,7 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
tweakless,
),
genTaskTest(
"commit reward, no outputs, fee rate exceeds inputs",
@ -365,6 +396,7 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr
tweakless,
),
genTaskTest(
"commit reward, no outputs, fee rate of 0 creates dust",
@ -377,21 +409,17 @@ var backupTaskTests = []backupTaskTest{
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
tweakless,
),
}...)
}
// TestBackupTaskBind tests the initialization and binding of a backupTask to a
// ClientSession. After a successful bind, all parameters of the justice
// transaction should be solidified, so we assert there correctness. In an
// unsuccessful bind, the session-dependent parameters should be unmodified so
// that the backup task can be rescheduled if necessary. Finally, we assert that
// the backup task is able to encrypt a valid justice kit, and that we can
// decrypt it using the breach txid.
func TestBackupTask(t *testing.T) {
for _, test := range backupTaskTests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
for _, test := range backupTaskTests {
t.Run(test.name, func(t *testing.T) {
testBackupTask(t, test)
})
}
@ -399,7 +427,10 @@ func TestBackupTask(t *testing.T) {
func testBackupTask(t *testing.T, test backupTaskTest) {
// Create a new backupTask from the channel id and breach info.
task := newBackupTask(&test.chanID, test.breachInfo, test.expSweepScript)
task := newBackupTask(
&test.chanID, test.breachInfo, test.expSweepScript,
test.tweakless,
)
// Assert that all parameters set during initialization are properly
// populated.

@ -90,8 +90,10 @@ type Client interface {
// state. If the method returns nil, the backup is guaranteed to be
// successful unless the client is force quit, or the justice
// transaction would create dust outputs when trying to abide by the
// negotiated policy.
BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution) error
// negotiated policy. If the channel we're trying to back up doesn't
// have a tweak for the remote party's output, then isTweakless should
// be true.
BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, bool) error
// Start initializes the watchtower client, allowing it process requests
// to backup revoked channel states.
@ -564,7 +566,7 @@ func (c *TowerClient) RegisterChannel(chanID lnwire.ChannelID) error {
// - breached outputs contain too little value to sweep at the target sweep fee
// rate.
func (c *TowerClient) BackupState(chanID *lnwire.ChannelID,
breachInfo *lnwallet.BreachRetribution) error {
breachInfo *lnwallet.BreachRetribution, isTweakless bool) error {
// Retrieve the cached sweep pkscript used for this channel.
c.backupMu.Lock()
@ -589,7 +591,9 @@ func (c *TowerClient) BackupState(chanID *lnwire.ChannelID,
c.chanCommitHeights[*chanID] = breachInfo.RevokedStateNum
c.backupMu.Unlock()
task := newBackupTask(chanID, breachInfo, summary.SweepPkScript)
task := newBackupTask(
chanID, breachInfo, summary.SweepPkScript, isTweakless,
)
return c.pipeline.QueueBackupTask(task)
}

@ -628,7 +628,7 @@ func (h *testHarness) backupState(id, i uint64, expErr error) {
_, retribution := h.channel(id).getState(i)
chanID := chanIDFromInt(id)
err := h.client.BackupState(&chanID, retribution)
err := h.client.BackupState(&chanID, retribution, false)
if err != expErr {
h.t.Fatalf("back error mismatch, want: %v, got: %v",
expErr, err)