sdk_relay/spec-technique.md
Nicolas Cantu e506f55e08 feat: Ajout du support des relais externes via external_nodes.conf
- Ajout de la fonction load_external_config() pour charger la configuration externe
- Ajout de la fonction parse_external_config() pour parser le fichier TOML
- Modification de discover_relays() pour inclure les relais externes
- Support des relais avec ancienne version (0.9.0) et capacités limitées
- Ajout du fichier EXEMPLES_PRATIQUES.md avec exemples d'utilisation
- Mise à jour de la documentation technique
2025-09-02 13:22:02 +02:00

30 KiB

Spécification Technique - sdk_relay

Vue d'ensemble

sdk_relay est un service Rust asynchrone qui implémente un relais pour les Silent Payments. Il agit comme un pont entre les applications clientes et l'infrastructure Bitcoin, fournissant une interface WebSocket pour la communication en temps réel.

Architecture du code

Structure des modules

src/
├── main.rs          # Point d'entrée, orchestration principale
├── config.rs        # Gestion de la configuration
├── daemon.rs        # Interface Bitcoin Core RPC
├── scan.rs          # Scan des blocs et transactions
├── message.rs       # Gestion des messages WebSocket
├── commit.rs        # Gestion des commits et membres
└── faucet.rs        # Service de faucet (développement)

Analyse détaillée des composants

1. Module main.rs - Orchestration principale

Variables globales statiques

// Gestion des connexions WebSocket
pub(crate) static PEERMAP: OnceLock<PeerMap> = OnceLock::new();
type PeerMap = Mutex<HashMap<SocketAddr, Tx>>;
type Tx = UnboundedSender<Message>;

// Interface Bitcoin Core RPC
pub(crate) static DAEMON: OnceLock<Mutex<Box<dyn RpcCall>>> = OnceLock::new();

// État de la blockchain
static CHAIN_TIP: AtomicU32 = AtomicU32::new(0);

// Protection contre les doubles dépenses
pub static FREEZED_UTXOS: OnceLock<Mutex<HashSet<OutPoint>>> = OnceLock::new();

// Wallet Silent Payments
pub static WALLET: OnceLock<Mutex<SpWallet>> = OnceLock::new();

// Stockage persistant
pub static STORAGE: OnceLock<Mutex<DiskStorage>> = OnceLock::new();

Fonction main() - Flux d'initialisation

  1. Chargement de la configuration

    let config = Config::read_from_file(".conf")?;
    
  2. Connexion à Bitcoin Core avec retry logic

    let mut retry_count = 0;
    const MAX_RETRIES: u32 = 5;
    const RETRY_DELAY_MS: u64 = 2000;
    
    let daemon = loop {
        match Daemon::connect(
            config.core_wallet.clone(),
            config.core_url.clone(),
            config.network,
            cookie_path,
        ) {
            Ok(daemon) => break daemon,
            Err(e) => {
                retry_count += 1;
                if retry_count >= MAX_RETRIES {
                    return Err(e.context("Failed to connect to Bitcoin Core"));
                }
                std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS * retry_count as u64));
            }
        }
    };
    
  3. Initialisation du wallet Silent Payments

    let sp_wallet = match wallet_file.load() {
        Ok(wallet_data) => {
            serde_json::from_value(wallet_data)?
        }
        Err(_) => {
            // Création d'un nouveau wallet
            let new_client = SpClient::new(
                SecretKey::new(&mut rng),
                SpendKey::Secret(SecretKey::new(&mut rng)),
                config.network,
            )?;
            let mut sp_wallet = SpWallet::new(new_client);
            sp_wallet.set_birthday(current_tip);
            sp_wallet.set_last_scan(current_tip);
            sp_wallet
        }
    };
    
  4. Chargement de l'état persistant

    let cached_processes: HashMap<OutPoint, Process> = match processes_file.load() {
        Ok(processes) => {
            let deserialized: OutPointProcessMap = serde_json::from_value(processes)?;
            deserialized.0
        }
        Err(_) => HashMap::new()
    };
    
  5. Initialisation des handlers asynchrones

    // Handlers de mise à jour
    tokio::spawn(handle_scan_updates(scan_rx));
    tokio::spawn(handle_state_updates(state_rx));
    
    // Handler ZMQ
    tokio::spawn(async move {
        handle_zmq(zmq_url, blindbit_url).await;
    });
    
  6. Démarrage du serveur WebSocket

    let listener = TcpListener::bind(config.ws_url).await?;
    
    while let Ok((stream, addr)) = listener.accept().await {
        tokio::spawn(handle_connection(stream, addr, our_sp_address));
    }
    

