Files
ietf-wimse-ect/refimpl/ect/ledger.go
Christian Nennemann f9357fdf88 Add WIMSE ECT reference implementation (Go)
- ect library: create, verify, DAG validation, ledger interface
- In-memory ledger and ECTStore for full ledger mode
- Test vectors and unit tests; two-agent demo (cmd/demo)
- README: document refimpl scope and usage

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 22:05:30 +01:00

108 lines
3.0 KiB
Go

package ect
import (
"errors"
"sync"
"time"
)
// LedgerEntry represents a single ECT record in the audit ledger (Section 9.3).
type LedgerEntry struct {
LedgerSequence int64 `json:"ledger_sequence"`
TaskID string `json:"task_id"`
AgentID string `json:"agent_id"`
Action string `json:"action"`
Parents []string `json:"parents"`
ECTJWS string `json:"ect_jws"`
SignatureVerified bool `json:"signature_verified"`
VerificationTime time.Time `json:"verification_timestamp"`
StoredTime time.Time `json:"stored_timestamp"`
}
// Ledger is the audit ledger interface per Section 9. Append-only; lookup by tid.
type Ledger interface {
// Append records a verified ECT. Returns the new ledger sequence number or error.
Append(ectJWS string, payload *Payload) (seq int64, err error)
// GetByTid returns the payload for the given task ID, or nil.
GetByTid(tid string) *Payload
// Contains returns true if (tid, wid) exists. wid may be empty for global scope.
Contains(tid, wid string) bool
// ECTStore implementation for DAG validation
ECTStore
}
// MemoryLedger is an in-memory, append-only ECT store implementing Ledger and ECTStore.
type MemoryLedger struct {
mu sync.RWMutex
seq int64
byTid map[string]*Payload
bySeq []LedgerEntry
entries []LedgerEntry // full entries for audit
}
// NewMemoryLedger creates an empty in-memory ledger.
func NewMemoryLedger() *MemoryLedger {
return &MemoryLedger{
byTid: make(map[string]*Payload),
bySeq: make([]LedgerEntry, 0),
}
}
// Append implements Ledger.
func (m *MemoryLedger) Append(ectJWS string, payload *Payload) (int64, error) {
if payload == nil {
return 0, nil
}
m.mu.Lock()
defer m.mu.Unlock()
// Uniqueness: tid must not already exist (scoped by wid when present)
wid := payload.Wid
if m.containsLocked(payload.Tid, wid) {
return 0, ErrTaskIDExists
}
m.seq++
entry := LedgerEntry{
LedgerSequence: m.seq,
TaskID: payload.Tid,
AgentID: payload.Iss,
Action: payload.ExecAct,
Parents: append([]string(nil), payload.Par...),
ECTJWS: ectJWS,
SignatureVerified: true,
VerificationTime: time.Now().UTC(),
StoredTime: time.Now().UTC(),
}
m.byTid[payload.Tid] = payload
m.bySeq = append(m.bySeq, entry)
m.entries = append(m.entries, entry)
return m.seq, nil
}
// GetByTid implements ECTStore and Ledger.
func (m *MemoryLedger) GetByTid(tid string) *Payload {
m.mu.RLock()
defer m.mu.RUnlock()
return m.byTid[tid]
}
// Contains implements ECTStore and Ledger.
func (m *MemoryLedger) Contains(tid, wid string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.containsLocked(tid, wid)
}
func (m *MemoryLedger) containsLocked(tid, wid string) bool {
p, ok := m.byTid[tid]
if !ok {
return false
}
if wid == "" {
return true
}
return p.Wid == wid
}
// ErrTaskIDExists is returned when appending an ECT whose tid already exists.
var ErrTaskIDExists = errors.New("ect: task ID already exists in ledger")