feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
//! Redis ready-registry bootstrap for the relay mesh.
|
||||
//!
|
||||
//! The registry is only the way into the mesh. Entries are membership hints:
|
||||
//! they tell a fresh runtime which peer endpoints to dial, but never decide
|
||||
//! session ownership or takeover. The fenced Redis session directory remains
|
||||
//! the arbiter for session generations.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use nostr::secp256k1::schnorr::Signature;
|
||||
use nostr::secp256k1::{Message, XOnlyPublicKey};
|
||||
use nostr::PublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{MeshError, RuntimeId};
|
||||
|
||||
pub const READY_KEY_PREFIX: &str = "mesh:ready:";
|
||||
pub const DEFAULT_REGISTRY_REFRESH: Duration = Duration::from_secs(15);
|
||||
pub const REGISTRY_EXPIRY_MULTIPLIER: u64 = 3;
|
||||
pub const ATTESTATION_CONTEXT: &str = "buzz-relay-mesh-ready-v1";
|
||||
|
||||
/// Relay-key-signed binding for a boot-unique runtime endpoint pubkey.
|
||||
///
|
||||
/// The relay public key is the deployment Nostr/secp256k1 identity. It never
|
||||
/// becomes the mesh runtime id; it only signs this Redis-published binding so
|
||||
/// peers can reject unauthenticated endpoint ids before dialing/accepting.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeAttestation {
|
||||
/// Nostr/secp256k1 relay public key, hex encoded.
|
||||
pub relay_pubkey: String,
|
||||
/// Schnorr signature by `relay_pubkey` over [`attestation_preimage`].
|
||||
pub relay_sig: String,
|
||||
}
|
||||
|
||||
impl RuntimeAttestation {
|
||||
pub fn new(relay_keys: &nostr::Keys, runtime_id: RuntimeId) -> Self {
|
||||
let relay_pubkey = relay_keys.public_key().to_hex();
|
||||
let message = attestation_message(runtime_id, &relay_pubkey);
|
||||
let relay_sig = relay_keys.sign_schnorr(&message).to_string();
|
||||
Self {
|
||||
relay_pubkey,
|
||||
relay_sig,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self, runtime_id: RuntimeId) -> Result<(), MeshError> {
|
||||
verify_attestation(runtime_id, &self.relay_pubkey, &self.relay_sig)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_attestation(
|
||||
runtime_id: RuntimeId,
|
||||
relay_pubkey: &str,
|
||||
relay_sig: &str,
|
||||
) -> Result<(), MeshError> {
|
||||
let relay_pubkey = PublicKey::from_hex(relay_pubkey).map_err(|err| {
|
||||
MeshError::Transport(format!(
|
||||
"ready registry attestation invalid relay_pubkey: {err}"
|
||||
))
|
||||
})?;
|
||||
let xonly: XOnlyPublicKey = relay_pubkey.xonly().map_err(|err| {
|
||||
MeshError::Transport(format!(
|
||||
"ready registry attestation relay_pubkey xonly conversion failed: {err}"
|
||||
))
|
||||
})?;
|
||||
let sig = Signature::from_str(relay_sig).map_err(|err| {
|
||||
MeshError::Transport(format!(
|
||||
"ready registry attestation invalid relay_sig: {err}"
|
||||
))
|
||||
})?;
|
||||
let message = attestation_message(runtime_id, &relay_pubkey.to_hex());
|
||||
nostr::secp256k1::SECP256K1
|
||||
.verify_schnorr(&sig, &message, &xonly)
|
||||
.map_err(|err| {
|
||||
MeshError::Transport(format!(
|
||||
"ready registry attestation signature verification failed: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Stable signed payload. Keep this textual and versioned so transport/relay
|
||||
/// integration can reproduce it exactly without depending on JSON key order.
|
||||
pub fn attestation_preimage(runtime_id: RuntimeId, relay_pubkey: &str) -> String {
|
||||
format!(
|
||||
"{ATTESTATION_CONTEXT}\nruntime_pubkey={}\nrelay_pubkey={relay_pubkey}",
|
||||
runtime_id.to_hex()
|
||||
)
|
||||
}
|
||||
|
||||
fn attestation_message(runtime_id: RuntimeId, relay_pubkey: &str) -> Message {
|
||||
let digest = Sha256::digest(attestation_preimage(runtime_id, relay_pubkey).as_bytes());
|
||||
Message::from_digest(digest.into())
|
||||
}
|
||||
|
||||
/// Value stored at `mesh:ready:{runtime_id}`.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ReadyRecord {
|
||||
pub runtime_id: RuntimeId,
|
||||
/// Explicit duplicate of `runtime_id` for the contract record shape: this
|
||||
/// is the boot-unique ed25519/iroh endpoint pubkey being attested.
|
||||
pub runtime_pubkey: String,
|
||||
/// Nostr/secp256k1 relay public key that signs `runtime_pubkey`.
|
||||
pub relay_pubkey: String,
|
||||
/// Schnorr signature by `relay_pubkey` over [`attestation_preimage`].
|
||||
pub relay_sig: String,
|
||||
/// Dialable iroh endpoint addresses, serialized as strings so this layer
|
||||
/// does not depend on transport internals.
|
||||
pub endpoint_addrs: Vec<String>,
|
||||
pub proto_version: u16,
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
impl ReadyRecord {
|
||||
pub fn new(
|
||||
runtime_id: RuntimeId,
|
||||
relay_keys: &nostr::Keys,
|
||||
endpoint_addrs: Vec<String>,
|
||||
proto_version: u16,
|
||||
capabilities: Vec<String>,
|
||||
) -> Self {
|
||||
let attestation = RuntimeAttestation::new(relay_keys, runtime_id);
|
||||
Self {
|
||||
runtime_id,
|
||||
runtime_pubkey: runtime_id.to_hex(),
|
||||
relay_pubkey: attestation.relay_pubkey,
|
||||
relay_sig: attestation.relay_sig,
|
||||
endpoint_addrs,
|
||||
proto_version,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> String {
|
||||
ready_key(self.runtime_id)
|
||||
}
|
||||
|
||||
pub fn verify_attestation(&self) -> Result<(), MeshError> {
|
||||
if self.runtime_pubkey != self.runtime_id.to_hex() {
|
||||
return Err(MeshError::Transport(format!(
|
||||
"ready registry runtime_id/runtime_pubkey mismatch: {} != {}",
|
||||
self.runtime_id, self.runtime_pubkey
|
||||
)));
|
||||
}
|
||||
verify_attestation(self.runtime_id, &self.relay_pubkey, &self.relay_sig)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ready_key(runtime_id: RuntimeId) -> String {
|
||||
format!("{READY_KEY_PREFIX}{runtime_id}")
|
||||
}
|
||||
|
||||
pub fn expiry_for(refresh: Duration) -> Duration {
|
||||
refresh.saturating_mul(REGISTRY_EXPIRY_MULTIPLIER as u32)
|
||||
}
|
||||
|
||||
/// Redis-backed mesh bootstrap registry.
|
||||
#[derive(Clone)]
|
||||
pub struct ReadyRegistry {
|
||||
pool: deadpool_redis::Pool,
|
||||
refresh: Duration,
|
||||
}
|
||||
|
||||
impl ReadyRegistry {
|
||||
pub fn new(pool: deadpool_redis::Pool, refresh: Duration) -> Self {
|
||||
Self { pool, refresh }
|
||||
}
|
||||
|
||||
pub fn refresh_interval(&self) -> Duration {
|
||||
self.refresh
|
||||
}
|
||||
|
||||
pub fn expiry(&self) -> Duration {
|
||||
expiry_for(self.refresh)
|
||||
}
|
||||
|
||||
/// Publish this runtime as ready. Callers MUST only invoke this after the
|
||||
/// relay would pass readiness (shutdown=false, Postgres reachable, Redis
|
||||
/// reachable). This method deliberately has no hidden readiness probe so the
|
||||
/// rule stays explicit at the relay boundary.
|
||||
pub async fn publish_ready(&self, record: &ReadyRecord) -> Result<(), MeshError> {
|
||||
record.verify_attestation()?;
|
||||
let mut conn = self.conn().await?;
|
||||
let payload = serde_json::to_string(record)
|
||||
.map_err(|e| MeshError::Transport(format!("ready registry encode: {e}")))?;
|
||||
let ttl_secs = self.expiry().as_secs().max(1);
|
||||
redis::cmd("SET")
|
||||
.arg(record.key())
|
||||
.arg(payload)
|
||||
.arg("EX")
|
||||
.arg(ttl_secs)
|
||||
.query_async::<()>(&mut conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove this runtime on clean shutdown. A crash is handled by TTL expiry.
|
||||
pub async fn clear_ready(&self, runtime_id: RuntimeId) -> Result<(), MeshError> {
|
||||
let mut conn = self.conn().await?;
|
||||
redis::cmd("DEL")
|
||||
.arg(ready_key(runtime_id))
|
||||
.query_async::<()>(&mut conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scan all ready records. Malformed/stale/unauthenticated values are
|
||||
/// skipped with a warn: a bad registry entry must not prevent bootstrap
|
||||
/// from healthy peers.
|
||||
pub async fn scan_ready(&self) -> Result<Vec<ReadyRecord>, MeshError> {
|
||||
let mut conn = self.conn().await?;
|
||||
let mut cursor = 0u64;
|
||||
let mut out = Vec::new();
|
||||
|
||||
loop {
|
||||
let (next, keys): (u64, Vec<String>) = redis::cmd("SCAN")
|
||||
.arg(cursor)
|
||||
.arg("MATCH")
|
||||
.arg(format!("{READY_KEY_PREFIX}*"))
|
||||
.arg("COUNT")
|
||||
.arg(100u32)
|
||||
.query_async(&mut conn)
|
||||
.await?;
|
||||
|
||||
for key in keys {
|
||||
let raw: Option<String> =
|
||||
redis::cmd("GET").arg(&key).query_async(&mut conn).await?;
|
||||
let Some(raw) = raw else { continue };
|
||||
match serde_json::from_str::<ReadyRecord>(&raw) {
|
||||
Ok(record) if record.key() == key => match record.verify_attestation() {
|
||||
Ok(()) => out.push(record),
|
||||
Err(err) => tracing::warn!(
|
||||
key,
|
||||
runtime_id = %record.runtime_id,
|
||||
%err,
|
||||
"mesh ready registry attestation failed — skipping"
|
||||
),
|
||||
},
|
||||
Ok(record) => tracing::warn!(
|
||||
key,
|
||||
runtime_id = %record.runtime_id,
|
||||
"mesh ready registry key/runtime mismatch — skipping"
|
||||
),
|
||||
Err(err) => {
|
||||
tracing::warn!(key, %err, "mesh ready registry decode failed — skipping")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if next == 0 {
|
||||
break;
|
||||
}
|
||||
cursor = next;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn heartbeat(&self, record: ReadyRecord) -> ReadyHeartbeat {
|
||||
ReadyHeartbeat {
|
||||
registry: self.clone(),
|
||||
record,
|
||||
published: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn conn(&self) -> Result<deadpool_redis::Connection, MeshError> {
|
||||
self.pool
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| MeshError::Transport(format!("redis pool: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Readiness-gated registry heartbeat.
|
||||
///
|
||||
/// The relay owns the readiness predicate; this helper owns the edge behavior:
|
||||
/// publish only while ready, clear on ready→not-ready, and clear on shutdown.
|
||||
pub struct ReadyHeartbeat {
|
||||
registry: ReadyRegistry,
|
||||
record: ReadyRecord,
|
||||
published: bool,
|
||||
}
|
||||
|
||||
impl ReadyHeartbeat {
|
||||
pub fn record(&self) -> &ReadyRecord {
|
||||
&self.record
|
||||
}
|
||||
|
||||
pub fn published(&self) -> bool {
|
||||
self.published
|
||||
}
|
||||
|
||||
pub async fn tick(&mut self, ready: bool) -> Result<(), MeshError> {
|
||||
if ready {
|
||||
self.registry.publish_ready(&self.record).await?;
|
||||
self.published = true;
|
||||
} else if self.published {
|
||||
self.registry.clear_ready(self.record.runtime_id).await?;
|
||||
self.published = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> Result<(), MeshError> {
|
||||
if self.published {
|
||||
self.registry.clear_ready(self.record.runtime_id).await?;
|
||||
self.published = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rid(byte: u8) -> RuntimeId {
|
||||
RuntimeId([byte; 32])
|
||||
}
|
||||
|
||||
fn relay_keys() -> nostr::Keys {
|
||||
nostr::Keys::generate()
|
||||
}
|
||||
|
||||
fn ready_record(byte: u8) -> ReadyRecord {
|
||||
ReadyRecord::new(rid(byte), &relay_keys(), vec![], 1, vec![])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_key_is_stable_and_namespaced() {
|
||||
assert_eq!(
|
||||
ready_key(rid(0xAB)),
|
||||
format!("mesh:ready:{}", "ab".repeat(32))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expiry_is_three_refreshes() {
|
||||
assert_eq!(expiry_for(Duration::from_secs(15)), Duration::from_secs(45));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_starts_unpublished() {
|
||||
let pool = deadpool_redis::Config::from_url("redis://127.0.0.1:6379")
|
||||
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
||||
.unwrap();
|
||||
let registry = ReadyRegistry::new(pool, Duration::from_secs(15));
|
||||
let heartbeat = registry.heartbeat(ready_record(1));
|
||||
assert!(!heartbeat.published());
|
||||
assert_eq!(heartbeat.record().runtime_id, rid(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_record_roundtrips_json() {
|
||||
let record = ReadyRecord::new(
|
||||
rid(7),
|
||||
&relay_keys(),
|
||||
vec!["127.0.0.1:3478".to_string()],
|
||||
1,
|
||||
vec!["realtime-media".to_string()],
|
||||
);
|
||||
let raw = serde_json::to_string(&record).unwrap();
|
||||
assert_eq!(serde_json::from_str::<ReadyRecord>(&raw).unwrap(), record);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_record_attestation_verifies_and_binds_runtime_pubkey() {
|
||||
let record = ready_record(9);
|
||||
record.verify_attestation().unwrap();
|
||||
|
||||
let mut tampered = record.clone();
|
||||
tampered.runtime_pubkey = rid(10).to_hex();
|
||||
assert!(tampered.verify_attestation().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_rejects_signature_for_other_runtime() {
|
||||
let mut record = ready_record(11);
|
||||
record.runtime_id = rid(12);
|
||||
record.runtime_pubkey = rid(12).to_hex();
|
||||
assert!(record.verify_attestation().is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user