From 8ca90e7905e3d55b883b7cf06bbdd37d9b98c355 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 20 Jan 2023 11:00:18 -0500 Subject: [PATCH] Initial In Instructions pallet and Serai client lib (#233) * Initial work on an In Inherents pallet * Add an event for when a batch is executed * Add a dummy provider for InInstructions * Add in-instructions to the node * Add the Serai runtime API to the processor * Move processor tests around * Build a subxt Client around Serai * Successfully get Batch events from Serai Renamed processor/substrate to processor/serai. * Much more robust InInstruction pallet * Implement the workaround from https://github.com/paritytech/subxt/issues/602 * Initial prototype of processor generated InInstructions * Correct PendingCoins data flow for InInstructions * Minor lint to in-instructions * Remove the global Serai connection for a partial re-impl * Correct ID handling of the processor test * Workaround the delay in the subscription * Make an unwrap an if let Some, remove old comments * Lint the processor toml * Rebase and update * Move substrate/in-instructions to substrate/in-instructions/pallet * Start an in-instructions primitives lib * Properly update processor to subxt 0.24 Also corrects failures from the rebase. * in-instructions cargo update * Implement IsFatalError * is_inherent -> true * Rename in-instructions crates and misc cleanup * Update documentation * cargo update * Misc update fixes * Replace height with block_number * Update processor src to latest subxt * Correct pipeline for InInstructions testing * Remove runtime::AccountId for serai_primitives::NativeAddress * Rewrite the in-instructions pallet Complete with respect to the currently written docs. Drops the custom serializer for just using SCALE. Makes slight tweaks as relevant. * Move instructions' InherentDataProvider to a client crate * Correct doc gen * Add serde to in-instructions-primitives * Add in-instructions-primitives to pallet * Heights -> BlockNumbers * Get batch pub test loop working * Update in instructions pallet terminology Removes the ambiguous Coin for Update. Removes pending/artificial latency for furture client work. Also moves to using serai_primitives::Coin. * Add a BlockNumber primitive * Belated cargo fmt * Further document why DifferentBatch isn't fatal * Correct processor sleeps * Remove metadata at compile time, add test framework for Serai nodes * Remove manual RPC client * Simplify update test * Improve re-exporting behavior of serai-runtime It now re-exports all pallets underneath it. * Add a function to get storage values to the Serai RPC * Update substrate/ to latest substrate * Create a dedicated crate for the Serai RPC * Remove unused dependencies in substrate/ * Remove unused dependencies in coins/ Out of scope for this branch, just minor and path of least resistance. * Use substrate/serai/client for the Serai RPC lib It's a bit out of place, since these client folders are intended for the node to access pallets and so on. This is for end-users to access Serai as a whole. In that sense, it made more sense as a top level folder, yet that also felt out of place. * Move InInstructions test to serai-client for now * Final cleanup * Update deny.toml * Cargo.lock update from merging develop * Update nightly Attempt to work around the current CI failure, which is a Rust ICE. We previously didn't upgrade due to clippy 10134, yet that's been reverted. * clippy * clippy * fmt * NativeAddress -> SeraiAddress * Sec fix on non-provided updates and doc fixes * Add Serai as a Coin Necessary in order to swap to Serai. * Add a BlockHash type, used for batch IDs * Remove origin from InInstruction Makes InInstructionTarget. Adds RefundableInInstruction with origin. * Document storage items in in-instructions * Rename serai/client/tests/serai.rs to updates.rs It only tested publishing updates and their successful acceptance. --- .github/actions/test-dependencies/action.yml | 15 ++ .github/nightly-version | 2 +- .github/workflows/tests.yml | 5 + Cargo.lock | 254 ++++++++++++++++-- Cargo.toml | 12 +- coins/ethereum/Cargo.toml | 1 - coins/monero/Cargo.toml | 5 +- coins/monero/src/tests/clsag.rs | 8 +- coins/monero/src/wallet/send/multisig.rs | 2 +- coins/monero/tests/runner.rs | 1 + deny.toml | 5 + docs/Serai.md | 4 +- docs/integrations/Ethereum.md | 4 +- docs/integrations/Instructions.md | 122 +++++---- docs/protocol/Constants.md | 28 +- processor/Cargo.toml | 10 +- processor/src/tests/mod.rs | 115 +------- processor/src/tests/monero.rs | 11 + processor/src/tests/send.rs | 106 ++++++++ substrate/in-instructions/client/Cargo.toml | 24 ++ substrate/in-instructions/client/LICENSE | 15 ++ substrate/in-instructions/client/src/lib.rs | 47 ++++ substrate/in-instructions/pallet/Cargo.toml | 51 ++++ substrate/in-instructions/pallet/LICENSE | 15 ++ substrate/in-instructions/pallet/src/lib.rs | 240 +++++++++++++++++ .../in-instructions/primitives/Cargo.toml | 25 ++ substrate/in-instructions/primitives/LICENSE | 21 ++ .../primitives/src/incoming.rs | 38 +++ .../in-instructions/primitives/src/lib.rs | 47 ++++ .../primitives/src/outgoing.rs | 25 ++ .../primitives/src/shorthand.rs | 54 ++++ substrate/node/Cargo.toml | 25 +- substrate/node/src/chain_spec.rs | 67 +++-- substrate/node/src/command.rs | 9 +- substrate/node/src/command_helper.rs | 39 +-- substrate/node/src/rpc.rs | 8 +- substrate/node/src/service.rs | 10 +- substrate/runtime/Cargo.toml | 8 +- substrate/runtime/src/lib.rs | 154 ++++++----- substrate/serai/client/Cargo.toml | 33 +++ substrate/serai/client/LICENSE | 15 ++ substrate/serai/client/src/in_instructions.rs | 49 ++++ substrate/serai/client/src/lib.rs | 77 ++++++ substrate/serai/client/tests/runner.rs | 50 ++++ substrate/serai/client/tests/updates.rs | 60 +++++ substrate/serai/primitives/Cargo.toml | 5 +- substrate/serai/primitives/src/amount.rs | 2 +- substrate/serai/primitives/src/coins.rs | 11 +- substrate/serai/primitives/src/lib.rs | 56 ++++ substrate/tendermint/client/Cargo.toml | 2 - substrate/tendermint/machine/Cargo.toml | 2 +- substrate/validator-sets/pallet/src/lib.rs | 15 +- .../validator-sets/primitives/Cargo.toml | 7 +- 53 files changed, 1613 insertions(+), 403 deletions(-) create mode 100644 processor/src/tests/monero.rs create mode 100644 processor/src/tests/send.rs create mode 100644 substrate/in-instructions/client/Cargo.toml create mode 100644 substrate/in-instructions/client/LICENSE create mode 100644 substrate/in-instructions/client/src/lib.rs create mode 100644 substrate/in-instructions/pallet/Cargo.toml create mode 100644 substrate/in-instructions/pallet/LICENSE create mode 100644 substrate/in-instructions/pallet/src/lib.rs create mode 100644 substrate/in-instructions/primitives/Cargo.toml create mode 100644 substrate/in-instructions/primitives/LICENSE create mode 100644 substrate/in-instructions/primitives/src/incoming.rs create mode 100644 substrate/in-instructions/primitives/src/lib.rs create mode 100644 substrate/in-instructions/primitives/src/outgoing.rs create mode 100644 substrate/in-instructions/primitives/src/shorthand.rs create mode 100644 substrate/serai/client/Cargo.toml create mode 100644 substrate/serai/client/LICENSE create mode 100644 substrate/serai/client/src/in_instructions.rs create mode 100644 substrate/serai/client/src/lib.rs create mode 100644 substrate/serai/client/tests/runner.rs create mode 100644 substrate/serai/client/tests/updates.rs diff --git a/.github/actions/test-dependencies/action.yml b/.github/actions/test-dependencies/action.yml index 9ce95eaf..6340e946 100644 --- a/.github/actions/test-dependencies/action.yml +++ b/.github/actions/test-dependencies/action.yml @@ -12,6 +12,11 @@ inputs: required: false default: v0.18.0.0 + serai: + description: "Run a Serai development node in the background" + required: false + default: false + runs: using: "composite" steps: @@ -32,3 +37,13 @@ runs: - name: Run a Monero Wallet-RPC uses: ./.github/actions/monero-wallet-rpc + + - name: Run a Serai Development Node + if: ${{ inputs.serai }} + shell: bash + run: | + cd substrate/node + cargo build + cd ../.. + + ./target/debug/serai-node --dev & diff --git a/.github/nightly-version b/.github/nightly-version index e9bf8776..14a3253f 100644 --- a/.github/nightly-version +++ b/.github/nightly-version @@ -1 +1 @@ -nightly-2022-12-01 +nightly-2023-01-16 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 079e1fa1..d7e7ff80 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -58,6 +58,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Build node + run: | + cd substrate/node + cargo build + - name: Run Tests run: cargo test --all-features diff --git a/Cargo.lock b/Cargo.lock index 5b3d0ec6..0487ff80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,6 +1698,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_builder" version = "0.11.2" @@ -2170,7 +2181,6 @@ dependencies = [ "ethers-solc", "eyre", "group", - "hex-literal", "k256", "modular-frost", "rand_core 0.6.4", @@ -3414,6 +3424,7 @@ dependencies = [ "rustls-native-certs", "tokio", "tokio-rustls", + "webpki-roots", ] [[package]] @@ -3547,6 +3558,46 @@ dependencies = [ "syn", ] +[[package]] +name = "in-instructions-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "in-instructions-pallet", + "jsonrpsee-core", + "jsonrpsee-http-client", + "parity-scale-codec", + "sp-inherents", +] + +[[package]] +name = "in-instructions-pallet" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "in-instructions-primitives", + "parity-scale-codec", + "scale-info", + "serai-primitives", + "serde", + "sp-inherents", + "sp-runtime", + "sp-std", + "thiserror", +] + +[[package]] +name = "in-instructions-primitives" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serai-primitives", + "serde", + "sp-core", +] + [[package]] name = "indenter" version = "0.3.3" @@ -3731,13 +3782,36 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d291e3a5818a2384645fd9756362e6d89cf0541b0b916fa7702ea4a9833608e" dependencies = [ + "jsonrpsee-client-transport", "jsonrpsee-core", + "jsonrpsee-http-client", "jsonrpsee-proc-macros", "jsonrpsee-server", "jsonrpsee-types", "tracing", ] +[[package]] +name = "jsonrpsee-client-transport" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965de52763f2004bc91ac5bcec504192440f0b568a5d621c59d9dbd6f886c3fb" +dependencies = [ + "futures-util", + "http", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "rustls-native-certs", + "soketto", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "webpki-roots", +] + [[package]] name = "jsonrpsee-core" version = "0.16.2" @@ -3746,9 +3820,11 @@ checksum = "a4e70b4439a751a5de7dd5ed55eacff78ebf4ffe0fc009cb1ebb11417f5b536b" dependencies = [ "anyhow", "arrayvec 0.7.2", + "async-lock", "async-trait", "beef", "futures-channel", + "futures-timer", "futures-util", "globset", "hyper", @@ -3764,6 +3840,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonrpsee-http-client" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc345b0a43c6bc49b947ebeb936e886a419ee3d894421790c969cc56040542ad" +dependencies = [ + "async-trait", + "hyper", + "hyper-rustls", + "jsonrpsee-core", + "jsonrpsee-types", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "jsonrpsee-proc-macros" version = "0.16.2" @@ -4832,7 +4927,6 @@ name = "monero-serai" version = "0.1.2-alpha" dependencies = [ "base58-monero", - "blake2", "curve25519-dalek 3.2.0", "dalek-ff-group", "digest_auth", @@ -7477,12 +7571,10 @@ dependencies = [ "sc-block-builder", "sc-client-api", "sc-consensus", - "sc-executor", "sc-network", "sc-network-common", "sc-network-gossip", "sc-service", - "sc-transaction-pool", "sp-api", "sp-application-crypto", "sp-blockchain", @@ -7594,6 +7686,29 @@ dependencies = [ "prometheus", ] +[[package]] +name = "scale-bits" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd7aca73785181cc41f0bbe017263e682b585ca660540ba569133901d013ecf" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "scale-decode" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d823d4be477fc33321f93d08fb6c2698273d044f01362dc27573a750deb7c233" +dependencies = [ + "parity-scale-codec", + "scale-bits", + "scale-info", + "thiserror", +] + [[package]] name = "scale-info" version = "2.3.1" @@ -7620,6 +7735,23 @@ dependencies = [ "syn", ] +[[package]] +name = "scale-value" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a5e7810815bd295da73e4216d1dfbced3c7c7c7054d70fa5f6e4c58123fff4" +dependencies = [ + "either", + "frame-metadata", + "parity-scale-codec", + "scale-bits", + "scale-decode", + "scale-info", + "serde", + "thiserror", + "yap", +] + [[package]] name = "schannel" version = "0.1.21" @@ -7823,6 +7955,23 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930c0acf610d3fdb5e2ab6213019aaa04e227ebe9547b0649ba599b16d788bd7" +[[package]] +name = "serai-client" +version = "0.1.0" +dependencies = [ + "in-instructions-primitives", + "jsonrpsee-server", + "lazy_static", + "parity-scale-codec", + "scale-value", + "serai-primitives", + "serai-runtime", + "serde", + "subxt", + "thiserror", + "tokio", +] + [[package]] name = "serai-node" version = "0.1.0" @@ -7831,12 +7980,8 @@ dependencies = [ "clap 4.1.1", "frame-benchmarking", "frame-benchmarking-cli", - "frame-system", - "futures", + "in-instructions-client", "jsonrpsee", - "log", - "pallet-tendermint", - "pallet-transaction-payment", "pallet-transaction-payment-rpc", "sc-basic-authorship", "sc-cli", @@ -7844,31 +7989,24 @@ dependencies = [ "sc-client-db", "sc-consensus", "sc-executor", - "sc-keystore", "sc-network", - "sc-rpc", "sc-rpc-api", "sc-service", "sc-telemetry", "sc-tendermint", "sc-transaction-pool", "sc-transaction-pool-api", - "serai-primitives", "serai-runtime", "sp-api", - "sp-application-crypto", "sp-block-builder", "sp-blockchain", "sp-consensus", "sp-core", "sp-inherents", "sp-keyring", - "sp-keystore", "sp-runtime", - "sp-tendermint", "substrate-build-script-utils", "substrate-frame-rpc-system", - "validator-sets-pallet", ] [[package]] @@ -7879,7 +8017,6 @@ dependencies = [ "scale-info", "serde", "sp-core", - "sp-std", ] [[package]] @@ -7913,6 +8050,7 @@ dependencies = [ "frame-system", "frame-system-rpc-runtime-api", "hex-literal", + "in-instructions-pallet", "pallet-assets", "pallet-balances", "pallet-session", @@ -7923,7 +8061,6 @@ dependencies = [ "scale-info", "serai-primitives", "sp-api", - "sp-application-crypto", "sp-block-builder", "sp-core", "sp-inherents", @@ -9016,6 +9153,79 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "subxt" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3cbc78fd36035a24883eada29e0205b9b1416172530a7d00a60c07d0337db0c" +dependencies = [ + "bitvec 1.0.1", + "derivative", + "frame-metadata", + "futures", + "getrandom 0.2.8", + "hex", + "jsonrpsee", + "parity-scale-codec", + "parking_lot 0.12.1", + "scale-decode", + "scale-info", + "scale-value", + "serde", + "serde_json", + "sp-core", + "sp-runtime", + "subxt-macro", + "subxt-metadata", + "thiserror", + "tracing", +] + +[[package]] +name = "subxt-codegen" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7722c31febf55eb300c73d977da5d65cfd6fb443419b1185b9abcdd9925fd7be" +dependencies = [ + "darling", + "frame-metadata", + "heck 0.4.0", + "hex", + "jsonrpsee", + "parity-scale-codec", + "proc-macro-error", + "proc-macro2", + "quote", + "scale-info", + "subxt-metadata", + "syn", + "tokio", +] + +[[package]] +name = "subxt-macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f64826f2c4ba20e3b2a86ec81a6ae8655ca6b6a4c2a6ccc888b6615efc2df14" +dependencies = [ + "darling", + "proc-macro-error", + "subxt-codegen", + "syn", +] + +[[package]] +name = "subxt-metadata" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869af75e23513538ad0af046af4a97b8d684e8d202e35ff4127ee061c1110813" +dependencies = [ + "frame-metadata", + "parity-scale-codec", + "scale-info", + "sp-core", +] + [[package]] name = "svm-rs" version = "0.2.19" @@ -9798,8 +10008,6 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-core", - "sp-std", ] [[package]] @@ -10789,6 +10997,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "yap" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc77f52dc9e9b10d55d3f4462c3b7fc393c4f17975d641542833ab2d3bc26ef" + [[package]] name = "yasna" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 7cdec0c1..34f7afce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,11 @@ members = [ "processor", "substrate/serai/primitives", + "substrate/serai/client", + + "substrate/in-instructions/primitives", + "substrate/in-instructions/pallet", + "substrate/in-instructions/client", "substrate/validator-sets/primitives", "substrate/validator-sets/pallet", @@ -37,7 +42,7 @@ members = [ ] # Always compile Monero (and a variety of dependencies) with optimizations due -# to the unoptimized performance of Bulletproofs +# to the extensive operations required for Bulletproofs [profile.dev.package] subtle = { opt-level = 3 } curve25519-dalek = { opt-level = 3 } @@ -55,3 +60,8 @@ monero-serai = { opt-level = 3 } [profile.release] panic = "unwind" + +# Required for subxt +[patch.crates-io] +sp-core = { git = "https://github.com/serai-dex/substrate" } +sp-runtime = { git = "https://github.com/serai-dex/substrate" } diff --git a/coins/ethereum/Cargo.toml b/coins/ethereum/Cargo.toml index 9d59efd8..aaf4f41b 100644 --- a/coins/ethereum/Cargo.toml +++ b/coins/ethereum/Cargo.toml @@ -13,7 +13,6 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -hex-literal = "0.3" thiserror = "1" rand_core = "0.6" diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 50f95958..9aa30105 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -25,11 +25,10 @@ zeroize = { version = "1.5", features = ["zeroize_derive"] } subtle = "2.4" sha3 = "0.10" -blake2 = { version = "0.10", optional = true } curve25519-dalek = { version = "3", features = ["std"] } -group = { version = "0.12" } +group = "0.12" dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.1" } multiexp = { path = "../../crypto/multiexp", version = "0.2", features = ["batch"] } @@ -60,4 +59,4 @@ monero-rpc = "0.3" frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.5", features = ["ed25519", "tests"] } [features] -multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"] +multisig = ["rand_chacha", "transcript", "frost", "dleq"] diff --git a/coins/monero/src/tests/clsag.rs b/coins/monero/src/tests/clsag.rs index 3becb414..258457d6 100644 --- a/coins/monero/src/tests/clsag.rs +++ b/coins/monero/src/tests/clsag.rs @@ -63,7 +63,7 @@ fn clsag() { Commitment::new(secrets.1, AMOUNT), Decoys { i: u8::try_from(real).unwrap(), - offsets: (1 ..= RING_LEN).into_iter().collect(), + offsets: (1 ..= RING_LEN).collect(), ring: ring.clone(), }, ) @@ -107,11 +107,7 @@ fn clsag_multisig() { Arc::new(RwLock::new(Some(ClsagDetails::new( ClsagInput::new( Commitment::new(randomness, AMOUNT), - Decoys { - i: RING_INDEX, - offsets: (1 ..= RING_LEN).into_iter().collect(), - ring: ring.clone(), - }, + Decoys { i: RING_INDEX, offsets: (1 ..= RING_LEN).collect(), ring: ring.clone() }, ) .unwrap(), mask_sum, diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index 3ba990f4..d665928e 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -248,7 +248,7 @@ impl SignMachine for TransactionSignMachine { // Find out who's included // This may not be a valid set of signers yet the algorithm machine will error if it's not commitments.remove(&self.i); // Remove, if it was included for some reason - let mut included = commitments.keys().into_iter().cloned().collect::>(); + let mut included = commitments.keys().cloned().collect::>(); included.push(self.i); included.sort_unstable(); diff --git a/coins/monero/tests/runner.rs b/coins/monero/tests/runner.rs index 6861889a..5eac16b5 100644 --- a/coins/monero/tests/runner.rs +++ b/coins/monero/tests/runner.rs @@ -59,6 +59,7 @@ pub async fn mine_until_unlocked(rpc: &Rpc, addr: &str, tx_hash: [u8; 32]) { } // Mines 60 blocks and returns an unlocked miner TX output. +#[allow(dead_code)] pub async fn get_miner_tx_output(rpc: &Rpc, view: &ViewPair) -> SpendableOutput { let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); diff --git a/deny.toml b/deny.toml index 18354eda..336ab699 100644 --- a/deny.toml +++ b/deny.toml @@ -48,6 +48,9 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-processor" }, + { allow = ["AGPL-3.0"], name = "in-instructions-pallet" }, + { allow = ["AGPL-3.0"], name = "in-instructions-client" }, + { allow = ["AGPL-3.0"], name = "validator-sets-pallet" }, { allow = ["AGPL-3.0"], name = "sp-tendermint" }, @@ -56,6 +59,8 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-runtime" }, { allow = ["AGPL-3.0"], name = "serai-node" }, + + { allow = ["AGPL-3.0"], name = "serai-client" }, ] [[licenses.clarify]] diff --git a/docs/Serai.md b/docs/Serai.md index d2e21d2e..04043f16 100644 --- a/docs/Serai.md +++ b/docs/Serai.md @@ -1,8 +1,8 @@ # Serai Serai is a decentralized execution layer whose validators form multisig wallets -for various connected networks, offering secure decentralized custody of foreign -assets to applications built on it. +for various connected networks, offering secure decentralized control of foreign +coins to applications built on it. Serai is exemplified by Serai DEX, an automated-market-maker (AMM) decentralized exchange, allowing swapping Bitcoin, Ether, DAI, and Monero. It is the premier diff --git a/docs/integrations/Ethereum.md b/docs/integrations/Ethereum.md index 48dbec75..e66a1f5b 100644 --- a/docs/integrations/Ethereum.md +++ b/docs/integrations/Ethereum.md @@ -9,11 +9,11 @@ Ethereum addresses are 20-byte hashes. Ethereum In Instructions are present via being appended to the calldata transferring funds to Serai. `origin` is automatically set to the party from which funds are being transferred. For an ERC20, this is `from`. For ETH, this -is the caller. `data` is limited to 255 bytes. +is the caller. ### Out Instructions -`data` is limited to 255 bytes. +`data` is limited to 512 bytes. If `data` is provided, the Ethereum Router will call a contract-calling child contract in order to sandbox it. The first byte of `data` designates which child diff --git a/docs/integrations/Instructions.md b/docs/integrations/Instructions.md index 18c82396..cec67eb2 100644 --- a/docs/integrations/Instructions.md +++ b/docs/integrations/Instructions.md @@ -3,90 +3,94 @@ Instructions are used to communicate with networks connected to Serai, and they come in two forms: - - In Instructions are [Application Calls](../Serai.md#application-calls), -paired with incoming funds. Encoded in transactions on connected networks, -Serai will parse out instructions when it receives funds, executing the included -calls. + - In Instructions are programmable specifications paired with incoming coins, +encoded into transactions on connected networks. Serai will parse included +instructions when it receives coins, executing the included specs. - - Out Instructions detail how to transfer assets, either to a Serai address or -an address native to the asset in question. + - Out Instructions detail how to transfer coins, either to a Serai address or +an address native to the coin in question. A transaction containing an In Instruction and an Out Instruction (to a native -address) will receive funds to Serai and send funds from Serai, without +address) will receive coins to Serai and send coins from Serai, without requiring directly performing any transactions on Serai itself. All instructions are encoded under [Shorthand](#shorthand). Shorthand provides frequent use cases to create minimal data representations on connected networks. Instructions are interpreted according to their non-Serai network. Addresses -have no validation performed, beyond being a valid enum entry (when applicable) -of the correct length, unless otherwise noted. If the processor is instructed to -act on invalid data, or send to itself, it will drop the entire instruction. +have no validation performed unless otherwise noted. If the processor is +instructed to act on invalid data, it will drop the entire instruction. ### Serialization - - Numbers are exclusively unsigned and encoded as compact integers under -SCALE. - - Enums are prefixed by an ordinal byte of their type, followed by their -actual values. - - Vectors are prefixed by their length. - - In Instruction fields are numbered and sequentially encoded, allowing -omission, each prefixed by an ordinal byte. This is due to its fields being more -frequently omitted than not, making their presence what's notable. - - All other types have their fields sequentially encoded with no markers. +Instructions are SCALE encoded. -Certain fields may be omitted depending on the network in question. +### Application Call -### In Instructions + - `application` (u16): The application of Serai to call. Currently, only 0, +Serai DEX is valid. + - `data` (Data): The data to call the application with. - - `origin` (Address): Address from the network of origin which sent funds in. - - `target` (Address): The ink! contract to transfer the incoming funds to. - - `data` (Vec\): The data to call `target` with. +### In Instruction + +InInstruction is an enum of SeraiAddress and ApplicationCall. + +The specified target will be minted an appropriate amount of the respective +Serai token. If an Application Call, the encoded call will be executed. + +### Refundable In Instruction + + - `origin` (Option\): Address, from the network of origin, +which sent coins in. + - `instruction` (InInstruction): The action to perform with the incoming +coins. Networks may automatically provide `origin`. If they do, the instruction may -still provide `origin`, overriding the automatically provided value. If no -`origin` is provided, the instruction is dropped. +still provide `origin`, overriding the automatically provided value. -Upon receiving funds, the respective Serai Asset contract is called, minting the -appropriate amount of coins, and transferring them to `target`, calling it with -the attached data. +If the instruction fails, coins are scheduled to be returned to `origin`, +if provided. -If the instruction fails, funds are scheduled to be returned to `origin`. +### Destination -### Out Instructions +Destination is an enum of SeraiAddress and ExternalAddress. - - `destination` (Enum { Native(Address), Serai(Address) }): Address to receive -funds to. - - `data` (Option\>): The data to call -the target with. +### Out Instruction -Transfer the funds included with this instruction to the specified address with -the specified data. Asset contracts perform no validation on native -addresses/data. + - `destination` (Destination): Address to receive coins to. + - `data` (Option\): The data to call the destination with. + +Transfer the coins included with this instruction to the specified address with +the specified data. No validation of external addresses/data is performed +on-chain. If data is specified for a chain not supporting data, it is silently +dropped. ### Shorthand -Shorthand is an enum which expands to an In Instruction. +Shorthand is an enum which expands to an Refundable In Instruction. ##### Raw -Raw Shorthand encodes a raw In Instruction with no further processing. This is -a verbose fallback option for infrequent use cases not covered by Shorthand. +Raw Shorthand encodes a raw Refundable In Instruction in a Data, with no further +processing. This is a verbose fallback option for infrequent use cases not +covered by Shorthand. ##### Swap - - `origin` (Option\
): In Instruction's `origin`. - - `coin` (Coin): Coin to swap funds for. - - `minimum` (Amount): Minimum amount of `coin` to receive. - - `out` (Out Instruction): Final destination for funds. + - `origin` (Option\): Refundable In Instruction's `origin`. + - `coin` (Coin): Coin to swap funds for. + - `minimum` (Amount): Minimum amount of `coin` to receive. + - `out` (Out Instruction): Final destination for funds. which expands to: ``` -In Instruction { +RefundableInInstruction { origin, - target: Router, - data: swap(Incoming Asset, out, minimum) + instruction: ApplicationCall { + application: DEX, + data: swap(Incoming Asset, coin, minimum, out) + } } ``` @@ -99,19 +103,23 @@ where `swap` is a function which: ##### Add Liquidity - - `origin` (Option\
): In Instruction's `origin`. - - `minimum` (Amount): Minimum amount of SRI to receive. - - `gas` (Amount): Amount of SRI to send to `address` to cover -gas in the future. - - `address` (Address): Account to send the created liquidity tokens. + - `origin` (Option\): Refundable In Instruction's `origin`. + - `minimum` (Amount): Minimum amount of SRI tokens to swap +half for. + - `gas` (Amount): Amount of SRI to send to `address` to +cover gas in the future. + - `address` (Address): Account to send the created liquidity +tokens. which expands to: ``` -In Instruction { +RefundableInInstruction { origin, - target: Router, - data: swap_and_add_liquidity(Incoming Asset, address, minimum, gas) + instruction: ApplicationCall { + application: DEX, + data: swap_and_add_liquidity(Incoming Asset, minimum, gas, address) + } } ``` @@ -120,5 +128,5 @@ where `swap_and_add_liquidity` is a function which: 1) Swaps half of the incoming funds for SRI. 2) Checks the amount of SRI received is greater than `minimum`. 3) Calls `swap_and_add_liquidity` with the amount of SRI received - `gas`, and -a matching amount of the incoming asset. +a matching amount of the incoming coin. 4) Transfers any leftover funds to `address`. diff --git a/docs/protocol/Constants.md b/docs/protocol/Constants.md index 88c09f4e..8ed0524f 100644 --- a/docs/protocol/Constants.md +++ b/docs/protocol/Constants.md @@ -5,14 +5,17 @@ These are the list of types used to represent various properties within the protocol. -| Alias | Type | -|------------------------|--------------------------------| -| Amount | u64 | -| Coin | u32 | -| Session | u32 | -| Validator Set Index | u16 | -| Validator Set Instance | (Session, Validator Set Index) | -| Key | Vec\ | +| Alias | Type | +|------------------------|----------------------------------------------| +| SeraiAddress | sr25519::Public (unchecked [u8; 32] wrapper) | +| Amount | u64 | +| Coin | u32 | +| Session | u32 | +| Validator Set Index | u16 | +| Validator Set Instance | (Session, Validator Set Index) | +| Key | BoundedVec\ | +| ExternalAddress | BoundedVec\ | +| Data | BoundedVec\ | ### Networks @@ -36,7 +39,8 @@ Coins exist over a network and have a distinct integer ID. | Coin | Network | ID | |----------|----------|----| -| Bitcoin | Bitcoin | 0 | -| Ether | Ethereum | 1 | -| DAI | Ethereum | 2 | -| Monero | Monero | 3 | +| Serai | Serai | 0 | +| Bitcoin | Bitcoin | 1 | +| Ether | Ethereum | 2 | +| DAI | Ethereum | 3 | +| Monero | Monero | 4 | diff --git a/processor/Cargo.toml b/processor/Cargo.toml index 695b16e4..c7760bab 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -14,26 +14,28 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +# Macros async-trait = "0.1" zeroize = "1.5" thiserror = "1" rand_core = "0.6" +# Cryptography group = "0.12" - curve25519-dalek = { version = "3", features = ["std"] } - -transcript = { package = "flexible-transcript", path = "../crypto/transcript", features = ["recommended"] } dalek-ff-group = { path = "../crypto/dalek-ff-group" } + +transcript = { package = "flexible-transcript", path = "../crypto/transcript" } frost = { package = "modular-frost", path = "../crypto/frost", features = ["ed25519"] } +# Monero monero-serai = { path = "../coins/monero", features = ["multisig"] } [dev-dependencies] rand_core = "0.6" hex = "0.4" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1", features = ["derive"] } serde_json = "1.0" futures = "0.3" diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 577fb205..e8abeade 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,113 +1,4 @@ -use std::{ - sync::{Arc, RwLock}, - collections::HashMap, -}; +mod send; +pub(crate) use send::test_send; -use async_trait::async_trait; - -use rand_core::OsRng; - -use crate::{ - NetworkError, Network, - coin::{Coin, Monero}, - wallet::{WalletKeys, MemCoinDb, Wallet}, -}; - -#[derive(Clone)] -struct LocalNetwork { - i: u16, - size: u16, - round: usize, - #[allow(clippy::type_complexity)] - rounds: Arc>>>>, -} - -impl LocalNetwork { - fn new(size: u16) -> Vec { - let rounds = Arc::new(RwLock::new(vec![])); - let mut res = vec![]; - for i in 1 ..= size { - res.push(LocalNetwork { i, size, round: 0, rounds: rounds.clone() }); - } - res - } -} - -#[async_trait] -impl Network for LocalNetwork { - async fn round(&mut self, data: Vec) -> Result>, NetworkError> { - { - let mut rounds = self.rounds.write().unwrap(); - if rounds.len() == self.round { - rounds.push(HashMap::new()); - } - rounds[self.round].insert(self.i, data); - } - - while { - let read = self.rounds.try_read().unwrap(); - read[self.round].len() != usize::from(self.size) - } { - tokio::task::yield_now().await; - } - - let mut res = self.rounds.try_read().unwrap()[self.round].clone(); - res.remove(&self.i); - self.round += 1; - Ok(res) - } -} - -async fn test_send(coin: C, fee: C::Fee) { - // Mine blocks so there's a confirmed block - coin.mine_block().await; - let latest = coin.get_latest_block_number().await.unwrap(); - - let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng); - let threshold = keys[&1].params().t(); - let mut networks = LocalNetwork::new(threshold); - - let mut wallets = vec![]; - for i in 1 ..= threshold { - let mut wallet = Wallet::new(MemCoinDb::new(), coin.clone()); - wallet.acknowledge_block(0, latest); - wallet.add_keys(&WalletKeys::new(keys.remove(&i).unwrap(), 0)); - wallets.push(wallet); - } - - // Get the chain to a length where blocks have sufficient confirmations - while (latest + (C::CONFIRMATIONS - 1)) > coin.get_latest_block_number().await.unwrap() { - coin.mine_block().await; - } - - for wallet in wallets.iter_mut() { - // Poll to activate the keys - wallet.poll().await.unwrap(); - } - - coin.test_send(wallets[0].address()).await; - - let mut futures = vec![]; - for (network, wallet) in networks.iter_mut().zip(wallets.iter_mut()) { - wallet.poll().await.unwrap(); - - let latest = coin.get_latest_block_number().await.unwrap(); - wallet.acknowledge_block(1, latest - (C::CONFIRMATIONS - 1)); - let signable = wallet - .prepare_sends(1, vec![(wallet.address(), 10000000000)], fee) - .await - .unwrap() - .1 - .swap_remove(0); - futures.push(wallet.attempt_send(network, signable)); - } - - println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0)); -} - -#[tokio::test] -async fn monero() { - let monero = Monero::new("http://127.0.0.1:18081".to_string()).await; - let fee = monero.get_fee().await; - test_send(monero, fee).await; -} +mod monero; diff --git a/processor/src/tests/monero.rs b/processor/src/tests/monero.rs new file mode 100644 index 00000000..68b8a621 --- /dev/null +++ b/processor/src/tests/monero.rs @@ -0,0 +1,11 @@ +use crate::{ + coin::{Coin, Monero}, + tests::test_send, +}; + +#[tokio::test] +async fn monero() { + let monero = Monero::new("http://127.0.0.1:18081".to_string()).await; + let fee = monero.get_fee().await; + test_send(monero, fee).await; +} diff --git a/processor/src/tests/send.rs b/processor/src/tests/send.rs new file mode 100644 index 00000000..3f0a5b55 --- /dev/null +++ b/processor/src/tests/send.rs @@ -0,0 +1,106 @@ +use std::{ + sync::{Arc, RwLock}, + collections::HashMap, +}; + +use async_trait::async_trait; + +use rand_core::OsRng; + +use crate::{ + NetworkError, Network, + coin::Coin, + wallet::{WalletKeys, MemCoinDb, Wallet}, +}; + +#[derive(Clone)] +struct LocalNetwork { + i: u16, + size: u16, + round: usize, + #[allow(clippy::type_complexity)] + rounds: Arc>>>>, +} + +impl LocalNetwork { + fn new(size: u16) -> Vec { + let rounds = Arc::new(RwLock::new(vec![])); + let mut res = vec![]; + for i in 1 ..= size { + res.push(LocalNetwork { i, size, round: 0, rounds: rounds.clone() }); + } + res + } +} + +#[async_trait] +impl Network for LocalNetwork { + async fn round(&mut self, data: Vec) -> Result>, NetworkError> { + { + let mut rounds = self.rounds.write().unwrap(); + if rounds.len() == self.round { + rounds.push(HashMap::new()); + } + rounds[self.round].insert(self.i, data); + } + + while { + let read = self.rounds.try_read().unwrap(); + read[self.round].len() != usize::from(self.size) + } { + tokio::task::yield_now().await; + } + + let mut res = self.rounds.try_read().unwrap()[self.round].clone(); + res.remove(&self.i); + self.round += 1; + Ok(res) + } +} + +pub async fn test_send(coin: C, fee: C::Fee) { + // Mine blocks so there's a confirmed block + coin.mine_block().await; + let latest = coin.get_latest_block_number().await.unwrap(); + + let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng); + let threshold = keys[&1].params().t(); + let mut networks = LocalNetwork::new(threshold); + + let mut wallets = vec![]; + for i in 1 ..= threshold { + let mut wallet = Wallet::new(MemCoinDb::new(), coin.clone()); + wallet.acknowledge_block(0, latest); + wallet.add_keys(&WalletKeys::new(keys.remove(&i).unwrap(), 0)); + wallets.push(wallet); + } + + // Get the chain to a length where blocks have sufficient confirmations + while (latest + (C::CONFIRMATIONS - 1)) > coin.get_latest_block_number().await.unwrap() { + coin.mine_block().await; + } + + for wallet in wallets.iter_mut() { + // Poll to activate the keys + wallet.poll().await.unwrap(); + } + + coin.test_send(wallets[0].address()).await; + + let mut futures = vec![]; + for (network, wallet) in networks.iter_mut().zip(wallets.iter_mut()) { + wallet.poll().await.unwrap(); + + let latest = coin.get_latest_block_number().await.unwrap(); + wallet.acknowledge_block(1, latest - (C::CONFIRMATIONS - 1)); + let signable = wallet + .prepare_sends(1, vec![(wallet.address(), 10000000000)], fee) + .await + .unwrap() + .1 + .swap_remove(0); + futures.push(wallet.attempt_send(network, signable)); + } + + println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0)); +} diff --git a/substrate/in-instructions/client/Cargo.toml b/substrate/in-instructions/client/Cargo.toml new file mode 100644 index 00000000..d5cda871 --- /dev/null +++ b/substrate/in-instructions/client/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "in-instructions-client" +version = "0.1.0" +description = "Package In Instructions into inherent transactions" +license = "AGPL-3.0-only" +authors = ["Luke Parker "] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +async-trait = "0.1" + +scale = { package = "parity-scale-codec", version = "3", features = ["derive", "max-encoded-len"] } + +jsonrpsee-core = "0.16" +jsonrpsee-http-client = "0.16" + +sp-inherents = { git = "https://github.com/serai-dex/substrate" } + +in-instructions-pallet = { path = "../pallet" } diff --git a/substrate/in-instructions/client/LICENSE b/substrate/in-instructions/client/LICENSE new file mode 100644 index 00000000..c425427c --- /dev/null +++ b/substrate/in-instructions/client/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2023 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/substrate/in-instructions/client/src/lib.rs b/substrate/in-instructions/client/src/lib.rs new file mode 100644 index 00000000..1571f999 --- /dev/null +++ b/substrate/in-instructions/client/src/lib.rs @@ -0,0 +1,47 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +use scale::Decode; + +use jsonrpsee_core::client::ClientT; +use jsonrpsee_http_client::HttpClientBuilder; + +use sp_inherents::{Error, InherentData, InherentIdentifier}; + +use in_instructions_pallet::{INHERENT_IDENTIFIER, Updates, InherentError}; + +pub struct InherentDataProvider; +impl InherentDataProvider { + #[allow(clippy::new_without_default)] // This isn't planned to forever have empty arguments + pub fn new() -> InherentDataProvider { + InherentDataProvider + } +} + +#[async_trait::async_trait] +impl sp_inherents::InherentDataProvider for InherentDataProvider { + async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { + let updates: Updates = (|| async { + let client = HttpClientBuilder::default().build("http://127.0.0.1:5134")?; + client.request("processor_coinUpdates", Vec::::new()).await + })() + .await + .map_err(|e| { + Error::Application(Box::from(format!("couldn't communicate with processor: {e}"))) + })?; + inherent_data.put_data(INHERENT_IDENTIFIER, &updates)?; + Ok(()) + } + + async fn try_handle_error( + &self, + identifier: &InherentIdentifier, + mut error: &[u8], + ) -> Option> { + if *identifier != INHERENT_IDENTIFIER { + return None; + } + + Some(Err(Error::Application(Box::from(::decode(&mut error).ok()?)))) + } +} diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml new file mode 100644 index 00000000..7895997e --- /dev/null +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "in-instructions-pallet" +version = "0.1.0" +description = "Execute calls via In Instructions from inherent transactions" +license = "AGPL-3.0-only" +authors = ["Luke Parker "] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +thiserror = { version = "1", optional = true } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "max-encoded-len"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } + +serde = { version = "1", optional = true } + +sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-inherents = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } + +frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } +frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } + +serai-primitives = { path = "../../serai/primitives", default-features = false } +in-instructions-primitives = { path = "../primitives", default-features = false } + +[features] +std = [ + "thiserror", + + "scale/std", + "scale-info/std", + + "serde", + + "sp-std/std", + "sp-inherents/std", + "sp-runtime/std", + + "frame-system/std", + "frame-support/std", + + "serai-primitives/std", + "in-instructions-primitives/std", +] +default = ["std"] diff --git a/substrate/in-instructions/pallet/LICENSE b/substrate/in-instructions/pallet/LICENSE new file mode 100644 index 00000000..c425427c --- /dev/null +++ b/substrate/in-instructions/pallet/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2023 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/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs new file mode 100644 index 00000000..7b5d5f8b --- /dev/null +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -0,0 +1,240 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +use scale::{Encode, Decode}; +use scale_info::TypeInfo; + +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_std::vec::Vec; +use sp_inherents::{InherentIdentifier, IsFatalError}; + +use sp_runtime::RuntimeDebug; + +use serai_primitives::{BlockNumber, BlockHash, Coin}; + +pub use in_instructions_primitives as primitives; +use primitives::InInstruction; + +pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"ininstrs"; + +#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct Batch { + pub id: BlockHash, + pub instructions: Vec, +} + +#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct Update { + // Coin's latest block number + pub block_number: BlockNumber, + pub batches: Vec, +} + +// None if the current block producer isn't operating over this coin or otherwise failed to get +// data +pub type Updates = Vec>; + +#[derive(Clone, Copy, Encode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Decode, thiserror::Error))] +pub enum InherentError { + #[cfg_attr(feature = "std", error("invalid call"))] + InvalidCall, + #[cfg_attr(feature = "std", error("inherent has {0} updates despite us having {1} coins"))] + InvalidUpdateQuantity(u32, u32), + #[cfg_attr( + feature = "std", + error("inherent for coin {0:?} has block number {1:?} despite us having {2:?}") + )] + UnrecognizedBlockNumber(Coin, BlockNumber, BlockNumber), + #[cfg_attr( + feature = "std", + error("inherent for coin {0:?} has block number {1:?} which doesn't succeed {2:?}") + )] + InvalidBlockNumber(Coin, BlockNumber, BlockNumber), + #[cfg_attr(feature = "std", error("coin {0:?} has {1} more batches than we do"))] + UnrecognizedBatches(Coin, u32), + #[cfg_attr(feature = "std", error("coin {0:?} has a different batch (ID {1:?})"))] + DifferentBatch(Coin, BlockHash), +} + +impl IsFatalError for InherentError { + fn is_fatal_error(&self) -> bool { + match self { + InherentError::InvalidCall | InherentError::InvalidUpdateQuantity(..) => true, + InherentError::UnrecognizedBlockNumber(..) => false, + InherentError::InvalidBlockNumber(..) => true, + InherentError::UnrecognizedBatches(..) => false, + // One of our nodes is definitively wrong. If it's ours (signified by it passing consensus), + // we should panic. If it's theirs, they should be slashed + // Unfortunately, we can't return fatal here to trigger a slash as fatal should only be used + // for undeniable, technical invalidity + // TODO: Code a way in which this still triggers a slash vote + InherentError::DifferentBatch(..) => false, + } + } +} + +fn coin_from_index(index: usize) -> Coin { + // Offset by 1 since Serai is the first coin, yet Serai doesn't have updates + Coin::from(1 + u32::try_from(index).unwrap()) +} + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + Batch { coin: Coin, id: BlockHash }, + } + + #[pallet::pallet] + #[pallet::generate_store(pub(crate) trait Store)] + pub struct Pallet(PhantomData); + + // Used to only allow one set of updates per block, preventing double updating + #[pallet::storage] + pub(crate) type Once = StorageValue<_, bool, ValueQuery>; + // Latest block number agreed upon for a coin + #[pallet::storage] + #[pallet::getter(fn block_number)] + pub(crate) type BlockNumbers = + StorageMap<_, Blake2_256, Coin, BlockNumber, ValueQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_: BlockNumberFor) { + Once::::take(); + } + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight((0, DispatchClass::Mandatory))] // TODO + pub fn execute(origin: OriginFor, updates: Updates) -> DispatchResult { + ensure_none(origin)?; + assert!(!Once::::exists()); + Once::::put(true); + + for (coin, update) in updates.iter().enumerate() { + if let Some(update) = update { + let coin = coin_from_index(coin); + BlockNumbers::::insert(coin, update.block_number); + + for batch in &update.batches { + // TODO: EXECUTE + Self::deposit_event(Event::Batch { coin, id: batch.id }); + } + } + } + + Ok(()) + } + } + + #[pallet::inherent] + impl ProvideInherent for Pallet { + type Call = Call; + type Error = InherentError; + const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; + + fn create_inherent(data: &InherentData) -> Option { + data + .get_data::(&INHERENT_IDENTIFIER) + .unwrap() + .map(|updates| Call::execute { updates }) + } + + // Assumes that only not yet handled batches are provided as inherent data + fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> { + // First unwrap is for the Result of fetching/decoding the Updates + // Second unwrap is for the Option of if they exist + let expected = data.get_data::(&INHERENT_IDENTIFIER).unwrap().unwrap(); + // Match to be exhaustive + let updates = match call { + Call::execute { ref updates } => updates, + _ => Err(InherentError::InvalidCall)?, + }; + + // The block producer should've provided one update per coin + // We, an honest node, did provide one update per coin + // Accordingly, we should have the same amount of updates + if updates.len() != expected.len() { + Err(InherentError::InvalidUpdateQuantity( + updates.len().try_into().unwrap(), + expected.len().try_into().unwrap(), + ))?; + } + + // This zip is safe since we verified they're equally sized + // This should be written as coins.zip(updates.iter().zip(&expected)), where coins is the + // validator set's coins + // That'd require having context on the validator set right now which isn't worth pulling in + // right now, when we only have one validator set + for (coin, both) in updates.iter().zip(&expected).enumerate() { + let coin = coin_from_index(coin); + match both { + // Block producer claims there's an update for this coin, as do we + (Some(update), Some(expected)) => { + if update.block_number.0 > expected.block_number.0 { + Err(InherentError::UnrecognizedBlockNumber( + coin, + update.block_number, + expected.block_number, + ))?; + } + + let prev = BlockNumbers::::get(coin); + if update.block_number.0 <= prev.0 { + Err(InherentError::InvalidBlockNumber(coin, update.block_number, prev))?; + } + + if update.batches.len() > expected.batches.len() { + Err(InherentError::UnrecognizedBatches( + coin, + (update.batches.len() - expected.batches.len()).try_into().unwrap(), + ))?; + } + + for (batch, expected) in update.batches.iter().zip(&expected.batches) { + if batch != expected { + Err(InherentError::DifferentBatch(coin, batch.id))?; + } + } + } + + // Block producer claims there's an update for this coin, yet we don't + (Some(update), None) => { + Err(InherentError::UnrecognizedBatches(coin, update.batches.len().try_into().unwrap()))? + } + + // Block producer didn't include update for this coin + (None, _) => (), + }; + } + + Ok(()) + } + + fn is_inherent(_: &Self::Call) -> bool { + true + } + } +} + +pub use pallet::*; diff --git a/substrate/in-instructions/primitives/Cargo.toml b/substrate/in-instructions/primitives/Cargo.toml new file mode 100644 index 00000000..c2d4c788 --- /dev/null +++ b/substrate/in-instructions/primitives/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "in-instructions-primitives" +version = "0.1.0" +description = "Serai instructions library, enabling encoding and decoding" +license = "MIT" +authors = ["Luke Parker "] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } + +serde = { version = "1", features = ["derive"], optional = true } + +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } + +serai-primitives = { path = "../../serai/primitives", default-features = false } + +[features] +std = ["scale/std", "scale-info/std", "serde", "sp-core/std", "serai-primitives/std"] +default = ["std"] diff --git a/substrate/in-instructions/primitives/LICENSE b/substrate/in-instructions/primitives/LICENSE new file mode 100644 index 00000000..6779f0ec --- /dev/null +++ b/substrate/in-instructions/primitives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/substrate/in-instructions/primitives/src/incoming.rs b/substrate/in-instructions/primitives/src/incoming.rs new file mode 100644 index 00000000..0a685a11 --- /dev/null +++ b/substrate/in-instructions/primitives/src/incoming.rs @@ -0,0 +1,38 @@ +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; + +use serai_primitives::SeraiAddress; + +use crate::{MAX_DATA_LEN, ExternalAddress}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Application { + DEX, +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct ApplicationCall { + application: Application, + data: BoundedVec>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum InInstruction { + Transfer(SeraiAddress), + Call(ApplicationCall), +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct RefundableInInstruction { + pub origin: Option, + pub instruction: InInstruction, +} diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs new file mode 100644 index 00000000..befc593e --- /dev/null +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -0,0 +1,47 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; + +// Monero, our current longest address candidate, has a longest address of featured with payment ID +// 1 (enum) + 1 (flags) + 64 (two keys) + 8 (payment ID) = 74 +pub const MAX_ADDRESS_LEN: u32 = 74; +// Should be enough for a Uniswap v3 call +pub const MAX_DATA_LEN: u32 = 512; + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct ExternalAddress(BoundedVec>); +impl ExternalAddress { + #[cfg(feature = "std")] + pub fn new(address: Vec) -> Result { + Ok(ExternalAddress(address.try_into().map_err(|_| "address length exceeds {MAX_ADDRESS_LEN}")?)) + } + + pub fn address(&self) -> &[u8] { + self.0.as_ref() + } + + #[cfg(feature = "std")] + pub fn consume(self) -> Vec { + self.0.into_inner() + } +} + +// Not "in" as "in" is a keyword +mod incoming; +pub use incoming::*; + +// Not "out" to match in +mod outgoing; +pub use outgoing::*; + +mod shorthand; +pub use shorthand::*; diff --git a/substrate/in-instructions/primitives/src/outgoing.rs b/substrate/in-instructions/primitives/src/outgoing.rs new file mode 100644 index 00000000..2e8cf426 --- /dev/null +++ b/substrate/in-instructions/primitives/src/outgoing.rs @@ -0,0 +1,25 @@ +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; + +use serai_primitives::SeraiAddress; + +use crate::{MAX_DATA_LEN, ExternalAddress}; + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Destination { + Native(SeraiAddress), + External(ExternalAddress), +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct OutInstruction { + destination: Destination, + data: Option>>, +} diff --git a/substrate/in-instructions/primitives/src/shorthand.rs b/substrate/in-instructions/primitives/src/shorthand.rs new file mode 100644 index 00000000..e3944510 --- /dev/null +++ b/substrate/in-instructions/primitives/src/shorthand.rs @@ -0,0 +1,54 @@ +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; + +use serai_primitives::{SeraiAddress, Coin, Amount}; + +use crate::{MAX_DATA_LEN, ExternalAddress, RefundableInInstruction, InInstruction, OutInstruction}; + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Shorthand { + Raw(BoundedVec>), + Swap { + origin: Option, + coin: Coin, + minimum: Amount, + out: OutInstruction, + }, + AddLiquidity { + origin: Option, + minimum: Amount, + gas: Amount, + address: SeraiAddress, + }, +} + +impl Shorthand { + pub fn transfer(origin: Option, address: SeraiAddress) -> Option { + Some(Self::Raw( + BoundedVec::try_from( + (RefundableInInstruction { origin, instruction: InInstruction::Transfer(address) }) + .encode(), + ) + .ok()?, + )) + } +} + +impl TryFrom for RefundableInInstruction { + type Error = &'static str; + fn try_from(shorthand: Shorthand) -> Result { + Ok(match shorthand { + Shorthand::Raw(raw) => { + RefundableInInstruction::decode(&mut raw.as_ref()).map_err(|_| "invalid raw instruction")? + } + Shorthand::Swap { .. } => todo!(), + Shorthand::AddLiquidity { .. } => todo!(), + }) + } +} diff --git a/substrate/node/Cargo.toml b/substrate/node/Cargo.toml index c20ddee2..dd51b15c 100644 --- a/substrate/node/Cargo.toml +++ b/substrate/node/Cargo.toml @@ -14,17 +14,11 @@ name = "serai-node" [dependencies] async-trait = "0.1" -log = "0.4" - -futures = { version = "0.3" } - clap = { version = "4", features = ["derive"] } jsonrpsee = { version = "0.16", features = ["server"] } sp-core = { git = "https://github.com/serai-dex/substrate" } -sp-application-crypto = { git = "https://github.com/serai-dex/substrate" } -sp-keystore = { git = "https://github.com/serai-dex/substrate" } sp-keyring = { git = "https://github.com/serai-dex/substrate" } sp-inherents = { git = "https://github.com/serai-dex/substrate" } sp-runtime = { git = "https://github.com/serai-dex/substrate" } @@ -33,7 +27,11 @@ sp-api = { git = "https://github.com/serai-dex/substrate" } sp-block-builder = { git = "https://github.com/serai-dex/substrate" } sp-consensus = { git = "https://github.com/serai-dex/substrate" } -sc-keystore = { git = "https://github.com/serai-dex/substrate" } +frame-benchmarking = { git = "https://github.com/serai-dex/substrate" } +frame-benchmarking-cli = { git = "https://github.com/serai-dex/substrate" } + +serai-runtime = { path = "../runtime" } + sc-transaction-pool = { git = "https://github.com/serai-dex/substrate" } sc-transaction-pool-api = { git = "https://github.com/serai-dex/substrate" } sc-basic-authorship = { git = "https://github.com/serai-dex/substrate" } @@ -47,24 +45,13 @@ sc-consensus = { git = "https://github.com/serai-dex/substrate" } sc-telemetry = { git = "https://github.com/serai-dex/substrate" } sc-cli = { git = "https://github.com/serai-dex/substrate" } -frame-system = { git = "https://github.com/serai-dex/substrate" } -frame-benchmarking = { git = "https://github.com/serai-dex/substrate" } -frame-benchmarking-cli = { git = "https://github.com/serai-dex/substrate" } -pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false } - -sc-rpc = { git = "https://github.com/serai-dex/substrate" } sc-rpc-api = { git = "https://github.com/serai-dex/substrate" } substrate-frame-rpc-system = { git = "https://github.com/serai-dex/substrate" } pallet-transaction-payment-rpc = { git = "https://github.com/serai-dex/substrate" } -serai-primitives = { path = "../serai/primitives" } +in-instructions-client = { path = "../in-instructions/client" } -validator-sets-pallet = { path = "../validator-sets/pallet" } - -sp-tendermint = { path = "../tendermint/primitives" } -pallet-tendermint = { path = "../tendermint/pallet", default-features = false } -serai-runtime = { path = "../runtime" } sc-tendermint = { path = "../tendermint/client" } [build-dependencies] diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index 58aa9321..63ca7a05 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -3,12 +3,9 @@ use sp_runtime::traits::TrailingZeroInput; use sc_service::ChainType; -use serai_primitives::*; -use pallet_tendermint::crypto::Public; - use serai_runtime::{ - WASM_BINARY, AccountId, opaque::SessionKeys, GenesisConfig, SystemConfig, BalancesConfig, - AssetsConfig, ValidatorSetsConfig, SessionConfig, + primitives::*, tendermint::crypto::Public, WASM_BINARY, opaque::SessionKeys, GenesisConfig, + SystemConfig, BalancesConfig, AssetsConfig, ValidatorSetsConfig, SessionConfig, }; pub type ChainSpec = sc_service::GenericChainSpec; @@ -17,22 +14,22 @@ fn insecure_pair_from_name(name: &'static str) -> Pair { Pair::from_string(&format!("//{name}"), None).unwrap() } -fn account_id_from_name(name: &'static str) -> AccountId { +fn address_from_name(name: &'static str) -> SeraiAddress { insecure_pair_from_name(name).public() } fn testnet_genesis( wasm_binary: &[u8], validators: &[&'static str], - endowed_accounts: Vec, + endowed_accounts: Vec, ) -> GenesisConfig { let session_key = |name| { - let key = account_id_from_name(name); + let key = address_from_name(name); (key, key, SessionKeys { tendermint: Public::from(key) }) }; // TODO: Replace with a call to the pallet to ask for its account - let owner = AccountId::decode(&mut TrailingZeroInput::new(b"tokens")).unwrap(); + let owner = SeraiAddress::decode(&mut TrailingZeroInput::new(b"tokens")).unwrap(); GenesisConfig { system: SystemConfig { code: wasm_binary.to_vec() }, @@ -54,8 +51,8 @@ fn testnet_genesis( validator_sets: ValidatorSetsConfig { bond: Amount(1_000_000) * COIN, - coins: Coin(4), - participants: validators.iter().map(|name| account_id_from_name(name)).collect(), + coins: vec![BITCOIN, ETHER, DAI, MONERO], + participants: validators.iter().map(|name| address_from_name(name)).collect(), }, session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() }, } @@ -75,18 +72,18 @@ pub fn development_config() -> Result { wasm_binary, &["Alice"], vec![ - account_id_from_name("Alice"), - account_id_from_name("Bob"), - account_id_from_name("Charlie"), - account_id_from_name("Dave"), - account_id_from_name("Eve"), - account_id_from_name("Ferdie"), - account_id_from_name("Alice//stash"), - account_id_from_name("Bob//stash"), - account_id_from_name("Charlie//stash"), - account_id_from_name("Dave//stash"), - account_id_from_name("Eve//stash"), - account_id_from_name("Ferdie//stash"), + address_from_name("Alice"), + address_from_name("Bob"), + address_from_name("Charlie"), + address_from_name("Dave"), + address_from_name("Eve"), + address_from_name("Ferdie"), + address_from_name("Alice//stash"), + address_from_name("Bob//stash"), + address_from_name("Charlie//stash"), + address_from_name("Dave//stash"), + address_from_name("Eve//stash"), + address_from_name("Ferdie//stash"), ], ) }, @@ -119,18 +116,18 @@ pub fn testnet_config() -> Result { wasm_binary, &["Alice", "Bob", "Charlie"], vec![ - account_id_from_name("Alice"), - account_id_from_name("Bob"), - account_id_from_name("Charlie"), - account_id_from_name("Dave"), - account_id_from_name("Eve"), - account_id_from_name("Ferdie"), - account_id_from_name("Alice//stash"), - account_id_from_name("Bob//stash"), - account_id_from_name("Charlie//stash"), - account_id_from_name("Dave//stash"), - account_id_from_name("Eve//stash"), - account_id_from_name("Ferdie//stash"), + address_from_name("Alice"), + address_from_name("Bob"), + address_from_name("Charlie"), + address_from_name("Dave"), + address_from_name("Eve"), + address_from_name("Ferdie"), + address_from_name("Alice//stash"), + address_from_name("Bob//stash"), + address_from_name("Charlie//stash"), + address_from_name("Dave//stash"), + address_from_name("Eve//stash"), + address_from_name("Ferdie//stash"), ], ) }, diff --git a/substrate/node/src/command.rs b/substrate/node/src/command.rs index 73fc1b31..20cd0e29 100644 --- a/substrate/node/src/command.rs +++ b/substrate/node/src/command.rs @@ -1,9 +1,10 @@ -use sc_service::{PruningMode, PartialComponents}; -use frame_benchmarking_cli::{ExtrinsicFactory, BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE}; -use sc_cli::{ChainSpec, RuntimeVersion, SubstrateCli}; - use serai_runtime::Block; +use sc_service::{PruningMode, PartialComponents}; + +use sc_cli::{ChainSpec, RuntimeVersion, SubstrateCli}; +use frame_benchmarking_cli::{ExtrinsicFactory, BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE}; + use crate::{ chain_spec, cli::{Cli, Subcommand}, diff --git a/substrate/node/src/command_helper.rs b/substrate/node/src/command_helper.rs index fc649357..7d944b69 100644 --- a/substrate/node/src/command_helper.rs +++ b/substrate/node/src/command_helper.rs @@ -9,8 +9,11 @@ use sp_runtime::OpaqueExtrinsic; use sc_cli::Result; use sc_client_api::BlockBackend; -use serai_runtime as runtime; -use runtime::SystemCall; +use serai_runtime::{ + VERSION, BlockHashCount, + system::{self, Call as SystemCall}, + transaction_payment, RuntimeCall, UncheckedExtrinsic, SignedPayload, Runtime, +}; use crate::service::FullClient; @@ -45,35 +48,33 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder { pub fn create_benchmark_extrinsic( client: &FullClient, sender: sp_core::sr25519::Pair, - call: runtime::RuntimeCall, + call: RuntimeCall, nonce: u32, -) -> runtime::UncheckedExtrinsic { +) -> UncheckedExtrinsic { let extra = ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( - u64::from( - runtime::BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2), - ), + system::CheckNonZeroSender::::new(), + system::CheckSpecVersion::::new(), + system::CheckTxVersion::::new(), + system::CheckGenesis::::new(), + system::CheckEra::::from(sp_runtime::generic::Era::mortal( + u64::from(BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2)), client.chain_info().best_number.into(), )), - frame_system::CheckNonce::::from(nonce), - frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(0), + system::CheckNonce::::from(nonce), + system::CheckWeight::::new(), + transaction_payment::ChargeTransactionPayment::::from(0), ); - runtime::UncheckedExtrinsic::new_signed( + UncheckedExtrinsic::new_signed( call.clone(), sender.public(), - runtime::SignedPayload::from_raw( + SignedPayload::from_raw( call, extra.clone(), ( (), - runtime::VERSION.spec_version, - runtime::VERSION.transaction_version, + VERSION.spec_version, + VERSION.transaction_version, client.block_hash(0).ok().flatten().unwrap(), client.chain_info().best_hash, (), diff --git a/substrate/node/src/rpc.rs b/substrate/node/src/rpc.rs index 076c8dc2..c5df739d 100644 --- a/substrate/node/src/rpc.rs +++ b/substrate/node/src/rpc.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use jsonrpsee::RpcModule; use sp_blockchain::{Error as BlockchainError, HeaderBackend, HeaderMetadata}; -use sc_transaction_pool_api::TransactionPool; use sp_block_builder::BlockBuilder; use sp_api::ProvideRuntimeApi; -pub use sc_rpc_api::DenyUnsafe; +use serai_runtime::{primitives::SeraiAddress, opaque::Block, Balance, Index}; -use serai_runtime::{opaque::Block, AccountId, Balance, Index}; +pub use sc_rpc_api::DenyUnsafe; +use sc_transaction_pool_api::TransactionPool; pub struct FullDeps { pub client: Arc, @@ -29,7 +29,7 @@ pub fn create_full< deps: FullDeps, ) -> Result, Box> where - C::Api: substrate_frame_rpc_system::AccountNonceApi + C::Api: substrate_frame_rpc_system::AccountNonceApi + pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi + BlockBuilder, { diff --git a/substrate/node/src/service.rs b/substrate/node/src/service.rs index f063641d..f6aa1e1f 100644 --- a/substrate/node/src/service.rs +++ b/substrate/node/src/service.rs @@ -11,6 +11,8 @@ use sp_inherents::CreateInherentDataProviders; use sp_consensus::DisableProofRecording; use sp_api::ProvideRuntimeApi; +use in_instructions_client::InherentDataProvider as InstructionsProvider; + use sc_executor::{NativeVersion, NativeExecutionDispatch, NativeElseWasmExecutor}; use sc_transaction_pool::FullPool; use sc_network::NetworkService; @@ -24,7 +26,7 @@ pub(crate) use sc_tendermint::{ TendermintClientMinimal, TendermintValidator, TendermintImport, TendermintAuthority, TendermintSelectChain, import_queue, }; -use serai_runtime::{self, BLOCK_SIZE, TARGET_BLOCK_TIME, opaque::Block, RuntimeApi}; +use serai_runtime::{self as runtime, BLOCK_SIZE, TARGET_BLOCK_TIME, opaque::Block, RuntimeApi}; type FullBackend = sc_service::TFullBackend; pub type FullClient = TFullClient>; @@ -46,7 +48,7 @@ impl NativeExecutionDispatch for ExecutorDispatch { type ExtendHostFunctions = (); fn dispatch(method: &str, data: &[u8]) -> Option> { - serai_runtime::api::dispatch(method, data) + runtime::api::dispatch(method, data) } fn native_version() -> NativeVersion { @@ -57,13 +59,13 @@ impl NativeExecutionDispatch for ExecutorDispatch { pub struct Cidp; #[async_trait::async_trait] impl CreateInherentDataProviders for Cidp { - type InherentDataProviders = (); + type InherentDataProviders = (InstructionsProvider,); async fn create_inherent_data_providers( &self, _: ::Hash, _: (), ) -> Result> { - Ok(()) + Ok((InstructionsProvider::new(),)) } } diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 78a5f32f..a379fdb9 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -12,13 +12,12 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -hex-literal = { version = "0.3.4", optional = true } +hex-literal = { version = "0.3", optional = true } codec = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-version = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-inherents = { git = "https://github.com/serai-dex/substrate", default-features = false } @@ -42,6 +41,8 @@ pallet-balances = { git = "https://github.com/serai-dex/substrate", default-feat pallet-assets = { git = "https://github.com/serai-dex/substrate", default-features = false } pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false } +in-instructions-pallet = { path = "../in-instructions/pallet", default-features = false } + validator-sets-pallet = { path = "../validator-sets/pallet", default-features = false } pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } pallet-tendermint = { path = "../tendermint/pallet", default-features = false } @@ -58,7 +59,6 @@ std = [ "scale-info/std", "sp-core/std", - "sp-application-crypto/std", "sp-std/std", "sp-version/std", "sp-inherents/std", @@ -81,6 +81,8 @@ std = [ "pallet-assets/std", "pallet-transaction-payment/std", + "in-instructions-pallet/std", + "validator-sets-pallet/std", "pallet-session/std", "pallet-tendermint/std", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 2f792076..91d48750 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -1,23 +1,46 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(not(feature = "std"), no_std)] #![recursion_limit = "256"] #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +// Re-export all components +pub use serai_primitives as primitives; + +pub use frame_system as system; +pub use frame_support as support; + +pub use pallet_balances as balances; +pub use pallet_transaction_payment as transaction_payment; + +pub use pallet_assets as assets; +pub use in_instructions_pallet as in_instructions; + +pub use validator_sets_pallet as validator_sets; + +pub use pallet_session as session; +pub use pallet_tendermint as tendermint; + +// Actually used by the runtime use sp_core::OpaqueMetadata; -pub use sp_core::sr25519::{Public, Signature}; +use sp_std::prelude::*; + +use sp_version::RuntimeVersion; +#[cfg(feature = "std")] +use sp_version::NativeVersion; + use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, KeyTypeId, traits::{Convert, OpaqueKeys, IdentityLookup, BlakeTwo256, Block as BlockT}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, Perbill, }; -use sp_std::prelude::*; -#[cfg(feature = "std")] -use sp_version::NativeVersion; -use sp_version::RuntimeVersion; -use frame_support::{ +use primitives::{PublicKey, Signature, SeraiAddress, Coin}; + +use support::{ traits::{ConstU8, ConstU32, ConstU64}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -25,22 +48,14 @@ use frame_support::{ }, parameter_types, construct_runtime, }; -pub use frame_system::Call as SystemCall; -use serai_primitives::Coin; +use transaction_payment::CurrencyAdapter; -pub use pallet_balances::Call as BalancesCall; -pub use pallet_assets::Call as AssetsCall; -use pallet_transaction_payment::CurrencyAdapter; - -use pallet_session::PeriodicSessions; +use session::PeriodicSessions; /// An index to a block. pub type BlockNumber = u32; -/// Account ID type, equivalent to a public key -pub type AccountId = Public; - /// Balance of an account. // Distinct from serai-primitives Amount due to Substrate's requirements on this type. // If Amount could be dropped in here, it would be. @@ -58,7 +73,7 @@ pub type Hash = sp_core::H256; pub mod opaque { use super::*; - pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; + use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; pub type Header = generic::Header; pub type Block = generic::Block; @@ -110,22 +125,22 @@ parameter_types! { pub const SS58Prefix: u8 = 42; // TODO: Remove for Bech32m // 1 MB block size limit - pub BlockLength: frame_system::limits::BlockLength = - frame_system::limits::BlockLength::max_with_normal_ratio(BLOCK_SIZE, NORMAL_DISPATCH_RATIO); - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::with_sensible_defaults( + pub BlockLength: system::limits::BlockLength = + system::limits::BlockLength::max_with_normal_ratio(BLOCK_SIZE, NORMAL_DISPATCH_RATIO); + pub BlockWeights: system::limits::BlockWeights = + system::limits::BlockWeights::with_sensible_defaults( Weight::from_ref_time(2u64 * WEIGHT_REF_TIME_PER_SECOND).set_proof_size(u64::MAX), NORMAL_DISPATCH_RATIO, ); } -impl frame_system::Config for Runtime { - type BaseCallFilter = frame_support::traits::Everything; +impl system::Config for Runtime { + type BaseCallFilter = support::traits::Everything; type BlockWeights = BlockWeights; type BlockLength = BlockLength; - type AccountId = AccountId; + type AccountId = SeraiAddress; type RuntimeCall = RuntimeCall; - type Lookup = IdentityLookup; + type Lookup = IdentityLookup; type Index = Index; type BlockNumber = BlockNumber; type Hash = Hash; @@ -142,14 +157,14 @@ impl frame_system::Config for Runtime { type OnKilledAccount = (); type OnSetCode = (); - type AccountData = pallet_balances::AccountData; + type AccountData = balances::AccountData; type SystemWeightInfo = (); type SS58Prefix = SS58Prefix; // TODO: Remove for Bech32m - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = support::traits::ConstU32<16>; } -impl pallet_balances::Config for Runtime { +impl balances::Config for Runtime { type MaxLocks = ConstU32<50>; type MaxReserves = (); type ReserveIdentifier = [u8; 8]; @@ -158,10 +173,10 @@ impl pallet_balances::Config for Runtime { type DustRemoval = (); type ExistentialDeposit = ConstU64<500>; type AccountStore = System; - type WeightInfo = pallet_balances::weights::SubstrateWeight; + type WeightInfo = balances::weights::SubstrateWeight; } -impl pallet_assets::Config for Runtime { +impl assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type Currency = Balances; @@ -171,9 +186,8 @@ impl pallet_assets::Config for Runtime { type StringLimit = ConstU32<32>; // Don't allow anyone to create assets - type CreateOrigin = - frame_support::traits::AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; + type CreateOrigin = support::traits::AsEnsureOriginWithArg>; + type ForceOrigin = system::EnsureRoot; // Don't charge fees nor kill accounts type RemoveItemsLimit = ConstU32<0>; @@ -188,12 +202,12 @@ impl pallet_assets::Config for Runtime { type Freezer = (); type Extra = (); - type WeightInfo = pallet_assets::weights::SubstrateWeight; + type WeightInfo = assets::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } -impl pallet_transaction_payment::Config for Runtime { +impl transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = CurrencyAdapter; type OperationalFeeMultiplier = ConstU8<5>; @@ -202,54 +216,57 @@ impl pallet_transaction_payment::Config for Runtime { type FeeMultiplierUpdate = (); } +impl in_instructions::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + const SESSION_LENGTH: BlockNumber = 5 * DAYS; type Sessions = PeriodicSessions, ConstU32<{ SESSION_LENGTH }>>; pub struct IdentityValidatorIdOf; -impl Convert> for IdentityValidatorIdOf { - fn convert(key: Public) -> Option { +impl Convert> for IdentityValidatorIdOf { + fn convert(key: PublicKey) -> Option { Some(key) } } -impl validator_sets_pallet::Config for Runtime { +impl validator_sets::Config for Runtime { type RuntimeEvent = RuntimeEvent; } -impl pallet_session::Config for Runtime { +impl session::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type ValidatorId = AccountId; + type ValidatorId = SeraiAddress; type ValidatorIdOf = IdentityValidatorIdOf; type ShouldEndSession = Sessions; type NextSessionRotation = Sessions; type SessionManager = (); type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; - type WeightInfo = pallet_session::weights::SubstrateWeight; + type WeightInfo = session::weights::SubstrateWeight; } -impl pallet_tendermint::Config for Runtime {} +impl tendermint::Config for Runtime {} -pub type Address = AccountId; pub type Header = generic::Header; pub type Block = generic::Block; pub type SignedExtra = ( - frame_system::CheckNonZeroSender, - frame_system::CheckSpecVersion, - frame_system::CheckTxVersion, - frame_system::CheckGenesis, - frame_system::CheckEra, - frame_system::CheckNonce, - frame_system::CheckWeight, - pallet_transaction_payment::ChargeTransactionPayment, + system::CheckNonZeroSender, + system::CheckSpecVersion, + system::CheckTxVersion, + system::CheckGenesis, + system::CheckEra, + system::CheckNonce, + system::CheckWeight, + transaction_payment::ChargeTransactionPayment, ); pub type UncheckedExtrinsic = - generic::UncheckedExtrinsic; + generic::UncheckedExtrinsic; pub type SignedPayload = generic::SignedPayload; pub type Executive = frame_executive::Executive< Runtime, Block, - frame_system::ChainContext, + system::ChainContext, Runtime, AllPalletsWithSystem, >; @@ -260,14 +277,18 @@ construct_runtime!( NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic { - System: frame_system, - Balances: pallet_balances, - Assets: pallet_assets, - TransactionPayment: pallet_transaction_payment, + System: system, - ValidatorSets: validator_sets_pallet, - Session: pallet_session, - Tendermint: pallet_tendermint, + Balances: balances, + TransactionPayment: transaction_payment, + + Assets: assets, + InInstructions: in_instructions, + + ValidatorSets: validator_sets, + + Session: session, + Tendermint: tendermint, } ); @@ -279,8 +300,8 @@ extern crate frame_benchmarking; mod benches { define_benchmarks!( [frame_benchmarking, BaselineBench::] - [frame_system, SystemBench::] - [pallet_balances, Balances] + [system, SystemBench::] + [balances, Balances] ); } @@ -359,13 +380,13 @@ sp_api::impl_runtime_apis! { Tendermint::session() } - fn validators() -> Vec { + fn validators() -> Vec { Session::validators() } } - impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { - fn account_nonce(account: AccountId) -> Index { + impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { + fn account_nonce(account: SeraiAddress) -> Index { System::account_nonce(account) } } @@ -380,10 +401,11 @@ sp_api::impl_runtime_apis! { ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { TransactionPayment::query_info(uxt, len) } + fn query_fee_details( uxt: ::Extrinsic, len: u32, - ) -> pallet_transaction_payment::FeeDetails { + ) -> transaction_payment::FeeDetails { TransactionPayment::query_fee_details(uxt, len) } } diff --git a/substrate/serai/client/Cargo.toml b/substrate/serai/client/Cargo.toml new file mode 100644 index 00000000..ca35be61 --- /dev/null +++ b/substrate/serai/client/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "serai-client" +version = "0.1.0" +description = "Client library for the Serai network" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/client" +authors = ["Luke Parker "] +keywords = ["serai"] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +thiserror = "1" + +serde = { version = "1", features = ["derive"] } + +scale = { package = "parity-scale-codec", version = "3" } +scale-value = "0.6" +subxt = "0.25" + +serai-primitives = { path = "../primitives", version = "0.1" } +in-instructions-primitives = { path = "../../in-instructions/primitives", version = "0.1" } +serai-runtime = { path = "../../runtime", version = "0.1" } + +[dev-dependencies] +lazy_static = "1" + +tokio = "1" + +jsonrpsee-server = "0.16" diff --git a/substrate/serai/client/LICENSE b/substrate/serai/client/LICENSE new file mode 100644 index 00000000..c425427c --- /dev/null +++ b/substrate/serai/client/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2023 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/substrate/serai/client/src/in_instructions.rs b/substrate/serai/client/src/in_instructions.rs new file mode 100644 index 00000000..8c26b80a --- /dev/null +++ b/substrate/serai/client/src/in_instructions.rs @@ -0,0 +1,49 @@ +use scale::Decode; + +use serai_runtime::{ + support::traits::PalletInfo as PalletInfoTrait, PalletInfo, in_instructions, InInstructions, + Runtime, +}; + +pub use in_instructions_primitives as primitives; + +use crate::{ + primitives::{Coin, BlockNumber}, + Serai, SeraiError, +}; + +const PALLET: &str = "InInstructions"; + +pub type InInstructionsEvent = in_instructions::Event; + +impl Serai { + pub async fn get_batch_events( + &self, + block: [u8; 32], + ) -> Result, SeraiError> { + let mut res = vec![]; + for event in + self.0.events().at(Some(block.into())).await.map_err(|_| SeraiError::RpcError)?.iter() + { + let event = event.map_err(|_| SeraiError::InvalidRuntime)?; + if PalletInfo::index::().unwrap() == usize::from(event.pallet_index()) { + let mut with_variant: &[u8] = + &[[event.variant_index()].as_ref(), event.field_bytes()].concat(); + let event = + InInstructionsEvent::decode(&mut with_variant).map_err(|_| SeraiError::InvalidRuntime)?; + if matches!(event, InInstructionsEvent::Batch { .. }) { + res.push(event); + } + } + } + Ok(res) + } + + pub async fn get_coin_block_number( + &self, + coin: Coin, + block: [u8; 32], + ) -> Result { + Ok(self.storage(PALLET, "BlockNumbers", Some(coin), block).await?.unwrap_or(BlockNumber(0))) + } +} diff --git a/substrate/serai/client/src/lib.rs b/substrate/serai/client/src/lib.rs new file mode 100644 index 00000000..36bfd8a5 --- /dev/null +++ b/substrate/serai/client/src/lib.rs @@ -0,0 +1,77 @@ +use thiserror::Error; + +use serde::Serialize; +use scale::Decode; + +use subxt::{tx::BaseExtrinsicParams, Config as SubxtConfig, OnlineClient}; + +pub use serai_primitives as primitives; +use primitives::{Signature, SeraiAddress}; + +use serai_runtime::{system::Config, Runtime}; + +pub mod in_instructions; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct SeraiConfig; +impl SubxtConfig for SeraiConfig { + type BlockNumber = ::BlockNumber; + + type Hash = ::Hash; + type Hashing = ::Hashing; + + type Index = ::Index; + type AccountId = ::AccountId; + // TODO: Bech32m + type Address = SeraiAddress; + + type Header = ::Header; + type Signature = Signature; + + type ExtrinsicParams = BaseExtrinsicParams; +} + +#[derive(Clone, Error, Debug)] +pub enum SeraiError { + #[error("failed to connect to serai")] + RpcError, + #[error("serai-client library was intended for a different runtime version")] + InvalidRuntime, +} + +#[derive(Clone)] +pub struct Serai(OnlineClient); + +impl Serai { + pub async fn new(url: &str) -> Result { + Ok(Serai(OnlineClient::::from_url(url).await.map_err(|_| SeraiError::RpcError)?)) + } + + async fn storage( + &self, + pallet: &'static str, + name: &'static str, + key: Option, + block: [u8; 32], + ) -> Result, SeraiError> { + let mut keys = vec![]; + if let Some(key) = key { + keys.push(scale_value::serde::to_value(key).unwrap()); + } + + let storage = self.0.storage(); + let address = subxt::dynamic::storage(pallet, name, keys); + debug_assert!(storage.validate(&address).is_ok()); + + storage + .fetch(&address, Some(block.into())) + .await + .map_err(|_| SeraiError::RpcError)? + .map(|res| R::decode(&mut res.encoded()).map_err(|_| SeraiError::InvalidRuntime)) + .transpose() + } + + pub async fn get_latest_block_hash(&self) -> Result<[u8; 32], SeraiError> { + Ok(self.0.rpc().finalized_head().await.map_err(|_| SeraiError::RpcError)?.into()) + } +} diff --git a/substrate/serai/client/tests/runner.rs b/substrate/serai/client/tests/runner.rs new file mode 100644 index 00000000..e5ed4542 --- /dev/null +++ b/substrate/serai/client/tests/runner.rs @@ -0,0 +1,50 @@ +use lazy_static::lazy_static; + +use tokio::sync::Mutex; + +pub const URL: &str = "ws://127.0.0.1:9944"; + +lazy_static! { + pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(()); +} + +#[macro_export] +macro_rules! serai_test { + ($(async fn $name: ident() $body: block)*) => { + $( + #[tokio::test] + async fn $name() { + let guard = runner::SEQUENTIAL.lock().await; + + // Spawn a fresh Serai node + let mut command = { + use core::time::Duration; + use std::{path::Path, process::Command}; + + let node = { + let this_crate = Path::new(env!("CARGO_MANIFEST_DIR")); + let top_level = this_crate.join("../../../"); + top_level.join("target/debug/serai-node") + }; + + let command = Command::new(node).arg("--dev").spawn().unwrap(); + while Serai::new(URL).await.is_err() { + tokio::time::sleep(Duration::from_secs(1)).await; + } + command + }; + + let local = tokio::task::LocalSet::new(); + local.run_until(async move { + if let Err(err) = tokio::task::spawn_local(async move { $body }).await { + drop(guard); + let _ = command.kill(); + Err(err).unwrap() + } else { + command.kill().unwrap(); + } + }).await; + } + )* + } +} diff --git a/substrate/serai/client/tests/updates.rs b/substrate/serai/client/tests/updates.rs new file mode 100644 index 00000000..ba97bc73 --- /dev/null +++ b/substrate/serai/client/tests/updates.rs @@ -0,0 +1,60 @@ +use core::time::Duration; + +use tokio::time::sleep; + +use serai_runtime::in_instructions::{Batch, Update}; + +use jsonrpsee_server::RpcModule; + +use serai_client::{ + primitives::{BlockNumber, BlockHash, SeraiAddress, BITCOIN}, + in_instructions::{primitives::InInstruction, InInstructionsEvent}, + Serai, +}; + +mod runner; +use runner::URL; + +serai_test!( + async fn publish_update() { + let mut rpc = RpcModule::new(()); + rpc + .register_async_method("processor_coinUpdates", |_, _| async move { + let batch = Batch { + id: BlockHash([0xaa; 32]), + instructions: vec![InInstruction::Transfer(SeraiAddress::from_raw([0xff; 32]))], + }; + + Ok(vec![Some(Update { block_number: BlockNumber(123), batches: vec![batch] })]) + }) + .unwrap(); + + let _handle = jsonrpsee_server::ServerBuilder::default() + .build("127.0.0.1:5134") + .await + .unwrap() + .start(rpc) + .unwrap(); + + let serai = Serai::new(URL).await.unwrap(); + loop { + let latest = serai.get_latest_block_hash().await.unwrap(); + let batches = serai.get_batch_events(latest).await.unwrap(); + if let Some(batch) = batches.get(0) { + match batch { + InInstructionsEvent::Batch { coin, id } => { + assert_eq!(coin, &BITCOIN); + assert_eq!(id, &BlockHash([0xaa; 32])); + assert_eq!( + serai.get_coin_block_number(BITCOIN, latest).await.unwrap(), + BlockNumber(123) + ); + return; + } + _ => panic!("get_batches returned non-batch"), + } + } + sleep(Duration::from_millis(50)).await; + } + } +); diff --git a/substrate/serai/primitives/Cargo.toml b/substrate/serai/primitives/Cargo.toml index 5d063638..5d6a1490 100644 --- a/substrate/serai/primitives/Cargo.toml +++ b/substrate/serai/primitives/Cargo.toml @@ -15,11 +15,10 @@ rustdoc-args = ["--cfg", "docsrs"] scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } -serde = { version = "1.0", features = ["derive"], optional = true } +serde = { version = "1", features = ["derive"], optional = true } sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } [features] -std = ["scale/std", "scale-info/std", "serde", "sp-core/std", "sp-std/std"] +std = ["scale/std", "scale-info/std", "serde", "sp-core/std"] default = ["std"] diff --git a/substrate/serai/primitives/src/amount.rs b/substrate/serai/primitives/src/amount.rs index e125e339..e3f0fcaf 100644 --- a/substrate/serai/primitives/src/amount.rs +++ b/substrate/serai/primitives/src/amount.rs @@ -9,7 +9,7 @@ use serde::{Serialize, Deserialize}; /// The type used for amounts. #[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, + Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, MaxEncodedLen, TypeInfo, )] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Amount(pub u64); diff --git a/substrate/serai/primitives/src/coins.rs b/substrate/serai/primitives/src/coins.rs index a1a7af3c..6a587df4 100644 --- a/substrate/serai/primitives/src/coins.rs +++ b/substrate/serai/primitives/src/coins.rs @@ -4,7 +4,7 @@ use scale_info::TypeInfo; use serde::{Serialize, Deserialize}; /// The type used to identify coins. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Coin(pub u32); impl From for Coin { @@ -13,7 +13,8 @@ impl From for Coin { } } -pub const BITCOIN: Coin = Coin(0); -pub const ETHER: Coin = Coin(1); -pub const DAI: Coin = Coin(2); -pub const MONERO: Coin = Coin(3); +pub const SERAI: Coin = Coin(0); +pub const BITCOIN: Coin = Coin(1); +pub const ETHER: Coin = Coin(2); +pub const DAI: Coin = Coin(3); +pub const MONERO: Coin = Coin(4); diff --git a/substrate/serai/primitives/src/lib.rs b/substrate/serai/primitives/src/lib.rs index 65e860b3..2d4dc14b 100644 --- a/substrate/serai/primitives/src/lib.rs +++ b/substrate/serai/primitives/src/lib.rs @@ -1,7 +1,63 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(not(feature = "std"), no_std)] +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ + H256, + sr25519::{Public, Signature as RistrettoSignature}, +}; + mod amount; pub use amount::*; mod coins; pub use coins::*; + +pub type PublicKey = Public; +pub type SeraiAddress = PublicKey; +pub type Signature = RistrettoSignature; + +/// The type used to identify block numbers. +// Doesn't re-export tendermint-machine's due to traits. +#[derive( + Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct BlockNumber(pub u32); +impl From for BlockNumber { + fn from(number: u32) -> BlockNumber { + BlockNumber(number) + } +} + +/// The type used to identify block hashes. +// This may not be universally compatible +// If a block exists with a hash which isn't 32-bytes, it can be hashed into a value with 32-bytes +// This would require the processor to maintain a mapping of 32-byte IDs to actual hashes, which +// would be fine +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct BlockHash(pub [u8; 32]); + +impl AsRef<[u8]> for BlockHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<[u8; 32]> for BlockHash { + fn from(hash: [u8; 32]) -> BlockHash { + BlockHash(hash) + } +} + +impl From for BlockHash { + fn from(hash: H256) -> BlockHash { + BlockHash(hash.into()) + } +} diff --git a/substrate/tendermint/client/Cargo.toml b/substrate/tendermint/client/Cargo.toml index 0698f633..e78f99bb 100644 --- a/substrate/tendermint/client/Cargo.toml +++ b/substrate/tendermint/client/Cargo.toml @@ -33,8 +33,6 @@ sp-consensus = { git = "https://github.com/serai-dex/substrate" } sp-tendermint = { path = "../primitives" } -sc-transaction-pool = { git = "https://github.com/serai-dex/substrate" } -sc-executor = { git = "https://github.com/serai-dex/substrate" } sc-network-common = { git = "https://github.com/serai-dex/substrate" } sc-network = { git = "https://github.com/serai-dex/substrate" } sc-network-gossip = { git = "https://github.com/serai-dex/substrate" } diff --git a/substrate/tendermint/machine/Cargo.toml b/substrate/tendermint/machine/Cargo.toml index 7184e057..4ba9337d 100644 --- a/substrate/tendermint/machine/Cargo.toml +++ b/substrate/tendermint/machine/Cargo.toml @@ -13,7 +13,7 @@ thiserror = "1" log = "0.4" -parity-scale-codec = { version = "3.2", features = ["derive"] } +parity-scale-codec = { version = "3", features = ["derive"] } futures = "0.3" tokio = { version = "1", features = ["macros", "sync", "time", "rt"] } diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 7ac8a700..b99fc2f5 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -17,14 +17,14 @@ pub mod pallet { } #[pallet::genesis_config] - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen)] + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { /// Bond requirement to join the initial validator set. /// Every participant at genesis will automatically be assumed to have this much bond. /// This bond cannot be withdrawn however as there's no stake behind it. pub bond: Amount, - /// Amount of coins to spawn the network with in the initial validator set. - pub coins: Coin, + /// Coins to spawn the network with in the initial validator set. + pub coins: Vec, /// List of participants to place in the genesis set. pub participants: Vec, } @@ -32,7 +32,7 @@ pub mod pallet { #[cfg(feature = "std")] impl Default for GenesisConfig { fn default() -> Self { - GenesisConfig { bond: Amount(1), coins: Coin(0), participants: vec![] } + GenesisConfig { bond: Amount(1), coins: vec![], participants: vec![] } } } @@ -95,11 +95,6 @@ pub mod pallet { #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { - let mut coins = Vec::new(); - for coin in 0 .. self.coins.0 { - coins.push(Coin(coin)); - } - let mut participants = Vec::new(); for participant in self.participants.clone() { participants.push((participant, self.bond)); @@ -109,7 +104,7 @@ pub mod pallet { ValidatorSetInstance(Session(0), ValidatorSetIndex(0)), Some(ValidatorSet { bond: self.bond, - coins: BoundedVec::try_from(coins).unwrap(), + coins: BoundedVec::try_from(self.coins.clone()).unwrap(), participants: BoundedVec::try_from(participants).unwrap(), }), ); diff --git a/substrate/validator-sets/primitives/Cargo.toml b/substrate/validator-sets/primitives/Cargo.toml index 7da45dd2..e458ae95 100644 --- a/substrate/validator-sets/primitives/Cargo.toml +++ b/substrate/validator-sets/primitives/Cargo.toml @@ -15,11 +15,8 @@ rustdoc-args = ["--cfg", "docsrs"] scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } -serde = { version = "1.0", features = ["derive"], optional = true } - -sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } +serde = { version = "1", features = ["derive"], optional = true } [features] -std = ["scale/std", "scale-info/std", "serde", "sp-core/std", "sp-std/std"] +std = ["scale/std", "scale-info/std", "serde"] default = ["std"]