Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
3996ce2
feat: author and accept storage submissions
mellowcroc Sep 15, 2026
5329176
fix: bind submissions to logical ceremony paths
mellowcroc Sep 15, 2026
89da1e4
Preserve released ceremony verification before protocol revision
mellowcroc Sep 15, 2026
e11c7a9
Add versioned trusted-coordinator checkpoint state and delivery recovery
mellowcroc Sep 15, 2026
fc8664c
Verify V4 checkpoint turn artifacts against replayed chain
mellowcroc Sep 15, 2026
36e4f47
Exercise V4 checkpoints with a real signed contribution turn
mellowcroc Sep 15, 2026
b2ba15b
Bind V4 custody evidence and verify phase lifecycle artifacts
mellowcroc Sep 15, 2026
0cee66e
Record verified enrollments and per-head mirror evidence in V4
mellowcroc Sep 15, 2026
16f40d1
Enforce checkpointed witness and beacon evidence before sealing
mellowcroc Sep 15, 2026
a686529
Bind V4 final candidates to complete coordinator replay
mellowcroc Sep 15, 2026
89b582f
Collect checkpointed audits without weakening release quorum
mellowcroc Sep 15, 2026
2bfae66
Derive operational evidence from authenticated V4 history
mellowcroc Sep 15, 2026
4ae6030
Preserve V4 incidents and enforce terminal abort and restart
mellowcroc Sep 15, 2026
3351b08
Verify exact V4 final review without duplicate contribution replay
mellowcroc Sep 15, 2026
9bc7b89
Bind V4 public verification to the exact bytes read
mellowcroc Sep 15, 2026
badfa74
Derive V4 review dependencies without historical payload copies
mellowcroc Sep 15, 2026
4cffdf1
Sign and verify exact V4 release packages with bound coordinator review
mellowcroc Sep 15, 2026
e8a1c95
Bound V4 release checksums for maximum review inventories
mellowcroc Sep 15, 2026
2a2a42d
Record exact private V4 release packages in ceremony checkpoints
mellowcroc Sep 15, 2026
4e6d3d3
Bind V4 production decisions to exact verified release packages
mellowcroc Sep 15, 2026
cb9fa69
Route production decision CLI through authenticated V4 verification
mellowcroc Sep 15, 2026
bf9029a
Wire V4 release package signing and verification without duplicate re…
mellowcroc Sep 15, 2026
cf8ef12
Expose opt-in V4 checkpoint preparation signing and structural inspec…
mellowcroc Sep 15, 2026
5c143c5
Add checkpoint-bound V4 bundle and release evidence commands
mellowcroc Sep 15, 2026
9841387
Add authenticated protocol and bounded checkpoint discovery
mellowcroc Sep 15, 2026
47ba50e
Cover discovery command wiring and rejected secret flags
mellowcroc Sep 15, 2026
26bf46c
Expose head-bound turn commitments and batch enrollment guidance
mellowcroc Sep 15, 2026
a32d27d
Inspect retained V4 contribution inventories for safe recovery
mellowcroc Sep 16, 2026
897fdb5
Inspect generated V4 contribution files before cleanup signing
mellowcroc Sep 16, 2026
390c5a5
Expose exact authenticated definition references for workflow binding
mellowcroc Sep 16, 2026
bc64ab8
Simplify storage-first ceremony turns
mellowcroc Sep 16, 2026
2861cf2
Check checkpoint reader close results
mellowcroc Sep 16, 2026
06d6b0d
Bind upload grants to allocation checkpoints
mellowcroc Sep 16, 2026
7f71a99
Derive initial storage checkpoint in proof tool
mellowcroc Sep 16, 2026
ee45b51
Derive signed V4 lifecycle checkpoints
mellowcroc Sep 16, 2026
f6ffbd1
Record signed V4 release review state
mellowcroc Sep 16, 2026
fc92ccb
Allow cross-platform V4 publication verification
mellowcroc Sep 16, 2026
f2481ad
Fix V4 lint findings
mellowcroc Sep 16, 2026
6d5cf0e
Prepare authenticated beacon evidence records
mellowcroc Sep 16, 2026
e729f69
Expose closed final release download inventory
mellowcroc Sep 16, 2026
fec26a6
Expose committed beacon evidence by phase
mellowcroc Sep 16, 2026
d2c7707
Simplify V4 beacon evidence
mellowcroc Sep 16, 2026
260dba9
Include circuit in initial V4 checkpoint
mellowcroc Sep 16, 2026
03b3b3d
Encode empty V4 evidence lists explicitly
mellowcroc Sep 16, 2026
ac753b3
Fix V4 final release inventory discovery
mellowcroc Sep 16, 2026
f4ce9d0
Fix V4 proof-tool regression coverage
mellowcroc Sep 16, 2026
8060cfc
Correct V4 cross-platform signing regression
mellowcroc Sep 16, 2026
8e73e81
Add direct V4 candidate rejection checkpoint
mellowcroc Sep 17, 2026
f35e524
Fix V4 checkpoint lint suggestions
mellowcroc Sep 17, 2026
cd6fe23
Classify semantic V4 candidate failures
mellowcroc Sep 17, 2026
4419de8
Fix V4 checkpoint command regression tests
mellowcroc Sep 17, 2026
c98459b
Classify invalid V4 candidate contents
mellowcroc Sep 17, 2026
fe4a298
Test idempotent V4 rejection replay
mellowcroc Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cmd/mpc-ceremony/atomic_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"errors"
"os"
"path/filepath"
"slices"
)

