From 78f22a8320732fab87340cd4292c3d234c1dd028 Mon Sep 17 00:00:00 2001 From: Phillip Michelsen Date: Mon, 16 Feb 2026 07:36:44 +0000 Subject: [PATCH] Switched to a DAG ledger --- cmd/chron-cli/main.go | 4 +- core/interfaces.go | 12 +- core/ledger.go | 323 ++++++++++++++++++++++++++++++++---------- core/types.go | 22 ++- 4 files changed, 277 insertions(+), 84 deletions(-) diff --git a/cmd/chron-cli/main.go b/cmd/chron-cli/main.go index 2f9f2ea..083634b 100644 --- a/cmd/chron-cli/main.go +++ b/cmd/chron-cli/main.go @@ -133,11 +133,11 @@ func main() { return } - if isZero32(ent.Previous) { + if isZero32(ent.Parents) { break } // Follow the linked list backwards - curID = core.EntryID(ent.Previous) + curID = core.EntryID(ent.Parents) } } diff --git a/core/interfaces.go b/core/interfaces.go index 7f08f7f..7d6f8cf 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -1,18 +1,24 @@ package core -import ( - "context" -) +import "context" type EntryStore interface { Store(ctx context.Context, entry Entry) error Load(ctx context.Context, id EntryID) (Entry, error) Exists(ctx context.Context, id EntryID) (bool, 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 { Set(ctx context.Context, name string, entryID EntryID) error Get(ctx context.Context, name string) (EntryID, bool, 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) } diff --git a/core/ledger.go b/core/ledger.go index c409c2f..8305fd3 100644 --- a/core/ledger.go +++ b/core/ledger.go @@ -1,11 +1,19 @@ package core import ( + "bytes" "context" "errors" + "sort" "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 { entryStore EntryStore referenceStore ReferenceStore @@ -18,111 +26,278 @@ func NewLedger(entryStore EntryStore, referenceStore ReferenceStore) (*Ledger, e }, nil } -func (l *Ledger) Append(ctx context.Context, payload []byte) error { - currentHeadEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD") - if err != nil { - return err - } +func (l *Ledger) Add(ctx context.Context, parents []EntryID, payload []byte) (EntryID, error) { + ps := normalizeParents(parents) - if !ok { - l.referenceStore.Set(ctx, "HEAD", EntryID{}) - currentHeadEntryID = EntryID{} - } + ts := time.Now() + id := ComputeEntryID(ps, ts, payload) - entryTime := time.Now() - entryID := ComputeEntryID(currentHeadEntryID, entryTime, payload) - - entry := Entry{ - EntryID: entryID, - Previous: currentHeadEntryID, - Timestamp: entryTime, + e := Entry{ + EntryID: id, + Parents: ps, + Timestamp: ts, Payload: payload, } - if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID { - panic("EntryID hash mismatch fuckup") + if err := l.VerifyEntry(e); err != nil { + 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 { 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 err + 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 { + 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 } -func (l *Ledger) AppendTo(ctx context.Context, prevEntryID EntryID, payload []byte) error { - entryTime := time.Now() - entryID := ComputeEntryID(prevEntryID, entryTime, payload) - - entry := Entry{ - EntryID: entryID, - Previous: prevEntryID, - Timestamp: entryTime, - Payload: payload, +func (l *Ledger) IsAncestor(ctx context.Context, ancestor, descendant EntryID) (bool, error) { + if ancestor == descendant { + return true, nil } - if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID { - panic("EntryID hash mismatch fuckup") - } - - err := l.entryStore.Store(ctx, entry) + found := false + err := l.WalkAncestors(ctx, []EntryID{descendant}, func(e Entry) bool { + for _, p := range e.Parents { + if p == ancestor { + found = true + return false + } + } + return true + }) if err != nil { - return err + return false, err } - - return nil + return found, nil } -func (l *Ledger) Get(ctx context.Context) (Entry, error) { - headEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD") +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 Entry{}, err + return nil, err } - if !ok { - return Entry{}, errors.New("HEAD not set") - } - - entry, err := l.entryStore.Load(ctx, headEntryID) - if err != nil { - return Entry{}, err - } - - return entry, nil + return out, nil } -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 +func normalizeParents(parents []EntryID) []EntryID { + if len(parents) == 0 { + return nil } - if !ok { - return Entry{}, errors.New("HEAD not set") + ps := make([]EntryID, len(parents)) + copy(ps, parents) + + 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 +} + +func dedupeIDs(ids []EntryID) []EntryID { + if len(ids) == 0 { + return nil } - entry, err := l.entryStore.Load(ctx, referenceEntryID) - if err != nil { - return Entry{}, err + tmp := make([]EntryID, len(ids)) + copy(tmp, ids) + 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 -} - -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) + return out } diff --git a/core/types.go b/core/types.go index dbbf319..048f717 100644 --- a/core/types.go +++ b/core/types.go @@ -1,7 +1,9 @@ package core import ( + "bytes" "encoding/binary" + "sort" "time" "lukechampine.com/blake3" @@ -12,25 +14,35 @@ type EntryID [32]byte type Entry struct { EntryID EntryID - Previous EntryID + Parents []EntryID Timestamp time.Time 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.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 binary.LittleEndian.PutUint64(i64[:], uint64(ts.UTC().UnixNano())) h.Write(i64[:]) - var u32 [4]byte binary.LittleEndian.PutUint32(u32[:], uint32(len(payload))) h.Write(u32[:]) - h.Write(payload) var id EntryID