diff --git a/README.md b/README.md index 7037783..c796f57 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ go get github.com/hashgraph-online/standards-sdk-go@latest | `pkg/hcs21` | HCS-21 adapter registry/declaration publish flows, topic helpers, and signature/digest verification utilities. | | `pkg/hcs26` | HCS-26 memo helpers and resolver flows for discovery, version, and manifest reconstruction. | | `pkg/hcs27` | HCS-27 checkpoint topic creation, publish/retrieval, validation, Merkle/proof helpers. | -| `pkg/inscriber` | Kiloscribe auth flow, websocket-first high-level inscription utilities, quote generation, bulk-files support, registry-broker quote/job helpers, and skill inscription helpers. | +| `pkg/inscriber` | Inscriber auth flow, websocket-first high-level inscription utilities, quote generation, bulk-files support, registry-broker quote/job helpers, and skill inscription helpers. | | `pkg/registrybroker` | Full Registry Broker client (search, adapters, agents, credits, verification, ledger auth, chat/encryption, feedback, skills). | | `pkg/mirror` | Mirror node client used by HCS and inscriber packages. | | `pkg/shared` | Network normalization, operator env loading, Hedera client/key parsing helpers. | @@ -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: ""}, - BatchRange: hcs27.BatchRange{ - Start: 1, - End: 1, - }, + Root: hcs27.RootCommitment{TreeSize: "1", RootHashB64u: ""}, } ``` diff --git a/examples/README.md b/examples/README.md index e3ad058..efaebe5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -19,8 +19,8 @@ Runnable examples for common [HOL Standards SDK](https://hol.org/docs/libraries/ | `hcs20-deploy-points` | [HCS-20](https://hol.org/docs/standards/hcs-20) | Deploys a private points topic | | `hcs21-build-declaration` | [HCS-21](https://hol.org/docs/standards/hcs-21) | Builds an adapter declaration | | `hcs26-parse-memos` | [HCS-26](https://hol.org/docs/standards/hcs-26) | Parses topic/transaction memos | -| `hcs27-publish-checkpoint` | [HCS-27](https://hol.org/docs/standards/hcs-27) | Publishes a Merkle checkpoint | -| `inscriber-auth-client` | [HCS-1](https://hol.org/docs/standards/hcs-1) | Authenticates with Kiloscribe | +| `hcs27-publish-checkpoint` | [HCS-27](https://hol.org/docs/standards/hcs-27) | Publishes inline + HRL-backed checkpoints and validates the chain | +| `inscriber-auth-client` | [HCS-1](https://hol.org/docs/standards/hcs-1) | Authenticates with the inscriber service | | `registry-broker-skill-domain-proof` | [HCS-26](https://hol.org/docs/standards/hcs-26) | Creates/verifies DNS TXT proof and checks trust delta | | `registry-broker-uaid-dns-verification` | [HCS-14](https://hol.org/docs/standards/hcs-14) | Verifies UAID DNS TXT binding and reads stored/live verification status | diff --git a/examples/hcs27-publish-checkpoint/README.md b/examples/hcs27-publish-checkpoint/README.md index 9a9300d..5f270d5 100644 --- a/examples/hcs27-publish-checkpoint/README.md +++ b/examples/hcs27-publish-checkpoint/README.md @@ -4,8 +4,10 @@ This example publishes an [HCS-27](https://hol.org/docs/standards/hcs-27) checkp ## What it does -1. Creates an HCS-27 checkpoint with a Merkle root commitment. -2. Publishes the checkpoint to an HCS topic on testnet. +1. Creates an HCS-27 checkpoint topic when `HCS27_CHECKPOINT_TOPIC_ID` is not set. +2. Publishes an inline HCS-27 checkpoint to the topic. +3. Publishes an overflow checkpoint that stores metadata via an `hcs://1/...` reference. +4. Fetches the topic records, prints the HRL reference, and validates the checkpoint chain. ## Run diff --git a/examples/hcs27-publish-checkpoint/main.go b/examples/hcs27-publish-checkpoint/main.go index 7588ab1..e40db7b 100644 --- a/examples/hcs27-publish-checkpoint/main.go +++ b/examples/hcs27-publish-checkpoint/main.go @@ -4,8 +4,11 @@ import ( "context" "crypto/sha256" "encoding/base64" + "encoding/json" "fmt" "os" + "strings" + "time" "github.com/hashgraph-online/standards-sdk-go/pkg/hcs27" ) @@ -14,8 +17,8 @@ func main() { accountID := os.Getenv("HEDERA_ACCOUNT_ID") privateKey := os.Getenv("HEDERA_PRIVATE_KEY") checkpointTopicID := os.Getenv("HCS27_CHECKPOINT_TOPIC_ID") - if accountID == "" || privateKey == "" || checkpointTopicID == "" { - fmt.Println("HEDERA_ACCOUNT_ID, HEDERA_PRIVATE_KEY, and HCS27_CHECKPOINT_TOPIC_ID are required") + if accountID == "" || privateKey == "" { + fmt.Println("HEDERA_ACCOUNT_ID and HEDERA_PRIVATE_KEY are required") return } @@ -28,43 +31,136 @@ func main() { panic(err) } - root := hashB64URL("standards-sdk-go-example-root") - metadata := hcs27.CheckpointMetadata{ + ctx := context.Background() + if checkpointTopicID == "" { + topicID, transactionID, createErr := client.CreateCheckpointTopic(ctx, hcs27.CreateTopicOptions{ + TTLSeconds: 3600, + UseOperatorAsAdmin: true, + UseOperatorAsSubmit: true, + }) + if createErr != nil { + panic(createErr) + } + checkpointTopicID = topicID + fmt.Printf("created checkpoint topic=%s tx=%s\n", checkpointTopicID, transactionID) + } else { + fmt.Printf("using existing checkpoint topic=%s\n", checkpointTopicID) + } + + existingRecords, err := client.GetCheckpoints(ctx, checkpointTopicID, nil) + if err != nil { + existingRecords = nil + } + + inlineMetadata := hcs27.CheckpointMetadata{ Type: "ans-checkpoint-v1", Stream: hcs27.StreamID{ Registry: "ans", - LogID: "default", + LogID: "go-example-inline", }, Log: &hcs27.LogProfile{ Algorithm: "sha-256", Leaf: "sha256(jcs(event))", - Merkle: "rfc6962", + Merkle: "rfc9162", }, Root: hcs27.RootCommitment{ - TreeSize: 1, - RootHashB64: root, + TreeSize: "1", + RootHashB64u: hashB64URL("standards-sdk-go-example-inline-root"), + }, + } + + inlineResult, err := client.PublishCheckpoint( + ctx, + checkpointTopicID, + inlineMetadata, + "standards-sdk-go inline checkpoint", + "", + ) + if err != nil { + panic(err) + } + fmt.Printf( + "published inline checkpoint sequence=%d tx=%s\n", + inlineResult.SequenceNumber, + inlineResult.TransactionID, + ) + + overflowMetadata := hcs27.CheckpointMetadata{ + Type: "ans-checkpoint-v1", + Stream: hcs27.StreamID{ + Registry: "ans", + LogID: "go-example-overflow", }, - BatchRange: hcs27.BatchRange{ - Start: 1, - End: 1, + Log: &hcs27.LogProfile{ + Algorithm: "sha-256", + Leaf: strings.Repeat("sha256(jcs(event))-", 90), + Merkle: "rfc9162", + }, + Root: hcs27.RootCommitment{ + TreeSize: "2", + RootHashB64u: hashB64URL("standards-sdk-go-example-overflow-root"), }, } - result, err := client.PublishCheckpoint( - context.Background(), + overflowResult, err := client.PublishCheckpoint( + ctx, checkpointTopicID, - metadata, - "standards-sdk-go checkpoint", + overflowMetadata, + "standards-sdk-go overflow checkpoint", "", ) if err != nil { panic(err) } + fmt.Printf( + "published overflow checkpoint sequence=%d tx=%s\n", + overflowResult.SequenceNumber, + overflowResult.TransactionID, + ) + + records, err := waitForCheckpoints( + ctx, + client, + checkpointTopicID, + len(existingRecords)+2, + ) + if err != nil { + panic(err) + } - fmt.Printf("published checkpoint sequence=%d tx=%s\n", result.SequenceNumber, result.TransactionID) + var overflowReference string + for _, record := range records { + var reference string + if json.Unmarshal(record.Message.Metadata, &reference) == nil && strings.HasPrefix(reference, "hcs://1/") { + overflowReference = reference + break + } + } + if overflowReference == "" { + panic("failed to find HCS-1 metadata reference in fetched checkpoints") + } + + fmt.Printf("resolved overflow metadata reference=%s\n", overflowReference) + fmt.Printf("validated checkpoint chain=%t\n", hcs27.ValidateCheckpointChain(records) == nil) } func hashB64URL(input string) string { sum := sha256.Sum256([]byte(input)) return base64.RawURLEncoding.EncodeToString(sum[:]) } + +func waitForCheckpoints( + ctx context.Context, + client *hcs27.Client, + topicID string, + minRecords int, +) ([]hcs27.CheckpointRecord, error) { + for attempt := 0; attempt < 20; attempt++ { + records, err := client.GetCheckpoints(ctx, topicID, nil) + if err == nil && len(records) >= minRecords { + return records, nil + } + time.Sleep(3 * time.Second) + } + return nil, fmt.Errorf("timed out waiting for %d checkpoint records", minRecords) +} diff --git a/examples/inscriber-auth-client/README.md b/examples/inscriber-auth-client/README.md index 4a327e7..abde5a0 100644 --- a/examples/inscriber-auth-client/README.md +++ b/examples/inscriber-auth-client/README.md @@ -1,10 +1,10 @@ # Inscriber: Authenticate and Initialize -This example authenticates against [Kiloscribe](https://hol.org/docs/standards/hcs-1) and initializes an inscriber client. +This example authenticates against the inscription service and initializes an inscriber client. ## What it does -1. Authenticates a Hedera account with the Kiloscribe inscription service. +1. Authenticates a Hedera account with the inscription service. 2. Initializes an inscriber client for writing data to HCS topics. ## Run diff --git a/pkg/hcs11/client.go b/pkg/hcs11/client.go index f7c8ba7..5168186 100644 --- a/pkg/hcs11/client.go +++ b/pkg/hcs11/client.go @@ -19,7 +19,7 @@ type Client struct { operatorPrivateKey string network string keyType string - kiloScribeBaseURL string + inscriberBaseURL string inscriberAuthURL string inscriberAPIURL string } @@ -98,9 +98,12 @@ func NewClient(config ClientConfig) (*Client, error) { keyType = "ed25519" } - kiloScribeBaseURL := strings.TrimSpace(config.KiloScribeBaseURL) - if kiloScribeBaseURL == "" { - kiloScribeBaseURL = "https://kiloscribe.com" + inscriberBaseURL := strings.TrimSpace(config.InscriberBaseURL) + if inscriberBaseURL == "" { + inscriberBaseURL = strings.TrimSpace(config.KiloScribeBaseURL) + } + if inscriberBaseURL == "" { + inscriberBaseURL = "https://kiloscribe.com" } return &Client{ @@ -110,7 +113,7 @@ func NewClient(config ClientConfig) (*Client, error) { operatorPrivateKey: operatorPrivateKey, network: network, keyType: keyType, - kiloScribeBaseURL: strings.TrimRight(kiloScribeBaseURL, "/"), + inscriberBaseURL: strings.TrimRight(inscriberBaseURL, "/"), inscriberAuthURL: strings.TrimSpace(config.InscriberAuthURL), inscriberAPIURL: strings.TrimSpace(config.InscriberAPIURL), }, nil diff --git a/pkg/hcs11/client_fetch.go b/pkg/hcs11/client_fetch.go index e689998..7a16149 100644 --- a/pkg/hcs11/client_fetch.go +++ b/pkg/hcs11/client_fetch.go @@ -119,7 +119,7 @@ func (c *Client) fetchFromHCSReference( profileTopicID := strings.TrimSpace(parts[3]) cdnURL := fmt.Sprintf( "%s/api/inscription-cdn/%s?network=%s", - c.kiloScribeBaseURL, + c.inscriberBaseURL, profileTopicID, strings.TrimSpace(network), ) diff --git a/pkg/hcs11/client_test.go b/pkg/hcs11/client_test.go index 1684f02..2732a95 100644 --- a/pkg/hcs11/client_test.go +++ b/pkg/hcs11/client_test.go @@ -117,9 +117,9 @@ func TestClientFetchProfileByAccountID(t *testing.T) { defer server.Close() client, err := NewClient(ClientConfig{ - Network: "testnet", - MirrorBaseURL: server.URL, - KiloScribeBaseURL: server.URL, + Network: "testnet", + MirrorBaseURL: server.URL, + InscriberBaseURL: server.URL, }) if err != nil { t.Fatalf("failed to initialize client: %v", err) diff --git a/pkg/hcs11/types.go b/pkg/hcs11/types.go index 11eda15..1aa648c 100644 --- a/pkg/hcs11/types.go +++ b/pkg/hcs11/types.go @@ -181,6 +181,7 @@ type ClientConfig struct { Auth Auth KeyType string MirrorBaseURL string + InscriberBaseURL string KiloScribeBaseURL string InscriberAuthURL string InscriberAPIURL string diff --git a/pkg/hcs2/client.go b/pkg/hcs2/client.go index 53b222d..f23918c 100644 --- a/pkg/hcs2/client.go +++ b/pkg/hcs2/client.go @@ -468,7 +468,7 @@ func (c *Client) submitMessage( }, nil } -// inscribeOverflow inscribes the payload via the Kiloscribe inscriber API and +// inscribeOverflow inscribes the payload via the inscriber API and // returns an HRL reference (e.g. "hcs://1/0.0.12345"). func (c *Client) inscribeOverflow(ctx context.Context, payload []byte) (string, error) { network := inscriber.NetworkTestnet diff --git a/pkg/hcs27/client.go b/pkg/hcs27/client.go index 368dcd4..30160d8 100644 --- a/pkg/hcs27/client.go +++ b/pkg/hcs27/client.go @@ -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{} @@ -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 { @@ -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) } } diff --git a/pkg/hcs27/client_overflow_test.go b/pkg/hcs27/client_overflow_test.go index d4042e3..addc902 100644 --- a/pkg/hcs27/client_overflow_test.go +++ b/pkg/hcs27/client_overflow_test.go @@ -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)), }, } } diff --git a/pkg/hcs27/coverage_test.go b/pkg/hcs27/coverage_test.go index 1e1fbeb..56aaf5d 100644 --- a/pkg/hcs27/coverage_test.go +++ b/pkg/hcs27/coverage_test.go @@ -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") @@ -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": []}`)) @@ -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") + } } diff --git a/pkg/hcs27/doc.go b/pkg/hcs27/doc.go index 14b98b7..d66ccc1 100644 --- a/pkg/hcs27/doc.go +++ b/pkg/hcs27/doc.go @@ -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: hcs27.RootCommitment{TreeSize: "100", RootHashB64u: ""}, // } // // This package is part of the HOL Standards SDK for Go. diff --git a/pkg/hcs27/integers.go b/pkg/hcs27/integers.go new file mode 100644 index 0000000..8c257ef --- /dev/null +++ b/pkg/hcs27/integers.go @@ -0,0 +1,30 @@ +package hcs27 + +import ( + "fmt" + "strconv" + "strings" +) + +func parseCanonicalUint64(fieldName, value string) (uint64, error) { + if value == "" { + return 0, fmt.Errorf("%s is required", fieldName) + } + if value != strings.TrimSpace(value) { + return 0, fmt.Errorf("%s must be a canonical base-10 string", fieldName) + } + if value != "0" && strings.HasPrefix(value, "0") { + return 0, fmt.Errorf("%s must be a canonical base-10 string", fieldName) + } + + parsed, err := strconv.ParseUint(value, 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) +} diff --git a/pkg/hcs27/integration_test.go b/pkg/hcs27/integration_test.go index afbc591..d87e3c5 100644 --- a/pkg/hcs27/integration_test.go +++ b/pkg/hcs27/integration_test.go @@ -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, }, } @@ -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, }, } @@ -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"), }, } diff --git a/pkg/hcs27/merkle.go b/pkg/hcs27/merkle.go index 5bb3661..31dd534 100644 --- a/pkg/hcs27/merkle.go +++ b/pkg/hcs27/merkle.go @@ -132,6 +132,37 @@ func VerifyInclusionProof( 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) { + if proof == nil { + return false, fmt.Errorf("proof is required") + } + 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 + } + rootHash, err := normalizeProofRootHash("rootHash", proof.RootHash) + if err != nil { + return false, err + } + + return VerifyInclusionProof( + leafIndex, + treeSize, + proof.LeafHash, + proof.Path, + rootHash, + ) +} + // VerifyConsistencyProof performs the requested operation. func VerifyConsistencyProof( oldTreeSize uint64, @@ -210,6 +241,65 @@ func VerifyConsistencyProof( 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) { + if proof == nil { + return false, fmt.Errorf("proof is required") + } + 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 + } + if oldTreeSize == 0 { + return VerifyConsistencyProof( + oldTreeSize, + newTreeSize, + proof.OldRootHash, + proof.NewRootHash, + proof.ConsistencyPath, + ) + } + oldRootHash, err := normalizeProofRootHash("oldRootHash", proof.OldRootHash) + if err != nil { + return false, err + } + newRootHash, err := normalizeProofRootHash("newRootHash", proof.NewRootHash) + if err != nil { + return false, err + } + + return VerifyConsistencyProof( + oldTreeSize, + newTreeSize, + oldRootHash, + newRootHash, + proof.ConsistencyPath, + ) +} + +func normalizeProofRootHash(fieldName, value string) (string, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return "", fmt.Errorf("%s is required", fieldName) + } + if decoded, err := base64.StdEncoding.DecodeString(trimmed); err == nil { + return base64.StdEncoding.EncodeToString(decoded), nil + } + decoded, err := base64.RawURLEncoding.DecodeString(trimmed) + if err != nil { + return "", fmt.Errorf("%s must be valid base64 or base64url: %w", fieldName, err) + } + return base64.StdEncoding.EncodeToString(decoded), nil +} + // CanonicalizeJSON performs the requested operation. func CanonicalizeJSON(value any) ([]byte, error) { normalized, err := normalizeJSONValue(value) diff --git a/pkg/hcs27/merkle_test.go b/pkg/hcs27/merkle_test.go index e09ebaf..4597fe8 100644 --- a/pkg/hcs27/merkle_test.go +++ b/pkg/hcs27/merkle_test.go @@ -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) { @@ -66,6 +81,56 @@ 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 TestProofObjectRootsAcceptBase64URL(t *testing.T) { + leafHex := "a12882925d08570166fe748ebdc16670fc0c69428e2b60ed388b35b52c91d6e2" + rootB64URL := base64.RawURLEncoding.EncodeToString(mustHex(leafHex)) + + ok, err := VerifyInclusionProofObject(&InclusionProof{ + LeafHash: leafHex, + LeafIndex: "0", + TreeSize: "1", + Path: []string{}, + RootHash: rootB64URL, + 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 with a base64url root hash") + } + + ok, err = VerifyConsistencyProofObject(&ConsistencyProof{ + OldTreeSize: "1", + NewTreeSize: "1", + OldRootHash: rootB64URL, + NewRootHash: rootB64URL, + ConsistencyPath: []string{}, + 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 with base64url root hashes") + } } func mustHex(value string) []byte { diff --git a/pkg/hcs27/types.go b/pkg/hcs27/types.go index be7f202..afb42e1 100644 --- a/pkg/hcs27/types.go +++ b/pkg/hcs27/types.go @@ -8,8 +8,10 @@ import ( ) const ( - ProtocolID = "hcs-27" - OperationName = "register" + ProtocolID = "hcs-27" + OperationName = "register" + checkpointMetadataType = "ans-checkpoint-v1" + merkleProfileRFC9162 = "rfc9162" ) type StreamID struct { @@ -24,18 +26,13 @@ type LogProfile struct { } type RootCommitment struct { - TreeSize uint64 `json:"treeSize"` - RootHashB64 string `json:"rootHashB64u"` + TreeSize string `json:"treeSize"` + RootHashB64u string `json:"rootHashB64u"` } type PreviousCommitment struct { - TreeSize uint64 `json:"treeSize"` - RootHashB64 string `json:"rootHashB64u"` -} - -type BatchRange struct { - Start uint64 `json:"start"` - End uint64 `json:"end"` + TreeSize string `json:"treeSize"` + RootHashB64u string `json:"rootHashB64u"` } type Signature struct { @@ -45,13 +42,12 @@ type Signature struct { } type CheckpointMetadata struct { - Type string `json:"type"` - Stream StreamID `json:"stream"` - Log *LogProfile `json:"log,omitempty"` - Root RootCommitment `json:"root"` - Previous *PreviousCommitment `json:"prev,omitempty"` - BatchRange BatchRange `json:"batch_range"` - Signature *Signature `json:"sig,omitempty"` + Type string `json:"type"` + Stream StreamID `json:"stream"` + Log *LogProfile `json:"log,omitempty"` + Root RootCommitment `json:"root"` + Previous *PreviousCommitment `json:"prev,omitempty"` + Signature *Signature `json:"sig,omitempty"` } type MetadataDigest struct { @@ -59,6 +55,26 @@ type MetadataDigest struct { DigestB64 string `json:"b64u"` } +type InclusionProof struct { + LeafHash string `json:"leafHash"` + LeafIndex string `json:"leafIndex"` + TreeSize string `json:"treeSize"` + Path []string `json:"path"` + RootHash string `json:"rootHash"` + // RootSignature is carried through for draft parity but is not verified here. + RootSignature string `json:"rootSignature,omitempty"` + TreeVersion int `json:"treeVersion"` +} + +type ConsistencyProof struct { + OldTreeSize string `json:"oldTreeSize"` + NewTreeSize string `json:"newTreeSize"` + OldRootHash string `json:"oldRootHash"` + NewRootHash string `json:"newRootHash"` + ConsistencyPath []string `json:"consistencyPath"` + TreeVersion int `json:"treeVersion"` +} + type CheckpointMessage struct { Protocol string `json:"p"` Operation string `json:"op"` diff --git a/pkg/hcs27/validation.go b/pkg/hcs27/validation.go index 6806181..1ebd387 100644 --- a/pkg/hcs27/validation.go +++ b/pkg/hcs27/validation.go @@ -88,8 +88,8 @@ func ValidateCheckpointMessage( } func validateMetadata(metadata CheckpointMetadata) error { - if strings.TrimSpace(metadata.Type) != "ans-checkpoint-v1" { - return fmt.Errorf("metadata.type must be ans-checkpoint-v1") + if strings.TrimSpace(metadata.Type) != checkpointMetadataType { + return fmt.Errorf("metadata.type must be %s", checkpointMetadataType) } if strings.TrimSpace(metadata.Stream.Registry) == "" { return fmt.Errorf("metadata.stream.registry is required") @@ -106,31 +106,29 @@ func validateMetadata(metadata CheckpointMetadata) error { if strings.TrimSpace(metadata.Log.Leaf) == "" { return fmt.Errorf("metadata.log.leaf is required") } - if strings.TrimSpace(metadata.Log.Merkle) == "" { - return fmt.Errorf("metadata.log.merkle is required") + if strings.TrimSpace(metadata.Log.Merkle) != merkleProfileRFC9162 { + return fmt.Errorf("metadata.log.merkle must be %s", merkleProfileRFC9162) } - if metadata.Root.TreeSize == 0 && metadata.BatchRange.End > 0 { - return fmt.Errorf("metadata.root.treeSize must be >= batch_range.end") + rootTreeSize, err := parseCanonicalUint64("metadata.root.treeSize", metadata.Root.TreeSize) + if err != nil { + return err } - if _, err := base64.RawURLEncoding.DecodeString(metadata.Root.RootHashB64); err != nil { + if _, err := base64.RawURLEncoding.DecodeString(metadata.Root.RootHashB64u); err != nil { return fmt.Errorf("metadata.root.rootHashB64u must be base64url: %w", err) } if metadata.Previous != nil { - if _, err := base64.RawURLEncoding.DecodeString(metadata.Previous.RootHashB64); err != nil { + previousTreeSize, err := parseCanonicalUint64("metadata.prev.treeSize", metadata.Previous.TreeSize) + if err != nil { + return err + } + if _, err := base64.RawURLEncoding.DecodeString(metadata.Previous.RootHashB64u); err != nil { return fmt.Errorf("metadata.prev.rootHashB64u must be base64url: %w", err) } - if metadata.Previous.TreeSize > metadata.Root.TreeSize { + if previousTreeSize > rootTreeSize { return fmt.Errorf("metadata.prev.treeSize must be <= metadata.root.treeSize") } } - if metadata.BatchRange.End < metadata.BatchRange.Start { - return fmt.Errorf("metadata.batch_range.end must be >= start") - } - if metadata.BatchRange.End > metadata.Root.TreeSize { - return fmt.Errorf("metadata.batch_range.end must be <= metadata.root.treeSize") - } - if metadata.Signature != nil { if strings.TrimSpace(metadata.Signature.Algorithm) == "" { return fmt.Errorf("metadata.sig.alg is required when metadata.sig is present") @@ -145,6 +143,5 @@ func validateMetadata(metadata CheckpointMetadata) error { return fmt.Errorf("metadata.sig.b64u must be base64url: %w", err) } } - return nil } diff --git a/pkg/hcs27/validation_test.go b/pkg/hcs27/validation_test.go index f70f8c2..42ddb95 100644 --- a/pkg/hcs27/validation_test.go +++ b/pkg/hcs27/validation_test.go @@ -9,6 +9,8 @@ import ( "testing" ) +const testHCS1Reference = "hcs://1/0.0.99999" + func TestBuildTopicMemo(t *testing.T) { memo := BuildTopicMemo(86400) if memo != "hcs-27:0:86400:0" { @@ -123,23 +125,19 @@ func buildValidMetadata() CheckpointMetadata { rootHash := sha256.Sum256([]byte("test")) rootHashB64 := base64.RawURLEncoding.EncodeToString(rootHash[:]) return CheckpointMetadata{ - Type: "ans-checkpoint-v1", + Type: checkpointMetadataType, Stream: StreamID{ Registry: "0.0.12345", LogID: "log-1", }, Log: &LogProfile{ Algorithm: "sha-256", - Leaf: "rfc6962", - Merkle: "rfc6962", + Leaf: "sha256(jcs(event))", + Merkle: merkleProfileRFC9162, }, Root: RootCommitment{ - TreeSize: 10, - RootHashB64: rootHashB64, - }, - BatchRange: BatchRange{ - Start: 0, - End: 10, + TreeSize: canonicalUint64(10), + RootHashB64u: rootHashB64, }, } } @@ -227,7 +225,7 @@ func TestValidateMetadataLogMerkle(t *testing.T) { func TestValidateMetadataRootHash(t *testing.T) { metadata := buildValidMetadata() - metadata.Root.RootHashB64 = "!invalid!" + metadata.Root.RootHashB64u = "!invalid!" err := validateMetadata(metadata) if err == nil { t.Fatal("expected error for invalid root hash") @@ -238,8 +236,8 @@ func TestValidateMetadataPrevious(t *testing.T) { metadata := buildValidMetadata() prevHash := sha256.Sum256([]byte("prev")) metadata.Previous = &PreviousCommitment{ - TreeSize: 5, - RootHashB64: base64.RawURLEncoding.EncodeToString(prevHash[:]), + TreeSize: canonicalUint64(5), + RootHashB64u: base64.RawURLEncoding.EncodeToString(prevHash[:]), } err := validateMetadata(metadata) if err != nil { @@ -250,8 +248,8 @@ func TestValidateMetadataPrevious(t *testing.T) { func TestValidateMetadataPreviousInvalidHash(t *testing.T) { metadata := buildValidMetadata() metadata.Previous = &PreviousCommitment{ - TreeSize: 5, - RootHashB64: "!invalid!", + TreeSize: canonicalUint64(5), + RootHashB64u: "!invalid!", } err := validateMetadata(metadata) if err == nil { @@ -263,8 +261,8 @@ func TestValidateMetadataPreviousTreeSizeTooLarge(t *testing.T) { metadata := buildValidMetadata() prevHash := sha256.Sum256([]byte("prev")) metadata.Previous = &PreviousCommitment{ - TreeSize: 100, - RootHashB64: base64.RawURLEncoding.EncodeToString(prevHash[:]), + TreeSize: canonicalUint64(100), + RootHashB64u: base64.RawURLEncoding.EncodeToString(prevHash[:]), } err := validateMetadata(metadata) if err == nil { @@ -272,25 +270,6 @@ func TestValidateMetadataPreviousTreeSizeTooLarge(t *testing.T) { } } -func TestValidateMetadataBatchRangeInvalid(t *testing.T) { - metadata := buildValidMetadata() - metadata.BatchRange.Start = 5 - metadata.BatchRange.End = 3 - err := validateMetadata(metadata) - if err == nil { - t.Fatal("expected error for batch_range.end < start") - } -} - -func TestValidateMetadataBatchRangeEndExceedsTreeSize(t *testing.T) { - metadata := buildValidMetadata() - metadata.BatchRange.End = 100 - err := validateMetadata(metadata) - if err == nil { - t.Fatal("expected error for batch_range.end > treeSize") - } -} - func TestValidateMetadataSignature(t *testing.T) { metadata := buildValidMetadata() sigBytes := sha256.Sum256([]byte("sig")) @@ -357,14 +336,30 @@ func TestValidateMetadataSignatureInvalidB64(t *testing.T) { } } -func TestValidateMetadataTreeSizeZeroWithBatch(t *testing.T) { +func TestValidateMetadataTreeSizeCanonicalDecimal(t *testing.T) { + metadata := buildValidMetadata() + metadata.Root.TreeSize = "001" + err := validateMetadata(metadata) + if err == nil { + t.Fatal("expected error for non-canonical treeSize") + } +} + +func TestValidateMetadataTreeSizeZero(t *testing.T) { + metadata := buildValidMetadata() + metadata.Root.TreeSize = "0" + err := validateMetadata(metadata) + if err != nil { + t.Fatalf("expected zero treeSize to be valid: %v", err) + } +} + +func TestValidateMetadataTreeSizeRejectsWhitespace(t *testing.T) { metadata := buildValidMetadata() - metadata.Root.TreeSize = 0 - metadata.BatchRange.Start = 0 - metadata.BatchRange.End = 5 + metadata.Root.TreeSize = " 1 " err := validateMetadata(metadata) if err == nil { - t.Fatal("expected error for treeSize=0 with batch_range.end>0") + t.Fatal("expected error for treeSize with surrounding whitespace") } } @@ -372,7 +367,7 @@ func TestValidateCheckpointMessageWithReference(t *testing.T) { metadata := buildValidMetadata() metadataBytes, _ := json.Marshal(metadata) - reference := "hcs://1/0.0.99999" + reference := testHCS1Reference referenceBytes, _ := json.Marshal(reference) resolver := func(ctx context.Context, ref string) ([]byte, error) { @@ -409,7 +404,7 @@ func TestValidateCheckpointMessageReferenceNonHCS1(t *testing.T) { } func TestValidateCheckpointMessageReferenceNoResolver(t *testing.T) { - reference := "hcs://1/0.0.99999" + reference := testHCS1Reference referenceBytes, _ := json.Marshal(reference) msg := CheckpointMessage{ @@ -427,7 +422,7 @@ func TestValidateCheckpointMessageWithDigest(t *testing.T) { metadata := buildValidMetadata() metadataBytes, _ := json.Marshal(metadata) - reference := "hcs://1/0.0.99999" + reference := testHCS1Reference referenceBytes, _ := json.Marshal(reference) sum := sha256.Sum256(metadataBytes) @@ -456,7 +451,7 @@ func TestValidateCheckpointMessageDigestMismatch(t *testing.T) { metadata := buildValidMetadata() metadataBytes, _ := json.Marshal(metadata) - reference := "hcs://1/0.0.99999" + reference := testHCS1Reference referenceBytes, _ := json.Marshal(reference) resolver := func(ctx context.Context, ref string) ([]byte, error) { @@ -482,7 +477,7 @@ func TestValidateCheckpointMessageDigestBadAlg(t *testing.T) { metadata := buildValidMetadata() metadataBytes, _ := json.Marshal(metadata) - reference := "hcs://1/0.0.99999" + reference := testHCS1Reference referenceBytes, _ := json.Marshal(reference) resolver := func(ctx context.Context, ref string) ([]byte, error) { @@ -531,14 +526,14 @@ func TestValidateCheckpointChain(t *testing.T) { { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, - Previous: &PreviousCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, + Previous: &PreviousCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, } @@ -557,14 +552,14 @@ func TestValidateCheckpointChainTreeSizeDecreased(t *testing.T) { { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, - Previous: &PreviousCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, + Previous: &PreviousCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, } @@ -583,13 +578,13 @@ func TestValidateCheckpointChainMissingPrev(t *testing.T) { { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, }, }, } @@ -608,14 +603,14 @@ func TestValidateCheckpointChainPrevTreeSizeMismatch(t *testing.T) { { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, - Previous: &PreviousCommitment{TreeSize: 3, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, + Previous: &PreviousCommitment{TreeSize: canonicalUint64(3), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, } @@ -635,14 +630,14 @@ func TestValidateCheckpointChainPrevRootHashMismatch(t *testing.T) { { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash1[:])}, }, }, { EffectiveMetadata: CheckpointMetadata{ Stream: StreamID{Registry: "0.0.1", LogID: "log-1"}, - Root: RootCommitment{TreeSize: 10, RootHashB64: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, - Previous: &PreviousCommitment{TreeSize: 5, RootHashB64: base64.RawURLEncoding.EncodeToString(wrongHash[:])}, + Root: RootCommitment{TreeSize: canonicalUint64(10), RootHashB64u: base64.RawURLEncoding.EncodeToString(rootHash2[:])}, + Previous: &PreviousCommitment{TreeSize: canonicalUint64(5), RootHashB64u: base64.RawURLEncoding.EncodeToString(wrongHash[:])}, }, }, } @@ -662,8 +657,8 @@ func TestExtractChunkTransactionIDString(t *testing.T) { func TestExtractChunkTransactionIDMap(t *testing.T) { result := extractChunkTransactionID(map[string]any{ - "account_id": "0.0.1", - "transaction_valid_start": "123.456", + "account_id": "0.0.1", + "transaction_valid_start": "123.456", }) if result != "0.0.1@123.456" { t.Fatalf("expected '0.0.1@123.456', got %q", result) @@ -672,8 +667,8 @@ func TestExtractChunkTransactionIDMap(t *testing.T) { func TestExtractChunkTransactionIDMapFallback(t *testing.T) { result := extractChunkTransactionID(map[string]any{ - "account_id": "0.0.1", - "valid_start_timestamp": "789.012", + "account_id": "0.0.1", + "valid_start_timestamp": "789.012", }) if result != "0.0.1@789.012" { t.Fatalf("expected '0.0.1@789.012', got %q", result) @@ -682,8 +677,8 @@ func TestExtractChunkTransactionIDMapFallback(t *testing.T) { func TestExtractChunkTransactionIDMapString(t *testing.T) { result := extractChunkTransactionID(map[string]string{ - "account_id": "0.0.2", - "transaction_valid_start": "111.222", + "account_id": "0.0.2", + "transaction_valid_start": "111.222", }) if result != "0.0.2@111.222" { t.Fatalf("expected '0.0.2@111.222', got %q", result) diff --git a/pkg/inscriber/doc.go b/pkg/inscriber/doc.go index faa048a..a10277f 100644 --- a/pkg/inscriber/doc.go +++ b/pkg/inscriber/doc.go @@ -1,10 +1,10 @@ -// Package inscriber provides the Kiloscribe authentication flow and high-level +// Package inscriber provides the inscription-service authentication flow and high-level // inscription utilities for the HOL ecosystem. It supports // websocket-first inscription, quote generation, bulk-files support, // Registry Broker quote and job helpers, and skill inscription helpers. // // The inscriber package is the primary interface for writing data to the Hedera -// public ledger via the Kiloscribe inscription service. It handles +// public ledger via the inscription service. It handles // authentication, file chunking, websocket transport, and cost estimation. // // # Authentication