package moves import ( "bytes" "encoding/json" "errors" "fmt" "io" "time" "github.com/carlosframework/platform/internal/provision" ) // The return lane (moves spec ยง7.8 returning, Plan 4 Task 4): a dirty // rollback's target sends its stopped state back to the source through a // staging lane named by the rollback id, moves///, in the // SOURCE's bucket (M17: the target can put into the source's moves/*). It is // a staging lane like the cut's (round-2 finding 6): every object goes in // through the guarded staging writer under the staging-work lease for // (source region, host), and the lease's bound is recorded in the seal, so // the lane's park (Plan 5) acknowledges only after the last request a paused // writer signed has expired. // // The return seal is journalled in the claim under JournalReturnSeal, the // baseline the return restores to under JournalReturnBaseline. The source's // install (Task 3's installSource) reads the first as its Seal and the // second as its Baseline. // The claim journal keys of a dirty return, written in one claim write // before the record says returned. const ( JournalReturnSeal = "return_seal" JournalReturnBaseline = "return_baseline" ) // ReturnLane is the return lane's prefix: a staging prefix of the rollback // id, in the source's bucket. func ReturnLane(host, rbID string) string { return StagingPrefix(host, rbID) } // ReturnSeal is the seal of a return lane. replicas are the lane's replica // objects: one snapshot [1, n] of the stopped database for a dirty return, // or every level file of the staged seal copied as given (J22). The lane is // built from the target's stopped state and covers it exactly, so FromDB // and SrcCovered are both true. leaseUntil is the staging lease's bound // when the last lane object was written. state is the lane's build state, // nil when the target had none. The caller's slices are copied. func ReturnSeal(n uint64, replicas []SealedObject, files, timers, state []SealedObject, now time.Time, leaseUntil time.Time) Seal { return Seal{V: SealVersion, N: n, Replica: sortObjects(replicas), Files: sortObjects(files), Timers: sortObjects(timers), State: sortObjects(state), SrcCovered: true, FromDB: true, SealedAt: now.UTC(), LeaseUntil: leaseUntil.UTC()} } // errBadBaseline is a return baseline that does not name what a return // restores to. var errBadBaseline = errors.New("moves: the return baseline is not usable") // validBaseline requires what the source's install compares against: the // final TXID, both files digests, the timers digest and the evidence it was // taken under. func validBaseline(b provision.MoveSource) error { if b.FinalTXID == 0 || b.Files == nil || b.Files.Plain == "" || b.Files.Sealed == "" || b.TimersSHA256 == "" || b.Evidence == "" { return fmt.Errorf("%w: %+v", errBadBaseline, b) } return nil } // EncodeReturnBaseline is the return's baseline as the claim journal // carries it (Task 3's installSource.Baseline: the final TXID N', the files // and timers digests, the carried replication and next_due, and the target's // stop evidence). func EncodeReturnBaseline(b provision.MoveSource) (string, error) { if err := validBaseline(b); err != nil { return "", err } raw, err := json.Marshal(b) if err != nil { return "", err } return string(raw), nil } // DecodeReturnBaseline reads a journalled baseline strictly (no unknown // field, no trailing data) and validates it. func DecodeReturnBaseline(raw string) (provision.MoveSource, error) { dec := json.NewDecoder(bytes.NewReader([]byte(raw))) dec.DisallowUnknownFields() var b provision.MoveSource if err := dec.Decode(&b); err != nil { return provision.MoveSource{}, fmt.Errorf("%w: %v", errBadBaseline, err) } if _, err := dec.Token(); err != io.EOF { return provision.MoveSource{}, fmt.Errorf("%w: trailing data", errBadBaseline) } if err := validBaseline(b); err != nil { return provision.MoveSource{}, err } return b, nil }