Skip to main content

DKLS23 Go Library Usage Guide

Table of Contents

  1. Overview
  2. Architecture
  3. Building the Library
  4. Key Concepts
  5. Distributed Key Generation (Keygen)
  6. Threshold Signing
  7. Complete Examples

Overview

This Go library provides bindings to a Rust-based implementation of the DKLS23 protocol for threshold ECDSA signatures. The library enables:

  • Distributed Key Generation (DKG): Generate ECDSA key shares across multiple parties
  • Threshold Signing: Create valid ECDSA signatures with a subset of parties
  • Key Refresh: Update key shares while preserving the public key

Architecture

Your Go Code

Go Wrappers (this package)

Rust Bindings (../go-dkls/)

DKLS Protocol (../../src/)

Package Structure

go-dkls/
├── sessions/ # Main API
│ ├── keygen.go # Key generation functions
│ ├── sign.go # Signing functions
│ ├── keyshare.go # Keyshare management
│ ├── setup.go # Setup message utilities
│ ├── common.go # Type definitions and helpers
│ └── *_test.go # Comprehensive test suite
├── errors/ # Error type mappings
│ └── lib_err.go
└── test/ # Test helpers
└── helper.go

Directory Structure

  • /wrapper/go-dkls/src/ - Rust C bindings (C-compatible exports)
  • /wrapper/go-wrappers/go-dkls/sessions/ - Go wrapper functions
  • /wrapper/go-wrappers/go-dkls/errors/ - Error type mapping
  • /wrapper/go-wrappers/go-dkls/test/ - Helper functions for testing

Building the Library

Prerequisites

  • Rust toolchain (1.70+)
  • Go (1.19+)
  • C compiler (for CGO)

Build Steps

cd /path/to/dkls23-rs/wrapper/go-wrappers

# Build Rust library first
make build

# Run tests
cd go-dkls/sessions;go test .

The build process:

  1. Compiles Rust code to a shared library (libgodkls.so/.dylib/.dll)
  2. Generates C header files via cbindgen
  3. Go code uses CGO to link against the shared library

Key Concepts

Handles

The library uses an opaque handle system to manage Rust objects from Go:

type Handle int32

Handles represent:

  • Session handles: Active protocol sessions (keygen, signing)
  • Keyshare handles: Distributed key shares
  • Presign handles: Presignature data

Important: Always free handles when done to prevent memory leaks.

Setup Messages

Setup messages coordinate protocol execution across parties:

// Keygen setup
setupMsg, err := session.DklsKeygenSetupMsgNew(threshold, keyID, ids)

// Sign setup
setupMsg, err := session.DklsSignSetupMsgNew(keyID, chainPath, messageHash, ids)

Setup messages contain:

  • Protocol parameters (threshold, key ID)
  • Participant identifiers
  • Message routing information

Decode information from setup messages:

// Decode key ID
keyID, err := session.DklsDecodeKeyID(setupMsg)

// Decode message hash (for sign setup)
messageHash, err := session.DklsDecodeMessage(setupMsg)

// Decode party name by index
partyName, err := session.DklsDecodePartyName(setupMsg, 0)

Message Flow

Protocol execution follows a message-passing pattern:

  1. Output messages: Get messages to send to other parties
  2. Route messages: Determine recipient for each message
  3. Input messages: Process received messages
  4. Check completion: Verify if protocol finished

Party Identifiers

Parties are identified by human-readable strings (e.g., "p1", "p2", "p3"):

// Create ID slice for 3 parties
ids := []byte("p1\x00p2\x00p3") // null-separated

Distributed Key Generation (Keygen)

Basic Keygen Flow

Generate distributed ECDSA key shares across multiple parties.

Step 1: Create Setup Message

One party (the coordinator) creates a setup message:

import session "go-wrapper/go-dkls/sessions"

threshold := 2 // t: minimum parties needed to sign
n := 3 // n: total number of parties

// Prepare party IDs (null-separated)
ids := []byte("p1\x00p2\x00p3")

// Create setup message
setupMsg, err := session.DklsKeygenSetupMsgNew(threshold, nil, ids)
if err != nil {
log.Fatal(err)
}

