package rootparticipation import ( "bytes" "context" "encoding/json" "errors" "io" "os" "strings" "github.com/carlosframework/platform/internal/rootstate" ) const Namespace = "/var/lib/carlos-claims/authority/exec" var ErrUnavailable = errors.New("root participation unavailable") type Census struct { Schema int Box, Enrollment string Migration, Revision, LaunchRevision uint64 Complete, NeverEnrolled bool Pending string } type Ledger struct { State rootstate.Store Box, Enrollment string VerifyLaunches func(context.Context, uint64) error ExcludeLaunches func(context.Context, func(context.Context) error) error } func Decode(raw []byte, v any) error { if len(raw) > rootstate.MaxRecord { return ErrUnavailable } d := json.NewDecoder(bytes.NewReader(raw)) d.DisallowUnknownFields() if err := d.Decode(v); err != nil { return err } if d.Decode(new(any)) != io.EOF { return ErrUnavailable } return nil } func (l Ledger) Census() (Census, error) { return l.readCensus(false) } func (l Ledger) RecoveryCensus() (Census, error) { return l.readCensus(true) } func (l Ledger) readCensus(recovery bool) (Census, error) { var c Census if l.State == nil { return c, ErrUnavailable } raw, e := l.State.Read("census.json") if e != nil { return c, e } if Decode(raw, &c) != nil || c.Schema != 1 || c.Revision == 0 || c.Box == "" || c.Enrollment == "" || !c.Complete || (c.Pending != "" && (!recovery || c.Pending != "acquisition")) || (c.Migration == 0 && !c.NeverEnrolled) || c.LaunchRevision == 0 || (l.Box != "" && c.Box != l.Box) || (l.Enrollment != "" && c.Enrollment != l.Enrollment) { return c, ErrUnavailable } return c, nil } func (l Ledger) writeCensus(c Census) error { raw, e := json.Marshal(c) if e != nil { return e } return l.State.Write("census.json", raw) } func (l Ledger) Acquire(ctx context.Context, commit func() error) error { if l.State == nil || commit == nil { return ErrUnavailable } unlock, e := l.lockTransaction(ctx) if e != nil { return e } defer unlock() c, e := l.Census() if e != nil { return e } if c.NeverEnrolled { return ErrUnavailable } if _, e = l.State.Read("update.json"); !errors.Is(e, os.ErrNotExist) { return ErrUnavailable } c.Pending = "acquisition" c.Revision++ if e = l.writeCensus(c); e != nil { return e } e = commit() c.Pending = "" return errors.Join(e, l.writeCensus(c)) } func (l Ledger) idle(ctx context.Context, c Census) error { if l.VerifyLaunches == nil { return ErrUnavailable } names, e := l.State.Names() if e != nil { return e } for _, name := range names { if name == "census.json" || name == "update.json" || strings.HasSuffix(name, ".lock") || strings.HasSuffix(name, ".live") { continue } if !strings.HasSuffix(name, ".json") { return ErrUnavailable } raw, e := l.State.Read(name) if e != nil { return e } var r ExecutionRecord if Decode(raw, &r) != nil || r.Schema != 1 || !r.Request.Valid() || r.Request.Host+".json" != name || r.Authority.Box != c.Box || r.Authority.Enrollment != c.Enrollment || r.Authority.Migration != c.Migration || r.Status != "settled" || r.Token == "" || r.Replay == nil { return ErrUnavailable } } return l.VerifyLaunches(ctx, c.LaunchRevision) } func (l Ledger) Idle(ctx context.Context) error { if l.State == nil { return ErrUnavailable } unlock, e := l.lockTransaction(ctx) if e != nil { return e } defer unlock() c, e := l.Census() if e != nil { return e } return l.idle(ctx, c) } func (l Ledger) BeginUpdate(ctx context.Context, id string) error { return l.BeginUpdateWith(ctx, id, nil) } func (l Ledger) BeginUpdateWith(ctx context.Context, id string, commit func() error) error { if l.State == nil || !validID(id) { return ErrUnavailable } unlock, e := l.lockTransaction(ctx) if e != nil { return e } defer unlock() c, e := l.Census() if e != nil { return e } if _, e = l.State.Read("update.json"); !errors.Is(e, os.ErrNotExist) { return ErrUnavailable } raw, _ := json.Marshal(struct { Schema int ID string Revision uint64 }{1, id, c.Revision}) if e = l.State.Write("update.json", raw); e != nil { return e } if e = l.idle(ctx, c); e != nil { return errors.Join(e, l.State.Remove("update.json")) } if commit != nil { return commit() } return nil } func (l Ledger) EndUpdateWith(ctx context.Context, id string, commit func() error) error { if l.State == nil { return ErrUnavailable } unlock, e := l.lockTransaction(ctx) if e != nil { return e } defer unlock() raw, e := l.State.Read("update.json") if e != nil { return e } var u struct { Schema int ID string Revision uint64 } if Decode(raw, &u) != nil || u.Schema != 1 || u.ID != id { return ErrUnavailable } c, e := l.Census() if e != nil { return e } if c.Revision != u.Revision { return ErrUnavailable } if e = l.idle(ctx, c); e != nil { return e } if commit != nil { if e = commit(); e != nil { return e } } return l.State.Remove("update.json") } // HoldUpdate is the acquisition barrier recovery leaves when it defers a // settlement its census refused: root's update record under id, written // under the transaction lock, which refuses every new acquisition. Unlike // BeginUpdate it runs no idle check, since it only adds a refusal and // starts nothing; EndUpdateWith, with its census, is still the only way // the record goes. An existing record is kept as it is, and a census with // an acquisition pending already refuses acquisitions, so neither writes. func (l Ledger) HoldUpdate(ctx context.Context, id string) error { if l.State == nil || !validID(id) { return ErrUnavailable } unlock, e := l.lockTransaction(ctx) if e != nil { return e } defer unlock() c, e := l.RecoveryCensus() if e != nil { return e } if c.Pending != "" { return nil } if _, e = l.State.Read("update.json"); !errors.Is(e, os.ErrNotExist) { return e } raw, _ := json.Marshal(struct { Schema int ID string Revision uint64 }{1, id, c.Revision}) return l.State.Write("update.json", raw) } func (l Ledger) UpdateID() (string, error) { if l.State == nil { return "", ErrUnavailable } raw, err := l.State.Read("update.json") if err != nil { return "", err } var u struct { Schema int ID string Revision uint64 } if Decode(raw, &u) != nil || u.Schema != 1 || !validID(u.ID) || u.Revision == 0 { return "", ErrUnavailable } return u.ID, nil } func (l Ledger) RecoverAcquisition(ctx context.Context) error { if l.State == nil { return ErrUnavailable } unlock, err := l.lockTransaction(ctx) if err != nil { return err } defer unlock() c, err := l.RecoveryCensus() if err != nil { return err } if c.Pending == "" { return nil } if err := l.idle(context.WithValue(ctx, acquisitionRecoveryKey{}, true), c); err != nil { return err } c.Pending = "" return l.writeCensus(c) } type acquisitionRecoveryKey struct{} func IsAcquisitionRecovery(ctx context.Context) bool { return ctx.Value(acquisitionRecoveryKey{}) == true }