2. Module config.rs - Gestion de la configuration

Structure Config

#[derive(Debug)]
pub struct Config {
    pub core_url: String,           // URL RPC Bitcoin Core
    pub core_wallet: Option<String>, // Nom du wallet
    pub ws_url: String,             // URL du serveur WebSocket
    pub wallet_name: String,        // Nom du fichier wallet
    pub network: Network,           // Réseau Bitcoin (mainnet/signet)
    pub blindbit_url: String,       // URL du service Blindbit
    pub zmq_url: String,            // URL ZMQ Bitcoin Core
    pub data_dir: String,           // Répertoire de données
    pub cookie_path: Option<String>, // Chemin du cookie d'authentification
}

Parsing du fichier de configuration

impl Config {
    pub fn read_from_file(filename: &str) -> Result<Self> {
        let mut file_content = HashMap::new();

        // Lecture ligne par ligne
        for line in reader.lines() {
            if let Ok(l) = line {
                // Ignore les commentaires et lignes vides
                if l.starts_with('#') || l.trim().is_empty() {
                    continue;
                }

                // Parse key=value
                if let Some((k, v)) = l.split_once('=') {
                    file_content.insert(k.to_owned(), v.trim_matches('\"').to_owned());
                }
            }
        }

        // Construction de la config avec validation
        let config = Config {
            core_url: file_content.remove("core_url")
                .ok_or(Error::msg("No \"core_url\""))?,
            // ... autres champs
        };

        Ok(config)
    }
}

3. Module daemon.rs - Interface Bitcoin Core RPC

Trait RpcCall

pub(crate) trait RpcCall: Send + Sync + std::fmt::Debug {
    fn get_blockchain_info(&self) -> Result<BlockchainInfo>;
    fn get_block_count(&self) -> Result<u64>;
    fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
    fn get_block(&self, block_hash: BlockHash) -> Result<Block>;
    fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, BlockFilter)>;
    fn get_mempool_entries(&self, txids: &[Txid]) -> Result<HashMap<Txid, MempoolEntry>>;
    fn broadcast(&self, tx: &Transaction) -> Result<Txid>;
    fn test_mempool_accept(&self, tx: &Transaction) -> Result<TestMempoolAcceptResult>;
    // ... autres méthodes
}

Implémentation Daemon

pub struct Daemon {
    client: Client,
    network: Network,
}

impl Daemon {
    pub fn connect(
        rpcwallet: Option<String>,
        rpc_url: String,
        network: Network,
        cookie_path: Option<PathBuf>,
    ) -> Result<Box<dyn RpcCall>> {
        // Construction de l'URL RPC
        let mut final_url = rpc_url;
        if let Some(ref wallet) = rpcwallet {
            final_url.push_str("/wallet/");
            final_url.push_str(wallet);
        }

        // Configuration de l'authentification
        let cookie_path = match cookie_path {
            Some(path) => path,
            None => {
                // Chemin par défaut
                let home = env::var("HOME")?;
                let mut default_path = PathBuf::from_str(&home)?;
                default_path.push(".bitcoin");
                default_path.push(network.to_core_arg());
                default_path.push(".cookie");
                default_path
            }
        };

        // Création du client RPC
        let daemon_auth = SensitiveAuth(Auth::CookieFile(cookie_path));
        let builder = jsonrpc::simple_http::SimpleHttpTransport::builder()
            .url(&final_url)?
            .timeout(Duration::from_secs(30));

        let client = Client::from_jsonrpc(jsonrpc::Client::with_transport(
            builder.build(),
        ));

        Ok(Box::new(Daemon { client, network }))
    }
}

4. Module scan.rs - Scan des blocs et transactions

Fonction scan_blocks

