Artifact 13c21b5eb80b89e81b167bc1e32c9105f8debcd2a538fc66995dae3b0d26fabf:
- File
internal/sync/horizon_verify.go
— part of check-in
[451d368554]
at
2026-10-24 13:16:26
on branch main
— feat(horizon): bind each seed to its horizon and sign seeds apart
Every seed an origin mints now carries T +horizon-seed <C> <rule> (draft-fossil-horizon-00 §5.2.1), inside the signed bytes. The card is not counted as a tag in force, Mint never derives a horizon-seed row into a seed, and Apply skips the card.
Before a clone holds a seed, signed or not, it checks the seed against its store cone (§5.2.4). The seed must carry exactly one binding card naming the store's C and rule, and its D must be no earlier than the held seed's. A seed that fails is refused as a wrongly signed one and the clone falls back to the content horizon. A deepen discards the held seed first, so the floor restarts at the new C. A held seed that is no longer bound is discarded when it is next applied.
Seeds are signed with their own horizon-seed-command and are no longer signed by clearsign and pgp-command. SSH seed signatures use the fossil-horizon-seed namespace. clearsign.Key.Verify now takes the namespace, and check-in verification keeps fossilscm.
Adds 9 conformance cases and flips HZ-5.2-8, HZ-5.2-11, HZ-5.2-12 and HZ-5.2-13 to covered. The zit interop gate moves to zit 0.7.1, whose seeds are byte-identical with ours. (user: [email protected], size: 4482) [annotate] [blame] [check-ins using] [more...]
package sync import ( "cmp" "context" "errors" "fmt" "slices" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/horizon/seed" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/tag" ) // ErrNoSeedToVerify is returned by VerifySeed for a store that is not a // structural horizon holding a seed. var ErrNoSeedToVerify = errors.New("horizon: the store holds no seed to verify") // SeedReport is what VerifySeed found: how many of the seed's T cards it // checked, and every tag at an entry where the seed and the derivation over // real artifacts disagree. type SeedReport struct { Checked int Mismatches []SeedMismatch } // SeedMismatch is one tag at one entry of K on which the seed and the // derivation disagree. Seeded and Derived spell the tag as a T card does, // its sign then its value ("*red", "+"), and are "" where it is absent. type SeedMismatch struct { Checkin, Tag, Seeded, Derived string } // VerifySeed is the verification mode of draft-fossil-horizon-00 §5.9: // trusting the seed becomes a choice with a known price. It deepens the // store to the content depth at the same C through peer, which fetches every // manifest below C and so the real tag artifacts and primary-parent chains // the seed summarized, and discards the seed (§5.8). The store, now deriving // from real artifacts alone, is compared with what the seed asserted at each // entry. The price is every manifest below C, and the store stays a // content-horizon store, which holds no seed; a transfer that fails puts the // structural cone and the seed back. func VerifySeed(ctx context.Context, r *repo.Repo, peer Transport, opts SyncOpts) (SeedReport, error) { if ctx == nil || r == nil || peer == nil { panic("sync.VerifySeed: nil context, repo or peer") } sc, err := content.StoreCone(r.DB()) if err != nil { return SeedReport{}, err } h, ok := horizonOf(sc) held, err := seed.Held(r.DB()) if err != nil { return SeedReport{}, err } if !ok || h.Depth() != cone.DepthStructural || held == nil { return SeedReport{}, ErrNoSeedToVerify } d, err := deck.Parse(held) if err != nil { return SeedReport{}, fmt.Errorf("horizon: the held seed: %w", err) } // The binding card asserts no tag state (§5.2.1), so there is nothing // to compare it with. seeded := map[string]map[string]string{} checked := 0 for _, t := range d.T { if t.Name == seed.BindingTag { continue } checked++ if seeded[t.UUID] == nil { seeded[t.UUID] = map[string]string{} } seeded[t.UUID][t.Name] = string(t.Type) + t.Value } if _, err := FetchCone(ctx, r, peer, withDepth(sc, cone.DepthContent), opts); err != nil { return SeedReport{}, fmt.Errorf("horizon: verifying the seed: %w", err) } report := SeedReport{Checked: checked} for entry, cards := range seeded { derived, err := derivedTags(r.DB(), entry) if err != nil { return SeedReport{}, err } for name := range derived { if _, ok := cards[name]; !ok { cards[name] = "" } } for name, want := range cards { if got := derived[name]; got != want { report.Mismatches = append(report.Mismatches, SeedMismatch{Checkin: entry, Tag: name, Seeded: want, Derived: got}) } } } slices.SortFunc(report.Mismatches, func(a, b SeedMismatch) int { return cmp.Or(cmp.Compare(a.Checkin, b.Checkin), cmp.Compare(a.Tag, b.Tag)) }) return report, nil } // derivedTags is the tag state the store derives at the check-in named uuid, // each tag spelled as a T card's sign and value. func derivedTags(q db.Querier, uuid string) (map[string]string, error) { rows, err := q.Query(`SELECT tag.tagname, tagxref.tagtype, coalesce(tagxref.value, '') FROM tagxref JOIN tag USING(tagid) JOIN blob ON blob.rid=tagxref.rid WHERE blob.uuid=? AND tagxref.tagtype>0`, uuid) if err != nil { return nil, fmt.Errorf("horizon: verifying the seed: %w", err) } defer rows.Close() out := map[string]string{} for rows.Next() { var name, value string var typ int if err := rows.Scan(&name, &typ, &value); err != nil { return nil, fmt.Errorf("horizon: verifying the seed: %w", err) } sign := deck.TagSingleton if typ == tag.TagPropagating { sign = deck.TagPropagating } out[name] = string(sign) + value } return out, rows.Err() }