Switched to a DAG ledger
This commit is contained in:
@@ -133,11 +133,11 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isZero32(ent.Previous) {
|
if isZero32(ent.Parents) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow the linked list backwards
|
// Follow the linked list backwards
|
||||||
curID = core.EntryID(ent.Previous)
|
curID = core.EntryID(ent.Parents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EntryStore interface {
|
type EntryStore interface {
|
||||||
Store(ctx context.Context, entry Entry) error
|
Store(ctx context.Context, entry Entry) error
|
||||||
Load(ctx context.Context, id EntryID) (Entry, error)
|
Load(ctx context.Context, id EntryID) (Entry, error)
|
||||||
Exists(ctx context.Context, id EntryID) (bool, error)
|
Exists(ctx context.Context, id EntryID) (bool, error)
|
||||||
Delete(ctx context.Context, id EntryID) error
|
Delete(ctx context.Context, id EntryID) error
|
||||||
|
|
||||||
|
StoreBatch(ctx context.Context, entries []Entry) error
|
||||||
|
LoadBatch(ctx context.Context, ids []EntryID) ([]Entry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReferenceStore interface {
|
type ReferenceStore interface {
|
||||||
Set(ctx context.Context, name string, entryID EntryID) error
|
Set(ctx context.Context, name string, entryID EntryID) error
|
||||||
Get(ctx context.Context, name string) (EntryID, bool, error)
|
Get(ctx context.Context, name string) (EntryID, bool, error)
|
||||||
Delete(ctx context.Context, name string) error
|
Delete(ctx context.Context, name string) error
|
||||||
|
|
||||||
|
List(ctx context.Context, prefix string) (map[string]EntryID, error)
|
||||||
|
|
||||||
|
SetBatch(ctx context.Context, refs map[string]EntryID) error
|
||||||
|
GetBatch(ctx context.Context, names []string) (map[string]EntryID, error)
|
||||||
}
|
}
|
||||||
|
|||||||
323
core/ledger.go
323
core/ledger.go
@@ -1,11 +1,19 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrHeadNotSet = errors.New("HEAD not set")
|
||||||
|
ErrEntryIDMismatch = errors.New("entry id mismatch")
|
||||||
|
ErrBatchLoadSize = errors.New("LoadBatch returned unexpected number of entries")
|
||||||
|
)
|
||||||
|
|
||||||
type Ledger struct {
|
type Ledger struct {
|
||||||
entryStore EntryStore
|
entryStore EntryStore
|
||||||
referenceStore ReferenceStore
|
referenceStore ReferenceStore
|
||||||
@@ -18,111 +26,278 @@ func NewLedger(entryStore EntryStore, referenceStore ReferenceStore) (*Ledger, e
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Ledger) Append(ctx context.Context, payload []byte) error {
|
func (l *Ledger) Add(ctx context.Context, parents []EntryID, payload []byte) (EntryID, error) {
|
||||||
currentHeadEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD")
|
ps := normalizeParents(parents)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
ts := time.Now()
|
||||||
l.referenceStore.Set(ctx, "HEAD", EntryID{})
|
id := ComputeEntryID(ps, ts, payload)
|
||||||
currentHeadEntryID = EntryID{}
|
|
||||||
}
|
|
||||||
|
|
||||||
entryTime := time.Now()
|
e := Entry{
|
||||||
entryID := ComputeEntryID(currentHeadEntryID, entryTime, payload)
|
EntryID: id,
|
||||||
|
Parents: ps,
|
||||||
entry := Entry{
|
Timestamp: ts,
|
||||||
EntryID: entryID,
|
|
||||||
Previous: currentHeadEntryID,
|
|
||||||
Timestamp: entryTime,
|
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID {
|
if err := l.VerifyEntry(e); err != nil {
|
||||||
panic("EntryID hash mismatch fuckup")
|
return EntryID{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.entryStore.Store(ctx, entry)
|
if err := l.entryStore.Store(ctx, e); err != nil {
|
||||||
|
return EntryID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) Append(ctx context.Context, payload []byte) (EntryID, error) {
|
||||||
|
head, ok, err := l.referenceStore.Get(ctx, "HEAD")
|
||||||
|
if err != nil {
|
||||||
|
return EntryID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var parents []EntryID
|
||||||
|
if ok {
|
||||||
|
parents = []EntryID{head}
|
||||||
|
} else {
|
||||||
|
parents = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := l.Add(ctx, parents, payload)
|
||||||
|
if err != nil {
|
||||||
|
return EntryID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.referenceStore.Set(ctx, "HEAD", id); err != nil {
|
||||||
|
return EntryID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) AppendTo(ctx context.Context, parent EntryID, payload []byte) (EntryID, error) {
|
||||||
|
return l.Add(ctx, []EntryID{parent}, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) Get(ctx context.Context, id EntryID) (Entry, error) {
|
||||||
|
return l.entryStore.Load(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) Exists(ctx context.Context, id EntryID) (bool, error) {
|
||||||
|
return l.entryStore.Exists(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) Verify(ctx context.Context, id EntryID) error {
|
||||||
|
e, err := l.entryStore.Load(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return l.VerifyEntry(e)
|
||||||
|
}
|
||||||
|
|
||||||
err = l.referenceStore.Set(ctx, "HEAD", entry.EntryID)
|
func (l *Ledger) VerifyEntry(e Entry) error {
|
||||||
|
want := ComputeEntryID(e.Parents, e.Timestamp, e.Payload)
|
||||||
|
if want != e.EntryID {
|
||||||
|
return ErrEntryIDMismatch
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) GetRef(ctx context.Context, name string) (EntryID, bool, error) {
|
||||||
|
return l.referenceStore.Get(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) SetRef(ctx context.Context, name string, id EntryID) error {
|
||||||
|
return l.referenceStore.Set(ctx, name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) DeleteRef(ctx context.Context, name string) error {
|
||||||
|
return l.referenceStore.Delete(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) ListRefs(ctx context.Context, prefix string) (map[string]EntryID, error) {
|
||||||
|
return l.referenceStore.List(ctx, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) SetRefs(ctx context.Context, refs map[string]EntryID) error {
|
||||||
|
return l.referenceStore.SetBatch(ctx, refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) GetRefs(ctx context.Context, names []string) (map[string]EntryID, error) {
|
||||||
|
return l.referenceStore.GetBatch(ctx, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) GetHead(ctx context.Context) (EntryID, bool, error) {
|
||||||
|
return l.referenceStore.Get(ctx, "HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) SetHead(ctx context.Context, id EntryID) error {
|
||||||
|
return l.referenceStore.Set(ctx, "HEAD", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) GetHeads(ctx context.Context, prefix string) ([]EntryID, error) {
|
||||||
|
m, err := l.referenceStore.List(ctx, prefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[EntryID]struct{}, len(m))
|
||||||
|
for _, id := range m {
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]EntryID, 0, len(seen))
|
||||||
|
for id := range seen {
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
return bytes.Compare(out[i][:], out[j][:]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) WalkAncestors(ctx context.Context, start []EntryID, fn func(Entry) bool) error {
|
||||||
|
frontier := dedupeIDs(start)
|
||||||
|
visited := make(map[EntryID]struct{}, 1024)
|
||||||
|
|
||||||
|
for len(frontier) > 0 {
|
||||||
|
batchIDs := make([]EntryID, 0, len(frontier))
|
||||||
|
for _, id := range frontier {
|
||||||
|
if _, ok := visited[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[id] = struct{}{}
|
||||||
|
batchIDs = append(batchIDs, id)
|
||||||
|
}
|
||||||
|
if len(batchIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := l.entryStore.LoadBatch(ctx, batchIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(entries) != len(batchIDs) {
|
||||||
|
return ErrBatchLoadSize
|
||||||
|
}
|
||||||
|
|
||||||
|
next := make([]EntryID, 0, len(batchIDs)*2)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if err := l.VerifyEntry(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fn(e) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range e.Parents {
|
||||||
|
next = append(next, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frontier = dedupeIDs(next)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Ledger) AppendTo(ctx context.Context, prevEntryID EntryID, payload []byte) error {
|
func (l *Ledger) IsAncestor(ctx context.Context, ancestor, descendant EntryID) (bool, error) {
|
||||||
entryTime := time.Now()
|
if ancestor == descendant {
|
||||||
entryID := ComputeEntryID(prevEntryID, entryTime, payload)
|
return true, nil
|
||||||
|
|
||||||
entry := Entry{
|
|
||||||
EntryID: entryID,
|
|
||||||
Previous: prevEntryID,
|
|
||||||
Timestamp: entryTime,
|
|
||||||
Payload: payload,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID {
|
found := false
|
||||||
panic("EntryID hash mismatch fuckup")
|
err := l.WalkAncestors(ctx, []EntryID{descendant}, func(e Entry) bool {
|
||||||
|
for _, p := range e.Parents {
|
||||||
|
if p == ancestor {
|
||||||
|
found = true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
err := l.entryStore.Store(ctx, entry)
|
return true
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Ledger) CommonAncestors(ctx context.Context, a, b []EntryID, limit int) ([]EntryID, error) {
|
||||||
|
aSet := make(map[EntryID]struct{}, 1024)
|
||||||
|
|
||||||
|
if err := l.WalkAncestors(ctx, a, func(e Entry) bool {
|
||||||
|
aSet[e.EntryID] = struct{}{}
|
||||||
|
return true
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []EntryID
|
||||||
|
seen := make(map[EntryID]struct{}, 64)
|
||||||
|
|
||||||
|
err := l.WalkAncestors(ctx, b, func(e Entry) bool {
|
||||||
|
if _, ok := aSet[e.EntryID]; ok {
|
||||||
|
if _, dup := seen[e.EntryID]; !dup {
|
||||||
|
seen[e.EntryID] = struct{}{}
|
||||||
|
out = append(out, e.EntryID)
|
||||||
|
if limit > 0 && len(out) >= limit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeParents(parents []EntryID) []EntryID {
|
||||||
|
if len(parents) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Ledger) Get(ctx context.Context) (Entry, error) {
|
ps := make([]EntryID, len(parents))
|
||||||
headEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD")
|
copy(ps, parents)
|
||||||
if err != nil {
|
|
||||||
return Entry{}, err
|
sort.Slice(ps, func(i, j int) bool {
|
||||||
|
return bytes.Compare(ps[i][:], ps[j][:]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
out := ps[:0]
|
||||||
|
var last EntryID
|
||||||
|
for i, p := range ps {
|
||||||
|
if i == 0 || p != last {
|
||||||
|
out = append(out, p)
|
||||||
|
last = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
func dedupeIDs(ids []EntryID) []EntryID {
|
||||||
return Entry{}, errors.New("HEAD not set")
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := l.entryStore.Load(ctx, headEntryID)
|
tmp := make([]EntryID, len(ids))
|
||||||
if err != nil {
|
copy(tmp, ids)
|
||||||
return Entry{}, err
|
sort.Slice(tmp, func(i, j int) bool {
|
||||||
|
return bytes.Compare(tmp[i][:], tmp[j][:]) < 0
|
||||||
|
})
|
||||||
|
out := tmp[:0]
|
||||||
|
var last EntryID
|
||||||
|
for i, id := range tmp {
|
||||||
|
if i == 0 || id != last {
|
||||||
|
out = append(out, id)
|
||||||
|
last = id
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
func (l *Ledger) GetFromReference(ctx context.Context, reference string) (Entry, error) {
|
|
||||||
referenceEntryID, ok, err := l.referenceStore.Get(ctx, reference)
|
|
||||||
if err != nil {
|
|
||||||
return Entry{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return Entry{}, errors.New("HEAD not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := l.entryStore.Load(ctx, referenceEntryID)
|
|
||||||
if err != nil {
|
|
||||||
return Entry{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Ledger) SetHead(ctx context.Context, entryID EntryID) error {
|
|
||||||
return l.referenceStore.Set(ctx, "HEAD", entryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Ledger) SetReference(ctx context.Context, reference string, entryID EntryID) error {
|
|
||||||
return l.referenceStore.Set(ctx, reference, entryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Ledger) RemoveReference(ctx context.Context, reference string) error {
|
|
||||||
return l.referenceStore.Delete(ctx, reference)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lukechampine.com/blake3"
|
"lukechampine.com/blake3"
|
||||||
@@ -12,25 +14,35 @@ type EntryID [32]byte
|
|||||||
type Entry struct {
|
type Entry struct {
|
||||||
EntryID EntryID
|
EntryID EntryID
|
||||||
|
|
||||||
Previous EntryID
|
Parents []EntryID
|
||||||
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
Payload []byte
|
Payload []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func ComputeEntryID(prev EntryID, ts time.Time, payload []byte) EntryID {
|
func ComputeEntryID(parents []EntryID, ts time.Time, payload []byte) EntryID {
|
||||||
h := blake3.New(32, nil)
|
h := blake3.New(32, nil)
|
||||||
|
|
||||||
h.Write(prev[:])
|
ps := make([]EntryID, len(parents))
|
||||||
|
copy(ps, parents)
|
||||||
|
sort.Slice(ps, func(i, j int) bool {
|
||||||
|
return bytes.Compare(ps[i][:], ps[j][:]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
var u32 [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(u32[:], uint32(len(ps)))
|
||||||
|
h.Write(u32[:])
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
h.Write(p[:])
|
||||||
|
}
|
||||||
|
|
||||||
var i64 [8]byte
|
var i64 [8]byte
|
||||||
binary.LittleEndian.PutUint64(i64[:], uint64(ts.UTC().UnixNano()))
|
binary.LittleEndian.PutUint64(i64[:], uint64(ts.UTC().UnixNano()))
|
||||||
h.Write(i64[:])
|
h.Write(i64[:])
|
||||||
|
|
||||||
var u32 [4]byte
|
|
||||||
binary.LittleEndian.PutUint32(u32[:], uint32(len(payload)))
|
binary.LittleEndian.PutUint32(u32[:], uint32(len(payload)))
|
||||||
h.Write(u32[:])
|
h.Write(u32[:])
|
||||||
|
|
||||||
h.Write(payload)
|
h.Write(payload)
|
||||||
|
|
||||||
var id EntryID
|
var id EntryID
|
||||||
|
|||||||
Reference in New Issue
Block a user