Parameters:

  • threshold (int): Minimum number of parties required for signing
  • keyID ([]byte): Optional unique key identifier (nil for random)
  • ids ([]byte): Null-separated list of party identifiers

Step 2: Each Party Creates a Session

Each party receives the setup message and creates their session:

// Party 1
partyID := []byte("p1")
sessionHandle, err := session.DklsKeygenSessionFromSetup(setupMsg, partyID)
if err != nil {
log.Fatal(err)
}
defer session.DklsKeygenSessionFree(sessionHandle)

Step 3: Message Exchange Loop

Parties exchange messages until keygen completes:

msgQueue := make(map[string][][]byte)  // Messages keyed by recipient ID
finished := false

for !finished {
// Output: Get messages to send
for {
msg, err := session.DklsKeygenSessionOutputMessage(sessionHandle)
if err != nil {
log.Fatal(err)
}
if msg == nil {
break // No more messages to send
}

// Route message to recipients
for idx := 0; idx < n; idx++ {
receiver, err := session.DklsKeygenSessionMessageReceiver(
sessionHandle, msg, idx)
if err != nil {
log.Fatal(err)
}
if receiver == "" {
break // No more receivers
}

// Queue message for receiver
msgQueue[receiver] = append(msgQueue[receiver], msg)
}
}

// Input: Process received messages
myID := "p1" // This party's ID
for _, msg := range msgQueue[myID] {
finished, err = session.DklsKeygenSessionInputMessage(sessionHandle, msg)
if err != nil {
log.Fatal(err)
}
if finished {
break
}
}
msgQueue[myID] = nil // Clear processed messages
}

Step 4: Finalize and Get Keyshare

When protocol completes, extract the keyshare:

keyshareHandle, err := session.DklsKeygenSessionFinish(sessionHandle)
if err != nil {
log.Fatal(err)
}

// Extract public key
publicKey, err := session.DklsKeysharePublicKey(keyshareHandle)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Public key: %x\n", publicKey)

// Serialize keyshare for storage
keyshareBytes, err := session.DklsKeyshareToBytes(keyshareHandle)
if err != nil {
log.Fatal(err)
}
// Save keyshareBytes securely...

Key Refresh

Update key shares while preserving the public key (useful for proactive security):

// Step 1: Load existing keyshares
oldKeyshare, err := session.DklsKeyshareFromBytes(oldKeyshareBytes)
if err != nil {
log.Fatal(err)
}

// Step 2: Create refresh setup (use same key ID)
keyID, err := session.DklsKeyshareKeyID(oldKeyshare)
if err != nil {
log.Fatal(err)
}
setupMsg, err := session.DklsKeygenSetupMsgNew(threshold, keyID, ids)
if err != nil {
log.Fatal(err)
}

// Step 3: Create refresh session (not regular keygen!)
sessionHandle, err := session.DklsKeyRefreshSessionFromSetup(
setupMsg, partyID, oldKeyshare)
if err != nil {
log.Fatal(err)
}

// Step 4-6: Same message exchange as regular keygen
// The resulting keyshare has the same public key but new shares

Keyshare Operations

// Serialize/deserialize keyshares
keyshareBytes, err := session.DklsKeyshareToBytes(keyshareHandle)
keyshareHandle, err := session.DklsKeyshareFromBytes(keyshareBytes)

// Get public key
publicKey, err := session.DklsKeysharePublicKey(keyshareHandle)

// Get key ID
keyID, err := session.DklsKeyshareKeyID(keyshareHandle)

// Get chain code (for HD wallets)
chainCode, err := session.DklsKeyshareChainCode(keyshareHandle)

// Derive child public key
childPubKey, err := session.DklsKeyshareDeriveChildPublicKey(
keyshareHandle, []byte("m/0/1"))

// Free handle when done
err = session.DklsKeyshareFree(keyshareHandle)

Threshold Signing

Step 1: Create Sign Setup

// Get key ID from keyshare
keyID, err := session.DklsKeyshareKeyID(keyshareHandle)
if err != nil {
log.Fatal(err)
}

