| 1 | package moves |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/provision" |
| 12 | ) |
| 13 | |
| 14 | // The return lane (moves spec §7.8 returning, Plan 4 Task 4): a dirty |
| 15 | // rollback's target sends its stopped state back to the source through a |
| 16 | // staging lane named by the rollback id, moves/<host>/<rb id>/, in the |
| 17 | // SOURCE's bucket (M17: the target can put into the source's moves/*). It is |
| 18 | // a staging lane like the cut's (round-2 finding 6): every object goes in |
| 19 | // through the guarded staging writer under the staging-work lease for |
| 20 | // (source region, host), and the lease's bound is recorded in the seal, so |
| 21 | // the lane's park (Plan 5) acknowledges only after the last request a paused |
| 22 | // writer signed has expired. |
| 23 | // |
| 24 | // The return seal is journalled in the claim under JournalReturnSeal, the |
| 25 | // baseline the return restores to under JournalReturnBaseline. The source's |
| 26 | // install (Task 3's installSource) reads the first as its Seal and the |
| 27 | // second as its Baseline. |
| 28 | |
| 29 | // The claim journal keys of a dirty return, written in one claim write |
| 30 | // before the record says returned. |
| 31 | const ( |
| 32 | JournalReturnSeal = "return_seal" |
| 33 | JournalReturnBaseline = "return_baseline" |
| 34 | ) |
| 35 | |
| 36 | // ReturnLane is the return lane's prefix: a staging prefix of the rollback |
| 37 | // id, in the source's bucket. |
| 38 | func ReturnLane(host, rbID string) string { return StagingPrefix(host, rbID) } |
| 39 | |
| 40 | // ReturnSeal is the seal of a return lane. replicas are the lane's replica |
| 41 | // objects: one snapshot [1, n] of the stopped database for a dirty return, |
| 42 | // or every level file of the staged seal copied as given (J22). The lane is |
| 43 | // built from the target's stopped state and covers it exactly, so FromDB |
| 44 | // and SrcCovered are both true. leaseUntil is the staging lease's bound |
| 45 | // when the last lane object was written. state is the lane's build state, |
| 46 | // nil when the target had none. The caller's slices are copied. |
| 47 | func ReturnSeal(n uint64, replicas []SealedObject, files, timers, state []SealedObject, now time.Time, leaseUntil time.Time) Seal { |
| 48 | return Seal{V: SealVersion, N: n, Replica: sortObjects(replicas), Files: sortObjects(files), Timers: sortObjects(timers), State: sortObjects(state), |
| 49 | SrcCovered: true, FromDB: true, SealedAt: now.UTC(), LeaseUntil: leaseUntil.UTC()} |
| 50 | } |
| 51 | |
| 52 | // errBadBaseline is a return baseline that does not name what a return |
| 53 | // restores to. |
| 54 | var errBadBaseline = errors.New("moves: the return baseline is not usable") |
| 55 | |
| 56 | // validBaseline requires what the source's install compares against: the |
| 57 | // final TXID, both files digests, the timers digest and the evidence it was |
| 58 | // taken under. |
| 59 | func validBaseline(b provision.MoveSource) error { |
| 60 | if b.FinalTXID == 0 || b.Files == nil || b.Files.Plain == "" || b.Files.Sealed == "" || b.TimersSHA256 == "" || b.Evidence == "" { |
| 61 | return fmt.Errorf("%w: %+v", errBadBaseline, b) |
| 62 | } |
| 63 | return nil |
| 64 | } |
| 65 | |
| 66 | // EncodeReturnBaseline is the return's baseline as the claim journal |
| 67 | // carries it (Task 3's installSource.Baseline: the final TXID N', the files |
| 68 | // and timers digests, the carried replication and next_due, and the target's |
| 69 | // stop evidence). |
| 70 | func EncodeReturnBaseline(b provision.MoveSource) (string, error) { |
| 71 | if err := validBaseline(b); err != nil { |
| 72 | return "", err |
| 73 | } |
| 74 | raw, err := json.Marshal(b) |
| 75 | if err != nil { |
| 76 | return "", err |
| 77 | } |
| 78 | return string(raw), nil |
| 79 | } |
| 80 | |
| 81 | // DecodeReturnBaseline reads a journalled baseline strictly (no unknown |
| 82 | // field, no trailing data) and validates it. |
| 83 | func DecodeReturnBaseline(raw string) (provision.MoveSource, error) { |
| 84 | dec := json.NewDecoder(bytes.NewReader([]byte(raw))) |
| 85 | dec.DisallowUnknownFields() |
| 86 | var b provision.MoveSource |
| 87 | if err := dec.Decode(&b); err != nil { |
| 88 | return provision.MoveSource{}, fmt.Errorf("%w: %v", errBadBaseline, err) |
| 89 | } |
| 90 | if _, err := dec.Token(); err != io.EOF { |
| 91 | return provision.MoveSource{}, fmt.Errorf("%w: trailing data", errBadBaseline) |
| 92 | } |
| 93 | if err := validBaseline(b); err != nil { |
| 94 | return provision.MoveSource{}, err |
| 95 | } |
| 96 | return b, nil |
| 97 | } |
| 98 | |