feat(ledger): add file-backed append-only Event Ledger prototype (Go) with verify

This commit is contained in:
shadowmo
2026-02-28 00:06:32 -06:00
parent 6f2a1aa4b1
commit a24aa16237
2 changed files with 142 additions and 0 deletions

124
runtime/ledger/ledger.go Normal file
View File

@@ -0,0 +1,124 @@
package ledger
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"time"
)
type Entry struct {
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
PrevHash string `json:"prev_hash"`
Hash string `json:"hash"`
}
type Ledger struct {
path string
file *os.File
last string
}
func sha256Hex(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func Open(path string) (*Ledger, error) {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
if err != nil {
return nil, err
}
l := &Ledger{path: path, file: f}
if err := l.loadLastHash(); err != nil {
return nil, err
}
return l, nil
}
func (l *Ledger) loadLastHash() error {
// read file line by line, keep last hash
f, err := os.Open(l.path)
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
var last string
for {
var e Entry
if err := dec.Decode(&e); err != nil {
break
}
last = e.Hash
}
l.last = last
return nil
}
func (l *Ledger) Append(eventType string, payload interface{}) (string, error) {
pbytes, err := json.Marshal(payload)
if err != nil {
return "", err
}
e := Entry{
Timestamp: time.Now().UnixNano(),
Type: eventType,
Payload: json.RawMessage(pbytes),
PrevHash: l.last,
}
// compute hash
hb, _ := json.Marshal(struct {
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
PrevHash string `json:"prev_hash"`
}{e.Timestamp, e.Type, e.Payload, e.PrevHash})
e.Hash = sha256Hex(hb)
enc, err := json.Marshal(e)
if err != nil {
return "", err
}
if _, err := l.file.Write(append(enc, '\n')); err != nil {
return "", err
}
l.last = e.Hash
return e.Hash, nil
}
func (l *Ledger) Verify() error {
f, err := os.Open(l.path)
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
var prev string
for {
var e Entry
if err := dec.Decode(&e); err != nil {
break
}
if e.PrevHash != prev {
return fmt.Errorf("broken chain: prev %s != entry.PrevHash %s", prev, e.PrevHash)
}
// recompute
hb, _ := json.Marshal(struct {
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
PrevHash string `json:"prev_hash"`
}{e.Timestamp, e.Type, e.Payload, e.PrevHash})
if sha256Hex(hb) != e.Hash {
return fmt.Errorf("hash mismatch for entry at %d", e.Timestamp)
}
prev = e.Hash
}
return nil
}
func (l *Ledger) Close() error { return l.file.Close() }

View File

@@ -0,0 +1,18 @@
package ledger
import (
"os"
"testing"
)
func TestAppendAndVerify(t *testing.T) {
p := "test_ledger.jsonl"
os.Remove(p)
l, err := Open(p)
if err != nil { t.Fatal(err) }
defer os.Remove(p)
defer l.Close()
if _, err := l.Append("test_event", map[string]string{"k":"v"}); err != nil { t.Fatal(err) }
if _, err := l.Append("test_event2", map[string]string{"a":"b"}); err != nil { t.Fatal(err) }
if err := l.Verify(); err != nil { t.Fatal(err) }
}