package assignment import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-sdk/go/keys" ) // The digests here are of values a proposal's ask and a seed were bound to before this package: the fields are written in // the order the first writer wrote them (the generated contract types order theirs by name), so a digest an earlier holder // bound an ask to is the one this computes. Golden values in the tests hold it. type providerForm struct { Node string `json:"node"` Module string `json:"module"` } type placedForm struct { Module string `json:"module"` Ports map[string]int64 `json:"ports,omitempty"` Bindings map[string]providerForm `json:"bindings,omitempty"` MeshWide map[string]any `json:"mesh-wide,omitempty"` HasMesh bool `json:"has-mesh-wide,omitempty"` Layer map[string]any `json:"layer,omitempty"` HasLayer bool `json:"has-layer,omitempty"` MeshValues map[string]any `json:"mesh-wide-values,omitempty"` Values map[string]any `json:"values,omitempty"` } type holdingForm struct { Claim string `json:"claim"` Scope string `json:"scope,omitempty"` Module string `json:"module"` Site string `json:"site,omitempty"` Kind string `json:"kind,omitempty"` } type decidedForm struct { Endpoint string `json:"endpoint,omitempty"` Site string `json:"site,omitempty"` Hub bool `json:"hub,omitempty"` Address string `json:"address,omitempty"` PublicDomain string `json:"public-domain,omitempty"` Account string `json:"account,omitempty"` AccountHome string `json:"account-home,omitempty"` AgentAccount string `json:"agent-account,omitempty"` AgentHome string `json:"agent-account-home,omitempty"` Adopted bool `json:"adopted,omitempty"` Taken []string `json:"taken,omitempty"` Tunnel *tunnelForm `json:"tunnel,omitempty"` PeerNames map[string]string `json:"peer-names,omitempty"` } type tunnelForm struct { Interface string `json:"interface"` Unit string `json:"unit,omitempty"` Config string `json:"config,omitempty"` Port int64 `json:"port,omitempty"` MTU int64 `json:"mtu,omitempty"` Address string `json:"address,omitempty"` Range string `json:"range,omitempty"` PublicKey string `json:"public-key,omitempty"` Peers []tunnelPeerForm `json:"peers,omitempty"` } type tunnelPeerForm struct { PublicKey string `json:"public-key"` Address string `json:"address,omitempty"` } type assignmentsForm struct { V int `json:"v"` Node string `json:"node"` Revision string `json:"revision,omitempty"` Decided decidedForm `json:"decided"` Modules []placedForm `json:"modules"` Pins map[string]providerForm `json:"pins,omitempty"` Holds []holdingForm `json:"holds,omitempty"` } type joiningForm struct { Identity string `json:"identity"` Overlay string `json:"overlay,omitempty"` Sealing string `json:"sealing"` Serving string `json:"serving,omitempty"` } type changeForm struct { Op string `json:"op"` Node string `json:"node"` Module string `json:"module,omitempty"` Ports map[string]int64 `json:"ports,omitempty"` Bindings map[string]providerForm `json:"bindings,omitempty"` Provision string `json:"provision,omitempty"` Provider *providerForm `json:"provider,omitempty"` Claim string `json:"claim,omitempty"` Scope string `json:"scope,omitempty"` Kind string `json:"kind,omitempty"` Site string `json:"site,omitempty"` Field string `json:"field,omitempty"` Value json.RawMessage `json:"value,omitempty"` MeshWide map[string]any `json:"mesh-wide,omitempty"` Layer map[string]any `json:"layer,omitempty"` MeshValues map[string]any `json:"mesh-wide-values,omitempty"` Values map[string]any `json:"values,omitempty"` Whole *assignmentsForm `json:"whole,omitempty"` Decided *decidedForm `json:"decided,omitempty"` Keys *joiningForm `json:"keys,omitempty"` } func providerOf(p seat.Provider) providerForm { return providerForm{Node: p.Node, Module: p.Module} } func providersOf(m map[string]seat.Provider) map[string]providerForm { if len(m) == 0 { return nil } out := make(map[string]providerForm, len(m)) for k, v := range m { out[k] = providerOf(v) } return out } func tunnelOf(t *seat.FoundTunnel) *tunnelForm { if t == nil { return nil } out := &tunnelForm{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.Mtu, Address: t.Address, Range: t.Range, PublicKey: t.PublicKey} for _, p := range t.Peers { out.Peers = append(out.Peers, tunnelPeerForm{PublicKey: p.PublicKey, Address: p.Address}) } return out } func decidedOf(d seat.NodeDecided) decidedForm { return decidedForm{Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address, PublicDomain: d.PublicDomain, Account: d.Account, AccountHome: d.AccountHome, AgentAccount: d.AgentAccount, AgentHome: d.AgentAccountHome, Adopted: d.Adopted, Taken: d.Taken, Tunnel: tunnelOf(d.Tunnel), PeerNames: d.PeerNames} } func assignmentsOf(p seat.Assignments) assignmentsForm { out := assignmentsForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided), Pins: providersOf(p.Pins)} if p.Modules != nil { out.Modules = []placedForm{} } for _, m := range p.Modules { out.Modules = append(out.Modules, placedForm{Module: m.Module, Ports: m.Ports, Bindings: providersOf(m.Bindings), MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values}) } for _, h := range p.Holds { out.Holds = append(out.Holds, holdingForm{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind}) } return out } func changeOf(c seat.ProposalChange) changeForm { out := changeForm{Op: c.Op, Node: c.Node, Module: c.Module, Ports: c.Ports, Bindings: providersOf(c.Bindings), Provision: c.Provision, Claim: c.Claim, Scope: c.Scope, Kind: c.Kind, Site: c.Site, Field: c.Field, MeshWide: c.MeshWide, Layer: c.Layer, MeshValues: c.MeshWideValues, Values: c.Values} if c.Provider != nil { p := providerOf(*c.Provider) out.Provider = &p } if c.Value != nil { out.Value, _ = json.Marshal(c.Value) } if c.Whole != nil { w := assignmentsOf(*c.Whole) out.Whole = &w } if c.Decided != nil { d := decidedOf(*c.Decided) out.Decided = &d } if c.Keys != nil { out.Keys = &joiningForm{Identity: c.Keys.Identity, Overlay: c.Keys.Overlay, Sealing: c.Keys.Sealing, Serving: c.Keys.Serving} } return out } // ExportDigest is the digest of an export's answer, every node's assignments, as the seed proposes it: the same // assignments give the same digest, whatever order they came in. func ExportDigest(assignments map[string]seat.Assignments) string { h := sha256.New() for _, node := range sortedKeys(assignments) { p := assignments[node] p.V = keys.Version // the export was made with the assignments encoded at the keys' version raw, _ := json.Marshal(assignmentsOf(p)) fmt.Fprintf(h, "%s\n%s\n", node, raw) } return "sha256:" + hex.EncodeToString(h.Sum(nil)) } // ChangesDigest is the digest of a proposal's changes, which its authorising ask is bound to (ยง5). func ChangesDigest(p seat.Proposal) string { var changes []changeForm if p.Changes != nil { changes = make([]changeForm, 0, len(p.Changes)) } for _, c := range p.Changes { changes = append(changes, changeOf(c)) } raw, _ := json.Marshal(struct { ID string `json:"id"` Changes []changeForm `json:"changes"` Base map[string]int64 `json:"base"` }{p.ID, changes, p.Base}) sum := sha256.Sum256(raw) return "sha256:" + hex.EncodeToString(sum[:]) }