pub async fn scan_blocks(
    blocks_to_scan: u32,
    blindbit_url: &str,
) -> Result<()> {
    let daemon = DAEMON.get().unwrap().lock_anyhow()?;
    let current_tip = daemon.get_block_count()?;

    // Calcul de la hauteur de départ
    let start_height = current_tip - blocks_to_scan as u64;

    // Récupération des filtres depuis Blindbit
    let filters = get_filters_from_blindbit(blindbit_url, start_height, current_tip).await?;

    // Scan de chaque bloc
    for (height, filter) in filters {
        let block_hash = daemon.get_block_hash(height)?;
        let block = daemon.get_block(block_hash)?;

        // Analyse des transactions du bloc
        for tx in block.txdata {
            if let Some(sp_outputs) = check_transaction_alone(&tx, &filter) {
                // Traitement des outputs Silent Payments détectés
                process_sp_outputs(sp_outputs, &tx, height).await?;
            }
        }
    }

    Ok(())
}

Fonction check_transaction_alone

fn check_transaction_alone(
    tx: &Transaction,
    filter: &BlockFilter,
) -> Option<Vec<SpOutput>> {
    let mut sp_outputs = Vec::new();

    // Vérification de chaque output
    for (vout, output) in tx.output.iter().enumerate() {
        // Test du filtre Blindbit
        if filter.test_output(&output.script_pubkey) {
            // Tentative de décodage Silent Payment
            if let Some(sp_output) = decode_sp_output(output, vout as u32) {
                sp_outputs.push(sp_output);
            }
        }
    }

    if sp_outputs.is_empty() {
        None
    } else {
        Some(sp_outputs)
    }
}

5. Module message.rs - Gestion des messages WebSocket

Architecture des messages

Le système de messages de sdk_relay utilise une architecture en couches avec des enveloppes (Envelope) qui encapsulent différents types de messages selon leur flag.

Structure Envelope

#[derive(Debug, Serialize, Deserialize)]
pub struct Envelope {
    pub flag: AnkFlag,      // Type de message
    pub content: String,    // Contenu JSON du message
}

Types de messages (AnkFlag)

#[derive(Debug, Serialize, Deserialize, Tsify)]
pub enum AnkFlag {
    NewTx,      // 0 - Transaction Bitcoin
    Faucet,     // 1 - Service de faucet
    Cipher,     // 2 - Messages chiffrés
    Commit,     // 3 - Messages de commit
    Handshake,  // 4 - Poignée de main initiale
    Sync,       // 5 - Synchronisation (non implémenté)
    Unknown,    // - Messages inconnus
}

1. Message Handshake (AnkFlag::Handshake)

Structure :

#[derive(Debug, Serialize, Deserialize, Tsify)]
pub struct HandshakeMessage {
    pub sp_address: String,                    // Adresse Silent Payment du client
    pub peers_list: OutPointMemberMap,         // Liste des pairs connectés
    pub processes_list: OutPointProcessMap,    // Liste des processus actifs
    pub chain_tip: u32,                        // Hauteur actuelle de la blockchain
}

Champs détaillés :

  • sp_address : Adresse Silent Payment du client qui se connecte
  • peers_list : Map des pairs connectés (OutPoint → Member)
  • processes_list : Map des processus actifs (OutPoint → Process)
  • chain_tip : Hauteur de la blockchain pour synchronisation

Utilisation : Échange initial lors de la connexion WebSocket pour synchroniser l'état

2. Message NewTx (AnkFlag::NewTx)

Structure :

#[derive(Debug, PartialEq, Serialize, Deserialize, Tsify)]
pub struct NewTxMessage {
    pub transaction: String,           // Transaction Bitcoin en hex
    pub tweak_data: Option<String>,   // Données de tweak Silent Payment
    pub error: Option<AnkError>,      // Erreur éventuelle
}

Champs détaillés :

  • transaction : Transaction Bitcoin sérialisée en format hexadécimal
  • tweak_data : Données de tweak pour les Silent Payments (optionnel)
  • error : Erreur éventuelle lors du traitement

Utilisation : Diffusion de nouvelles transactions Bitcoin à tous les pairs

3. Message Commit (AnkFlag::Commit)

Structure :

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Tsify)]
pub struct CommitMessage {
    pub process_id: OutPoint,              // Identifiant du processus
    pub pcd_commitment: PcdCommitments,    // Engagements PCD
    pub roles: Roles,                      // Rôles des participants
    pub public_data: Pcd,                  // Données publiques
    pub validation_tokens: Vec<Proof>,     // Tokens de validation
    pub error: Option<AnkError>,           // Erreur éventuelle
}