// writeAtomicOutputDir publishes a small closed set of result files with a
// no-replace directory rename. A byte-identical completed result is an
// idempotent success; incomplete or conflicting output is never overwritten.
func writeAtomicOutputDir(outDir string, files map[string][]byte) (err error) {
parent := filepath.Dir(outDir)
if err := os.MkdirAll(parent, 0o700); err != nil {
return err
}
if _, err := os.Lstat(outDir); err == nil {
info, statErr := os.Lstat(outDir)
if statErr != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return errors.New("atomic output path exists but is not a regular directory")
}
for name, expected := range files {
actual, readErr := readRegularOperationalFile(filepath.Join(outDir, name), maxOperationalRecordBytes)
if readErr != nil || !slices.Equal(actual, expected) {
return errors.New("atomic output already exists with conflicting or incomplete contents")
}
}
entries, readErr := os.ReadDir(outDir)
if readErr != nil || len(entries) != len(files) {
return errors.New("atomic output already exists with conflicting or incomplete contents")
}
return nil
} else if !os.IsNotExist(err) {
return err
}
tmp, err := os.MkdirTemp(parent, ".atomic-output-*")
if err != nil {
return err
}
complete := false
defer func() {
if !complete {
_ = os.RemoveAll(tmp)
}
}()
for name, data := range files {
if err := writeFreshOperationalFile(filepath.Join(tmp, name), data, 0o600); err != nil {
return err
}
}
if err := syncDirectory(tmp); err != nil {
return err
}
if err := renameDirectoryNoReplace(tmp, outDir); err != nil {
return err
}
complete = true
return syncDirectory(parent)
}
36 changes: 36 additions & 0 deletions cmd/mpc-ceremony/atomic_output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestAtomicOutputDirectoryIsIdempotentAndClosed(t *testing.T) {
out := filepath.Join(t.TempDir(), "result")
files := map[string][]byte{"checkpoint.json": []byte("checkpoint"), "checkpoint.sig": []byte("signature")}
if err := writeAtomicOutputDir(out, files); err != nil {
t.Fatal(err)
}
if err := writeAtomicOutputDir(out, files); err != nil {
t.Fatalf("byte-identical retry: %v", err)
}
if err := os.WriteFile(filepath.Join(out, "checkpoint.sig"), []byte("changed"), 0o600); err != nil {
t.Fatal(err)
}
if err := writeAtomicOutputDir(out, files); err == nil || !strings.Contains(err.Error(), "conflicting or incomplete") {
t.Fatalf("conflicting retry = %v", err)
}
}

