package host import ( "context" "fmt" "github.com/carlosframework/platform/internal/hostclaim" ) // RelocationReleaseObligation is the stop request's obligation component // for a relocation's release (ยง5.8 release step 4). A release retires // nothing, so it names no park-local chore; root's FenceAndStop requires // the component, and the receipt carries it so a retirement stop can never // be offered as a release, nor a release as a retirement. const RelocationReleaseObligation = "relocation-release" // StopReceiptReleases is hostclaim.ReleaseAuthority over main's // relocation-release discharge (claim Plan 11): a source's journaled // release counts only once RelocationReleaseReceipts.Complete has verified // root's exact receipt, journaled its key and digest, and discharged the // obligation. The release must cite exactly that key and digest, the claim // must owe this operation no relocation release any more, and the receipt // is verified again here, at the pin, by the region worker (Plan 11 Task // E: the adapter consumes the same signed receipt, never an agent-written // digest alone). That check is main's own: Complete's replay form, which // re-reads the bytes and checks the digest, the canonical form, every // bound field, the roster entry and root's signature under the releasing // box's census key, and writes nothing once the journal holds the key and // digest and no obligation is owed. A verify-only export from main // (RelocationReleaseReceipts.Verify) would replace the replay call. // // Main discharges a relocation-release only for the source in releasing. // A failed target's release in probe-failed has no obligation root will // stop for, so it is refused here: the nested relocation waits, and its // release deadline makes it stuck. type StopReceiptReleases struct { Receipts RelocationReleaseReceipts } var _ hostclaim.ReleaseAuthority = StopReceiptReleases{} func (a StopReceiptReleases) VerifyRelease(ctx context.Context, c hostclaim.Claim, r hostclaim.Release) error { if c.Holder == nil || a.Receipts.Store == nil { return ErrClaimStopUnavailable } if err := releaseDischarged(&c, r.Receipt, r.ReceiptSHA256); err != nil { return err } return verifyDischargedReceipt(ctx, a.Receipts, &c, r.Receipt, r.ReceiptSHA256) } // releaseDischarged checks c journals key and digest as its relocating // operation's discharged release receipt, and owes that operation no // relocation release. func releaseDischarged(c *hostclaim.Claim, key, digest string) error { if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseReleasing || c.Op.ID == "" { return fmt.Errorf("%w: only a source's release in releasing is discharged by root's receipt", ErrClaimStopReceipt) } if key == "" || c.Op.Journal["release_stop_receipt"] != key || c.Op.Journal["release_stop_sha256"] != digest { return fmt.Errorf("%w: the release does not cite the receipt that discharged it", ErrClaimStopReceipt) } for _, ob := range c.Obligations { if ob.Kind == RelocationReleaseObligation && ob.Op == c.Op.ID { return fmt.Errorf("%w: the relocation release is still owed", ErrClaimStopReceipt) } } return nil } // verifyDischargedReceipt re-verifies a discharged receipt with main's // RelocationReleaseReceipts.Complete in its replay form. The caller has // checked releaseDischarged, so Complete takes its replay branch and // writes nothing; the observation's ETag is never compared there, and an // unexpected write could only fail its CAS against it. func verifyDischargedReceipt(ctx context.Context, receipts RelocationReleaseReceipts, c *hostclaim.Claim, key, digest string) error { _, err := receipts.Complete(ctx, hostclaim.Observed{Host: c.Host, Claim: c, ETag: "verify-only"}, key, digest) return err } // RelocationReleaseDischarge is the relocation-release obligation's two // claim CASes around root's stop (claim Plan 11): Queue before the stop is // requested, Complete with root's exact receipt after it. type RelocationReleaseDischarge interface { Queue(context.Context, hostclaim.Observed) (hostclaim.Observed, error) Complete(ctx context.Context, observed hostclaim.Observed, key, digest string) (hostclaim.Observed, error) // Verify re-verifies a receipt observed has already discharged, and // writes nothing. Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error } // ReleaseObligation is production's RelocationReleaseDischarge: main's // QueueRelocationRelease and RelocationReleaseReceipts.Complete, over the // receipts' store (the claims' store). type ReleaseObligation struct { Receipts RelocationReleaseReceipts } var _ RelocationReleaseDischarge = ReleaseObligation{} // relocationReleaseRetirement is the obligation's stable retirement // identity: one release of one writer session of one installation, under // one operation. func relocationReleaseRetirement(c *hostclaim.Claim) string { return fmt.Sprintf("%s/%d/%d", c.Op.ID, c.Holder.Stamp, c.Holder.Session) } // Queue records the obligation for obs's release, once. An identical // obligation already owed, or one already discharged by this operation's // receipt, leaves the claim as it is; any other refuses. func (o ReleaseObligation) Queue(ctx context.Context, obs hostclaim.Observed) (hostclaim.Observed, error) { if o.Receipts.Store == nil || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { return hostclaim.Observed{}, ErrClaimStopUnavailable } c := obs.Claim if c.Op.Journal["release_stop_receipt"] != "" { return obs, nil // discharged: root replays the receipt, Complete finds it done } queue := hostclaim.QueueRelocationRelease(c.Op.ID, c.Op.Exec, c.Op.Intent["region"], relocationReleaseRetirement(c)) for _, ob := range c.Obligations { if ob.ID == RelocationReleaseObligation { // Already owed: the same obligation passes unchanged (Queue // returns before writing), any other refuses. return obs, queue(c) } } return hostclaim.Update(ctx, o.Receipts.Store, obs, queue) } // Complete discharges the obligation with root's receipt on a fresh read. func (o ReleaseObligation) Complete(ctx context.Context, obs hostclaim.Observed, key, digest string) (hostclaim.Observed, error) { if o.Receipts.Store == nil { return hostclaim.Observed{}, ErrClaimStopUnavailable } fresh, err := hostclaim.Read(ctx, o.Receipts.Store, obs.Host) if err != nil { return hostclaim.Observed{}, err } return o.Receipts.Complete(ctx, fresh, key, digest) } // Verify re-verifies the discharged receipt key and digest name on // observed's claim, with main's Complete in its replay form. func (o ReleaseObligation) Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error { if o.Receipts.Store == nil || observed.Claim == nil || observed.Claim.Holder == nil { return ErrClaimStopUnavailable } if err := releaseDischarged(observed.Claim, key, digest); err != nil { return err } return verifyDischargedReceipt(ctx, o.Receipts, observed.Claim, key, digest) }