Bind Eve receipts to immutable invocations

This commit is contained in:
Meta Vens 2026-08-19 04:18:18 +02:00
commit 4167fc32ca
12 changed files with 65 additions and 13 deletions

View file

@ -14,6 +14,10 @@ export interface EveCommandReceipt {
}[]; }[];
issuedAtUtc?: string; issuedAtUtc?: string;
sourceVersion: number; sourceVersion: number;
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string;
navigation?: { navigation?: {
verseId: string; verseId: string;
authorityRuntimeId?: string; authorityRuntimeId?: string;

View file

@ -622,6 +622,11 @@ export declare const eveContractSchemas: {
readonly type: "integer"; readonly type: "integer";
readonly minimum: 0; readonly minimum: 0;
}; };
readonly invocationHash: {
readonly type: "string";
readonly minLength: 1;
readonly description: "Canonical digest of the immutable invocation envelope finalized by this receipt.";
};
readonly navigation: { readonly navigation: {
readonly type: "object"; readonly type: "object";
readonly required: readonly ["verseId", "surfaceId"]; readonly required: readonly ["verseId", "surfaceId"];
@ -630,6 +635,10 @@ export declare const eveContractSchemas: {
readonly type: "string"; readonly type: "string";
readonly minLength: 1; readonly minLength: 1;
}; };
readonly authorityRuntimeId: {
readonly type: "string";
readonly minLength: 1;
};
readonly providerId: { readonly providerId: {
readonly type: "string"; readonly type: "string";
}; };

View file

@ -746,6 +746,11 @@ export const eveContractSchemas = {
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0
}, },
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": { "navigation": {
"type": "object", "type": "object",
"required": [ "required": [
@ -757,6 +762,10 @@ export const eveContractSchemas = {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"authorityRuntimeId": {
"type": "string",
"minLength": 1
},
"providerId": { "providerId": {
"type": "string" "type": "string"
}, },

View file

@ -16,6 +16,10 @@ diagnostics?: {
}[] }[]
issuedAtUtc?: string issuedAtUtc?: string
sourceVersion: number sourceVersion: number
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string
navigation?: { navigation?: {
verseId: string verseId: string
authorityRuntimeId?: string authorityRuntimeId?: string

View file

@ -746,6 +746,11 @@ export const eveContractSchemas = {
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0
}, },
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": { "navigation": {
"type": "object", "type": "object",
"required": [ "required": [

View file

@ -81,6 +81,7 @@ test("accepts canonical provider, surface, command, invocation, and receipt docu
providerId: "voidbot.swarm", providerId: "voidbot.swarm",
surfaceId: "voidbot.swarm", surfaceId: "voidbot.swarm",
sourceVersion: 1, sourceVersion: 1,
invocationHash: "sha256:command-1",
}).state, "reconciled"); }).state, "reconciled");
}); });

View file

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Description>Portable Eve surface and provider contracts.</Description> <Description>Portable Eve surface and provider contracts.</Description>
<PackageId>GameCult.Eve.Surface</PackageId> <PackageId>GameCult.Eve.Surface</PackageId>
<Version>0.3.3</Version> <Version>0.3.4</Version>
<Authors>GameCult</Authors> <Authors>GameCult</Authors>
<PackageLicenseExpression>MPL-2.0</PackageLicenseExpression> <PackageLicenseExpression>MPL-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/GameCult/Eve</PackageProjectUrl> <PackageProjectUrl>https://github.com/GameCult/Eve</PackageProjectUrl>

View file

