diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8bf4084d..e1c54349 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,6 +48,7 @@ jobs: -p serai-processor-utxo-scheduler-primitives \ -p serai-processor-utxo-scheduler \ -p serai-processor-transaction-chaining-scheduler \ + -p serai-processor-smart-contract-scheduler \ -p serai-processor-signers \ -p serai-processor-bin \ -p serai-bitcoin-processor \ diff --git a/Cargo.lock b/Cargo.lock index c3e39a09..147cc295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8350,18 +8350,25 @@ name = "serai-ethereum-processor" version = "0.1.0" dependencies = [ "borsh", - "const-hex", - "env_logger", + "ciphersuite", + "dkg", "ethereum-serai", + "flexible-transcript", "hex", "k256", "log", + "modular-frost", "parity-scale-codec", + "rand_core", + "serai-client", "serai-db", - "serai-env", - "serai-message-queue", - "serai-processor-messages", - "serde_json", + "serai-processor-bin", + "serai-processor-key-gen", + "serai-processor-primitives", + "serai-processor-scanner", + "serai-processor-scheduler-primitives", + "serai-processor-signers", + "serai-processor-smart-contract-scheduler", "tokio", "zalloc", ] @@ -8781,6 +8788,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serai-processor-smart-contract-scheduler" +version = "0.1.0" +dependencies = [ + "borsh", + "group", + "parity-scale-codec", + "serai-db", + "serai-primitives", + "serai-processor-primitives", + "serai-processor-scanner", + "serai-processor-scheduler-primitives", +] + [[package]] name = "serai-processor-tests" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b35b3318..adaa63db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ members = [ "processor/scheduler/utxo/primitives", "processor/scheduler/utxo/standard", "processor/scheduler/utxo/transaction-chaining", + "processor/scheduler/smart-contract", "processor/signers", "processor/bin", diff --git a/deny.toml b/deny.toml index ef195411..0e013f5e 100644 --- a/deny.toml +++ b/deny.toml @@ -55,6 +55,7 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-processor-utxo-scheduler-primitives" }, { allow = ["AGPL-3.0"], name = "serai-processor-standard-scheduler" }, { allow = ["AGPL-3.0"], name = "serai-processor-transaction-chaining-scheduler" }, + { allow = ["AGPL-3.0"], name = "serai-processor-smart-contract-scheduler" }, { allow = ["AGPL-3.0"], name = "serai-processor-signers" }, { allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" }, diff --git a/processor/ethereum/Cargo.toml b/processor/ethereum/Cargo.toml index ea65d570..ede9c71b 100644 --- a/processor/ethereum/Cargo.toml +++ b/processor/ethereum/Cargo.toml @@ -17,27 +17,38 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -const-hex = { version = "1", default-features = false } +rand_core = { version = "0.6", default-features = false } + hex = { version = "0.4", default-features = false, features = ["std"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } -serde_json = { version = "1", default-features = false, features = ["std"] } + +transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] } +ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] } +dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] } +frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false } k256 = { version = "^0.13.1", default-features = false, features = ["std"] } ethereum-serai = { path = "../../networks/ethereum", default-features = false, optional = true } -log = { version = "0.4", default-features = false, features = ["std"] } -env_logger = { version = "0.10", default-features = false, features = ["humantime"] } -tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } +serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] } zalloc = { path = "../../common/zalloc" } +log = { version = "0.4", default-features = false, features = ["std"] } +tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } + serai-db = { path = "../../common/db" } -serai-env = { path = "../../common/env" } -messages = { package = "serai-processor-messages", path = "../messages" } +key-gen = { package = "serai-processor-key-gen", path = "../key-gen" } -message-queue = { package = "serai-message-queue", path = "../../message-queue" } +primitives = { package = "serai-processor-primitives", path = "../primitives" } +scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" } +scanner = { package = "serai-processor-scanner", path = "../scanner" } +smart-contract-scheduler = { package = "serai-processor-smart-contract-scheduler", path = "../scheduler/smart-contract" } +signers = { package = "serai-processor-signers", path = "../signers" } + +bin = { package = "serai-processor-bin", path = "../bin" } [features] -parity-db = ["serai-db/parity-db"] -rocksdb = ["serai-db/rocksdb"] +parity-db = ["bin/parity-db"] +rocksdb = ["bin/rocksdb"] diff --git a/processor/scheduler/smart-contract/Cargo.toml b/processor/scheduler/smart-contract/Cargo.toml new file mode 100644 index 00000000..69ce9840 --- /dev/null +++ b/processor/scheduler/smart-contract/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "serai-processor-smart-contract-scheduler" +version = "0.1.0" +description = "Scheduler for a smart contract representing the Serai processor" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/processor/scheduler/smart-contract" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.cargo-machete] +ignored = ["scale", "borsh"] + +[lints] +workspace = true + +[dependencies] +group = { version = "0.13", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } +borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } + +serai-primitives = { path = "../../../substrate/primitives", default-features = false, features = ["std"] } + +serai-db = { path = "../../../common/db" } + +primitives = { package = "serai-processor-primitives", path = "../../primitives" } +scanner = { package = "serai-processor-scanner", path = "../../scanner" } +scheduler-primitives = { package = "serai-processor-scheduler-primitives", path = "../primitives" } diff --git a/processor/scheduler/smart-contract/LICENSE b/processor/scheduler/smart-contract/LICENSE new file mode 100644 index 00000000..e091b149 --- /dev/null +++ b/processor/scheduler/smart-contract/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/processor/scheduler/smart-contract/README.md b/processor/scheduler/smart-contract/README.md new file mode 100644 index 00000000..0be94d20 --- /dev/null +++ b/processor/scheduler/smart-contract/README.md @@ -0,0 +1,3 @@ +# Smart Contract Scheduler + +A scheduler for a smart contract representing the Serai processor. diff --git a/processor/scheduler/smart-contract/src/lib.rs b/processor/scheduler/smart-contract/src/lib.rs new file mode 100644 index 00000000..091ffe6a --- /dev/null +++ b/processor/scheduler/smart-contract/src/lib.rs @@ -0,0 +1,136 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use core::{marker::PhantomData, future::Future}; +use std::collections::HashMap; + +use group::GroupEncoding; + +use serai_db::{Get, DbTxn, create_db}; + +use primitives::{ReceivedOutput, Payment}; +use scanner::{ + LifetimeStage, ScannerFeed, KeyFor, AddressFor, EventualityFor, BlockFor, SchedulerUpdate, + KeyScopedEventualities, Scheduler as SchedulerTrait, +}; +use scheduler_primitives::*; + +create_db! { + SmartContractScheduler { + NextNonce: () -> u64, + } +} + +/// A smart contract. +pub trait SmartContract: 'static + Send { + /// The type representing a signable transaction. + type SignableTransaction: SignableTransaction; + + /// Rotate from the retiring key to the new key. + fn rotate( + nonce: u64, + retiring_key: KeyFor, + new_key: KeyFor, + ) -> (Self::SignableTransaction, EventualityFor); + /// Fulfill the set of payments, dropping any not worth handling. + fn fulfill( + starting_nonce: u64, + payments: Vec>>, + ) -> Vec<(Self::SignableTransaction, EventualityFor)>; +} + +/// A scheduler for a smart contract representing the Serai processor. +#[allow(non_snake_case)] +#[derive(Clone, Default)] +pub struct Scheduler> { + _S: PhantomData, + _SC: PhantomData, +} + +fn fulfill_payments>( + txn: &mut impl DbTxn, + active_keys: &[(KeyFor, LifetimeStage)], + payments: Vec>>, +) -> KeyScopedEventualities { + let key = match active_keys[0].1 { + LifetimeStage::ActiveYetNotReporting | + LifetimeStage::Active | + LifetimeStage::UsingNewForChange => active_keys[0].0, + LifetimeStage::Forwarding | LifetimeStage::Finishing => active_keys[1].0, + }; + + let mut nonce = NextNonce::get(txn).unwrap_or(0); + let mut eventualities = Vec::with_capacity(1); + for (signable, eventuality) in SC::fulfill(nonce, payments) { + TransactionsToSign::::send(txn, &key, &signable); + nonce += 1; + eventualities.push(eventuality); + } + NextNonce::set(txn, &nonce); + HashMap::from([(key.to_bytes().as_ref().to_vec(), eventualities)]) +} + +impl> SchedulerTrait for Scheduler { + type EphemeralError = (); + type SignableTransaction = SC::SignableTransaction; + + fn activate_key(_txn: &mut impl DbTxn, _key: KeyFor) {} + + fn flush_key( + &self, + txn: &mut impl DbTxn, + _block: &BlockFor, + retiring_key: KeyFor, + new_key: KeyFor, + ) -> impl Send + Future, Self::EphemeralError>> { + async move { + let nonce = NextNonce::get(txn).unwrap_or(0); + let (signable, eventuality) = SC::rotate(nonce, retiring_key, new_key); + NextNonce::set(txn, &(nonce + 1)); + TransactionsToSign::::send(txn, &retiring_key, &signable); + Ok(HashMap::from([(retiring_key.to_bytes().as_ref().to_vec(), vec![eventuality])])) + } + } + + fn retire_key(_txn: &mut impl DbTxn, _key: KeyFor) {} + + fn update( + &self, + txn: &mut impl DbTxn, + _block: &BlockFor, + active_keys: &[(KeyFor, LifetimeStage)], + update: SchedulerUpdate, + ) -> impl Send + Future, Self::EphemeralError>> { + async move { + // We ignore the outputs as we don't need to know our current state as it never suffers + // partial availability + + // We shouldn't have any forwards though + assert!(update.forwards().is_empty()); + + // Create the transactions for the returns + Ok(fulfill_payments::( + txn, + active_keys, + update + .returns() + .iter() + .map(|to_return| { + Payment::new(to_return.address().clone(), to_return.output().balance(), None) + }) + .collect::>(), + )) + } + } + + fn fulfill( + &self, + txn: &mut impl DbTxn, + _block: &BlockFor, + active_keys: &[(KeyFor, LifetimeStage)], + payments: Vec>>, + ) -> impl Send + Future, Self::EphemeralError>> { + async move { Ok(fulfill_payments::(txn, active_keys, payments)) } + } +} diff --git a/processor/scheduler/utxo/standard/src/lib.rs b/processor/scheduler/utxo/standard/src/lib.rs index 208ae8a0..dc2ccb06 100644 --- a/processor/scheduler/utxo/standard/src/lib.rs +++ b/processor/scheduler/utxo/standard/src/lib.rs @@ -470,7 +470,7 @@ impl> SchedulerTrait for Schedul } } - // Create the transactions for the forwards/burns + // Create the transactions for the forwards/returns { let mut planned_txs = vec![]; for forward in update.forwards() { diff --git a/processor/scheduler/utxo/transaction-chaining/src/lib.rs b/processor/scheduler/utxo/transaction-chaining/src/lib.rs index 961c6fcb..93bdf1f3 100644 --- a/processor/scheduler/utxo/transaction-chaining/src/lib.rs +++ b/processor/scheduler/utxo/transaction-chaining/src/lib.rs @@ -488,7 +488,7 @@ impl>> Sched } } - // Create the transactions for the forwards/burns + // Create the transactions for the forwards/returns { let mut planned_txs = vec![]; for forward in update.forwards() { diff --git a/processor/src/multisigs/scheduler/smart_contract.rs b/processor/src/multisigs/scheduler/smart_contract.rs deleted file mode 100644 index 3da8acf4..00000000 --- a/processor/src/multisigs/scheduler/smart_contract.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::{io, collections::HashSet}; - -use ciphersuite::{group::GroupEncoding, Ciphersuite}; - -use serai_client::primitives::{NetworkId, Coin, Balance}; - -use crate::{ - Get, DbTxn, Db, Payment, Plan, create_db, - networks::{Output, Network}, - multisigs::scheduler::{SchedulerAddendum, Scheduler as SchedulerTrait}, -}; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Scheduler { - key: ::G, - coins: HashSet, - rotated: bool, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Addendum { - Nonce(u64), - RotateTo { nonce: u64, new_key: ::G }, -} - -impl SchedulerAddendum for Addendum { - fn read(reader: &mut R) -> io::Result { - let mut kind = [0xff]; - reader.read_exact(&mut kind)?; - match kind[0] { - 0 => { - let mut nonce = [0; 8]; - reader.read_exact(&mut nonce)?; - Ok(Addendum::Nonce(u64::from_le_bytes(nonce))) - } - 1 => { - let mut nonce = [0; 8]; - reader.read_exact(&mut nonce)?; - let nonce = u64::from_le_bytes(nonce); - - let new_key = N::Curve::read_G(reader)?; - Ok(Addendum::RotateTo { nonce, new_key }) - } - _ => Err(io::Error::other("reading unknown Addendum type"))?, - } - } - fn write(&self, writer: &mut W) -> io::Result<()> { - match self { - Addendum::Nonce(nonce) => { - writer.write_all(&[0])?; - writer.write_all(&nonce.to_le_bytes()) - } - Addendum::RotateTo { nonce, new_key } => { - writer.write_all(&[1])?; - writer.write_all(&nonce.to_le_bytes())?; - writer.write_all(new_key.to_bytes().as_ref()) - } - } - } -} - -create_db! { - SchedulerDb { - LastNonce: () -> u64, - RotatedTo: (key: &[u8]) -> Vec, - } -} - -impl> SchedulerTrait for Scheduler { - type Addendum = Addendum; - - /// Check if this Scheduler is empty. - fn empty(&self) -> bool { - self.rotated - } - - /// Create a new Scheduler. - fn new( - _txn: &mut D::Transaction<'_>, - key: ::G, - network: NetworkId, - ) -> Self { - assert!(N::branch_address(key).is_none()); - assert!(N::change_address(key).is_none()); - assert!(N::forward_address(key).is_none()); - - Scheduler { key, coins: network.coins().iter().copied().collect(), rotated: false } - } - - /// Load a Scheduler from the DB. - fn from_db( - db: &D, - key: ::G, - network: NetworkId, - ) -> io::Result { - Ok(Scheduler { - key, - coins: network.coins().iter().copied().collect(), - rotated: RotatedTo::get(db, key.to_bytes().as_ref()).is_some(), - }) - } - - fn can_use_branch(&self, _balance: Balance) -> bool { - false - } - - fn schedule( - &mut self, - txn: &mut D::Transaction<'_>, - utxos: Vec, - payments: Vec>, - key_for_any_change: ::G, - force_spend: bool, - ) -> Vec> { - for utxo in utxos { - assert!(self.coins.contains(&utxo.balance().coin)); - } - - let mut nonce = LastNonce::get(txn).unwrap_or(1); - let mut plans = vec![]; - for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) { - // Once we rotate, all further payments should be scheduled via the new multisig - assert!(!self.rotated); - plans.push(Plan { - key: self.key, - inputs: vec![], - payments: chunk.to_vec(), - change: None, - scheduler_addendum: Addendum::Nonce(nonce), - }); - nonce += 1; - } - - // If we're supposed to rotate to the new key, create an empty Plan which will signify the key - // update - if force_spend && (!self.rotated) { - plans.push(Plan { - key: self.key, - inputs: vec![], - payments: vec![], - change: None, - scheduler_addendum: Addendum::RotateTo { nonce, new_key: key_for_any_change }, - }); - nonce += 1; - self.rotated = true; - RotatedTo::set( - txn, - self.key.to_bytes().as_ref(), - &key_for_any_change.to_bytes().as_ref().to_vec(), - ); - } - - LastNonce::set(txn, &nonce); - - plans - } - - fn consume_payments(&mut self, _txn: &mut D::Transaction<'_>) -> Vec> { - vec![] - } - - fn created_output( - &mut self, - _txn: &mut D::Transaction<'_>, - _expected: u64, - _actual: Option, - ) { - panic!("Smart Contract Scheduler created a Branch output") - } - - /// Refund a specific output. - fn refund_plan( - &mut self, - txn: &mut D::Transaction<'_>, - output: N::Output, - refund_to: N::Address, - ) -> Plan { - let current_key = RotatedTo::get(txn, self.key.to_bytes().as_ref()) - .and_then(|key_bytes| ::read_G(&mut key_bytes.as_slice()).ok()) - .unwrap_or(self.key); - - let nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1); - LastNonce::set(txn, &(nonce + 1)); - Plan { - key: current_key, - inputs: vec![], - payments: vec![Payment { address: refund_to, data: None, balance: output.balance() }], - change: None, - scheduler_addendum: Addendum::Nonce(nonce), - } - } - - fn shim_forward_plan(_output: N::Output, _to: ::G) -> Option> { - None - } - - /// Forward a specific output to the new multisig. - /// - /// Returns None if no forwarding is necessary. - fn forward_plan( - &mut self, - _txn: &mut D::Transaction<'_>, - _output: N::Output, - _to: ::G, - ) -> Option> { - None - } -}