Skip to main content

Architecture

Both reference apps split into two layers:

  • App/ — UI, blockchain network code (RPC, address derivation), account state, faucet integration. Free to import any third-party library. Has no idea what a keyshare looks like.
  • Vault/VaultSessionManager, EncryptedStorageClient, EncryptedFileStorage, hardware-backed signing. The only place keyshare bytes ever touch disk. Vault types that aren't part of the public API surface are marked internal so the App layer can't reach them even by accident.

The split is a property of the example, not a requirement of the SDK — see Custom storage client for the minimum the SDK actually expects. The walls in the example exist so an integrator can copy the design knowing it has the property "the SDK is the only writer of keyshare bytes."

Source tree

silent-shard-sdk/with-android/duo-initiator/
├── app/ # Application module
│ └── src/main/kotlin/com/silencelaboratories/silentshard/demo/
│ ├── data/
│ │ ├── model/ # Chain, Token, WalletAccount
│ │ └── network/ # Ethereum / Solana / faucet RPC
│ ├── feature/
│ │ ├── setup/ # Wallet creation flow
│ │ ├── blockchain/ # Dashboard, send, fund, AccountManager
│ │ └── export/ # Backup & restore
│ ├── navigation/ # Routes, AppNavHost
│ ├── ui/ # Theme, components
│ ├── util/ # Constants, AddressUtils
│ ├── MainActivity.kt
│ └── WalletApp.kt
└── vault/ # Vault module (internal types)
└── src/main/kotlin/com/silencelaboratories/silentshard/demo/vault/
├── model/ # KeyResult
├── session/ # KeyType, VaultSessionManager
├── signing/ # HardwareMessageSigner, P256KeyManager
├── storage/ # EncryptedFileStorage, EncryptedStorageClient,
│ # VaultReconcileStoreDao
└── utils/ # VaultCryptoUtils, EllipticCurveEncodingUtils

How the SDK plugs in

The SDK exposes one factory call per algorithm. The example creates two sessions (one per KeyType), each bound to a dedicated storage client:

vault/.../session/VaultSessionManager.kt
private val ecdsaStorage = EncryptedStorageClient(store, KeyType.ECDSA)
private val eddsaStorage = EncryptedStorageClient(store, KeyType.EdDSA)

private val ecdsaSession: DuoSession =
SilentShard.ECDSA.duoInitiator(messageSigner, CLOUD_VERIFYING_KEY, websocketConfig, ecdsaStorage)

private val eddsaSession: DuoSession =
SilentShard.EdDSA.duoInitiator(messageSigner, CLOUD_VERIFYING_KEY, websocketConfig, eddsaStorage)
Vault/Session/VaultSessionManager.swift
self.ecdsaStorage = EncryptedStorageClient(store: fileStorage, algorithm: .ecdsa)
self.eddsaStorage = EncryptedStorageClient(store: fileStorage, algorithm: .eddsa)

let session = SilentShard.ECDSA.duoInitiator(
messageSigner: messageSigner,
cloudVerifyingKey: Self.cloudVerifyingKey,
websocketConfig: createWebsocketConfig(),
storageClient: ecdsaStorage
)

Why one storage client per KeyType

The SDK's StorageClient interface declares a keyType — the algorithm family that instance serves — and its contract is explicit: an app running more than one family supplies a separate StorageClient per family rather than multiplexing. The example follows that contract, binding one EncryptedStorageClient to each KeyType and handing every session its matching client.

The record the SDK asks it to persist, ReconcileStoreDao, is the other half of the story: it carries only keyId, currentKeyshare, and stagedKeyshareno algorithm tag. So when VaultSessionManager.sign(keyId) reads a record back to choose the ECDSA or EdDSA session, the record alone doesn't say which one. The example closes that gap by serializing the client's bound KeyType as the first line of the encrypted blob; on read it returns a VaultReconcileStoreDao (Android) / VaultReconcileRecord (iOS) — a small wrapper that exposes the algorithm alongside the SDK's dao fields — and the session manager dispatches on dao.keyType.

Both storage clients share the same EncryptedFileStorage, so a read from either instance can resolve any keyId regardless of which algorithm wrote it — which is exactly why each record carries its own embedded tag. The storage layout details are in Storage client.

The name VaultReconcileStoreDao isn't arbitrary — the SDK writes two fields (currentKeyshare and stagedKeyshare) specifically so a crashed keyRefresh can be committed on next launch. The two-slot state machine and the example's reconcile() + reconcileOnStartup() hooks are documented in Reconciliation.

Protocol-level architecture

This page covers the example app's layering. For how the duo protocol itself works (party roles, message exchange, threshold properties), see the Silent Shard Duo Architecture docs.

Dashboard