channeldb: create new channeldb package, update lnwallet to use new API

* Initial draft of brain dump of chandler. Nothing yet set in stone.
* Will most likely move the storage of all structs to a more “column”
oriented approach. Such that, small updates like incrementing the total
satoshi sent don’t result in the entire struct being serialized and
written.
* Some skeleton structs for other possible data we might want to store
are also included.
* Seem valuable to record as much data as possible for record keeping,
visualization, debugging, etc. Will need to set up a time+space+dirty
cache to ensure performance isn’t impacted too much.
This commit is contained in:
Olaoluwa Osuntokun
2015-12-26 12:35:15 -06:00
parent d7a1c5d337
commit 4fdb2763e6
13 changed files with 194 additions and 131 deletions

410
channeldb/channel.go Normal file
View File

@@ -0,0 +1,410 @@
package channeldb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"li.lan/labs/plasma/shachain"
)
var (
openChannelBucket = []byte("o")
closedChannelBucket = []byte("c")
activeChanKey = []byte("a")
// TODO(roasbeef): replace w/ tesnet-L also revisit dependancy...
ActiveNetParams = &chaincfg.TestNet3Params
)
// Payment...
type Payment struct {
// r [32]byte
// path *Route
}
// ClosedChannel...
type ClosedChannel struct {
}
// OpenChannel...
// TODO(roasbeef): store only the essentials? optimize space...
// TODO(roasbeef): switch to "column store"
type OpenChannel struct {
// Hash? or Their current pubKey?
// TODO(roasbeef): switch to Tadge's LNId
TheirLNID [wire.HashSize]byte
// The ID of a channel is the txid of the funding transaction.
ChanID [wire.HashSize]byte
MinFeePerKb btcutil.Amount
// Our reserve. Assume symmetric reserve amounts. Only needed if the
// funding type is CLTV.
//ReserveAmount btcutil.Amount
// Keys for both sides to be used for the commitment transactions.
OurCommitKey *btcec.PrivateKey
TheirCommitKey *btcec.PublicKey
// Tracking total channel capacity, and the amount of funds allocated
// to each side.
Capacity btcutil.Amount
OurBalance btcutil.Amount
TheirBalance btcutil.Amount
// Commitment transactions for both sides (they're asymmetric). Also
// their signature which lets us spend our version of the commitment
// transaction.
TheirCommitTx *wire.MsgTx
OurCommitTx *wire.MsgTx // TODO(roasbeef): store hash instead?
TheirCommitSig []byte // TODO(roasbeef): fixed length?, same w/ redeem
// The final funding transaction. Kept wallet-related records.
FundingTx *wire.MsgTx
MultiSigKey *btcec.PrivateKey
FundingRedeemScript []byte
// Current revocation for their commitment transaction. However, since
// this is the hash, and not the pre-image, we can't yet verify that
// it's actually in the chain.
TheirCurrentRevocation [wire.HashSize]byte
TheirShaChain *shachain.HyperShaChain
OurShaChain *shachain.HyperShaChain
// Final delivery address
OurDeliveryAddress btcutil.Address
TheirDeliveryAddress btcutil.Address
// In blocks
CsvDelay uint32
// TODO(roasbeef): track fees, other stats?
NumUpdates uint64
TotalSatoshisSent uint64
TotalSatoshisReceived uint64
CreationTime time.Time
}
// PutOpenChannel...
func (c *DB) PutOpenChannel(channel *OpenChannel) error {
return c.namespace.Update(func(tx walletdb.Tx) error {
// Get the bucket dedicated to storing the meta-data for open
// channels.
rootBucket := tx.RootBucket()
openChanBucket, err := rootBucket.CreateBucketIfNotExists(openChannelBucket)
if err != nil {
return err
}
return putOpenChannel(openChanBucket, channel, c.addrmgr)
})
}
// GetOpenChannel...
// TODO(roasbeef): assumes only 1 active channel per-node
func (c *DB) FetchOpenChannel(nodeID [32]byte) (*OpenChannel, error) {
var channel *OpenChannel
err := c.namespace.View(func(tx walletdb.Tx) error {
// Get the bucket dedicated to storing the meta-data for open
// channels.
rootBucket := tx.RootBucket()
openChanBucket := rootBucket.Bucket(openChannelBucket)
if openChannelBucket == nil {
return fmt.Errorf("open channel bucket does not exist")
}
oChannel, err := fetchOpenChannel(openChanBucket, nodeID,
c.addrmgr)
if err != nil {
return err
}
channel = oChannel
return nil
})
return channel, err
}
// putChannel...
func putOpenChannel(activeChanBucket walletdb.Bucket, channel *OpenChannel,
addrmgr *waddrmgr.Manager) error {
// Generate a serialized version of the open channel. The addrmgr is
// required in order to encrypt densitive data.
var b bytes.Buffer
if err := channel.Encode(&b, addrmgr); err != nil {
return err
}
// Grab the bucket dedicated to storing data related to this particular
// node.
nodeBucket, err := activeChanBucket.CreateBucketIfNotExists(channel.TheirLNID[:])
if err != nil {
return err
}
return nodeBucket.Put(activeChanKey, b.Bytes())
}
// fetchOpenChannel
func fetchOpenChannel(bucket walletdb.Bucket, nodeID [32]byte,
addrmgr *waddrmgr.Manager) (*OpenChannel, error) {
// Grab the bucket dedicated to storing data related to this particular
// node.
nodeBucket := bucket.Bucket(nodeID[:])
if nodeBucket == nil {
return nil, fmt.Errorf("channel bucket for node does not exist")
}
serializedChannel := nodeBucket.Get(activeChanKey)
if serializedChannel == nil {
// TODO(roasbeef): make proper in error.go
return nil, fmt.Errorf("node has no open channels")
}
// Decode the serialized channel state, using the addrmgr to decrypt
// sensitive information.
channel := &OpenChannel{}
reader := bytes.NewReader(serializedChannel)
if err := channel.Decode(reader, addrmgr); err != nil {
return nil, err
}
return channel, nil
}
// Encode...
// TODO(roasbeef): checksum
func (o *OpenChannel) Encode(b io.Writer, addrManager *waddrmgr.Manager) error {
if _, err := b.Write(o.TheirLNID[:]); err != nil {
return err
}
if _, err := b.Write(o.ChanID[:]); err != nil {
return err
}
if err := binary.Write(b, endian, uint64(o.MinFeePerKb)); err != nil {
return err
}
encryptedPriv, err := addrManager.Encrypt(waddrmgr.CKTPrivate,
o.OurCommitKey.Serialize())
if err != nil {
return err
}
if _, err := b.Write(encryptedPriv); err != nil {
return err
}
if _, err := b.Write(o.TheirCommitKey.SerializeCompressed()); err != nil {
return err
}
if err := binary.Write(b, endian, uint64(o.Capacity)); err != nil {
return err
}
if err := binary.Write(b, endian, uint64(o.OurBalance)); err != nil {
return err
}
if err := binary.Write(b, endian, uint64(o.TheirBalance)); err != nil {
return err
}
if err := o.TheirCommitTx.Serialize(b); err != nil {
return err
}
if err := o.OurCommitTx.Serialize(b); err != nil {
return err
}
if _, err := b.Write(o.TheirCommitSig[:]); err != nil {
return err
}
if err := o.FundingTx.Serialize(b); err != nil {
return err
}
encryptedPriv, err = addrManager.Encrypt(waddrmgr.CKTPrivate,
o.MultiSigKey.Serialize())
if err != nil {
return err
}
if _, err := b.Write(encryptedPriv); err != nil {
return err
}
if _, err := b.Write(o.FundingRedeemScript); err != nil {
return err
}
if _, err := b.Write(o.TheirCurrentRevocation[:]); err != nil {
return err
}
// TODO(roasbeef): serialize shachains
if _, err := b.Write([]byte(o.OurDeliveryAddress.EncodeAddress())); err != nil {
return err
}
if _, err := b.Write([]byte(o.TheirDeliveryAddress.EncodeAddress())); err != nil {
return err
}
if err := binary.Write(b, endian, o.CsvDelay); err != nil {
return err
}
if err := binary.Write(b, endian, o.NumUpdates); err != nil {
return err
}
if err := binary.Write(b, endian, o.TotalSatoshisSent); err != nil {
return err
}
if err := binary.Write(b, endian, o.TotalSatoshisReceived); err != nil {
return err
}
if err := binary.Write(b, endian, o.CreationTime.Unix()); err != nil {
return err
}
return nil
}
// Decode...
func (o *OpenChannel) Decode(b io.Reader, addrManager *waddrmgr.Manager) error {
var scratch [8]byte
if _, err := b.Read(o.TheirLNID[:]); err != nil {
return err
}
if _, err := b.Read(o.ChanID[:]); err != nil {
return err
}
if _, err := b.Read(scratch[:]); err != nil {
return err
}
o.MinFeePerKb = btcutil.Amount(endian.Uint64(scratch[:]))
// nonce + serPrivKey + mac
var encryptedPriv [24 + 32 + 16]byte
if _, err := b.Read(encryptedPriv[:]); err != nil {
return err
}
decryptedPriv, err := addrManager.Decrypt(waddrmgr.CKTPrivate, encryptedPriv[:])
if err != nil {
return err
}
o.OurCommitKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), decryptedPriv)
var serPubKey [33]byte
if _, err := b.Read(serPubKey[:]); err != nil {
return err
}
o.TheirCommitKey, err = btcec.ParsePubKey(serPubKey[:], btcec.S256())
if err != nil {
return err
}
if _, err := b.Read(scratch[:]); err != nil {
return err
}
o.Capacity = btcutil.Amount(endian.Uint64(scratch[:]))
if _, err := b.Read(scratch[:]); err != nil {
return err
}
o.OurBalance = btcutil.Amount(endian.Uint64(scratch[:]))
if _, err := b.Read(scratch[:]); err != nil {
return err
}
o.TheirBalance = btcutil.Amount(endian.Uint64(scratch[:]))
o.TheirCommitTx = wire.NewMsgTx()
if err := o.TheirCommitTx.Deserialize(b); err != nil {
return err
}
o.OurCommitTx = wire.NewMsgTx()
if err := o.OurCommitTx.Deserialize(b); err != nil {
return err
}
var sig [64]byte
if _, err := b.Read(sig[:]); err != nil {
return err
}
o.TheirCommitSig = sig[:]
if err != nil {
return err
}
o.FundingTx = wire.NewMsgTx()
if err := o.FundingTx.Deserialize(b); err != nil {
return err
}
if _, err := b.Read(encryptedPriv[:]); err != nil {
return err
}
decryptedPriv, err = addrManager.Decrypt(waddrmgr.CKTPrivate, encryptedPriv[:])
if err != nil {
return err
}
o.MultiSigKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), decryptedPriv)
var redeemScript [71]byte
if _, err := b.Read(redeemScript[:]); err != nil {
return err
}
o.FundingRedeemScript = redeemScript[:]
if _, err := b.Read(o.TheirCurrentRevocation[:]); err != nil {
return err
}
var addr [34]byte
if _, err := b.Read(addr[:]); err != nil {
return err
}
o.OurDeliveryAddress, err = btcutil.DecodeAddress(string(addr[:]), ActiveNetParams)
if err != nil {
return err
}
if _, err := b.Read(addr[:]); err != nil {
return err
}
o.TheirDeliveryAddress, err = btcutil.DecodeAddress(string(addr[:]), ActiveNetParams)
if err != nil {
return err
}
if err := binary.Read(b, endian, &o.CsvDelay); err != nil {
return err
}
if err := binary.Read(b, endian, &o.NumUpdates); err != nil {
return err
}
if err := binary.Read(b, endian, &o.TotalSatoshisSent); err != nil {
return err
}
if err := binary.Read(b, endian, &o.TotalSatoshisReceived); err != nil {
return err
}
var unix int64
if err := binary.Read(b, endian, &unix); err != nil {
return err
}
o.CreationTime = time.Unix(unix, 0)
return nil
}