func TestAtomicOutputFailureDoesNotPublishDirectory(t *testing.T) {
parent := t.TempDir()
out := filepath.Join(parent, "result")
if err := writeAtomicOutputDir(out, map[string][]byte{"nested/file": []byte("invalid")}); err == nil {
t.Fatal("invalid nested output succeeded")
}
if _, err := os.Lstat(out); !os.IsNotExist(err) {
t.Fatalf("failed output was published: %v", err)
}
}
95 changes: 92 additions & 3 deletions cmd/mpc-ceremony/checkpoint_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ type builtCheckpointEvidence struct {
request mpcceremony.CheckpointSigningRequest
}

type checkpointAcceptanceSigner func(
trusted *mpcceremony.TrustedCeremony,
checkpoint mpcceremony.Checkpoint,
slot mpcceremony.CheckpointSubmissionSlot,
envelope mpcceremony.SubmissionEnvelopeV1,
envelopeRefs mpcceremony.SignedArtifactRefs,
manifest mpcceremony.ArtifactRef,
) ([]byte, []byte, mpcceremony.SignedArtifactRefs, error)

func parseCheckpoint(invocation Invocation, args []string) (Invocation, error) {
if len(args) == 0 {
return Invocation{}, &usageError{message: "missing checkpoint command", topic: []string{"checkpoint"}}
Expand All @@ -36,6 +45,14 @@ func parseCheckpoint(invocation Invocation, args []string) (Invocation, error) {
return Invocation{}, &helpRequest{topic: append([]string{"checkpoint"}, args[1:]...)}
}
switch args[0] {
case "verify-release-v4":
options, err := parseEvidenceV4(CommandCheckpointVerifyReleaseV4, args[1:])
invocation.Command, invocation.Options = CommandCheckpointVerifyReleaseV4, options
return invocation, wrapCommandError(err, "checkpoint", args[0])
case "prepare-v4", "sign-v4", "initialize-v4", "record-v4", "allocate-v4", "accept-candidate-v4", "reject-candidate-v4", "verify-stored-v4", "inspect-signed-v4", "inspect-enrollments-v4":
options, err := parseCheckpointV4(args[0], args[1:])
invocation.Command, invocation.Options = Command("checkpoint "+args[0]), options
return invocation, wrapCommandError(err, "checkpoint", args[0])
case "prepare":
options, err := parseCheckpointPrepare(args[1:])
invocation.Command, invocation.Options = CommandCheckpointPrepare, options
Expand Down Expand Up @@ -580,6 +597,9 @@ func buildCheckpointEvidenceWithParent(options CheckpointEvidenceOptions, verify
if err != nil {
return builtCheckpointEvidence{}, err
}
if trusted.Definition.Schema == mpcceremony.DefinitionSchemaV4 {
return builtCheckpointEvidence{}, errors.New("definition v4 requires the explicit V4 checkpoint commands")
}
definitionRefs, err := checkpointPairRefs(options.ArtifactRoot, options.CeremonyPath, options.CeremonySignaturePath)
if err != nil {
return builtCheckpointEvidence{}, fmt.Errorf("definition references: %w", err)
Expand Down Expand Up @@ -1372,7 +1392,7 @@ func buildPhase1ClosedCheckpoint(options CheckpointEvidenceOptions, trusted *mpc
}

func checkpointSchemaForDefinition(definition mpcceremony.CeremonyDefinition) string {
if definition.Schema == mpcceremony.DefinitionSchema {
if definition.Schema == mpcceremony.DefinitionSchemaV3 {
return mpcceremony.CheckpointSchema
}
return mpcceremony.CheckpointSchemaV1
Expand Down Expand Up @@ -1446,21 +1466,33 @@ func buildReceiptCheckpoint(options CheckpointEvidenceOptions, trusted *mpccerem
if err != nil {
return builtCheckpointEvidence{}, err
}
if err := requireSubmissionEnvelopeNames(slot, envelopeRefs); err != nil {
return builtCheckpointEvidence{}, err
}
envelope, err := mpcceremony.VerifySignedSubmissionEnvelope(trusted.Definition, previous, slot, envelopeBytes, envelopeSignatureBytes)
if err != nil {
return builtCheckpointEvidence{}, err
}
if envelope.Kind != mpcceremony.CheckpointSubmissionReceipt {
return builtCheckpointEvidence{}, errors.New("receipt-accepted checkpoint requires a receipt submission envelope")
}
if err := requireReceiptPayloadNames(slot, envelope.Payloads); err != nil {
return builtCheckpointEvidence{}, err
}
if err := verifyReceiptEnvelopePayloads(options.ArtifactRoot, trusted, previous, envelope); err != nil {
return builtCheckpointEvidence{}, err
}
manifestBytes, manifest, err := checkpointArtifactBytes(options.ArtifactRoot, options.ManifestPath, maxOperationalRecordBytes)
if err != nil {
return builtCheckpointEvidence{}, fmt.Errorf("submission manifest: %w", err)
}
ackBytes, ackSignatureBytes, ackRefs, err := checkpointSignedBytes(options.ArtifactRoot, options.AcknowledgementPath, options.AcknowledgementSignaturePath)
var ackBytes, ackSignatureBytes []byte
var ackRefs mpcceremony.SignedArtifactRefs
if options.AcceptanceSigner != nil {
ackBytes, ackSignatureBytes, ackRefs, err = options.AcceptanceSigner(trusted, previous, slot, envelope, envelopeRefs, manifest)
} else {
ackBytes, ackSignatureBytes, ackRefs, err = checkpointAcknowledgementBytes(options)
}
if err != nil {
return builtCheckpointEvidence{}, fmt.Errorf("submission acknowledgement: %w", err)
}
Expand Down Expand Up @@ -1539,6 +1571,9 @@ func buildCandidateCheckpoint(options CheckpointEvidenceOptions, trusted *mpccer
if err != nil {
return builtCheckpointEvidence{}, err
}
if err := requireSubmissionEnvelopeNames(slot, envelopeRefs); err != nil {
return builtCheckpointEvidence{}, err
}
if slot.Kind != mpcceremony.CheckpointSubmissionCandidate {
return builtCheckpointEvidence{}, errors.New("candidate-accepted checkpoint requires a candidate submission slot")
}
Expand All @@ -1558,7 +1593,13 @@ func buildCandidateCheckpoint(options CheckpointEvidenceOptions, trusted *mpccer
if err != nil {
return builtCheckpointEvidence{}, fmt.Errorf("candidate manifest: %w", err)
}
ackBytes, ackSignatureBytes, ackRefs, err := checkpointSignedBytes(options.ArtifactRoot, options.AcknowledgementPath, options.AcknowledgementSignaturePath)
var ackBytes, ackSignatureBytes []byte
var ackRefs mpcceremony.SignedArtifactRefs
if options.AcceptanceSigner != nil {
ackBytes, ackSignatureBytes, ackRefs, err = options.AcceptanceSigner(trusted, previous, slot, envelope, envelopeRefs, manifest)
} else {
ackBytes, ackSignatureBytes, ackRefs, err = checkpointAcknowledgementBytes(options)
}
if err != nil {
return builtCheckpointEvidence{}, fmt.Errorf("candidate acknowledgement: %w", err)
}
Expand Down Expand Up @@ -1787,6 +1828,29 @@ func findAllocatedSubmission(checkpoint mpcceremony.Checkpoint, envelope mpccere
return mpcceremony.CheckpointSubmissionSlot{}, errors.New("submission envelope does not match an allocated checkpoint slot")
}

func requireSubmissionEnvelopeNames(slot mpcceremony.CheckpointSubmissionSlot, refs mpcceremony.SignedArtifactRefs) error {
base := strings.TrimSuffix(slot.ManifestKey, "/manifest.json")
if refs.Record.Name != base+"/envelope.json" || refs.Signature.Name != base+"/envelope.sig" {
return errors.New("submission envelope does not use the preallocated storage path")
}
return nil
}

func requireReceiptPayloadNames(slot mpcceremony.CheckpointSubmissionSlot, refs []mpcceremony.ArtifactRef) error {
base := fmt.Sprintf("%s/custody/%04d", slot.Phase, slot.Index)
want := []string{base + "/outbound-receipt.json", base + "/outbound-receipt.sig"}
got := make([]string, len(refs))
for i := range refs {
got[i] = refs[i].Name
}
slices.Sort(got)
slices.Sort(want)
if !slices.Equal(got, want) {
return errors.New("receipt submission does not use the deterministic ceremony evidence paths")
}
return nil
}

func checkpointSignedBytes(root, recordPath, signaturePath string) ([]byte, []byte, mpcceremony.SignedArtifactRefs, error) {
recordBytes, recordRef, err := checkpointArtifactBytes(root, recordPath, maxOperationalRecordBytes)
if err != nil {
Expand All @@ -1800,6 +1864,31 @@ func checkpointSignedBytes(root, recordPath, signaturePath string) ([]byte, []by
return recordBytes, signatureBytes, refs, nil
}

func checkpointAcknowledgementBytes(options CheckpointEvidenceOptions) ([]byte, []byte, mpcceremony.SignedArtifactRefs, error) {
if options.AcknowledgementRecordName == "" && options.AcknowledgementSignatureName == "" {
return checkpointSignedBytes(options.ArtifactRoot, options.AcknowledgementPath, options.AcknowledgementSignaturePath)
}
if options.AcknowledgementRecordName == "" || options.AcknowledgementSignatureName == "" {
return nil, nil, mpcceremony.SignedArtifactRefs{}, errors.New("both intended acknowledgement names are required")
}
record, err := readRegularOperationalFile(options.AcknowledgementPath, maxOperationalRecordBytes)
if err != nil {
return nil, nil, mpcceremony.SignedArtifactRefs{}, err
}
signature, err := readRegularOperationalFile(options.AcknowledgementSignaturePath, 4096)
if err != nil {
return nil, nil, mpcceremony.SignedArtifactRefs{}, err
}
refs := mpcceremony.SignedArtifactRefs{
Record: mpcceremony.ArtifactRef{Name: options.AcknowledgementRecordName, Digest: mpcceremony.NewDigest(record)},
Signature: mpcceremony.ArtifactRef{Name: options.AcknowledgementSignatureName, Digest: mpcceremony.NewDigest(signature)},
}
if err := refs.Validate(); err != nil {
return nil, nil, mpcceremony.SignedArtifactRefs{}, err
}
return record, signature, refs, nil
}

func requireCheckpointArtifactName(ref mpcceremony.ArtifactRef, expected, label string) error {
if ref.Name != expected {
return fmt.Errorf("%s must use canonical storage path %q, got %q", label, expected, ref.Name)
Expand Down
19 changes: 13 additions & 6 deletions cmd/mpc-ceremony/checkpoint_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ func TestCheckpointPrepareReceiptAcceptedAuthenticatesInnerEvidence(t *testing.T
if err != nil {
t.Fatal(err)
}
receiptPath := filepath.Join(fixture.root, "submissions", "receipt", slot.AttemptID, "receipt.json")
receiptSignaturePath := filepath.Join(fixture.root, "submissions", "receipt", slot.AttemptID, "receipt.sig")
receiptPath := filepath.Join(fixture.root, "phase1", "custody", "0001", "outbound-receipt.json")
receiptSignaturePath := filepath.Join(fixture.root, "phase1", "custody", "0001", "outbound-receipt.sig")
if err := os.MkdirAll(filepath.Dir(receiptPath), 0o700); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -221,6 +221,9 @@ func TestCheckpointPrepareReceiptAcceptedAuthenticatesInnerEvidence(t *testing.T
}
envelopePath := filepath.Join(fixture.root, "submissions", "receipt", slot.AttemptID, "envelope.json")
envelopeSignaturePath := filepath.Join(fixture.root, "submissions", "receipt", slot.AttemptID, "envelope.sig")
if err := os.MkdirAll(filepath.Dir(envelopePath), 0o700); err != nil {
t.Fatal(err)
}
envelopeBytes, envelopeSignatureBytes, err := mpcceremony.SignSubmissionEnvelope(fixture.definition, cp1, slot, envelope, participantKey)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -785,7 +788,7 @@ func prepareAndSignCandidateCheckpoint(t *testing.T, fixture checkpointCLIFixtur
ParentCheckpointSHA256: slot.BasisCheckpointSHA256, AllocationCheckpointSHA256: mpcceremony.NewDigest(previousBytes).SHA256,
ParentHeadID: slot.ParentHeadID, AttemptID: slot.AttemptID, ManifestKey: slot.ManifestKey, Payloads: payloads,
}
envelopeDir := filepath.Join(fixture.root, "submissions", string(phase)+"-candidate", slot.AttemptID)
envelopeDir := filepath.Join(fixture.root, filepath.FromSlash(strings.TrimSuffix(slot.ManifestKey, "/manifest.json")))
if err := os.MkdirAll(envelopeDir, 0o700); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1037,11 +1040,11 @@ func prepareAndSignReceiptCheckpoint(t *testing.T, fixture checkpointCLIFixture,
if err != nil {
t.Fatal(err)
}
receiptDir := filepath.Join(fixture.root, "submissions", "receipt", slot.AttemptID)
receiptDir := filepath.Join(fixture.root, string(phase), "custody", fmt.Sprintf("%04d", slot.Index))
if err := os.MkdirAll(receiptDir, 0o700); err != nil {
t.Fatal(err)
}
receiptPath, receiptSignaturePath := filepath.Join(receiptDir, "receipt.json"), filepath.Join(receiptDir, "receipt.sig")
receiptPath, receiptSignaturePath := filepath.Join(receiptDir, "outbound-receipt.json"), filepath.Join(receiptDir, "outbound-receipt.sig")
receiptBytes, receiptSignatureBytes, err := mpcceremony.SignRecord(receipt, fixture.definition.Roster[0].Identity.KeyID, participantKey)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1071,7 +1074,11 @@ func prepareAndSignReceiptCheckpoint(t *testing.T, fixture checkpointCLIFixture,
AttemptID: slot.AttemptID, ManifestKey: slot.ManifestKey,
Payloads: checkpointSortedArtifacts(receiptRef, receiptSignatureRef),
}
envelopePath, envelopeSignaturePath := filepath.Join(receiptDir, "envelope.json"), filepath.Join(receiptDir, "envelope.sig")
envelopeDir := filepath.Join(fixture.root, filepath.FromSlash(strings.TrimSuffix(slot.ManifestKey, "/manifest.json")))
if err := os.MkdirAll(envelopeDir, 0o700); err != nil {
t.Fatal(err)
}
envelopePath, envelopeSignaturePath := filepath.Join(envelopeDir, "envelope.json"), filepath.Join(envelopeDir, "envelope.sig")
envelopeBytes, envelopeSignatureBytes, err := mpcceremony.SignSubmissionEnvelope(fixture.definition, cp1, slot, envelope, participantKey)
if err != nil {
t.Fatal(err)
Expand Down
Loading
Loading