refactor(relay): deserialize revocation pubkeys with sdk

Signed-off-by: jm <jm@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
jm
2026-09-21 21:37:22 +00:00
parent ecfe1739e8
commit 0cbfe3eead
2 changed files with 34 additions and 28 deletions

View File

@@ -13,7 +13,7 @@ use axum::{
response::Json,
};
use chrono::{DateTime, SecondsFormat, Utc};
use nostr::PublicKey;
use nostr::{secp256k1::XOnlyPublicKey, PublicKey};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
@@ -34,7 +34,7 @@ struct RevocationNotificationRequest {
id: String,
#[serde(rename = "type")]
notification_type: String,
target_pubkey: String,
target_pubkey: XOnlyPublicKey,
occurred_at: String,
}
@@ -68,7 +68,7 @@ fn validate_notification(
return Err("id must be a canonical UUID");
}
let target_pubkey = parse_target_pubkey(&request.target_pubkey)?;
let target_pubkey = PublicKey::from(request.target_pubkey);
let occurred_at = validate_occurred_at(&request.occurred_at)?;
Ok(ValidatedRevocationNotification {
@@ -78,18 +78,6 @@ fn validate_notification(
})
}
fn parse_target_pubkey(value: &str) -> Result<PublicKey, &'static str> {
let pubkey = PublicKey::from_hex(value)
.map_err(|_| "target_pubkey must be 64 lowercase hex characters")?;
if pubkey.to_hex() != value {
return Err("target_pubkey must be 64 lowercase hex characters");
}
pubkey
.xonly()
.map_err(|_| "target_pubkey is not a valid x-only secp256k1 public key")?;
Ok(pubkey)
}
fn validate_occurred_at(value: &str) -> Result<DateTime<Utc>, &'static str> {
if value.len() > MAX_OCCURRED_AT_BYTES || !value.ends_with('Z') {
return Err("occurred_at must be an RFC3339 UTC timestamp ending in Z");
@@ -165,11 +153,17 @@ mod tests {
version: 1,
id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
notification_type: "identity.revoked".to_string(),
target_pubkey: VALID_PUBKEY.to_string(),
target_pubkey: VALID_PUBKEY.parse().expect("valid x-only public key"),
occurred_at: "2026-09-21T12:34:56Z".to_string(),
}
}
fn request_json(target_pubkey: &str) -> String {
format!(
r#"{{"version":1,"id":"550e8400-e29b-41d4-a716-446655440000","type":"identity.revoked","target_pubkey":"{target_pubkey}","occurred_at":"2026-09-21T12:34:56Z"}}"#
)
}
#[test]
fn exact_contract_is_valid() {
let notification = validate_notification(valid_request()).expect("valid notification");
@@ -204,14 +198,24 @@ mod tests {
let mut noncanonical_id = valid_request();
noncanonical_id.id = "550E8400-E29B-41D4-A716-446655440000".to_string();
assert!(validate_notification(noncanonical_id).is_err());
}
let mut uppercase_key = valid_request();
uppercase_key.target_pubkey = VALID_PUBKEY.to_uppercase();
assert!(validate_notification(uppercase_key).is_err());
#[test]
fn target_pubkey_deserialization_uses_sdk_hex_type() {
let uppercase = VALID_PUBKEY.to_uppercase();
let request =
serde_json::from_str::<RevocationNotificationRequest>(&request_json(&uppercase))
.expect("uppercase hex public key");
let notification = validate_notification(request).expect("valid notification");
assert_eq!(notification.target_pubkey.to_hex(), VALID_PUBKEY);
let mut invalid_point = valid_request();
invalid_point.target_pubkey = "0".repeat(64);
assert!(validate_notification(invalid_point).is_err());
let invalid_point = "0".repeat(64);
for invalid in ["not-hex", invalid_point.as_str()] {
assert!(
serde_json::from_str::<RevocationNotificationRequest>(&request_json(invalid))
.is_err()
);
}
}
#[test]

View File

@@ -14,7 +14,7 @@ Authorization: Nostr <base64 signed kind-27235 event>
"version": 1,
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "identity.revoked",
"target_pubkey": "<64 lowercase hex characters>",
"target_pubkey": "<64 hex characters>",
"occurred_at": "2026-09-21T12:34:56Z"
}
```
@@ -28,11 +28,13 @@ accepts at most 4 KiB.
The JSON contract is strict: unknown fields are rejected; `version` and `type`
have the fixed values above; `id` is a canonical hyphenated UUID;
`target_pubkey` is a valid lowercase x-only secp256k1 public key; and
`occurred_at` is an RFC3339 UTC timestamp ending in `Z`. Fractional seconds are
accepted. An equivalent `+00:00` suffix is not accepted because the wire
contract requires `Z`. The occurrence time is informational and has no recency
requirement; NIP-98 independently supplies authentication freshness.
`target_pubkey` is a valid 64-character hexadecimal x-only secp256k1 public
key; and `occurred_at` is an RFC3339 UTC timestamp ending in `Z`. Public-key
hex is accepted in either letter case and normalized to lowercase in logs.
Fractional seconds are accepted. An equivalent `+00:00` suffix is not accepted
because the wire contract requires `Z`. The occurrence time is informational
and has no recency requirement; NIP-98 independently supplies authentication
freshness.
Success returns: