package hostlife import ( "crypto/sha256" "fmt" "hash/fnv" "os" ) var _ = fnv.New32a // rev 1's stand-in hash, kept importable // Two boxes per region (region HA spec 1 §9 L18; claim rev 6.30 §11). // // boxesPerRegion is the world's shape: one box per region, today's model, // where every seed is byte-identical to the one-box baseline (spec §4: a // region with one box behaves exactly as today); or two, where every // region starts with a second enrolled box and the placement, relocation // and termination rules below apply. HOSTLIFE_BOXES=2 selects the two-box // shape for a sweep (mutate.sh inherits it); TestHostLifecycleTwoBox sets // it for its own run. A two-box world draws its extra random values only // after the one-box draws of the same step, so the one-box stream is // untouched. var boxesPerRegion = 1 func init() { if os.Getenv("HOSTLIFE_BOXES") == "2" { boxesPerRegion = 2 } } // regionBoxes is every live box of region r, by id. func (w *world) regionBoxes(r int) []*box { var out []*box for _, b := range w.boxes { if b.Region == r && !b.Destroyed { out = append(out, b) } } return out } // addBox enrolls a second box in region r of a witness world: the secrets // key held, no faults, id nRegions+r (the sweep's second boxes carry the // same ids; replacements are 100 and up). func (w *world) addBox(r int) *box { b := &box{ID: nRegions + r, Region: r, HasKey: true, Enrolled: true, DeleteOnTermination: true} b.forget() w.boxes = append(w.boxes, b) w.logf("witness: box %d enrolled in region %d", b.ID, r) return b } // tieHash is the model's stand-in for claim P5-D2's SHA-256 of // host + "\x00" + box reference: a fixed function of the reference, never // a draw, so the choice is the same on every replay. func tieHash(id int) uint32 { return tieHashFor(host, id) } // tieHashFor is claim P5-D2's tie-break for the named host (rev 4, Astra // R1-8 and 1b-next R18-2): the first four bytes of SHA-256(host + "\x00" + // box reference), so two hostnames on equal loads may choose different // boxes. The mutation of tie-hash-host hashes a constant name, as the // round-18 chooser bug did. func tieHashFor(hostName string, id int) uint32 { if !on("tie-hash-host") { hostName = host } sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%d", hostName, id))) return uint32(sum[0])<<24 | uint32(sum[1])<<16 | uint32(sum[2])<<8 | uint32(sum[3]) } // hostsOn counts the instances of app placed on b (app "" counts every // app): the claimed host on its executor, or on the box its create or // move named. func (w *world) hostsOn(b *box, app string) int { n := 0 if w.h2.Kind != "" && w.h2.Box == b.ID && (app == "" || w.h2.App == app) { n = 1 // the second host, placed by its create (Task 5) } c := w.claim if c == nil || c.Holder == nil || (app != "" && c.Holder.App != app) { return n } switch { case c.Op.Kind == "creating" && c.Op.NewBox == b.ID: return n + 1 case c.Op.Exec == b.ID: return n + 1 } return n } // owesLocal: an open park-local chore names b. func (w *world) owesLocal(c *claim, b *box) bool { if c == nil { return false } for _, o := range c.Obl { if o.Kind == "park-local" && o.Box == b.ID { return true } } return false } // chooseBox is region HA §5.2's choose() (H12, L8; a move's target under // L15) with nothing excluded. func (w *world) chooseBox(r int, app string) *box { return w.chooseBoxExcept(r, app) } // chooseBoxExcept is choose() over the region's eligible boxes minus // except (a relocation's source, its replaced targets): among the // eligible boxes of region r (§2: live, running the claim's executable, // holding the secrets key, not on lost, not owing a park-local for the // host, enrolled: an unenrolled reference is never eligible, HA7; rule // reloc-enrolled-only, Task 3), prefer one running no instance of app, // then the fewest hosts, then the greatest tieHash. With one box in the // region it is that box, as today (spec §4). It is a pure function of the // world; the caller records the choice in the claim's intent and never // substitutes (rule create-choice-once). func (w *world) chooseBoxExcept(r int, app string, except ...int) *box { return w.chooseBoxFor(host, r, app, except...) } // chooseBoxFor is choose() for the named host (its tie-break hashes that // name). The sole-box shortcut, today's behaviour, is the one-box shape's // alone: with region HA on, one remaining candidate is checked like any // other (rev 4, Astra R1-3; rule ha-sole-box-eligibility). func (w *world) chooseBoxFor(hostName string, r int, app string, except ...int) *box { bs := w.regionBoxes(r) if len(bs) <= 1 && (w.perRegion <= 1 || !on("ha-sole-box-eligibility")) { if len(bs) == 0 || contains(except, bs[0].ID) { return nil } return bs[0] } c := w.claim var best *box var bestKey [3]int for _, b := range bs { if contains(except, b.ID) || b.Old || !b.HasKey || (c != nil && contains(c.Lost, b.ID)) { continue } if w.owesLocal(c, b) && (len(except) == 0 || on("reloc-replace-not-owing")) { continue // §2: a box owing a park-local for the host is never chosen (the mutation of reloc-replace-not-owing lifts it for a relocation's target) } if on("reloc-enrolled-only") && !b.Enrolled { continue } key := [3]int{w.hostsOn(b, app), w.hostsOn(b, ""), -int(tieHashFor(hostName, b.ID))} if best == nil || key[0] < bestKey[0] || key[0] == bestKey[0] && (key[1] < bestKey[1] || key[1] == bestKey[1] && key[2] < bestKey[2]) { best, bestKey = b, key } } return best } // adoptionOwner is the box adoption runs on (claim P5-D2, region HA §9 K5): // in the one-box shape it is the region's box, as today; in the two-box // shape exactly one live box must hold the legacy host's local state (its // row or database), and zero or several refuse adoption, even when only // one box of the region is left (placement does not prove ownership). func (w *world) adoptionOwner(r int) *box { bs := w.regionBoxes(r) if w.perRegion <= 1 { if len(bs) == 0 { return nil } return bs[0] } var owner *box for _, b := range bs { holds := b.Row != nil && b.Row.Inc == w.rec.Inc || b.DB != nil && b.DB.Inc == w.rec.Inc if !holds { continue } if owner != nil { return nil // several proven owners: refused } owner = b } return owner } // ownsObligation: b may run chore o. A park-local is its box's alone; a // regional chore is any live box of its region's (claim P6-D5: regional // workers), as it was the region's one box's. func (w *world) ownsObligation(o obligation, b *box) bool { if o.Kind == "park-local" { return w.ownerOf(o) == b } if o.Owner != "" { return o.Owner == boxOwner(b.ID) && !b.Destroyed // box:, the legacy owner form: that box alone, until a fence or a declared loss passes it to the region } return b.Region == o.Region && !b.Destroyed }