carlos / platform Public

hostclaim: Plan 13 fix round 1 (home alias only from the signed fleet home designation; named boxes use their own identity, a move's home name only for verified remote history; readiness, coverage and lost-target parked checks through the binding; reasons show the roster name; lint catches call and map comparisons)

Answers Astra's P1/P2/P2/P3 and Opus's P2-1..3 and P3s on d94d8b73.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev dafb7366ae09a832cccb7761142c1c55f5da9347 parent d94d8b7
46 files changed, +780 −133
  • home_region_comparison_test.go +108 −23
  • internal/console/apiinstancemove.go +11 −4
  • internal/console/apiinstancemove_claim_test.go +11 −0
  • internal/console/claim_invariant_world_test.go +12 −0
  • internal/console/claim_runtime.go +3 −0
  • internal/console/movesrecovery.go +1 −1
  • internal/console/plan13_region_test.go +27 −0
  • internal/host/claimassembly.go +1 −1
  • internal/host/claimcleanup_identity.go +1 −1
  • internal/host/claimhandover_recovery.go +1 −1
  • internal/host/claimlaunch_authority.go +1 −1
  • internal/host/claimproduction.go +6 −5
  • internal/host/claimrecoveryreceipt.go +1 −1
  • internal/host/claimregion.go +6 −3
  • internal/host/movesland_probe_test.go +4 −1
  • internal/host/movesphase1.go +4 −1
  • internal/host/movespolicy.go +9 −3
  • internal/host/movesprobeowners.go +54 −35
  • internal/host/movesreport.go +23 −6
  • internal/host/movessources.go +4 −9
  • internal/host/plan12_handover_test.go +5 −2
  • internal/host/plan13_amendments_test.go +2 −2
  • internal/host/plan13_fix1_test.go +112 −0
  • internal/host/plan13_policy_test.go +16 −1
  • internal/host/plan13_receipt_test.go +2 −1
  • internal/host/plan13_region_test.go +37 −3
  • internal/host/plan13_views_test.go +32 −3
  • internal/hostactors/plan13_home_test.go +37 −0
  • internal/hostactors/roster.go +9 −0
  • internal/hostbox/identity.go +3 −2
  • internal/hostbox/work.go +8 −6
  • internal/hostclaim/fence.go +6 −3
  • internal/hostclaim/generation.go +4 −4
  • internal/hostclaim/move_target_fence.go +4 −2
  • internal/hostclaim/plan13_amendments_test.go +69 −0
  • internal/hostfence/claim_fence.go +1 −1
  • internal/hostops/move_enrollment.go +4 −1
  • internal/hostops/move_fence_regions.go +1 −1
  • internal/hostops/move_recovery_bridge_test.go +2 −1
  • internal/hostops/plan13_enrollment_test.go +17 −1
  • internal/hostops/plan13_fence_test.go +42 −0
  • internal/hostops/plan13_fix1_test.go +29 −0
  • internal/hostops/production_create.go +3 −0
  • internal/park/plan13_observe_test.go +37 −0
  • internal/park/region_names.go +7 −0
  • internal/provision/plan9_b8_census_test.go +3 −3

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	335.908s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.168s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.329s
ok  	github.com/carlosframework/platform/internal/activator	210.316s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.011s
ok  	github.com/carlosframework/platform/internal/analytics	0.030s
ok  	github.com/carlosframework/platform/internal/appconfig	2.442s
ok  	github.com/carlosframework/platform/internal/apperr	0.104s
ok  	github.com/carlosframework/platform/internal/applog	0.183s
ok  	github.com/carlosframework/platform/internal/awsinv	0.015s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.010s
ok  	github.com/carlosframework/platform/internal/boxid	0.850s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.013s
ok  	github.com/carlosframework/platform/internal/caps	0.021s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.208s
ok  	github.com/carlosframework/platform/internal/changelog	0.290s
ok  	github.com/carlosframework/platform/internal/clamd	0.058s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	357.259s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.084s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.026s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	25.773s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.125s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.019s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/host	55.887s
ok  	github.com/carlosframework/platform/internal/hostactors	0.384s
ok  	github.com/carlosframework/platform/internal/hostbox	1.279s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.250s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	0.317s
ok  	github.com/carlosframework/platform/internal/hostfence	0.029s
ok  	github.com/carlosframework/platform/internal/hostfiles	4.142s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	0.134s
ok  	github.com/carlosframework/platform/internal/hostops	1.411s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.012s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.131s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.012s
ok  	github.com/carlosframework/platform/internal/instancectl	1.704s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.697s
ok  	github.com/carlosframework/platform/internal/instancefs	0.016s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.019s
ok  	github.com/carlosframework/platform/internal/layout	1.376s
ok  	github.com/carlosframework/platform/internal/lease	9.453s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.159s
ok  	github.com/carlosframework/platform/internal/maintenance	0.083s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.540s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.062s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.237s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.746s
ok  	github.com/carlosframework/platform/internal/objstore	0.164s
ok  	github.com/carlosframework/platform/internal/ops	0.159s
ok  	github.com/carlosframework/platform/internal/park	0.185s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	3.190s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	3.731s
ok  	github.com/carlosframework/platform/internal/relay	7.227s
ok  	github.com/carlosframework/platform/internal/release	18.292s
ok  	github.com/carlosframework/platform/internal/rollout	1.505s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.005s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.241s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	2.494s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.623s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.173s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.002s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.558s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.025s
=== RUN   TestT4StorageProbeMinio
2026/10/12 11:26:53 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DDC49EF6B29873, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.016s
minio contract: PASS
diff --git a/home_region_comparison_test.go b/home_region_comparison_test.go
index 799a41282..93d43918c 100644
--- a/home_region_comparison_test.go
+++ b/home_region_comparison_test.go
@@ -13,32 +13,78 @@ import (
)
// String fields cannot encode the region vocabulary in their Go types. This
-// lint catches direct comparisons at the recognizable record/engine boundary;
-// runtime witnesses cover aliases carried through local variables and calls.
-func plan13MixedRegion(x *ast.BinaryExpr) bool {
- if x.Op != token.EQL && x.Op != token.NEQ {
- return false
+// lint checks scalar comparisons, call arguments and map keys at recognizable
+// record/engine boundaries. Runtime witnesses cover local variable aliases.
+func plan13Expr(e ast.Node) string {
+ var b bytes.Buffer
+ _ = printer.Fprint(&b, token.NewFileSet(), e)
+ return b.String()
+}
+func plan13Placement(s string) bool {
+ if strings.Contains(s, "PlacementRegion(") {
+ return true
}
- render := func(e ast.Expr) string {
- var b bytes.Buffer
- _ = printer.Fprint(&b, token.NewFileSet(), e)
- return b.String()
+ return s == "to" || s == "from" || s == "rec.Region" || s == "record.Region" || s == "recRegion" || s == "mv.To" || s == "mv.From" || s == "m.To" || s == "m.From" || strings.HasSuffix(s, ".Move.To") || strings.HasSuffix(s, ".Move.From")
+}
+func plan13Engine(s string) bool {
+ if strings.Contains(s, ".Intent[") && (strings.HasSuffix(s, `["to"]`) || strings.HasSuffix(s, `["region"]`)) {
+ return true
}
- placement := func(s string) bool {
- return s == "rec.Region" || s == "record.Region" || s == "recRegion" || s == "mv.To" || s == "mv.From" || s == "m.To" || s == "m.From" || strings.HasSuffix(s, ".Move.To") || strings.HasSuffix(s, ".Move.From")
+ if strings.HasSuffix(s, ".Region()") || strings.HasSuffix(s, ".Touched.Regions") || strings.HasSuffix(s, ".QuietAfter") {
+ return true
}
- engine := func(s string) bool {
- if strings.Contains(s, ".Intent[") && (strings.HasSuffix(s, `["to"]`) || strings.HasSuffix(s, `["region"]`)) {
- return true
+ switch s {
+ case "actor.Region", "src.Region", "dst.Region", "b.Region", "engine.Region", "e.Region", "r.Holder.Region", "in.SourceRegion", "in.TargetRegion", "s.Region", "a.Region", "o.Region", "m.Region", "t.Region", "fence.Region", "verified.Region", "regions":
+ return true
+ }
+ return false
+}
+func plan13Operand(e ast.Expr, classify func(string) bool) bool {
+ if classify(plan13Expr(e)) {
+ return true
+ }
+ switch x := e.(type) {
+ case *ast.ParenExpr:
+ return plan13Operand(x.X, classify)
+ case *ast.CallExpr:
+ switch plan13Expr(x.Fun) {
+ case "park.SameRegion", "regionname.SameRegion", "SameRegion", "park.PlacementMatches", "PlacementMatches", "park.QuietUntil", "QuietUntil":
+ return false
}
- switch s {
- case "actor.Region", "src.Region", "dst.Region", "b.Region", "engine.Region", "e.Region", "r.Holder.Region", "in.SourceRegion", "in.TargetRegion":
- return true
+ for _, arg := range x.Args {
+ if plan13Operand(arg, classify) {
+ return true
+ }
}
+ }
+ return false
+}
+func plan13MixedRegion(x *ast.BinaryExpr) bool {
+ if x.Op != token.EQL && x.Op != token.NEQ {
return false
}
- a, b := render(x.X), render(x.Y)
- return placement(a) && engine(b) || placement(b) && engine(a)
+ return plan13Operand(x.X, plan13Placement) && plan13Operand(x.Y, plan13Engine) || plan13Operand(x.Y, plan13Placement) && plan13Operand(x.X, plan13Engine)
+}
+func plan13MixedRegionNode(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.BinaryExpr:
+ return plan13MixedRegion(x)
+ case *ast.IndexExpr:
+ return plan13Operand(x.X, plan13Engine) && plan13Operand(x.Index, plan13Placement)
+ case *ast.CallExpr:
+ name := plan13Expr(x.Fun)
+ switch name {
+ case "park.SameRegion", "regionname.SameRegion", "SameRegion", "park.PlacementMatches", "PlacementMatches", "park.QuietUntil", "QuietUntil":
+ return false
+ }
+ placement, engine := false, false
+ for _, arg := range x.Args {
+ placement = placement || plan13Operand(arg, plan13Placement)
+ engine = engine || plan13Operand(arg, plan13Engine)
+ }
+ return placement && engine
+ }
+ return false
}
func TestPlan13NoDirectPlacementEngineComparison(t *testing.T) {
@@ -61,8 +107,8 @@ func TestPlan13NoDirectPlacementEngineComparison(t *testing.T) {
return err
}
ast.Inspect(f, func(n ast.Node) bool {
- if b, ok := n.(*ast.BinaryExpr); ok && plan13MixedRegion(b) {
- t.Errorf("%s: placement and engine regions need the shared rule", fset.Position(b.Pos()))
+ if plan13MixedRegionNode(n) {
+ t.Errorf("%s: placement and engine regions need the shared rule: %s", fset.Position(n.Pos()), plan13Expr(n))
}
return true
})
@@ -74,7 +120,7 @@ func TestPlan13NoDirectPlacementEngineComparison(t *testing.T) {
}
func TestPlan13RegionComparisonLintWitness(t *testing.T) {
- for _, expr := range []string{`rec.Region != src.Region`, `rec.Move.To == c.Op.Intent["to"]`, `in.SourceRegion == mv.From`} {
+ for _, expr := range []string{`rec.Region != src.Region`, `rec.Move.To == c.Op.Intent["to"]`, `in.SourceRegion == mv.From`, `park.PlacementRegion(rec.Region) != src.Region`, `rec.Move.To == s.Region`, `m.From == m.Region`, `rec.Region == fence.Region()`, `to == c.Op.Intent["to"]`, `canonical(rec.Region) == canonical(actor.Region)`} {
e, err := parser.ParseExpr(expr)
if err != nil {
t.Fatal(err)
@@ -83,7 +129,7 @@ func TestPlan13RegionComparisonLintWitness(t *testing.T) {
t.Fatal("missed", expr)
}
}
- for _, expr := range []string{`rec.Region != cfg.Region`, `rec.Move.To == rec.Region`, `park.PlacementRegion(rec.Region) != src.Region`, `receipt.Region != box.Region`} {
+ for _, expr := range []string{`rec.Region != cfg.Region`, `rec.Move.To == rec.Region`, `receipt.Region != box.Region`, `canonical(rec.Region) != canonical(cfg.Region)`} {
e, err := parser.ParseExpr(expr)
if err != nil {
t.Fatal(err)
@@ -93,3 +139,42 @@ func TestPlan13RegionComparisonLintWitness(t *testing.T) {
}
}
}
+
+func TestPlan13RegionCallAndMapLintWitness(t *testing.T) {
+ for _, expr := range []string{
+ `containsString(c.Touched.Regions, rec.Move.To)`,
+ `slices.Contains(c.Touched.Regions, rec.Move.To)`,
+ `strings.EqualFold(rec.Region, actor.Region)`,
+ `same(rec.Move.From, verified.Region())`,
+ `same(canonical(rec.Move.From), canonical(verified.Region()))`,
+ `c.QuietAfter[rec.Move.To]`,
+ `c.QuietAfter[canonical(rec.Move.To)]`,
+ `regions[rec.Region]`,
+ `slices.Contains(c.Touched.Regions, park.PlacementRegion(rec.Move.To))`,
+ `c.QuietAfter[park.PlacementRegion(rec.Move.To)]`,
+ } {
+ e, err := parser.ParseExpr(expr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !plan13MixedRegionNode(e) {
+ t.Fatal("missed", expr)
+ }
+ }
+ for _, expr := range []string{
+ `containsString(c.Touched.Boxes, box)`,
+ `strings.EqualFold(rec.Region, cfg.Region)`,
+ `park.PlacementMatches(home, rec.Region, actor.Region)`,
+ `park.SameRegion(home, park.PlacementRegion(rec.Region), actor.Region)`,
+ `park.QuietUntil(c.QuietAfter, home, park.PlacementRegion(rec.Move.To))`,
+ `fleet.targets[rec.Region]`,
+ } {
+ e, err := parser.ParseExpr(expr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if plan13MixedRegionNode(e) {
+ t.Fatal("false positive", expr)
+ }
+ }
+}
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go
index 11f42b8cb..ca41bae39 100644
--- a/internal/console/apiinstancemove.go
+++ b/internal/console/apiinstancemove.go
@@ -251,7 +251,11 @@ func stagingCodes(caps map[string]placementCapability) string {
// record's "" (the unconstrained value the fleet's reconcile has always
// scoped by). The record NEVER stores the code; the API never shows "".
func (s *Server) homeRegionIn(region string) string {
- return homePlacementCode(s.cfg.HomeRegion, region)
+ home := s.cfg.HomeRegion
+ if s.cfg.HostClaim {
+ home = s.cfg.dbClaimHomeRegion()
+ }
+ return homePlacementCode(home, region)
}
// homePlacementCode is homeRegionIn's rule for a given home name: the
@@ -264,6 +268,9 @@ func homePlacementCode(home, region string) string {
func (s *Server) homeRegionOut(region string) string {
if region == "" {
+ if s.cfg.HostClaim {
+ return s.cfg.dbClaimHomeRegion()
+ }
return s.cfg.HomeRegion
}
return region
@@ -767,7 +774,7 @@ func (s *Server) startClaimModeMove(w http.ResponseWriter, r *http.Request, acct
ctx := r.Context()
a := s.cfg.ClaimAuthority
to := ms.To
- if !claimMoveRosterPlaces(roster.Roster, obs.Claim.Op.Exec, target, recRegion, to, s.cfg.HomeRegion) {
+ if !roster.Roster.MatchesHome(s.cfg.dbClaimHomeRegion()) || !claimMoveRosterPlaces(roster.Roster, obs.Claim.Op.Exec, target, recRegion, to, s.cfg.dbClaimHomeRegion()) {
jsonError(w, http.StatusConflict, "the host roster does not allow this move. the source and target must be active boxes in "+from+" and "+to)
return
}
@@ -804,7 +811,7 @@ func (s *Server) startClaimModeMove(w http.ResponseWriter, r *http.Request, acct
}
return
}
- moving, err := hostops.BeginClaimMove(ctx, s.cfg.Releases, a.Directory, h, obs, ms.ID, ms.By, target, to, ms.Now, s.cfg.HomeRegion)
+ moving, err := hostops.BeginClaimMove(ctx, s.cfg.Releases, a.Directory, h, obs, ms.ID, ms.By, target, to, ms.Now, s.cfg.dbClaimHomeRegion())
if err != nil {
switch {
case errors.Is(err, hostops.ErrIdentity), errors.Is(err, hostactors.ErrRoster):
@@ -1568,7 +1575,7 @@ func (s *Server) regionHasLiveBox(ctx context.Context, region string) (live, kno
aws = c.AWSRegion
}
}
- return len(eligibleBoxes(snap, roster, region, aws, s.cfg.HomeRegion, s.expectedSecretsFP(), nil)) > 0, true
+ return len(eligibleBoxes(snap, roster, region, aws, s.cfg.dbClaimHomeRegion(), s.expectedSecretsFP(), nil)) > 0, true
}
// ---- the delete door during a move (Plan 6 Task 2b, preflight L6) ----
diff --git a/internal/console/apiinstancemove_claim_test.go b/internal/console/apiinstancemove_claim_test.go
index 0604c3427..99698a97a 100644
--- a/internal/console/apiinstancemove_claim_test.go
+++ b/internal/console/apiinstancemove_claim_test.go
@@ -2,6 +2,7 @@ package console
import (
"context"
+ "crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -571,6 +572,16 @@ func TestMoveDoorHostClaimPlacesAHomeSource(t *testing.T) {
if rec, err := provision.GetRecord(ctx, fx.rel, "bab", "titogo", "acme.go.tito.io"); err != nil || rec.Region != "" {
t.Fatalf("fixture: the record is not home's: %+v %v", rec, err)
}
+ d := hostactors.Directory{Store: fx.rel, PublicKey: fx.priv.Public().(ed25519.PublicKey), Bucket: moveTestBucket}
+ roster, err := d.Read(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ next := roster.Roster
+ next.HomeRegion = "ie"
+ if _, err := d.Replace(ctx, release.RootSigner(fx.priv), roster, next); err != nil {
+ t.Fatal(err)
+ }
armClaimMoves(t, fx, true, true, nil)
code, _, out := rawReq(t, http.MethodPost, u+"/acme.go.tito.io/move", token, `{"region":"uk"}`)
if code != http.StatusAccepted {
diff --git a/internal/console/claim_invariant_world_test.go b/internal/console/claim_invariant_world_test.go
index e7afb98fd..ac9e74b26 100644
--- a/internal/console/claim_invariant_world_test.go
+++ b/internal/console/claim_invariant_world_test.go
@@ -545,6 +545,18 @@ func newClaimCaseWith(t *testing.T, opts ...func(*Config)) *claimCase {
for _, opt := range opts {
opt(&cfg)
}
+ if home := cfg.dbClaimHomeRegion(); home != "" {
+ d := hostactors.Directory{Store: rawStore, PublicKey: pub, Bucket: claimFxBucket}
+ o, e := d.Read(ctx)
+ if e != nil {
+ t.Fatal(e)
+ }
+ r := o.Roster
+ r.HomeRegion = home
+ if _, e = d.Replace(ctx, release.RootSigner(priv), o, r); e != nil {
+ t.Fatal(e)
+ }
+ }
srv, err = NewServer(cfg, st)
if err != nil {
t.Fatalf("NewServer with HostClaim: %v", err)
diff --git a/internal/console/claim_runtime.go b/internal/console/claim_runtime.go
index 6192b81f0..570ae1d52 100644
--- a/internal/console/claim_runtime.go
+++ b/internal/console/claim_runtime.go
@@ -62,6 +62,9 @@ func (s *Server) ConfigureClaimRuntime(ctx context.Context) error {
if err != nil {
return err
}
+ if !roster.Roster.MatchesHome(s.cfg.dbClaimHomeRegion()) {
+ return hostops.ErrContract
+ }
self, ok := roster.Roster.Consoles[s.cfg.HostConsoleID]
if !ok || len(self.Namespaces) != 1 {
return hostops.ErrContract
diff --git a/internal/console/movesrecovery.go b/internal/console/movesrecovery.go
index 0d97102cd..2b60f6496 100644
--- a/internal/console/movesrecovery.go
+++ b/internal/console/movesrecovery.go
@@ -366,7 +366,7 @@ func (s *Server) movesRecovery(ctx context.Context, g *hostops.MoveRecoveryGrant
s.pageMove(ctx, movePageCloseOverdue, o, c, rec, now)
return nil
case moveActAbortOverdue:
- if s.moveAbortOverdue(o, park.QuietUntil(c.QuietAfter, s.cfg.HomeRegion, park.PlacementRegion(rec.Move.To)), now) {
+ if s.moveAbortOverdue(o, park.QuietUntil(c.QuietAfter, s.cfg.dbClaimHomeRegion(), park.PlacementRegion(rec.Move.To)), now) {
s.pageMove(ctx, movePageAbortOverdue, o, c, rec, now)
}
return nil
diff --git a/internal/console/plan13_region_test.go b/internal/console/plan13_region_test.go
index 0f3ad3048..5a0ed2e3f 100644
--- a/internal/console/plan13_region_test.go
+++ b/internal/console/plan13_region_test.go
@@ -1,6 +1,7 @@
package console
import (
+ "github.com/carlosframework/platform/internal/hostactors"
"github.com/carlosframework/platform/internal/park"
"testing"
)
@@ -34,3 +35,29 @@ func TestPlan13ConsoleClaimRegion(t *testing.T) {
}
}
}
+
+func TestPlan13ConsoleResolvedHomeAgreement(t *testing.T) {
+ for _, cfg := range []Config{
+ {HostClaim: true, HomeRegion: "ie", HibernateRegion: "eu-west-1", HibernateBucket: "data"},
+ {HostClaim: true, HibernateRegion: "eu-west-1", HibernateBucket: "data"},
+ } {
+ home := cfg.dbClaimHomeRegion()
+ if !(hostactors.Roster{HomeRegion: home}).MatchesHome(home) {
+ t.Fatal(cfg)
+ }
+ if (hostactors.Roster{HomeRegion: "ca"}).MatchesHome(home) {
+ t.Fatal("conflicting console home accepted")
+ }
+ }
+}
+
+func TestPlan13UnnamedHomeMoveNames(t *testing.T) {
+ s := &Server{cfg: Config{HostClaim: true, HibernateRegion: "eu-west-1", HibernateBucket: "data"}}
+ if s.homeRegionOut("") != "eu-west-1" || s.homeRegionIn("eu-west-1") != "" {
+ t.Fatal("move door used a second home binding")
+ }
+ s.cfg.HostClaim = false
+ if s.homeRegionOut("") != "" || s.homeRegionIn("eu-west-1") != "eu-west-1" {
+ t.Fatal("flag-off changed")
+ }
+}
diff --git a/internal/host/claimassembly.go b/internal/host/claimassembly.go
index 9c51e3d7f..0e6e41ae1 100644
--- a/internal/host/claimassembly.go
+++ b/internal/host/claimassembly.go
@@ -156,7 +156,7 @@ func (a ClaimAssembly) Build(ctx context.Context) (ClaimProviders, error) {
}
a.Guard.Claims = hostguard.StoreReader{Store: a.Claims}
// Continue is the moves C1 session table, nil unless both flags are on (D1).
- source := hostbox.WorkSource{HomeRegion: actor.HomeRegion, Guard: a.Guard, Box: actor.Box, Region: park.PlacementRegion(actor.Region), Now: time.Now, Policy: a.Policy.Check, Local: a.Identity.Local(a.Local), Continue: a.Wake.Continue}
+ source := hostbox.WorkSource{FleetHomeRegion: actor.FleetHomeRegion, HomeRegion: actor.HomeRegion, Guard: a.Guard, Box: actor.Box, Region: park.PlacementRegion(actor.Region), Now: time.Now, Policy: a.Policy.Check, Local: a.Identity.Local(a.Local), Continue: a.Wake.Continue}
a.Reconcile.Reconciler.Claims = a.Claims
a.Reconcile.Reconciler.Guard = a.Guard
a.Reconcile.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: source}
diff --git a/internal/host/claimcleanup_identity.go b/internal/host/claimcleanup_identity.go
index 750032d1d..5b2d8cca3 100644
--- a/internal/host/claimcleanup_identity.go
+++ b/internal/host/claimcleanup_identity.go
@@ -35,7 +35,7 @@ func verifyClaimCleanupEnrollment(ctx context.Context, identity hostbox.Identity
if !ok || roster.Roster.RequestLimitNS != int64(hostbox.RequestLimit) {
return nil, ErrClaimStopIdentity
}
- actor := hostbox.Actor{ControlBucket: directory.Bucket, Box: row.BoxID, Enrollment: row.Enrollment, Revision: roster.Roster.Revision, Region: row.Region, Bucket: identity.Bucket, Namespace: row.Namespace, Cloud: hostbox.CloudIdentity{Provider: row.Cloud.Provider, Account: row.Cloud.Account, Region: row.Cloud.Region, InstanceID: row.Cloud.InstanceID}}
+ actor := hostbox.Actor{FleetHomeRegion: roster.Roster.HomeRegion, ControlBucket: directory.Bucket, Box: row.BoxID, Enrollment: row.Enrollment, Revision: roster.Roster.Revision, Region: row.Region, Bucket: identity.Bucket, Namespace: row.Namespace, Cloud: hostbox.CloudIdentity{Provider: row.Cloud.Provider, Account: row.Cloud.Account, Region: row.Cloud.Region, InstanceID: row.Cloud.InstanceID}}
identity.Roster = cleanupCloudRoster{actor}
if _, err := identity.Verify(ctx); err != nil {
return nil, err
diff --git a/internal/host/claimhandover_recovery.go b/internal/host/claimhandover_recovery.go
index 70bd9c624..38a190dbb 100644
--- a/internal/host/claimhandover_recovery.go
+++ b/internal/host/claimhandover_recovery.go
@@ -73,7 +73,7 @@ func (h claimCrossBoxHandover) recoveryEvidence(ctx context.Context, o hostclaim
if err != nil {
return err
}
- if verified.Box() != p.Box || !park.SameRegion(op.Intent["home_region"], verified.Region(), op.Intent["to"]) {
+ if (op.Intent["home_region"] != "" && op.Intent["home_region"] != verified.FleetHomeRegion()) || verified.Box() != p.Box || !park.SameRegion(op.Intent["home_region"], verified.Region(), op.Intent["to"]) {
return hostclaim.ErrFence
}
return nil
diff --git a/internal/host/claimlaunch_authority.go b/internal/host/claimlaunch_authority.go
index 7e7bb5e3d..e7258733b 100644
--- a/internal/host/claimlaunch_authority.go
+++ b/internal/host/claimlaunch_authority.go
@@ -106,7 +106,7 @@ func (h *ClaimLaunchRoot) authorize(ctx context.Context, peer StopPeer, q hostbo
if err := h.Kernel.VerifyDigest(ctx, q.Executable, r.Executable, q.Spec.Digest); err != nil {
return err
}
- selection := hostbox.Selection{Host: t.Host, Action: claimSpawnAction(c), Box: t.Box, Region: t.Region, Captured: hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: t.Session}
+ selection := hostbox.Selection{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Host: t.Host, Action: claimSpawnAction(c), Box: t.Box, Region: t.Region, Captured: hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: t.Session}
if err := h.Local.Check(ctx, actor, c, selection); err != nil {
return err
}
diff --git a/internal/host/claimproduction.go b/internal/host/claimproduction.go
index cdfd3ff8a..c5958ae80 100644
--- a/internal/host/claimproduction.go
+++ b/internal/host/claimproduction.go
@@ -64,6 +64,7 @@ func RegisterProductionClaimMoveSources(s ClaimProductionMoveSources) error {
// and the directory's row for the box, whose enrollment and cloud instance
// are the enrollment's.
type productionRosterRow struct {
+ home string
pub ed25519.PublicKey
enrollment hostfloor.Enrollment
directory hostactors.Directory
@@ -92,7 +93,7 @@ func readProductionRosterRow(ctx context.Context, store release.Store, getenv fu
if !ok || row.Enrollment != enrollment.Enrollment || row.Cloud.InstanceID != enrollment.InstanceID {
return rr, hostbox.ErrChanged
}
- return productionRosterRow{pub: pub, enrollment: enrollment, directory: directory, row: row}, nil
+ return productionRosterRow{home: roster.Roster.HomeRegion, pub: pub, enrollment: enrollment, directory: directory, row: row}, nil
}
// ProductionClaimOwnerProvider constructs claim-owned authority directly. Only
@@ -118,7 +119,7 @@ func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store r
return out, hostwrite.ErrUnavailable
}
authority := productionClaimRoster{Directory: directory, DataBucket: dataBucket, FloorDir: filepath.Join(r.OptDir, "system", "guard")}
- _, homeRegion, err := claimRegionBinding(row.Region, os.Getenv("CARLOS_REGION"))
+ _, homeRegion, err := claimRegionBinding(row.Region, os.Getenv("CARLOS_REGION"), rr.home)
if err != nil {
return out, err
}
@@ -345,7 +346,7 @@ func (p productionClaimRoster) Actor(ctx context.Context, box string) (hostbox.A
if local.Digest != remote.Digest || o.Roster.Publication != "published" || o.Roster.FloorSHA256 != remote.Digest {
return a, hostfloor.ErrFloor
}
- return hostbox.Actor{ControlBucket: p.Directory.Bucket, Box: box, Enrollment: b.Enrollment, Revision: o.Roster.Revision, Region: b.Region, Bucket: p.DataBucket, Namespace: b.Namespace, Eligible: true, Cloud: hostbox.CloudIdentity{Provider: b.Cloud.Provider, Account: b.Cloud.Account, Region: b.Cloud.Region, InstanceID: b.Cloud.InstanceID}}, nil
+ return hostbox.Actor{FleetHomeRegion: o.Roster.HomeRegion, ControlBucket: p.Directory.Bucket, Box: box, Enrollment: b.Enrollment, Revision: o.Roster.Revision, Region: b.Region, Bucket: p.DataBucket, Namespace: b.Namespace, Eligible: true, Cloud: hostbox.CloudIdentity{Provider: b.Cloud.Provider, Account: b.Cloud.Account, Region: b.Cloud.Region, InstanceID: b.Cloud.InstanceID}}, nil
}
type productionClaimRegional struct {
@@ -366,7 +367,7 @@ func (p productionClaimRegional) View(ctx context.Context, o hostclaim.Observed)
if o.Claim == nil {
return park.View{}, park.ErrUnsafe
}
- home, err := park.BoundHome(actor.HomeRegion, o.Claim.Op.Intent["home_region"])
+ home, err := park.VerifiedHome(actor.FleetHomeRegion, actor.HomeRegion, actor.Region, o.Claim.Op.Intent["home_region"])
if err != nil {
return park.View{}, err
}
@@ -458,7 +459,7 @@ func (t claimAdminTokens) With(ctx context.Context, host string, effect func() e
return err
}
c, h := o.Claim, o.Claim.Holder
- sel := hostbox.Selection{Host: host, Action: "spawn", Box: actor.Box, Region: actor.Region, Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: h.Generation, Session: h.Session}
+ sel := hostbox.Selection{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Host: host, Action: "spawn", Box: actor.Box, Region: actor.Region, Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: h.Generation, Session: h.Session}
check := func(c *hostclaim.Claim) error {
if c.Op.Kind != sel.Kind || c.Op.ID != sel.Op || c.Op.Phase != sel.Phase || c.Holder.Incarnation != sel.Captured.Incarnation || c.Holder.Stamp != sel.Captured.Stamp || c.Holder.Session != sel.Session || c.Holder.Generation != sel.Generation {
return hostbox.ErrChanged
diff --git a/internal/host/claimrecoveryreceipt.go b/internal/host/claimrecoveryreceipt.go
index b9d9c26d5..5d6052101 100644
--- a/internal/host/claimrecoveryreceipt.go
+++ b/internal/host/claimrecoveryreceipt.go
@@ -46,7 +46,7 @@ func (h claimCrossBoxHandover) recoveryStopReceipt(ctx context.Context, o hostcl
return err
}
box, ok := roster.Roster.Boxes[p.Box]
- if !ok || box.Enrollment != r.Enrollment || !park.SameRegion("", box.Region, r.Region) {
+ if (c.Op.Intent["home_region"] != "" && !roster.Roster.MatchesHome(c.Op.Intent["home_region"])) || !ok || box.Enrollment != r.Enrollment || !park.SameRegion("", box.Region, r.Region) {
return ErrClaimStopIdentity
}
sig, _, err := h.Evidence.Get(ctx, key+".sig")
diff --git a/internal/host/claimregion.go b/internal/host/claimregion.go
index 7fb2e8a41..673c391cb 100644
--- a/internal/host/claimregion.go
+++ b/internal/host/claimregion.go
@@ -5,15 +5,18 @@ import (
"github.com/carlosframework/platform/internal/park"
)
-// The signed roster retains the claim's engine name. CARLOS_REGION supplies
-// this box's placement code; only its empty home placement binds an alias.
-func claimRegionBinding(roster, code string) (engine, home string, err error) {
+// The signed roster supplies both the engine name and the fleet home.
+// An empty local placement binds an alias only when those names agree.
+func claimRegionBinding(roster, code, fleetHome string) (engine, home string, err error) {
engine = park.PlacementRegion(roster)
placement := park.PlacementRegion(code)
if roster == "" || engine == "" || placement == "" || (code != "" && engine != placement) {
return "", "", hostbox.ErrChanged
}
if code == "" {
+ if fleetHome == "" || roster != fleetHome {
+ return "", "", hostbox.ErrChanged
+ }
home = engine
}
return engine, home, nil
diff --git a/internal/host/movesland_probe_test.go b/internal/host/movesland_probe_test.go
index aae06d1fd..467141f59 100644
--- a/internal/host/movesland_probe_test.go
+++ b/internal/host/movesland_probe_test.go
@@ -66,7 +66,10 @@ func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activat
}
// The predecessor's region is the probe owners' own rule: the last
// admission's region, or the move's source's engine region.
- from := predecessorRegion(h, m.From)
+ from, err := predecessorRegion(h, m.From)
+ if err != nil {
+ return activator.ProbeResult{}, err
+ }
admitted, err := hostclaim.Update(ctx, p.h.s, obs, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: p1ID, ObservedPhase: provision.LandProbing,
ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "uk-box", Mode: "cold", NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: "uk", PredecessorRegion: from}))
if err != nil {
diff --git a/internal/host/movesphase1.go b/internal/host/movesphase1.go
index 192deae17..ba94c7068 100644
--- a/internal/host/movesphase1.go
+++ b/internal/host/movesphase1.go
@@ -11,6 +11,7 @@ import (
"log"
"os"
"path/filepath"
+ "slices"
"strings"
"time"
@@ -319,7 +320,9 @@ func (w *movesWorker) sourceRound(ctx context.Context, s release.Store, rec prov
// K12 and T9: the target region and box are in touched before the
// first staging write.
targetBox := obs.Claim.Op.Intent["target"]
- if !containsString(obs.Claim.Touched.Regions, park.PlacementRegion(rec.Move.To)) || (targetBox != "" && !containsString(obs.Claim.Touched.Boxes, targetBox)) {
+ if !slices.ContainsFunc(obs.Claim.Touched.Regions, func(region string) bool {
+ return park.PlacementMatches(obs.Claim.Op.Intent["home_region"], rec.Move.To, region)
+ }) || (targetBox != "" && !containsString(obs.Claim.Touched.Boxes, targetBox)) {
var boxes []string
if targetBox != "" {
boxes = []string{targetBox}
diff --git a/internal/host/movespolicy.go b/internal/host/movespolicy.go
index e60ba6afd..0d0df30fb 100644
--- a/internal/host/movespolicy.go
+++ b/internal/host/movespolicy.go
@@ -297,7 +297,10 @@ func MovesClaimPolicyWithInputs(read MovesRecordReader, committed func(context.C
if len(c.Op.Participants) != 2 || c.Op.Participants[1] != s.Box || c.Op.Intent["target"] != s.Box || c.Op.Intent["source"] == s.Box {
return refuse("this box is not the move's target", nil)
}
- if !park.SameRegion(c.Op.Intent["home_region"], c.Op.Intent["to"], s.Region) {
+ if _, err := park.VerifiedHome(s.FleetHomeRegion, s.HomeRegion, s.Region, c.Op.Intent["home_region"]); err != nil {
+ return refuse("this region is not the move's target region", err)
+ }
+ if !park.SameRegion(s.HomeRegion, c.Op.Intent["to"], s.Region) {
return refuse("this region is not the move's target region", nil)
}
for _, lost := range c.Lost {
@@ -416,7 +419,10 @@ func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostcla
if len(c.Op.Participants) == 0 || c.Op.Participants[0] != s.Box || c.Op.Intent["source"] != s.Box || c.Op.Intent["target"] == s.Box {
return refuse("this box is not the move's source", nil)
}
- if park.SameRegion(c.Op.Intent["home_region"], c.Op.Intent["to"], s.Region) {
+ if _, err := park.VerifiedHome(s.FleetHomeRegion, s.HomeRegion, s.Region, c.Op.Intent["home_region"]); err != nil {
+ return refuse("this region is the move's target region", err)
+ }
+ if park.SameRegion(s.HomeRegion, c.Op.Intent["to"], s.Region) {
return refuse("this region is the move's target region", nil)
}
for _, lost := range c.Lost {
@@ -437,7 +443,7 @@ func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostcla
if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation {
return refuse("the record is not this move", nil)
}
- if !park.PlacementMatches(c.Op.Intent["home_region"], rec.Move.From, s.Region) {
+ if !park.PlacementMatches(s.HomeRegion, rec.Move.From, s.Region) {
return refuse("this region is not the move's source region", nil)
}
var (
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go
index ac9f2a678..ae575bcfb 100644
--- a/internal/host/movesprobeowners.go
+++ b/internal/host/movesprobeowners.go
@@ -92,6 +92,7 @@ import (
// Production: nothing registers this owner set yet. The probe is unarmed in
// production until Plan 5 Task 9 arms the parks; the tests use fakes.
type movesProbeOwners struct {
+ HomeRegion, FleetHomeRegion string
// Claims is the claim store (the deployment store in production).
Claims release.Store
Records MovesRecordReader
@@ -203,7 +204,11 @@ func (m *movesProbeOwners) landing(ctx context.Context, o hostclaim.Observed) (p
return v, hostbox.ErrUnsupported
}
c := o.Claim
- v.homeRegion = c.Op.Intent["home_region"]
+ var bindingErr error
+ v.homeRegion, bindingErr = park.VerifiedHome(m.FleetHomeRegion, m.HomeRegion, m.Region, c.Op.Intent["home_region"])
+ if bindingErr != nil {
+ return v, bindingErr
+ }
switch c.Op.Phase {
case hostclaim.PhaseLandingProbing:
case hostclaim.PhaseLandingCommitted:
@@ -212,7 +217,7 @@ func (m *movesProbeOwners) landing(ctx context.Context, o hostclaim.Observed) (p
return v, hostbox.ErrChanged
}
if c.Host != o.Host || c.Op.ID == "" || c.Op.Exec != m.Box || c.Op.Intent["target"] != m.Box ||
- c.Op.Intent["source"] == m.Box || !park.SameRegion(c.Op.Intent["home_region"], c.Op.Intent["to"], m.Region) || len(c.Op.Participants) != 2 || c.Op.Participants[1] != m.Box {
+ c.Op.Intent["source"] == m.Box || !park.SameRegion(m.HomeRegion, c.Op.Intent["to"], m.Region) || len(c.Op.Participants) != 2 || c.Op.Participants[1] != m.Box {
return v, hostbox.ErrChanged
}
for _, lost := range c.Lost {
@@ -229,7 +234,7 @@ func (m *movesProbeOwners) landing(ctx context.Context, o hostclaim.Observed) (p
return v, hostbox.ErrChanged
}
mv := rec.Move
- if park.PlacementRegion(mv.From) == "" || mv.ID != v.op || mv.Incarnation != v.holder.Incarnation || !park.PlacementMatches(c.Op.Intent["home_region"], mv.To, m.Region) || mv.Inputs == nil || mv.Inputs.Release == "" {
+ if park.PlacementRegion(mv.From) == "" || mv.ID != v.op || mv.Incarnation != v.holder.Incarnation || !park.PlacementMatches(m.HomeRegion, mv.To, m.Region) || mv.Inputs == nil || mv.Inputs.Release == "" {
return v, hostbox.ErrChanged
}
if v.committed {
@@ -366,7 +371,7 @@ func (m *movesProbeOwners) descendsFromMove(h *hostclaim.Holder, g, op string) b
if a.Generation != g || a.Mode != "cold" {
continue
}
- if a.Box != m.Box || a.Region != m.Region {
+ if a.Box != m.Box || !park.SameRegion(m.HomeRegion, a.Region, m.Region) {
return false
}
g, found = a.Predecessor, true
@@ -380,7 +385,7 @@ func (m *movesProbeOwners) descendsFromMove(h *hostclaim.Holder, g, op string) b
return false
}
for _, a := range h.Admissions {
- if a.Generation == op && a.Box == m.Box && a.Region == m.Region && a.Mode == "cold" {
+ if a.Generation == op && a.Box == m.Box && park.SameRegion(m.HomeRegion, a.Region, m.Region) && a.Mode == "cold" {
return true
}
}
@@ -389,24 +394,18 @@ func (m *movesProbeOwners) descendsFromMove(h *hostclaim.Holder, g, op string) b
// predecessorRegion is AdmitWriter's predecessor region: the last
// admission's, or the move's source region before any.
-func predecessorRegion(h *hostclaim.Holder, from string) string {
+func predecessorRegion(h *hostclaim.Holder, from string) (string, error) {
+ if !provision.ValidRegion(from) {
+ return "", hostbox.ErrChanged
+ }
if n := len(h.Admissions); n > 0 {
- return h.Admissions[n-1].Region
- }
- // from is the move's source placement code. Its engine region is the
- // predecessor's (park.HomeRegion for the home's ""): the claim reads a
- // predecessor region of "" as holding nothing anywhere and covers it
- // with no chore, which a home generation does not (home-region parks,
- // part 1). A code that is no placement is kept as it is: it names no
- // region, so nothing covers it by mistake.
- if r := park.PlacementRegion(from); r != "" {
- return r
- }
- return from
+ return h.Admissions[n-1].Region, nil
+ }
+ return park.PlacementRegion(from), nil
}
-func (m *movesProbeOwners) request(host string, h *hostclaim.Holder, home string) genselect.Request {
- return genselect.Request{Bucket: m.Bucket, Region: m.Region, Host: host, Holder: park.HolderRegionView(*h, home, m.Region)}
+func (m *movesProbeOwners) request(host string, h *hostclaim.Holder) genselect.Request {
+ return genselect.Request{Bucket: m.Bucket, Region: m.Region, Host: host, Holder: park.HolderRegionView(*h, m.HomeRegion, m.Region)}
}
func preparedPin(v probeLanding) genselect.PreparedPin {
@@ -444,7 +443,7 @@ func (m *movesProbeOwners) currentMarker(ctx context.Context, h *hostclaim.Holde
// certified remote once it is.
func (m *movesProbeOwners) baseline(ctx context.Context, host string, v probeLanding, prepared bool) (*genselect.Plan, error) {
if prepared {
- return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder, v.homeRegion), preparedPin(v))
+ return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v))
}
digest, err := manifestDigest(v.manifest)
if err != nil {
@@ -455,9 +454,9 @@ func (m *movesProbeOwners) baseline(ctx context.Context, host string, v probeLan
return nil, err
}
if !m.landingMarker(marker, host, v, digest) {
- return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder, v.homeRegion), preparedPin(v))
+ return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v))
}
- return genselect.Select(ctx, m.Data, m.request(host, v.holder, v.homeRegion))
+ return genselect.Select(ctx, m.Data, m.request(host, v.holder))
}
// localMarkers reads .files-gen (nil when absent) and <dbPath>.gen ("" when
@@ -534,8 +533,12 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host
}
h := v.holder
prepared := firstAdmission(h, m.Box)
+ predecessor, err := predecessorRegion(h, v.rec.Move.From)
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
admission := hostclaim.Admission{ObservedOpID: v.op, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: m.Box, Mode: "cold",
- KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessorRegion(h, v.rec.Move.From)}
+ KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessor}
if prepared {
files, db, err := m.localMarkers(o.Host, rt.DBPath)
if err != nil {
@@ -597,9 +600,13 @@ func (m *movesProbeOwners) AdmissionEvidence(ctx context.Context, o hostclaim.Ob
return err
}
h := v.holder
- if in.ObservedOpID != v.op || in.ObservedPhase != o.Claim.Op.Phase || in.ObservedSession != h.Session || in.ObservedGeneration != h.Generation || in.Box != m.Box || in.Region != m.Region ||
+ predecessor, err := predecessorRegion(h, v.rec.Move.From)
+ if err != nil {
+ return err
+ }
+ if in.ObservedOpID != v.op || in.ObservedPhase != o.Claim.Op.Phase || in.ObservedSession != h.Session || in.ObservedGeneration != h.Generation || in.Box != m.Box || !park.SameRegion(m.HomeRegion, in.Region, m.Region) ||
in.Mode != "cold" || in.PredecessorCertified || in.LossID != "" || len(in.AdditionalWritesLost) != 0 || in.KeepStamp != (h.Stamp > h.Session) ||
- in.PredecessorRegion != predecessorRegion(h, v.rec.Move.From) || baseline == nil || in.RemoteMaxTXID != baseline.MaxTXID {
+ !park.SameRegion(v.homeRegion, in.PredecessorRegion, predecessor) || baseline == nil || in.RemoteMaxTXID != baseline.MaxTXID {
return hostbox.ErrChanged
}
prepared := firstAdmission(h, m.Box)
@@ -988,7 +995,7 @@ func (m *movesProbeOwners) PublishFenced(ctx context.Context, o hostclaim.Observ
}
bounded, cancel := context.WithTimeout(ctx, hostbox.RequestLimit)
defer cancel()
- plan, err := genselect.Select(bounded, m.Data, m.request(o.Host, h, o.Claim.Op.Intent["home_region"]))
+ plan, err := genselect.Select(bounded, m.Data, m.request(o.Host, h))
if err != nil {
return ClaimPublicationReceipt{}, err
}
@@ -1215,7 +1222,7 @@ func (m *movesProbeOwners) PublicationEvidence(ctx context.Context, o hostclaim.
if !ok || err != nil || receipt.MarkerVersion == "" || receipt.Root.VersionID == "" || !strings.HasPrefix(receipt.Root.Key, prefix) {
return hostbox.ErrChanged
}
- plan, err := genselect.Select(ctx, m.Data, m.request(o.Host, h, o.Claim.Op.Intent["home_region"]))
+ plan, err := genselect.Select(ctx, m.Data, m.request(o.Host, h))
if err != nil {
return err
}
@@ -1286,10 +1293,14 @@ func withoutProbeVars(env []string) []string {
return out
}
-func (m *movesProbeOwners) committedAdmission(o hostclaim.Observed, v probeLanding, gen string, plan *genselect.Plan) hostclaim.Admission {
+func (m *movesProbeOwners) committedAdmission(o hostclaim.Observed, v probeLanding, gen string, plan *genselect.Plan) (hostclaim.Admission, error) {
h := v.holder
+ predecessor, err := predecessorRegion(h, v.rec.Move.From)
+ if err != nil {
+ return hostclaim.Admission{}, err
+ }
return hostclaim.Admission{ObservedOpID: v.op, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: m.Box, Mode: "cold",
- NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessorRegion(h, v.rec.Move.From), RemoteMaxTXID: plan.MaxTXID}
+ NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessor, RemoteMaxTXID: plan.MaxTXID}, nil
}
func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Route, o hostclaim.Observed, v probeLanding) (ClaimLaunchInputs, error) {
@@ -1304,7 +1315,7 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout
if gen == v.op || !claimFreshGeneration(h, gen) {
return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
}
- plan, err := genselect.Select(ctx, m.Data, m.request(o.Host, h, o.Claim.Op.Intent["home_region"]))
+ plan, err := genselect.Select(ctx, m.Data, m.request(o.Host, h))
if err != nil {
return ClaimLaunchInputs{}, err
}
@@ -1322,7 +1333,11 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout
if pins.EnvReport == "" || id != pinnedLaunchIdentity(pins) {
return ClaimLaunchInputs{}, fmt.Errorf("%w: the launch is not the move's pinned inputs", hostbox.ErrChanged)
}
- return ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: m.committedAdmission(o, v, gen, plan),
+ admission, err := m.committedAdmission(o, v, gen, plan)
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ return ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: admission,
Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: withoutProbeVars(l.Env),
DBPath: rt.DBPath, Version: l.Version, Limit: l.Limit}, nil
}
@@ -1332,10 +1347,14 @@ func (m *movesProbeOwners) committedEvidence(ctx context.Context, o hostclaim.Ob
if baseline == nil || h.WriterBox != m.Box || in.NewGeneration == v.op || !claimFreshGeneration(h, in.NewGeneration) || in.PredecessorCertified || in.LossID != "" || len(in.AdditionalWritesLost) != 0 {
return hostbox.ErrChanged
}
- if !reflect.DeepEqual(in, m.committedAdmission(o, v, in.NewGeneration, baseline)) {
+ want, err := m.committedAdmission(o, v, in.NewGeneration, baseline)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(in, want) {
return hostbox.ErrChanged
}
- fresh, err := genselect.Select(ctx, m.Data, m.request(o.Host, h, o.Claim.Op.Intent["home_region"]))
+ fresh, err := genselect.Select(ctx, m.Data, m.request(o.Host, h))
if err != nil {
return err
}
@@ -1509,7 +1528,7 @@ func (m *movesProbeOwners) unpublishedPath(h *hostclaim.Holder, from, to string)
break
}
}
- if found == nil || found.Box != m.Box || found.Region != m.Region || found.Predecessor == "" {
+ if found == nil || found.Box != m.Box || !park.SameRegion(m.HomeRegion, found.Region, m.Region) || found.Predecessor == "" {
return nil, false
}
g = found.Predecessor
diff --git a/internal/host/movesreport.go b/internal/host/movesreport.go
index abf3362ea..97a47cf4c 100644
--- a/internal/host/movesreport.go
+++ b/internal/host/movesreport.go
@@ -12,6 +12,7 @@ import (
"log"
"os"
"path/filepath"
+ "slices"
"strings"
"time"
@@ -243,7 +244,7 @@ func (w *movesWorker) targetReport(ctx context.Context, s release.Store, rec pro
}
rep.Checks[moves.CheckDisk] = w.checkDisk(max(staged, dbBytes), replicaErr, art)
rep.Checks[moves.CheckRelease] = w.checkRelease(ctx, s, rec, source)
- rep.Checks[moves.CheckOneOwner] = checkOneOwner(fleet, rec.Move.To)
+ rep.Checks[moves.CheckOneOwner] = checkOneOwner(fleet, rec.Move.To, moveRegionName(obs.Claim, park.PlacementRegion(rec.Move.To)))
rep.Checks[moves.CheckClaim] = w.checkClaim(ctx, obs, rec)
rep.Checks[moves.CheckLane] = w.checkLane(ctx, rec, fleet)
rep.Checks[moves.CheckQuiet] = checkQuiet(obs, park.PlacementRegion(rec.Move.To), now)
@@ -705,9 +706,13 @@ func (w *movesWorker) checkRelease(ctx context.Context, s release.Store, rec pro
// checkOneOwner is T8: exactly one hosting box publishes the target
// placement.
-func checkOneOwner(fleet moveFleet, to string) moves.Check {
+func checkOneOwner(fleet moveFleet, to string, names ...string) moves.Check {
+ name := moves.RegionText(to)
+ if len(names) == 1 && names[0] != "" {
+ name = names[0]
+ }
if n := len(fleet.boxes[to]); n != 1 {
- return moves.Check{Reason: t8Count(n, to)}
+ return moves.Check{Reason: t8Count(n, name)}
}
return moves.Check{OK: true}
}
@@ -724,8 +729,10 @@ func (w *movesWorker) checkClaim(ctx context.Context, obs hostclaim.Observed, re
if c.Holder == nil || c.Holder.Incarnation != rec.Move.Incarnation {
return moves.Check{Reason: t9Incarnation}
}
- if !containsString(c.Touched.Regions, rec.Move.To) {
- return moves.Check{Reason: t9Untouched(rec.Move.To)}
+ if !slices.ContainsFunc(c.Touched.Regions, func(region string) bool {
+ return park.PlacementMatches(c.Op.Intent["home_region"], rec.Move.To, region)
+ }) {
+ return moves.Check{Reason: t9Untouched(moveRegionName(c, park.PlacementRegion(rec.Move.To)))}
}
if !containsString(c.Touched.Boxes, w.cfg.Box) {
return moves.Check{Reason: t9Untouched(w.cfg.Box)}
@@ -773,7 +780,7 @@ func (w *movesWorker) checkLane(ctx context.Context, rec provision.Record, fleet
// checkQuiet is T12: the claim's quiet_after for the target region passed.
func checkQuiet(obs hostclaim.Observed, region string, now time.Time) moves.Check {
if until := park.QuietUntil(obs.Claim.QuietAfter, obs.Claim.Op.Intent["home_region"], region); !until.IsZero() && now.Before(until) {
- return moves.Check{Reason: t12NotQuiet(region, until)}
+ return moves.Check{Reason: t12NotQuiet(moveRegionName(obs.Claim, region), until)}
}
return moves.Check{OK: true}
}
@@ -811,3 +818,13 @@ func (w *movesWorker) measure(ctx context.Context, rec provision.Record) moves.M
w.mu.Unlock()
return m
}
+
+func moveRegionName(c *hostclaim.Claim, region string) string {
+ if park.SameRegion(c.Op.Intent["home_region"], region, park.HomeRegion) {
+ if home := c.Op.Intent["home_region"]; home != "" {
+ return home
+ }
+ return moves.HomeRegionText
+ }
+ return region
+}
diff --git a/internal/host/movessources.go b/internal/host/movessources.go
index 64907a727..73c975bbc 100644
--- a/internal/host/movessources.go
+++ b/internal/host/movessources.go
@@ -71,13 +71,8 @@ const movesNoLiveRegion = "@none"
// which every engine entry refuses.
func (c moveCfg) parkRegion() string { return park.PlacementRegion(c.Region) }
-// movesBoxRegion is the park view's region resolver in production: this box's
-// region as the claim knows it (resolve, the verified roster row), accepted
-// only when it is the engine region of this box's own placement code. A home
-// box (placement "") whose claim names it anything but park.HomeRegion, the
-// roster's own name for home among them, refuses: the view would otherwise
-// read a home record's "" as HomeRegion while this box's engine runs under
-// another name, and protect nothing of home's live set here.
+// movesBoxRegion refuses a placement that disagrees with the verified roster.
+// productionRosterRegion maps home only after checking the signed designation.
func movesBoxRegion(placement string, resolve func(context.Context) (string, error)) func(context.Context) (string, error) {
return func(ctx context.Context) (string, error) {
want := park.PlacementRegion(placement)
@@ -706,10 +701,10 @@ func productionRosterRegion(store release.Store, getenv func(string) string) fun
if err != nil {
return "", err
}
- _, _, err = claimRegionBinding(rr.row.Region, getenv("CARLOS_REGION"))
+ engine, home, err := claimRegionBinding(rr.row.Region, getenv("CARLOS_REGION"), rr.home)
if err != nil {
return "", err
}
- return park.PlacementRegion(getenv("CARLOS_REGION")), nil
+ return park.PlacementRegion(park.HomePlacementCode(home, engine)), nil
}
}
diff --git a/internal/host/plan12_handover_test.go b/internal/host/plan12_handover_test.go
index e4a6d1012..484ea9329 100644
--- a/internal/host/plan12_handover_test.go
+++ b/internal/host/plan12_handover_test.go
@@ -22,13 +22,16 @@ import (
"time"
)
-type plan12FenceAuthority struct{ reject bool }
+type plan12FenceAuthority struct {
+ reject bool
+ home string
+}
func (a plan12FenceAuthority) ValidateFence(context.Context, termfence.Record) (hostclaim.FenceBinding, error) {
if a.reject {
return hostclaim.FenceBinding{}, hostclaim.ErrFence
}
- return hostclaim.FenceBinding{Region: "uk", RosterRevision: 1, RosterETag: "roster"}, nil
+ return hostclaim.FenceBinding{FleetHomeRegion: a.home, Region: "uk", RosterRevision: 1, RosterETag: "roster"}, nil
}
func plan12Handover(t *testing.T, kind, mode string) (*recoveryFixture, hostclaim.Observed, claimCrossBoxHandover) {
t.Helper()
diff --git a/internal/host/plan13_amendments_test.go b/internal/host/plan13_amendments_test.go
index 30aae691d..2e266831a 100644
--- a/internal/host/plan13_amendments_test.go
+++ b/internal/host/plan13_amendments_test.go
@@ -18,8 +18,8 @@ func TestPlan13LandingInvalidSourceWithoutHistory(t *testing.T) {
if _, err := f.owners.landing(t.Context(), o); err == nil {
t.Fatal("invalid source accepted", from)
}
- if got := predecessorRegion(c.Holder, from); got != from || got == "" {
- t.Fatal("invalid predecessor became chore-free", got)
+ if got, err := predecessorRegion(c.Holder, from); err == nil || got != "" {
+ t.Fatal("invalid predecessor accepted", got, err)
}
}
}
diff --git a/internal/host/plan13_fix1_test.go b/internal/host/plan13_fix1_test.go
new file mode 100644
index 000000000..13383954b
--- /dev/null
+++ b/internal/host/plan13_fix1_test.go
@@ -0,0 +1,112 @@
+package host
+
+import (
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestPlan13HomeTargetReadiness(t *testing.T) {
+ for _, stored := range []string{park.HomeRegion, "ie", "ca"} {
+ t.Run(stored, func(t *testing.T) {
+ h := newPhase1Harness(t)
+ h.start(t, "manual")
+ rec := h.read(t, p1Host)
+ rec.Move.To = ""
+ obs, err := hostclaim.Read(t.Context(), h.s, p1Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ obs.Claim.Op.Intent["home_region"] = "ie"
+ obs.Claim.Touched.Regions = []string{stored}
+ obs.Claim.Touched.Boxes = append(obs.Claim.Touched.Boxes, "uk-box")
+ raw, err := json.Marshal(moves.MoveJSON{MoveID: rec.Move.ID, Incarnation: rec.Move.Incarnation, Host: rec.Host, From: rec.Move.From, To: rec.Move.To})
+ if err != nil {
+ t.Fatal(err)
+ }
+ h.f.store("data-uk", moves.MoveJSONKey(rec.Host, rec.Move.ID), fakeMoveObject{lease.MirrorObject{ETag: "x", Size: int64(len(raw))}, string(raw)})
+ w := &movesWorker{cfg: h.targetCfg(), mirrors: map[string]lease.MirrorStore{}}
+ got := w.checkClaim(t.Context(), obs, rec)
+ if got.OK != (stored != "ca") {
+ t.Fatalf("%s: %+v", stored, got)
+ }
+ if !got.OK && got.Reason != t9Untouched("ie") {
+ t.Fatal(got.Reason)
+ }
+ })
+ }
+}
+func TestPlan13HomeQuietReason(t *testing.T) {
+ now := time.Unix(1000, 0)
+ o := hostclaim.Observed{Claim: &hostclaim.Claim{Op: hostclaim.Op{Intent: map[string]string{"home_region": "ie"}}, QuietAfter: map[string]time.Time{park.HomeRegion: now.Add(time.Minute)}}}
+ got := checkQuiet(o, park.HomeRegion, now)
+ if got.OK || strings.Contains(got.Reason, park.HomeRegion) || got.Reason != t12NotQuiet("ie", now.Add(time.Minute)) {
+ t.Fatal(got)
+ }
+}
+
+func TestPlan13ProbeHomeHistory(t *testing.T) {
+ h := &hostclaim.Holder{Admissions: []hostclaim.AdmissionRecord{
+ {Generation: "move", Region: "ie", Box: "box", Mode: "cold"},
+ {Generation: "next", Predecessor: "move", Region: "@home", Box: "box", Mode: "cold"},
+ }}
+ for _, home := range []string{"ie", ""} {
+ m := &movesProbeOwners{Region: "ie", HomeRegion: home, Box: "box"}
+ if got := m.descendsFromMove(h, "next", "move"); got != (home != "") {
+ t.Fatal("descent", home, got)
+ }
+ _, ok := m.unpublishedPath(h, "next", "move")
+ if ok != (home != "") {
+ t.Fatal("unpublished path", home, ok)
+ }
+ }
+}
+
+func TestPlan13LandingUsesLocalBinding(t *testing.T) {
+ for _, tc := range []struct {
+ local, fleet, operation string
+ ok bool
+ }{
+ {"", "ie", "uk", false}, {"", "uk", "uk", false}, {"uk", "uk", "uk", true}, {"uk", "ie", "uk", false},
+ } {
+ f := newPOFixture(t)
+ c := f.poFlipClaim()
+ c.Op.Intent["to"], c.Op.Intent["home_region"] = park.HomeRegion, tc.operation
+ f.rec.Move.To = ""
+ f.owners.HomeRegion, f.owners.FleetHomeRegion = tc.local, tc.fleet
+ _, err := f.owners.landing(t.Context(), hostclaim.Observed{Host: poHost, Claim: c})
+ if (err == nil) != tc.ok {
+ t.Fatalf("%+v: %v", tc, err)
+ }
+ }
+}
+func TestPlan13OwnStorageProjectionIgnoresOperationHome(t *testing.T) {
+ h := &hostclaim.Holder{Admissions: []hostclaim.AdmissionRecord{{Region: park.HomeRegion, PredecessorRegion: park.HomeRegion}}}
+ m := &movesProbeOwners{Region: "uk"}
+ r := m.request("test.example", h)
+ if r.Holder.Admissions[0].Region != park.HomeRegion {
+ t.Fatal("named storage borrowed a home alias")
+ }
+ m.HomeRegion = "uk"
+ r = m.request("test.example", h)
+ if r.Holder.Admissions[0].Region != "uk" || h.Admissions[0].Region != park.HomeRegion {
+ t.Fatal("home view lost binding or rewrote history")
+ }
+}
+
+func TestPlan13HomeOwnerReason(t *testing.T) {
+ got := checkOneOwner(moveFleet{}, "", "ie")
+ if got.OK || got.Reason != t8Count(0, "ie") {
+ t.Fatal(got)
+ }
+ o := hostclaim.Observed{Claim: &hostclaim.Claim{QuietAfter: map[string]time.Time{park.HomeRegion: time.Unix(2, 0)}}}
+ got = checkQuiet(o, park.HomeRegion, time.Unix(1, 0))
+ if strings.Contains(got.Reason, park.HomeRegion) || strings.Contains(got.Reason, "left recently") {
+ t.Fatal(got)
+ }
+}
diff --git a/internal/host/plan13_policy_test.go b/internal/host/plan13_policy_test.go
index 7c36f0bfe..e8988990d 100644
--- a/internal/host/plan13_policy_test.go
+++ b/internal/host/plan13_policy_test.go
@@ -15,8 +15,23 @@ func TestPlan13HomeMovePolicy(t *testing.T) {
s := mpSelection(c, "restore")
policy, _ := mpPolicy()
err := policy(t.Context(), c, s, mpIdentity("restore"))
- if (err == nil) != (home != "") {
+ if err == nil {
t.Fatalf("home=%q: %v", home, err)
}
}
}
+
+func TestPlan13VerifiedHomeMovePolicy(t *testing.T) {
+ c := mpClaim(hostclaim.KindLanding, provision.LandConfigured)
+ c.Op.Intent["to"], c.Op.Intent["home_region"] = park.HomeRegion, "uk"
+ s := mpSelection(c, "restore")
+ s.HomeRegion, s.FleetHomeRegion = "uk", "uk"
+ policy, _ := mpPolicy()
+ if err := policy(t.Context(), c, s, mpIdentity("restore")); err != nil {
+ t.Fatal(err)
+ }
+ s.FleetHomeRegion = "ie"
+ if err := policy(t.Context(), c, s, mpIdentity("restore")); err == nil {
+ t.Fatal("conflicting fleet home accepted")
+ }
+}
diff --git a/internal/host/plan13_receipt_test.go b/internal/host/plan13_receipt_test.go
index 850aa3c5f..085992ef8 100644
--- a/internal/host/plan13_receipt_test.go
+++ b/internal/host/plan13_receipt_test.go
@@ -26,7 +26,7 @@ func TestPlan13RecoveryHomeReceipt(t *testing.T) {
key := ed25519.NewKeyFromSeed([]byte(strings.Repeat("b", 32)))
en := strings.Repeat("a", 32)
d := hostactors.Directory{Store: f.claims, PublicKey: f.signer.Public().(ed25519.PublicKey), Bucket: "bucket"}
- roster := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box-2": {Enrollment: en, BoxID: "box-2", Label: "box-2", Namespace: "platform", Region: "uk", Pool: "default", State: "active", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-2", InstanceID: "i-target"}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
+ roster := hostactors.Roster{HomeRegion: "uk", V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box-2": {Enrollment: en, BoxID: "box-2", Label: "box-2", Namespace: "platform", Region: "uk", Pool: "default", State: "active", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-2", InstanceID: "i-target"}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
if bad == "roster-name" {
b := roster.Boxes["box-2"]
b.Region = "ie"
@@ -85,6 +85,7 @@ func TestPlan13HomeRecoveryFenceBinding(t *testing.T) {
_, o, h := plan12Handover(t, hostclaim.KindRestoring, "fence")
o.Claim.Op.Intent["to"] = "@home"
o.Claim.Op.Intent["home_region"] = home
+ h.FenceAuthority = plan12FenceAuthority{home: "uk"}
err := h.recoveryEvidence(t.Context(), o)
if (err == nil) != (home == "uk") {
t.Fatal(home, err)
diff --git a/internal/host/plan13_region_test.go b/internal/host/plan13_region_test.go
index 51916c9e0..2c10da20d 100644
--- a/internal/host/plan13_region_test.go
+++ b/internal/host/plan13_region_test.go
@@ -3,6 +3,7 @@ package host
import (
"errors"
"github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/park"
"testing"
)
@@ -13,9 +14,9 @@ func TestPlan13ClaimRegionBinding(t *testing.T) {
bad bool
}{
{"ie", "", "ie", "ie", false}, {"uk", "uk", "uk", "", false},
- {"uk", "ie", "", "", true}, {"", "", "", "", true}, {"@home", "", "", "", true}, {"ie", "@home", "", "", true},
+ {"ca", "", "", "", true}, {"uk", "ie", "", "", true}, {"", "", "", "", true}, {"@home", "", "", "", true}, {"ie", "@home", "", "", true},
} {
- e, h, err := claimRegionBinding(tc.roster, tc.code)
+ e, h, err := claimRegionBinding(tc.roster, tc.code, "ie")
if e != tc.engine || h != tc.home || (err != nil) != tc.bad {
t.Fatalf("%+v: %q %q %v", tc, e, h, err)
}
@@ -23,7 +24,7 @@ func TestPlan13ClaimRegionBinding(t *testing.T) {
}
func TestPlan13VerifyRegionalHomeNames(t *testing.T) {
for _, home := range []string{"region", ""} {
- identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", ControlBucket: "control", Revision: 1})
+ identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", FleetHomeRegion: "region", ControlBucket: "control", Revision: 1})
identity.HomeRegion = home
actor, err := identity.Verify(t.Context())
if err != nil {
@@ -42,3 +43,36 @@ func TestPlan13VerifyRegionalHomeNames(t *testing.T) {
}
}
}
+
+func TestPlan13UnverifiedAliasRefused(t *testing.T) {
+ for _, fleet := range []string{"", "ie"} {
+ identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "ca", FleetHomeRegion: fleet, Revision: 1})
+ identity.HomeRegion = "ca"
+ if _, err := identity.Verify(t.Context()); err == nil {
+ t.Fatal("unverified home alias accepted", fleet)
+ }
+ }
+}
+
+func TestPlan13NamedBoxCannotAcknowledgeHomeDelete(t *testing.T) {
+ if _, _, err := claimRegionBinding("ca", "", "ie"); err == nil {
+ t.Fatal("missing CARLOS_REGION granted alias")
+ }
+ identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "ca-box", Enrollment: "enrolled", Region: "ca", FleetHomeRegion: "ie", Revision: 1})
+ p := productionClaimRegional{Identity: identity, Local: &productionClaimLocal{Identity: identity, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: t.TempDir()}, assembly: &ClaimAssembly{}, lifetime: t.Context()}}
+ for _, kind := range []string{hostclaim.KindDeleting, hostclaim.KindParking, hostclaim.KindPurging} {
+ ob := hostclaim.Obligation{ID: "chore", Op: "delete", Kind: "park-replica", Owner: "region:ca", Region: "ca", Retirement: "retirement"}
+ o := hostclaim.Observed{Host: "test.example", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{ID: "delete", Kind: kind}, Obligations: []hostclaim.Obligation{ob}}}
+ if err := p.Authorize(t.Context(), o, ob); err != nil {
+ t.Fatal("named control", err)
+ }
+ ob.Region, ob.Owner = park.HomeRegion, "region:@home"
+ o.Claim.Obligations[0] = ob
+ if err := p.Authorize(t.Context(), o, ob); err == nil {
+ t.Fatal("acknowledged home delete", kind)
+ }
+ if (park.Engine{Region: "ca"}).OwnsRegion(park.HomeRegion) {
+ t.Fatal("named engine owns home")
+ }
+ }
+}
diff --git a/internal/host/plan13_views_test.go b/internal/host/plan13_views_test.go
index a065a7895..d58ff900b 100644
--- a/internal/host/plan13_views_test.go
+++ b/internal/host/plan13_views_test.go
@@ -7,6 +7,7 @@ import (
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
"testing"
)
@@ -16,8 +17,8 @@ func TestPlan13ProductionRemoteHomeView(t *testing.T) {
f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: provision.MoveLanding}
h := f.holder("src", "src@ie")
o := hostclaim.Observed{Host: vwHost, ETag: "tag", Claim: &hostclaim.Claim{Host: vwHost, Holder: h, Op: hostclaim.Op{ID: vwMove, Kind: hostclaim.KindLanding, Phase: "installing", Intent: map[string]string{"home_region": "ie"}}}}
- identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "uk", ControlBucket: "control", Revision: 1})
- p := productionClaimRegional{Identity: identity, Parks: &claimMoveParkSources{View: f.view("uk")}}
+ identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "uk", FleetHomeRegion: "ie", ControlBucket: "control", Revision: 1})
+ p := productionClaimRegional{Identity: identity, Local: &productionClaimLocal{Identity: identity, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: t.TempDir()}, assembly: &ClaimAssembly{}, lifetime: t.Context()}, Parks: &claimMoveParkSources{View: f.view("uk")}}
if _, err := p.View(t.Context(), hostclaim.Observed{}); err == nil {
t.Fatal("missing claim accepted")
}
@@ -31,7 +32,35 @@ func TestPlan13ProductionRemoteHomeView(t *testing.T) {
if h.Admissions[0].Region != "ie" {
t.Fatal("history rewritten")
}
- if err := p.Authorize(t.Context(), o, hostclaim.Obligation{Op: vwMove, Owner: "region:@home", Region: park.HomeRegion}); err == nil {
+ ob := hostclaim.Obligation{ID: "persisted", Kind: "park-generation", Op: vwMove, Owner: "region:uk", Region: "uk", Generation: "old", Retirement: "retirement"}
+ store := release.NewDirStore(t.TempDir())
+ op := o.Claim.Op
+ o, err = hostclaim.Update(t.Context(), store, hostclaim.Observed{Host: vwHost, Absent: true}, func(c *hostclaim.Claim) error {
+ c.Holder = h
+ c.Op = op
+ c.Obligations = []hostclaim.Obligation{ob}
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Read(t.Context(), store, vwHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := p.Authorize(t.Context(), o, ob); err != nil {
+ t.Fatal("authorized control", err)
+ }
+ ob.Owner, ob.Region = "region:@home", park.HomeRegion
+ o, err = hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error { c.Obligations = []hostclaim.Obligation{ob}; return nil })
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Read(t.Context(), store, vwHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := p.Authorize(t.Context(), o, ob); err == nil {
t.Fatal("named worker owns home")
}
}
diff --git a/internal/hostactors/plan13_home_test.go b/internal/hostactors/plan13_home_test.go
new file mode 100644
index 000000000..01509e147
--- /dev/null
+++ b/internal/hostactors/plan13_home_test.go
@@ -0,0 +1,37 @@
+package hostactors
+
+import "testing"
+
+func TestPlan13SignedHomeAgreement(t *testing.T) {
+ for _, home := range []string{"", "ie", "ca", "@home", "IE"} {
+ r := Roster{HomeRegion: "ie"}
+ if r.MatchesHome(home) != (home == "ie") {
+ t.Fatal(home)
+ }
+ }
+ if !(Roster{}).MatchesHome("") || (Roster{}).MatchesHome("ie") {
+ t.Fatal("legacy roster grants alias")
+ }
+}
+
+func TestPlan13HomeDesignationIsSignedAndOptional(t *testing.T) {
+ d, sg, o := fixture(t)
+ if _, err := d.Read(t.Context()); err != nil {
+ t.Fatal("legacy roster", err)
+ }
+ next := Clone(o.Roster)
+ next.HomeRegion = "ie"
+ updated, err := d.Replace(t.Context(), sg, o, next)
+ if err != nil {
+ t.Fatal(err)
+ }
+ read, err := d.Read(t.Context())
+ if err != nil || !read.Roster.MatchesHome("ie") {
+ t.Fatal(read, err)
+ }
+ next = Clone(updated.Roster)
+ next.HomeRegion = "@home"
+ if _, err := d.Replace(t.Context(), sg, updated, next); err == nil {
+ t.Fatal("engine token accepted as designation")
+ }
+}
diff --git a/internal/hostactors/roster.go b/internal/hostactors/roster.go
index 70114f142..478054861 100644
--- a/internal/hostactors/roster.go
+++ b/internal/hostactors/roster.go
@@ -15,6 +15,7 @@ import (
"github.com/carlosframework/platform/internal/boxid"
"github.com/carlosframework/platform/internal/hostquarantine"
+ "github.com/carlosframework/platform/internal/regionname"
"github.com/carlosframework/platform/internal/release"
)
@@ -61,6 +62,7 @@ type Reservation struct {
Hashes map[string][]string `json:"hashes,omitempty"`
}
type Roster struct {
+ HomeRegion string `json:"home_region,omitempty"`
RequestLimitNS int64 `json:"request_limit_ns,omitempty"`
EnrollmentAttempts map[string]EnrollmentAttempt `json:"enrollment_attempts,omitempty"`
Publication string `json:"publication,omitempty"`
@@ -91,6 +93,9 @@ func (r Roster) Validate(bucket string) error {
if r.V != 1 || bucket == "" || r.DeploymentBucket != bucket || r.Revision == 0 || r.Boxes == nil || r.Consoles == nil || r.Holds == nil || r.Activations == nil {
return ErrRoster
}
+ if r.HomeRegion != "" && regionname.PlacementRegion(r.HomeRegion) != r.HomeRegion {
+ return ErrRoster
+ }
if r.Publication != "" && r.Publication != "preparing" && r.Publication != "published" {
return ErrRoster
}
@@ -323,3 +328,7 @@ func (d Directory) Initialize(ctx context.Context, sg release.Signer) error {
}
return release.CreateExact(ctx, d.Store, Key, wire)
}
+
+func (r Roster) MatchesHome(home string) bool {
+ return home == r.HomeRegion && (home == "" || regionname.PlacementRegion(home) == home)
+}
diff --git a/internal/hostbox/identity.go b/internal/hostbox/identity.go
index 90473432b..97f268ca5 100644
--- a/internal/hostbox/identity.go
+++ b/internal/hostbox/identity.go
@@ -13,6 +13,7 @@ import (
// Actor is the verified projection of the sole control/host-actors.json roster.
// Roster authenticates that document and its revision before returning a row.
type Actor struct {
+ FleetHomeRegion string
HomeRegion string
ControlBucket string
Box, Enrollment string
@@ -70,7 +71,7 @@ func (p IdentityProvider) Verify(ctx context.Context) (Actor, error) {
if ev.Box != p.Box || ev.Cloud != cloud {
return Actor{}, ErrChanged
}
- if p.HomeRegion != "" && (p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) {
+ if p.HomeRegion != "" && (p.HomeRegion != a.FleetHomeRegion || p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) {
return Actor{}, ErrChanged
}
a.HomeRegion = p.HomeRegion
@@ -97,7 +98,7 @@ func (p IdentityProvider) Local(authority LocalAuthority) func(context.Context,
if err != nil {
return err
}
- if s.Box != actor.Box || s.Region != actor.Region {
+ if s.Box != actor.Box || s.Region != actor.Region || s.HomeRegion != actor.HomeRegion || s.FleetHomeRegion != actor.FleetHomeRegion {
return ErrChanged
}
return authority.Check(ctx, actor, c, s)
diff --git a/internal/hostbox/work.go b/internal/hostbox/work.go
index caf015968..14831fbf9 100644
--- a/internal/hostbox/work.go
+++ b/internal/hostbox/work.go
@@ -16,6 +16,7 @@ var ErrChanged = errors.New("hostbox: selected authority changed")
var ErrUnsupported = errors.New("hostbox: unresolved lifecycle contract")
type Selection struct {
+ HomeRegion, FleetHomeRegion string
Host, Action, Box, Region string
Captured hostguard.Captured
Kind, Op, Phase, Generation string
@@ -23,11 +24,12 @@ type Selection struct {
}
type WorkSource struct {
- HomeRegion string
- Prepare func(context.Context, hostclaim.Observed) error
- Guard hostguard.Guard
- Box, Region string
- Now func() time.Time
+ FleetHomeRegion string
+ HomeRegion string
+ Prepare func(context.Context, hostclaim.Observed) error
+ Guard hostguard.Guard
+ Box, Region string
+ Now func() time.Time
// Policy checks the action's role/phase and the selected config/release.
Policy func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error
// Local checks quarantine, execution deadlines and failed cleanup under flock.
@@ -76,7 +78,7 @@ func (s WorkSource) Select(ctx context.Context, host, action string) (hostwrite.
if c.Holder.Incarnation != captured.Incarnation || c.Holder.Stamp != captured.Stamp || captured.Stamp == 0 {
return nil, ErrChanged
}
- w := selectedWork{source: s, selection: Selection{Host: host, Action: action, Box: s.Box, Region: s.Region, Captured: captured, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: c.Holder.Session}, checked: &checkedPhase{phase: c.Op.Phase}}
+ w := selectedWork{source: s, selection: Selection{HomeRegion: s.HomeRegion, FleetHomeRegion: s.FleetHomeRegion, Host: host, Action: action, Box: s.Box, Region: s.Region, Captured: captured, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: c.Holder.Session}, checked: &checkedPhase{phase: c.Op.Phase}}
return w, nil
}
diff --git a/internal/hostclaim/fence.go b/internal/hostclaim/fence.go
index bb90dd9bf..e8d050b76 100644
--- a/internal/hostclaim/fence.go
+++ b/internal/hostclaim/fence.go
@@ -21,9 +21,10 @@ type FenceRef struct {
}
type FenceBinding struct {
- Region string
- RosterRevision uint64
- RosterETag string
+ FleetHomeRegion string
+ Region string
+ RosterRevision uint64
+ RosterETag string
}
// FenceAuthority checks and identifies one authenticated roster observation.
@@ -107,3 +108,5 @@ func VerifyFenceForBox(ctx context.Context, s release.Store, authority FenceAuth
}
return nil
}
+
+func (f VerifiedFence) FleetHomeRegion() string { return f.binding.FleetHomeRegion }
diff --git a/internal/hostclaim/generation.go b/internal/hostclaim/generation.go
index ff6173146..351462b6b 100644
--- a/internal/hostclaim/generation.go
+++ b/internal/hostclaim/generation.go
@@ -115,7 +115,7 @@ func AdmitWriter(in Admission) func(*Claim) error {
if len(h.Admissions) > 0 {
predecessor = h.Admissions[len(h.Admissions)-1]
}
- if in.Mode == "warm" && in.Region != predecessor.Region {
+ if in.Mode == "warm" && !regionname.SameRegion(c.Op.Intent["home_region"], in.Region, predecessor.Region) {
return fmt.Errorf("%w: a warm admission stays in its predecessor's region", ErrWrongState)
}
type writerSession struct {
@@ -145,7 +145,7 @@ func AdmitWriter(in Admission) func(*Claim) error {
// Only a park of the holder's whole location retires the
// predecessor; a scoped park of another location (an old
// incarnation's residue, a staging prefix) does not.
- if ob.Kind == "park-replica" && ob.Region == entry.PredecessorRegion &&
+ if ob.Kind == "park-replica" && regionname.SameRegion(c.Op.Intent["home_region"], ob.Region, entry.PredecessorRegion) &&
(ob.Park == nil || ob.Park.Location == h.Location) {
entry.Covered = true
break
@@ -227,12 +227,12 @@ func PublishGeneration(observedOpID, observedPhase string, observedSession uint6
record.Covered = true
continue
}
- if record.PredecessorRegion != region {
+ if !regionname.SameRegion(c.Op.Intent["home_region"], record.PredecessorRegion, region) {
continue
}
queued := false
for _, ob := range c.Obligations {
- if ob.Kind == "park-generation" && ob.Region == region && ob.Generation == predecessor {
+ if ob.Kind == "park-generation" && regionname.SameRegion(c.Op.Intent["home_region"], ob.Region, region) && ob.Generation == predecessor {
queued = true
break
}
diff --git a/internal/hostclaim/move_target_fence.go b/internal/hostclaim/move_target_fence.go
index 514943b72..799aa5841 100644
--- a/internal/hostclaim/move_target_fence.go
+++ b/internal/hostclaim/move_target_fence.go
@@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"github.com/carlosframework/platform/internal/hostops/fencerelease"
+ "github.com/carlosframework/platform/internal/regionname"
"github.com/carlosframework/platform/internal/release"
+ "strings"
"time"
)
@@ -156,7 +158,7 @@ func ConsumedMoveTargetFence(c *Claim) (FenceRef, bool) {
// LostMoveTargetParked requires revocation and completion of every target chore.
func LostMoveTargetParked(c *Claim, region string) bool {
- if c == nil || c.Holder == nil || c.Op.Kind != KindRestoring || region == "" || region != c.Op.Intent["to"] {
+ if c == nil || c.Holder == nil || c.Op.Kind != KindRestoring || region == "" || !regionname.SameRegion(c.Op.Intent["home_region"], region, c.Op.Intent["to"]) {
return false
}
target := c.Op.Intent["target"]
@@ -164,7 +166,7 @@ func LostMoveTargetParked(c *Claim, region string) bool {
return false
}
for _, o := range c.Obligations {
- if o.Op == c.Op.ID && (o.Box == target || o.Owner == "box:"+target || o.Region == region || o.Owner == "region:"+region) {
+ if o.Op == c.Op.ID && (o.Box == target || o.Owner == "box:"+target || regionname.SameRegion(c.Op.Intent["home_region"], o.Region, region) || (strings.HasPrefix(o.Owner, "region:") && regionname.SameRegion(c.Op.Intent["home_region"], strings.TrimPrefix(o.Owner, "region:"), region))) {
return false
}
}
diff --git a/internal/hostclaim/plan13_amendments_test.go b/internal/hostclaim/plan13_amendments_test.go
index f9bb655a7..6a8477fd8 100644
--- a/internal/hostclaim/plan13_amendments_test.go
+++ b/internal/hostclaim/plan13_amendments_test.go
@@ -66,3 +66,72 @@ func TestPlan13WholeLocationCoversHomeWithoutObjects(t *testing.T) {
}
}
}
+
+func TestPlan13ReturningWriterHomeCoverage(t *testing.T) {
+ for _, bound := range []bool{false, true} {
+ c := targetWroteLanding(t)
+ if err := RollBack(moveA, rbA)(c); err != nil {
+ t.Fatal(err)
+ }
+ c.Op.Intent["to"] = "@home"
+ if bound {
+ c.Op.Intent["home_region"] = "uk"
+ }
+ n := 0
+ ids := func() string { n++; return fmt.Sprint("park", n) }
+ if err := TargetReleased(moveA, "uk-box", "@home", stoppedTarget, releaseScope(c), ids)(c); err != nil {
+ t.Fatal(err)
+ }
+ if err := ReturnToSource(moveA, "ie-box", false, ReturnFence{})(c); err != nil {
+ t.Fatal(err)
+ }
+ h := c.Holder
+ in := Admission{ObservedOpID: c.Op.ID, ObservedPhase: c.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: c.Op.Exec, Mode: "cold", NewGeneration: "return", Region: "ie"}
+ if err := AdmitWriter(in)(c); err != nil {
+ t.Fatal(err)
+ }
+ last := c.Holder.Admissions[len(c.Holder.Admissions)-1]
+ if last.Covered != bound || last.PredecessorRegion != "uk" {
+ t.Fatalf("bound=%v: %+v", bound, last)
+ }
+ for _, ob := range append([]Obligation(nil), c.Obligations...) {
+ if err := CompleteObligation(ob.Op, ob.ID)(c); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if c.Holder.Admissions[len(c.Holder.Admissions)-1].Covered != bound {
+ t.Fatal("empty cleanup lost coverage")
+ }
+ }
+}
+func TestPlan13RetainedHomeChoreBlocksLostTargetParked(t *testing.T) {
+ for _, ob := range []Obligation{
+ {ID: "pending", Op: "move", Kind: "park-generation", Region: "ie", Owner: "region:ie"},
+ {ID: "pending", Op: "move", Kind: "park-generation", Region: "other", Owner: "region:ie"},
+ } {
+ c := &Claim{Holder: &Holder{}, Op: Op{ID: "move", Kind: KindRestoring, Exec: "source", Intent: map[string]string{"target": "target", "to": "@home", "home_region": "ie"}}, Lost: []string{"target"}, Obligations: []Obligation{ob}}
+ if LostMoveTargetParked(c, "@home") {
+ t.Fatal("pending retained-name chore ignored", ob)
+ }
+ c.Obligations = nil
+ if !LostMoveTargetParked(c, "ie") {
+ t.Fatal("completed bound target refused")
+ }
+ }
+}
+
+func TestPlan13WarmAndPublishedHomeCoverage(t *testing.T) {
+ for _, home := range []string{"ie", ""} {
+ c := &Claim{Gen: 2, Holder: &Holder{Session: 2, Stamp: 2, Generation: "new", WriterBox: "box", Admissions: []AdmissionRecord{{Session: 2, Region: "ie", PredecessorRegion: "ie", Generation: "new", Predecessor: "old", Mode: "cold"}}}, Op: Op{Kind: KindServing, Exec: "box", Intent: map[string]string{"home_region": home}}}
+ if err := PublishGeneration("", "", 2, "new", "@home", counter())(c); err != nil {
+ t.Fatal(err)
+ }
+ if c.Holder.Admissions[0].Covered != (home != "") {
+ t.Fatal("publication misses bound predecessor", home)
+ }
+ in := Admission{ObservedSession: 2, ObservedGeneration: "new", Box: "box", Mode: "warm", PredecessorCertified: true, Region: "@home"}
+ if err := AdmitWriter(in)(c); (err == nil) != (home != "") {
+ t.Fatal("warm admission", home, err)
+ }
+ }
+}
diff --git a/internal/hostfence/claim_fence.go b/internal/hostfence/claim_fence.go
index 3a45e34dc..cda241eb3 100644
--- a/internal/hostfence/claim_fence.go
+++ b/internal/hostfence/claim_fence.go
@@ -29,7 +29,7 @@ func (a ClaimFenceAuthority) ValidateFence(ctx context.Context, f termfence.Reco
if err := a.Directory.ValidateTerminationFenceAuthority(ctx, o, f.Box, f, a.Now()); err != nil {
return hostclaim.FenceBinding{}, err
}
- return hostclaim.FenceBinding{Region: o.Roster.Boxes[f.Box].Region, RosterRevision: o.Roster.Revision, RosterETag: o.ETag}, nil
+ return hostclaim.FenceBinding{FleetHomeRegion: o.Roster.HomeRegion, Region: o.Roster.Boxes[f.Box].Region, RosterRevision: o.Roster.Revision, RosterETag: o.ETag}, nil
}
func ClaimFenceRef(ref EvidenceRef) hostclaim.FenceRef {
diff --git a/internal/hostops/move_enrollment.go b/internal/hostops/move_enrollment.go
index 7c646ac46..84ff45882 100644
--- a/internal/hostops/move_enrollment.go
+++ b/internal/hostops/move_enrollment.go
@@ -83,9 +83,12 @@ func BeginClaimMove(ctx context.Context, s release.Store, d hostactors.Directory
if err != nil {
return hostclaim.Observed{}, err
}
+ if !roster.Roster.MatchesHome(home) {
+ return hostclaim.Observed{}, ErrIdentity
+ }
src, sourceOK := roster.Roster.Boxes[source]
dst, targetOK := roster.Roster.Boxes[target]
- if !sourceOK || !targetOK || source == target || src.State != "active" || dst.State != "active" || src.Region == "" || dst.Region == "" || park.PlacementRegion(park.HomePlacementCode(home, dst.Region)) != park.PlacementRegion(to) || park.PlacementRegion(park.HomePlacementCode(home, src.Region)) == park.PlacementRegion(to) {
+ if !sourceOK || !targetOK || source == target || src.State != "active" || dst.State != "active" || src.Region == "" || dst.Region == "" {
return hostclaim.Observed{}, ErrIdentity
}
// Lookup also refuses a box with a termination fence.
diff --git a/internal/hostops/move_fence_regions.go b/internal/hostops/move_fence_regions.go
index 869122619..9b7499f1d 100644
--- a/internal/hostops/move_fence_regions.go
+++ b/internal/hostops/move_fence_regions.go
@@ -23,7 +23,7 @@ func (a MoveFenceRegions) ValidateFence(ctx context.Context, f termfence.Record)
if err != nil {
return hostclaim.FenceBinding{}, err
}
- if binding.Region == "" || (a.HomeRegion != "" && park.PlacementRegion(a.HomeRegion) != a.HomeRegion) {
+ if binding.Region == "" || (a.HomeRegion != "" && (a.HomeRegion != binding.FleetHomeRegion || park.PlacementRegion(a.HomeRegion) != a.HomeRegion)) {
return hostclaim.FenceBinding{}, hostclaim.ErrFence
}
binding.Region = park.PlacementRegion(park.HomePlacementCode(a.HomeRegion, binding.Region))
diff --git a/internal/hostops/move_recovery_bridge_test.go b/internal/hostops/move_recovery_bridge_test.go
index 79a092493..7f5f9a055 100644
--- a/internal/hostops/move_recovery_bridge_test.go
+++ b/internal/hostops/move_recovery_bridge_test.go
@@ -20,6 +20,7 @@ import (
)
type c7ReplayAuthority struct {
+ home string
record termfence.Record
region string
}
@@ -35,7 +36,7 @@ func (a c7ReplayAuthority) ValidateFence(_ context.Context, f termfence.Record)
if f != a.record {
return hostclaim.FenceBinding{}, hostclaim.ErrFence
}
- return hostclaim.FenceBinding{Region: a.region, RosterRevision: 1, RosterETag: "roster"}, nil
+ return hostclaim.FenceBinding{FleetHomeRegion: a.home, Region: a.region, RosterRevision: 1, RosterETag: "roster"}, nil
}
type c7DecisionHook struct {
diff --git a/internal/hostops/plan13_enrollment_test.go b/internal/hostops/plan13_enrollment_test.go
index 43d9fd2f6..ad558ccdc 100644
--- a/internal/hostops/plan13_enrollment_test.go
+++ b/internal/hostops/plan13_enrollment_test.go
@@ -2,6 +2,7 @@ package hostops
import (
"encoding/json"
+ "github.com/carlosframework/platform/internal/hostactors"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
@@ -15,7 +16,7 @@ func TestPlan13BeginClaimMoveHomeAndNamed(t *testing.T) {
{"named", "", "ie", "uk", "uk-box", "uk"},
} {
t.Run(tc.name, func(t *testing.T) {
- f := newMoveStartFixture(t, nil)
+ f := newMoveStartFixture(t, func(r *hostactors.Roster) { r.HomeRegion = tc.home })
rec, etag, err := provision.GetRecordETag(t.Context(), f.raw, "bab", "app", f.host)
if err != nil {
t.Fatal(err)
@@ -48,3 +49,18 @@ func TestPlan13BeginClaimMoveHomeAndNamed(t *testing.T) {
})
}
}
+
+func TestPlan13MoveStartRefusesFalseHome(t *testing.T) {
+ f := newMoveStartFixture(t, func(r *hostactors.Roster) { r.HomeRegion = "ie" })
+ o := f.read(t)
+ h, err := f.authority.RegisterMove(t.Context(), o, "canonical", moveStartID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := BeginClaimMove(t.Context(), f.raw, f.authority.Directory, h, o, moveStartID, "operator", "uk-box", "", f.now, "uk"); err == nil {
+ t.Fatal("operation home overrode signed fleet home")
+ }
+ if f.read(t).ETag != o.ETag {
+ t.Fatal("refusal mutated claim")
+ }
+}
diff --git a/internal/hostops/plan13_fence_test.go b/internal/hostops/plan13_fence_test.go
index 1ac1e2e2b..a29b085d8 100644
--- a/internal/hostops/plan13_fence_test.go
+++ b/internal/hostops/plan13_fence_test.go
@@ -34,6 +34,12 @@ func TestPlan13FenceRecoveryHomeAndNamed(t *testing.T) {
key := "control/box-fences/" + strings.Repeat("a", 32) + ".json"
r.Move.Rollback = &provision.MoveRollback{ID: "rb_abcdefghijklmnopqrstuvwxyz", Reason: provision.RollbackTargetFenced, Evidence: key, RequestedAt: &at, TargetPhase: provision.RbTargetLost, LossRef: key, LostAt: &at, SourcePhase: provision.RbSourceWaiting}
})
+ if which == "source-home" {
+ w.authority.home = "uk"
+ }
+ if which == "target-home" {
+ w.authority.home = "ie"
+ }
run := func() error {
return w.control.RunRecovery(t.Context(), func(ctx context.Context) error {
return runMoveRecovery(ctx, w.claim(t), func(ctx context.Context, g *MoveRecoveryGrant) error { return g.ReleaseFencedTarget(ctx, w.ref) }, w.authority)
@@ -68,6 +74,42 @@ func TestPlan13FenceRecoveryHomeAndNamed(t *testing.T) {
if which == "target-home" && rec.Move.To != "" {
t.Fatal("target placement rewritten")
}
+ if which == "target-home" {
+ o := w.claim(t)
+ for _, ob := range o.Claim.Obligations {
+ var e error
+ o, e = hostclaim.Update(t.Context(), w.raw, o, hostclaim.CompleteObligation(ob.Op, ob.ID))
+ if e != nil {
+ t.Fatal(e)
+ }
+ }
+ o, err = hostclaim.Update(t.Context(), w.raw, o, func(c *hostclaim.Claim) error {
+ c.Obligations = append(c.Obligations, hostclaim.Obligation{ID: "retained", Op: c.Op.ID, Kind: "park-generation", Region: "ie", Owner: "region:ie", Generation: "old", Retirement: "retirement"})
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ stamp := func() error {
+ return w.control.RunRecovery(t.Context(), func(ctx context.Context) error {
+ return runMoveRecovery(ctx, w.claim(t), func(ctx context.Context, g *MoveRecoveryGrant) error { return g.StampLostMoveTargetParked(ctx) }, w.authority)
+ })
+ }
+ if err := stamp(); err == nil {
+ t.Fatal("parked stamp ignored retained-name chore")
+ }
+ rec, err = provision.GetRecord(t.Context(), w.raw, "bab", "app", w.host)
+ if err != nil || rec.Move.Rollback.TargetParkedAt != nil {
+ t.Fatal("premature parked stamp", err)
+ }
+ if _, err = hostclaim.Update(t.Context(), w.raw, o, hostclaim.CompleteObligation(o.Claim.Op.ID, "retained")); err != nil {
+ t.Fatal(err)
+ }
+ if err := stamp(); err != nil {
+ t.Fatal("completed target refused", err)
+ }
+ }
+
})
}
}
diff --git a/internal/hostops/plan13_fix1_test.go b/internal/hostops/plan13_fix1_test.go
new file mode 100644
index 000000000..99eefd2b9
--- /dev/null
+++ b/internal/hostops/plan13_fix1_test.go
@@ -0,0 +1,29 @@
+package hostops
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/termfence"
+ "testing"
+)
+
+type plan13BoundFence struct{ home, region string }
+
+func (a plan13BoundFence) ValidateFence(context.Context, termfence.Record) (hostclaim.FenceBinding, error) {
+ return hostclaim.FenceBinding{FleetHomeRegion: a.home, Region: a.region, RosterRevision: 1, RosterETag: "signed"}, nil
+}
+func TestPlan13FenceRequiresSignedHome(t *testing.T) {
+ for _, home := range []string{"ie", "ca", ""} {
+ a := MoveFenceRegions{Authority: plan13BoundFence{home: "ie", region: "ca"}, HomeRegion: home}
+ got, err := a.ValidateFence(t.Context(), termfence.Record{})
+ if home == "ca" {
+ if err == nil {
+ t.Fatal("named fence projected to home", got)
+ }
+ continue
+ }
+ if err != nil || got.Region != "ca" {
+ t.Fatal(got, err)
+ }
+ }
+}
diff --git a/internal/hostops/production_create.go b/internal/hostops/production_create.go
index 01d09bd6e..36b7f6776 100644
--- a/internal/hostops/production_create.go
+++ b/internal/hostops/production_create.go
@@ -122,6 +122,9 @@ func (a ProductionAuthority) selectExecutor(ctx context.Context, r Record) (stri
if err != nil {
return "", "", "", err
}
+ if !roster.Roster.MatchesHome(a.HomeRegion) {
+ return "", "", "", ErrIdentity
+ }
candidates := map[string]*executorCandidate{}
servingScope := 0
for ref, b := range roster.Roster.Boxes {
diff --git a/internal/park/plan13_observe_test.go b/internal/park/plan13_observe_test.go
index 8f7e7c283..ede6df026 100644
--- a/internal/park/plan13_observe_test.go
+++ b/internal/park/plan13_observe_test.go
@@ -95,3 +95,40 @@ func TestPlan13SweepAbsentClaimRefuses(t *testing.T) {
t.Fatal("absent claim accepted")
}
}
+
+func TestPlan13ReturningWriterEmptyHomePark(t *testing.T) {
+ f := newFixture(t)
+ choreIn(t, f, HomeRegion)
+ f.e.HomeRegion = "ie"
+ engineIn(f, HomeRegion, HomeRegion)
+ o, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Update(t.Context(), f.e.Claims, o, func(c *hostclaim.Claim) error {
+ c.Op = hostclaim.Op{ID: "op", Kind: hostclaim.KindRestoring, Phase: hostclaim.PhaseClaimed, Exec: "returner", Intent: map[string]string{"home_region": "ie"}}
+ c.Holder.Generation, c.Holder.Session, c.Holder.WriterBox = "old", 1, "target"
+ c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "old", Region: "ie", Session: 1, Box: "target", Mode: "cold"}}
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Update(t.Context(), f.e.Claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: "op", ObservedPhase: hostclaim.PhaseClaimed, ObservedSession: 1, ObservedGeneration: "old", Region: "uk", Box: "returner", Mode: "cold", NewGeneration: "return"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !o.Claim.Holder.Admissions[1].Covered {
+ t.Fatal("returning predecessor uncovered")
+ }
+ if err := f.e.Run(t.Context(), f.w); err != nil {
+ t.Fatal(err)
+ }
+ done, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(done.Claim.Obligations) != 0 || !done.Claim.Holder.Admissions[1].Covered || done.Claim.Holder.Admissions[1].PredecessorRegion != "ie" {
+ t.Fatal(done.Claim)
+ }
+}
diff --git a/internal/park/region_names.go b/internal/park/region_names.go
index 3faa5ee25..f865694db 100644
--- a/internal/park/region_names.go
+++ b/internal/park/region_names.go
@@ -60,3 +60,10 @@ func EnginePlacement(home, engine string) (string, bool) {
}
return engine, PlacementRegion(engine) == engine
}
+
+func VerifiedHome(fleet, local, region, operation string) (string, error) {
+ if (fleet != "" && PlacementRegion(fleet) != fleet) || (operation != "" && operation != fleet) || (local != "" && (local != fleet || local != region)) || (local == "" && operation != "" && operation == region) {
+ return "", ErrUnsafe
+ }
+ return BoundHome(local, operation)
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 6cbe9dc46..79e6a5f5f 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -474,7 +474,7 @@ func Reviewed(ctx context.Context, s Store, k string) error {
var plan9B8CensusInventory = []censusEntry{
{"internal/hostbox/parkleases.go", "ReplicaParkLeaseSet", "key-site", "lit:instances", "0ca8d9b743402fda9d15cc215464423b35e21c1c2b47f91294d76188b7d43fb6"},
{"internal/host/claimretirementcensus.go", "ClaimRetirementCensus", "key-site", "lit:instances", "abc0bd8c5f5d328abfac8821f42f8f23954723ed6506747f33b0197824e2611f"},
- {"internal/host/claimproduction.go", "productionClaimRegional.View", "key-site", "lit:instances", "85fa91931c06b2fb1f8e53512a219190fcb146b9919b1e625b8562c299209840"},
+ {"internal/host/claimproduction.go", "productionClaimRegional.View", "key-site", "lit:instances", "533203b2aa729801438da4f3a85799a58ea0e67d20eb5625f0de32ea623f1607"},
{"internal/hostops/move_loss_admission.go", "moveLossOwner.ValidateLossRecovery", "key-site", "key:RecordKey", "1ad4af4595c764fc5051e50c2b187472cb20ce73c3b3e8245b8527228f326f5f"},
{"internal/host/claimretirementcensus.go", "claimArchivedLocations", "key-site", "lit:instances", "9ba8ea4ecb5ad63621c1322467b9a80f8f5703896ba1fc3f7cb1addd56418950"},
{"internal/host/claimretirementcensus.go", "claimLegacyIncarnation", "key-site", "lit:instances", "31a14ea105c67e00a24f06b9500a221ee5bfc47402b9c33cecfde807e51db530"},
@@ -705,11 +705,11 @@ var plan9B8CensusInventory = []censusEntry{
{"internal/host/movesmeasure.go", "measureS3", "nonrecord", "sel:Put", "3ae9fc093b453e19a5b19be0929533b30a0e2a3e998ba8dc766f41287db2caca"},
{"internal/host/movespass.go", "legacyMoveRound", "record-writer", "key:SetRecordMove", "1e13a3ea099d45318d3b64b067448c87dc4a136cd909348f526e4583ac00150a"},
{"internal/host/movesphase1.go", "writeMoveLedger", "nonrecord", "sel:Put", "597a70ac06ceb4e9eb52bb9f61b28a4937e703fa4a0916f64bd95d9594f0556e"},
- {"internal/host/movesphase1.go", "movesWorker.sourceRound", "nonrecord", "sel:PutIfAbsentVersioned", "810e5a9abac7dea9220f83eae924cea99b0eca0f002764c88ec35de26a356dff"},
+ {"internal/host/movesphase1.go", "movesWorker.sourceRound", "nonrecord", "sel:PutIfAbsentVersioned", "a2989e948ebe33ceeaaf1be25c8cdb2bdfff4cf5e55b5ed28a03605e1ec81756"},
{"internal/host/movesphase1.go", "movesWorker.refreshCrossRate", "nonrecord", "sel:CopyVersioned", "7e03e680bd61be26dddcf168cacd2427bb535571dfd326c4a3130ebe65ea1968"},
{"internal/host/movesphase1.go", "movesWorker.writeSourceReport", "nonrecord", "sel:PutReplace", "370407cca2c1b91f6e5e7f135238f04c0dce9d7986e6e1ee9bebbf88cad0b9e0"},
{"internal/host/movesphase1.go", "movesWorker.decideReady", "record-writer", "key:SetRecordMove", "d29c22eafacfb894b96cd0c40ec447b6ab02f46ea087201685627861f992085d"},
- {"internal/host/movesreport.go", "movesWorker.targetReport", "nonrecord", "sel:PutReplace", "7a8eaabb9c56b2352969ea49124481a1d3687376b1f9ab6b9ce9ae49d0573843"},
+ {"internal/host/movesreport.go", "movesWorker.targetReport", "nonrecord", "sel:PutReplace", "9a761daeb0a9018ad409ed353c6da6f9e523c63d46ab2c248000327d5f7fcd4f"},
{"internal/host/movesstore.go", "laneProbe", "nonrecord", "sel:PutIfAbsent", "f4a17a1143e7370b9039b2e66e736606015c46619bab92c553ce7b1ad6b7918b"},
{"internal/host/snapshot_loop.go", "registrySnapshotPass", "nonrecord", "sel:Put", "6b01b46b361c3a2d1d432659a0d234c01975849b9a8ac5571858fdd78d376849"},
{"internal/host/snapshot_loop.go", "putReplacing", "nonrecord", "sel:Put", "11e8639effce142eab707b67cbdd425d0d7cea510ddf95d292bdba8f79c9b510"},