Champs détaillés :

  • process_id : Identifiant unique du processus (OutPoint)
  • pcd_commitment : Map des engagements PCD (champ → hash)
  • roles : Définition des rôles et permissions
  • public_data : Données publiques du processus
  • validation_tokens : Preuves cryptographiques de validation
  • error : Erreur éventuelle lors du traitement

Utilisation : Gestion des processus collaboratifs et de leurs états

4. Message Faucet (AnkFlag::Faucet)

Structure :

#[derive(Debug, Serialize, Deserialize, Tsify)]
pub struct FaucetMessage {
    pub sp_address: String,        // Adresse Silent Payment
    pub commitment: String,        // Engagement cryptographique
    pub error: Option<AnkError>,   // Erreur éventuelle
}

Champs détaillés :

  • sp_address : Adresse Silent Payment du demandeur
  • commitment : Engagement cryptographique pour éviter les abus
  • error : Erreur éventuelle lors du traitement

Utilisation : Service de faucet pour obtenir des fonds de test

5. Message Cipher (AnkFlag::Cipher)

Structure : Le contenu est une chaîne JSON contenant des données chiffrées.

Champs détaillés :

  • content : Données chiffrées en format hexadécimal

Utilisation : Communication chiffrée entre pairs pour les messages privés

6. Message Sync (AnkFlag::Sync)

Structure : Non implémentée actuellement (todo!)

Utilisation : Synchronisation avancée entre pairs (futur)

7. Message Unknown (AnkFlag::Unknown)

Structure : Messages de type inconnu

Utilisation : Gestion des messages non reconnus

Cache de messages

#[derive(Debug)]
pub(crate) struct MessageCache {
    store: Mutex<HashMap<String, Instant>>,
}

Fonctionnalités :

  • Déduplication : Évite le traitement multiple du même message
  • Expiration : Messages supprimés après 20 secondes
  • Nettoyage automatique : Toutes les 5 secondes

Types de broadcast

pub(crate) enum BroadcastType {
    Sender(SocketAddr),           // Envoi au seul expéditeur
    ExcludeSender(SocketAddr),    // Envoi à tous sauf l'expéditeur
    ToAll,                        // Envoi à tous les pairs
}

Types de données complexes

OutPointMemberMap et OutPointProcessMap
// Map des pairs connectés
pub type OutPointMemberMap = HashMap<OutPoint, Member>;

// Map des processus actifs
pub type OutPointProcessMap = HashMap<OutPoint, Process>;
Structure Member
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Member {
    pub outpoint: OutPoint,           // Identifiant unique du membre
    pub public_key: PublicKey,        // Clé publique du membre
    pub balance: Amount,              // Solde actuel
    pub last_commit: Option<Commit>,  // Dernier commit effectué
}
Structure Process
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Process {
    pub process_id: OutPoint,         // Identifiant du processus
    pub state_id: [u8; 32],          // ID de l'état actuel
    pub roles: Roles,                 // Rôles des participants
    pub public_data: Pcd,             // Données publiques
    pub validation_tokens: Vec<Proof>, // Tokens de validation
}
Structure Pcd (Process Control Data)
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Pcd {
    pub fields: HashMap<String, Field>,  // Champs de données
    pub validation_rules: Vec<ValidationRule>, // Règles de validation
}
Structure Roles
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Roles {
    pub role_definitions: HashMap<String, RoleDefinition>, // Définitions des rôles
}
Structure Proof
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Proof {
    signature: Signature,    // Signature Schnorr
    message: [u8; 32]       // Hash du message signé
}
Structure PcdCommitments
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct PcdCommitments {
    pub commitments: HashMap<String, [u8; 32]>, // Map champ → hash
}

Format des messages JSON

Exemple Handshake
{
  "flag": "Handshake",
  "content": "{\"sp_address\":\"sp1...\",\"peers_list\":{},\"processes_list\":{},\"chain_tip\":123456}"
}
Exemple NewTx
{
  "flag": "NewTx",
  "content": "{\"transaction\":\"02000000...\",\"tweak_data\":null,\"error\":null}"
}
Exemple Commit
{
  "flag": "Commit",
  "content": "{\"process_id\":\"...\",\"pcd_commitment\":{},\"roles\":{},\"public_data\":{},\"validation_tokens\":[],\"error\":null}"
}

