DKLS23 Go Library Usage Guide
Table of Contents
- Overview
- Architecture
- Building the Library
- Key Concepts
- Distributed Key Generation (Keygen)
- Threshold Signing
- 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:
- Compiles Rust code to a shared library (
libgodkls.so/.dylib/.dll) - Generates C header files via
cbindgen - 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:
- Output messages: Get messages to send to other parties
- Route messages: Determine recipient for each message
- Input messages: Process received messages
- 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 signingkeyID([]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:
- Allocation: Rust allocates objects, returns handles to Go
- Access: Go passes handles back for operations
- 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 protocolLIB_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
- Repository: https://github.com/silence-laboratories/dkls23-rs
- DKLS Paper: https://eprint.iacr.org/2023/765