Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,7 @@ client, _ := hcs27.NewClient(hcs27.ClientConfig{
metadata := hcs27.CheckpointMetadata{
Type: "ans-checkpoint-v1",
Stream: hcs27.StreamID{Registry: "ans", LogID: "default"},
Root: hcs27.RootCommitment{TreeSize: 1, RootHashB64: "<base64url-root>"},
BatchRange: hcs27.BatchRange{
Start: 1,
End: 1,
},
Root: hcs27.RootCommitment{TreeSize: "1", RootHashB64u: "<base64url-root>"},
}
```

Expand Down
10 changes: 3 additions & 7 deletions examples/hcs27-publish-checkpoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,11 @@ func main() {
Log: &hcs27.LogProfile{
Algorithm: "sha-256",
Leaf: "sha256(jcs(event))",
Merkle: "rfc6962",
Merkle: "rfc9162",
},
Root: hcs27.RootCommitment{
TreeSize: 1,
RootHashB64: root,
},
BatchRange: hcs27.BatchRange{
Start: 1,
End: 1,
TreeSize: "1",
RootHashB64u: root,
},
}

Expand Down
26 changes: 20 additions & 6 deletions pkg/hcs27/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ func (c *Client) GetCheckpoints(
// ValidateCheckpointChain validates the provided input value.
func ValidateCheckpointChain(records []CheckpointRecord) error {
type previousRecord struct {
TreeSize uint64
RootHashB64 string
TreeSize uint64
RootHashB64u string
}

streams := map[string]previousRecord{}
Expand All @@ -409,9 +409,16 @@ func ValidateCheckpointChain(records []CheckpointRecord) error {
)

previous, exists := streams[streamID]
currentTreeSize, err := parseCanonicalUint64(
"metadata.root.treeSize",
record.EffectiveMetadata.Root.TreeSize,
)
if err != nil {
return err
}
current := previousRecord{
TreeSize: record.EffectiveMetadata.Root.TreeSize,
RootHashB64: record.EffectiveMetadata.Root.RootHashB64,
TreeSize: currentTreeSize,
RootHashB64u: record.EffectiveMetadata.Root.RootHashB64u,
}

if exists {
Expand All @@ -421,10 +428,17 @@ func ValidateCheckpointChain(records []CheckpointRecord) error {
if record.EffectiveMetadata.Previous == nil {
return fmt.Errorf("missing prev linkage for stream %s", streamID)
}
if record.EffectiveMetadata.Previous.TreeSize != previous.TreeSize {
previousTreeSize, err := parseCanonicalUint64(
"metadata.prev.treeSize",
record.EffectiveMetadata.Previous.TreeSize,
)
if err != nil {
return err
}
if previousTreeSize != previous.TreeSize {
return fmt.Errorf("prev.treeSize mismatch for stream %s", streamID)
}
if record.EffectiveMetadata.Previous.RootHashB64 != previous.RootHashB64 {
if record.EffectiveMetadata.Previous.RootHashB64u != previous.RootHashB64u {
return fmt.Errorf("prev.rootHashB64u mismatch for stream %s", streamID)
}
}
Expand Down
10 changes: 3 additions & 7 deletions pkg/hcs27/client_overflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,11 @@ func testCheckpointMetadata(leafProfile string) CheckpointMetadata {
Log: &LogProfile{
Algorithm: "sha-256",
Leaf: leaf,
Merkle: "rfc6962",
Merkle: "rfc9162",
},
Root: RootCommitment{
TreeSize: 1,
RootHashB64: base64.RawURLEncoding.EncodeToString(make([]byte, 32)),
},
BatchRange: BatchRange{
Start: 1,
End: 1,
TreeSize: canonicalUint64(1),
RootHashB64u: base64.RawURLEncoding.EncodeToString(make([]byte, 32)),
},
}
}
Expand Down
20 changes: 14 additions & 6 deletions pkg/hcs27/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestNewClientFailures(t *testing.T) {
if err == nil {
t.Fatal("expected err invalid op string")
}

_, err = NewClient(ClientConfig{Network: "testnet", OperatorAccountID: "0.0.1", OperatorPrivateKey: "invalid-pk"})
if err == nil {
t.Fatal("expected err invalid pk string")
Expand Down Expand Up @@ -63,15 +63,19 @@ func TestExecutionFailures(t *testing.T) {
UseOperatorAsAdmin: true,
})
// because `hederaClient` hits network, this usually fails
if err == nil { t.Fatal("expected fail") }
if err == nil {
t.Fatal("expected fail")
}

_, err = client.PublishCheckpoint(ctx, "0.0.1", CheckpointMetadata{}, "", "")
if err == nil { t.Fatal("expected fail") }
if err == nil {
t.Fatal("expected fail")
}
}

func TestMirrorFailures(t *testing.T) {
pk, _ := hedera.PrivateKeyGenerateEcdsa()

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messages": []}`))
Expand All @@ -88,8 +92,12 @@ func TestMirrorFailures(t *testing.T) {
ctx := context.Background()

_, err := client.GetCheckpoints(ctx, "0.0.1", nil)
if err != nil { t.Fatalf("unexpected err: %v", err) }
if err != nil {
t.Fatalf("unexpected err: %v", err)
}

_, err = client.ResolveHCS1Reference(ctx, "hcs://1/0.0.1")
if err == nil { t.Fatal("expected fail") }
if err == nil {
t.Fatal("expected fail")
}
}
2 changes: 1 addition & 1 deletion pkg/hcs27/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// metadata := hcs27.CheckpointMetadata{
// Type: "ans-checkpoint-v1",
// Stream: hcs27.StreamID{Registry: "ans", LogID: "default"},
// Root: hcs27.RootCommitment{TreeSize: 100, RootHashB64: "<root>"},
// Root: hcs27.RootCommitment{TreeSize: "100", RootHashB64u: "<root>"},
// }
//
// This package is part of the HOL Standards SDK for Go.
Expand Down
28 changes: 28 additions & 0 deletions pkg/hcs27/integers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package hcs27

