Skip to content

Go client

The Go client package connects directly to the Zentalk relay network: anonymous registration and connection via zero-knowledge proofs, encrypted peer-to-peer messaging, username lookup, and group messaging. It speaks the relay wire protocol — for plain HTTP integrations, call the REST API with net/http instead.

import "github.com/ZentaChain/zentalk-node/pkg/client"

The package is importable today from the zentalk-node module.

Concept Description
Identity A local ZK identity: a 32-byte secret, a public commitment, and an RSA key pair, stored as a JSON file. The secret never leaves the device.
Registration The identity’s commitment is registered with a relay and assigned a Merkle tree index.
Anonymous connection On connect, the client generates a Groth16 proof of tree membership. The relay learns that you are registered, not who you are, and assigns an anonymous address.
Proving key A Groth16 proving key required to generate connection proofs, produced by the zkp-setup tool.
Ephemeral key A per-connection RSA key pair used for encrypted messaging, deliberately unlinked from the identity.

See Security overview for the broader security model.

All constructors and methods below are defined in pkg/client.

Client is the main entry point; it composes the identity, registration, ZKP, messaging, address, and key managers.

type ClientConfig struct {
RelayAddr string // Relay server address, e.g. "localhost:8080"
IdentityPath string // Where the identity JSON file is stored
ProvingKey groth16.ProvingKey // Proving key for ZK proofs
}
func NewClient(config *ClientConfig) *Client
Method Description
Initialize() error Loads the identity file, or creates a new identity if none exists
Register(metadata map[string]string) error Registers the identity’s commitment with the relay (optional metadata such as "username")
Connect() error Fetches the Merkle tree, generates a ZK handshake proof, and connects anonymously
Disconnect() error Closes the relay connection
IsConnected() bool / IsRegistered() bool Connection and registration state
SendMessage(recipientAddr protocol.Address, recipientPubKey *rsa.PublicKey, message string) error Sends an encrypted message
ReceiveMessage() (*ReceivedMessage, error) Blocks until one message is received and decrypted
StartMessageListener(handler func(*ReceivedMessage)) error Delivers incoming messages to a handler in the background
RegisterUsername(username string) error Registers a username for address lookup
LookupAddress(username string) (protocol.Address, error) Resolves a username to a protocol address
AdvertiseKey() error Publishes the ephemeral public key so others can send you encrypted messages
LookupKey(addr protocol.Address) (*rsa.PublicKey, error) Fetches a peer’s advertised public key
GetIdentity() *Identity / GetAnonymousAddress() protocol.Address / GetEphemeralPublicKey() *rsa.PublicKey Accessors
InitializeGroups(dbPath, password string) error Opens the encrypted local group database and enables group support

Received messages have this shape:

type ReceivedMessage struct {
From protocol.Address
To protocol.Address
Content string
Timestamp time.Time
}
func NewIdentityManager(identityPath string) *IdentityManager

IdentityManager persists identities as JSON: GenerateIdentity(), SaveIdentity(), LoadIdentity(), HasIdentity(), GetOrCreateIdentity() (*Identity, bool, error) (the bool reports whether a new identity was created), and DeleteIdentity().

type Identity struct {
Secret [32]byte // Private: never share
Commitment zkp.Commitment // Public: registered with the relay
TreeIndex uint64 // Assigned by the relay after registration
IsRegistered bool
PrivateKeyPEM []byte // RSA private key (PEM)
PublicKeyPEM []byte // RSA public key (PEM)
}

Identity also exposes GetPrivateKey(), GetPublicKey(), and GetPublicKeyDER().

Group messaging is layered on top of a connected Client and a local encrypted message database:

func NewGroupClient(client *Client, db *storage.MessageDB) *GroupClient
Method Description
CreateGroup(name, description string, groupType group.GroupType) (*group.Group, error) Create a group; group.GroupTypePrivate (invite-only) or group.GroupTypePublic (joinable via link)
JoinGroup(joinCode string) (*group.Group, error) Join a public group by join code
InviteMember(groupID string, memberAddr protocol.Address, memberUsername string, memberPubKey *rsa.PublicKey) error Invite a member
SendGroupMessage(groupID string, message string) error Send a message to all members
GetGroupMessages(groupID string, limit, offset int) ([]*group.GroupMessage, error) Read stored group messages
GetMyGroups() ([]*group.Group, error) / GetGroupMembers(groupID string) ([]*group.Member, error) Listings
UpdateGroupInfo(groupID, name, description string) error Update metadata
PromoteMember / DemoteMember / KickMember Admin operations (each takes groupID string, memberAddr protocol.Address)
LeaveGroup(groupID string) error Leave a group

