feature/ledger-prototype #1
124
runtime/ledger/ledger.go
Normal file
124
runtime/ledger/ledger.go
Normal 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() }
|
||||||
18
runtime/ledger/ledger_test.go
Normal file
18
runtime/ledger/ledger_test.go
Normal 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) }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user