281
channeldb/channel_test.go Normal file
View File

@@ -0,0 +1,281 @@
package channeldb
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
)
var (
sig = bytes.Repeat([]byte{1}, 64)
key = [wire.HashSize]byte{
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
0x68, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
0xd, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1e, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9,
}
id = [wire.HashSize]byte{
0x51, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
0x48, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
0x2d, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1f, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9,
}
testTx = &wire.MsgTx{
Version: 1,
TxIn: []*wire.TxIn{
&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: wire.ShaHash{},
Index: 0xffffffff,
},
SignatureScript: []byte{0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62},
Sequence: 0xffffffff,
},
},
TxOut: []*wire.TxOut{
&wire.TxOut{
Value: 5000000000,
PkScript: []byte{
0x41, // OP_DATA_65
0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5,
0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42,
0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1,
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24,
0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97,
0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20,
0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63,
0xa6, // 65-byte signature
0xac, // OP_CHECKSIG
},
},
},
LockTime: 5,
}
)
// createDbNamespace creates a new wallet database at the provided path and
// returns it along with the address manager namespace.
func createDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) {
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
fmt.Println("fuk")
return nil, nil, err
}
namespace, err := db.Namespace([]byte("waddr"))
if err != nil {
db.Close()
return nil, nil, err
}
return db, namespace, nil
}
// setupManager creates a new address manager and returns a teardown function
// that should be invoked to ensure it is closed and removed upon completion.
func createTestManager(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager) {
t.Parallel()
// Create a new manager in a temp directory.
dirName, err := ioutil.TempDir("", "mgrtest")
if err != nil {
t.Fatalf("Failed to create db temp dir: %v", err)
}
dbPath := filepath.Join(dirName, "mgrtest.db")
db, namespace, err := createDbNamespace(dbPath)
if err != nil {
_ = os.RemoveAll(dirName)
t.Fatalf("createDbNamespace: unexpected error: %v", err)
}
mgr, err = waddrmgr.Create(namespace, key[:], []byte("test"),
[]byte("test"), ActiveNetParams, nil)
if err != nil {
db.Close()
_ = os.RemoveAll(dirName)
t.Fatalf("Failed to create Manager: %v", err)
}
tearDownFunc = func() {
mgr.Close()
db.Close()
_ = os.RemoveAll(dirName)
}
if err := mgr.Unlock([]byte("test")); err != nil {
t.Fatalf("unable to unlock mgr: %v", err)
}
return tearDownFunc, mgr
}
func TestOpenChannelEncodeDecode(t *testing.T) {
teardown, manager := createTestManager(t)
defer teardown()
privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), key[:])
addr, err := btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), ActiveNetParams)
if err != nil {
t.Fatalf("unable to create delivery address")
}
script, err := txscript.MultiSigScript([]*btcutil.AddressPubKey{addr, addr}, 2)
if err != nil {
t.Fatalf("unable to create redeemScript")
}
state := OpenChannel{
TheirLNID: id,
ChanID: id,
MinFeePerKb: btcutil.Amount(5000),
OurCommitKey: privKey,
TheirCommitKey: pubKey,
Capacity: btcutil.Amount(10000),
OurBalance: btcutil.Amount(3000),
TheirBalance: btcutil.Amount(7000),
TheirCommitTx: testTx,
OurCommitTx: testTx,
TheirCommitSig: sig,
FundingTx: testTx,
MultiSigKey: privKey,
FundingRedeemScript: script,
TheirCurrentRevocation: id,
OurDeliveryAddress: addr,
TheirDeliveryAddress: addr,
CsvDelay: 5,
NumUpdates: 1,
TotalSatoshisSent: 1,
TotalSatoshisReceived: 2,
CreationTime: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
}
var b bytes.Buffer
if err := state.Encode(&b, manager); err != nil {
t.Fatalf("unable to encode channel state: %v", err)
}
reader := bytes.NewReader(b.Bytes())
newState := &OpenChannel{}
if err := newState.Decode(reader, manager); err != nil {
t.Fatalf("unable to decode channel state: %v", err)
}
// The decoded channel state should be identical to what we stored
// above.
if !bytes.Equal(state.TheirLNID[:], newState.TheirLNID[:]) {
t.Fatalf("their id doesn't match")
}
if !bytes.Equal(state.ChanID[:], newState.ChanID[:]) {
t.Fatalf("chan id's don't match")
}
if state.MinFeePerKb != newState.MinFeePerKb {
t.Fatalf("fee/kb doens't match")
}
if !bytes.Equal(state.OurCommitKey.Serialize(),
newState.OurCommitKey.Serialize()) {
t.Fatalf("our commit key dont't match")
}
if !bytes.Equal(state.TheirCommitKey.SerializeCompressed(),
newState.TheirCommitKey.SerializeCompressed()) {
t.Fatalf("their commit key dont't match")
}
if state.Capacity != newState.Capacity {
t.Fatalf("capacity doesn't match")
}
if state.OurBalance != newState.OurBalance {
t.Fatalf("our balance doesn't match")
}
if state.TheirBalance != newState.TheirBalance {
t.Fatalf("their balance doesn't match")
}
var b1, b2 bytes.Buffer
if err := state.TheirCommitTx.Serialize(&b1); err != nil {
t.Fatalf("unable to serialize transaction")
}
if err := newState.TheirCommitTx.Serialize(&b2); err != nil {
t.Fatalf("unable to serialize transaction")
}
if !bytes.Equal(b1.Bytes(), b2.Bytes()) {
t.Fatalf("theirCommitTx doesn't match")
}
b1.Reset()
b2.Reset()
if err := state.OurCommitTx.Serialize(&b1); err != nil {
t.Fatalf("unable to serialize transaction")
}
if err := newState.OurCommitTx.Serialize(&b2); err != nil {
t.Fatalf("unable to serialize transaction")
}
if !bytes.Equal(b1.Bytes(), b2.Bytes()) {
t.Fatalf("ourCommitTx doesn't match")
}
if !bytes.Equal(state.TheirCommitSig, newState.TheirCommitSig) {
t.Fatalf("theirCommitSig doesn't match")
}
b1.Reset()
b2.Reset()
if err := state.FundingTx.Serialize(&b1); err != nil {
t.Fatalf("unable to serialize transaction")
}
if err := newState.FundingTx.Serialize(&b2); err != nil {
t.Fatalf("unable to serialize transaction")
}
if !bytes.Equal(b1.Bytes(), b2.Bytes()) {
t.Fatalf("funding tx doesn't match")
}
if !bytes.Equal(state.MultiSigKey.Serialize(),
newState.MultiSigKey.Serialize()) {
t.Fatalf("multisig key doesn't match")
}
if !bytes.Equal(state.FundingRedeemScript, newState.FundingRedeemScript) {
t.Fatalf("redeem script doesn't match")
}
if state.OurDeliveryAddress.EncodeAddress() != newState.OurDeliveryAddress.EncodeAddress() {
t.Fatalf("our delivery address doesn't match")
}
if state.TheirDeliveryAddress.EncodeAddress() != newState.TheirDeliveryAddress.EncodeAddress() {
t.Fatalf("their delivery address doesn't match")
}
if state.NumUpdates != newState.NumUpdates {
t.Fatalf("num updates doesn't match: %v vs %v",
state.NumUpdates, newState.NumUpdates)
}
if state.CsvDelay != newState.CsvDelay {
t.Fatalf("csv delay doesn't match: %v vs %v",
state.CsvDelay, newState.CsvDelay)
}
if state.TotalSatoshisSent != newState.TotalSatoshisSent {
t.Fatalf("satoshis sent doesn't match: %v vs %v",
state.TotalSatoshisSent, newState.TotalSatoshisSent)
}
if state.TotalSatoshisReceived != newState.TotalSatoshisReceived {
t.Fatalf("satoshis received doesn't match")
}
if state.CreationTime.Unix() != newState.CreationTime.Unix() {
t.Fatalf("creation time doesn't match")
}
}
func TestOpenChannelEncodeDecodeCorruption(t *testing.T) {
}

