fix(snapshot): encode strict generation heads
- commit
- d6a5676f844c3ff7ac56f10bd8f76132885da3b5
- author
- brice · · 2026-07-22 02:13 UTC
- parent
- fc94e20
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +76 -0
A internal/usecase/snapshot/head_test.go +44 -0
| @@ -0,0 +1,44 @@ | |||
| 1 | + | package snapshot | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestHeadCodecIsDeterministicAndStrict(t *testing.T) { | |
| 9 | + | head := Head{Generation: 7, ManifestDigest: SnapshotDigest{1}} | |
| 10 | + | encoded, err := MarshalHead(head) | |
| 11 | + | if err != nil { | |
| 12 | + | t.Fatal(err) | |
| 13 | + | } | |
| 14 | + | encodedAgain, err := MarshalHead(head) | |
| 15 | + | if err != nil { | |
| 16 | + | t.Fatal(err) | |
| 17 | + | } | |
| 18 | + | if string(encoded) != string(encodedAgain) { | |
| 19 | + | t.Fatal("head encoding is not deterministic") | |
| 20 | + | } | |
| 21 | + | got, err := UnmarshalHead(encoded) | |
| 22 | + | if err != nil || got != head { | |
| 23 | + | t.Fatalf("head round trip = %#v, %v", got, err) | |
| 24 | + | } | |
| 25 | + | for n := range len(encoded) { | |
| 26 | + | if _, err := UnmarshalHead(encoded[:n]); err == nil { | |
| 27 | + | t.Fatalf("prefix %d accepted", n) | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | if _, err := UnmarshalHead(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) { | |
| 31 | + | t.Fatalf("trailing data error = %v", err) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func TestHeadCodecRejectsInvalidDeclarations(t *testing.T) { | |
| 36 | + | for _, head := range []Head{ | |
| 37 | + | {ManifestDigest: SnapshotDigest{1}}, | |
| 38 | + | {Generation: 1}, | |
| 39 | + | } { | |
| 40 | + | if _, err := MarshalHead(head); !errors.Is(err, ErrInvalidData) { | |
| 41 | + | t.Fatalf("MarshalHead(%#v) error = %v, want invalid data", head, err) | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | } | |
| @@ -0,0 +1,44 @@ | |||||
| 1 | + | package snapshot | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestHeadCodecIsDeterministicAndStrict(t *testing.T) { | |||
| 9 | + | head := Head{Generation: 7, ManifestDigest: SnapshotDigest{1}} | |||
| 10 | + | encoded, err := MarshalHead(head) | |||
| 11 | + | if err != nil { | |||
| 12 | + | t.Fatal(err) | |||
| 13 | + | } | |||
| 14 | + | encodedAgain, err := MarshalHead(head) | |||
| 15 | + | if err != nil { | |||
| 16 | + | t.Fatal(err) | |||
| 17 | + | } | |||
| 18 | + | if string(encoded) != string(encodedAgain) { | |||
| 19 | + | t.Fatal("head encoding is not deterministic") | |||
| 20 | + | } | |||
| 21 | + | got, err := UnmarshalHead(encoded) | |||
| 22 | + | if err != nil || got != head { | |||
| 23 | + | t.Fatalf("head round trip = %#v, %v", got, err) | |||
| 24 | + | } | |||
| 25 | + | for n := range len(encoded) { | |||
| 26 | + | if _, err := UnmarshalHead(encoded[:n]); err == nil { | |||
| 27 | + | t.Fatalf("prefix %d accepted", n) | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | if _, err := UnmarshalHead(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) { | |||
| 31 | + | t.Fatalf("trailing data error = %v", err) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func TestHeadCodecRejectsInvalidDeclarations(t *testing.T) { | |||
| 36 | + | for _, head := range []Head{ | |||
| 37 | + | {ManifestDigest: SnapshotDigest{1}}, | |||
| 38 | + | {Generation: 1}, | |||
| 39 | + | } { | |||
| 40 | + | if _, err := MarshalHead(head); !errors.Is(err, ErrInvalidData) { | |||
| 41 | + | t.Fatalf("MarshalHead(%#v) error = %v, want invalid data", head, err) | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | } | |||
M internal/usecase/snapshot/manifest.go +32 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | const ( | |
| 15 | 15 | manifestMagic = "VEVM" | |
| 16 | + | headMagic = "VEVH" | |
| 16 | 17 | objectMagic = "VEVO" | |
| 17 | 18 | ManifestVersion = uint16(1) | |
| 18 | 19 | manifestHeaderSize = 16 | |
| @@ -59,6 +60,37 @@ type Head struct { | |||
| 59 | 60 | ManifestDigest SnapshotDigest | |
| 60 | 61 | } | |
| 61 | 62 | ||
| 63 | + | // MarshalHead encodes the deterministic, strict VEVH generation pointer. | |
| 64 | + | func MarshalHead(h Head) ([]byte, error) { | |
| 65 | + | if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) { | |
| 66 | + | return nil, fmt.Errorf("%w: head", ErrInvalidData) | |
| 67 | + | } | |
| 68 | + | var w payloadWriter | |
| 69 | + | w.putUint64(h.Generation) | |
| 70 | + | w.b = append(w.b, h.ManifestDigest[:]...) | |
| 71 | + | return marshalManifestEnvelope(headMagic, w.b) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // UnmarshalHead decodes a complete VEVH generation pointer. | |
| 75 | + | func UnmarshalHead(encoded []byte) (Head, error) { | |
| 76 | + | body, err := unmarshalManifestEnvelope(encoded, headMagic) | |
| 77 | + | if err != nil { | |
| 78 | + | return Head{}, err | |
| 79 | + | } | |
| 80 | + | if len(body) < 8+sha256.Size { | |
| 81 | + | return Head{}, ErrShortPayload | |
| 82 | + | } | |
| 83 | + | if len(body) != 8+sha256.Size { | |
| 84 | + | return Head{}, ErrTrailingBytes | |
| 85 | + | } | |
| 86 | + | head := Head{Generation: binary.BigEndian.Uint64(body[:8])} | |
| 87 | + | copy(head.ManifestDigest[:], body[8:]) | |
| 88 | + | if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) { | |
| 89 | + | return Head{}, fmt.Errorf("%w: head", ErrInvalidData) | |
| 90 | + | } | |
| 91 | + | return head, nil | |
| 92 | + | } | |
| 93 | + | ||
| 62 | 94 | // Manifest is the complete VEVM payload carried directly in a publication. | |
| 63 | 95 | type Manifest struct { | |
| 64 | 96 | Generation uint64 | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | const ( | 14 | const ( | ||
| 15 | manifestMagic = "VEVM" | 15 | manifestMagic = "VEVM" | ||
| 16 | + | headMagic = "VEVH" | |||
| 16 | objectMagic = "VEVO" | 17 | objectMagic = "VEVO" | ||
| 17 | ManifestVersion = uint16(1) | 18 | ManifestVersion = uint16(1) | ||
| 18 | manifestHeaderSize = 16 | 19 | manifestHeaderSize = 16 | ||
| @@ -59,6 +60,37 @@ type Head struct { | |||||
| 59 | ManifestDigest SnapshotDigest | 60 | ManifestDigest SnapshotDigest | ||
| 60 | } | 61 | } | ||
| 61 | 62 | ||||
| 63 | + | // MarshalHead encodes the deterministic, strict VEVH generation pointer. | |||
| 64 | + | func MarshalHead(h Head) ([]byte, error) { | |||
| 65 | + | if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) { | |||
| 66 | + | return nil, fmt.Errorf("%w: head", ErrInvalidData) | |||
| 67 | + | } | |||
| 68 | + | var w payloadWriter | |||
| 69 | + | w.putUint64(h.Generation) | |||
| 70 | + | w.b = append(w.b, h.ManifestDigest[:]...) | |||
| 71 | + | return marshalManifestEnvelope(headMagic, w.b) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // UnmarshalHead decodes a complete VEVH generation pointer. | |||
| 75 | + | func UnmarshalHead(encoded []byte) (Head, error) { | |||
| 76 | + | body, err := unmarshalManifestEnvelope(encoded, headMagic) | |||
| 77 | + | if err != nil { | |||
| 78 | + | return Head{}, err | |||
| 79 | + | } | |||
| 80 | + | if len(body) < 8+sha256.Size { | |||
| 81 | + | return Head{}, ErrShortPayload | |||
| 82 | + | } | |||
| 83 | + | if len(body) != 8+sha256.Size { | |||
| 84 | + | return Head{}, ErrTrailingBytes | |||
| 85 | + | } | |||
| 86 | + | head := Head{Generation: binary.BigEndian.Uint64(body[:8])} | |||
| 87 | + | copy(head.ManifestDigest[:], body[8:]) | |||
| 88 | + | if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) { | |||
| 89 | + | return Head{}, fmt.Errorf("%w: head", ErrInvalidData) | |||
| 90 | + | } | |||
| 91 | + | return head, nil | |||
| 92 | + | } | |||
| 93 | + | ||||
| 62 | // Manifest is the complete VEVM payload carried directly in a publication. | 94 | // Manifest is the complete VEVM payload carried directly in a publication. | ||
| 63 | type Manifest struct { | 95 | type Manifest struct { | ||
| 64 | Generation uint64 | 96 | Generation uint64 | ||