Sign
Produce a signature over a message hash. The device and the Duo Server each sign with their own share, and the results combine into a single, standard signature. The full private key is never reassembled.
Please refer to the Session creation section to learn how to create a new session.
Full example
- ECDSA
- EdDSA
- MLDSA
import { type EcdsaSession } from '@silencelaboratories/silent-shard-sdk/ecdsa';
// This could be a hash digest of any message you want to sign.
// For example, for the Ethereum transaction signing, you would use the keccak256 hash of the transaction data.
const messageHash = 'e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03';
// The full message to sign. The SDK digests it with the given hashAlgo before signing.
// For an EIP-1559 transaction this would be the RLP-encoded transaction bytes (hex).
const message =
'02ef0182012c843b9aca00850165a0bc008252089470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0';
export const signGen = async (session: EcdsaSession) => {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
const keyshare = await session.keygen();
const signConfig = {
keyshare,
messageHash,
};
const signature = await session.sign(signConfig);
console.log('Signature:', signature);
};
// Sign with hash: pass the full message and the hash algorithm; the SDK digests it before signing.
export const signWithHash = async (session: EcdsaSession) => {
const keyshare = await session.keygen();
const signConfig = {
keyshare,
message,
hashAlgo: 'KECCAK256',
} as const;
const signature = await session.signWithHash(signConfig);
console.log('Signature (with hash):', signature);
};
// Sign with hash & custom tags: attach a TLV stream read by the policy engine.
// Tag slot 0 carries the transaction type.
export const signWithHashAndTag = async (session: EcdsaSession) => {
const keyshare = await session.keygen();
const signConfig = {
keyshare,
message,
hashAlgo: 'KECCAK256',
customTags: {
0: 'eip1559',
},
} as const;
const signature = await session.signWithHash(signConfig);
console.log('Signature (with hash & custom tag):', signature);
};
import { type EddsaSession } from '@silencelaboratories/silent-shard-sdk/eddsa';
const messageHash = 'e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03';
export const signGen = async (session: EddsaSession) => {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
const keyshare = await session.keygen();
const signConfig = {
keyshare,
messageHash,
};
const signature = await session.sign(signConfig);
console.log('Signature:', signature);
};
// Sign with custom tags: attach a TLV stream read by the policy engine.
// Tag slot 0 carries the transaction type.
export const signWithCustomTags = async (session: EddsaSession) => {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
const keyshare = await session.keygen();
const customTags = {
0: 'solanaTransaction',
};
const signature = await session.sign({
keyshare,
messageHash,
customTags,
});
console.log('Signature with custom tags:', signature);
};
import { MldsaLevels } from '@silencelaboratories/silent-shard-sdk';
import { type MldsaSession } from '@silencelaboratories/silent-shard-sdk/mldsa';
const messageHash = 'e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03';
export const signGen = async (session: MldsaSession) => {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
const keyshare = await session.keygen();
const signConfig = {
keyshare,
messageHash,
// Mldsa signature level
level: MldsaLevels.MlDsa44,
};
const signature = await session.sign(signConfig);
console.log('Signature:', signature);
};
// Sign with custom tags: attach a TLV stream read by the policy engine.
// Tag slot 0 carries the transaction type.
export const signWithCustomTags = async (session: MldsaSession) => {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
const keyshare = await session.keygen();
const customTags = {
0: 'mldsa',
};
const signature = await session.sign({
keyshare,
messageHash,
customTags,
level: MldsaLevels.MlDsa44,
});
console.log('Signature with custom tags:', signature);
};
- The
signmethod takes a EcdsaSignConfig object as an argument. keyshare(Keyshare) is the client's "share" of the MPC wallet.messageHashis the hash of the message to be signed as a hex string.- When
session.sign()is called, the app and the server exchange messages to generate an ECDSA signature. signatureis the ECDSA signature (hex string) ofmessageHash, corresponding to the public key (or address) of the wallet.
Signing a full message
sign() sends a 32-byte hash, so the server only ever sees that hash — it cannot tell what the message actually says.
signWithHash — EcdsaSignWithHashConfig
Available on EcdsaSession only. Instead of messageHash, pass:
message— the full message to sign, as a hex string. For an EIP-1559 transaction, this is the RLP-encoded unsigned transaction.hashAlgo— one ofKECCAK256,SHA256,SHA256DorHASH32. The SDK hashesmessagewith it before signing, so the signature matches whatsign()would have produced over that hash.
Because the server receives the whole message, it can apply transaction policies to it. EdDSA and ML-DSA sessions do not have signWithHash.
customTags — SignCustomTags
Optional on every algorithm, with either sign() or signWithHash().
- Keys are tag slots — integers from
0to63. - Each value is a non-empty UTF-8 string (up to 65536 bytes), or an array of strings to put several values in one slot.
- Tags are encoded as a TLV stream into the signing setup message and delivered to your backend, which can act on them in its DSG hook.
What each slot means is defined by your backend, not by the SDK.
Please refer to the Session creation section to learn how to create a new session.
Full example
- ECDSA
- EdDSA
- Taproot
import 'dart:typed_data';
import 'package:silent_shard_sdk/silent_shard_sdk.dart' as sdk;
// This could be a hash digest of any message you want to sign.
// For example, for the Ethereum transaction signing, you would use the keccak256 hash of the transaction data.
const messageHash =
'e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03';
Future<Uint8List> signGen(sdk.EcdsaSession session) async {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
final sdk.DklsKeyshare keyshare = await session.keygen();
print('Keyshare created, public key: ${keyshare.publicKeyHex}');
final signature = await session.sign(
keyId: keyshare.keyId,
messageHash: messageHash,
);
print('Signature: $signature');
return signature;
}
import 'package:silent_shard_sdk/silent_shard_sdk.dart' as sdk;
// This could be a hash digest of any message you want to sign.
// For example, for the Ethereum transaction signing, you would use the keccak256 hash of the transaction data.
const messageHash =
'e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03';
Future<void> signGen(sdk.EddsaSession session) async {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
final sdk.SchnorrKeyshare keyshare = await session.keygen();
print('Keyshare created, public key: ${keyshare.publicKeyHex}');
final signature = await session.sign(
keyId: keyshare.keyId,
messageHash: messageHash,
);
print('Signature: $signature');
}
import 'dart:typed_data';
import 'package:silent_shard_sdk/silent_shard_sdk.dart' as sdk;
// This could be a hash digest of any message you want to sign.
// For Bitcoin Taproot transactions, this is the sighash as defined in BIP 341.
const messageHash =
'5ae9337fe6b54559bf2c16aea4472f8f8cfab3cfa6844547801fb8c752151552';
Future<Uint8List> signGen(sdk.TaprootSession session) async {
// Creating a new keyshare for demo purpose. In real application, you can use an existing keyshare.
final sdk.TaprootKeyshare keyshare = await session.keygen();
print('Keyshare created, public key: ${keyshare.publicKeyHex}');
final signature = await session.sign(
keyId: keyshare.keyId,
messageHash: messageHash,
);
print('Signature: $signature');
return signature;
}
- DklsKeyshare is the client's "share" of the MPC wallet.
messageHashis the hash of the message to be signed as a hex string.- When
session.sign()is called, the app and the server exchange messages to generate an ECDSA signature. signatureis the signature (hex string) ofmessageHash, corresponding to the public key (or address) of the wallet.
This distributed signing process allows for secure transaction authorization while preserving the key's distributed nature, exemplifying the MPC wallet's enhanced security model.
Step 1 : Create Session
- Create DuoSession if you haven't already.
Step 2 : Perform Sign
- Call duoSession.sign() which returns Result of Success with Signature ByteArray or Failure with exception.
Example
val messageHash = "e2a159d17b7bb714aed7675d7c7d394dec8d2e4337842848104694bf89c71c03"
// Sign the message hash with the key addressed by keyId (returned from keygen/import).
suspend fun performSignature(keyId: String, duoSession: DuoSession): ByteArray {
return withContext(Dispatchers.IO) {
duoSession.sign(
keyId = keyId,
message = messageHash,
derivationPath = "m" // This is the default; use your desired path, e.g. "m/1/2"
).getOrThrow()
}
}
keyIdaddresses the keyshare to sign with (returned by keygen/import); the SDK reads the share from your StorageClient.messageHashis the hash of the message to be signed as a hexString.- duoSession.sign() performs message exchange between mobile and server to generate a ECDSA/EdDSA signature.
- Result of duoSession.sign() could be a
SuccesswithByteArray(ECDSA/EdDSA signature) ofmessageHash, corresponding to the public key (or address) of the wallet orFailurewithException.
This distributed signing process allows for secure transaction authorization while preserving the key's distributed nature, exemplifying the MPC wallet's enhanced security model.
Step 1 : Create Session
- Create DuoSession if you haven't already.
Step 2 : Perform Sign
- Call duoSession.sign() with the
keyIdof the keyshare to sign with. It returns aResultofSuccesswith theSignaturebytes asDataorFailurewitherror.
Example
let MESSAGE_HASH = "53c48e76b32d4fb862249a81f0fc95da2d3b16bf53771cc03fd512ef5d4e6ed9"
// Sign the message hash with the key addressed by keyId (from keygen/import).
func performSignature(keyId: String, duoSession: DuoSession) async -> Data? {
let result = await duoSession.sign(
keyId: keyId, message: MESSAGE_HASH,
derivationPath: "m" // This is the default; use your desired path, e.g. "m/1/2"
)
// returns nil if the operation fails, or handle it however your flow needs
switch result {
case .success(let signatureBytes):
// do something with the signature bytes
Swift.print(signatureBytes)
return signatureBytes
case .failure(let error):
// show the error to the user or abort the process
Swift.print(error)
return nil
}
}
keyIdaddresses the client's keyshare in your storage client (returned bykeygen/import).messageHashis the hash of the message to be signed, passed as a hexString.- duoSession.sign() performs message exchange between mobile and server to generate a ECDSA/EdDSA signature.
- Result of duoSession.sign() could be a
SuccesswithData(ECDSA/EdDSA signature) ofmessageHash, corresponding to the public key (or address) of the wallet orFailurewitherror.
Handling the operation
This is an MPC operation, so it takes a few seconds (the app and the Duo Server exchange several messages). Show a non-blocking loading state while it runs, confirm on success, and offer a retry on failure. When something goes wrong, the SDK surfaces the error for you to handle:
| Error | What it means | How to handle |
|---|---|---|
| Keyshares not in sync, run reconcile | A previous operation didn't finish cleanly (for example the app was force-closed mid-operation) | Run reconcile, then retry |
| Server error | The server ended the session unexpectedly | Show the error and offer a retry |
| Connection / transport error | The server was unreachable or the connection dropped | Show the error and offer a retry |
All of these operations are safe to retry from the start.