| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "fmt" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/genselect" |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | ) |
| 11 | |
| 12 | // LossAdmissionSources validates every frozen source against its remote baseline. |
| 13 | // The writer verifies a signed declaration or the claim's consumed automatic fence. |
| 14 | type LossAdmissionSources func(context.Context, hostclaim.Observed, hostclaim.LossRecord, hostclaim.Admission, *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error) |
| 15 | |
| 16 | type LossAdmission struct { |
| 17 | PublicKey ed25519.PublicKey |
| 18 | Owner hostclaim.LossRecoveryOwner |
| 19 | Sources LossAdmissionSources |
| 20 | } |
| 21 | |
| 22 | func (w Writer) admitLoss(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan, id string) (hostclaim.Observed, error) { |
| 23 | if w.Loss == nil || w.Loss.Owner == nil || w.Loss.Sources == nil || len(w.Loss.PublicKey) != ed25519.PublicKeySize { |
| 24 | return hostclaim.Observed{}, hostclaim.ErrLossOwnerRequired |
| 25 | } |
| 26 | if o.Claim.Op.Kind != hostclaim.KindRestoring || o.Claim.Op.Phase != hostclaim.PhaseProbing || in.Coverage != "" || in.KeepStamp || (in.LossID != "" && in.LossID != id) { |
| 27 | return hostclaim.Observed{}, hostclaim.ErrLossRecord |
| 28 | } |
| 29 | // Select validates the restore point; loss sources describe independent baselines. |
| 30 | if err := w.restorePoint(ctx, o, in, baseline); err != nil { |
| 31 | return hostclaim.Observed{}, err |
| 32 | } |
| 33 | loss, err := hostclaim.ReadLossRecord(ctx, w.Claims, w.Loss.PublicKey, id) |
| 34 | if err != nil { |
| 35 | return hostclaim.Observed{}, err |
| 36 | } |
| 37 | r := loss.Record |
| 38 | if r.State != "committed" || r.Host != o.Host || r.OpID != o.Claim.Op.ID || r.HolderIncarnation != o.Claim.Holder.Incarnation { |
| 39 | return hostclaim.Observed{}, hostclaim.ErrLossRecord |
| 40 | } |
| 41 | sources, err := w.Loss.Sources(ctx, o, r, in, baseline) |
| 42 | if err != nil { |
| 43 | return hostclaim.Observed{}, err |
| 44 | } |
| 45 | p := hostclaim.AdmissionRecord{Box: o.Claim.Holder.WriterBox, Session: o.Claim.Holder.Session, Generation: o.Claim.Holder.Generation} |
| 46 | if a := o.Claim.Holder.Admissions; len(a) > 0 { |
| 47 | p = a[len(a)-1] |
| 48 | } |
| 49 | in.AdditionalWritesLost = nil |
| 50 | for _, s := range sources { |
| 51 | if s.Box == p.Box && s.Session == p.Session && s.Generation == p.Generation { |
| 52 | in.RemoteMaxTXID = s.RemoteMaxTXID |
| 53 | } else { |
| 54 | in.AdditionalWritesLost = append(in.AdditionalWritesLost, hostclaim.WritesLost{Box: s.Box, Session: s.Session, Generation: s.Generation, RemoteMaxTXID: s.RemoteMaxTXID}) |
| 55 | } |
| 56 | } |
| 57 | if err := w.AdmissionEvidence(ctx, o, in, baseline); err != nil { |
| 58 | return hostclaim.Observed{}, err |
| 59 | } |
| 60 | return hostclaim.AdmitWriterWithLoss(ctx, w.Claims, w.Loss.PublicKey, o, in, id, sources, w.Loss.Owner) |
| 61 | } |
| 62 | |
| 63 | func (w Writer) admitFencedLoss(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan, record hostclaim.LossRecord) (hostclaim.Observed, error) { |
| 64 | if w.Loss == nil || w.Loss.Sources == nil { |
| 65 | return hostclaim.Observed{}, hostclaim.ErrLossOwnerRequired |
| 66 | } |
| 67 | if err := w.restorePoint(ctx, o, in, baseline); err != nil { |
| 68 | return hostclaim.Observed{}, err |
| 69 | } |
| 70 | sources, err := w.Loss.Sources(ctx, o, record, in, baseline) |
| 71 | if err != nil { |
| 72 | return hostclaim.Observed{}, err |
| 73 | } |
| 74 | if len(sources) != 1 { |
| 75 | return hostclaim.Observed{}, hostclaim.ErrFence |
| 76 | } |
| 77 | in.RemoteMaxTXID = sources[0].RemoteMaxTXID |
| 78 | if err = w.AdmissionEvidence(ctx, o, in, baseline); err != nil { |
| 79 | return hostclaim.Observed{}, err |
| 80 | } |
| 81 | return hostclaim.AdmitWriterWithFencedTargetLoss(ctx, w.Claims, o, in, sources) |
| 82 | } |
| 83 | |
| 84 | // restorePoint is a loss admission's restore point: Select's plan, whose |
| 85 | // maximum must be the admission's RemoteMaxTXID and, read from the plan's |
| 86 | // own selected files by genselect.GenerationMax (the rule moves' loss |
| 87 | // baseline validator reads, platform-1e on #236), the maximum of the plan's |
| 88 | // own generation. It reads no bucket. The predecessor's loss boundary, which |
| 89 | // replaces RemoteMaxTXID after the loss sources, is a separate value. |
| 90 | func (w Writer) restorePoint(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) error { |
| 91 | if baseline == nil || baseline.MaxTXID != in.RemoteMaxTXID { |
| 92 | return ErrChanged |
| 93 | } |
| 94 | max, err := genselect.GenerationMax(baseline, o.Claim.Holder.Location, baseline.Generation) |
| 95 | if err != nil { |
| 96 | return fmt.Errorf("%w: the restore plan: %w", ErrChanged, err) |
| 97 | } |
| 98 | if max != in.RemoteMaxTXID { |
| 99 | return fmt.Errorf("%w: the restore plan's files reach %d, not its maximum", ErrChanged, max) |
| 100 | } |
| 101 | return nil |
| 102 | } |
| 103 | |