channeldb: add persistent invoice workflow

This commit adds the necessary database functionality required for a
high-level payment invoice workflow. Invoices can be added dealing the
requirements for fulfillment, looked by payment hash, and the finally
also settled by payment hash. For record keeping and the possibility of
reconciling future disputes, invoices are currently never deleted from
disk. Instead when an invoice is settled a bit is toggled indicating as
much.

The current invoiceManger within the daemon will be modified to use
this persistent invoice store, only storing certain “debug” invoices in
memory as dictated by a command line flag.
This commit is contained in:
Olaoluwa Osuntokun 2016-09-16 17:23:37 -07:00
parent 255b1b7727
commit b264ba198f
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
3 changed files with 423 additions and 0 deletions

@ -8,4 +8,7 @@ var (
ErrNoActiveChannels = fmt.Errorf("no active channels exist")
ErrChannelNoExist = fmt.Errorf("this channel does not exist")
ErrNoPastDeltas = fmt.Errorf("channel has no recorded deltas")
ErrInvoiceNotFound = fmt.Errorf("unable to locate invoice")
ErrDuplicateInvoice = fmt.Errorf("invoice with payment hash already exists")
)

76
channeldb/invoice_test.go Normal file

@ -0,0 +1,76 @@
package channeldb
import (
"reflect"
"testing"
"time"
"github.com/btcsuite/fastsha256"
"github.com/davecgh/go-spew/spew"
"github.com/roasbeef/btcutil"
)
func TestInvoiceWorkflow(t *testing.T) {
db, cleanUp, err := makeTestDB()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
defer cleanUp()
// Create a fake invoice which we'll use several times in the tests
// below.
fakeInvoice := &Invoice{
CreationDate: time.Now(),
}
copy(fakeInvoice.Memo[:], []byte("memo"))
copy(fakeInvoice.Receipt[:], []byte("recipt"))
copy(fakeInvoice.Terms.PaymentPreimage[:], rev[:])
fakeInvoice.Terms.Value = btcutil.Amount(10000)
// Add the invoice to the database, this should suceed as there aren't
// any existing invoices within the database with the same payment
// hash.
if err := db.AddInvoice(fakeInvoice); err != nil {
t.Fatalf("unable to find invoice: %v", err)
}
// Attempt to retrieve the invoice which was just added to the
// database. It should be found, and the invoice returned should be
// identical to the one created above.
paymentHash := fastsha256.Sum256(fakeInvoice.Terms.PaymentPreimage[:])
dbInvoice, err := db.LookupInvoice(paymentHash)
if err != nil {
t.Fatalf("unable to find invoice: %v", err)
}
if !reflect.DeepEqual(fakeInvoice, dbInvoice) {
t.Fatalf("invoice fetched from db doesn't match original %v vs %v",
spew.Sdump(fakeInvoice), spew.Sdump(dbInvoice))
}
// Settle the invoice, the versin retreived from the database should
// now have the settled bit toggle to true.
if err := db.SettleInvoice(paymentHash); err != nil {
t.Fatalf("unable to settle invoice: %v", err)
}
dbInvoice2, err := db.LookupInvoice(paymentHash)
if err != nil {
t.Fatalf("unable to fetch invoice: %v", err)
}
if !dbInvoice2.Terms.Settled {
t.Fatalf("invoice should now be settled but isn't")
}
// Attempt to insert generated above again, this should fail as
// duplicates are rejected by the processing logic.
if err := db.AddInvoice(fakeInvoice); err != ErrDuplicateInvoice {
t.Fatalf("invoice insertion should fail due to duplication, "+
"instead %v", err)
}
// Attempt to look up a non-existant invoice, this should also fail but
// with a "not found" error.
var fakeHash [32]byte
if _, err := db.LookupInvoice(fakeHash); err != ErrInvoiceNotFound {
t.Fatalf("lookup should have failed, instead %v", err)
}
}

344
channeldb/invoices.go Normal file

