From c73143029788abdf61139fc897bced7dfaf0a768 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 16:26:38 +0100 Subject: [PATCH] Harden released world witness evidence --- .../PlayMode/GenericWorldCaptureTests.cs | 87 ++++++++++++++----- scripts/run-aetheria-daemon-world-witness.ps1 | 46 +++++++++- 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 31c58b6..344a044 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -271,21 +271,23 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var deadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < deadline && - (runtime.LastReceipt == null || runtime.ActiveVersion <= initialVersion || + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != request.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion <= initialVersion || Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition) < 0.01f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(request.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + request.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + initialVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition); Assert.That(movementDistance, Is.GreaterThan(0.01f)); movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); @@ -295,15 +297,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); - Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + focus.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + movementVersion); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); var focusVersion = runtime.ActiveVersion; @@ -311,18 +317,18 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var actionDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < actionDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + action.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + focusVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); @@ -366,7 +372,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; - Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); + Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -375,6 +381,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); + var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); @@ -404,6 +412,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion, runtime.ActiveVersion, movementDistance, + playerRenderers.Length, + pilotChangedPixels, mapRenderers.Count, mapChangedPixels, (camera.cullingMask & (1 << mapLayer)) == 0, @@ -433,6 +443,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int playerRendererCount, + int pilotChangedPixels, int mapChannelRendererCount, int mapChangedPixels, bool pilotCameraExcludesMapChannel, @@ -453,6 +465,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + playerRendererCount = playerRendererCount, + pilotChangedPixels = pilotChangedPixels, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -474,6 +488,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int playerRendererCount; + public int pilotChangedPixels; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -487,6 +503,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string commandKind; public string commandId; public string state; + public string receiptId; + public string ownerRepo; + public string authority; + public string providerId; + public string surfaceId; + public bool isProviderOwned; public long sourceVersion; public static WitnessReceipt From(string commandKind, EveUnitySceneCommandReceipt receipt) @@ -496,11 +518,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests commandKind = commandKind, commandId = receipt.CommandId, state = receipt.State, + receiptId = receipt.ReceiptId, + ownerRepo = receipt.OwnerRepo, + authority = receipt.Authority, + providerId = receipt.ProviderId, + surfaceId = receipt.SurfaceId, + isProviderOwned = receipt.IsProviderOwned, sourceVersion = receipt.SourceVersion }; } } + private static void AssertReconciledProviderReceipt( + EveUnitySceneCommandReceipt receipt, + string commandId, + string providerId, + string surfaceId, + long minimumSourceVersion) + { + Assert.That(receipt, Is.Not.Null); + Assert.That(receipt.CommandId, Is.EqualTo(commandId)); + Assert.That(receipt.State, Is.EqualTo("reconciled"), receipt.Message); + Assert.That(receipt.IsProviderOwned, Is.True); + Assert.That(receipt.ProviderId, Is.EqualTo(providerId)); + Assert.That(receipt.SurfaceId, Is.EqualTo(surfaceId)); + Assert.That(receipt.ReceiptId, Is.Not.Empty); + Assert.That(receipt.OwnerRepo, Is.Not.Empty); + Assert.That(receipt.Authority, Is.Not.Empty); + Assert.That(receipt.SourceVersion, Is.GreaterThan(minimumSourceVersion)); + } + private static EveUnityPlayableWorldEntityMarker FindMarker(GameObject root, string entityId) { foreach (var marker in root.GetComponentsInChildren()) diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 16e2629..c549801 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -12,6 +12,8 @@ param( ) $ErrorActionPreference = "Stop" +$expectedEveUnityCommit = "ed21479e467faea8ff624f2d2f5a7d2fecf913f4" +$expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } @@ -34,9 +36,17 @@ foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonPr } New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null +foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $witnessPath)) { + if (Test-Path -LiteralPath $priorArtifact) { Remove-Item -LiteralPath $priorArtifact -Force } +} if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { Remove-Item -LiteralPath $assetCachePath -Recurse -Force } +$initialBodyCount = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue).Count +$initialPartialCount = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue).Count +if ($CacheState -eq "cold" -and ($initialBodyCount -ne 0 -or $initialPartialCount -ne 0)) { + throw "Cold witness cache was not empty before Unity launch. bodies=$initialBodyCount partials=$initialPartialCount" +} $cacheWasWarm = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -ErrorAction SilentlyContinue).Count -gt 0 if ($CacheState -eq "warm" -and -not $cacheWasWarm) { throw "Warm witness requested without an existing verified bundle cache at $assetCachePath" @@ -155,13 +165,45 @@ try { if (-not (Test-Path $capturePath) -or (Get-Item $capturePath).Length -lt 1024) { throw "Live world capture is missing: $capturePath" } if (-not (Test-Path $mapCapturePath) -or (Get-Item $mapCapturePath).Length -lt 1024) { throw "Live map-channel capture is missing: $mapCapturePath" } if (-not (Test-Path $factsPath)) { throw "Live world witness facts are missing: $factsPath" } + foreach ($freshArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath)) { + if ((Get-Item -LiteralPath $freshArtifact).LastWriteTimeUtc -lt $witnessStartedAt.UtcDateTime) { + throw "Live witness artifact predates this run: $freshArtifact" + } + } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" + $sceneLock = $releaseLock.dependencies.'org.gamecult.eve.unity-scene' + $cultLibLock = $releaseLock.dependencies.'org.gamecult.cultlib' + if (-not $releasedPackageClient) { throw "Released witness must run ReleaseConsumerProject, got '$projectRoot'." } + if ($sceneLock.source -ne "git" -or $cultLibLock.source -ne "git" -or + $sceneLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { + throw "Released witness resolved a non-git/local package dependency." + } + if ($sceneLock.hash -ne $expectedEveUnityCommit -or $cultLibLock.hash -ne $expectedCultLibCommit) { + throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) cultlib=$($cultLibLock.hash)" + } + $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) + $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) + if ($finalBodies.Count -ne 1 -or $finalPartials.Count -ne 0) { + throw "Content promotion is incomplete. bodies=$($finalBodies.Count) partials=$($finalPartials.Count)" + } + $bodyHash = (Get-FileHash -LiteralPath $finalBodies[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() + if ($finalBodies[0].BaseName -ne $bodyHash) { + throw "Promoted content body name does not match its SHA-256. name=$($finalBodies[0].BaseName) hash=$bodyHash" + } $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot - $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.eve.unity-scene'.hash - $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.cultlib'.hash + $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $sceneLock.hash + $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $cultLibLock.hash + $facts | Add-Member -NotePropertyName contentDelivery -NotePropertyValue ([ordered]@{ + initialBodyCount = $initialBodyCount + initialPartialCount = $initialPartialCount + finalBodyCount = $finalBodies.Count + finalPartialCount = $finalPartials.Count + contentHash = $bodyHash + sizeBytes = $finalBodies[0].Length + }) $capture = Get-Item -LiteralPath $capturePath $mapCapture = Get-Item -LiteralPath $mapCapturePath $resultsArtifact = Get-Item -LiteralPath $resultsPath