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

A runnable, self-contained example of the X3DH key-agreement primitives is also included in the repository at examples/x3dh_example.go (go run ./examples/x3dh_example.go).

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