From 9a4db108935ef836940917c55936f3f26586759d Mon Sep 17 00:00:00 2001 From: Joshy Orndorff Date: Sat, 17 Feb 2024 11:29:38 -0500 Subject: [PATCH 1/2] initial sketch from first night --- Cargo.lock | 11 +++ Cargo.toml | 1 + wardrobe/sudo-token/Cargo.toml | 23 +++++ wardrobe/sudo-token/src/lib.rs | 154 +++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 wardrobe/sudo-token/Cargo.toml create mode 100644 wardrobe/sudo-token/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 006e61606..0cda3bdac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13208,6 +13208,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "sudo-token" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-runtime", + "tuxedo-core", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 62b8f5292..006468ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "wardrobe/money", "wardrobe/parachain", "wardrobe/poe", + "wardrobe/sudo-token", "wardrobe/timestamp", "wardrobe/kitties", "wardrobe/runtime_upgrade", diff --git a/wardrobe/sudo-token/Cargo.toml b/wardrobe/sudo-token/Cargo.toml new file mode 100644 index 000000000..b95671136 --- /dev/null +++ b/wardrobe/sudo-token/Cargo.toml @@ -0,0 +1,23 @@ +[package] +description = "A Tuxedo piece that implements simple token based access to privledged runtime functions." +edition = "2021" +name = "sudo-token" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +parity-scale-codec = { features = [ "derive" ], workspace = true } +scale-info = { features = [ "derive" ], workspace = true } +serde = { features = [ "derive" ], workspace = true } +sp-runtime = { default_features = false, workspace = true } +tuxedo-core = { default-features = false, path = "../../tuxedo-core" } + +[features] +default = [ "std" ] +std = [ + "tuxedo-core/std", + "parity-scale-codec/std", + "sp-runtime/std", + "serde/std", +] diff --git a/wardrobe/sudo-token/src/lib.rs b/wardrobe/sudo-token/src/lib.rs new file mode 100644 index 000000000..cb5574719 --- /dev/null +++ b/wardrobe/sudo-token/src/lib.rs @@ -0,0 +1,154 @@ +//! Some functionality in a Runtime needs to be gated behind some form of on-chain +//! governance. This pallet implements a token-based solution to restrict access to +//! sensitive transactions to callers who have access to a specific token. +//! +//! One simple way to manage this token is to lock it behind a signature check verifier, +//! Or some other private ownership verifier. In this configuration it is similar to +//! FRAME's pallet sudo. One advantage over pallet sudo is that +//! +//! You could achieve basic council-like governance by locking the token behind a +//! multisig verifier, or composing it with an on-chain stateful multisig. +//! +//! Currently using the token requires consuming it and recreating it. But in the +//! future, peeks may also allow verifiers, and then peeking would be sufficient. +//! +//! More complex governance like token voting is not yet in scope for consideration. + +#![cfg_attr(not(feature = "std"), no_std)] + +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_runtime::transaction_validity::TransactionPriority; +use tuxedo_core::{ + dynamic_typing::{DynamicallyTypedData, UtxoData}, ensure, ConstraintChecker, SimpleConstraintChecker +}; + +#[cfg(test)] +mod tests; + +/// A simple one-off token that represents the ability to access elevated privledges. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct SudoToken; + +impl UtxoData for SudoToken { + const TYPE_ID: [u8; 4] = *b"sudo"; +} + +/// Reasons that the sudo token constraint checkers may fail +#[derive(Debug, Eq, PartialEq)] +pub enum ConstraintCheckerError { + /// No inputs were presented in the transaction. But the sudo token must be consumed. + NoInputs, + /// The first input to the transaction must be the sudo token, but it was not. + FirstInputIsNotSudoToken, + /// + NoOutput, + /// + FirstOutputIsNotSudoToken, +} + +/// Call some transaction with escalated privledges +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct SudoCall; + +impl SimpleConstraintChecker for SudoCall { + type Error = ConstraintCheckerError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + _peeks: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // Make sure the first input is the sudo token. + // If the caller is able to consume this token, they may have the elevated access. + ensure!(); + + // Make sure the first output is the same sudo token. + ensure!(); + + // + } +} + +/// A constraint checker for simple death of an amoeba. +/// +/// Any amoeba can be killed by providing it as the sole input to this constraint checker. No +/// new outputs are ever created. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct AmoebaDeath; + +impl SimpleConstraintChecker for AmoebaDeath { + type Error = ConstraintCheckerError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + _peeks: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // Make sure there is a single victim + ensure!(!input_data.is_empty(), ConstraintCheckerError::NoVictim); + ensure!( + input_data.len() == 1, + ConstraintCheckerError::TooManyVictims + ); + + // We don't actually need to check any details of the victim, but we do need to make sure + // we have the correct type. + let _victim = input_data[0] + .extract::() + .map_err(|_| ConstraintCheckerError::BadlyTypedInput)?; + + // Make sure there are no outputs + ensure!( + output_data.is_empty(), + ConstraintCheckerError::DeathMayNotCreate + ); + + Ok(0) + } +} + +/// A constraint checker for simple creation of an amoeba. +/// +/// A new amoeba can be created by providing it as the sole output to this constraint checker. No +/// inputs are ever consumed. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct AmoebaCreation; + +impl SimpleConstraintChecker for AmoebaCreation { + type Error = ConstraintCheckerError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + _peeks: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // Make sure there is a single created amoeba + ensure!( + !output_data.is_empty(), + ConstraintCheckerError::CreatedNothing + ); + ensure!( + output_data.len() == 1, + ConstraintCheckerError::CreatedTooMany + ); + let eve = output_data[0] + .extract::() + .map_err(|_| ConstraintCheckerError::BadlyTypedOutput)?; + + // Make sure the newly created amoeba has generation 0 + ensure!(eve.generation == 0, ConstraintCheckerError::WrongGeneration); + + // Make sure there are no inputs + ensure!( + input_data.is_empty(), + ConstraintCheckerError::CreationMayNotConsume + ); + + Ok(0) + } +} From 14d0a18125103d107bc29ea9b7bc07e393c55ba3 Mon Sep 17 00:00:00 2001 From: Joshy Orndorff Date: Tue, 20 Feb 2024 12:27:57 -0500 Subject: [PATCH 2/2] pivot toward unforgeable access tokens --- Cargo.lock | 22 +-- Cargo.toml | 2 +- wardrobe/sudo-token/src/lib.rs | 154 --------------- .../Cargo.toml | 4 +- wardrobe/unforgeable-access-tokens/src/lib.rs | 175 ++++++++++++++++++ 5 files changed, 189 insertions(+), 168 deletions(-) delete mode 100644 wardrobe/sudo-token/src/lib.rs rename wardrobe/{sudo-token => unforgeable-access-tokens}/Cargo.toml (82%) create mode 100644 wardrobe/unforgeable-access-tokens/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0cda3bdac..e4594c983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13208,17 +13208,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" -[[package]] -name = "sudo-token" -version = "0.1.0" -dependencies = [ - "parity-scale-codec", - "scale-info", - "serde", - "sp-runtime", - "tuxedo-core", -] - [[package]] name = "syn" version = "1.0.109" @@ -14141,6 +14130,17 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unforgeable-access-tokens" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-runtime", + "tuxedo-core", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index 006468ef1..a6ad97448 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ "wardrobe/money", "wardrobe/parachain", "wardrobe/poe", - "wardrobe/sudo-token", + "wardrobe/unforgeable-access-tokens", "wardrobe/timestamp", "wardrobe/kitties", "wardrobe/runtime_upgrade", diff --git a/wardrobe/sudo-token/src/lib.rs b/wardrobe/sudo-token/src/lib.rs deleted file mode 100644 index cb5574719..000000000 --- a/wardrobe/sudo-token/src/lib.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Some functionality in a Runtime needs to be gated behind some form of on-chain -//! governance. This pallet implements a token-based solution to restrict access to -//! sensitive transactions to callers who have access to a specific token. -//! -//! One simple way to manage this token is to lock it behind a signature check verifier, -//! Or some other private ownership verifier. In this configuration it is similar to -//! FRAME's pallet sudo. One advantage over pallet sudo is that -//! -//! You could achieve basic council-like governance by locking the token behind a -//! multisig verifier, or composing it with an on-chain stateful multisig. -//! -//! Currently using the token requires consuming it and recreating it. But in the -//! future, peeks may also allow verifiers, and then peeking would be sufficient. -//! -//! More complex governance like token voting is not yet in scope for consideration. - -#![cfg_attr(not(feature = "std"), no_std)] - -use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; -use serde::{Deserialize, Serialize}; -use sp_runtime::transaction_validity::TransactionPriority; -use tuxedo_core::{ - dynamic_typing::{DynamicallyTypedData, UtxoData}, ensure, ConstraintChecker, SimpleConstraintChecker -}; - -#[cfg(test)] -mod tests; - -/// A simple one-off token that represents the ability to access elevated privledges. -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] -pub struct SudoToken; - -impl UtxoData for SudoToken { - const TYPE_ID: [u8; 4] = *b"sudo"; -} - -/// Reasons that the sudo token constraint checkers may fail -#[derive(Debug, Eq, PartialEq)] -pub enum ConstraintCheckerError { - /// No inputs were presented in the transaction. But the sudo token must be consumed. - NoInputs, - /// The first input to the transaction must be the sudo token, but it was not. - FirstInputIsNotSudoToken, - /// - NoOutput, - /// - FirstOutputIsNotSudoToken, -} - -/// Call some transaction with escalated privledges -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub struct SudoCall; - -impl SimpleConstraintChecker for SudoCall { - type Error = ConstraintCheckerError; - - fn check( - &self, - input_data: &[DynamicallyTypedData], - _peeks: &[DynamicallyTypedData], - output_data: &[DynamicallyTypedData], - ) -> Result { - // Make sure the first input is the sudo token. - // If the caller is able to consume this token, they may have the elevated access. - ensure!(); - - // Make sure the first output is the same sudo token. - ensure!(); - - // - } -} - -/// A constraint checker for simple death of an amoeba. -/// -/// Any amoeba can be killed by providing it as the sole input to this constraint checker. No -/// new outputs are ever created. -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub struct AmoebaDeath; - -impl SimpleConstraintChecker for AmoebaDeath { - type Error = ConstraintCheckerError; - - fn check( - &self, - input_data: &[DynamicallyTypedData], - _peeks: &[DynamicallyTypedData], - output_data: &[DynamicallyTypedData], - ) -> Result { - // Make sure there is a single victim - ensure!(!input_data.is_empty(), ConstraintCheckerError::NoVictim); - ensure!( - input_data.len() == 1, - ConstraintCheckerError::TooManyVictims - ); - - // We don't actually need to check any details of the victim, but we do need to make sure - // we have the correct type. - let _victim = input_data[0] - .extract::() - .map_err(|_| ConstraintCheckerError::BadlyTypedInput)?; - - // Make sure there are no outputs - ensure!( - output_data.is_empty(), - ConstraintCheckerError::DeathMayNotCreate - ); - - Ok(0) - } -} - -/// A constraint checker for simple creation of an amoeba. -/// -/// A new amoeba can be created by providing it as the sole output to this constraint checker. No -/// inputs are ever consumed. -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub struct AmoebaCreation; - -impl SimpleConstraintChecker for AmoebaCreation { - type Error = ConstraintCheckerError; - - fn check( - &self, - input_data: &[DynamicallyTypedData], - _peeks: &[DynamicallyTypedData], - output_data: &[DynamicallyTypedData], - ) -> Result { - // Make sure there is a single created amoeba - ensure!( - !output_data.is_empty(), - ConstraintCheckerError::CreatedNothing - ); - ensure!( - output_data.len() == 1, - ConstraintCheckerError::CreatedTooMany - ); - let eve = output_data[0] - .extract::() - .map_err(|_| ConstraintCheckerError::BadlyTypedOutput)?; - - // Make sure the newly created amoeba has generation 0 - ensure!(eve.generation == 0, ConstraintCheckerError::WrongGeneration); - - // Make sure there are no inputs - ensure!( - input_data.is_empty(), - ConstraintCheckerError::CreationMayNotConsume - ); - - Ok(0) - } -} diff --git a/wardrobe/sudo-token/Cargo.toml b/wardrobe/unforgeable-access-tokens/Cargo.toml similarity index 82% rename from wardrobe/sudo-token/Cargo.toml rename to wardrobe/unforgeable-access-tokens/Cargo.toml index b95671136..e8174c735 100644 --- a/wardrobe/sudo-token/Cargo.toml +++ b/wardrobe/unforgeable-access-tokens/Cargo.toml @@ -1,7 +1,7 @@ [package] -description = "A Tuxedo piece that implements simple token based access to privledged runtime functions." +description = "An object capability system for Tuxedo Runtimes." edition = "2021" -name = "sudo-token" +name = "unforgeable-access-tokens" version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/wardrobe/unforgeable-access-tokens/src/lib.rs b/wardrobe/unforgeable-access-tokens/src/lib.rs new file mode 100644 index 000000000..707422548 --- /dev/null +++ b/wardrobe/unforgeable-access-tokens/src/lib.rs @@ -0,0 +1,175 @@ +//! An unforgeable access token is an NFT whose purpose is to grant access to some +//! on-chain functionality to the bearer. +//! +//! For example, some chains have the ability to pay bounties to users, modify balances, +//! or even upgrade the code of the runtime itself. None of these functionalities should +//! be exposed to the general public. +//! +//! An unforgeable token can be created at genesis or through a transaction. Each new +//! unforgeable token has a unique id based on the block hash and the creation transaction's +//! index in the block. This prevents the same token from ever being created twice. +//! +//! ## Managing Ownership +//! +//! This piece very little in terms of managing ownership of unforgeable tokens. +//! That is because unforgeable tokens can be managed using the same kinds of verifiers or +//! on-chain daos that any other token is managed with. The most obvious way is Tuxedo's verifiers. +//! +//! For a simple example, consider a privileged address who should be the only one to access +//! the unforgeable token. You can achieve this by protecting the unforgeable token with simple +//! signature checking verifier. Or if you want something more akin to a council, you could use +//! a multisig verifier. +//! +//! In order to change the sudo account or update the multisig members, a single constraint +//! checker called `BumpToken` exists. It consumes a single unforgeable token and re-creates +//! the same token. This allows the token holder to swap verifiers when necessary. +//! +//! ## Composition with other constraint checkers +//! +//! This piece does not provide much functionality itself. It only provides the ability to create +//! and bump unforgeable tokens. In order for those tokens to be useful, this piece should +//! be composed with one or more additional pieces... + +#![cfg_attr(not(feature = "std"), no_std)] + +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_runtime::transaction_validity::TransactionPriority; +use tuxedo_core::{ + dynamic_typing::{DynamicallyTypedData, UtxoData}, ensure, SimpleConstraintChecker +}; + +// #[cfg(test)] +// mod tests; + +/// A simple non-fungible token that can not be forged +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct UnforgeableToken{ + /// Sequential serial number for each unforgeable name created. + serial_number: u32, +} + +impl UtxoData for UnforgeableToken { + const TYPE_ID: [u8; 4] = *b"unfo"; +} + +/// The counter for the serial number of each created unforgeable token. +/// +/// If you want to allow creating unforgeable tokens after genesis, this +/// must be present in the genesis config. No new tokens can be created +/// without peeking at this one. +/// +/// This token should be unique in the runtime. +/// +/// For now we require a total ordering over the creation of new unforgeable tokens. +/// This is how we guarantee that you never create two with the same id. +/// I have a suspicion this could be improved with the use of splittable / mergable +/// pseudo random number generators. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct UnforgeableTokenFactory { + /// The serial number of the next unforgeable token that will be created. + pub next_serial: u32, +} + +impl UtxoData for UnforgeableTokenFactory { + const TYPE_ID: [u8; 4] = *b"unff"; +} + +/// Reasons that the sudo token constraint checkers may fail +#[derive(Debug, Eq, PartialEq)] +pub enum UnforgeableTokenError { + // Bumping + + /// No inputs were presented in the transaction. But the sudo token must be consumed. + NoInputs, + /// The first input to the transaction must be the sudo token, but it was not. + InputIsNotUnforgeableToken, + /// + NoOutput, + /// + OutputIsNotUnforgeableToken, + /// + NoFirstOutput, + + // Creating + + /// You have not consumed the proper unforgeable token factory to create a new unforgeable token. + NoFactoryPresent, + +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +/// Allows updating the verifier that is protecting a particular unforgeable token. +pub struct BumpUnforgeableToken; + +impl SimpleConstraintChecker for BumpUnforgeableToken { + type Error = UnforgeableTokenError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + peek_data: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // ensure one properly typed input + // unsure one properly typed output + // ensure input equals output + todo!() + } +} + +// TODO is this every actually necessary? +// Is it better to make another pice do the creation through the help of a trait? +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +/// Allows a user to create a new unforgeable token by calling this transaction. +/// +/// Not all runtimes will want to expose this functionality to users. +/// In simple cases it is sufficient to have a small number of unforgeable tokens +/// created at genesis. +pub struct CreateUnforgeableToken; + +impl SimpleConstraintChecker for CreateUnforgeableToken { + type Error = UnforgeableTokenError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + peek_data: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // ensure one input that is a factory + // unsure first output is the properly updated factory + // ensure other output is the correct new unforgeable token + todo!() + } +} + + +/// DO NOT USE IN PRODUCTION!!!!!!!! +/// +/// Allows a user to forge an unforgeable token. This could be useful for testing +/// purposes. This is also a really useful transaction type to study to help new +/// users understand where the security of unforgeable access tokens comes from. +/// +/// Take note of the differences between this transaction and the normal creation +/// transactions. That one requires the use of a factory which guarantees the tokens +/// are unique. This one does not have any factory to confirm the serial numbers are +/// unique, and thus the tokens are forgeable. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct ForgeUnforgeableToken; + +impl SimpleConstraintChecker for ForgeUnforgeableToken { + type Error = UnforgeableTokenError; + + fn check( + &self, + input_data: &[DynamicallyTypedData], + peek_data: &[DynamicallyTypedData], + output_data: &[DynamicallyTypedData], + ) -> Result { + // ensure no inputs + // ensure one properly typed output + todo!() + } +} \ No newline at end of file