| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/hostclaim" |
| 8 | ) |
| 9 | |
| 10 | // RelocationReleaseObligation is the stop request's obligation component |
| 11 | // for a relocation's release (§5.8 release step 4). A release retires |
| 12 | // nothing, so it names no park-local chore; root's FenceAndStop requires |
| 13 | // the component, and the receipt carries it so a retirement stop can never |
| 14 | // be offered as a release, nor a release as a retirement. |
| 15 | const RelocationReleaseObligation = "relocation-release" |
| 16 | |
| 17 | // StopReceiptReleases is hostclaim.ReleaseAuthority over main's |
| 18 | // relocation-release discharge (claim Plan 11): a source's journaled |
| 19 | // release counts only once RelocationReleaseReceipts.Complete has verified |
| 20 | // root's exact receipt, journaled its key and digest, and discharged the |
| 21 | // obligation. The release must cite exactly that key and digest, the claim |
| 22 | // must owe this operation no relocation release any more, and the receipt |
| 23 | // is verified again here, at the pin, by the region worker (Plan 11 Task |
| 24 | // E: the adapter consumes the same signed receipt, never an agent-written |
| 25 | // digest alone). That check is main's own: Complete's replay form, which |
| 26 | // re-reads the bytes and checks the digest, the canonical form, every |
| 27 | // bound field, the roster entry and root's signature under the releasing |
| 28 | // box's census key, and writes nothing once the journal holds the key and |
| 29 | // digest and no obligation is owed. A verify-only export from main |
| 30 | // (RelocationReleaseReceipts.Verify) would replace the replay call. |
| 31 | // |
| 32 | // Main discharges a relocation-release only for the source in releasing. |
| 33 | // A failed target's release in probe-failed has no obligation root will |
| 34 | // stop for, so it is refused here: the nested relocation waits, and its |
| 35 | // release deadline makes it stuck. |
| 36 | type StopReceiptReleases struct { |
| 37 | Receipts RelocationReleaseReceipts |
| 38 | } |
| 39 | |
| 40 | var _ hostclaim.ReleaseAuthority = StopReceiptReleases{} |
| 41 | |
| 42 | func (a StopReceiptReleases) VerifyRelease(ctx context.Context, c hostclaim.Claim, r hostclaim.Release) error { |
| 43 | if c.Holder == nil || a.Receipts.Store == nil { |
| 44 | return ErrClaimStopUnavailable |
| 45 | } |
| 46 | if err := releaseDischarged(&c, r.Receipt, r.ReceiptSHA256); err != nil { |
| 47 | return err |
| 48 | } |
| 49 | return verifyDischargedReceipt(ctx, a.Receipts, &c, r.Receipt, r.ReceiptSHA256) |
| 50 | } |
| 51 | |
| 52 | // releaseDischarged checks c journals key and digest as its relocating |
| 53 | // operation's discharged release receipt, and owes that operation no |
| 54 | // relocation release. |
| 55 | func releaseDischarged(c *hostclaim.Claim, key, digest string) error { |
| 56 | if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseReleasing || c.Op.ID == "" { |
| 57 | return fmt.Errorf("%w: only a source's release in releasing is discharged by root's receipt", ErrClaimStopReceipt) |
| 58 | } |
| 59 | if key == "" || c.Op.Journal["release_stop_receipt"] != key || c.Op.Journal["release_stop_sha256"] != digest { |
| 60 | return fmt.Errorf("%w: the release does not cite the receipt that discharged it", ErrClaimStopReceipt) |
| 61 | } |
| 62 | for _, ob := range c.Obligations { |
| 63 | if ob.Kind == RelocationReleaseObligation && ob.Op == c.Op.ID { |
| 64 | return fmt.Errorf("%w: the relocation release is still owed", ErrClaimStopReceipt) |
| 65 | } |
| 66 | } |
| 67 | return nil |
| 68 | } |
| 69 | |
| 70 | // verifyDischargedReceipt re-verifies a discharged receipt with main's |
| 71 | // RelocationReleaseReceipts.Complete in its replay form. The caller has |
| 72 | // checked releaseDischarged, so Complete takes its replay branch and |
| 73 | // writes nothing; the observation's ETag is never compared there, and an |
| 74 | // unexpected write could only fail its CAS against it. |
| 75 | func verifyDischargedReceipt(ctx context.Context, receipts RelocationReleaseReceipts, c *hostclaim.Claim, key, digest string) error { |
| 76 | _, err := receipts.Complete(ctx, hostclaim.Observed{Host: c.Host, Claim: c, ETag: "verify-only"}, key, digest) |
| 77 | return err |
| 78 | } |
| 79 | |
| 80 | // RelocationReleaseDischarge is the relocation-release obligation's two |
| 81 | // claim CASes around root's stop (claim Plan 11): Queue before the stop is |
| 82 | // requested, Complete with root's exact receipt after it. |
| 83 | type RelocationReleaseDischarge interface { |
| 84 | Queue(context.Context, hostclaim.Observed) (hostclaim.Observed, error) |
| 85 | Complete(ctx context.Context, observed hostclaim.Observed, key, digest string) (hostclaim.Observed, error) |
| 86 | // Verify re-verifies a receipt observed has already discharged, and |
| 87 | // writes nothing. |
| 88 | Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error |
| 89 | } |
| 90 | |
| 91 | // ReleaseObligation is production's RelocationReleaseDischarge: main's |
| 92 | // QueueRelocationRelease and RelocationReleaseReceipts.Complete, over the |
| 93 | // receipts' store (the claims' store). |
| 94 | type ReleaseObligation struct { |
| 95 | Receipts RelocationReleaseReceipts |
| 96 | } |
| 97 | |
| 98 | var _ RelocationReleaseDischarge = ReleaseObligation{} |
| 99 | |
| 100 | // relocationReleaseRetirement is the obligation's stable retirement |
| 101 | // identity: one release of one writer session of one installation, under |
| 102 | // one operation. |
| 103 | func relocationReleaseRetirement(c *hostclaim.Claim) string { |
| 104 | return fmt.Sprintf("%s/%d/%d", c.Op.ID, c.Holder.Stamp, c.Holder.Session) |
| 105 | } |
| 106 | |
| 107 | // Queue records the obligation for obs's release, once. An identical |
| 108 | // obligation already owed, or one already discharged by this operation's |
| 109 | // receipt, leaves the claim as it is; any other refuses. |
| 110 | func (o ReleaseObligation) Queue(ctx context.Context, obs hostclaim.Observed) (hostclaim.Observed, error) { |
| 111 | if o.Receipts.Store == nil || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { |
| 112 | return hostclaim.Observed{}, ErrClaimStopUnavailable |
| 113 | } |
| 114 | c := obs.Claim |
| 115 | if c.Op.Journal["release_stop_receipt"] != "" { |
| 116 | return obs, nil // discharged: root replays the receipt, Complete finds it done |
| 117 | } |
| 118 | queue := hostclaim.QueueRelocationRelease(c.Op.ID, c.Op.Exec, c.Op.Intent["region"], relocationReleaseRetirement(c)) |
| 119 | for _, ob := range c.Obligations { |
| 120 | if ob.ID == RelocationReleaseObligation { |
| 121 | // Already owed: the same obligation passes unchanged (Queue |
| 122 | // returns before writing), any other refuses. |
| 123 | return obs, queue(c) |
| 124 | } |
| 125 | } |
| 126 | return hostclaim.Update(ctx, o.Receipts.Store, obs, queue) |
| 127 | } |
| 128 | |
| 129 | // Complete discharges the obligation with root's receipt on a fresh read. |
| 130 | func (o ReleaseObligation) Complete(ctx context.Context, obs hostclaim.Observed, key, digest string) (hostclaim.Observed, error) { |
| 131 | if o.Receipts.Store == nil { |
| 132 | return hostclaim.Observed{}, ErrClaimStopUnavailable |
| 133 | } |
| 134 | fresh, err := hostclaim.Read(ctx, o.Receipts.Store, obs.Host) |
| 135 | if err != nil { |
| 136 | return hostclaim.Observed{}, err |
| 137 | } |
| 138 | return o.Receipts.Complete(ctx, fresh, key, digest) |
| 139 | } |
| 140 | |
| 141 | // Verify re-verifies the discharged receipt key and digest name on |
| 142 | // observed's claim, with main's Complete in its replay form. |
| 143 | func (o ReleaseObligation) Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error { |
| 144 | if o.Receipts.Store == nil || observed.Claim == nil || observed.Claim.Holder == nil { |
| 145 | return ErrClaimStopUnavailable |
| 146 | } |
| 147 | if err := releaseDischarged(observed.Claim, key, digest); err != nil { |
| 148 | return err |
| 149 | } |
| 150 | return verifyDischargedReceipt(ctx, o.Receipts, observed.Claim, key, digest) |
| 151 | } |
| 152 | |