Completed minimal Ledger implementation, first tests in chron-cli

This commit is contained in:
2026-02-13 08:03:07 +00:00
parent ca1a3e266b
commit 2c2ce5fd01
5 changed files with 276 additions and 46 deletions

View File

@@ -1,46 +1,140 @@
package main package main
import ( import (
"context"
"encoding/hex"
"errors"
"fmt" "fmt"
"time" "sort"
"git.michelsen.id/chron/core" "git.michelsen.id/chron/core"
"github.com/google/uuid"
"lukechampine.com/blake3"
) )
func main() { type InProcEntryStore struct {
payload := []byte{0, 1, 0, 1} entries map[core.EntryID]core.Entry
payloadDigest := blake3.Sum256(payload) }
entryID, err := uuid.NewV7() func (e *InProcEntryStore) Store(ctx context.Context, entry core.Entry) error {
entryIDBytes, err := entryID.MarshalBinary() if _, ok := e.entries[entry.EntryID]; ok {
if err != nil { return errors.New("entry already exists")
return }
} e.entries[entry.EntryID] = entry
ledgerID, err := uuid.NewV7() return nil
ledgerIDBytes, err := ledgerID.MarshalBinary() }
if err != nil {
return func (e *InProcEntryStore) Load(ctx context.Context, id core.EntryID) (core.Entry, error) {
} entry, ok := e.entries[id]
payloadID, err := uuid.NewV7() if !ok {
payloadIDBytes, err := payloadID.MarshalBinary() return core.Entry{}, errors.New("entry does not exist")
if err != nil { }
return return entry, nil
} }
entry := core.Entry{ func (e *InProcEntryStore) Exists(ctx context.Context, id core.EntryID) (bool, error) {
EntryID: core.EntryID(entryIDBytes), _, ok := e.entries[id]
LedgerID: core.LedgerID(ledgerIDBytes), return ok, nil
Seq: 1, }
Timestamp: time.Now(),
PayloadID: core.PayloadID(payloadIDBytes), func (e *InProcEntryStore) Delete(ctx context.Context, id core.EntryID) error {
PayloadDigest: payloadDigest, if _, ok := e.entries[id]; !ok {
} return errors.New("entry does not exist")
}
entryHash := core.HashEntry(entry) delete(e.entries, id)
entry.EntryHash = entryHash return nil
}
fmt.Printf("Entry: %+v\n", entry)
fmt.Printf("EntryHash: %x\n", entryHash) 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[:]))
}
}
} }

View File

@@ -13,6 +13,6 @@ type EntryStore interface {
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, error) Get(ctx context.Context, name string) (EntryID, bool, error)
Delete(ctx context.Context, name string) error Delete(ctx context.Context, name string) error
} }

View File

@@ -1,6 +1,10 @@
package core package core
import () import (
"context"
"errors"
"time"
)
type Ledger struct { type Ledger struct {
entryStore EntryStore entryStore EntryStore
@@ -14,10 +18,111 @@ func NewLedger(entryStore EntryStore, referenceStore ReferenceStore) (*Ledger, e
}, nil }, nil
} }
func (l *Ledger) Append() {} func (l *Ledger) Append(ctx context.Context, payload []byte) error {
func (l *Ledger) AppendToReference() {} currentHeadEntryID, ok, err := l.referenceStore.Get(ctx, "HEAD")
func (l *Ledger) Get() {} if err != nil {
func (l *Ledger) GetFromReference() {} return err
func (l *Ledger) SetHead() {} }
func (l *Ledger) SetReference() {}
func (l *Ledger) RemoveReference() {} 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)
}

10
go.mod
View File

@@ -5,6 +5,16 @@ go 1.25.5
require github.com/google/uuid v1.6.0 require github.com/google/uuid v1.6.0
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // 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 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
) )

21
go.sum
View File

@@ -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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= 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=