neco-secp 0.1.1

minimum dependency secp256k1 and Nostr signing core
Documentation
use crate::hex::hex_encode;
use crate::{
    EventId, SchnorrSignature, SecpError, SecretKey, SignedEvent, UnsignedEvent, XOnlyPublicKey,
};
use neco_json::JsonValue;
use neco_sha2::Sha256;

pub fn serialize_event(
    pubkey: &XOnlyPublicKey,
    event: &UnsignedEvent,
) -> Result<String, SecpError> {
    let tags = JsonValue::Array(
        event
            .tags
            .iter()
            .map(|tag| JsonValue::Array(tag.iter().map(|s| JsonValue::String(s.clone())).collect()))
            .collect(),
    );
    let payload = JsonValue::Array(vec![
        JsonValue::Number(0.0),
        JsonValue::String(hex_encode(&pubkey.to_bytes())),
        JsonValue::Number(event.created_at as f64),
        JsonValue::Number(event.kind as f64),
        tags,
        JsonValue::String(event.content.clone()),
    ]);
    let bytes = neco_json::encode(&payload).map_err(SecpError::from)?;
    String::from_utf8(bytes).map_err(|e| SecpError::Json(e.to_string()))
}

pub fn compute_event_id(
    pubkey: &XOnlyPublicKey,
    event: &UnsignedEvent,
) -> Result<EventId, SecpError> {
    let serialized = serialize_event(pubkey, event)?;
    let mut hasher = Sha256::new();
    hasher.update(serialized.as_bytes());
    let mut bytes = [0u8; 32];
    bytes.copy_from_slice(&hasher.finalize());
    Ok(EventId::from_bytes(bytes))
}

pub fn finalize_event(event: UnsignedEvent, secret: &SecretKey) -> Result<SignedEvent, SecpError> {
    let pubkey = secret.xonly_public_key()?;
    let id = compute_event_id(&pubkey, &event)?;
    let sig = secret.sign_schnorr_prehash(id.to_bytes())?;
    Ok(SignedEvent {
        id,
        pubkey,
        created_at: event.created_at,
        kind: event.kind,
        tags: event.tags,
        content: event.content,
        sig,
    })
}

/// 決定的な BIP-340 aux_rand (全ゼロ) で署名する `finalize_event` の亜種。
///
/// 同じ入力に対して常に同じ `SignedEvent` を返すため、fixture 照合やクロスレイヤーの
/// 等価性検証に使える。BIP-340 はフォールトアタック耐性のためフレッシュな aux_rand を
/// 推奨しており、通常の nostr クライアント実装ではランダム aux を使う
/// [`finalize_event`] を使用すること。
pub fn finalize_event_deterministic(
    event: UnsignedEvent,
    secret: &SecretKey,
) -> Result<SignedEvent, SecpError> {
    let pubkey = secret.xonly_public_key()?;
    let id = compute_event_id(&pubkey, &event)?;
    let sig = secret.sign_schnorr_prehash_deterministic(id.to_bytes())?;
    Ok(SignedEvent {
        id,
        pubkey,
        created_at: event.created_at,
        kind: event.kind,
        tags: event.tags,
        content: event.content,
        sig,
    })
}

pub fn serialize_signed_event(event: &SignedEvent) -> Result<String, SecpError> {
    let tags = JsonValue::Array(
        event
            .tags
            .iter()
            .map(|tag| JsonValue::Array(tag.iter().map(|s| JsonValue::String(s.clone())).collect()))
            .collect(),
    );
    let content_encoded =
        neco_json::encode(&JsonValue::String(event.content.clone())).map_err(SecpError::from)?;
    let content_str =
        String::from_utf8(content_encoded).map_err(|e| SecpError::Json(e.to_string()))?;
    let tags_encoded = neco_json::encode(&tags).map_err(SecpError::from)?;
    let tags_str = String::from_utf8(tags_encoded).map_err(|e| SecpError::Json(e.to_string()))?;
    Ok(format!(
        "{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":{},\"tags\":{},\"content\":{},\"sig\":\"{}\"}}",
        hex_encode(&event.id.to_bytes()),
        hex_encode(&event.pubkey.to_bytes()),
        event.created_at,
        event.kind,
        tags_str,
        content_str,
        hex_encode(&event.sig.to_bytes())
    ))
}

