mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user