For the group cryptography design, see Group messaging.

Lower-level building blocks, used internally by Client.Connect() and available directly:

func NewTreeSyncClient(relayAddr string) *TreeSyncClient
// FetchTree() (*zkp.MerkleTree, error)
// FetchTreeWithTimeout(timeout time.Duration) (*zkp.MerkleTree, error)
// VerifyTreeIntegrity(tree *zkp.MerkleTree) error
func NewZKPClient(provingKey groth16.ProvingKey, tree *zkp.MerkleTree,
identity *Identity, ephemeralPubKey *rsa.PublicKey) (*ZKPClient, error)
// GenerateHandshakeProof() (*protocol.ZKHandshakeMessage, error)
// GetEpoch() uint64, GetCommitment() zkp.Commitment, GetTreeIndex() uint64

The flow below mirrors the zentalk-client CLI: register once, then connect, look up a recipient, and send an encrypted message.

send.go
package main
import (
"log"
"github.com/ZentaChain/zentalk-node/pkg/client"
"github.com/ZentaChain/zentalk-node/pkg/zkp"
)
func main() {
// Load the Groth16 proving key (generated by the zkp-setup tool)
pk, err := zkp.LoadProvingKey("./zkp-keys/proving_key.bin")
if err != nil {
log.Fatalf("load proving key: %v", err)
}
cl := client.NewClient(&client.ClientConfig{
RelayAddr: "localhost:8080",
IdentityPath: "./zkp-identity.json",
ProvingKey: pk,
})
// Load or create the local ZK identity
if err := cl.Initialize(); err != nil {
log.Fatalf("initialize: %v", err)
}
// One-time registration of the identity's commitment with the relay
if !cl.IsRegistered() {
if err := cl.Register(map[string]string{"username": "alice"}); err != nil {
log.Fatalf("register: %v", err)
}
}
// Connect anonymously (generates a ZK membership proof)
if err := cl.Connect(); err != nil {
log.Fatalf("connect: %v", err)
}
defer cl.Disconnect()
// Resolve the recipient and fetch their advertised public key
addr, err := cl.LookupAddress("bob")
if err != nil {
log.Fatalf("lookup address: %v", err)
}
pubKey, err := cl.LookupKey(addr)
if err != nil {
log.Fatalf("lookup key: %v", err) // recipient must have run AdvertiseKey()
}
// Send an encrypted message
if err := cl.SendMessage(addr, pubKey, "hello over the relay"); err != nil {
log.Fatalf("send: %v", err)
}
}

To receive messages, the peer advertises its key and listens:

listen.go (recipient side)
if err := cl.AdvertiseKey(); err != nil {
log.Fatal(err)
}
err := cl.StartMessageListener(func(msg *client.ReceivedMessage) {
log.Printf("from %x: %s (%s)", msg.From[:8], msg.Content, msg.Timestamp)
})
if err != nil {
log.Fatal(err)
}
select {} // keep listening

For a runnable reference to the X3DH key-agreement primitives themselves, see the tests in pkg/protocol (go test ./pkg/protocol -run X3DH -v). These cover the classical handshake and a post-quantum hybrid variant, but note that the Go hybrid path is not reachable from any production code path: protocol.HybridInitialMessage has neither an Encode nor a Decode method and no caller outside its own file, so no Go binary can send or receive one. The hybrid handshake that matters on the wire is the TypeScript one (post-quantum hybrid encryption).

The CLI in cmd/zentalk-client runs the ZKP messaging flow end to end without requiring any code.

Terminal window
go build -o zentalk-client ./cmd/zentalk-client

Full flag and command reference: CLI reference.

Proving keys must exist before connecting; generate them once with the setup tool:

Terminal window
go run cmd/zkp-setup/main.go -output ./zkp-keys

Then, with a relay running on localhost:8080:

Terminal window
# Terminal 1 — Bob registers, advertises his key, and listens
./zentalk-client -cmd register -username bob
./zentalk-client -cmd register-username -username bob
./zentalk-client -cmd advertise-key
./zentalk-client -cmd listen
# Terminal 2 — Alice registers and sends Bob a message
./zentalk-client -cmd register -username alice -identity ./alice-identity.json
./zentalk-client -cmd send -identity ./alice-identity.json \
-username bob -message "hello bob"
  • SDK overview — how this package fits alongside the REST API
  • REST API — for HTTP integrations in Go via net/http
  • Security overview — ZKP and E2EE model
  • X3DH — the key agreement used for message encryption