package runtime import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "sync" "git.novox.be/novox/mesh-sdk/go/buscalls" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-tools/node-tools/internal/bus" "github.com/novox/mesh-tools/node-tools/internal/launch" "github.com/novox/mesh-tools/node-tools/internal/ledger" ) // A module's ledgers as the tool runner serves them (novox/hq ADR 0597, design 25 § 12.4, § 12.5; issue 1966): opened // for the module whose bundle or whose repair tool asks, from its own bindings alone, with its types and the connection // its history is published on. // LedgerTypes is where the runner reads a ledger's types, by module and local name. Where it reads them from on the // live mesh is not decided (issue 1966: the binding carries no types, and no record names a source); until it is, // the runner refuses to open a ledger and says why, and a test gives the types. type LedgerTypes func(module, local string) ([]catalogue.LedgerType, error) // errTypesUndecided is the refusal of a ledger whose types the runner has no source for. func errTypesUndecided(module, local string) error { return fmt.Errorf("the tool runner cannot open %s's ledger %q: it reads a ledger's types from nowhere yet, since its "+ "binding carries none and no record names their source (issue 1966)", module, local) } // ledgerSource is how the runner opens a module's ledger: its types, and the connection its history and snapshots are // published on (the module's ledger credential, ADR 0597 §9). type ledgerSource struct { types LedgerTypes writer func(module string) (*bus.Conn, error) } // callsInFlight are the tool calls the runtime is serving, each with the module whose bundle serves it and the caller // the runtime proved from the subject the call arrived on (bus.CalledSubject, novox/hq issue 365). type callsInFlight struct { mu sync.Mutex of map[string]callInFlight } type callInFlight struct{ module, caller string } // begin keeps a call of module in flight for caller, under an id no bundle can guess, until end. func (f *callsInFlight) begin(module, caller string) (string, func()) { var raw [16]byte _, _ = rand.Read(raw[:]) id := hex.EncodeToString(raw[:]) f.mu.Lock() if f.of == nil { f.of = map[string]callInFlight{} } f.of[id] = callInFlight{module, caller} f.mu.Unlock() return id, func() { f.mu.Lock() delete(f.of, id) f.mu.Unlock() } } // authorOf is the author an apply of module naming call is stamped with (ADR 0597 §7): the module's account, and the // caller the runtime proved for that call; naming no call, the account alone. A call not in flight, or another // module's, is refused, naming it. Nothing the bundle writes is taken as the author. func (f *callsInFlight) authorOf(module, account, call string) (buscalls.Author, error) { author := buscalls.Author{Account: account} if call == "" { return author, nil } f.mu.Lock() inFlight, ok := f.of[call] f.mu.Unlock() switch { case !ok: return author, fmt.Errorf("the apply names the call %s, which is not a tool call in flight here; an apply names the "+ "call it serves as its tools/call's _meta %s gave it, or none (novox/hq ADR 0597 §7)", call, buscalls.MetaCall) case inFlight.module != module: return author, fmt.Errorf("the apply names the call %s, which is another module's tool call; %s names only its own "+ "(novox/hq ADR 0597 §7)", call, module) } author.Caller = inFlight.caller return author, nil } // Begin keeps one of the module's tool calls in flight for the caller the runtime proved (launch.Bus). func (b *moduleBus) Begin(caller string) (string, func()) { return b.all.calls.begin(b.module, caller) } // ledger opens one of a module's ledgers by its local name. func (c *consumers) ledger(module, local string) (*ledger.Ledger, error) { b, err := c.conn.Binding(module, local, catalogue.StorageLedger) if err != nil { return nil, err } types := c.ledgers.types if types == nil { return nil, errTypesUndecided(module, local) } ts, err := types(module, local) if err != nil { return nil, err } var writer *bus.Conn if c.ledgers.writer != nil { if writer, err = c.ledgers.writer(module); err != nil { return nil, err } } history, state, snapshots := c.conn.LedgerStores(b, writer) // The owning module's authenticated account: the user of the connection its history is published on. account := module if writer != nil && writer.Credential().User != "" { account = writer.Credential().User } return &ledger.Ledger{Module: module, Name: local, Account: account, Types: ts, History: history, State: state, Snapshots: snapshots, Indexes: c.conn.LedgerIndexes(b), Logf: c.logf}, nil } // ledgerCall answers a bundle's ledger call, as the module whose bundle asks. func (b *moduleBus) ledgerCall(method string, params json.RawMessage) (any, error) { var named struct { Ledger string `json:"ledger"` } if err := decode(method, params, &named); err != nil { return nil, err } if method == buscalls.LedgerApply { var r buscalls.ApplyRequest if err := decode(method, params, &r); err != nil { return nil, err } // The call is judged first: a forged one is refused before anything of the ledger is touched. if _, err := b.all.calls.authorOf(b.module, "", r.Call); err != nil { return nil, err } l, err := b.all.ledger(b.module, r.Ledger) if err != nil { return nil, err } author, err := b.all.calls.authorOf(b.module, l.Account, r.Call) if err != nil { return nil, err } return l.Apply(r, author) } l, err := b.all.ledger(b.module, named.Ledger) if err != nil { return nil, err } switch method { case buscalls.LedgerGet: var r buscalls.InstanceRequest _ = json.Unmarshal(params, &r) return l.Get(r) case buscalls.LedgerHistory: var r buscalls.HistoryRequest _ = json.Unmarshal(params, &r) return l.ReadHistory(r) case buscalls.LedgerQuery: var r buscalls.QueryRequest _ = json.Unmarshal(params, &r) return l.Query(r) case buscalls.LedgerRebuild: var r buscalls.RebuildRequest _ = json.Unmarshal(params, &r) return l.Rebuild(r) case buscalls.LedgerReindex: var r buscalls.ReindexRequest _ = json.Unmarshal(params, &r) return l.Reindex(r) case buscalls.LedgerSnapshot: var r buscalls.InstanceRequest _ = json.Unmarshal(params, &r) return l.Snapshot(r) case buscalls.LedgerLag: var r buscalls.LedgerRequest _ = json.Unmarshal(params, &r) return l.Lag(r) } return nil, fmt.Errorf("the runtime answers no %s", method) } // ledgerWatch hands a bundle each change of its ledger's states. func (b *moduleBus) ledgerWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) { var r buscalls.LedgerWatchRequest if err := decode(buscalls.LedgerWatch, params, &r); err != nil { return nil, err } l, err := b.all.ledger(b.module, r.Ledger) if err != nil { return nil, err } return l.Watch(r, func(change buscalls.InstanceChange) error { raw, err := json.Marshal(change) if err != nil { return err } return deliver(raw) }) } // hasLedger is whether a module's membership binds a ledger. func hasLedger(conn *bus.Conn, module string) bool { for _, b := range conn.Bindings(module) { if b.Storage == catalogue.StorageLedger { return true } } return false } // The repair tools the runner serves for every module owning a ledger (design 25 § 12.5), named so no bundle's tool is // taken for one. const ( RepairRebuild = "ledger_rebuild" RepairReindex = "ledger_reindex" RepairSnapshot = "ledger_snapshot" RepairLag = "ledger_lag" ) // The effects a tool carries (novox/hq ADR 0387 §1). const ( EffectReads = "reads" EffectActs = "acts" ) // repairTools are a module's ledger repairs as tools, each limited to its own ledgers: the request's ledger is a local // name of the module whose tool is called. func (c *consumers) repairTools(module string) []launch.Tool { schema := func(props string, required ...string) json.RawMessage { req, _ := json.Marshal(append([]string{"ledger"}, required...)) return json.RawMessage(`{"type":"object","properties":{"ledger":{"type":"string","description":"the ledger's local name"}` + props + `},"required":` + string(req) + `}`) } instance := `,"instance":{"type":"string","description":"one instance, such as decision/0290; every instance when empty"}` run := func(do func(l *ledger.Ledger, args json.RawMessage) (any, error)) func(string, json.RawMessage) (json.RawMessage, error) { return func(_ string, args json.RawMessage) (json.RawMessage, error) { var named struct { Ledger string `json:"ledger"` } if err := json.Unmarshal(args, &named); err != nil || named.Ledger == "" { return nil, errors.New("a repair names the ledger, by its local name") } l, err := c.ledger(module, named.Ledger) if err != nil { return nil, err } answer, err := do(l, args) if err != nil { return nil, err } return json.Marshal(answer) } } return []launch.Tool{ {Name: RepairRebuild, Effect: EffectActs, Input: schema(instance + `,"from":{"type":"string","enum":["history","snapshot"],"description":"derive from the whole history (the default) or from the snapshot and the operations after it"}`), Description: "Derive a ledger instance's state again from its history or its snapshot, and write it and its snapshot (novox/hq ADR 0597 §10).", Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) { var r buscalls.RebuildRequest _ = json.Unmarshal(args, &r) return l.Rebuild(r) })}, {Name: RepairReindex, Effect: EffectActs, Input: schema(`,"index":{"type":"string","description":"the declared index"}`, "index"), Description: "Build a ledger's declared index again from its states, derived from its history (novox/hq ADR 0597 §4, §10).", Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) { var r buscalls.ReindexRequest _ = json.Unmarshal(args, &r) return l.Reindex(r) })}, {Name: RepairSnapshot, Effect: EffectActs, Input: schema(instance), Description: "Rewrite the snapshot of a ledger instance that is behind its state (novox/hq ADR 0597 §8, §10).", Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) { var r buscalls.InstanceRequest _ = json.Unmarshal(args, &r) return l.Snapshot(r) })}, {Name: RepairLag, Effect: EffectReads, Input: schema(""), Description: "Say every ledger instance whose state or snapshot is behind its history, and each declared index behind it (novox/hq ADR 0597 §10).", Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) { var r buscalls.LedgerRequest _ = json.Unmarshal(args, &r) return l.Lag(r) })}, } }