+package logic
+
+// main transfer logic
+
+import (
+ "bytes"
+ "crypto/openpgp"
+ "epoint/document"
+ "epoint/store"
+ "fmt"
+ "time"
+)
+
+// TODO: do in docs?
+const IntLimit = 1e15
+
+var db *store.Conn
+
+func AddKeys(d []byte) (err error) {
+ entities, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(d))
+ if err != nil {
+ return
+ }
+ // TODO: allow multiple key uploads at once?
+ if len(entities) > 100 {
+ err = fmt.Errorf("expected at most 100 keys; got %d", len(entities))
+ return
+ }
+ for _, e := range entities {
+ // TODO: various checks..
+ b := new(bytes.Buffer)
+ err = e.Serialize(b)
+ if err != nil {
+ return
+ }
+ fpr := fmt.Sprintf("%040X", e.PrimaryKey.Fingerprint)
+ err = db.Set("key", fpr, b.Bytes())
+ if err != nil {
+ return
+ }
+ err = db.Append("fprlist/64", fpr[len(fpr)-16:], []byte(fpr))
+ if err != nil {
+ return
+ }
+ err = db.Append("fprlist/32", fpr[len(fpr)-8:], []byte(fpr))
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+func CertByDraft(draftid string) (d []byte, err error) {
+ certid, err := db.Get("certid/debit", draftid)
+ if err != nil {
+ // TODO: we have the draft but the cert is not ready
+ return
+ }
+ d, err = db.Get("cert", string(certid))
+ if err != nil {
+ // shouldn't happen, cert is not available
+ return
+ }
+ return
+}
+
+func CertByDebitCert(debitid string) (d []byte, err error) {
+ creditid, err := db.Get("certid/credit", debitid)
+ if err != nil {
+ // TODO: we have the debit cert but the credit cert is not ready
+ return
+ }
+ d, err = db.Get("cert", string(creditid))
+ if err != nil {
+ // shouldn't happen, cert is not available
+ return
+ }
+ return
+}
+
+// parse clear signed draft and verify it
+func ParseDraft(d []byte) (draft *document.Draft, draftid string, err error) {
+ iv, signed, err := document.Parse(d)
+ if err != nil {
+ return
+ }
+ draft, ok := iv.(*document.Draft)
+ if !ok {
+ err = fmt.Errorf("ParseDraft: expected a draft docuent")
+ return
+ }
+ draftid = document.Id(signed)
+/*
+ k, err := db.Get("key", draft.Drawer)
+ if err != nil {
+ return
+ }
+ kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k))
+ if err != nil {
+ // internal error: pubkey cannot be parsed
+ return
+ }
+ cleaned, err = document.Verify(signed, kr)
+ if err != nil {
+ return
+ }
+ // TODO: verify issuer
+ _, err = db.Get("key", draft.Beneficiary)
+ if err != nil {
+ return
+ }
+*/
+ // TODO: do various format checks (AuthorizedBy check etc)
+ if draft.Amount <= 0 || draft.Amount >= IntLimit {
+ err = fmt.Errorf("draft amount is invalid: %d", draft.Amount)
+ return
+ }
+ return
+}
+
+func ParseDebitCert(d []byte) (cert *document.DebitCert, certid string, err error) {
+ iv, signed, err := document.Parse(d)
+ if err != nil {
+ return
+ }
+ cert, ok := iv.(*document.DebitCert)
+ if !ok {
+ err = fmt.Errorf("ParseDebitCert: expected a debit docuent")
+ return
+ }
+/*
+ // TODO: keep our key at hand
+ k, err := db.Get("key", cert.AuthorizedBy)
+ if err != nil {
+ return
+ }
+ kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k))
+ if err != nil {
+ // internal error: pubkey cannot be parsed
+ return
+ }
+ // must clean up to make sure the hash is ok
+ cleaned, err = document.Verify(signed, kr)
+ if err != nil {
+ return
+ }
+*/
+ certid = document.Id(signed)
+ return
+}
+
+func NewDebitCert(draftid string, draft *document.Draft) (*document.DebitCert, error) {
+ cert := new(document.DebitCert)
+ cert.Holder = draft.Drawer
+ cert.Date = time.Seconds()
+ cert.Denomination = "epoint"
+ cert.Issuer = draft.Issuer
+ cert.AuthorizedBy = draft.AuthorizedBy
+ cert.Difference = -draft.Amount
+ cert.Draft = draftid
+ cert.Beneficiary = draft.Beneficiary
+
+ oid, err := db.Get("certid/last", draft.Drawer)
+ oldcertid := string(oid)
+ if err != nil {
+ // first cert: drawer is issuer
+ if draft.Drawer != draft.Issuer {
+ return nil, fmt.Errorf("drawer must be the issuer when drawing an empty account")
+ }
+ cert.Serial = 1
+ cert.Balance = cert.Difference
+ cert.LastDebitSerial = 0
+ cert.LastCreditSerial = 0
+ } else {
+ d, err := db.Get("cert", oldcertid)
+ if err != nil {
+ return nil, err
+ }
+ iv, _, err := document.Parse(d)
+ if err != nil {
+ // internal error
+ return nil, err
+ }
+ // TODO: this is a hack
+ oldcert, err := document.ToCert(iv)
+ // TODO: sanity checks? oldcert.Holder == draft.Drawer
+ cert.Serial = oldcert.Serial + 1
+ cert.Balance = oldcert.Balance + cert.Difference
+ if cert.Balance <= -IntLimit {
+ return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance)
+ }
+ if oldcert.Balance > 0 && cert.Balance < 0 {
+ return nil, fmt.Errorf("insufficient funds: %d", oldcert.Balance)
+ }
+ cert.LastDebitSerial = oldcert.LastDebitSerial
+ cert.LastCreditSerial = oldcert.LastCreditSerial
+ if oldcert.IsDebit {
+ cert.LastDebitSerial = oldcert.Serial
+ } else {
+ cert.LastCreditSerial = oldcert.Serial
+ }
+ cert.LastCert = &oldcertid
+ }
+ return cert, nil
+}
+
+func NewCreditCert(draftid string, draft *document.Draft, dcertid string, dcert *document.DebitCert) (*document.CreditCert, error) {
+ cert := new(document.CreditCert)
+ // TODO: get from old cert instead?
+ cert.Holder = dcert.Beneficiary
+ cert.Date = time.Seconds()
+ // TODO: get these from the cert holder pubkey
+ cert.Denomination = "epoint"
+ cert.Issuer = draft.Issuer
+ cert.AuthorizedBy = dcert.AuthorizedBy // TODO: draft vs dcert vs serverside decision
+ cert.Difference = -dcert.Difference
+ cert.Draft = draftid
+ cert.Drawer = dcert.Holder
+ cert.DebitCert = dcertid
+
+ oid, err := db.Get("certid/last", dcert.Beneficiary)
+ oldcertid := string(oid)
+ if err != nil {
+ // this is the first cert
+ cert.Serial = 1
+ cert.Balance = cert.Difference
+ cert.LastDebitSerial = 0
+ cert.LastCreditSerial = 0
+ } else {
+ d, err := db.Get("cert", oldcertid)
+ if err != nil {
+ // internal error
+ return nil, err
+ }
+ iv, _, err := document.Parse(d)
+ if err != nil {
+ // internal error
+ return nil, err
+ }
+ // TODO: this is a hack
+ oldcert, err := document.ToCert(iv)
+ if err != nil {
+ // internal error
+ return nil, err
+ }
+ cert.Serial = oldcert.Serial + 1
+ cert.Balance = oldcert.Balance + cert.Difference
+ if cert.Balance >= IntLimit {
+ return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance)
+ }
+ cert.LastDebitSerial = oldcert.LastDebitSerial
+ cert.LastCreditSerial = oldcert.LastCreditSerial
+ if oldcert.IsDebit {
+ cert.LastDebitSerial = oldcert.Serial
+ } else {
+ cert.LastCreditSerial = oldcert.Serial
+ }
+ cert.LastCert = &oldcertid
+ }
+ return cert, nil
+}
+
+// TODO: draft ref
+func EvalDraft(d []byte, sk *openpgp.Entity) (r []byte, err error) {
+ draft, draftid, err := ParseDraft(d)
+ if err != nil {
+ return
+ }
+ _, err = db.Get("draft", draftid)
+ if err == nil {
+ // found
+ return CertByDraft(draftid)
+ }
+ // if draft is ok we save it
+ err = db.Set("draft", draftid, d)
+ if err != nil {
+ // internal error
+ return
+ }
+
+ // debit cert
+ cert, err := NewDebitCert(draftid, draft)
+ if err != nil {
+ return
+ }
+ r, signed, err := document.Format(cert, sk)
+ certid := document.Id(signed)
+ err = db.Set("cert", certid, r)
+ if err != nil {
+ // internal error
+ return
+ }
+ err = db.Set("certid/debit", draftid, []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ err = db.Set("certid/last", cert.Holder, []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ // TODO: append?
+ err = db.Set("certid/all", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ return
+}
+
+func EvalDebitCert(d []byte, sk *openpgp.Entity) (r []byte, err error) {
+ dcert, dcertid, err := ParseDebitCert(d)
+ if err != nil {
+ return
+ }
+ r, err = CertByDebitCert(dcertid)
+ if err == nil {
+ // found
+ return
+ }
+ // TODO: we only need the draft to know the issuer (+beneficiary)
+ // it should be in the pubkey
+ d, err = db.Get("draft", dcert.Draft)
+ if err != nil {
+ // internal error
+ return
+ }
+ iv, _, err := document.Parse(d)
+ if err != nil {
+ // internal error
+ return
+ }
+ draft, ok := iv.(*document.Draft)
+ if !ok {
+ // internal error
+ err = fmt.Errorf("EvalDebitCert: expected draft from internal db")
+ return
+ }
+
+ // credit side
+ // TODO: check pubkey etc
+ cert, err := NewCreditCert(dcert.Draft, draft, dcertid, dcert)
+ if err != nil {
+ // internal error
+ return
+ }
+ r, signed, err := document.Format(cert, sk)
+ if err != nil {
+ // internal error
+ return
+ }
+ certid := document.Id(signed)
+ err = db.Set("cert", certid, r)
+ if err != nil {
+ // internal error
+ return
+ }
+ err = db.Set("certid/credit", dcertid, []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ err = db.Set("certid/last", cert.Holder, []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ // TODO: append?
+ err = db.Set("certid/all", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid))
+ if err != nil {
+ // internal error
+ return
+ }
+ return
+}
+
+func Init() (err error) {
+ db, err = store.Open("teststore")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("key")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("cert")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("draft")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("certid/credit")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("certid/debit")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("certid/last")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("certid/all")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("fprlist/64")
+ if err != nil {
+ return
+ }
+ err = db.Ensure("fprlist/32")
+ if err != nil {
+ return
+ }
+ return
+}