Skip to main content
Version: 1.9

Aleo Signing with EdBLS12-377

The EdBls12377 class provides threshold signing for the Aleo network using the frost-ed-bls12-377 MPC protocol. As with every Sodot scheme, the private key is never reconstructed — it is split into t-of-n shares and signing happens collaboratively between a threshold of parties.

Aleo signing differs from the other schemes in two important ways:

  • A key produces an Aleo computeKey and address (rather than a single raw public key), and the same key can extract an Aleo view key for decrypting records.
  • Instead of signing an opaque message hash, you usually sign the fields of an Aleo execution request. Arbitrary byte messages are also supported through signBytes.
warning

Derivation is not available for ed-bls12-377 keys — each key corresponds to a single Aleo address.

Key Generation

Generate a threshold computeKey/address pair. As with the other schemes, the room must be created on your server using your API_KEY, and every party exchanges its keygenId over an authenticated channel before joining the keygen room.

import { EdBls12377 } from '@sodot/sodot-node-sdk';

const N = 3;
const T = 2;
const edBls12377 = new EdBls12377();
const API_KEY = 'MY_API_KEY';

// Your server side creates a room for 3 parties using its API_KEY
const keygenRoomUuid = await edBls12377.createRoom(N, API_KEY);
// All parties call initKeygen to get an EdBls12377InitKeygenResult, that contains a keygenId
const keygenInitResult = await edBls12377.initKeygen();
// All parties receive the keygenIds from all other parties over an authenticated channel
const keygenIds = [keygenId1, keygenId2];
// All parties join the keygen room
const keygenResult = await edBls12377.keygen(keygenRoomUuid, N, T, keygenInitResult, keygenIds);
// Discard the keygenInitResult object at this point
// keygenResult now holds { computeKey, address, secretShare }
API reference

Full details can be found here.

Signing an Aleo Request

signRequest signs the fields of an Aleo execution request with a threshold subset of parties. The payload is an EdBls12377RequestSignPayload:

  • function_id — the identifier of the function being executed.
  • is_root — whether this is the root request of the execution.
  • program_checksum — the program checksum, or null.
  • inputs — one EdBls12377RequestInput per request input.

Each input is one of two shapes. Non-record inputs use { type, index, fields }, where type is one of 'constant', 'public', 'private', or 'external_record'. Record inputs use { type: 'record', h, tag }. All request fields are passed as Uint8Array; the SDK hex-encodes them before calling the native bindings.

import { EdBls12377 } from '@sodot/sodot-node-sdk';

const T = 2;
const edBls12377 = new EdBls12377();
const API_KEY = 'MY_API_KEY';
const bytes32 = (value: number) => Uint8Array.from([value, ...new Uint8Array(31)]);

// A signing room is created on your server for T parties
const signingRoomUuid = await edBls12377.createRoom(T, API_KEY);

const payload = {
function_id: bytes32(0x29),
is_root: false,
program_checksum: bytes32(0x0b),
inputs: [{ type: 'constant', index: bytes32(0x00), fields: [bytes32(0x01)] }],
};

// T parties join the signing room
const signedRequest = await edBls12377.signRequest(signingRoomUuid, keygenResult, payload);
// signedRequest holds byte-encoded { signer, signature, tvk, gammas }

The result is an EdBls12377SignedRequest containing the byte-encoded signer, signature, tvk, and gammas. See the end-to-end example below for how these fields are fed into the Provable SDK to build and broadcast a transaction.

Signing Arbitrary Bytes

signBytes produces an Aleo Schnorr signature over a raw byte message, matching snarkVM's Signature::sign_bytes / Signature::verify_bytes. The signature is returned as a bech32 sign1... string (parseable by snarkVM's Signature::from_str) and verifies against the threshold key's address.

const message = new TextEncoder().encode('hello aleo');
const signature = await edBls12377.signBytes(signingRoomUuid, keygenResult, message);
// signature is a bech32-encoded Aleo Signature string, e.g. "sign1..."
note

The message is bit-packed into 252-bit field-element chunks before signing; messages up to ~128 KiB are supported. Signing fails before any MPC traffic if the message is too large, or if its packed form structurally matches an Aleo request preimage (a cross-protocol confusion guard).

View Keys and Key Export

Beyond exporting the full private key, ed-bls12-377 keys can also export an Aleo view key, which is used to decrypt records owned by the address. Both can be exported either to a single designated party through the relay server, or reconstructed locally from a threshold of shares (offline recovery).

// Online: export to a single designated party. The recipient first publishes its export ID.
const exportTo = await edBls12377.exportID(keygenResult);
const viewKey = await edBls12377.exportViewKey(viewKeyExportRoomUuid, keygenResult, exportTo);
const fullKey = await edBls12377.exportFullPrivateKey(fullKeyExportRoomUuid, keygenResult, exportTo);
// Every party except the recipient receives `undefined`.