@ -366,8 +366,9 @@ namespace GameCult.Eve.Surface
string message, string message,
string issuedAtUtc, string issuedAtUtc,
long sourceVersion, long sourceVersion,
EveSurfaceNavigationTarget? navigation = null) EveSurfaceNavigationTarget? navigation = null,
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation) string invocationHash = "")
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash)
{ {
} }
@ -385,7 +386,8 @@ namespace GameCult.Eve.Surface
string message, string message,
string issuedAtUtc, string issuedAtUtc,
long sourceVersion, long sourceVersion,
EveSurfaceNavigationTarget? navigation = null) EveSurfaceNavigationTarget? navigation = null,
string invocationHash = "")
{ {
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ReceiptId = receiptId ?? ""; ReceiptId = receiptId ?? "";
@ -400,6 +402,7 @@ namespace GameCult.Eve.Surface
IssuedAtUtc = issuedAtUtc ?? ""; IssuedAtUtc = issuedAtUtc ?? "";
SourceVersion = sourceVersion; SourceVersion = sourceVersion;
Navigation = navigation; Navigation = navigation;
InvocationHash = invocationHash ?? "";
} }
[Key(0)] public string Schema { get; } [Key(0)] public string Schema { get; }
@ -415,6 +418,7 @@ namespace GameCult.Eve.Surface
[Key(10)] public string IssuedAtUtc { get; } [Key(10)] public string IssuedAtUtc { get; }
[Key(11)] public long SourceVersion { get; } [Key(11)] public long SourceVersion { get; }
[Key(12)] public EveSurfaceNavigationTarget? Navigation { get; } [Key(12)] public EveSurfaceNavigationTarget? Navigation { get; }
[Key(13)] public string InvocationHash { get; }
} }
/// <summary> /// <summary>
@ -424,7 +428,7 @@ namespace GameCult.Eve.Surface
public sealed class EveCommandReceiptCompatibilityFormatter : public sealed class EveCommandReceiptCompatibilityFormatter :
IMessagePackFormatter<EveCommandReceiptDocument?> IMessagePackFormatter<EveCommandReceiptDocument?>
{ {
private const int LegacyFieldCount = 13; private const int LegacyFieldCount = 14;
private const int RequiredAndScalarFieldCount = 12; private const int RequiredAndScalarFieldCount = 12;
public void Serialize( public void Serialize(
@ -434,9 +438,9 @@ namespace GameCult.Eve.Surface
{ {
if (value == null) { writer.WriteNil(); return; } if (value == null) { writer.WriteNil(); return; }
writer.WriteMapHeader(value.Navigation == null var optionalFields = (value.Navigation == null ? 0 : 1) +
? RequiredAndScalarFieldCount (string.IsNullOrWhiteSpace(value.InvocationHash) ? 0 : 1);
: RequiredAndScalarFieldCount + 1); writer.WriteMapHeader(RequiredAndScalarFieldCount + optionalFields);
Write(ref writer, "schema", value.Schema); Write(ref writer, "schema", value.Schema);
Write(ref writer, "receiptId", value.ReceiptId); Write(ref writer, "receiptId", value.ReceiptId);
Write(ref writer, "commandId", value.CommandId); Write(ref writer, "commandId", value.CommandId);
@ -454,6 +458,8 @@ namespace GameCult.Eve.Surface
writer.Write("navigation"); writer.Write("navigation");
WriteNavigation(ref writer, value.Navigation); WriteNavigation(ref writer, value.Navigation);
} }
if (!string.IsNullOrWhiteSpace(value.InvocationHash))
Write(ref writer, "invocationHash", value.InvocationHash);
} }
public EveCommandReceiptDocument? Deserialize( public EveCommandReceiptDocument? Deserialize(
@ -483,10 +489,11 @@ namespace GameCult.Eve.Surface
var issuedAtUtc = ReadString(ref reader, fields, 10); var issuedAtUtc = ReadString(ref reader, fields, 10);
var sourceVersion = fields > 11 ? reader.ReadInt64() : 0; var sourceVersion = fields > 11 ? reader.ReadInt64() : 0;
var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null; var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null;
var invocationHash = ReadString(ref reader, fields, 13);
for (var index = LegacyFieldCount; index < fields; index++) reader.Skip(); for (var index = LegacyFieldCount; index < fields; index++) reader.Skip();
return new EveCommandReceiptDocument( return new EveCommandReceiptDocument(
schema, receiptId, commandId, command, state, ownerRepo, authority, schema, receiptId, commandId, command, state, ownerRepo, authority,
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation); providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash);
} }
finally { reader.Depth--; } finally { reader.Depth--; }
} }
@ -511,6 +518,7 @@ namespace GameCult.Eve.Surface
var issuedAtUtc = ""; var issuedAtUtc = "";
long sourceVersion = 0; long sourceVersion = 0;
EveSurfaceNavigationTarget? navigation = null; EveSurfaceNavigationTarget? navigation = null;
var invocationHash = "";
var fields = reader.ReadMapHeader(); var fields = reader.ReadMapHeader();
for (var index = 0; index < fields; index++) for (var index = 0; index < fields; index++)
{ {
@ -529,13 +537,14 @@ namespace GameCult.Eve.Surface
case "issuedAtUtc": issuedAtUtc = ReadString(ref reader); break; case "issuedAtUtc": issuedAtUtc = ReadString(ref reader); break;
case "sourceVersion": sourceVersion = reader.ReadInt64(); break; case "sourceVersion": sourceVersion = reader.ReadInt64(); break;
case "navigation": navigation = ReadNavigation(ref reader, options); break; case "navigation": navigation = ReadNavigation(ref reader, options); break;
case "invocationHash": invocationHash = ReadString(ref reader); break;
default: reader.Skip(); break; default: reader.Skip(); break;
} }
} }
return new EveCommandReceiptDocument( return new EveCommandReceiptDocument(
schema, receiptId, commandId, command, state, ownerRepo, authority, schema, receiptId, commandId, command, state, ownerRepo, authority,
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation); providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash);
} }
finally { reader.Depth--; } finally { reader.Depth--; }
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "org.gamecult.eve.surface", "name": "org.gamecult.eve.surface",
"displayName": "Eve Surface Contract", "displayName": "Eve Surface Contract",
"version": "0.3.3", "version": "0.3.4",
"unity": "2021.3", "unity": "2021.3",
"description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.", "description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.",
"dependencies": { "dependencies": {

View file

@ -69,6 +69,11 @@
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0
}, },
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": { "navigation": {
"type": "object", "type": "object",
"required": [ "required": [

View file

@ -209,7 +209,8 @@ public sealed class EveSurfaceSerializationCompatibilityTests
"aetheria.pilot", "aetheria.pilot",
"interactive-world", "interactive-world",
new[] { "cultnet+tcp://odin.gamecult.example:3076" }, new[] { "cultnet+tcp://odin.gamecult.example:3076" },
"commander-daemon")); "commander-daemon"),
"sha256:launch-envelope");
var bytes = MessagePackSerializer.Serialize(document, Options); var bytes = MessagePackSerializer.Serialize(document, Options);
var reader = new MessagePackReader(bytes); var reader = new MessagePackReader(bytes);
@ -217,7 +218,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options); var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options);
Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map)); Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map));
Assert.That(reader.ReadMapHeader(), Is.EqualTo(13)); Assert.That(reader.ReadMapHeader(), Is.EqualTo(14));
Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object)); Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object));
Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation, Is.Not.Null);
Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria")); Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria"));
@ -225,6 +226,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests
Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot"));
Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world")); Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world"));
Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" })); Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" }));
Assert.That(restored.InvocationHash, Is.EqualTo("sha256:launch-envelope"));
} }
[Test] [Test]
@ -263,6 +265,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests
Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation, Is.Not.Null);
Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot"));
Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty); Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty);
Assert.That(restored.InvocationHash, Is.Empty);
}); });
} }
@ -289,7 +292,9 @@ public sealed class EveSurfaceSerializationCompatibilityTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False); Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False);
Assert.That(wireJson.RootElement.TryGetProperty("invocationHash", out _), Is.False);
Assert.That(restored.Navigation, Is.Null); Assert.That(restored.Navigation, Is.Null);
Assert.That(restored.InvocationHash, Is.Empty);
}); });
} }

View file

@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" /> <ProjectReference Include="..\..\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>