package hostbox import ( "context" "crypto/ed25519" "fmt" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" ) // LossAdmissionSources validates every frozen source against its remote baseline. // The writer verifies a signed declaration or the claim's consumed automatic fence. type LossAdmissionSources func(context.Context, hostclaim.Observed, hostclaim.LossRecord, hostclaim.Admission, *genselect.Plan) ([]hostclaim.ValidatedWriteSource, error) type LossAdmission struct { PublicKey ed25519.PublicKey Owner hostclaim.LossRecoveryOwner Sources LossAdmissionSources } func (w Writer) admitLoss(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan, id string) (hostclaim.Observed, error) { if w.Loss == nil || w.Loss.Owner == nil || w.Loss.Sources == nil || len(w.Loss.PublicKey) != ed25519.PublicKeySize { return hostclaim.Observed{}, hostclaim.ErrLossOwnerRequired } if o.Claim.Op.Kind != hostclaim.KindRestoring || o.Claim.Op.Phase != hostclaim.PhaseProbing || in.Coverage != "" || in.KeepStamp || (in.LossID != "" && in.LossID != id) { return hostclaim.Observed{}, hostclaim.ErrLossRecord } // Select validates the restore point; loss sources describe independent baselines. if err := w.restorePoint(ctx, o, in, baseline); err != nil { return hostclaim.Observed{}, err } loss, err := hostclaim.ReadLossRecord(ctx, w.Claims, w.Loss.PublicKey, id) if err != nil { return hostclaim.Observed{}, err } r := loss.Record if r.State != "committed" || r.Host != o.Host || r.OpID != o.Claim.Op.ID || r.HolderIncarnation != o.Claim.Holder.Incarnation { return hostclaim.Observed{}, hostclaim.ErrLossRecord } sources, err := w.Loss.Sources(ctx, o, r, in, baseline) if err != nil { return hostclaim.Observed{}, err } p := hostclaim.AdmissionRecord{Box: o.Claim.Holder.WriterBox, Session: o.Claim.Holder.Session, Generation: o.Claim.Holder.Generation} if a := o.Claim.Holder.Admissions; len(a) > 0 { p = a[len(a)-1] } in.AdditionalWritesLost = nil for _, s := range sources { if s.Box == p.Box && s.Session == p.Session && s.Generation == p.Generation { in.RemoteMaxTXID = s.RemoteMaxTXID } else { in.AdditionalWritesLost = append(in.AdditionalWritesLost, hostclaim.WritesLost{Box: s.Box, Session: s.Session, Generation: s.Generation, RemoteMaxTXID: s.RemoteMaxTXID}) } } if err := w.AdmissionEvidence(ctx, o, in, baseline); err != nil { return hostclaim.Observed{}, err } return hostclaim.AdmitWriterWithLoss(ctx, w.Claims, w.Loss.PublicKey, o, in, id, sources, w.Loss.Owner) } func (w Writer) admitFencedLoss(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan, record hostclaim.LossRecord) (hostclaim.Observed, error) { if w.Loss == nil || w.Loss.Sources == nil { return hostclaim.Observed{}, hostclaim.ErrLossOwnerRequired } if err := w.restorePoint(ctx, o, in, baseline); err != nil { return hostclaim.Observed{}, err } sources, err := w.Loss.Sources(ctx, o, record, in, baseline) if err != nil { return hostclaim.Observed{}, err } if len(sources) != 1 { return hostclaim.Observed{}, hostclaim.ErrFence } in.RemoteMaxTXID = sources[0].RemoteMaxTXID if err = w.AdmissionEvidence(ctx, o, in, baseline); err != nil { return hostclaim.Observed{}, err } return hostclaim.AdmitWriterWithFencedTargetLoss(ctx, w.Claims, o, in, sources) } // restorePoint is a loss admission's restore point: Select's plan, whose // maximum must be the admission's RemoteMaxTXID and, read from the plan's // own selected files by genselect.GenerationMax (the rule moves' loss // baseline validator reads, platform-1e on #236), the maximum of the plan's // own generation. It reads no bucket. The predecessor's loss boundary, which // replaces RemoteMaxTXID after the loss sources, is a separate value. func (w Writer) restorePoint(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) error { if baseline == nil || baseline.MaxTXID != in.RemoteMaxTXID { return ErrChanged } max, err := genselect.GenerationMax(baseline, o.Claim.Holder.Location, baseline.Generation) if err != nil { return fmt.Errorf("%w: the restore plan: %w", ErrChanged, err) } if max != in.RemoteMaxTXID { return fmt.Errorf("%w: the restore plan's files reach %d, not its maximum", ErrChanged, max) } return nil }