diff --git a/examples/README.md b/examples/README.md index 2090d8f..ee723cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,3 +7,9 @@ number before the ciphertext can be successfully decrypted. This example also demonstrates the idea of a "trusted dealer", i.e. some trusted entity that is responsible for generating the keys. +- [`Threshold Signing`](threshold_sig.rs) - Demonstrates how threshold signing +can be used to generate an append-only ledger of chat messages. Each node +running our chat protocol receives and signs messages (using its share of the +network's master secret-key). The network adds a new message to the ledger once +enough nodes (`threshold + 1`) have signed a given message. + diff --git a/examples/threshold_sig.rs b/examples/threshold_sig.rs new file mode 100644 index 0000000..314a623 --- /dev/null +++ b/examples/threshold_sig.rs @@ -0,0 +1,225 @@ +extern crate rand; +extern crate threshold_crypto; + +use std::collections::BTreeMap; + +use threshold_crypto::{ + PublicKeySet, PublicKeyShare, SecretKeySet, SecretKeyShare, Signature, SignatureShare, +}; + +type UserId = usize; +type NodeId = usize; +type Msg = String; + +// The database schema that validator nodes use to store messages that they receive from users. +// Messages are first indexed numerically by user ID then alphabetically by message. Each message +// is mapped to its list of valdidator signatures. +type MsgDatabase = BTreeMap>>; + +// An append-only list of chat message "blocks". Each block contains the user ID for the user who +// broadcast the message to the network, the message text, and the combined signature of the +// message. A block can be appended to this list each time our chat protocol runs its consensus +// algorithm. +type ChatLog = Vec<(UserId, Msg, Signature)>; + +// Represents a network of nodes running a distributed chat protocol. Clients, or "users", of our +// network, create a string that they want to append to the network's `chat_log`, they broadcast +// this message to the network, each node that receives the message signs it with their +// signing-key. When the network runs a round of consensus, each node contributes its set of signed +// messages, the first message that has received `threshold + 1` signatures from validator nodes, +// gets added to the `chat_log`. +struct ChatNetwork { + pk_set: PublicKeySet, + nodes: Vec, + chat_log: ChatLog, + n_users: usize, +} + +impl ChatNetwork { + // Creates a new network of nodes running our distributed chat protocol. + // + // # Arguments + // + // `n_nodes` - the number of validator/signing nodes in the network. + // `threshold` - our protocol requires a message to have `threshold + 1` validator signatures + // before it can be added to the `chat_log`. + fn new(n_nodes: usize, threshold: usize) -> Self { + let mut rng = rand::thread_rng(); + let sk_set = SecretKeySet::random(threshold, &mut rng).unwrap(); + let pk_set = sk_set.public_keys(); + + let nodes = (0..n_nodes) + .map(|id| { + let sk_share = sk_set.secret_key_share(id).unwrap(); + let pk_share = pk_set.public_key_share(id); + Node::new(id, sk_share, pk_share) + }) + .collect(); + + ChatNetwork { + pk_set, + nodes, + chat_log: vec![], + n_users: 0, + } + } + + fn create_user(&mut self) -> User { + let user_id = self.n_users; + let user = User::new(user_id); + self.n_users += 1; + user + } + + fn get_node(&self, id: NodeId) -> &Node { + self.nodes.get(id).expect("No `Node` exists with that ID") + } + + fn get_mut_node(&mut self, id: NodeId) -> &mut Node { + self.nodes + .get_mut(id) + .expect("No `Node` exists with that ID") + } + + // Run a single round of the consensus algorithm. If consensus produced a new block, append + // that block the chat log. + fn step(&mut self) { + if let Some(block) = self.run_consensus() { + self.chat_log.push(block); + } + } + + // Our chat protocol's consensus algorithm. Produces a new block to be appended to the chat + // log. Our consensus uses threshold-signing to verify that a message has received enough + // signature shares (i.e. has been signed by `threshold + 1` nodes). + fn run_consensus(&self) -> Option<(UserId, Msg, Signature)> { + // Create a new `MsgDatabase` of every message that has been signed by a validator node. + let all_pending: MsgDatabase = self.nodes.iter().fold( + BTreeMap::new(), + |mut all_pending, node| { + for (user_id, signed_msgs) in &node.pending { + let mut user_msgs = all_pending.entry(*user_id).or_insert_with(BTreeMap::new); + for (msg, sigs) in signed_msgs.iter() { + let sigs = sigs.iter().cloned(); + user_msgs + .entry(msg.to_string()) + .or_insert_with(Vec::new) + .extend(sigs); + } + } + all_pending + }, + ); + + // Iterate over the `MsgDatabase` numerically by user ID, then iterate over each user's + // messages alphabetically. Try to combine the validator signatures. The first message that + // has received `threshold + 1` node signatures, will produce a valid "combined" signature + // and will be added to the chat log. + for (user_id, signed_msgs) in &all_pending { + for (msg, sigs) in signed_msgs.iter() { + let sigs = sigs.iter().filter_map(|node_sig| { + let node_sig_is_valid = self + .get_node(node_sig.node_id) + .pk_share + .verify(&node_sig.sig, msg.as_bytes()); + + if node_sig_is_valid { + Some((node_sig.node_id, &node_sig.sig)) + } else { + None + } + }); + + if let Ok(sig) = self.pk_set.combine_signatures(sigs) { + return Some((*user_id, msg.clone(), sig)); + } + } + } + + None + } +} + +// A node the network that is running our chat protocol. +struct Node { + id: NodeId, + sk_share: SecretKeyShare, + pk_share: PublicKeyShare, + pending: MsgDatabase, +} + +impl Node { + fn new(id: NodeId, sk_share: SecretKeyShare, pk_share: PublicKeyShare) -> Self { + Node { + id, + sk_share, + pk_share, + pending: BTreeMap::new(), + } + } + + // Receives a message from a user, signs the with message with the node's signing-key share, + // then adds the signed message to its database of `pending` messages. + fn recv(&mut self, user_id: UserId, msg: Msg) { + let sig = NodeSignature { + node_id: self.id, + sig: self.sk_share.sign(msg.as_bytes()), + }; + self.pending + .entry(user_id) + .or_insert_with(BTreeMap::new) + .entry(msg) + .or_insert_with(Vec::new) + .push(sig); + } +} + +#[derive(Clone, Debug)] +struct NodeSignature { + node_id: NodeId, + sig: SignatureShare, +} + +// A client of our chat protocol. +struct User { + id: UserId, +} + +impl User { + fn new(id: UserId) -> Self { + User { id } + } + + // Sends a message to one of the network's validator nodes. + fn send(&self, node: &mut Node, msg: Msg) { + node.recv(self.id, msg); + } +} + +fn main() { + // Creates a new network of 3 nodes running our chat protocol. The protocol has a + // signing-threshold of 1, i.e. each message requires 2 validator signatures before it can be + // added to the chat log. + let mut network = ChatNetwork::new(3, 1); + let node1 = network.get_node(0).id; + let node2 = network.get_node(1).id; + + // Register a new user, Alice, with the network. Alice wants to add a message to the chat log. + let alice = network.create_user(); + let alice_greeting = "hey, this is alice".to_string(); + + // Alice sends here message to a validator. The validator signs the message. Before Alice can + // send her message to a second validator, the network runs a round of consensus. Because + // Alice's message has only one validator signature, it is not added to the chat log. + alice.send(network.get_mut_node(node1), alice_greeting.clone()); + network.step(); + assert!(network.chat_log.is_empty()); + + // Alice sends here message to a second validator. the validator signs the message. Alice's + // message now has two signatures (which is `threshold + 1` signatures). The network runs a + // round of consensus, which successfully creates a combined-signature for Alice's message. + // Alice's message is appended to the chat log. + alice.send(network.get_mut_node(node2), alice_greeting.clone()); + network.step(); + assert_eq!(network.chat_log.len(), 1); +}