54
channeldb/db.go Normal file
View File

@@ -0,0 +1,54 @@
package channeldb
import (
"bytes"
"encoding/binary"
"sync"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
var (
endian = binary.BigEndian
)
var bufPool = &sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
// Store...
// TODO(roasbeef): CHECKSUMS, REDUNDANCY, etc etc.
type DB struct {
// TODO(roasbeef): caching, etc?
addrmgr *waddrmgr.Manager
namespace walletdb.Namespace
}
// Wipe...
func (d *DB) Wipe() error {
return d.namespace.Update(func(tx walletdb.Tx) error {
rootBucket := tx.RootBucket()
// TODO(roasbeef): other buckets
return rootBucket.DeleteBucket(openChannelBucket)
})
}
// New...
// TODO(roasbeef): re-visit this dependancy...
func New(addrmgr *waddrmgr.Manager, namespace walletdb.Namespace) *DB {
// TODO(roasbeef): create buckets if not created?
return &DB{addrmgr, namespace}
}
// Open...
// TODO(roasbeef): create+open, ditch New, fixes above
func Open() *DB {
return nil
}
// Create...
func Create() *DB {
return nil
}

1
channeldb/doc.go Normal file
View File

@@ -0,0 +1 @@
package channeldb

1
channeldb/error.go Normal file
View File

@@ -0,0 +1 @@
package channeldb

1
channeldb/fees.go Normal file
View File

@@ -0,0 +1 @@
package channeldb

1
channeldb/log.go Normal file
View File

@@ -0,0 +1 @@
package channeldb

1
channeldb/nodes.go Normal file
View File

@@ -0,0 +1 @@
package channeldb

1
channeldb/route.go Normal file
View File

@@ -0,0 +1 @@
package channeldb