Tools: Building a Decentralized Registry in Go with HCS-2 on Hedera

Tools: Building a Decentralized Registry in Go with HCS-2 on Hedera

Source: Dev.to

Why Go? ## What is an HCS-2 Topic Registry? ## Prerequisites ## Step 1: Initialize the HCS-2 Client ## Step 2: Creating an Indexed Registry ## Step 3: Writing Data to the Registry ## Step 4: Reading the Decentralized State ## Under the Hood ## Next Steps With the explosive growth of AI agents, distributed systems, and decentralized applications (dApps), there's an increasing need for immutable, decentralized data sharing. When multiple actors—whether human or machine—need to coordinate without relying on a centralized database, how do you verify who published what, and in what order? Enter the Hedera Consensus Service (HCS). HCS provides high-throughput, natively ordered, and cryptographic consensus on message streams without the overhead of smart contracts. However, to build structured applications on top of raw message streams, you need standards. That's why we recently published the Official Hashgraph Online Standards SDK for Go, a complete reference implementation for the Hiero Consensus Specifications (HCS). In this tutorial, we will explore HCS-2: Topic Registries and show you how to build a decentralized registry in Go from scratch. Go (Golang) is uniquely positioned for decentralized systems. Its native concurrency model (goroutines), fast compilation, and strongly typed ecosystem make it the language of choice for building robust infrastructure like the Hedera Mirror Node, Hyperledger Fabric, and countless Web3 indexers. By releasing a dedicated Go SDK for HCS, we are providing Go developers with a first-class, typed, and idiomatic path to interacting with decentralized standards on the Hedera public ledger. The HCS-2 Specification defines a standard for creating append-only, verifiable data registries on Hedera. At its core, a registry is a Hedera topic where messages conform to a specific JSON schema. HCS-2 supports two types of registries: This is perfect for use cases like: Let's write some code to see this in action. Before we begin, you'll need: Create a new directory for your project and initialize a Go module: Next, install the Standards SDK for Go: Set your environment variables (or place them in a .env file): First, let's create our main Go file and initialize the hcs2 client. The SDK provides a clean configuration pattern for setting up the client. Now, let's create a new decentralized registry. We will create an Indexed registry, which functions like a global, immutable key-value store. When you create a registry using the SDK, it communicates directly with the Hedera network to allocate a new cryptographic Consensus Topic and formats the topic memo according to the HCS-2 specification. Append this to your main.go: With our topic ID in hand, we can now publish structured entries to the registry. In an Indexed registry, updating the state of a specific item is as simple as publishing a new message with the same Index (key). The SDK automatically constructs the correct JSON payload and signs it. Writing is only half the battle. How do we retrieve the state of our registry? Because HCS topics are public, anyone (not just you) can read the state of this registry using a Hedera Mirror Node. The SDK provides built-in methods to query the mirror node, parse the messages, and reconstruct the final "state" of the key-value store. If you ever published a new message for device-001 saying "status": "maintenance", the GetIndexedRegistryState function would automatically resolve the history and return the most recent valid state for that key. When you use the standards-sdk-go, you are interacting with the Hedera network through strongly typed, validated interfaces. But what's actually happening? We've barely scratched the surface of what the Standards SDK can do. From this foundation, you can dive into more advanced protocols: Ready to start building? 👉 Dive into the SDK Documentation or star the standards-sdk-go repository on GitHub to stay updated on the latest Go ecosystem releases! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: mkdir go-registry-demo cd go-registry-demo go mod init go-registry-demo Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: mkdir go-registry-demo cd go-registry-demo go mod init go-registry-demo CODE_BLOCK: mkdir go-registry-demo cd go-registry-demo go mod init go-registry-demo CODE_BLOCK: go get github.com/hashgraph-online/standards-sdk-go@latest Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: go get github.com/hashgraph-online/standards-sdk-go@latest CODE_BLOCK: go get github.com/hashgraph-online/standards-sdk-go@latest CODE_BLOCK: export HEDERA_ACCOUNT_ID="0.0.xxxxx" export HEDERA_PRIVATE_KEY="302..." export HEDERA_NETWORK="testnet" Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: export HEDERA_ACCOUNT_ID="0.0.xxxxx" export HEDERA_PRIVATE_KEY="302..." export HEDERA_NETWORK="testnet" CODE_BLOCK: export HEDERA_ACCOUNT_ID="0.0.xxxxx" export HEDERA_PRIVATE_KEY="302..." export HEDERA_NETWORK="testnet" CODE_BLOCK: package main import ( "context" "fmt" "log" "os" "github.com/hashgraph-online/standards-sdk-go/pkg/hcs2" ) func main() { ctx := context.Background() // Initialize the HCS-2 Client // Notice we can pull configuration directly from the environment client, err := hcs2.NewClient(hcs2.ClientConfig{ OperatorAccountID: os.Getenv("HEDERA_ACCOUNT_ID"), OperatorPrivateKey: os.Getenv("HEDERA_PRIVATE_KEY"), Network: "testnet", // Explicitly use testnet }) if err != nil { log.Fatalf("Failed to initialize HCS-2 client: %v", err) } fmt.Println("Successfully initialized HCS-2 Client!") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: package main import ( "context" "fmt" "log" "os" "github.com/hashgraph-online/standards-sdk-go/pkg/hcs2" ) func main() { ctx := context.Background() // Initialize the HCS-2 Client // Notice we can pull configuration directly from the environment client, err := hcs2.NewClient(hcs2.ClientConfig{ OperatorAccountID: os.Getenv("HEDERA_ACCOUNT_ID"), OperatorPrivateKey: os.Getenv("HEDERA_PRIVATE_KEY"), Network: "testnet", // Explicitly use testnet }) if err != nil { log.Fatalf("Failed to initialize HCS-2 client: %v", err) } fmt.Println("Successfully initialized HCS-2 Client!") } CODE_BLOCK: package main import ( "context" "fmt" "log" "os" "github.com/hashgraph-online/standards-sdk-go/pkg/hcs2" ) func main() { ctx := context.Background() // Initialize the HCS-2 Client // Notice we can pull configuration directly from the environment client, err := hcs2.NewClient(hcs2.ClientConfig{ OperatorAccountID: os.Getenv("HEDERA_ACCOUNT_ID"), OperatorPrivateKey: os.Getenv("HEDERA_PRIVATE_KEY"), Network: "testnet", // Explicitly use testnet }) if err != nil { log.Fatalf("Failed to initialize HCS-2 client: %v", err) } fmt.Println("Successfully initialized HCS-2 Client!") } CODE_BLOCK: // ... previous code ... fmt.Println("Creating a new HCS-2 Indexed Registry...") // Create the registry result, err := client.CreateRegistry(ctx, hcs2.CreateRegistryOptions{ RegistryType: hcs2.RegistryTypeIndexed, UseOperatorAsAdmin: true, // We retain administrative control UseOperatorAsSubmit: true, // We must sign future entries }) if err != nil { log.Fatalf("Failed to create registry: %v", err) } topicID := result.TopicID.String() fmt.Printf("✅ Registry created successfully!\nTopic ID: %s\n", topicID) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // ... previous code ... fmt.Println("Creating a new HCS-2 Indexed Registry...") // Create the registry result, err := client.CreateRegistry(ctx, hcs2.CreateRegistryOptions{ RegistryType: hcs2.RegistryTypeIndexed, UseOperatorAsAdmin: true, // We retain administrative control UseOperatorAsSubmit: true, // We must sign future entries }) if err != nil { log.Fatalf("Failed to create registry: %v", err) } topicID := result.TopicID.String() fmt.Printf("✅ Registry created successfully!\nTopic ID: %s\n", topicID) CODE_BLOCK: // ... previous code ... fmt.Println("Creating a new HCS-2 Indexed Registry...") // Create the registry result, err := client.CreateRegistry(ctx, hcs2.CreateRegistryOptions{ RegistryType: hcs2.RegistryTypeIndexed, UseOperatorAsAdmin: true, // We retain administrative control UseOperatorAsSubmit: true, // We must sign future entries }) if err != nil { log.Fatalf("Failed to create registry: %v", err) } topicID := result.TopicID.String() fmt.Printf("✅ Registry created successfully!\nTopic ID: %s\n", topicID) CODE_BLOCK: // ... previous code ... // Let's define the data we want to register // We'll register two physical IoT devices entries := []struct { Index string Data map[string]any }{ { Index: "device-001", Data: map[string]any{"type": "sensor", "status": "active"}, }, { Index: "device-002", Data: map[string]any{"type": "actuator", "status": "offline"}, }, } for _, entry := range entries { fmt.Printf("Registering %s...\n", entry.Index) publishResult, err := client.PublishIndexedEntry(ctx, hcs2.PublishIndexedEntryOptions{ TopicID: topicID, Index: entry.Index, Data: entry.Data, }) if err != nil { log.Fatalf("Failed to publish entry: %v", err) } fmt.Printf("Published entry securely at Sequence #%d\n", publishResult.SequenceNumber) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // ... previous code ... // Let's define the data we want to register // We'll register two physical IoT devices entries := []struct { Index string Data map[string]any }{ { Index: "device-001", Data: map[string]any{"type": "sensor", "status": "active"}, }, { Index: "device-002", Data: map[string]any{"type": "actuator", "status": "offline"}, }, } for _, entry := range entries { fmt.Printf("Registering %s...\n", entry.Index) publishResult, err := client.PublishIndexedEntry(ctx, hcs2.PublishIndexedEntryOptions{ TopicID: topicID, Index: entry.Index, Data: entry.Data, }) if err != nil { log.Fatalf("Failed to publish entry: %v", err) } fmt.Printf("Published entry securely at Sequence #%d\n", publishResult.SequenceNumber) } CODE_BLOCK: // ... previous code ... // Let's define the data we want to register // We'll register two physical IoT devices entries := []struct { Index string Data map[string]any }{ { Index: "device-001", Data: map[string]any{"type": "sensor", "status": "active"}, }, { Index: "device-002", Data: map[string]any{"type": "actuator", "status": "offline"}, }, } for _, entry := range entries { fmt.Printf("Registering %s...\n", entry.Index) publishResult, err := client.PublishIndexedEntry(ctx, hcs2.PublishIndexedEntryOptions{ TopicID: topicID, Index: entry.Index, Data: entry.Data, }) if err != nil { log.Fatalf("Failed to publish entry: %v", err) } fmt.Printf("Published entry securely at Sequence #%d\n", publishResult.SequenceNumber) } CODE_BLOCK: // ... previous code ... fmt.Println("Fetching current registry state from Hedera Mirror Node...") // Fetch all valid entries for our topic state, err := client.GetIndexedRegistryState(ctx, topicID) if err != nil { log.Fatalf("Failed to fetch state: %v", err) } fmt.Println("\n--- Current Registry State ---") for key, value := range state { fmt.Printf("Key [%s]: %v\n", key, value) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // ... previous code ... fmt.Println("Fetching current registry state from Hedera Mirror Node...") // Fetch all valid entries for our topic state, err := client.GetIndexedRegistryState(ctx, topicID) if err != nil { log.Fatalf("Failed to fetch state: %v", err) } fmt.Println("\n--- Current Registry State ---") for key, value := range state { fmt.Printf("Key [%s]: %v\n", key, value) } } CODE_BLOCK: // ... previous code ... fmt.Println("Fetching current registry state from Hedera Mirror Node...") // Fetch all valid entries for our topic state, err := client.GetIndexedRegistryState(ctx, topicID) if err != nil { log.Fatalf("Failed to fetch state: %v", err) } fmt.Println("\n--- Current Registry State ---") for key, value := range state { fmt.Printf("Key [%s]: %v\n", key, value) } } - Append-Only: A continuous stream of events or records. - Indexed: A key-value store where later entries can "overwrite" or update the state of previous keys using deterministic indexing. - AI Agent identity directories - Decentralized package managers - Supply chain provenance logs - Certificate revocation lists - Go 1.22 or higher installed. - A Hedera Testnet account (Account ID and Private Key). You can get one from the Hedera Developer Portal. - Deterministic Memos: The CreateRegistry call automatically formats the memo of the Hedera topic to hcs-2;indexed, signaling to the rest of the ecosystem how to interpret the messages. - Schema Enforcement: The SDK ensures that messages submitted to the topic strictly adhere to the HCS-2 JSON schema. - Consensus Targeting: Messages are delivered instantly across the globe with fair-ordering consensus applied by the Hedera validators. - HCS-14 (Universal Agent IDs): Resolve AI identities. - Registry Broker: Use the pkg/registrybroker to interact directly with the global Hashgraph Online Registry Broker.