pub fn parse_signed_event(json: &str) -> Result<SignedEvent, SecpError> {
    let value = neco_json::parse(json.as_bytes()).map_err(SecpError::from)?;
    if !value.is_object() {
        return Err(SecpError::InvalidEvent(
            "signed event must be a JSON object",
        ));
    }

    let id = parse_hex32(required_string(&value, "id")?, "id")?;
    let pubkey = parse_hex32(required_string(&value, "pubkey")?, "pubkey")?;
    let created_at = required_u64(&value, "created_at")?;
    let kind = required_u32(&value, "kind")?;
    let tags = parse_tags(required_value(&value, "tags")?)?;
    let content = required_string(&value, "content")?.to_string();
    let sig = parse_hex64(required_string(&value, "sig")?, "sig")?;

    Ok(SignedEvent {
        id: EventId::from_bytes(id),
        pubkey: XOnlyPublicKey::from_bytes(pubkey)?,
        created_at,
        kind,
        tags,
        content,
        sig: SchnorrSignature { bytes: sig },
    })
}

pub fn verify_event(event: &SignedEvent) -> Result<(), SecpError> {
    let unsigned = UnsignedEvent {
        created_at: event.created_at,
        kind: event.kind,
        tags: event.tags.clone(),
        content: event.content.clone(),
    };
    let expected = compute_event_id(&event.pubkey, &unsigned)?;
    if expected != event.id {
        return Err(SecpError::InvalidEvent("event id mismatch"));
    }
    event
        .pubkey
        .verify_schnorr_prehash(event.id.to_bytes(), &event.sig)
}

fn required_value<'a>(
    object: &'a JsonValue,
    field: &'static str,
) -> Result<&'a JsonValue, SecpError> {
    object
        .get(field)
        .ok_or(SecpError::InvalidEvent(missing_field(field)))
}

fn required_string<'a>(object: &'a JsonValue, field: &'static str) -> Result<&'a str, SecpError> {
    required_value(object, field)?
        .as_str()
        .ok_or(SecpError::InvalidEvent(expected_field(field)))
}

fn required_u64(object: &JsonValue, field: &'static str) -> Result<u64, SecpError> {
    required_value(object, field)?
        .as_u64()
        .ok_or(SecpError::InvalidEvent(expected_field(field)))
}

fn required_u32(object: &JsonValue, field: &'static str) -> Result<u32, SecpError> {
    required_u64(object, field)?
        .try_into()
        .map_err(|_| SecpError::InvalidEvent(expected_field(field)))
}

fn parse_tags(value: &JsonValue) -> Result<Vec<Vec<String>>, SecpError> {
    let tags = value
        .as_array()
        .ok_or(SecpError::InvalidEvent("tags must be an array"))?;
    tags.iter()
        .map(|tag| {
            let tag = tag
                .as_array()
                .ok_or(SecpError::InvalidEvent("tag must be an array"))?;
            tag.iter()
                .map(|entry| {
                    entry
                        .as_str()
                        .map(ToOwned::to_owned)
                        .ok_or(SecpError::InvalidEvent("tag entry must be a string"))
                })
                .collect()
        })
        .collect()
}

fn parse_hex32(hex: &str, field: &'static str) -> Result<[u8; 32], SecpError> {
    let bytes = crate::hex::hex_decode(hex)
        .map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))?;
    bytes
        .try_into()
        .map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))
}

fn parse_hex64(hex: &str, field: &'static str) -> Result<[u8; 64], SecpError> {
    let bytes = crate::hex::hex_decode(hex)
        .map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))?;
    bytes
        .try_into()
        .map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))
}

fn missing_field(field: &'static str) -> &'static str {
    match field {
        "id" => "missing id",
        "pubkey" => "missing pubkey",
        "created_at" => "missing created_at",
        "kind" => "missing kind",
        "tags" => "missing tags",
        "content" => "missing content",
        "sig" => "missing sig",
        _ => "missing field",
    }
}

fn expected_field(field: &'static str) -> &'static str {
    match field {
        "id" => "id must be a string",
        "pubkey" => "pubkey must be a string",
        "created_at" => "created_at must be an integer",
        "kind" => "kind must be an integer",
        "content" => "content must be a string",
        "sig" => "sig must be a string",
        _ => "invalid field type",
    }
}

fn invalid_hex_field(field: &'static str) -> &'static str {
    match field {
        "id" => "id must be 64 hex characters",
        "pubkey" => "pubkey must be 64 hex characters",
        "sig" => "sig must be 128 hex characters",
        _ => "invalid hex field",
    }
}