DuoSession

interface DuoSession

Two-party MPC (Multi-Party Computation) session for secure distributed cryptographic operations.

DuoSession enables secure threshold signature schemes between two parties without ever reconstructing the complete private key. Both parties must cooperate to sign, providing enhanced security over single-key solutions. Supports ECDSA and EdDSA with operations for key generation, signing, key management, and backup/recovery.

Getting Started

1. Implement Your Key Provider

The key provider manages your device's secure master signing key. This key is used to authenticate MPC operations and should be stored in hardware-backed secure storage.

// Example: Using platform secure storage
class SecureKeyProvider : MasterSigningKeyProvider {
private val secureStore = getPlatformSecureStorage()
private val keyAlias = "mpc_master_key"

override val verifyingKey: ByteArray
get() = secureStore.getPublicKey(keyAlias)

override val masterSigningKeyType = MasterSigningKeyType.ECDSA

override fun sign(data: ByteArray): ByteArray {
return secureStore.sign(keyAlias, data)
}
}

2. Implement Storage Client

The storage client manages keyshare persistence and tracks staged/current keyshare states. It uses ReconcileStoreDao which has three important fields:

  • keyId: Unique identifier for the keyshare

  • stagedKeyshare: Keyshare that completed MPC operation but not yet reconciled

  • currentKeyshare: Fully reconciled and active keyshare ready for signing

class MyStorageClient : StorageClient<ReconcileStoreDao> {
private val storage = mutableMapOf<String, ReconcileStoreDao>()

override suspend fun write(dao: ReconcileStoreDao) {
storage[dao.keyId] = dao
}

override suspend fun read(key: String): ReconcileStoreDao? {
return storage[key]
}
}

3. Create Session with Demo Server

// Demo server configuration
val storageClient = MyStorageClient()

val session = SilentShard.ECDSA.createDuoSession(
keyProvider = SecureKeyProvider(),
cloudVerifyingKey = "01829099948d222f9731323900bc42bd3cc6f4e692f920705a9959b3e52378f188",
websocketConfig = WebsocketConfig(
url = "wss://demo-server.silencelaboratories.com"
),
storageClient = storageClient
)

4. Generate and Use Keyshare

// Generate distributed keyshare (internally reconciled)
val keyshare = session.keygen().getOrThrow()

// Get public key for address derivation
val publicKey = SilentShard.ECDSA.getKeysharePublicKey(keyshare).getOrThrow()
val address = deriveAddress(publicKey)

// Sign transaction
val txHash = hashTransaction(transaction)
val signature = session.signature(
keyshare = keyshare,
message = txHash.toHexString(),
derivationPath = "m/44'/X'/0'/0/0"
).getOrThrow()

5. Handle Reconciliation Failures (Advanced)

If reconciliation fails, the keyshare stays in stagedKeyshare field. You can retry:

// Check storage for staged keyshare
val keyId = "your_key_id"
val dao = storageClient.read(keyId)

if (dao?.stagedKeyshare != null && dao.currentKeyshare == null) {
// Reconciliation was incomplete - retry it
println("Found staged keyshare, retrying reconciliation...")
val reconciledKeyshare = session.reconcileKeyshare(keyId).getOrThrow()
println("Reconciliation successful!")
}

Thread Safety

All operations are suspend functions requiring coroutine context. Do not call from main/UI thread.

Advanced: Custom Network Transport

Option 1: Custom WebSocket Configuration (Web-Based)

If you need custom WebSocket handling (custom headers, authentication, reconnection logic):

class CustomWebSocketClient(
private val url: String,
private val authToken: String
) : NetworkClient<WebSocketAction> {
private var connection: WebSocket? = null

override suspend fun send(action: WebSocketAction): ByteArray = suspendCoroutine { cont ->
// Custom connection with authentication
connection = WebSocket(url).apply {
setHeader("Authorization", "Bearer $authToken")
setHeader("X-Client-Version", "1.0.0")

onOpen {
// Send MPC data
send(action.payload)
}

onMessage { data ->
// Receive MPC response
cont.resume(data)
}

onError { error ->
cont.resumeWithException(error)
}
}

connection?.connect()
}

override suspend fun disconnect() {
connection?.close()
}
}

// Use custom WebSocket client
val session = SilentShard.ECDSA.createDuoSession(
keyProvider = keyProvider,
cloudVerifyingKey = serverPublicKey,
networkClient = CustomWebSocketClient(
url = "wss://your-custom-server.com/mpc",
authToken = getUserAuthToken()
),
storageClient = storage,
actionProvider = CustomWebSocketActionProvider()
)

Option 2: BLE Hardware Wallet (Non-Web)

For specialized hardware scenarios like Bluetooth Low Energy:

// BLE hardware wallet transport
class BLEDuoClient(private val device: BluetoothDevice) : NetworkClient<BLEAction> {
override suspend fun send(action: BLEAction): ByteArray {
val characteristic = device.getCharacteristic(action.uuid)
characteristic.writeValue(action.payload)
return characteristic.readValue()
}
}

val session = SilentShard.ECDSA.createDuoSession(
keyProvider = keyProvider,
cloudVerifyingKey = hardwareWalletPublicKey,
networkClient = BLEDuoClient(bleDevice),
storageClient = storage,
actionProvider = BLEDuoActionProvider()
)

See also

for keyshare persistence

for understanding staged vs current keyshares

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
abstract val verifyingKey: String

The public verifying key of this party's master signing key (hex format). Share with the other party to establish secure communication.

Functions

Link copied to clipboard
abstract suspend fun export(hostKeyshare: ByteArray, otherEncryptedKeyshare: ByteArray, hostEncryptionKey: ByteArray, otherDecryptionKey: ByteArray): Result<ByteArray>

Exports keyshare data for secure backup.

Link copied to clipboard
abstract suspend fun import(keysharePrivateKey: ByteArray, rootChainCode: String): Result<ByteArray>

Imports a keyshare from backup data or existing private key.

Link copied to clipboard
abstract suspend fun keygen(): Result<ByteArray>

Generates a new distributed keyshare through two-party MPC.

Link copied to clipboard
abstract suspend fun keyRefresh(keyshare: ByteArray): Result<ByteArray>

Refreshes an existing keyshare without changing its public key.

Link copied to clipboard
abstract suspend fun reconcileKeyshare(keyId: String): Result<ByteArray>

Reconciles a staged keyshare to complete the after-part and make it active for signing.

Link copied to clipboard
abstract suspend fun signature(keyshare: ByteArray, message: String, derivationPath: String = "m"): Result<ByteArray>

Creates a cryptographic signature using two-party threshold signing.

Link copied to clipboard
abstract suspend fun verifyBackup(keyshare: ByteArray, backupData: ByteArray, rsaPublicKey: ByteArray, label: String = ""): Result<Boolean>

Verifies the integrity of a backup using RSA signature.