breacharbiter: adds persistence to retribution flow

This commit introduces a RetributionStore interface, which
  establishes the methods used to access persisted information
  regarding breached channels. A RetributionStore is used to
  persist retributionInfo regarding all channels for which
  the wallet has signaled a breach.

  The current design could be improved by moving certain
  functionality, e.g. closing channels and htlc links, such
  that they are handled by upstream by their respective
  subsystems. This was investigated, but deemed preferable to
  postpone to a later update to prevent the current
  implementation from sprawling amongst too many packages.

  The test suite creates a mockRetributionStore and ensures that
  it exhibits the same behavior as the retribution store backed
  by a channeldb.DB.
This commit is contained in:
Conner Fromknecht
2017-07-25 22:57:29 -07:00
committed by Olaoluwa Osuntokun
parent 6ffe33f01a
commit c3736e6893
2 changed files with 514 additions and 212 deletions

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"reflect"
"sync"
"testing"
"github.com/lightningnetwork/lnd/channeldb"
@@ -77,7 +78,7 @@ var (
breachSignDescs = []lnwallet.SignDescriptor{
{
PrivateTweak: []byte{
SingleTweak: []byte{
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
@@ -107,7 +108,7 @@ var (
HashType: txscript.SigHashAll,
},
{
PrivateTweak: []byte{
SingleTweak: []byte{
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
@@ -137,7 +138,7 @@ var (
HashType: txscript.SigHashAll,
},
{
PrivateTweak: []byte{
SingleTweak: []byte{
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
@@ -199,10 +200,12 @@ var (
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
},
chanPoint: breachOutPoints[0],
selfOutput: &breachedOutputs[0],
revokedOutput: &breachedOutputs[1],
htlcOutputs: []*breachedOutput{},
chanPoint: breachOutPoints[0],
capacity: btcutil.Amount(1e7),
settledBalance: btcutil.Amount(1e7),
selfOutput: &breachedOutputs[0],
revokedOutput: &breachedOutputs[1],
htlcOutputs: []*breachedOutput{},
},
{
commitHash: [chainhash.HashSize]byte{
@@ -211,9 +214,11 @@ var (
0x2d, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1f, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9,
},
chanPoint: breachOutPoints[1],
selfOutput: &breachedOutputs[0],
revokedOutput: &breachedOutputs[1],
chanPoint: breachOutPoints[1],
capacity: btcutil.Amount(1e7),
settledBalance: btcutil.Amount(1e7),
selfOutput: &breachedOutputs[0],
revokedOutput: &breachedOutputs[1],
htlcOutputs: []*breachedOutput{
&breachedOutputs[1],
&breachedOutputs[2],
@@ -224,7 +229,7 @@ var (
// Parse the pubkeys in the breached outputs.
func initBreachedOutputs() error {
for i := 0; i < len(breachedOutputs); i++ {
for i := range breachedOutputs {
bo := &breachedOutputs[i]
// Parse the sign descriptor's pubkey.
@@ -234,7 +239,7 @@ func initBreachedOutputs() error {
return fmt.Errorf("unable to parse pubkey: %v", breachKeys[i])
}
sd.PubKey = pubkey
bo.signDescriptor = sd
bo.signDescriptor = *sd
}
return nil
@@ -278,6 +283,12 @@ func TestRetributionSerialization(t *testing.T) {
for i := 0; i < len(retributions); i++ {
ret := &retributions[i]
remoteIdentity, err := btcec.ParsePubKey(breachKeys[i], btcec.S256())
if err != nil {
t.Fatalf("unable to parse public key [%v]: %v", i, err)
}
ret.remoteIdentity = *remoteIdentity
var buf bytes.Buffer
if err := ret.Encode(&buf); err != nil {
@@ -298,37 +309,104 @@ func TestRetributionSerialization(t *testing.T) {
}
}
// TODO(phlip9): reuse existing function?
// makeTestDB creates a new instance of the ChannelDB for testing purposes. A
// callback which cleans up the created temporary directories is also returned
// and intended to be executed after the test completes.
func makeTestDB() (*channeldb.DB, func(), error) {
var db *channeldb.DB
// copyRetInfo creates a complete copy of the given retributionInfo.
func copyRetInfo(retInfo *retributionInfo) *retributionInfo {
ret := &retributionInfo{
commitHash: retInfo.commitHash,
chanPoint: retInfo.chanPoint,
remoteIdentity: retInfo.remoteIdentity,
capacity: retInfo.capacity,
settledBalance: retInfo.settledBalance,
selfOutput: retInfo.selfOutput,
revokedOutput: retInfo.revokedOutput,
htlcOutputs: make([]*breachedOutput, len(retInfo.htlcOutputs)),
doneChan: make(chan struct{}),
}
for i, htlco := range retInfo.htlcOutputs {
ret.htlcOutputs[i] = htlco
}
return ret
}
// mockRetributionStore implements the RetributionStore interface and is backed
// by an in-memory map. Access to the internal state is provided by a mutex.
// TODO(cfromknecht) extend to support and test controlled failures.
type mockRetributionStore struct {
mu sync.Mutex
state map[wire.OutPoint]*retributionInfo
}
func newMockRetributionStore() *mockRetributionStore {
return &mockRetributionStore{
mu: sync.Mutex{},
state: make(map[wire.OutPoint]*retributionInfo),
}
}
func (rs *mockRetributionStore) Add(retInfo *retributionInfo) error {
rs.mu.Lock()
rs.state[retInfo.chanPoint] = copyRetInfo(retInfo)
rs.mu.Unlock()
return nil
}
func (rs *mockRetributionStore) Remove(key *wire.OutPoint) error {
rs.mu.Lock()
delete(rs.state, *key)
rs.mu.Unlock()
return nil
}
func (rs *mockRetributionStore) ForAll(cb func(*retributionInfo) error) error {
rs.mu.Lock()
defer rs.mu.Unlock()
for _, retInfo := range rs.state {
if err := cb(copyRetInfo(retInfo)); err != nil {
return err
}
}
return nil
}
// TestMockRetributionStore instantiates a mockRetributionStore and tests its
// behavior using the general RetributionStore test suite.
func TestMockRetributionStore(t *testing.T) {
mrs := newMockRetributionStore()
testRetributionStore(mrs, t)
}
// TestChannelDBRetributionStore instantiates a retributionStore backed by a
// channeldb.DB, and tests its behavior using the general RetributionStore test
// suite.
func TestChannelDBRetributionStore(t *testing.T) {
// First, create a temporary directory to be used for the duration of
// this test.
tempDirName, err := ioutil.TempDir("", "channeldb")
if err != nil {
return nil, nil, err
t.Fatalf("unable to initialize temp directory for channeldb: %v", err)
}
defer os.RemoveAll(tempDirName)
// Next, create channeldb for the first time.
db, err = channeldb.Open(tempDirName)
db, err := channeldb.Open(tempDirName)
if err != nil {
return nil, nil, err
t.Fatalf("unable to open channeldb: %v", err)
}
defer db.Close()
cleanUp := func() {
if db != nil {
db.Close()
}
os.RemoveAll(tempDirName)
}
return db, cleanUp, nil
// Finally, instantiate retribution store and execute RetributionStore test
// suite.
rs := newRetributionStore(db)
testRetributionStore(rs, t)
}
func countRetributions(t *testing.T, rs *retributionStore) int {
func countRetributions(t *testing.T, rs RetributionStore) int {
count := 0
err := rs.ForAll(func(_ *retributionInfo) error {
count++
@@ -341,32 +419,29 @@ func countRetributions(t *testing.T, rs *retributionStore) int {
}
// Test that the retribution persistence layer works.
func TestRetributionStore(t *testing.T) {
db, cleanUp, err := makeTestDB()
defer cleanUp()
if err != nil {
t.Fatalf("unable to create test db: %v", err)
}
func testRetributionStore(rs RetributionStore, t *testing.T) {
if err := initBreachedOutputs(); err != nil {
t.Fatalf("unable to init breached outputs: %v", err)
}
rs := newRetributionStore(db)
// Make sure that a new retribution store is actually emtpy.
if count := countRetributions(t, rs); count != 0 {
t.Fatalf("expected 0 retributions, found %v", count)
}
// Add some retribution states to the store.
// Add first retribution state to the store.
if err := rs.Add(&retributions[0]); err != nil {
t.Fatalf("unable to add to retribution store: %v", err)
}
// Ensure that the retribution store has one retribution.
if count := countRetributions(t, rs); count != 1 {
t.Fatalf("expected 1 retributions, found %v", count)
}
// Add second retribution state to the store.
if err := rs.Add(&retributions[1]); err != nil {
t.Fatalf("unable to add to retribution store: %v", err)
}
// There should be 2 retributions in the store.
if count := countRetributions(t, rs); count != 2 {
t.Fatalf("expected 2 retributions, found %v", count)
@@ -387,6 +462,11 @@ func TestRetributionStore(t *testing.T) {
if err := rs.Remove(&retributions[0].chanPoint); err != nil {
t.Fatalf("unable to remove from retribution store: %v", err)
}
// Ensure that the retribution store has one retribution.
if count := countRetributions(t, rs); count != 1 {
t.Fatalf("expected 1 retributions, found %v", count)
}
if err := rs.Remove(&retributions[1].chanPoint); err != nil {
t.Fatalf("unable to remove from retribution store: %v", err)
}