carlos / platform Public

region ha: the relocation's prepared-files binding holds P's build stamp and the replacement's install stamp apart, each exactly, so a target replaced at installed, or in probing before P's first admission, which keeps P and its prepared authority under a new install stamp, establishes P's files through main's C8 provider (Astra round 7, P2).

The install now reads the claim's prepared_authority. Its install journal's files_manifest step binds P's key, op, digest and exact version at P's build stamp, prepared_authority.install_stamp, and the install refuses a manifest whose digest is not the authority's. The journal itself, like the local_install receipt, carries this box's install stamp, the claim's current install_stamp.

The reader checks each side against its own source:
- the step: decoded against the authority's stamp, its digest equal to the authority's;
- the receipt, the journal and the holder: against install_stamp;
- the build stamp must be set and never past the install stamp.

Neither stamp stands in for the other.

Tests:
- The install fixture journals the prepared authority as ToInstalled does, and can move to a fresh replacement box (onBox).
- A replacement at installed and one in probing establish P's files through the real provider. Both failed on the previous code.
- The replaced target's install journal, with or without its receipt, refuses, and so does an authority stamped at, or past, the install stamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 15ffddfaf8ebc106ee00ed967e9a328e23118192 parent 911c490
4 files changed, +275 −51
  • internal/host/movesfiles.go +20 −13
  • internal/host/relocationinstall.go +24 −7
  • internal/host/relocationinstall_linux_test.go +42 −14
  • internal/host/relocationprobefiles_linux_test.go +189 −17
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go
index 245b8053e..683d50f21 100644
--- a/internal/host/movesfiles.go
+++ b/internal/host/movesfiles.go
@@ -711,16 +711,23 @@ func movesPreparedManifest(dataDir string, o hostclaim.Observed, gen, key string
return decodePreparedManifest(raw, key, o.Claim.Op.ID, stamp)
}
-// relocationPreparedManifest is a relocation's binding (region HA §5.8):
-// the claim's prepared_authority names P, its install stamp and the
-// digest of P's prepared files manifest (sequence 0); the claim's
-// local_install receipt names this box and the digest of its install
-// journal; that journal, read whole and held to the receipt's digest,
-// names the host, the op, this box, the stamp and P, and its
-// files_manifest step binds P's key, the op, the stamp, the digest and the
-// exact version the install read. The step's digest must be the
-// authority's. The probe's admission keeps the install stamp, so the
-// holder's stamp is that stamp too. Anything missing or different refuses.
+// relocationPreparedManifest is a relocation's binding (region HA §5.8).
+// It holds two stamps apart, each exactly. P's build stamp is the one the
+// claim's prepared_authority binds (install_stamp when ToInstalled ran):
+// the authority names P, that stamp and the digest of P's prepared files
+// manifest (sequence 0), and the install journal's files_manifest step
+// binds P's key, the op, that build stamp, the digest and the exact
+// version. This box's install stamp is the claim's current install_stamp,
+// which a replacement target issues anew while keeping P and its
+// authority (hostclaim ReplaceTarget at installed, or in probing before
+// P's first admission): the holder's stamp (the probe's admission keeps
+// it), the claim's local_install receipt (this box, that stamp, the
+// digest of its install journal) and the journal itself (read whole, held
+// to the receipt's digest, naming the host, the op, this box, that stamp
+// and P) all carry it. The build precedes every install, so the build
+// stamp is never past the install stamp. The step's digest must be the
+// authority's. Anything missing or different refuses; neither stamp
+// stands in for the other.
func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen, key string) (preparedManifest, error) {
c := o.Claim
auth, err := preparedAuthority(c)
@@ -731,8 +738,8 @@ func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen,
if err != nil {
return preparedManifest{}, err
}
- if auth.Generation != gen || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) || auth.InstallStamp != stamp || c.Holder.Stamp != stamp {
- return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v is not P %s's at the install stamp %d", hostbox.ErrChanged, auth, gen, stamp)
+ if auth.Generation != gen || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) || auth.InstallStamp == 0 || auth.InstallStamp > stamp || c.Holder.Stamp != stamp {
+ return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v is not a build of P %s at or before the install stamp %d", hostbox.ErrChanged, auth, gen, stamp)
}
var receipt hostclaim.LocalInstall
if json.Unmarshal([]byte(c.Op.Journal["local_install"]), &receipt) != nil || receipt.Box != box || receipt.InstallStamp != stamp || !claimHexDigest(receipt.JournalSHA256) {
@@ -760,7 +767,7 @@ func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen,
if !ok {
return preparedManifest{}, fmt.Errorf("%w: the install journal of %s has no prepared files manifest", errLandingChanged, c.Op.ID)
}
- pm, err := decodePreparedManifest(step, key, c.Op.ID, stamp)
+ pm, err := decodePreparedManifest(step, key, c.Op.ID, auth.InstallStamp)
if err != nil {
return preparedManifest{}, err
}
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go
index d6835c4d2..4b25b3503 100644
--- a/internal/host/relocationinstall.go
+++ b/internal/host/relocationinstall.go
@@ -190,20 +190,29 @@ type relocationInstallJournal struct {
// prepared files manifest (JSON preparedManifest: key, sha256, version,
// op, stamp): what the probe's C8 establishment consumes as its baseline,
// read by exactly that version (relocationPreparedManifest), as a move's
-// op journal binds its own (landStepFilesManifest).
+// op journal binds its own (landStepFilesManifest). Its stamp is P's build
+// stamp, the one the claim's prepared_authority binds (install_stamp when
+// ToInstalled ran), not this box's install stamp: a replacement target
+// keeps P and its authority under a new install stamp (hostclaim
+// ReplaceTarget at installed, or in probing before P's first admission),
+// and its own install journal records its own stamp beside this binding.
const relocationStepFilesManifest = "files_manifest"
// relocationPreparedBinding binds P's prepared files manifest for the
// probe's establishment: the current version of P's manifest key as the
// versioned listing names it, read by that exact version, which must be a
// prepared manifest (sequence 0) with exactly the entries the install
-// judged (loaded), bound to the op and the install stamp with the digest
-// of its bytes. A missing, deleted or unversioned manifest, or one that is
-// not what the install judged, refuses.
-func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, bucket, prefix, op string, stamp uint64, loaded hostfiles.Manifest) (preparedManifest, error) {
+// judged (loaded) and the digest the claim's prepared authority (auth)
+// verified, bound to the op and P's build stamp (auth.InstallStamp). A
+// missing, deleted or unversioned manifest, one that is not what the
+// install judged, or an authority that is not P's, refuses.
+func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, bucket, prefix, op string, auth hostclaim.PreparedAuthority, loaded hostfiles.Manifest) (preparedManifest, error) {
if data == nil || bucket == "" {
return preparedManifest{}, ErrClaimStopUnavailable
}
+ if auth.InstallStamp == 0 || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) {
+ return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v binds no build of P's files manifest", hostrelocate.ErrRefused, auth)
+ }
key := hostfiles.ManifestKey(prefix)
vs, err := data.ListVersions(ctx, bucket, key)
if err != nil {
@@ -230,7 +239,11 @@ func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, b
if err := json.Unmarshal(raw, &m); err != nil || m.V != hostfiles.ManifestVersion || m.Seq != 0 || loaded.Seq != 0 || !sameEntries(m, loaded) {
return preparedManifest{}, errors.Join(fmt.Errorf("%w: P's files manifest %s@%s is not the prepared manifest the install judged", hostrelocate.ErrRefused, key, version), err)
}
- return preparedManifest{Key: key, SHA256: hostfilesSum(raw), Version: version, Op: op, Stamp: stamp}, nil
+ sum := hostfilesSum(raw)
+ if sum != auth.FilesManifestSHA256 {
+ return preparedManifest{}, fmt.Errorf("%w: P's files manifest %s@%s is not the prepared authority's", hostrelocate.ErrRefused, key, version)
+ }
+ return preparedManifest{Key: key, SHA256: sum, Version: version, Op: op, Stamp: auth.InstallStamp}, nil
}
// relocationOpDir is the operation's local journal directory (ruling 44):
@@ -470,7 +483,11 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont
}
// The binding the probe's C8 establishment reads P's prepared
// manifest by: its exact version, journalled below.
- binding, err := relocationPreparedBinding(ctx, cfg.Versions, cfg.Bucket, prefix, c.Op.ID, stamp, pm)
+ auth, err := preparedAuthority(c)
+ if err != nil {
+ return hostclaim.LocalInstall{}, err
+ }
+ binding, err := relocationPreparedBinding(ctx, cfg.Versions, cfg.Bucket, prefix, c.Op.ID, auth, pm)
if err != nil {
return hostclaim.LocalInstall{}, err
}
diff --git a/internal/host/relocationinstall_linux_test.go b/internal/host/relocationinstall_linux_test.go
index c9c18fc12..31601027a 100644
--- a/internal/host/relocationinstall_linux_test.go
+++ b/internal/host/relocationinstall_linux_test.go
@@ -95,8 +95,16 @@ func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture {
if err := b.build(); err != nil {
t.Fatal(err)
}
+ // installed as ToInstalled leaves it: the verified build's prepared
+ // authority, bound to the install stamp the build ran at.
+ auth, done, err := b.verifier.Verify(ctx, b.obs)
+ if err != nil || !done {
+ t.Fatalf("P is not verified: done=%v %v", done, err)
+ }
o, err := hostclaim.Update(ctx, b.claims, b.obs, func(c *hostclaim.Claim) error {
c.Op.Phase = hostclaim.PhaseInstalled
+ auth.InstallStamp = c.Holder.Stamp
+ c.Op.Journal["prepared_authority"] = string(mustJSON(t, auth))
c.Op.Intent["target_enrollment"] = f.actor.Enrollment
c.Op.Journal["exec_ack.src"], c.Op.Journal["exec_ack.dst"] = `{"op":"rel-1"}`, `{"op":"rel-1"}`
return nil
@@ -105,34 +113,54 @@ func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture {
t.Fatal(err)
}
b.obs = o
+ f.onBox(r, f.actor)
+ b.cfg.render = f.render
+ b.cfg.materialize = f.materialize
+ b.cfg.verifyLive = f.verifyLive
+ return f
+}
+
+// onBox points the fixture's install at box actor over the census box r:
+// its registry, typed inventory, launch journal and census, its guard over
+// r's data directory, main's reconciler and role assembly, and the
+// install's paths and work source. A replacement target is a second call
+// with a fresh box (newReplacedProbeFilesFixture).
+func (f *installFixture) onBox(r ClaimRuntimeResources, actor hostbox.Actor) {
+ t, b := f.t, f.buildFixture
+ var err error
+ f.r, f.actor = r, actor
+ if err := os.MkdirAll(filepath.Join(r.DataDir, "hostguard"), 0o770); err != nil {
+ t.Fatal(err)
+ }
if f.reg, err = registry.Open(filepath.Join(r.DataDir, "registry.db")); err != nil {
t.Fatal(err)
}
- t.Cleanup(func() { f.reg.Close() })
+ reg := f.reg
+ t.Cleanup(func() { reg.Close() })
if f.inv, err = hostbox.OpenStopInventoryDB(filepath.Join(r.DataDir, ".hostclaims", "inventory.db")); err != nil {
t.Fatal(err)
}
- t.Cleanup(func() { f.inv.Close() })
+ inv := f.inv
+ t.Cleanup(func() { inv.Close() })
launches, err := hostbox.OpenLaunchJournal(filepath.Join(r.DataDir, ".hostclaims", "launches.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { launches.Close() })
- census := &hostbox.EmptyInstallationCensus{Inventory: f.inv, Registry: f.reg, Launches: launches, Box: "dst", Enrollment: f.actor.Enrollment, DirectoryRevision: "4",
+ census := &hostbox.EmptyInstallationCensus{Inventory: f.inv, Registry: f.reg, Launches: launches, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "4",
Roots: map[string]string{"data": r.DataDir, "opt": r.OptDir, "units": r.UnitsDir}, SharedPaths: claimGlobalSharedPaths(r)}
+ if b.a.Guard.DataDir != r.DataDir {
+ b.a.Guard = hostguard.Guard{DataDir: r.DataDir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: b.claims}}
+ }
guard := b.a.Guard
- b.a.Reconcile = &hostbox.ReconcileAdapter{Registry: f.reg, Inventory: f.inv, Census: census, Box: "dst", Enrollment: f.actor.Enrollment, DirectoryRevision: "4",
- Reconciler: hostbox.Reconciler{Claims: b.claims, Guard: guard, Box: "dst", Region: "ie", Local: park.Local{Guard: guard, Claims: b.claims, DataDir: r.DataDir, Box: "dst"}}}
- b.a.Wake = &ClaimWakeAssembly{Roles: &ClaimRoleAssembly{Inventory: f.inv, Identity: handoverRecoveryIdentity(t, f.actor), ManagedRoots: map[string]string{"data": r.DataDir}}}
+ b.a.Reconcile = &hostbox.ReconcileAdapter{Registry: f.reg, Inventory: f.inv, Census: census, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "4",
+ Reconciler: hostbox.Reconciler{Claims: b.claims, Guard: guard, Box: actor.Box, Region: "ie", Local: park.Local{Guard: guard, Claims: b.claims, DataDir: r.DataDir, Box: actor.Box}}}
+ b.a.Wake = &ClaimWakeAssembly{Roles: &ClaimRoleAssembly{Inventory: f.inv, Identity: handoverRecoveryIdentity(t, actor), ManagedRoots: map[string]string{"data": r.DataDir}}}
b.a.stopLocal = func(context.Context) (claimStopLocal, error) {
- return claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "ctl", PID: 100, ProcessStart: "5000"}, nil
+ return claimStopLocal{Actor: actor, BootID: "boot", ControllerID: "ctl", PID: 100, ProcessStart: "5000"}, nil
}
- b.cfg.Registry, b.cfg.OptDir = f.reg, r.OptDir
- b.cfg.Source.Guard, b.cfg.Source.Now = guard, time.Now
- b.cfg.render = f.render
- b.cfg.materialize = f.materialize
- b.cfg.verifyLive = f.verifyLive
- return f
+ b.cfg.Registry, b.cfg.OptDir, b.cfg.DataDir, b.cfg.WorkDir = f.reg, r.OptDir, r.DataDir, filepath.Join(r.DataDir, "relocate", "build")
+ b.cfg.Source.Box, b.cfg.Source.Guard, b.cfg.Source.Now = actor.Box, guard, time.Now
}
// render stands for appconfig.RenderHeldUnder: it selects its work with
@@ -145,7 +173,7 @@ func (f *installFixture) render(ctx context.Context, _ release.Store, _ *ecdh.Pr
if err != nil {
return appconfig.HeldRender{}, err
}
- if rt.Status != "relocating" || rt.HoldOp != op || box != "dst" {
+ if rt.Status != "relocating" || rt.HoldOp != op || box != f.actor.Box {
return appconfig.HeldRender{}, appconfig.ErrNotHeld
}
var body strings.Builder
diff --git a/internal/host/relocationprobefiles_linux_test.go b/internal/host/relocationprobefiles_linux_test.go
index d7476f6f2..8bd741de1 100644
--- a/internal/host/relocationprobefiles_linux_test.go
+++ b/internal/host/relocationprobefiles_linux_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strconv"
"strings"
"testing"
"time"
@@ -31,34 +32,112 @@ type probeFilesFixture struct {
*installFixture
receipt hostclaim.LocalInstall
inputs *RelocationWakeInputs
+ // replaced is what the replaced target left when the fixture replaced
+ // it (newReplacedProbeFilesFixture): its receipt and its install
+ // journal's bytes, at P's build stamp.
+ replaced struct {
+ receipt hostclaim.LocalInstall
+ journal []byte
+ }
}
func newProbeFilesFixture(t *testing.T, opts buildOptions) *probeFilesFixture {
t.Helper()
- ctx := t.Context()
- // Claim Plan 15's origin record is on, as the provider's origin proof
- // (ClaimOriginProof) needs: every admission through the claim's CAS,
- // the source's g1 and the probe's P, is recorded.
+ f := newProbeFilesInstalled(t, opts)
+ f.installAndProbe()
+ f.bindProvider()
+ return f
+}
+
+// newProbeFilesInstalled is the fixture at installed, P built and its
+// prepared authority journalled, before any local install. Claim Plan
+// 15's origin record is on, as the provider's origin proof
+// (ClaimOriginProof) needs: every admission through the claim's CAS, the
+// source's g1 and the probe's P, is recorded.
+func newProbeFilesInstalled(t *testing.T, opts buildOptions) *probeFilesFixture {
+ t.Helper()
t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
- f := &probeFilesFixture{installFixture: newInstallFixtureWith(t, opts)}
- auth, done, err := f.verifier.Verify(ctx, f.read())
- if err != nil || !done {
- t.Fatalf("P is not verified: done=%v %v", done, err)
+ return &probeFilesFixture{installFixture: newInstallFixtureWith(t, opts)}
+}
+
+// installAndProbe is the executor's local install at the current install
+// stamp, its receipt journalled, and its entry into probing.
+func (f *probeFilesFixture) installAndProbe() {
+ f.t.Helper()
+ var err error
+ if f.receipt, err = f.install(); err != nil {
+ f.t.Fatal(err)
+ }
+ f.update(hostclaim.RecordLocalInstall("rel-1", f.receipt))
+ f.update(hostclaim.ToProbing("rel-1", time.Now()))
+}
+
+// newReplacedProbeFilesFixture replaces a terminated target that had made
+// its local install, at phase (installed, or probing before P's first
+// admission), as hostclaim.ReplaceTarget does for both: install(c, next,
+// P, installed) names the replacement box as the executor, keeps P and its
+// prepared authority, issues a new install stamp (the CAS's generation)
+// and drops the receipt; ToInstalled does not run again. The replacement,
+// dst2, is a fresh box: its guard is activated at the new stamp, and it
+// makes its own local install, records it and enters probing. The replaced
+// target's receipt and journal bytes are kept for the negatives.
+func newReplacedProbeFilesFixture(t *testing.T, opts buildOptions, phase string) *probeFilesFixture {
+ t.Helper()
+ f := newProbeFilesInstalled(t, opts)
+ var err error
+ if f.replaced.receipt, err = f.install(); err != nil {
+ t.Fatal(err)
+ }
+ f.update(hostclaim.RecordLocalInstall("rel-1", f.replaced.receipt))
+ if phase == hostclaim.PhaseRelocatingProbing {
+ f.update(hostclaim.ToProbing("rel-1", time.Now()))
+ }
+ if f.read().Claim.Op.Phase != phase {
+ t.Fatalf("the replaced target is at %s, not %s", f.read().Claim.Op.Phase, phase)
+ }
+ path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
+ if f.replaced.journal, err = os.ReadFile(path); err != nil {
+ t.Fatal(err)
}
+ build := f.read().Claim.Holder.Stamp
+ next := hostbox.Actor{Box: "dst2", Enrollment: strings.Repeat("b", 32), Region: "ie", Revision: 4}
f.update(func(c *hostclaim.Claim) error {
- auth.InstallStamp = c.Holder.Stamp
- c.Op.Journal["prepared_authority"] = string(mustJSON(t, auth))
+ if hostclaim.PreparedOpened(c) {
+ return errors.New("P was opened")
+ }
+ stamp := c.Gen + 1 // hostclaim install(): the CAS's resulting gen
+ c.Op.Exec, c.Op.Phase = next.Box, hostclaim.PhaseInstalled
+ c.Op.Intent["target_box"], c.Op.Intent["target_enrollment"] = next.Box, next.Enrollment
+ c.Op.Participants = []string{"src", next.Box}
+ c.Op.Journal["exec_ack."+next.Box] = `{"op":"rel-1"}`
+ c.Holder.Stamp = stamp
+ c.Op.Journal["install_stamp"] = strconv.FormatUint(stamp, 10)
+ delete(c.Op.Journal, "local_install")
return nil
})
- if f.receipt, err = f.install(); err != nil {
+ c := f.read().Claim
+ auth, err := preparedAuthority(c)
+ if err != nil || auth.InstallStamp != build || c.Holder.Stamp == build {
+ t.Fatalf("the replacement keeps P's authority at the build stamp %d under a new stamp %d: %+v %v", build, c.Holder.Stamp, auth, err)
+ }
+ f.onBox(claimCensusBox(t), next)
+ if err := f.a.Guard.Activate(t.Context(), buildHost, next.Box, nil); err != nil {
t.Fatal(err)
}
- f.update(hostclaim.RecordLocalInstall("rel-1", f.receipt))
- f.update(hostclaim.ToProbing("rel-1", time.Now()))
+ f.installAndProbe()
+ f.bindProvider()
+ return f
+}
+
+// bindProvider opens the real C8 provider the production source opens
+// and binds the relocation's wake inputs to it.
+func (f *probeFilesFixture) bindProvider() {
+ t := f.t
+ ctx := t.Context()
src := &movesFilesSource{DataDir: f.r.DataDir, Routes: f.reg, newFiles: func(context.Context, string, string) (release.Store, error) {
return bucketStore{b: f.bucket, bucket: "data"}, nil
}}
- c8, err := src.Open(ctx, f.claims, f.bucket, hostbox.Actor{Box: "dst", Region: "ie", Bucket: "data", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}})
+ c8, err := src.Open(ctx, f.claims, f.bucket, hostbox.Actor{Box: f.actor.Box, Region: "ie", Bucket: "data", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}})
if err != nil {
t.Fatal(err)
}
@@ -67,9 +146,8 @@ func newProbeFilesFixture(t *testing.T, opts buildOptions) *probeFilesFixture {
Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/opt/carlos/live/" + buildHost, ExecutableSHA256: strings.Repeat("b", 64), Version: "v1"}, nil
},
- FilesManifest: c8, Markers: plan9D5Markers{f.bucket}, Claims: f.claims, FilesStore: f.files, Data: f.bucket, Bucket: "data", Region: "ie", Box: "dst",
+ FilesManifest: c8, Markers: plan9D5Markers{f.bucket}, Claims: f.claims, FilesStore: f.files, Data: f.bucket, Bucket: "data", Region: "ie", Box: f.actor.Box,
NewID: func() string { seq++; return fmt.Sprintf("fresh%d", seq) }, Now: time.Now, Releases: &roReleases{}}
- return f
}
func (f *probeFilesFixture) update(tr func(*hostclaim.Claim) error) {
@@ -99,7 +177,7 @@ func (f *probeFilesFixture) probe(env string) (ClaimLaunchInputs, error) {
if err != nil {
return in, err
}
- counted, err := hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt("rel-1", "dst"))
+ counted, err := hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt("rel-1", f.actor.Box))
if err != nil {
f.t.Fatal(err)
}
@@ -275,3 +353,97 @@ func TestRelocationPreparedBindingIsRelocationsOwn(t *testing.T) {
}
})
}
+
+// Merge round 7: a replacement target keeps P and its prepared authority
+// at P's build stamp while it installs under a new install stamp (at
+// installed, or in probing before P's first admission). Its first probe
+// establishes P's files through the real provider: P's manifest bound at
+// the build stamp, its own receipt and install journal at the new one.
+// The replacement's install journal records P's build stamp in its
+// files_manifest step and its own stamp as the journal's install stamp.
+func TestRelocationReplacementProbeEstablishesPsFiles(t *testing.T) {
+ opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
+ for _, phase := range []string{hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} {
+ t.Run("replaced at "+phase, func(t *testing.T) {
+ f := newReplacedProbeFilesFixture(t, opts, phase)
+ c := f.read().Claim
+ auth, _ := preparedAuthority(c)
+ if f.receipt.InstallStamp != c.Holder.Stamp || f.receipt.InstallStamp == auth.InstallStamp {
+ t.Fatalf("the replacement's receipt %+v is not at its own install stamp %d", f.receipt, c.Holder.Stamp)
+ }
+ if _, err := f.probe(firstProbeEnv); err != nil {
+ t.Fatalf("the replacement's first probe: %v", err)
+ }
+ m, pm := f.established()
+ if pm.Stamp != auth.InstallStamp || m.Platform[hostfiles.EnvItem].SHA256 != hostfilesSum([]byte(firstProbeEnv)) || m.Entries["public/index.html"].SHA256 == "" {
+ t.Fatalf("binding %+v (build stamp %d), manifest %+v", pm, auth.InstallStamp, m)
+ }
+ path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
+ raw, _ := os.ReadFile(path)
+ var j relocationInstallJournal
+ if err := json.Unmarshal(raw, &j); err != nil || j.InstallStamp != c.Holder.Stamp {
+ t.Fatalf("the replacement's journal is at %d, not its install stamp %d: %v", j.InstallStamp, c.Holder.Stamp, err)
+ }
+ })
+ }
+}
+
+// Merge round 7: neither stamp stands in for the other. The replaced
+// target's install journal (P's build stamp), or its receipt, under the
+// replacement's install stamp refuses; a prepared authority whose stamp
+// is not the build stamp the install bound, or is past the install
+// stamp, refuses; and nothing is established.
+func TestRelocationReplacementRefusesTheWrongStamp(t *testing.T) {
+ opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
+ for name, tamper := range map[string]func(f *probeFilesFixture){
+ "the replaced target's install journal": func(f *probeFilesFixture) {
+ path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
+ if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil {
+ f.t.Fatal(err)
+ }
+ },
+ "the replaced target's install journal and receipt": func(f *probeFilesFixture) {
+ path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
+ if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil {
+ f.t.Fatal(err)
+ }
+ f.update(func(c *hostclaim.Claim) error {
+ c.Op.Journal["local_install"] = string(mustJSON(f.t, f.replaced.receipt))
+ return nil
+ })
+ },
+ "an authority at the install stamp": func(f *probeFilesFixture) {
+ f.update(func(c *hostclaim.Claim) error {
+ auth, err := preparedAuthority(c)
+ if err != nil {
+ return err
+ }
+ auth.InstallStamp = c.Holder.Stamp
+ c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth))
+ return nil
+ })
+ },
+ "an authority past the install stamp": func(f *probeFilesFixture) {
+ f.update(func(c *hostclaim.Claim) error {
+ auth, err := preparedAuthority(c)
+ if err != nil {
+ return err
+ }
+ auth.InstallStamp = c.Holder.Stamp + 1
+ c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth))
+ return nil
+ })
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseRelocatingProbing)
+ tamper(f)
+ if _, err := f.probe(firstProbeEnv); err == nil {
+ t.Fatal("established under the wrong stamp")
+ }
+ if m, _, _ := hostfiles.Load(t.Context(), f.files, movesFilesPrefix(f.read().Claim.Holder.Location, "p1")); m.Seq != 0 {
+ t.Fatalf("P's manifest was replaced: %+v", m)
+ }
+ })
+ }
+}