// Offline: reconstruct locally from a threshold-sized array of key shares (no relay server).
const viewKeyOffline = await edBls12377.offlineExportViewKey([keygenResult1, keygenResult2]);
const fullKeyOffline = await edBls12377.offlineExportFullPrivateKey([keygenResult1, keygenResult2]);

Refresh, Resharing, and Import

ed-bls12-377 keys support the same lifecycle operations as the other schemes: refresh to rotate the secret shares without changing the computeKey/address, resharing to move the key to a new quorum, and importing an externally generated key.

warning

Resharing and key import are advanced features. Please consult with the Sodot team before using them — see the API reference for the full safety requirements.

End-to-End: Submitting an Aleo Transaction

This example shows the full flow of building, signing, and broadcasting an Aleo transaction using the threshold key together with the Provable SDK. The helper toRequestSignPayload maps the Provable SDK's signing inputs into the signRequest payload shape.

import { EdBls12377 } from '@sodot/sodot-node-sdk';
import {
Address,
ProgramManager,
ViewKey,
buildExecutionRequestFromExternallySignedData,
computeExternalSigningInputs,
} from '@provablehq/sdk/testnet.js';

const TEST_URL = 'us1-test.sodot.dev';
const API_KEY = 'MY_API_KEY';
const PROVER_URI = 'https://accelerate.provable.com';
const programName = 'credits.aleo';
const functionName = 'transfer_public_to_private';
const inputs = ['aleo1recipient...', '1u64'];
const inputTypes = ['address.private', 'u64.public'];
const seedHex = '...'; // 32-byte Aleo seed hex

function toRequestSignPayload(signingInputs) {
return {
function_id: signingInputs.functionId,
is_root: signingInputs.isRoot,
program_checksum: signingInputs.checksum ?? null,
inputs: signingInputs.requestInputs.map((requestInput) => {
if (requestInput.signingInputType === 'record') {
if (!requestInput.h || !requestInput.tag) {
throw new Error('Record input missing h/tag. Provide viewKey when computing record signing inputs.');
}
return { type: 'record', h: requestInput.h, tag: requestInput.tag };
}
return {
type: requestInput.signingInputType,
index: requestInput.index,
fields: requestInput.data,
};
}),
};
}

const edBls12377 = new EdBls12377(TEST_URL);

// Import an existing Aleo seed into a 1-of-1 threshold key (any t-of-n setup also works).
const importRoomUuid = await edBls12377.createRoom(1, API_KEY);
const importerInit = await edBls12377.initKeygen();
const imported = await edBls12377.importPrivateKeyImporter(importRoomUuid, 1, seedHex, importerInit, [], false);

// Derive the address from the exported view key.
const viewKey = await edBls12377.offlineExportViewKey([imported]);
const signer = Address.from_view_key(ViewKey.from_string(viewKey)).to_string();

// Compute the request signing inputs. `viewKey` is optional when there are no record inputs;
// providing it also derives `signer` and `skTag`.
const signingInputs = await computeExternalSigningInputs({
programName,
functionName,
inputs,
inputTypes,
isRoot: true,
checksum: null,
viewKey,
outputFormat: 'bytes',
});
if (!signingInputs.skTag) {
throw new Error('Expected skTag when viewKey is provided.');
}

// Threshold-sign the request.
const signingRoomUuid = await edBls12377.createRoom(1, API_KEY);
const signedRequest = await edBls12377.signRequest(signingRoomUuid, imported, toRequestSignPayload(signingInputs));

// Reconstruct the execution request from the externally signed data.
const executionRequest = buildExecutionRequestFromExternallySignedData({
programId: programName,
functionName,
inputs,
inputTypes,
signature: signedRequest.signature,
tvk: signedRequest.tvk,
signer: signedRequest.signer,
skTag: signingInputs.skTag,
});

const programManager = new ProgramManager();
programManager.networkClient.setProverUri(PROVER_URI);

const provingRequest = await programManager.provingRequest({
programName,
functionName,
inputs,
priorityFee: 0,
privateFee: false,
broadcast: true,
executionRequest,
});

const provingResult = await programManager.networkClient.submitProvingRequest({
provingRequest,
dpsPrivacy: true,
});

console.log('Signer:', signer);
console.log('Transaction ID:', provingResult.transaction.id);

buildExecutionRequestFromExternallySignedData supports three reconstruction strategies depending on your privacy model — { viewKey, gammas }, { recordViewKeys, gammas }, or { inputIds }. In all cases the payload sent to signRequest still uses { type: 'record', h, tag } for record inputs. See the EdBls12377 API reference for the full method documentation.