mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-10 21:04:40 +00:00
Define all coordinator transaction types
This commit is contained in:
parent
90f67b5e54
commit
2cfee536f6
10 changed files with 372 additions and 0 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -1313,7 +1313,12 @@ dependencies = [
|
||||||
name = "coordinator"
|
name = "coordinator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"blake2",
|
||||||
|
"modular-frost",
|
||||||
|
"processor-messages",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tributary-chain",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -10673,7 +10678,12 @@ dependencies = [
|
||||||
name = "tributary-chain"
|
name = "tributary-chain"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"blake2",
|
||||||
|
"ciphersuite",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"schnorr-signatures",
|
||||||
"tendermint-machine",
|
"tendermint-machine",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -14,4 +14,16 @@ all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
blake2 = "0.10"
|
||||||
|
|
||||||
|
frost = { package = "modular-frost", path = "../crypto/frost" }
|
||||||
|
|
||||||
|
processor-messages = { package = "processor-messages", path = "../processor/messages" }
|
||||||
|
tributary = { package = "tributary-chain", path = "./tributary" }
|
||||||
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
|
tributary = { package = "tributary-chain", path = "./tributary", features = ["tests"] }
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
|
mod transaction;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {}
|
async fn main() {}
|
||||||
|
|
1
coordinator/src/tests/mod.rs
Normal file
1
coordinator/src/tests/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mod transaction;
|
81
coordinator/src/tests/transaction.rs
Normal file
81
coordinator/src/tests/transaction.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use frost::Participant;
|
||||||
|
|
||||||
|
use tributary::{ReadWrite, tests::random_signed};
|
||||||
|
|
||||||
|
use crate::transaction::{SignData, Transaction};
|
||||||
|
|
||||||
|
fn random_u32<R: RngCore>(rng: &mut R) -> u32 {
|
||||||
|
u32::try_from(rng.next_u64() >> 32).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_vec<R: RngCore>(rng: &mut R, limit: usize) -> Vec<u8> {
|
||||||
|
let len = usize::try_from(rng.next_u64() % u64::try_from(limit).unwrap()).unwrap();
|
||||||
|
let mut res = vec![0; len];
|
||||||
|
rng.fill_bytes(&mut res);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_sign_data<R: RngCore>(rng: &mut R) -> SignData {
|
||||||
|
let mut plan = [0; 32];
|
||||||
|
rng.fill_bytes(&mut plan);
|
||||||
|
|
||||||
|
SignData {
|
||||||
|
plan,
|
||||||
|
attempt: random_u32(&mut OsRng),
|
||||||
|
|
||||||
|
data: random_vec(&mut OsRng, 512),
|
||||||
|
|
||||||
|
signed: random_signed(&mut OsRng),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_read_write<RW: Eq + Debug + ReadWrite>(value: RW) {
|
||||||
|
assert_eq!(value, RW::read::<&[u8]>(&mut value.serialize().as_ref()).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_sign_data() {
|
||||||
|
test_read_write(random_sign_data(&mut OsRng));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_transaction() {
|
||||||
|
test_read_write(Transaction::DkgCommitments(
|
||||||
|
random_u32(&mut OsRng),
|
||||||
|
random_vec(&mut OsRng, 512),
|
||||||
|
random_signed(&mut OsRng),
|
||||||
|
));
|
||||||
|
|
||||||
|
{
|
||||||
|
// This supports a variable share length, yet share length is expected to be constant among
|
||||||
|
// shares
|
||||||
|
let share_len = usize::try_from(96 + (OsRng.next_u64() % 32)).unwrap();
|
||||||
|
// Create a valid map of shares
|
||||||
|
let mut shares = HashMap::new();
|
||||||
|
// Create up to 500 participants
|
||||||
|
for i in 0 .. (OsRng.next_u64() % 500) {
|
||||||
|
let mut share = vec![0; share_len];
|
||||||
|
OsRng.fill_bytes(&mut share);
|
||||||
|
shares.insert(Participant::new(u16::try_from(i + 1).unwrap()).unwrap(), share);
|
||||||
|
}
|
||||||
|
|
||||||
|
test_read_write(Transaction::DkgShares(
|
||||||
|
random_u32(&mut OsRng),
|
||||||
|
shares,
|
||||||
|
random_signed(&mut OsRng),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
test_read_write(Transaction::SignPreprocess(random_sign_data(&mut OsRng)));
|
||||||
|
test_read_write(Transaction::SignShare(random_sign_data(&mut OsRng)));
|
||||||
|
|
||||||
|
test_read_write(Transaction::FinalizedBlock(OsRng.next_u64()));
|
||||||
|
|
||||||
|
test_read_write(Transaction::BatchPreprocess(random_sign_data(&mut OsRng)));
|
||||||
|
test_read_write(Transaction::BatchShare(random_sign_data(&mut OsRng)));
|
||||||
|
}
|
223
coordinator/src/transaction.rs
Normal file
223
coordinator/src/transaction.rs
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
use std::{io, collections::HashMap};
|
||||||
|
|
||||||
|
use blake2::{Digest, Blake2s256};
|
||||||
|
|
||||||
|
use frost::Participant;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
use tributary::{
|
||||||
|
ReadWrite, Signed, TransactionError, TransactionKind, Transaction as TransactionTrait
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct SignData {
|
||||||
|
pub plan: [u8; 32],
|
||||||
|
pub attempt: u32,
|
||||||
|
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
|
||||||
|
pub signed: Signed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadWrite for SignData {
|
||||||
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
|
let mut plan = [0; 32];
|
||||||
|
reader.read_exact(&mut plan)?;
|
||||||
|
|
||||||
|
let mut attempt = [0; 4];
|
||||||
|
reader.read_exact(&mut attempt)?;
|
||||||
|
let attempt = u32::from_le_bytes(attempt);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
let mut data_len = [0; 2];
|
||||||
|
reader.read_exact(&mut data_len)?;
|
||||||
|
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
|
||||||
|
reader.read_exact(&mut data)?;
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
let signed = Signed::read(reader)?;
|
||||||
|
|
||||||
|
Ok(SignData { plan, attempt, data, signed })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
|
writer.write_all(&self.plan)?;
|
||||||
|
writer.write_all(&self.attempt.to_le_bytes())?;
|
||||||
|
|
||||||
|
writer.write_all(&u16::try_from(self.data.len()).unwrap().to_le_bytes())?;
|
||||||
|
writer.write_all(&self.data)?;
|
||||||
|
|
||||||
|
self.signed.write(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Transaction {
|
||||||
|
// Once this completes successfully, no more instances should be created.
|
||||||
|
DkgCommitments(u32, Vec<u8>, Signed),
|
||||||
|
DkgShares(u32, HashMap<Participant, Vec<u8>>, Signed),
|
||||||
|
|
||||||
|
SignPreprocess(SignData),
|
||||||
|
SignShare(SignData),
|
||||||
|
|
||||||
|
FinalizedBlock(u64),
|
||||||
|
|
||||||
|
BatchPreprocess(SignData),
|
||||||
|
BatchShare(SignData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadWrite for Transaction {
|
||||||
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
|
let mut kind = [0];
|
||||||
|
reader.read_exact(&mut kind)?;
|
||||||
|
|
||||||
|
match kind[0] {
|
||||||
|
0 => {
|
||||||
|
let mut attempt = [0; 4];
|
||||||
|
reader.read_exact(&mut attempt)?;
|
||||||
|
let attempt = u32::from_le_bytes(attempt);
|
||||||
|
|
||||||
|
let commitments = {
|
||||||
|
let mut commitments_len = [0; 2];
|
||||||
|
reader.read_exact(&mut commitments_len)?;
|
||||||
|
let mut commitments = vec![0; usize::from(u16::from_le_bytes(commitments_len))];
|
||||||
|
reader.read_exact(&mut commitments)?;
|
||||||
|
commitments
|
||||||
|
};
|
||||||
|
|
||||||
|
let signed = Signed::read(reader)?;
|
||||||
|
|
||||||
|
Ok(Transaction::DkgCommitments(attempt, commitments, signed))
|
||||||
|
}
|
||||||
|
|
||||||
|
1 => {
|
||||||
|
let mut attempt = [0; 4];
|
||||||
|
reader.read_exact(&mut attempt)?;
|
||||||
|
let attempt = u32::from_le_bytes(attempt);
|
||||||
|
|
||||||
|
let shares = {
|
||||||
|
let mut share_quantity = [0; 2];
|
||||||
|
reader.read_exact(&mut share_quantity)?;
|
||||||
|
|
||||||
|
let mut share_len = [0; 1];
|
||||||
|
reader.read_exact(&mut share_len)?;
|
||||||
|
let share_len = usize::from(share_len[0]);
|
||||||
|
|
||||||
|
let mut shares = HashMap::new();
|
||||||
|
for i in 0 .. u16::from_le_bytes(share_quantity) {
|
||||||
|
let participant = Participant::new(i + 1).unwrap();
|
||||||
|
let mut share = vec![0; share_len];
|
||||||
|
reader.read_exact(&mut share)?;
|
||||||
|
shares.insert(participant, share);
|
||||||
|
}
|
||||||
|
shares
|
||||||
|
};
|
||||||
|
|
||||||
|
let signed = Signed::read(reader)?;
|
||||||
|
|
||||||
|
Ok(Transaction::DkgShares(attempt, shares, signed))
|
||||||
|
}
|
||||||
|
|
||||||
|
2 => SignData::read(reader).map(Transaction::SignPreprocess),
|
||||||
|
3 => SignData::read(reader).map(Transaction::SignShare),
|
||||||
|
|
||||||
|
4 => {
|
||||||
|
let mut block = [0; 8];
|
||||||
|
reader.read_exact(&mut block)?;
|
||||||
|
Ok(Transaction::FinalizedBlock(u64::from_le_bytes(block)))
|
||||||
|
}
|
||||||
|
|
||||||
|
5 => SignData::read(reader).map(Transaction::BatchPreprocess),
|
||||||
|
6 => SignData::read(reader).map(Transaction::BatchShare),
|
||||||
|
_ => Err(io::Error::new(io::ErrorKind::Other, "invalid transaction type")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Transaction::DkgCommitments(attempt, commitments, signed) => {
|
||||||
|
writer.write_all(&[0])?;
|
||||||
|
writer.write_all(&attempt.to_le_bytes())?;
|
||||||
|
writer.write_all(&u16::try_from(commitments.len()).unwrap().to_le_bytes())?;
|
||||||
|
writer.write_all(commitments)?;
|
||||||
|
signed.write(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction::DkgShares(attempt, shares, signed) => {
|
||||||
|
writer.write_all(&[1])?;
|
||||||
|
writer.write_all(&attempt.to_le_bytes())?;
|
||||||
|
writer.write_all(&u16::try_from(shares.len()).unwrap().to_le_bytes())?;
|
||||||
|
let mut share_len = None;
|
||||||
|
for participant in 0 .. shares.len() {
|
||||||
|
let share = &shares[&Participant::new(u16::try_from(participant + 1).unwrap()).unwrap()];
|
||||||
|
if let Some(share_len) = share_len {
|
||||||
|
if share.len() != share_len {
|
||||||
|
panic!("variable length shares");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writer.write_all(&[u8::try_from(share.len()).unwrap()])?;
|
||||||
|
share_len = Some(share.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_all(share)?;
|
||||||
|
}
|
||||||
|
signed.write(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction::SignPreprocess(data) => {
|
||||||
|
writer.write_all(&[2])?;
|
||||||
|
data.write(writer)
|
||||||
|
}
|
||||||
|
Transaction::SignShare(data) => {
|
||||||
|
writer.write_all(&[3])?;
|
||||||
|
data.write(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction::FinalizedBlock(block) => {
|
||||||
|
writer.write_all(&[4])?;
|
||||||
|
writer.write_all(&block.to_le_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction::BatchPreprocess(data) => {
|
||||||
|
writer.write_all(&[5])?;
|
||||||
|
data.write(writer)
|
||||||
|
}
|
||||||
|
Transaction::BatchShare(data) => {
|
||||||
|
writer.write_all(&[6])?;
|
||||||
|
data.write(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionTrait for Transaction {
|
||||||
|
fn kind(&self) -> TransactionKind {
|
||||||
|
match self {
|
||||||
|
Transaction::DkgCommitments(_, _, signed) => TransactionKind::Signed(signed.clone()),
|
||||||
|
Transaction::DkgShares(_, _, signed) => TransactionKind::Signed(signed.clone()),
|
||||||
|
|
||||||
|
Transaction::SignPreprocess(data) => TransactionKind::Signed(data.signed.clone()),
|
||||||
|
Transaction::SignShare(data) => TransactionKind::Signed(data.signed.clone()),
|
||||||
|
|
||||||
|
Transaction::FinalizedBlock(_) => TransactionKind::Provided,
|
||||||
|
|
||||||
|
Transaction::BatchPreprocess(data) => TransactionKind::Signed(data.signed.clone()),
|
||||||
|
Transaction::BatchShare(data) => TransactionKind::Signed(data.signed.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash(&self) -> [u8; 32] {
|
||||||
|
let mut tx = self.serialize();
|
||||||
|
if let TransactionKind::Signed(signed) = self.kind() {
|
||||||
|
assert_eq!(&tx[(tx.len() - 64) ..], &signed.signature.serialize());
|
||||||
|
tx.truncate(tx.len() - 64);
|
||||||
|
}
|
||||||
|
Blake2s256::digest(tx).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(&self) -> Result<(), TransactionError> {
|
||||||
|
// TODO: Augment with checks that the Vecs can be deser'd and are for recognized IDs
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,17 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
|
rand_core = { version = "0.6", optional = true }
|
||||||
|
|
||||||
blake2 = "0.10"
|
blake2 = "0.10"
|
||||||
|
|
||||||
ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
||||||
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" }
|
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" }
|
||||||
|
|
||||||
tendermint = { package = "tendermint-machine", path = "./tendermint" }
|
tendermint = { package = "tendermint-machine", path = "./tendermint" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
tests = ["rand_core"]
|
||||||
|
|
|
@ -9,6 +9,9 @@ pub use transaction::*;
|
||||||
mod block;
|
mod block;
|
||||||
pub use block::*;
|
pub use block::*;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "tests"))]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
/// An item which can be read and written.
|
/// An item which can be read and written.
|
||||||
pub trait ReadWrite: Sized {
|
pub trait ReadWrite: Sized {
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self>;
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self>;
|
||||||
|
|
2
coordinator/tributary/src/tests/mod.rs
Normal file
2
coordinator/tributary/src/tests/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod transaction;
|
||||||
|
pub use transaction::*;
|
27
coordinator/tributary/src/tests/transaction.rs
Normal file
27
coordinator/tributary/src/tests/transaction.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
use ciphersuite::{
|
||||||
|
group::{ff::Field, Group},
|
||||||
|
Ciphersuite, Ristretto,
|
||||||
|
};
|
||||||
|
use schnorr::SchnorrSignature;
|
||||||
|
|
||||||
|
use crate::Signed;
|
||||||
|
|
||||||
|
pub fn random_signed<R: RngCore>(rng: &mut R) -> Signed {
|
||||||
|
Signed {
|
||||||
|
signer: <Ristretto as Ciphersuite>::G::random(&mut *rng),
|
||||||
|
nonce: u32::try_from(rng.next_u64() >> 32).unwrap(),
|
||||||
|
signature: SchnorrSignature::<Ristretto> {
|
||||||
|
R: <Ristretto as Ciphersuite>::G::random(&mut *rng),
|
||||||
|
s: <Ristretto as Ciphersuite>::F::random(rng),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_signed() {
|
||||||
|
use crate::ReadWrite;
|
||||||
|
let signed = signed(&mut rand_core::OsRng);
|
||||||
|
assert_eq!(Signed::read::<&[u8]>(&mut signed.serialize().as_ref()).unwrap(), signed);
|
||||||
|
}
|
Loading…
Reference in a new issue