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
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[:]))
}
}
}

View File

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

View File

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

10
go.mod
View File

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

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/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=