Traitement des messages

pub fn process_message(raw_msg: &str, addr: SocketAddr) {
    // Vérification du cache pour éviter les doublons
    let cache = MESSAGECACHE.get().expect("Cache should be initialized");
    if cache.contains(raw_msg) {
        log::debug!("Message already processed, dropping");
        return;
    } else {
        cache.insert(raw_msg.to_owned());
    }

    // Parsing de l'enveloppe
    match serde_json::from_str::<Envelope>(raw_msg) {
        Ok(ank_msg) => match ank_msg.flag {
            AnkFlag::Faucet => process_faucet_message(ank_msg, addr),
            AnkFlag::NewTx => process_new_tx_message(ank_msg, addr),
            AnkFlag::Cipher => process_cipher_message(ank_msg, addr),
            AnkFlag::Commit => process_commit_message(ank_msg, addr),
            AnkFlag::Unknown => process_unknown_message(ank_msg, addr),
            AnkFlag::Sync => todo!(),
            AnkFlag::Handshake => log::debug!("Received init message from {}", addr),
        },
        Err(_) => log::error!("Failed to parse network message"),
    }
}

Handler de connexion WebSocket

async fn handle_connection(
    raw_stream: TcpStream,
    addr: SocketAddr,
    our_sp_address: SilentPaymentAddress,
) {
    // Upgrade vers WebSocket
    let ws_stream = match tokio_tungstenite::accept_async(raw_stream).await {
        Ok(stream) => stream,
        Err(e) => {
            log::error!("WebSocket handshake failed: {}", e);
            return;
        }
    };

    // Ajout à la map des peers
    let (tx, rx) = unbounded_channel();
    {
        let mut peer_map = PEERMAP.get().unwrap().lock_anyhow().unwrap();
        peer_map.insert(addr, tx);
    }

    // Boucle de traitement des messages
    let (mut ws_sender, mut ws_receiver) = ws_stream.split();

    // Handler des messages entrants
    let incoming = rx.map(Ok).forward(ws_sender);

    // Handler des messages sortants
    let outgoing = ws_receiver.try_for_each(|msg| async {
        match msg {
            Message::Text(text) => {
                let message: MessageType = serde_json::from_str(&text)?;
                process_message(message, addr).await?;
            }
            Message::Close(_) => return Ok(()),
            _ => {}
        }
        Ok(())
    });

    // Exécution concurrente
    pin_mut!(incoming, outgoing);
    future::select(incoming, outgoing).await;
}

Traitement des messages

pub async fn process_message(message: MessageType, addr: SocketAddr) -> Result<()> {
    match message {
        MessageType::Handshake(handshake) => {
            // Validation de la version et des capacités
            if handshake.version != "1.0" {
                return Err(Error::msg("Unsupported version"));
            }

            // Envoi de la confirmation
            let response = MessageType::Handshake(HandshakeMessage {
                version: "1.0".to_string(),
                capabilities: vec!["silent_payments".to_string(), "broadcast".to_string()],
            });

            broadcast_to_peer(addr, response).await?;
        }

        MessageType::NewTx(new_tx) => {
            // Validation et broadcast de la transaction
            let tx = deserialize::<Transaction>(&Vec::from_hex(&new_tx.transaction)?)?;

            let daemon = DAEMON.get().unwrap().lock_anyhow()?;
            daemon.test_mempool_accept(&tx)?;
            daemon.broadcast(&tx)?;

            // Notification aux autres peers
            broadcast_message(BroadcastType::NewTx(new_tx), Some(addr)).await?;
        }

        MessageType::Broadcast(broadcast) => {
            // Relay du message broadcast
            broadcast_message(BroadcastType::Custom(broadcast), Some(addr)).await?;
        }
    }

    Ok(())
}

6. Module commit.rs - Gestion des commits et membres

Structure des données

pub static MEMBERLIST: OnceLock<Mutex<HashMap<OutPoint, Member>>> = OnceLock::new();

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Member {
    pub outpoint: OutPoint,
    pub public_key: PublicKey,
    pub balance: Amount,
    pub last_commit: Option<Commit>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Commit {
    pub txid: Txid,
    pub height: u32,
    pub timestamp: u64,
    pub amount: Amount,
}

Gestion des membres

pub fn lock_members() -> Result<MutexGuard<'static, HashMap<OutPoint, Member>>> {
    MEMBERLIST
        .get_or_init(|| Mutex::new(HashMap::new()))
        .lock_anyhow()
}

