From 2c2ce5fd0179b2118e8dfa5d9de9ca22766b3432 Mon Sep 17 00:00:00 2001 From: Phillip Michelsen Date: Fri, 13 Feb 2026 08:03:07 +0000 Subject: [PATCH] Completed minimal Ledger implementation, first tests in chron-cli --- cmd/chron-cli/main.go | 168 ++++++++++++++++++++++++++++++++---------- core/interfaces.go | 2 +- core/ledger.go | 121 ++++++++++++++++++++++++++++-- go.mod | 10 +++ go.sum | 21 ++++++ 5 files changed, 276 insertions(+), 46 deletions(-) diff --git a/cmd/chron-cli/main.go b/cmd/chron-cli/main.go index d03499b..d8557bd 100644 --- a/cmd/chron-cli/main.go +++ b/cmd/chron-cli/main.go @@ -1,46 +1,140 @@ package main import ( + "context" + "encoding/hex" + "errors" "fmt" - "time" + "sort" "git.michelsen.id/chron/core" - "github.com/google/uuid" - "lukechampine.com/blake3" ) -func main() { - payload := []byte{0, 1, 0, 1} - payloadDigest := blake3.Sum256(payload) - - entryID, err := uuid.NewV7() - entryIDBytes, err := entryID.MarshalBinary() - if err != nil { - return - } - ledgerID, err := uuid.NewV7() - ledgerIDBytes, err := ledgerID.MarshalBinary() - if err != nil { - return - } - payloadID, err := uuid.NewV7() - payloadIDBytes, err := payloadID.MarshalBinary() - if err != nil { - return - } - - entry := core.Entry{ - EntryID: core.EntryID(entryIDBytes), - LedgerID: core.LedgerID(ledgerIDBytes), - Seq: 1, - Timestamp: time.Now(), - PayloadID: core.PayloadID(payloadIDBytes), - PayloadDigest: payloadDigest, - } - - entryHash := core.HashEntry(entry) - entry.EntryHash = entryHash - - fmt.Printf("Entry: %+v\n", entry) - fmt.Printf("EntryHash: %x\n", entryHash) +type InProcEntryStore struct { + entries map[core.EntryID]core.Entry +} + +func (e *InProcEntryStore) Store(ctx context.Context, entry core.Entry) error { + if _, ok := e.entries[entry.EntryID]; ok { + return errors.New("entry already exists") + } + e.entries[entry.EntryID] = entry + return nil +} + +func (e *InProcEntryStore) Load(ctx context.Context, id core.EntryID) (core.Entry, error) { + entry, ok := e.entries[id] + if !ok { + return core.Entry{}, errors.New("entry does not exist") + } + return entry, nil +} + +func (e *InProcEntryStore) Exists(ctx context.Context, id core.EntryID) (bool, error) { + _, ok := e.entries[id] + return ok, nil +} + +func (e *InProcEntryStore) Delete(ctx context.Context, id core.EntryID) error { + if _, ok := e.entries[id]; !ok { + return errors.New("entry does not exist") + } + delete(e.entries, id) + return nil +} + +type InProcReferenceStore struct { + references map[string]core.EntryID +} + +func (r *InProcReferenceStore) Set(ctx context.Context, name string, entryID core.EntryID) error { + r.references[name] = entryID + return nil +} + +func (r *InProcReferenceStore) Get(ctx context.Context, name string) (core.EntryID, bool, error) { + entryID, ok := r.references[name] + if !ok { + return core.EntryID{}, false, nil + } + return entryID, true, nil +} + +func (r *InProcReferenceStore) Delete(ctx context.Context, name string) error { + if _, ok := r.references[name]; !ok { + return errors.New("reference does not exist") + } + delete(r.references, name) + return nil +} + +func main() { + ctx := context.TODO() + + entryStore := InProcEntryStore{ + entries: make(map[core.EntryID]core.Entry), + } + referenceStore := InProcReferenceStore{ + references: make(map[string]core.EntryID), + } + + ledger, err := core.NewLedger(&entryStore, &referenceStore) + if err != nil { + fmt.Println(err) + return + } + + for i := range 500 { + data := fmt.Sprintf("test%d", i) + + err = ledger.Append(ctx, []byte(data)) + if err != nil { + fmt.Println(err) + return + } + } + + entries := make([]core.Entry, 0, len(entryStore.entries)) + for _, e := range entryStore.entries { + entries = append(entries, e) + } + + sort.Slice(entries, func(i, j int) bool { + if entries[i].Timestamp.Equal(entries[j].Timestamp) { + return hex.EncodeToString(entries[i].EntryID[:]) < + hex.EncodeToString(entries[j].EntryID[:]) + } + return entries[i].Timestamp.Before(entries[j].Timestamp) + }) + + fmt.Println("Entries:") + if len(entries) == 0 { + fmt.Println(" (none)") + } else { + for _, e := range entries { + fmt.Printf(" ts=%d id=%s prev=%s payload=%q\n", + e.Timestamp.UnixNano(), + hex.EncodeToString(e.EntryID[:]), + hex.EncodeToString(e.Previous[:]), + e.Payload, + ) + } + } + + // ---- Print references (name -> EntryID hex) ---- + names := make([]string, 0, len(referenceStore.references)) + for name := range referenceStore.references { + names = append(names, name) + } + sort.Strings(names) + + fmt.Println("References:") + if len(names) == 0 { + fmt.Println(" (none)") + } else { + for _, name := range names { + id := referenceStore.references[name] + fmt.Printf(" %s -> %s\n", name, hex.EncodeToString(id[:])) + } + } } diff --git a/core/interfaces.go b/core/interfaces.go index f242360..7f08f7f 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -13,6 +13,6 @@ type EntryStore interface { type ReferenceStore interface { Set(ctx context.Context, name string, entryID EntryID) error - Get(ctx context.Context, name string) (EntryID, error) + Get(ctx context.Context, name string) (EntryID, bool, error) Delete(ctx context.Context, name string) error } diff --git a/core/ledger.go b/core/ledger.go index d075869..c409c2f 100644 --- a/core/ledger.go +++ b/core/ledger.go @@ -1,6 +1,10 @@ package core -import () +import ( + "context" + "errors" + "time" +) type Ledger struct { entryStore EntryStore @@ -14,10 +18,111 @@ func NewLedger(entryStore EntryStore, referenceStore ReferenceStore) (*Ledger, e }, nil } -func (l *Ledger) Append() {} -func (l *Ledger) AppendToReference() {} -func (l *Ledger) Get() {} -func (l *Ledger) GetFromReference() {} -func (l *Ledger) SetHead() {} -func (l *Ledger) SetReference() {} -func (l *Ledger) RemoveReference() {} +func (l *Ledger) Append(ctx context.Context, payload []byte) error { + currentHeadEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD") + if err != nil { + return err + } + + if !ok { + l.referenceStore.Set(ctx, "HEAD", EntryID{}) + currentHeadEntryID = EntryID{} + } + + entryTime := time.Now() + entryID := ComputeEntryID(currentHeadEntryID, entryTime, payload) + + entry := Entry{ + EntryID: entryID, + Previous: currentHeadEntryID, + Timestamp: entryTime, + Payload: payload, + } + + if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID { + panic("EntryID hash mismatch fuckup") + } + + err = l.entryStore.Store(ctx, entry) + if err != nil { + return err + } + + err = l.referenceStore.Set(ctx, "HEAD", entry.EntryID) + if err != nil { + return err + } + + 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, + } + + if ComputeEntryID(entry.Previous, entry.Timestamp, entry.Payload) != entryID { + panic("EntryID hash mismatch fuckup") + } + + err := l.entryStore.Store(ctx, entry) + if err != nil { + return err + } + + return nil +} + +func (l *Ledger) Get(ctx context.Context) (Entry, error) { + headEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD") + if err != nil { + return Entry{}, 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 +} + +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) +} diff --git a/go.mod b/go.mod index 7e74eca..a50bc2c 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,16 @@ go 1.25.5 require github.com/google/uuid v1.6.0 require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sys v0.37.0 // indirect lukechampine.com/blake3 v1.4.1 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.45.0 // indirect ) diff --git a/go.sum b/go.sum index 6d581f3..68fa08e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,27 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs= +modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=