125 lines
2.6 KiB
Go
125 lines
2.6 KiB
Go
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() }
|