@ -0,0 +1,344 @@
package channeldb
import (
"bytes"
"io"
"time"
"github.com/boltdb/bolt"
"github.com/btcsuite/fastsha256"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
)
var (
// invoiceBucket is the name of the bucket within the database that
// stores all data related to invoices no matter their final state.
// Within the invoice bucket, each invoice is keyed by its invoice ID
// which is a monotonically increasing uint32.
invoiceBucket = []byte("invoices")
// numInvoicesKey is the name of key which houses the auto-incrementing
// invoice ID which is essentially used as a primary key. With each
// invoice inserted, the primary key is incremented by one. Within the
// above bucket invoices are uniquely identified by the invoice ID.
numInvoicesKey = []byte("nik")
// paymentHashIndexBucket is the name of the sub-bucket within the
// invoiceBucket which indexes all invoices by their payment hash. The
// payment hash is the sha256 of the invoice's payment preimage. This
// index is used to detect duplicates, and also to provide a fast path
// for looking up incoming HTLC's to determine if we're able to settle
// them fully.
paymentHashIndexBucket = []byte("paymenthashes")
)
const (
// MaxMemoSize is maximum size of the memo field within invoices stored
// in the database.
MaxMemoSize = 1024
// MaxReceiptSize is the maximum size of the payment receipt stored
// within the database along side incoming/outgoing invoices.
MaxReceiptSize = 1024
)
// Invoice is a payment invoice generated by a payee in order to request
// payment for some good or service. The inclusion of invoices within Lightning
// creates a payment work flow for merchants very similar to that of the
// existing financial system within PayPal, etc. Invoices are added to the
// database when a payment is requested, then can be settled manually once the
// payment is received at the upper layer. For record keeping purposes,
// invoices are never deleted from the database, instead a bit is toggled
// denoting the invoice has been fully settled. Within the database, all
// invoices must have a unique payment hash which is generated by taking the
// sha256 of the payment
// preimage.
type Invoice struct {
// Memo is an optional memo to be stored along side an invoice. The
// memo may contain further details pertaining to the invoice itself,
// or any other message which fits within the size constraints.
Memo [MaxMemoSize]byte
// Receipt is an optional field dedicated for storing a
// cryptographically binding receipt of payment.
//
// TODO(roasbeef): document scheme.
Receipt [MaxReceiptSize]byte
// CreationDate is the exact time the invoice was created.
CreationDate time.Time
// Terms are the contractual payment terms of the invoice. Once
// all the terms have been satisfied by the payer, then the invoice can
// be considered fully fulfilled.
//
// TODO(roasbeef): later allow for multiple terms to fulfill the final
// invoice: payment fragmentation, etc.
Terms ContractTerm
}
// ContractTerm is a companion struct to the Invoice struct. This struct houses
// the necessary conditions required before the invoice can be considered fully
// settled by the payee.
type ContractTerm struct {
// PaymentPreimage is the preimage which is to be revealed in the
// occasion that an HTLC paying to the hash of this preimage is
// extended.
PaymentPreimage [32]byte
// Value is the expected amount to be payed to an HTLC which can be
// satisfied by the above preimage.
Value btcutil.Amount
// Settled indicates if this particular contract term has been fully
// settled by the payer.
Settled bool
}
// AddInvoice inserts the targeted invoice into the database. If the invoice
// has *any* payment hashes which already exists within the database, then the
// insertion will be aborted and rejected due to the strict policy banning any
// duplicate payment hashes.
func (d *DB) AddInvoice(i *Invoice) error {
return d.store.Update(func(tx *bolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(paymentHashIndexBucket)
if err != nil {
return err
}
// Ensure that an invoice an identical payment hash doesn't
// already exist within the index.
paymentHash := fastsha256.Sum256(i.Terms.PaymentPreimage[:])
if invoiceIndex.Get(paymentHash[:]) != nil {
return ErrDuplicateInvoice
}
// If the current running payment ID counter hasn't yet been
// created, then create it now.
var invoiceNum uint32
invoiceCounter := invoices.Get(numInvoicesKey)
if invoiceCounter == nil {
var scratch [4]byte
byteOrder.PutUint32(scratch[:], invoiceNum)
if err := invoices.Put(numInvoicesKey, scratch[:]); err != nil {
return nil
}
} else {
invoiceNum = byteOrder.Uint32(invoiceCounter)
}
// index from payment hash to ID?
return putInvoice(invoices, invoiceIndex, i, invoiceNum)
})
}
// LookupInvoice attempts to look up an invoice according to it's 32 byte
// payment hash. In an invoice which can settle the HTLC identified by the
// passed payment hash isnt't found, then an error is returned. Otherwise, the
// full invoice is returned. Before setting the incoming HTLC, the values
// SHOULD be checked to ensure the payer meets the agreed upon contractual
// terms of the payment.
func (d *DB) LookupInvoice(paymentHash [32]byte) (*Invoice, error) {
var invoice *Invoice
err := d.store.View(func(tx *bolt.Tx) error {
invoices := tx.Bucket(invoiceBucket)
if invoices == nil {
return ErrInvoiceNotFound
}
invoiceIndex := invoices.Bucket(paymentHashIndexBucket)
if invoiceIndex == nil {
return ErrInvoiceNotFound
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(paymentHash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
// An invoice matching the payment hash has been found, so
// retrieve the record of the invoice itself.
i, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return err
}
invoice = i
return nil
})
if err != nil {
return nil, err
}
return invoice, nil
}
// SettleInvoice attempts to mark an invoice corresponding to the passed
// payment hash as fully settled. If an invoice matching the passed payment
// hash doesn't existing within the database, then the action will fail with a
// "not found" error.
func (d *DB) SettleInvoice(paymentHash [32]byte) error {
return d.store.Update(func(tx *bolt.Tx) error {
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
if err != nil {
return err
}
invoiceIndex, err := invoices.CreateBucketIfNotExists(paymentHashIndexBucket)
if err != nil {
return err
}
// Check the invoice index to see if an invoice paying to this
// hash exists within the DB.
invoiceNum := invoiceIndex.Get(paymentHash[:])
if invoiceNum == nil {
return ErrInvoiceNotFound
}
return settleInvoice(invoices, invoiceNum)
})
}
func putInvoice(invoices *bolt.Bucket, invoiceIndex *bolt.Bucket,
i *Invoice, invoiceNum uint32) error {
// Create the invoice key which is just the big-endian representation
// of the invoice number.
var invoiceKey [4]byte
byteOrder.PutUint32(invoiceKey[:], invoiceNum)
// Increment the num invoice counter index so the next invoice bares
// the proper ID.
var scratch [4]byte
invoiceCounter := invoiceNum + 1
byteOrder.PutUint32(scratch[:], invoiceCounter)
if err := invoices.Put(numInvoicesKey, scratch[:]); err != nil {
return err
}
// Add the payment hash to the invoice index. This'll let us quickly
// identify if we can settle an incoming payment, and also to possibly
// allow a single invoice to have multiple payment installations.
paymentHash := fastsha256.Sum256(i.Terms.PaymentPreimage[:])
if err := invoiceIndex.Put(paymentHash[:], invoiceKey[:]); err != nil {
return err
}
// Finally, serialize the invoice itself to be written to the disk.
var buf bytes.Buffer
if err := serializeInvoice(&buf, i); err != nil {
return nil
}
return invoices.Put(invoiceKey[:], buf.Bytes())
}
func serializeInvoice(w io.Writer, i *Invoice) error {
if _, err := w.Write(i.Memo[:]); err != nil {
return err
}
if _, err := w.Write(i.Receipt[:]); err != nil {
return err
}
birthBytes, err := i.CreationDate.MarshalBinary()
if err != nil {
return err
}
if err := wire.WriteVarBytes(w, 0, birthBytes); err != nil {
return err
}
if _, err := w.Write(i.Terms.PaymentPreimage[:]); err != nil {
return err
}
var scratch [8]byte
byteOrder.PutUint64(scratch[:], uint64(i.Terms.Value))
if _, err := w.Write(scratch[:]); err != nil {
return err
}
var settleByte [1]byte
if i.Terms.Settled {
settleByte[0] = 1
}
if _, err := w.Write(settleByte[:]); err != nil {
return err
}
return nil
}
func fetchInvoice(invoiceNum []byte, invoices *bolt.Bucket) (*Invoice, error) {
invoiceBytes := invoices.Get(invoiceNum)
if invoiceBytes == nil {
return nil, ErrInvoiceNotFound
}
invoiceReader := bytes.NewReader(invoiceBytes)
return deserializeInvoice(invoiceReader)
}
func deserializeInvoice(r io.Reader) (*Invoice, error) {
invoice := &Invoice{}
// TODO(roasbeef): use read full everywhere
if _, err := io.ReadFull(r, invoice.Memo[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, invoice.Receipt[:]); err != nil {
return nil, err
}
birthBytes, err := wire.ReadVarBytes(r, 0, 300, "birth")
if err != nil {
return nil, err
}
if err := invoice.CreationDate.UnmarshalBinary(birthBytes); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, invoice.Terms.PaymentPreimage[:]); err != nil {
return nil, err
}
var scratch [8]byte
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return nil, err
}
invoice.Terms.Value = btcutil.Amount(byteOrder.Uint64(scratch[:]))
var settleByte [1]byte
if _, err := io.ReadFull(r, settleByte[:]); err != nil {
return nil, err
}
if settleByte[0] == 1 {
invoice.Terms.Settled = true
}
return invoice, nil
}
func settleInvoice(invoices *bolt.Bucket, invoiceNum []byte) error {
invoice, err := fetchInvoice(invoiceNum, invoices)
if err != nil {
return err
}
invoice.Terms.Settled = true
var buf bytes.Buffer
if err := serializeInvoice(&buf, invoice); err != nil {
return nil
}
return invoices.Put(invoiceNum[:], buf.Bytes())
}