// Message to sign (must be 32 bytes - typically a hash)
messageHash := sha256.Sum256([]byte("Hello, world!"))

// Party IDs participating in this signature (must be >= threshold)
signerIDs := []byte("p1\x00p2") // 2 signers

// Create sign setup
setupMsg, err := session.DklsSignSetupMsgNew(
keyID,
nil, // chainPath (nil for root key)
messageHash[:], // message hash
signerIDs, // signers
)
if err != nil {
log.Fatal(err)
}

Step 2: Create Sign Sessions

// Each signer creates a session with their keyshare
sessionHandle, err := session.DklsSignSessionFromSetup(
setupMsg,
partyID,
keyshareHandle, // This party's keyshare
)
if err != nil {
log.Fatal(err)
}
defer session.DklsSignSessionFree(sessionHandle)

Step 3: Message Exchange Loop

Similar to keygen, but using sign-specific functions:

msgQueue := make(map[string][][]byte)
finished := false

for !finished {
// Output messages
for {
msg, err := session.DklsSignSessionOutputMessage(sessionHandle)
if err != nil {
log.Fatal(err)
}
if len(msg) == 0 {
break
}

// Route messages
for idx := 0; idx < numSigners; idx++ {
receiver, err := session.DklsSignSessionMessageReceiver(
sessionHandle, msg, idx)
if err != nil {
log.Fatal(err)
}
if len(receiver) == 0 {
break
}
msgQueue[string(receiver)] = append(msgQueue[string(receiver)], msg)
}
}

// Input messages
for _, msg := range msgQueue[myID] {
finished, err = session.DklsSignSessionInputMessage(sessionHandle, msg)
if err != nil {
log.Fatal(err)
}
}
msgQueue[myID] = nil
}

Step 4: Get Signature

signatureBytes, err := session.DklsSignSessionFinish(sessionHandle)
if err != nil {
log.Fatal(err)
}

// signatureBytes format: [R (32 bytes) || S (32 bytes) || RecoveryID (1 byte)]
// Total: 65 bytes
r := signatureBytes[0:32]
s := signatureBytes[32:64]
recoveryID := signatureBytes[64]

fmt.Printf("Signature R: %x\n", r)
fmt.Printf("Signature S: %x\n", s)
fmt.Printf("Recovery ID: %d\n", recoveryID)

Step 5: Verify Signature

import (
"crypto/ecdsa"
"math/big"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
)

// Get public key from keyshare
publicKeyBytes, err := session.DklsKeysharePublicKey(keyshareHandle)
if err != nil {
log.Fatal(err)
}

// Decompress public key
vkX, vkY := secp256k1.DecompressPubkey(publicKeyBytes)
publicKey := ecdsa.PublicKey{
Curve: secp256k1.S256(),
X: vkX,
Y: vkY,
}

// Verify signature
rBig := new(big.Int).SetBytes(r)
sBig := new(big.Int).SetBytes(s)
valid := ecdsa.Verify(&publicKey, messageHash[:], rBig, sBig)
fmt.Printf("Signature valid: %v\n", valid)


Complete Examples

Example 1: Simple 2-of-3 Keygen and Sign

package main

import (
"crypto/sha256"
"fmt"
"log"
session "go-wrapper/go-dkls/sessions"
)

