docs: ajouter une spécification technique détaillée du fonctionnement de sdk_relay - Analyse complète de l'architecture et des modules - Documentation détaillée du flux de données - Explication des stratégies de gestion d'erreurs - Description des optimisations de performance - Spécification des mécanismes de sécurité - Guide de configuration et déploiement - Exemples de code et tests
This commit is contained in:
parent
1f82b06f83
commit
c12f3f2189
839
spec-technique.md
Normal file
839
spec-technique.md
Normal file
@ -0,0 +1,839 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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**
|
||||||
|
```rust
|
||||||
|
let config = Config::read_from_file(".conf")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Connexion à Bitcoin Core avec retry logic**
|
||||||
|
```rust
|
||||||
|
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**
|
||||||
|
```rust
|
||||||
|
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**
|
||||||
|
```rust
|
||||||
|
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**
|
||||||
|
```rust
|
||||||
|
// 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**
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
#### Structure des messages
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum MessageType {
|
||||||
|
Handshake(HandshakeMessage),
|
||||||
|
NewTx(NewTxMessage),
|
||||||
|
Broadcast(BroadcastMessage),
|
||||||
|
BalanceUpdate(BalanceUpdateMessage),
|
||||||
|
TxDetected(TxDetectedMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct HandshakeMessage {
|
||||||
|
pub version: String,
|
||||||
|
pub capabilities: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct NewTxMessage {
|
||||||
|
pub transaction: String, // Transaction hex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Handler de connexion WebSocket
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUST_LOG=debug,sdk_relay=trace
|
||||||
|
export HOME=/home/bitcoin
|
||||||
|
export BITCOIN_COOKIE_PATH=/home/bitcoin/.4nk/bitcoin.cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fichier de configuration
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
Loading…
x
Reference in New Issue
Block a user