package host import ( "context" "crypto/sha256" "encoding/hex" "fmt" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" ) // relocationCompletedHandover is the first serving cold wake after a // relocation completed on this box (rulings 49 and 56; Astra round 2 // finding 6). The predecessor is the probe session S that the target // stopped at relocating/probing with root's writer-session receipt, and // the serving op journals that stop (CompletedRelocation). Nothing is // stopped again and nothing is relabelled: before admission the receipt // is found and verified in this box's guard journal (in place of main's // Wake.Stop, which would ask a serving-shaped re-stop of S); after // admission it is handed to root's ColdHandover.Run as the exact prior, // which Run accepts beside the separately validated successor without // matching their operation ids. type relocationCompletedHandover struct { Guard hostguard.Guard Identity hostbox.IdentityProvider Socket string Box string } // completedPrior reports whether prior is a serving claim whose writer is // the stopped probe session this box completed a relocation with. A // serving claim on this box whose completion record is present but cannot // be read is an error, never "not the completed prior" (absence is never // evidence): routed to main's stop it would re-stop S with a serving-shaped // fence that replaces the probe's. func (h relocationCompletedHandover) completedPrior(prior hostclaim.Observed) (hostclaim.CompletedRelocation, bool, error) { c := prior.Claim if c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindServing || c.Op.Exec != h.Box || c.Holder.WriterBox != h.Box { return hostclaim.CompletedRelocation{}, false, nil } done, ok, err := hostclaim.ReadCompletedRelocation(c) if err != nil { return hostclaim.CompletedRelocation{}, false, err } if !ok || done.Stop.Box != h.Box || done.Stop.Session != c.Holder.Session || done.Stamp != c.Holder.Stamp || done.Generation != c.Holder.Generation { return hostclaim.CompletedRelocation{}, false, nil } return done, true, nil } // receipt is root's completed writer-session stop of S, exactly the one // the serving op journals: fence id, phase and the receipt bytes' digest. func (h relocationCompletedHandover) receipt(ctx context.Context, prior hostclaim.Observed) (hostguard.FenceReceipt, error) { done, ok, err := h.completedPrior(prior) if err != nil { return hostguard.FenceReceipt{}, err } if !ok { return hostguard.FenceReceipt{}, hostbox.ErrChanged } r, err := writerSessionReceipt(ctx, h.Guard, prior.Host, hostguard.Captured{Incarnation: prior.Claim.Holder.Incarnation, Stamp: done.Stamp}, done.Op, h.Box, done.Stop.Session) if err != nil { return hostguard.FenceReceipt{}, err } sum := sha256.Sum256(r.Bytes) if r.Target.FenceID != done.Stop.Receipt || r.Target.Phase != done.Phase || hex.EncodeToString(sum[:]) != done.Stop.ReceiptSHA256 { return hostguard.FenceReceipt{}, fmt.Errorf("%w: session %d's stop under %s is not the one the serving op records", ErrClaimStopReceipt, done.Stop.Session, done.Op) } return r, nil } // writerSessionReceipt is root's completed writer-session stop of session // (hostguard.WriterStopObligation) for op on box, from this box's guard // journal: the one reader of a probe's stop, for the probe stop and for // the serving handover after it. Two different completed stops of the // same session under the same op are a defect of the journal, refused as // main's CompletedColdPredecessor refuses them (ErrFenceTarget): neither // is "the" receipt, and the stop never guesses which to record. func writerSessionReceipt(ctx context.Context, guard hostguard.Guard, host string, captured hostguard.Captured, op, box string, session uint64) (hostguard.FenceReceipt, error) { receipts, err := guard.CompletedWriterStops(ctx, host, captured, op, box) if err != nil { return hostguard.FenceReceipt{}, err } want := hostguard.WriterStopObligation(session) var found hostguard.FenceReceipt for _, r := range receipts { if r.Target.Session != session || r.Target.ObligationID != want { continue } if found.Target.FenceID != "" && found.Target != r.Target { return hostguard.FenceReceipt{}, fmt.Errorf("%w: root completed more than one stop of session %d under %s", hostguard.ErrFenceTarget, session, op) } found = r } if found.Target.FenceID == "" { return hostguard.FenceReceipt{}, fmt.Errorf("%w: root has no completed stop of session %d under %s", ErrClaimStopReceipt, session, op) } return found, nil } // Before replaces the predecessor stop for exactly this prior: the stop // already happened, so its receipt is found and verified, nothing more. func (h relocationCompletedHandover) Before(ctx context.Context, prior hostclaim.Observed) error { r, err := h.receipt(ctx, prior) if err != nil { return err } return verifyRetirementReceipt(ctx, r) } // After is main's cold handover with that receipt as the prior. func (h relocationCompletedHandover) After() func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return coldHandoverWith(h.Identity, h.Socket, h.receipt) }