import (
"fmt"
"strconv"
"strings"
)

func parseCanonicalUint64(fieldName string, value string) (uint64, error) {

Check failure on line 9 in pkg/hcs27/integers.go

View workflow job for this annotation

GitHub Actions / Lint Code

paramTypeCombine: func(fieldName string, value string) (uint64, error) could be replaced with func(fieldName, value string) (uint64, error) (gocritic)
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return 0, fmt.Errorf("%s is required", fieldName)
}
if trimmed != "0" && strings.HasPrefix(trimmed, "0") {
return 0, fmt.Errorf("%s must be a canonical base-10 string", fieldName)
}

parsed, err := strconv.ParseUint(trimmed, 10, 64)
if err != nil {
return 0, fmt.Errorf("%s must be a canonical base-10 string: %w", fieldName, err)
}

return parsed, nil
}

func canonicalUint64(value uint64) string {
return strconv.FormatUint(value, 10)
}
34 changes: 11 additions & 23 deletions pkg/hcs27/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,11 @@ func TestHCS27Integration_CheckpointChain(t *testing.T) {
Log: &LogProfile{
Algorithm: "sha-256",
Leaf: "sha256(jcs(event))",
Merkle: "rfc6962",
Merkle: "rfc9162",
},
Root: RootCommitment{
TreeSize: 1,
RootHashB64: rootOne,
},
BatchRange: BatchRange{
Start: 1,
End: 1,
TreeSize: canonicalUint64(1),
RootHashB64u: rootOne,
},
}

Expand All @@ -93,19 +89,15 @@ func TestHCS27Integration_CheckpointChain(t *testing.T) {
Log: &LogProfile{
Algorithm: "sha-256",
Leaf: "sha256(jcs(event))",
Merkle: "rfc6962",
Merkle: "rfc9162",
},
Root: RootCommitment{
TreeSize: 2,
RootHashB64: rootTwo,
TreeSize: canonicalUint64(2),
RootHashB64u: rootTwo,
},
Previous: &PreviousCommitment{
TreeSize: 1,
RootHashB64: rootOne,
},
BatchRange: BatchRange{
Start: 2,
End: 2,
TreeSize: canonicalUint64(1),
RootHashB64u: rootOne,
},
}

Expand Down Expand Up @@ -170,15 +162,11 @@ func TestHCS27Integration_MetadataOverflowUsesHRL(t *testing.T) {
Log: &LogProfile{
Algorithm: "sha-256",
Leaf: strings.Repeat("sha256(jcs(event))-", 90),
Merkle: "rfc6962",
Merkle: "rfc9162",
},
Root: RootCommitment{
TreeSize: 1,
RootHashB64: hashB64URL("go-sdk-hcs27-overflow-root-1"),
},
BatchRange: BatchRange{
Start: 1,
End: 1,
TreeSize: canonicalUint64(1),
RootHashB64u: hashB64URL("go-sdk-hcs27-overflow-root-1"),
},
}

