Tinylayer home · Enclave guide
Complete statechain signer source
This page renders the exact enclave/src/lib.rs compiled by the repository.
It contains the global BIP340 template signer, opaque lazy activation,
capability compare-and-swap, exact retry cache, and Bitcoin-free HTTP workload.
The binary’s 13-line listener is
enclave/src/main.rs.
Tests are kept outside the listing.
//! Rollback-protected BIP448 statechain linearizer.
#![forbid(unsafe_code)]
use std::collections::HashMap;
use secp256k1::{Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey, rand, schnorr::Signature};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
pub const PROTOCOL_VERSION: u32 = 2;
pub const INITIAL_STATE_NUMBER: u64 = 1;
pub type CoinId = [u8; 32];
pub type Capability = [u8; 32];
pub type TemplateHash = [u8; 32];
pub type Enclave = Signer<21_000>;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ActivationRequest {
pub coin_id: CoinId,
pub initial_capability_hash: [u8; 32],
pub initial_template_hash: TemplateHash,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SignRequest {
pub coin_id: CoinId,
pub current_capability: Capability,
pub next_capability_hash: [u8; 32],
pub next_state_number: u64,
pub template_hash: TemplateHash,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SignResponse {
pub state_number: u64,
pub template_hash: TemplateHash,
pub signature: Signature,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ServiceInfo {
pub protocol_version: u32,
pub signing_pubkey: XOnlyPublicKey,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoinStatus {
pub coin_id: CoinId,
pub signing_pubkey: XOnlyPublicKey,
pub capability_hash: [u8; 32],
pub state_number: u64,
pub template_hash: TemplateHash,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "method", content = "params")]
pub enum Request {
Info,
Activate(ActivationRequest),
Status { coin_id: CoinId },
Sign(SignRequest),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "method", content = "result")]
pub enum Response {
Info(ServiceInfo),
Status(CoinStatus),
Signature(SignResponse),
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum Error {
#[error("coin is not activated")]
UnknownCoin,
#[error("activation conflicts with existing coin state")]
ActivationConflict,
#[error("enclave coin capacity is exhausted")]
CapacityReached,
#[error("current capability is stale")]
Unauthorized,
#[error("next capability is unchanged")]
UnchangedCapability,
#[error("state number must increase by exactly one")]
InvalidStateNumber,
#[error("next template is unchanged")]
UnchangedTemplate,
#[error("state number is exhausted")]
StateNumberOverflow,
}
pub fn capability_hash(capability: &Capability) -> [u8; 32] {
tagged_hash(b"Tinylayer/Capability/v2", &[capability])
}
fn tagged_hash(tag: &[u8], parts: &[&[u8]]) -> [u8; 32] {
let tag_hash = Sha256::digest(tag);
let mut hash = Sha256::new();
hash.update(tag_hash);
hash.update(tag_hash);
parts.iter().for_each(|part| hash.update(part));
hash.finalize().into()
}
pub struct Signer<const LIMIT: usize> {
signing_key: SecretKey,
coins: HashMap<CoinId, Coin>,
}
struct Coin {
capability_hash: [u8; 32],
state_number: u64,
template_hash: TemplateHash,
activation: ActivationRequest,
last: Option<(SignRequest, SignResponse)>,
}
impl<const LIMIT: usize> Signer<LIMIT> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::with_signing_key(SecretKey::new(&mut rand::thread_rng()))
}
pub fn with_signing_key(signing_key: SecretKey) -> Self {
Self {
signing_key,
coins: HashMap::new(),
}
}
pub fn handle(&mut self, request: Request) -> Result<Response, Error> {
match request {
Request::Info => Ok(Response::Info(self.info())),
Request::Activate(request) => self.activate(request).map(Response::Status),
Request::Status { coin_id } => self.status(coin_id).map(Response::Status),
Request::Sign(request) => self.sign(request).map(Response::Signature),
}
}
pub fn info(&self) -> ServiceInfo {
ServiceInfo {
protocol_version: PROTOCOL_VERSION,
signing_pubkey: self.signing_pubkey(),
}
}
pub fn activate(&mut self, request: ActivationRequest) -> Result<CoinStatus, Error> {
if let Some(coin) = self.coins.get(&request.coin_id) {
if coin.activation == request {
return Ok(coin.status(request.coin_id, self.signing_pubkey()));
}
return Err(Error::ActivationConflict);
}
if self.coins.len() >= LIMIT {
return Err(Error::CapacityReached);
}
let request_coin_id = request.coin_id;
let coin = Coin {
capability_hash: request.initial_capability_hash,
state_number: INITIAL_STATE_NUMBER,
template_hash: request.initial_template_hash,
activation: request,
last: None,
};
let status = coin.status(request_coin_id, self.signing_pubkey());
self.coins.insert(request_coin_id, coin);
Ok(status)
}
pub fn status(&self, coin_id: CoinId) -> Result<CoinStatus, Error> {
self.coins
.get(&coin_id)
.map(|coin| coin.status(coin_id, self.signing_pubkey()))
.ok_or(Error::UnknownCoin)
}
pub fn sign(&mut self, request: SignRequest) -> Result<SignResponse, Error> {
let coin = self
.coins
.get_mut(&request.coin_id)
.ok_or(Error::UnknownCoin)?;
if let Some((last, response)) = &coin.last
&& last == &request
{
return Ok(*response);
}
let current_capability_hash = capability_hash(&request.current_capability);
if current_capability_hash != coin.capability_hash {
return Err(Error::Unauthorized);
}
if request.next_capability_hash == current_capability_hash {
return Err(Error::UnchangedCapability);
}
let next_state_number = coin
.state_number
.checked_add(1)
.ok_or(Error::StateNumberOverflow)?;
if request.next_state_number != next_state_number {
return Err(Error::InvalidStateNumber);
}
if request.template_hash == coin.template_hash {
return Err(Error::UnchangedTemplate);
}
let keypair = Keypair::from_secret_key(&Secp256k1::new(), &self.signing_key);
let response = SignResponse {
state_number: request.next_state_number,
template_hash: request.template_hash,
signature: Secp256k1::new()
.sign_schnorr_no_aux_rand(&Message::from_digest(request.template_hash), &keypair),
};
(
coin.capability_hash,
coin.state_number,
coin.template_hash,
coin.last,
) = (
request.next_capability_hash,
next_state_number,
request.template_hash,
Some((request, response)),
);
Ok(response)
}
pub fn coin_count(&self) -> usize {
self.coins.len()
}
fn signing_pubkey(&self) -> XOnlyPublicKey {
self.signing_key.x_only_public_key(&Secp256k1::new()).0
}
}
impl Coin {
fn status(&self, coin_id: CoinId, signing_pubkey: XOnlyPublicKey) -> CoinStatus {
CoinStatus {
coin_id,
signing_pubkey,
capability_hash: self.capability_hash,
state_number: self.state_number,
template_hash: self.template_hash,
}
}
}
#[cfg(feature = "workload")]
pub mod workload {
use std::sync::Arc;
use axum::{
Json, Router,
extract::{DefaultBodyLimit, State},
http::StatusCode,
routing::{get, post},
};
use tokio::sync::Mutex;
use crate::{Enclave, Request, Response};
pub fn router(enclave: Enclave) -> Router {
Router::new()
.route("/health", get(|| async { "ok" }))
.route("/v2", post(handle))
.layer(DefaultBodyLimit::max(4 * 1024))
.with_state(Arc::new(Mutex::new(enclave)))
}
async fn handle(
State(enclave): State<Arc<Mutex<Enclave>>>,
Json(request): Json<Request>,
) -> Result<Json<Response>, (StatusCode, String)> {
enclave
.lock()
.await
.handle(request)
.map(Json)
.map_err(|error| (StatusCode::CONFLICT, error.to_string()))
}
}