Switched to a DAG ledger

This commit is contained in:
2026-02-16 07:36:44 +00:00
parent 4e830bf5d0
commit 78f22a8320
4 changed files with 277 additions and 84 deletions

View File

@@ -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)
} }
} }

View File

@@ -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)
} }

View File

@@ -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 { 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 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 {
err := l.entryStore.Store(ctx, entry) found = true
return false
}
}
return true
})
if err != nil { if err != nil {
return err return false, err
} }
return found, nil
return nil
} }
func (l *Ledger) Get(ctx context.Context) (Entry, error) { func (l *Ledger) CommonAncestors(ctx context.Context, a, b []EntryID, limit int) ([]EntryID, error) {
headEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD") 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 { if err != nil {
return Entry{}, err return nil, err
} }
if !ok { return out, nil
return Entry{}, errors.New("HEAD not set")
}
entry, err := l.entryStore.Load(ctx, headEntryID)
if err != nil {
return Entry{}, err
}
return entry, nil
} }
func (l *Ledger) GetFromReference(ctx context.Context, reference string) (Entry, error) { func normalizeParents(parents []EntryID) []EntryID {
referenceEntryID, ok, err := l.referenceStore.Get(ctx, reference) if len(parents) == 0 {
if err != nil { return nil
return Entry{}, err
} }
if !ok { ps := make([]EntryID, len(parents))
return Entry{}, errors.New("HEAD not set") 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) 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 out
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)
} }

View File

@@ -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