Expand Down
48 changes: 48 additions & 0 deletions pkg/hcs27/merkle.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@
return sn == 0 && base64.StdEncoding.EncodeToString(current) == expectedRootB64, nil
}

// VerifyInclusionProofObject verifies a proof object that follows the HCS-27 draft shape.
func VerifyInclusionProofObject(proof InclusionProof) (bool, error) {

Check failure on line 136 in pkg/hcs27/merkle.go

View workflow job for this annotation

GitHub Actions / Lint Code

hugeParam: proof is heavy (112 bytes); consider passing it by pointer (gocritic)
if proof.TreeVersion != 1 {
return false, fmt.Errorf("treeVersion must be 1")
}

leafIndex, err := parseCanonicalUint64("leafIndex", proof.LeafIndex)
if err != nil {
return false, err
}
treeSize, err := parseCanonicalUint64("treeSize", proof.TreeSize)
if err != nil {
return false, err
}

return VerifyInclusionProof(
leafIndex,
treeSize,
proof.LeafHash,
proof.Path,
proof.RootHash,
)
}

// VerifyConsistencyProof performs the requested operation.
func VerifyConsistencyProof(
oldTreeSize uint64,
Expand Down Expand Up @@ -210,6 +234,30 @@
base64.StdEncoding.EncodeToString(sr) == newRootB64, nil
}

// VerifyConsistencyProofObject verifies a consistency proof object that follows the HCS-27 draft shape.
func VerifyConsistencyProofObject(proof ConsistencyProof) (bool, error) {

Check failure on line 238 in pkg/hcs27/merkle.go

View workflow job for this annotation

GitHub Actions / Lint Code

hugeParam: proof is heavy (96 bytes); consider passing it by pointer (gocritic)
if proof.TreeVersion != 1 {
return false, fmt.Errorf("treeVersion must be 1")
}

oldTreeSize, err := parseCanonicalUint64("oldTreeSize", proof.OldTreeSize)
if err != nil {
return false, err
}
newTreeSize, err := parseCanonicalUint64("newTreeSize", proof.NewTreeSize)
if err != nil {
return false, err
}

return VerifyConsistencyProof(
oldTreeSize,
newTreeSize,
proof.OldRootHash,
proof.NewRootHash,
proof.ConsistencyPath,
)
}

// CanonicalizeJSON performs the requested operation.
func CanonicalizeJSON(value any) ([]byte, error) {
normalized, err := normalizeJSONValue(value)
Expand Down
30 changes: 30 additions & 0 deletions pkg/hcs27/merkle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ func TestSingleEntryLeafVector(t *testing.T) {
if !ok {
t.Fatalf("expected inclusion proof to verify for single-entry tree")
}

ok, err = VerifyInclusionProofObject(InclusionProof{
LeafHash: leafHex,
LeafIndex: "0",
TreeSize: "1",
Path: []string{},
RootHash: base64.StdEncoding.EncodeToString(mustHex(leafHex)),
TreeVersion: 1,
})
if err != nil {
t.Fatalf("inclusion proof object verification returned error: %v", err)
}
if !ok {
t.Fatalf("expected inclusion proof object to verify for single-entry tree")
}
}

func TestConsistencyProof_EmptyToAny(t *testing.T) {
Expand All @@ -66,6 +81,21 @@ func TestConsistencyProof_EmptyToAny(t *testing.T) {
if !ok {
t.Fatalf("expected consistency proof to verify for oldTreeSize=0")
}

ok, err = VerifyConsistencyProofObject(ConsistencyProof{
OldTreeSize: "0",
NewTreeSize: "10",
OldRootHash: "",
NewRootHash: "ignored",
ConsistencyPath: nil,
TreeVersion: 1,
})
if err != nil {
t.Fatalf("consistency proof object verification returned error: %v", err)
}
if !ok {
t.Fatalf("expected consistency proof object to verify for oldTreeSize=0")
}
}

func mustHex(value string) []byte {
Expand Down
Loading
Loading