From 47650f19efe2f600133a2ff23837ea29f86aac05 Mon Sep 17 00:00:00 2001 From: dubbelosix Date: Fri, 29 Sep 2023 22:23:07 +0530 Subject: [PATCH] offchain processing macro and example for sov-nft-module (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * offchain processing * BlockScout integration fixes (#917) * Fixes * retesteth config Signed-off-by: Filippo Costa --------- Signed-off-by: Filippo Costa * modules_api: simplify public key definition (#918) * simplify publi key definition * private key * `EVM`: Add missing docs (#914) * rename pending_block to block_env * make evm internals private * refactor query.rs * AccountData doc * query.rs docs * fix exports * add missing docs * cargo fmt * Add readme * Update README.md * Update README.md * better docs * Cleanup prover docs/logs (#919) * Cleanup prever docs/logs * fmt * Bump markdown from 1.0.0-alpha.13 to 1.0.0-alpha.14 (#924) Bumps [markdown](https://github.com/wooorm/markdown-rs) from 1.0.0-alpha.13 to 1.0.0-alpha.14. - [Release notes](https://github.com/wooorm/markdown-rs/releases) - [Commits](https://github.com/wooorm/markdown-rs/compare/1.0.0-alpha.13...1.0.0-alpha.14) --- updated-dependencies: - dependency-name: markdown dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump rayon from 1.7.0 to 1.8.0 (#923) Bumps [rayon](https://github.com/rayon-rs/rayon) from 1.7.0 to 1.8.0. - [Changelog](https://github.com/rayon-rs/rayon/blob/master/RELEASES.md) - [Commits](https://github.com/rayon-rs/rayon/compare/rayon-core-v1.7.0...rayon-core-v1.8.0) --- updated-dependencies: - dependency-name: rayon dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump prost-build from 0.11.9 to 0.12.1 (#930) Bumps [prost-build](https://github.com/tokio-rs/prost) from 0.11.9 to 0.12.1. - [Release notes](https://github.com/tokio-rs/prost/releases) - [Commits](https://github.com/tokio-rs/prost/compare/v0.11.9...v0.12.1) --- updated-dependencies: - dependency-name: prost-build dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump parking_lot from 0.11.2 to 0.12.1 (#927) Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.11.2 to 0.12.1. - [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md) - [Commits](https://github.com/Amanieu/parking_lot/compare/0.11.2...0.12.1) --- updated-dependencies: - dependency-name: parking_lot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: use json as manifest constants instead of toml (#922) * feat: use json as manifest constants instead of toml Currently, we parse the manifest file as TOML. However, we will use JSON for the genesis format. For improved consistency, it is desirable to have the constants manifest file with the same format. * fix CI lints * use parent as argument for error reporting * refactor gas config parse to return declaration * fix ci lints * Bump tungstenite from 0.20.0 to 0.20.1 (#931) Bumps [tungstenite](https://github.com/snapview/tungstenite-rs) from 0.20.0 to 0.20.1. - [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.20.0...v0.20.1) --- updated-dependencies: - dependency-name: tungstenite dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * EVM: Implement eth_call (#921) * implement eth_call * implement tests and error handling for eth_call * move errors inside evm crate * cleanup result * fix call env * comment test differences * rebase * Revert "rebase" This reverts commit 44e41b23dac2a68839cad896ef6c46c20ca410cc. * fix unused imports * fix review notes * use mix_hash as prevrandao * improve test s * cargo lock * some fixes * move common code into lib * added issue to the nft script * add nft cli binary to nft-utils * Revert "add nft cli binary to nft-utils" This reverts commit d443009db9b438112d388e4e78e19d2dfc19fe3f. * move nft-utils back to sovereign/nft-utils and move sql to sql.rs * remove un-necessary deps in nft-utils * Run tests with prover enabled. (#932) * Remove demo-rollup dep from demo-prover * add methods to demo-rollup * fix cargo hack * fix cargo hack * Cargo.toml * install toolchain * install toolchain * modify workflow * update cargo.toml * rust.yaml * Add guest-mock * use real prover in bank tests * native * fix lint * update yaml * fic coverage * ELF in CI * update bench get_guest_options() * update build.rs * remove local from demo-rollup * fix lint * remove ide_setup * Remove riscv32im-risc0-zkvm-elf * fix: require `native` when applicable for all targets (#937) * fix: require `native` when applicable for all targets Prior to this commit, some checks of all-targets without the feature `native` would break. This commit introduces a fix for every workspace member to be consistent with the feature set. * change feature requirement to self-dev-dep * update deps * fix dupl celestia dependencies * Remove default trait bound from Module (#941) * Remove demo-rollup-local job from CI (#943) * Delta's go first, remove `get` from StateCheckpoint (#953) * Make genesis config serializable (#956) * Make genesis config serializable * Evm config serde * Fix chain state integ test * fix vec-setter * Introduce: `PublicKeyHex` in `sov-modules-api` (#954) * Add PublicKeyHex * PubKeyHex impl * Update DefaultPublicKey::from_str * Add tests * Remove println * Add doc * estimate-gas signed (#947) * remove include and make it a module * fix packages.yml * EVM: Implement account related RPC (#958) * implement account endpoints * test account endpoints * remove unnecessary conversions * feature: gas meter (#795) * feat: add gas meter to working set This commit introduces `GasMeter`, encapsulated by `WorkingSet`. It will allow the user to consume scalar gas from the working set, and define arbitrary price parsed from a constants.json manifest file at compilation. At each compilation, the `ModuleInfo` derive macro will parse such file, and set the gas price configuration. * fix lint fmt * fix ci test expected error string * update default context to 2 dimensions * Read `accounts` genesis from a file. (#959) * PrivateKeyHex in accounts * Add test_config_serialization * Add accounts.json * fix ci * fix CI * cleanup * Arbitrary PublicKeyHex * PublicKeyHex impl * fix a wrong merge conflict --------- Signed-off-by: Filippo Costa Signed-off-by: dependabot[bot] Co-authored-by: Filippo Neysofu Costa Co-authored-by: Blazej Kolad Co-authored-by: Preston Evans <32944016+preston-evans98@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Lopes Co-authored-by: Orkun Mahir Kılıç Co-authored-by: Nikolai Golub --- Cargo.lock | 112 ++++++++++ Cargo.toml | 2 + .../methods/guest-celestia/Cargo.lock | 5 + .../demo-prover/methods/guest-mock/Cargo.lock | 5 + examples/demo-rollup/Cargo.toml | 2 +- examples/demo-stf/Cargo.toml | 1 + full-node/sov-sequencer/src/utils.rs | 33 +++ full-node/sov-stf-runner/src/batch_builder.rs | 8 +- .../sov-nft-module/Cargo.toml | 6 + .../sov-nft-module/offchain_readme.md | 60 ++++++ .../sov-nft-module/src/call.rs | 12 +- .../sov-nft-module/src/init_db.sql | 50 +++++ .../sov-nft-module/src/lib.rs | 3 + .../sov-nft-module/src/offchain.rs | 123 +++++++++++ .../sov-nft-module/src/sql.rs | 30 +++ .../sov-modules-api/src/transaction.rs | 4 +- module-system/sov-modules-macros/src/lib.rs | 39 +++- .../sov-modules-macros/src/offchain.rs | 31 +++ packages_to_publish.yml | 1 + utils/nft-utils/Cargo.toml | 25 +++ utils/nft-utils/src/lib.rs | 201 ++++++++++++++++++ utils/nft-utils/src/main.rs | 105 +++++++++ 22 files changed, 850 insertions(+), 8 deletions(-) create mode 100644 module-system/module-implementations/sov-nft-module/offchain_readme.md create mode 100644 module-system/module-implementations/sov-nft-module/src/init_db.sql create mode 100644 module-system/module-implementations/sov-nft-module/src/offchain.rs create mode 100644 module-system/module-implementations/sov-nft-module/src/sql.rs create mode 100644 module-system/sov-modules-macros/src/offchain.rs create mode 100644 utils/nft-utils/Cargo.toml create mode 100644 utils/nft-utils/src/lib.rs create mode 100644 utils/nft-utils/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2ba866a41..8d7dd54a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,6 +2626,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4273,6 +4279,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nft-utils" +version = "0.2.0" +dependencies = [ + "borsh", + "demo-stf", + "sov-modules-api", + "sov-nft-module", + "sov-rollup-interface", + "sov-sequencer", + "tokio", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -4982,6 +5001,49 @@ dependencies = [ "serde", ] +[[package]] +name = "postgres" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7915b33ed60abc46040cbcaa25ffa1c7ec240668e0477c4f3070786f5916d451" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +dependencies = [ + "base64 0.21.4", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2 0.10.7", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -7441,6 +7503,7 @@ dependencies = [ "anyhow", "borsh", "jsonrpsee", + "postgres", "schemars", "serde", "serde_json", @@ -7451,6 +7514,8 @@ dependencies = [ "sov-rollup-interface", "sov-state", "tempfile", + "tokio", + "tracing", ] [[package]] @@ -7728,6 +7793,17 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.9.3" @@ -8138,6 +8214,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2 0.5.4", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -8830,6 +8932,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wildmatch" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 729d60289..aa556b226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ members = [ "utils/zk-cycle-macros", "utils/zk-cycle-utils", + "utils/nft-utils", "utils/bashtestmd", + "utils/nft-utils", "module-system/sov-cli", "module-system/sov-modules-stf-template", diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index b4574b135..da5807d78 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1854,3 +1854,8 @@ dependencies = [ "quote", "syn 2.0.37", ] + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index d4302d1f0..db5de049c 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -1272,3 +1272,8 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-rollup/Cargo.toml b/examples/demo-rollup/Cargo.toml index b014a7afc..2814df35e 100644 --- a/examples/demo-rollup/Cargo.toml +++ b/examples/demo-rollup/Cargo.toml @@ -79,7 +79,7 @@ native = ["anyhow", "jsonrpsee", "serde", "serde_json", "tracing", "tokio", "tra "sov-state/native", "sov-cli", "clap", "sov-celestia-adapter/native", "sov-db", "sov-sequencer", "sov-stf-runner/native", "sov-modules-api/native", "sov-rollup-interface/native"] bench = ["native", "async-trait", "borsh", "hex"] - +offchain = ["demo-stf/offchain"] [[bench]] name = "rollup_bench" diff --git a/examples/demo-stf/Cargo.toml b/examples/demo-stf/Cargo.toml index 17babeb8f..28a93152e 100644 --- a/examples/demo-stf/Cargo.toml +++ b/examples/demo-stf/Cargo.toml @@ -51,6 +51,7 @@ rand = "0.8" [features] default = [] +offchain = ["sov-nft-module/offchain"] experimental = ["sov-evm/experimental", "reth-primitives"] native = [ "sov-stf-runner/native", diff --git a/full-node/sov-sequencer/src/utils.rs b/full-node/sov-sequencer/src/utils.rs index a4a828ee9..56a42311e 100644 --- a/full-node/sov-sequencer/src/utils.rs +++ b/full-node/sov-sequencer/src/utils.rs @@ -38,6 +38,39 @@ impl SimpleClient { Ok(()) } + /// Sends multiple transactions to the sequencer for immediate publication. + pub async fn send_transactions( + &self, + txs: Vec, + chunk_size: Option, + ) -> Result<(), anyhow::Error> { + let serialized_txs: Vec> = txs + .into_iter() + .map(|tx| tx.try_to_vec()) + .collect::>()?; + + match chunk_size { + Some(batch_size) => { + for chunk in serialized_txs.chunks(batch_size) { + let response: String = self + .http_client + .request("sequencer_publishBatch", chunk.to_vec()) + .await?; + info!("publish batch response for chunk: {:?}", response); + } + } + None => { + let response: String = self + .http_client + .request("sequencer_publishBatch", serialized_txs) + .await?; + info!("publish batch response: {:?}", response); + } + } + + Ok(()) + } + /// Get a reference to the underlying [`HttpClient`] pub fn http(&self) -> &HttpClient { &self.http_client diff --git a/full-node/sov-stf-runner/src/batch_builder.rs b/full-node/sov-stf-runner/src/batch_builder.rs index 47c935c52..e6b3cd009 100644 --- a/full-node/sov-stf-runner/src/batch_builder.rs +++ b/full-node/sov-stf-runner/src/batch_builder.rs @@ -125,14 +125,14 @@ where current_batch_size += tx_len; } - if txs.is_empty() { - bail!("No valid transactions are available"); - } - for (tx, err) in dismissed { warn!("Transaction 0x{} was dismissed: {:?}", hex::encode(tx), err); } + if txs.is_empty() { + bail!("No valid transactions are available"); + } + Ok(txs) } } diff --git a/module-system/module-implementations/sov-nft-module/Cargo.toml b/module-system/module-implementations/sov-nft-module/Cargo.toml index a063098d0..d1f4e0d9f 100644 --- a/module-system/module-implementations/sov-nft-module/Cargo.toml +++ b/module-system/module-implementations/sov-nft-module/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "sov-nft-module" +description = "A Sovereign SDK module for managing non-fungible tokens" version = { workspace = true } edition = { workspace = true } authors = { workspace = true } @@ -21,6 +22,10 @@ sov-modules-api = { path = "../../sov-modules-api" } sov-modules-macros = {path = "../../sov-modules-macros"} sov-state = { path = "../../sov-state" } +postgres = { version = "0.19.7", optional = true } +tokio = { version = "1.32.0", features=["full"], optional = true } +tracing = { workspace = true, optional = true } + [dev-dependencies] sov-rollup-interface = { path = "../../../rollup-interface" } sov-data-generators = { path = "../../utils/sov-data-generators" } @@ -29,5 +34,6 @@ sov-nft-module = { version = "*", features = ["native"], path = "." } [features] default = [] +offchain = ["postgres","tokio","tracing"] native = ["serde_json", "jsonrpsee", "schemars", "sov-state/native", "sov-modules-api/native", ] test = ["native"] diff --git a/module-system/module-implementations/sov-nft-module/offchain_readme.md b/module-system/module-implementations/sov-nft-module/offchain_readme.md new file mode 100644 index 000000000..3a4345ddf --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/offchain_readme.md @@ -0,0 +1,60 @@ +## Offchain testing + +### Introduction +This readme outlines the steps to demonstrate the offchain processing functionality that is part of the `sov-nft-module` + +### Steps +* Install postgres on your system +* Start the postgres terminal +* Create the tables necessary for offchain processing + +```bash +psql postgres -f sovereign/module-system/module-implementations/sov-nft-module/src/init_db.sql +``` +* The above command runs the `init_db.sql` script which creates 3 tables + * `collections` - tracking the NFT collections that have been created, their supply and other info + * `nfts` - tracking the individual NFTs, the `token_uri` pointing to offchain state and other info + * `top_owners` - tracks the number of NFTs of each collection that a user owns + * running the following query can show the top owners for a specific collection + ```sql + SELECT owner, count + FROM top_owners + WHERE collection_address = ( + SELECT collection_address + FROM collections + WHERE collection_name = 'your_collection_name' + ) + ORDER BY count DESC + LIMIT 5; + ``` + * running the following query can show the largest owner for each collection + ```sql + SELECT + collection_name, + owner, + count + FROM ( + SELECT c.collection_name, t.owner, t.count, + RANK() OVER (PARTITION BY t.collection_address ORDER BY t.count DESC) as rank + FROM top_owners t + INNER JOIN collections c ON c.collection_address = t.collection_address + ) sub + WHERE rank = 1; + ``` + +* Run the demo rollup in offchain mode +```bash +rm -rf demo_data; POSTGRES_CONNECTION_STRING="postgresql://username:password@localhost/postgres" cargo run --features offchain -- --da-layer mock +``` +* Explanation of the above command + * `rm -rf demo_data` is to wipe the rollup state. For testing its better to start with clean state + * `POSTGRES_CONNECTION_STRING` is to allow the offchain component of the `sov-nft-module` to connect to postgres instance + * `--features offchain` is necessary to enable offchain processing. Without the feature, the functions are no-ops + * `--da-layer mock` is used to run an in-memory local DA layer +* Run the NFT minting script +```bash +$ cd sovereign/utils/nft-utils +$ cargo run +``` + * The above script creates 3 NFT collections, mints some NFTs to each collection + * The tables can be explored by connecting to postgres and running sample queries from above \ No newline at end of file diff --git a/module-system/module-implementations/sov-nft-module/src/call.rs b/module-system/module-implementations/sov-nft-module/src/call.rs index 095942e78..028d62498 100644 --- a/module-system/module-implementations/sov-nft-module/src/call.rs +++ b/module-system/module-implementations/sov-nft-module/src/call.rs @@ -2,6 +2,7 @@ use anyhow::Result; use sov_modules_api::{CallResponse, Context, WorkingSet}; use crate::address::UserAddress; +use crate::offchain::{update_collection, update_nft}; use crate::{Collection, CollectionAddress, Nft, NftIdentifier, NonFungibleToken, TokenId}; #[cfg_attr( @@ -87,6 +88,7 @@ impl NonFungibleToken { )?; self.collections .set(&collection_address, &collection, working_set); + update_collection(&collection); Ok(CallResponse::default()) } @@ -107,6 +109,7 @@ impl NonFungibleToken { collection.set_collection_uri(collection_uri); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -126,6 +129,7 @@ impl NonFungibleToken { collection.freeze(); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -165,6 +169,9 @@ impl NonFungibleToken { self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); + update_nft(&new_nft, None); + Ok(CallResponse::default()) } @@ -178,12 +185,14 @@ impl NonFungibleToken { ) -> Result { let mut owned_nft = Nft::get_owned_nft(nft_id, collection_address, &self.nfts, context, working_set)?; + let original_owner = owned_nft.inner().get_owner().clone(); owned_nft.set_owner(to); self.nfts.set( &NftIdentifier(nft_id, collection_address.clone()), owned_nft.inner(), working_set, ); + update_nft(owned_nft.inner(), Some(original_owner.clone())); Ok(CallResponse::default()) } @@ -211,10 +220,11 @@ impl NonFungibleToken { mutable_nft.update_token_uri(&uri); } self.nfts.set( - &NftIdentifier(token_id, collection_address), + &NftIdentifier(token_id, collection_address.clone()), mutable_nft.inner(), working_set, ); + update_nft(mutable_nft.inner(), None); Ok(CallResponse::default()) } } diff --git a/module-system/module-implementations/sov-nft-module/src/init_db.sql b/module-system/module-implementations/sov-nft-module/src/init_db.sql new file mode 100644 index 000000000..ed89fddad --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/init_db.sql @@ -0,0 +1,50 @@ +-- Drop existing tables if they exist +DROP TABLE IF EXISTS top_owners CASCADE; +DROP TABLE IF EXISTS nfts CASCADE; +DROP TABLE IF EXISTS collections CASCADE; + +-- Create collection table +CREATE TABLE collections +( + collection_address TEXT PRIMARY KEY, + collection_name TEXT NOT NULL, + creator_address TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + metadata_url TEXT, + supply BIGINT NOT NULL +); + +-- Create index on creator_address to quickly find collections by a creator +CREATE INDEX idx_creator ON collections (creator_address); + +-- Create nft table +CREATE TABLE nfts +( + collection_address TEXT NOT NULL, + nft_id BIGINT NOT NULL, + metadata_url TEXT, + owner TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + PRIMARY KEY (collection_address, nft_id) +); + +-- Create index on owner to quickly find NFTs owned by a particular address +CREATE INDEX idx_nft_owner ON nfts (owner); + +-- Create index on collection_address to quickly find NFTs belonging to a particular collection +CREATE INDEX idx_nft_collection ON nfts (collection_address); + +-- Create top_owners table +CREATE TABLE top_owners +( + owner TEXT NOT NULL, + collection_address TEXT NOT NULL, + count BIGINT NOT NULL, + PRIMARY KEY (owner, collection_address) +); + +-- Create index on collection_address to quickly find top owners in a particular collection +CREATE INDEX idx_top_owners_collection ON top_owners (collection_address); + +-- Create index on count to quickly find top owners by count (optional) +CREATE INDEX idx_top_owners_count ON top_owners (count); diff --git a/module-system/module-implementations/sov-nft-module/src/lib.rs b/module-system/module-implementations/sov-nft-module/src/lib.rs index 65df31d34..b1d26129b 100644 --- a/module-system/module-implementations/sov-nft-module/src/lib.rs +++ b/module-system/module-implementations/sov-nft-module/src/lib.rs @@ -16,6 +16,9 @@ mod query; pub use query::*; use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Context, Error, Module, ModuleInfo, StateMap, WorkingSet}; +mod offchain; +#[cfg(feature = "offchain")] +mod sql; /// Utility functions. pub mod utils; diff --git a/module-system/module-implementations/sov-nft-module/src/offchain.rs b/module-system/module-implementations/sov-nft-module/src/offchain.rs new file mode 100644 index 000000000..a2d6a192c --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/offchain.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "offchain")] +use postgres::NoTls; +use sov_modules_macros::offchain; + +#[cfg(feature = "offchain")] +use crate::sql::*; +#[cfg(feature = "offchain")] +use crate::utils::get_collection_address; +#[cfg(feature = "offchain")] +use crate::CollectionAddress; +use crate::{Collection, Nft, OwnerAddress}; + +/// Syncs a collection to the corresponding table "collections" in postgres +#[offchain] +pub fn update_collection(collection: &Collection) { + // Extract the necessary metadata from the collection + let collection_name = collection.get_name(); + let creator_address = collection.get_creator(); + let frozen = collection.is_frozen(); + let metadata_url = collection.get_collection_uri(); + let supply = collection.get_supply(); + let collection_address: CollectionAddress = + get_collection_address(collection_name, creator_address.as_ref()); + let collection_address_str = collection_address.to_string(); + let creator_address_str = creator_address.to_string(); + // postgres insert + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + match postgres::Client::connect(&conn_string, NoTls) { + Ok(mut client) => { + let result = client.execute( + INSERT_OR_UPDATE_COLLECTION, + &[ + &collection_address_str, + &collection_name, + &creator_address_str, + &frozen, + &metadata_url, + &(supply as i64), + ], + ); + if let Err(e) = result { + tracing::error!("Failed to execute query: {}", e); + } + } + Err(e) => { + tracing::error!("Failed to connect to the database: {}", e); + } + } + } else { + tracing::error!("Environment variable POSTGRES_CONNECTION_STRING is not set"); + } + }) +} + +/// Syncs an NFT to the corresponding table "nfts" in postgres +/// Additionally, this function also has logic to track the counts of NFTs held by each user +/// in each collection. +#[offchain] +pub fn update_nft(nft: &Nft, old_owner: Option>) { + let collection_address = nft.get_collection_address().to_string(); + let nft_id = nft.get_token_id(); + let new_owner_str = nft.get_owner().to_string(); + let frozen = nft.is_frozen(); + let metadata_url = nft.get_token_uri(); + let old_owner_address = old_owner.map(|x| x.to_string()); + + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + let mut client = postgres::Client::connect(&conn_string, NoTls).unwrap(); + + // Check current owner in the database for the NFT + let rows = client + .query( + QUERY_OWNER_FROM_NFTS, + &[&collection_address, &(nft_id as i64)], + ) + .unwrap(); + + let db_owner: Option = rows.get(0).map(|row| row.get(0)); + + // Handle ownership change logic for top_owners table + if let Some(db_owner_str) = db_owner { + if old_owner_address.is_none() { + // This means it's a mint operation but the NFT already exists in the table. + // Do nothing as we shouldn't increment in this scenario. + } else if old_owner_address.as_ref() != Some(&new_owner_str) { + // Transfer occurred + + // Decrement count for the database owner (which would be the old owner in a transfer scenario) + let _ = client.execute( + DECREMENT_COUNT_FOR_OLD_OWNER, + &[&db_owner_str, &collection_address], + ); + + // Increment count for new owner + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + } else if old_owner_address.is_none() { + // Mint operation, and NFT doesn't exist in the database. Increment for the new owner. + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + + // Update NFT information after handling top_owners logic + let _ = client.execute( + INSERT_OR_UPDATE_NFT, + &[ + &collection_address, + &(nft_id as i64), + &metadata_url, + &new_owner_str, + &frozen, + ], + ); + } + }) +} diff --git a/module-system/module-implementations/sov-nft-module/src/sql.rs b/module-system/module-implementations/sov-nft-module/src/sql.rs new file mode 100644 index 000000000..6b521a877 --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/sql.rs @@ -0,0 +1,30 @@ +pub const INSERT_OR_UPDATE_COLLECTION: &str = "INSERT INTO collections (\ + collection_address, collection_name, creator_address,\ + frozen, metadata_url, supply)\ + VALUES ($1, $2, $3, $4, $5, $6)\ + ON CONFLICT (collection_address)\ + DO UPDATE SET collection_name = EXCLUDED.collection_name,\ + creator_address = EXCLUDED.creator_address,\ + frozen = EXCLUDED.frozen,\ + metadata_url = EXCLUDED.metadata_url,\ + supply = EXCLUDED.supply"; + +pub const QUERY_OWNER_FROM_NFTS: &str = + "SELECT owner FROM nfts WHERE collection_address = $1 AND nft_id = $2"; + +pub const DECREMENT_COUNT_FOR_OLD_OWNER: &str = "UPDATE top_owners SET count = count - 1 \ + WHERE owner = $1 AND collection_address = $2 AND count > 0"; + +pub const INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER: &str = + "INSERT INTO top_owners (owner, collection_address, count) VALUES ($1, $2, 1) \ + ON CONFLICT (owner, collection_address) \ + DO UPDATE SET count = top_owners.count + 1"; + +pub const INSERT_OR_UPDATE_NFT: &str = "INSERT INTO nfts (\ + collection_address, nft_id, metadata_url,\ + owner, frozen)\ + VALUES ($1, $2, $3, $4, $5)\ + ON CONFLICT (collection_address, nft_id)\ + DO UPDATE SET metadata_url = EXCLUDED.metadata_url,\ + owner = EXCLUDED.owner,\ + frozen = EXCLUDED.frozen"; diff --git a/module-system/sov-modules-api/src/transaction.rs b/module-system/sov-modules-api/src/transaction.rs index 1810fbd86..bad6da527 100644 --- a/module-system/sov-modules-api/src/transaction.rs +++ b/module-system/sov-modules-api/src/transaction.rs @@ -6,7 +6,9 @@ use crate::PrivateKey; use crate::{Context, Signature}; /// A Transaction object that is compatible with the module-system/sov-default-stf. -#[derive(Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize)] +#[derive( + Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize, serde::Serialize, +)] pub struct Transaction { signature: C::Signature, pub_key: C::PublicKey, diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 40a942af5..c5bd5d1ca 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -18,6 +18,7 @@ mod manifest; mod module_call_json_schema; mod module_info; mod new_types; +mod offchain; #[cfg(feature = "native")] mod rpc; @@ -29,10 +30,11 @@ use dispatch::genesis::GenesisMacro; use dispatch::message_codec::MessageCodec; use module_call_json_schema::derive_module_call_json_schema; use new_types::address_type_helper; +use offchain::offchain_generator; use proc_macro::TokenStream; #[cfg(feature = "native")] use rpc::ExposeRpcMacro; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, ItemFn}; #[proc_macro_derive(ModuleInfo, attributes(state, module, address, gas))] pub fn module_info(input: TokenStream) -> TokenStream { @@ -263,3 +265,38 @@ pub fn address_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); handle_macro_error(address_type_helper(input)) } + +/// The offchain macro is used to annotate functions that should only be executed by the rollup +/// when the "offchain" feature flag is passed. The macro produces one of two functions depending on +/// the presence flag. +/// "offchain" feature enabled: function is present as defined +/// "offchain" feature absent: function body is replaced with an empty definition +/// +/// The idea here is that offchain computation is optionally enabled for a full node and is not +/// part of chain state and does not impact consensus, prover or anything else. +/// +/// ## Example +/// ``` +/// use sov_modules_macros::offchain; +/// #[offchain] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// ``` +/// +/// This is exactly equivalent to hand-writing +///``` +/// #[cfg(feature = "offchain")] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// +/// #[cfg(not(feature = "offchain"))] +/// fn redis_insert(count: u64){ +/// } +///``` +#[proc_macro_attribute] +pub fn offchain(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + handle_macro_error(offchain_generator(input)) +} diff --git a/module-system/sov-modules-macros/src/offchain.rs b/module-system/sov-modules-macros/src/offchain.rs new file mode 100644 index 000000000..274021ad9 --- /dev/null +++ b/module-system/sov-modules-macros/src/offchain.rs @@ -0,0 +1,31 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::ItemFn; + +pub fn offchain_generator(function: ItemFn) -> Result { + let visibility = &function.vis; + let name = &function.sig.ident; + let inputs = &function.sig.inputs; + let output = &function.sig.output; + let block = &function.block; + let generics = &function.sig.generics; + let where_clause = &function.sig.generics.where_clause; + let asyncness = &function.sig.asyncness; + + let output = quote! { + // The "real" function + #[cfg(feature = "offchain")] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + #block + } + + // The no-op function + #[cfg(not(feature = "offchain"))] + #[allow(unused_variables)] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + // Do nothing. Should be optimized away + } + }; + + Ok(output.into()) +} diff --git a/packages_to_publish.yml b/packages_to_publish.yml index cf287870f..2143d509a 100644 --- a/packages_to_publish.yml +++ b/packages_to_publish.yml @@ -17,6 +17,7 @@ - sov-prover-incentives - sov-chain-state - sov-blob-storage +- sov-nft-module # Adapters - sov-risc0-adapter diff --git a/utils/nft-utils/Cargo.toml b/utils/nft-utils/Cargo.toml new file mode 100644 index 000000000..6375f1ee6 --- /dev/null +++ b/utils/nft-utils/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nft-utils" +authors = { workspace = true } +description = "Utils for NFTs" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } +readme = "README.md" +resolver = "2" +autotests = false +publish = false + +[dependencies] +borsh = { workspace = true } +tokio = { workspace = true } + +sov-nft-module = { path = "../../module-system/module-implementations/sov-nft-module", features = ["native"]} +sov-sequencer = { path = "../../full-node/sov-sequencer"} +demo-stf = {path = "../../examples/demo-stf"} +sov-rollup-interface = {path = "../../rollup-interface"} + +sov-modules-api = {path = "../../module-system/sov-modules-api", features = ["native"]} diff --git a/utils/nft-utils/src/lib.rs b/utils/nft-utils/src/lib.rs new file mode 100644 index 000000000..bb280d598 --- /dev/null +++ b/utils/nft-utils/src/lib.rs @@ -0,0 +1,201 @@ +use borsh::ser::BorshSerialize; +use demo_stf::runtime::RuntimeCall; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::transaction::Transaction; +use sov_modules_api::{Address, PrivateKey}; +use sov_nft_module::utils::get_collection_address; +use sov_nft_module::{CallMessage, CollectionAddress, UserAddress}; +use sov_rollup_interface::mocks::MockDaSpec; + +fn get_collection_metadata_url(base_url: &str, collection_address: &str) -> String { + format!("{}/collection/{}", base_url, collection_address) +} + +fn get_nft_metadata_url(base_url: &str, collection_address: &str, nft_id: u64) -> String { + format!("{}/nft/{}/{}", base_url, collection_address, nft_id) +} + +/// Convenience and readability wrapper for build_create_collection_transaction +pub fn build_create_collection_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + base_uri: &str, + collections: &[&str], +) -> Vec> { + collections + .iter() + .map(|&collection_name| { + let tx = build_create_collection_transaction( + creator_pk, + *start_nonce, + collection_name, + base_uri, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to create a new NFT collection. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to be created. +/// +/// # Returns +/// +/// Returns a signed transaction for creating a new NFT collection. +pub fn build_create_collection_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + base_uri: &str, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + + let collection_uri = get_collection_metadata_url(base_uri, &collection_address.to_string()); + let create_collection_message = RuntimeCall::::nft( + CallMessage::::CreateCollection { + name: collection_name.to_string(), + collection_uri, + }, + ); + Transaction::::new_signed_tx( + signer, + create_collection_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_mint_nft_transaction +pub fn build_mint_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + collection: &str, + start_nft_id: &mut u64, + num: usize, + base_uri: &str, + owner_pk: &DefaultPrivateKey, +) -> Vec> { + (0..num) + .map(|_| { + let tx = build_mint_nft_transaction( + creator_pk, + *start_nonce, + collection, + *start_nft_id, + base_uri, + &owner_pk.default_address(), + ); + *start_nft_id += 1; + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to mint a new NFT. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the new NFT. +/// * `owner`: The address of the user to whom the NFT will be minted. +/// +/// # Returns +/// +/// Returns a signed transaction for minting a new NFT to a specified user. +pub fn build_mint_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + token_id: u64, + base_uri: &str, + owner: &Address, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + let token_uri = get_nft_metadata_url(base_uri, &collection_address.to_string(), token_id); + let mint_nft_message = + RuntimeCall::::nft(CallMessage::::MintNft { + collection_name: collection_name.to_string(), + token_uri, + token_id, + owner: UserAddress::new(owner), + frozen: false, + }); + Transaction::::new_signed_tx( + signer, + mint_nft_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_transfer_nft_transaction +pub fn build_transfer_transactions( + signer: &DefaultPrivateKey, + start_nonce: &mut u64, + collection_address: &CollectionAddress, + nft_ids: Vec, +) -> Vec> { + nft_ids + .into_iter() + .map(|nft_id| { + let new_owner = DefaultPrivateKey::generate().default_address(); + let tx = build_transfer_nft_transaction( + signer, + *start_nonce, + collection_address, + nft_id, + &new_owner, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to transfer an NFT to another user. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_address`: The address of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the NFT being transferred. +/// * `to`: The address of the user to whom the NFT will be transferred. +/// +/// # Returns +/// +/// Returns a signed transaction for transferring an NFT to a specified user. +pub fn build_transfer_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_address: &CollectionAddress, + token_id: u64, + to: &Address, +) -> Transaction { + let transfer_message = RuntimeCall::::nft(CallMessage::< + DefaultContext, + >::TransferNft { + collection_address: collection_address.clone(), + token_id, + to: UserAddress::new(to), + }); + Transaction::::new_signed_tx( + signer, + transfer_message.try_to_vec().unwrap(), + nonce, + ) +} diff --git a/utils/nft-utils/src/main.rs b/utils/nft-utils/src/main.rs new file mode 100644 index 000000000..15d01ae71 --- /dev/null +++ b/utils/nft-utils/src/main.rs @@ -0,0 +1,105 @@ +use std::thread; +use std::time::Duration; + +use nft_utils::{ + build_create_collection_transactions, build_mint_transactions, build_transfer_transactions, +}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_nft_module::utils::get_collection_address; +use sov_sequencer::utils::SimpleClient; + +const COLLECTION_1: &str = "Sovereign Squirrel Syndicate"; +const COLLECTION_2: &str = "Celestial Dolphins"; +const COLLECTION_3: &str = "Risky Rhinos"; + +const DUMMY_URL: &str = "http://foobar.storage"; + +const PK1: [u8; 32] = [ + 199, 23, 116, 41, 227, 173, 69, 178, 7, 24, 164, 151, 88, 149, 52, 187, 102, 167, 163, 248, 38, + 86, 207, 66, 87, 81, 56, 66, 211, 150, 208, 155, +]; +const PK2: [u8; 32] = [ + 92, 136, 187, 3, 235, 27, 9, 215, 232, 93, 24, 78, 85, 255, 234, 60, 152, 21, 139, 246, 151, + 129, 152, 227, 231, 204, 38, 84, 159, 129, 71, 143, +]; +const PK3: [u8; 32] = [ + 233, 139, 68, 72, 169, 252, 229, 117, 72, 144, 47, 191, 13, 42, 32, 107, 190, 52, 102, 210, + 161, 208, 245, 116, 93, 84, 37, 87, 171, 44, 30, 239, +]; + +#[tokio::main] +async fn main() { + let creator_pk = DefaultPrivateKey::try_from(&PK1[..]).unwrap(); + let owner_1_pk = DefaultPrivateKey::try_from(&PK2[..]).unwrap(); + let owner_2_pk = DefaultPrivateKey::try_from(&PK3[..]).unwrap(); + + let client = SimpleClient::new("localhost", 12345).await.unwrap(); + + let mut nonce = 0; + let collections = [COLLECTION_1, COLLECTION_2, COLLECTION_3]; + let transactions = + build_create_collection_transactions(&creator_pk, &mut nonce, DUMMY_URL, &collections); + client.send_transactions(transactions, None).await.unwrap(); + + // sleep is necessary because of how the sequencer currently works + // without the sleep, there is a concurrency issue and some transactions would be ignored + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(1000)); + + let mut nft_id = 1; + let mut transactions = build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 15, + DUMMY_URL, + &owner_1_pk, + ); + + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 5, + DUMMY_URL, + &owner_2_pk, + )); + let mut nft_id = 1; + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_2, + &mut nft_id, + 20, + DUMMY_URL, + &owner_1_pk, + )); + + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(3000)); + + let collection_1_address = get_collection_address::( + COLLECTION_1, + creator_pk.default_address().as_ref(), + ); + + let mut owner_1_nonce = 0; + let nft_ids_to_transfer: Vec = (1..=6).collect(); + transactions = build_transfer_transactions( + &owner_1_pk, + &mut owner_1_nonce, + &collection_1_address, + nft_ids_to_transfer, + ); + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); +}