diff --git a/channeldb/error.go b/channeldb/error.go index a60876b1..35359864 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -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") ) diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go new file mode 100644 index 00000000..0cf0e0e6 --- /dev/null +++ b/channeldb/invoice_test.go @@ -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) + } +} diff --git a/channeldb/invoices.go b/channeldb/invoices.go new file mode 100644 index 00000000..7d022039 --- /dev/null +++ b/channeldb/invoices.go @@ -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()) +}