func main() {
threshold := 2
numParties := 3

// ========== KEYGEN ==========

// Step 1: Create setup
ids := []byte("alice\x00bob\x00charlie")
setupMsg, err := session.DklsKeygenSetupMsgNew(threshold, nil, ids)
if err != nil {
log.Fatal(err)
}

// Step 2: Each party creates session (simulated locally)
type Party struct {
ID string
Session session.Handle
}

parties := []Party{
{ID: "alice"},
{ID: "bob"},
{ID: "charlie"},
}

for i := range parties {
parties[i].Session, err = session.DklsKeygenSessionFromSetup(
setupMsg, []byte(parties[i].ID))
if err != nil {
log.Fatal(err)
}
}

// Step 3: Message exchange loop
msgQueue := make(map[string][][]byte)
keyshares := make([]session.Handle, 0, numParties)

for len(keyshares) < numParties {
// Output phase
for _, party := range parties {
for {
msg, _ := session.DklsKeygenSessionOutputMessage(party.Session)
if msg == nil {
break
}

for idx := 0; idx < numParties; idx++ {
receiver, _ := session.DklsKeygenSessionMessageReceiver(
party.Session, msg, idx)
if receiver == "" {
break
}
msgQueue[receiver] = append(msgQueue[receiver], msg)
}
}
}

// Input phase
for _, party := range parties {
for _, msg := range msgQueue[party.ID] {
finished, _ := session.DklsKeygenSessionInputMessage(
party.Session, msg)
if finished {
keyshare, _ := session.DklsKeygenSessionFinish(party.Session)
keyshares = append(keyshares, keyshare)
}
}
msgQueue[party.ID] = nil
}
}

fmt.Println("✓ Keygen complete!")

// ========== SIGN ==========

// Use first 2 keyshares (threshold)
signingParties := parties[:2]
signingKeyshares := keyshares[:2]

// Step 1: Create sign setup
keyID, _ := session.DklsKeyshareKeyID(signingKeyshares[0])
message := []byte("Transaction data")
messageHash := sha256.Sum256(message)
signerIDs := []byte("alice\x00bob")

signSetup, err := session.DklsSignSetupMsgNew(
keyID, nil, messageHash[:], signerIDs)
if err != nil {
log.Fatal(err)
}

// Step 2: Create sign sessions
for i := range signingParties {
signingParties[i].Session, err = session.DklsSignSessionFromSetup(
signSetup, []byte(signingParties[i].ID), signingKeyshares[i])
if err != nil {
log.Fatal(err)
}
}

// Step 3: Message exchange (similar to keygen)
msgQueue = make(map[string][][]byte)
signatures := make([][]byte, 0, threshold)

for len(signatures) < threshold {
// Output phase
for _, party := range signingParties {
for {
msg, _ := session.DklsSignSessionOutputMessage(party.Session)
if len(msg) == 0 {
break
}

for idx := 0; idx < threshold; idx++ {
receiver, _ := session.DklsSignSessionMessageReceiver(
party.Session, msg, idx)
if len(receiver) == 0 {
break
}
msgQueue[string(receiver)] = append(
msgQueue[string(receiver)], msg)
}
}
}

// Input phase
for _, party := range signingParties {
for _, msg := range msgQueue[party.ID] {
finished, _ := session.DklsSignSessionInputMessage(
party.Session, msg)
if finished {
sig, _ := session.DklsSignSessionFinish(party.Session)
signatures = append(signatures, sig)
}
}
msgQueue[party.ID] = nil
}
}

fmt.Println("✓ Signing complete!")
fmt.Printf("Signature: %x\n", signatures[0])
// Get public key
publicKey, _ := session.DklsKeysharePublicKey(keyshares[0])
vkX, vkY := secp256k1.DecompressPubkey(publicKey)
vk := ecdsa.PublicKey{Curve: secp256k1.S256(), X: vkX, Y: vkY}

// Verify
r := big.NewInt(0).SetBytes(signatures[0][:32])
s := big.NewInt(0).SetBytes(signatures[0][32:64])
verified := ecdsa.Verify(&vk, messageHash[:], r, s)
fmt.Printf("Signature verified: %v\n", verified)
}

Error Handling

The library maps Rust errors to Go errors:

import "go-wrapper/go-dkls/errors"

// Common errors
switch err {
case errors.LIB_NULL_PTR:
// Null pointer passed
case errors.LIB_INVALID_HANDLE:
// Invalid handle used
case errors.LIB_SETUP_MESSAGE_VALIDATION:
// Setup message validation failed
case errors.LIB_KEYGEN_ERROR:
// Key generation protocol error
case errors.LIB_SIGNGEN_ERROR:
// Signature generation protocol error
case errors.LIB_ABORT_PROTOCOL_PARTY_1:
// Party 1 aborted the protocol
// (similarly for PARTY_2, PARTY_3, etc.)
case errors.LIB_ABORT_PROTOCOL_AND_BAN_PARTY_1:
// Party 1 is malicious and should be banned
}