pub fn add_member(outpoint: OutPoint, public_key: PublicKey) -> Result<()> {
    let mut members = lock_members()?;

    let member = Member {
        outpoint,
        public_key,
        balance: Amount::ZERO,
        last_commit: None,
    };

    members.insert(outpoint, member);
    Ok(())
}

pub fn update_member_balance(outpoint: OutPoint, amount: Amount) -> Result<()> {
    let mut members = lock_members()?;

    if let Some(member) = members.get_mut(&outpoint) {
        member.balance = amount;
    }

    Ok(())
}

7. Gestion de l'état persistant

Structure DiskStorage

#[derive(Debug)]
pub struct DiskStorage {
    pub wallet_file: StateFile,
    pub processes_file: StateFile,
    pub members_file: StateFile,
}

#[derive(Debug)]
pub struct StateFile {
    path: PathBuf,
}

Opérations de persistance

impl StateFile {
    fn save(&self, json: &Value) -> Result<()> {
        let mut f = fs::File::options()
            .write(true)
            .truncate(true)
            .open(&self.path)?;

        let stringified = serde_json::to_string(&json)?;
        f.write_all(stringified.as_bytes())?;
        Ok(())
    }

    fn load(&self) -> Result<Value> {
        let mut f = fs::File::open(&self.path)?;
        let mut content = vec![];
        f.read_to_end(&mut content)?;

        let res: Value = serde_json::from_slice(&content)?;
        Ok(res)
    }
}

Flux de données détaillé

1. Initialisation du service

main() → Config::read_from_file() → Daemon::connect() →
SpWallet::new() → load_persistent_state() →
spawn_handlers() → TcpListener::bind() → accept_loop()

2. Traitement d'une connexion WebSocket

TcpListener::accept() → handle_connection() →
tokio_tungstenite::accept_async() →
PEERMAP.insert() → message_loop()

3. Traitement d'un message

Message::Text() → serde_json::from_str() →
process_message() → match MessageType →
handle_specific_message() → broadcast_to_peers()

4. Scan des blocs

handle_zmq() → scan_blocks() →
get_filters_from_blindbit() →
check_transaction_alone() →
process_sp_outputs() → update_wallet()

5. Broadcast de transaction

NewTxMessage → deserialize() →
test_mempool_accept() → broadcast() →
broadcast_message() → send_to_all_peers()

Gestion des erreurs

Stratégies de retry

// Retry avec backoff exponentiel pour Bitcoin Core
let mut retry_count = 0;
const MAX_RETRIES: u32 = 5;
const RETRY_DELAY_MS: u64 = 2000;

loop {
    match operation() {
        Ok(result) => break result,
        Err(e) => {
            retry_count += 1;
            if retry_count >= MAX_RETRIES {
                return Err(e);
            }
            std::thread::sleep(std::time::Duration::from_millis(
                RETRY_DELAY_MS * retry_count as u64
            ));
        }
    }
}

Gestion des connexions WebSocket

// Nettoyage automatique des connexions fermées
tokio::spawn(MessageCache::clean_up());

// Gestion gracieuse de la fermeture
match ws_receiver.try_for_each(|msg| async {
    match msg {
        Message::Close(_) => return Ok(()),
        // ... autres cas
    }
}).await {
    Ok(_) => log::info!("WebSocket connection closed gracefully"),
    Err(e) => log::error!("WebSocket error: {}", e),
}

Optimisations de performance

1. Gestion asynchrone

  • tokio runtime : Gestion multi-thread des connexions
  • futures : Opérations non-bloquantes
  • channels : Communication inter-tasks

2. Pooling de connexions

// Réutilisation des connexions RPC
pub static DAEMON: OnceLock<Mutex<Box<dyn RpcCall>>> = OnceLock::new();

// Map des connexions WebSocket actives
pub static PEERMAP: OnceLock<PeerMap> = OnceLock::new();

3. Cache et persistance

// Cache des processus en mémoire
pub static CACHEDPROCESSES: OnceLock<Mutex<HashMap<OutPoint, Process>>> = OnceLock::new();

// Persistance automatique
impl Drop for DiskStorage {
    fn drop(&mut self) {
        // Sauvegarde automatique à la fermeture
        self.save_all().ok();
    }
}