Rust Bindings Reference

Binding Layer (/wrapper/go-dkls/src/)

The Rust bindings provide C-compatible functions:

Keygen Bindings (keygen.rs)

// Create keygen setup message
#[no_mangle]
pub extern "C" fn dkls_keygen_setupmsg_new(
threshold: u32,
key_id: Option<&go_slice>,
ids: Option<&go_slice>,
setup_msg: Option<&mut tss_buffer>,
) -> lib_error

// Create keygen session from setup
#[no_mangle]
pub extern "C" fn dkls_keygen_session_from_setup(
setup: Option<&go_slice>,
id: Option<&go_slice>,
hnd: Option<&mut Handle>,
) -> lib_error

// Process input message
#[no_mangle]
pub extern "C" fn dkls_keygen_session_input_message(
session: Handle,
message: Option<&go_slice>,
finished: Option<&mut i32>,
) -> lib_error

// Get output message
#[no_mangle]
pub extern "C" fn dkls_keygen_session_output_message(
session: Handle,
message: Option<&mut tss_buffer>,
) -> lib_error

// Finish keygen
#[no_mangle]
pub extern "C" fn dkls_keygen_session_finish(
session: Handle,
keyshare: Option<&mut Handle>,
) -> lib_error

Sign Bindings (sign.rs)

// Create sign setup message
#[no_mangle]
pub extern "C" fn dkls_sign_setupmsg_new(
key_id: Option<&go_slice>,
chain_path: Option<&go_slice>,
message_hash: Option<&go_slice>,
ids: Option<&go_slice>,
setup_msg: Option<&mut tss_buffer>,
) -> lib_error

// Create finish setup message
#[no_mangle]
pub extern "C" fn dkls_finish_setupmsg_new(
session_id: Option<&go_slice>,
message_hash: Option<&go_slice>,
ids: Option<&go_slice>,
setup_msg: Option<&mut tss_buffer>,
) -> lib_error

// Create sign session
#[no_mangle]
pub extern "C" fn dkls_sign_session_from_setup(
setup: Option<&go_slice>,
id: Option<&go_slice>,
share_or_presign: Handle,
hnd: Option<&mut Handle>,
) -> lib_error

// Finish signing
#[no_mangle]
pub extern "C" fn dkls_sign_session_finish(
session: Handle,
output: Option<&mut tss_buffer>,
) -> lib_error

Memory Management

The bindings use a handle-based system to manage Rust objects:

  1. Allocation: Rust allocates objects, returns handles to Go
  2. Access: Go passes handles back for operations
  3. Deallocation: Go calls *_free() when done
pub(crate) struct Handle {
// Opaque pointer to Rust object
}

impl Handle {
pub fn allocate<T>(obj: T) -> Self {
// Store object in handle table
}

pub fn get<T>(&self) -> Result<T, lib_error> {
// Retrieve object from handle table
}
}
  • LIB_ABORT_PROTOCOL_PARTY_X: Party X aborted protocol
  • LIB_ABORT_PROTOCOL_AND_BAN_PARTY_X: Party X is malicious

Type Conversions (Internal)

Go to C

// Go []byte to C go_slice
func cGoSlice(byteArray []byte, pinner *runtime.Pinner) *C.go_slice

// Go Handle to C Handle
func cHandle(handle Handle) C.Handle

C to Go

// C tss_buffer to Go []byte
buf := C.GoBytes(unsafe.Pointer(cBuffer.ptr), C.int(cBuffer.len))

// C Handle to Go Handle
handle := Handle(cHandle._0)

Data Structures

go_slice

Represents a Go slice in C/Rust:

#[repr(C)]
pub struct go_slice {
pub ptr: *const u8,
pub len: usize,
pub cap: usize,
}

tss_buffer

Represents an allocated buffer returned to Go:

#[repr(C)]
pub struct tss_buffer {
pub ptr: *mut u8,
pub len: usize,
}

impl tss_buffer {
pub fn from<T: Into<Vec<u8>>>(data: T) -> Self {
// Allocate buffer for Go
}

pub fn free(&mut self) {
// Deallocate buffer
}
}

Additional Resources