Sécurité

1. Authentification

// Cookie Bitcoin Core sécurisé
let daemon_auth = SensitiveAuth(Auth::CookieFile(cookie_path));

// Permissions de fichiers restrictives
fs::set_permissions(&cookie_path, fs::Permissions::from_mode(0o600))?;

2. Validation des données

// Validation des transactions avant broadcast
daemon.test_mempool_accept(&tx)?;

// Validation des messages WebSocket
if handshake.version != "1.0" {
    return Err(Error::msg("Unsupported version"));
}

3. Protection contre les attaques

// Limitation du nombre de connexions
const MAX_CONNECTIONS: usize = 1000;

// Timeout sur les opérations
.timeout(Duration::from_secs(30))

Métriques et monitoring

1. Logs structurés

log::info!("Using wallet with address {}", our_sp_address);
log::info!("Found {} outputs for a total balance of {}",
    sp_wallet.get_outputs().len(), sp_wallet.get_balance());
log::warn!("Failed to connect to Bitcoin Core (attempt {}/{}): {}",
    retry_count, MAX_RETRIES, e);

2. Métriques internes

  • Nombre de connexions WebSocket actives
  • Transactions traitées par seconde
  • Latence des opérations RPC
  • Utilisation mémoire et CPU

3. Healthcheck

// Vérification de la connectivité Bitcoin Core
daemon.get_blockchain_info()?;

// Vérification du service Blindbit
reqwest::get(&format!("{}/health", blindbit_url)).await?;

// Vérification du serveur WebSocket
TcpStream::connect(ws_url).await?;

Tests

1. Tests unitaires

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_config_parsing() {
        let config = Config::read_from_file("test.conf").unwrap();
        assert_eq!(config.network, Network::Signet);
    }

    #[test]
    fn test_message_serialization() {
        let message = MessageType::Handshake(HandshakeMessage {
            version: "1.0".to_string(),
            capabilities: vec!["silent_payments".to_string()],
        });

        let serialized = serde_json::to_string(&message).unwrap();
        let deserialized: MessageType = serde_json::from_str(&serialized).unwrap();

        assert!(matches!(deserialized, MessageType::Handshake(_)));
    }
}

2. Tests d'intégration

#[tokio::test]
async fn test_websocket_connection() {
    // Setup
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();

    // Test
    let client = tokio_tungstenite::connect_async(
        format!("ws://{}", addr)
    ).await.unwrap();

    // Assertions
    assert!(client.0.is_ok());
}

Dépendances externes

1. Bitcoin Core

  • RPC Interface : Opérations blockchain
  • ZMQ Events : Notifications en temps réel
  • Cookie Authentication : Sécurité

2. Blindbit Service

  • Block Filters : Filtrage des transactions
  • Silent Payments : Décodage des outputs SP
  • HTTP API : Communication REST

3. sdk_common

  • SpClient : Client Silent Payments
  • SpWallet : Gestion des wallets SP
  • Network : Types réseau Bitcoin

Configuration de déploiement

1. Variables d'environnement

export RUST_LOG=debug,sdk_relay=trace
export HOME=/home/bitcoin
export BITCOIN_COOKIE_PATH=/home/bitcoin/.4nk/bitcoin.cookie

2. Fichier de configuration

# Bitcoin Core
core_url=http://bitcoin:18443
core_wallet=relay_wallet
network=signet

# WebSocket
ws_url=0.0.0.0:8090

# Services
blindbit_url=http://blindbit:8000
zmq_url=tcp://bitcoin:29000

# Storage
data_dir=.4nk
wallet_name=relay_wallet.json
cookie_path=/home/bitcoin/.4nk/bitcoin.cookie

3. Permissions système

# Utilisateur bitcoin
useradd -m -d /home/bitcoin -g bitcoin bitcoin

# Permissions des fichiers
chown -R bitcoin:bitcoin /home/bitcoin/.4nk
chmod 600 /home/bitcoin/.4nk/bitcoin.cookie

Conclusion

sdk_relay est un service Rust robuste et performant qui implémente un relais complet pour les Silent Payments. Son architecture modulaire, sa gestion asynchrone et ses mécanismes de sécurité en font une solution production-ready pour l'intégration des paiements silencieux avec Bitcoin Core.