diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f7d495..36429ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,9 +67,9 @@ jobs: with: submodules: recursive - name: Build - run: cargo build --release + run: cargo build --release --features short-block-time - name: Run tests - run: cargo test --release + run: cargo test --release --features short-block-time - name: Prepare functional tests uses: actions/setup-python@v2 with: @@ -79,4 +79,10 @@ jobs: - name: Install scalecodec run: python -m pip install 'scalecodec == 0.11.18' - name: Run functional tests - run: python test/functional/test_runner.py + run: python test/functional/test_runner.py --jobs 1 + - name: Save test artifacts on failure + uses: actions/upload-artifact@v2 + if: failure() + with: + name: functional-tests-logs + path: /tmp/mintlayer* diff --git a/Cargo.lock b/Cargo.lock index e84a3ad..f73b185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3402,7 +3402,7 @@ dependencies = [ "frame-benchmarking-cli", "jsonrpc-core", "log", - "node-template-runtime", + "mintlayer-runtime", "pallet-contracts", "pallet-contracts-rpc", "pallet-transaction-payment-rpc", @@ -3446,6 +3446,52 @@ dependencies = [ "ureq", ] +[[package]] +name = "mintlayer-runtime" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-election-provider-support", + "frame-executive", + "frame-support", + "frame-system", + "frame-system-benchmarking", + "frame-system-rpc-runtime-api", + "hex-literal 0.3.3", + "log", + "pallet-aura", + "pallet-authorship", + "pallet-balances", + "pallet-contracts", + "pallet-contracts-primitives", + "pallet-contracts-rpc-runtime-api", + "pallet-grandpa", + "pallet-pp", + "pallet-randomness-collective-flip", + "pallet-session", + "pallet-staking", + "pallet-sudo", + "pallet-timestamp", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc-runtime-api", + "pallet-utxo", + "pallet-utxo-rpc-runtime-api", + "parity-scale-codec", + "sp-api", + "sp-block-builder", + "sp-consensus-aura", + "sp-core", + "sp-inherents", + "sp-offchain", + "sp-runtime", + "sp-session", + "sp-staking", + "sp-std", + "sp-transaction-pool", + "sp-version", + "substrate-wasm-builder", +] + [[package]] name = "mio" version = "0.6.23" @@ -3659,53 +3705,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "node-template-runtime" -version = "3.0.0" -dependencies = [ - "frame-benchmarking", - "frame-election-provider-support", - "frame-executive", - "frame-support", - "frame-system", - "frame-system-benchmarking", - "frame-system-rpc-runtime-api", - "hex-literal 0.3.3", - "log", - "pallet-aura", - "pallet-authorship", - "pallet-balances", - "pallet-contracts", - "pallet-contracts-primitives", - "pallet-contracts-rpc-runtime-api", - "pallet-grandpa", - "pallet-pp", - "pallet-randomness-collective-flip", - "pallet-session", - "pallet-staking", - "pallet-sudo", - "pallet-template", - "pallet-timestamp", - "pallet-transaction-payment", - "pallet-transaction-payment-rpc-runtime-api", - "pallet-utxo", - "pallet-utxo-rpc-runtime-api", - "parity-scale-codec", - "sp-api", - "sp-block-builder", - "sp-consensus-aura", - "sp-core", - "sp-inherents", - "sp-offchain", - "sp-runtime", - "sp-session", - "sp-staking", - "sp-std", - "sp-transaction-pool", - "sp-version", - "substrate-wasm-builder", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -4097,20 +4096,6 @@ dependencies = [ "sp-std", ] -[[package]] -name = "pallet-template" -version = "3.0.0" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "parity-scale-codec", - "serde", - "sp-core", - "sp-io", - "sp-runtime", -] - [[package]] name = "pallet-timestamp" version = "4.0.0-dev" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9d24952 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM debian:bullseye + +ENV REQUIRED_PACKAGES git clang curl libssl-dev llvm libudev-dev + +RUN apt-get update \ + && apt-get install -y $REQUIRED_PACKAGES \ + && curl https://sh.rustup.rs -sSf | sh -s -- -y \ + && $HOME/.cargo/bin/rustup default stable \ + && $HOME/.cargo/bin/rustup update \ + && $HOME/.cargo/bin/rustup update nightly \ + && $HOME/.cargo/bin/rustup target add wasm32-unknown-unknown --toolchain nightly + +RUN apt-get update \ + && apt-get install -y clang-9 \ + && git clone https://github.com/mintlayer/core \ + && cd core \ + && $HOME/.cargo/bin/cargo build --release + +WORKDIR /core + +EXPOSE 30333 + +CMD RUST_LOG=info target/release/mintlayer-core --base-path /tmp/ml-core --chain=assets/Testnet1Spec.json diff --git a/README.md b/README.md deleted file mode 100644 index 4cfeb56..0000000 --- a/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# Mintlayer core - -https://www.mintlayer.org/ - -For a more technical introduction to Mintlayer visit [our docs](https://docs.mintlayer.org/). - -A draft of the consensus paper can be found [here](https://www.mintlayer.org/docs/DSA-consensus-paper-draft.pdf). - -## Security issues -If you find an issue related to the security of Mintlayer then please contact us at security@mintlayer.org so we can address the issue. Mintlayer has a [bug bounty program](https://www.mintlayer.org/bug-bounties) so if your security issue is valid you are elligble for a reward paid in MLT. Do not disclose the security issue publicly until the core Mintlayer team has agreed the issue can be disclosed. See [SECURITY.md](https://github.com/mintlayer/core/blob/master/SECURITY.md) for more info. - -## Bugs -Non-security related bugs should be opened as [issues](https://github.com/mintlayer/core/issues/new) in the core Mintlayer repo. Give as much detail as possible. If you want to fix a bug then see our guidelines for [contributing](https://github.com/mintlayer/core/blob/master/CONTRIBUTING.md). - -## How to build and run Mintlayer - -### Rust Setup - -First, complete the [basic Rust setup instructions](https://github.com/mintlayer/core/blob/master/doc/rust-setup.md). - -### Run - -Use Rust's native `cargo` command to build and launch the template node: - -```sh -cargo run --release -- --dev --tmp -``` - -### Build - -The `cargo run` command will perform an initial build. Use the following command to build the node -without launching it: - -```sh -cargo build --release -``` -or - -`cargo build` to build a debug version - -to purge the local chain run `./target/release/mintlayer-core purge-chain --dev` - -### Docs - -Once the project has been built, the following command can be used to explore all parameters and -subcommands: - -```sh -./target/release/mintlayer-core -h -``` - -You can also find docs in the docs directory and within the directory for a specific pallet or lib. - - -### Single-Node Development Chain - -This command will start the single-node development chain with persistent state: - -```bash -./target/release/mintlayer-core --dev -``` - -Purge the development chain's state: - -```bash -./target/release/mintlayer-core purge-chain --dev -``` - -Start the development chain with detailed logging: - -```bash -RUST_LOG=debug RUST_BACKTRACE=1 ./target/release/mintlayer-core -lruntime=debug --dev -``` - -### Connect with Polkadot-JS Apps Front-end - -Once the node template is running locally, you can connect it with **Polkadot-JS Apps** front-end -to interact with your chain. [Click here](https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944) connecting the Apps to your local node template. - -### Connect with Mintlayer's UI -TODO - -### Multi-Node Local Testnet - -If you want to see the multi-node consensus algorithm in action, refer to -[our Start a Private Network tutorial](https://substrate.dev/docs/en/tutorials/start-a-private-network/). - -## Project Structure - -### Node - -- Networking: Mintlayer uses [libp2p](https://libp2p.io/) as its native networking stack for all inter-node communication. -- Bootnodes: Mintlayer has [bootnodes](https://github.com/mintlayer/core/blob/master/assets/bootnodes.json) that a new node will attempt to boot to unless a specific node is specified by the user -- Consensus: Mintlayer uses [AURA](https://docs.rs/sc-consensus-aura/0.9.0/sc_consensus_aura/) as its base consensus algorithm for the time being. There will be an update to introduce [DSA](https://www.mintlayer.org/docs/DSA-consensus-paper-draft.pdf) in the future but DSA is still in development. -- Finality: Since we are using AURA for our consensus we currently rely on [GRANDPA](https://docs.rs/sc-finality-grandpa/0.9.0/sc_finality_grandpa/) for finality. -- Chain Spec: You can find our chain specification in [chain_spec.rs](https://github.com/mintlayer/core/blob/master/node/src/chain_spec.rs). It defines the basics of the chain such as the genesis block and the bootnodes. -- Services: [service.rs](https://github.com/mintlayer/core/blob/master/node/src/service.rs) defines the node implementation itself. It is here you'll find the consensus setup. - - -### Runtime - -For more information on what a [runtime](https://substrate.dev/docs/en/knowledgebase/getting-started/glossary#runtime) is follow the link. -Code in the runtime must be written in `no_std` Rust since it compiles to Wasm. - -- lib.rs: The main file in Mintlayer's runtime. Here you'll find the Mintlayer specific code for block production such as the block production period. -- staking.rs: Here you'll find Mintlayer's staking implementation. - - -### Pallets - -Mintlayer relies on a host of Substrate pallets and a few Mintlayer specific pallets. - -- pp: The implementation of programmable pools on Mintlayer. Essentially Wasm smart contracts -- utxo: Mintlayer's UTXO system - -### Libs - -Libs is home to code that is code that Mintlayer relies on but isn't technically a pallet. - -- chainscript: Mintlayer's bitcoin script implementation. -- bech32: code for handling transactions with destinations encoded using bech32 - -### Testing - -You'll find unit tests littered throughout the codebase but the test directory is home to the functional test framework which is heavily based on Bitcoin's functional test framework. - -### Crypto -As it stands Mintlayer uses Schnorr for all crypto-related things. There is a plan to move to our BLS implementation in the near future but this, as it stands, is a work in progress. - -### Contributing -[See this guide](https://github.com/mintlayer/core/blob/master/CONTRIBUTING.md) - -### Branches -The key branches are master and staging. Master is used for fully tested code, staging is used as the development branch. Fixes or features should be created on new branches branched from staging. A PR is then created to merge the branch into staging where it will require a review from a member of the Mintlayer team. To merge into master create a PR to merge staging to master, a review is required and CI will run. Only select people have push access to master. - -### Firewall rules - -The node uses TCP port 30333 for communications, this needs to be opened if you want to allow -inbound connections. - -Using UFW: -`sudo ufw allow 30333/tcp` - -Using iptables: -`sudo iptables -A INPUT -p tcp --dport 30333 -j ACCEPT` diff --git a/README.md b/README.md new file mode 120000 index 0000000..0e01b43 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +docs/README.md \ No newline at end of file diff --git a/doc/rust-setup.md b/doc/rust-setup.md index 34f6e43..3a61968 100644 --- a/doc/rust-setup.md +++ b/doc/rust-setup.md @@ -3,7 +3,7 @@ title: Installation --- This page will guide you through the steps needed to prepare a computer for development with the -Substrate Node Template. Since Substrate is built with +Substrate Node. Since Substrate is built with [the Rust programming language](https://www.rust-lang.org/), the first thing you will need to do is prepare the computer for Rust development - these steps will vary based on the computer's operating system. Once Rust is configured, you will use its toolchains to interact with Rust projects; the diff --git a/docker-compose.yml b/docker-compose.yml index cfc4437..36bcddb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,16 +2,16 @@ version: "3.2" services: dev: - container_name: node-template + container_name: mintlayer-core image: paritytech/ci-linux:974ba3ac-20201006 - working_dir: /var/www/node-template + working_dir: /var/www/mintlayer-core ports: - "9944:9944" environment: - - CARGO_HOME=/var/www/node-template/.cargo + - CARGO_HOME=/var/www/mintlayer-core/.cargo volumes: - - .:/var/www/node-template + - .:/var/www/mintlayer-core - type: bind source: ./.local target: /root/.local - command: bash -c "cargo build --release && ./target/release/node-template --dev --ws-external" + command: bash -c "cargo build --release && ./target/release/mintlayer-core --dev --ws-external" diff --git a/docs/README.md b/docs/README.md index 20ee934..55b5515 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,22 @@ - -# RUNNING +# Running ## Quick start Binaries can be found [here](https://github.com/mintlayer/core/releases). + +Running a node also requires as input a chain specification. +Currently a single chain specification for the testnet is provided, and can be downloaded using curl: + +``` +curl --proto '=https' -sSf \ + https://raw.githubusercontent.com/mintlayer/core/master/assets/Testnet1Spec.json \ + --output Testnet1Spec.json +``` Download and run: ``` -mintlayer-core --base-path data/my_first_ml_node --validator --rpc-external --rpc-methods Unsafe --chain=Testnet1Spec.json +mintlayer-core \ + --base-path data/my_first_ml_node \ + --validator \ + --chain=Testnet1Spec.json ``` to start a node. It will automatically connect to the Mintlayer bootnodes. @@ -80,7 +91,7 @@ See [Mintlayer installation on Windows](windows_installation.md) ## Running a node Clone the repository: -```bash +``` git clone https://github.com/mintlayer/core.git ``` @@ -94,29 +105,66 @@ to build the project. Finally, to run a node: ``` -RUST_LOG=info ./target/release/mintlayer-core --base-path [PATH_TO_DB] --name [NODE_NAME] --port [P2P_PORT] --ws-port [WEB_SOCKET_PORT] --rpc-port [RPC_PORT] --validator --rpc-methods Unsafe --chain=[CHAIN_SPEC] +RUST_LOG=info ./target/release/mintlayer-core \ + --base-path [PATH_TO_DB] \ + --name [NODE_NAME] \ + --port [P2P_PORT] \ + --ws-port [WEB_SOCKET_PORT] \ + --rpc-port [RPC_PORT] \ + --validator \ + --chain=[CHAIN_SPEC] ``` + For example, ``` -RUST_LOG=info ./target/release/mintlayer-core --base-path data/node1 --name brian --port 30333 --ws-port 9945 --rpc-port 9933 --validator --rpc-methods Unsafe --chain=Testnet1Spec.json +RUST_LOG=info ./target/release/mintlayer-core \ + --base-path data/node1 \ + --name brian \ + --port 30333 \ + --ws-port 9945 \ + --rpc-port 9933 \ + --validator \ + --chain=Testnet1Spec.json ``` + Let's look at these flags in detail: -|
Flags
| Descriptions | -| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--base-path` | Specifies a directory where Mintlayer should store all the data related to this chain. If the directory does not exist, it will be created for you. If other blockchain data already exists there you will get an error. Either clear the directory or choose a different one. | -| `--chain local` | Specifies which chain specification to use. There are a few prepackaged options including `local`, `development`, and `staging` but generally one specifies their own chain spec file. We'll specify our own file in a later step. | -| `--alice` | Puts the predefined Alice keys (both for block production and finalization) in the node's keystore. Generally one should generate their own keys and insert them with an RPC call. We'll generate our own keys in a later step. This flag also makes Alice a validator. | -| `--port 30333` | Specifies the port that your node will listen for p2p traffic on. `30333` is the default and this flag can be omitted if you're happy with the default. If Bob's node will run on the same physical system, you will need to explicitly specify a different port for it. | -| `--ws-port 9945` | Specifies the port that your node will listen for incoming WebSocket traffic on. The default value is `9944`. This example uses a custom web socket port number (`9945`). | -| `--rpc-port 9933` | Specifies the port that your node will listen for incoming RPC traffic on. `9933` is the default, so this parameter may be omitted. | -| `--node-key ` | The Ed25519 secret key to use for `libp2p` networking. The value is parsed as a hex-encoded Ed25519 32 byte secret key, i.e. 64 hex characters. WARNING: Secrets provided as command-line arguments are easily exposed. Use of this option should be limited to development and testing. | -| `--telemetry-url` | Tells the node to send telemetry data to a particular server. The one we've chosen here is hosted by Parity and is available for anyone to use. You may also host your own (beyond the scope of this article) or omit this flag entirely. | -| `--validator` | Means that we want to participate in block production and finalization rather than just sync the network. | +|
Flags
| Descriptions | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `--base-path` | Specifies a directory where Mintlayer should store all the data related to this chain. If the directory does not exist, it will be created for you. If other blockchain data already exists there you will get an error. Either clear the directory or choose a different one. | +| `--chain=[CHAIN_SPEC_FILE]` | Specifies which chain specification to use. A chain specification, or "chain spec", is a collection of configuration information that dictates which network a blockchain node will connect to, which entities it will initially communicate with, and what consensus-critical state it must have at genesis. | +| `--alice` | Puts the predefined Alice keys (both for block production and finalization) in the node's keystore. Generally one should generate their own keys and insert them with an RPC call. We'll generate our own keys in a later step. This flag also makes Alice a validator. | +| `--port 30333` | Specifies the port that your node will listen for p2p traffic on. `30333` is the default and this flag can be omitted if you're happy with the default. If Bob's node will run on the same physical system, you will need to explicitly specify a different port for it. | +| `--ws-port 9945` | Specifies the port that your node will listen for incoming WebSocket traffic on. The default value is `9944`. This example uses a custom web socket port number (`9945`). | +| `--rpc-port 9933` | Specifies the port that your node will listen for incoming RPC traffic on. `9933` is the default, so this parameter may be omitted. | +| `--telemetry-url` | Tells the node to send telemetry data to a particular server. The one we've chosen here is hosted by Parity and is available for anyone to use. You may also host your own (beyond the scope of this article) or omit this flag entirely. | +| `--validator` | Means that we want to participate in block production and finalization rather than just sync the network. | + +*Note*: As a safety precaution, the node will listen to RPC interfaces on localhost only. +It is possible expose the node's RPC port publicly `--rpc-external`, but only do this if you understand the risks involved. As a safer alternative to enable RPC calls from outside the node, consider using tunnels. + +**TODO** Provide an explanation/examples of methods that are unsafe to call. Or link to a list, together with an explanation of why each method is unsafe. + +*Note*: Some RPC calls can be used to control the node's behavior and should never (or rarely) be exposed. We call such methods _Unsafe_, and they are disabled by default. It is possible to enable them using `--rpc-methods Unsafe`. + +## Docker setup + +Alternatively, Docker can be used to launch a Mintlayer node. In the root directory, run: + +``` +docker build -t mintlayer-core . +docker run -t mintlayer-core +``` + +If you want to save the blockchain to host, run: +``` +docker run -v ~/ml-blockchain:/tmp/ml-core -t mintlayer-core +``` + ## Create a chain specification -In the preceding example, we used `--chain local` which is a predefined "chain spec" that has Alice and Bob specified as validators along with many other useful defaults. -In this example we will create a two-node network using our own custom chain specification. The process generalizes to more nodes in a straightforward manner. +In the preceding example, we used `--chain=Testnet1Spec.json` which is a predefined chain spec provided by Mintlayer. +In this example we will create a network using our own custom chain specification. Rather than writing our chain spec completely from scratch, we'll just make a few modifications to the one we used before. To start, we need to export the chain spec to a file named @@ -192,7 +240,71 @@ the proper storage keys. Finally share the `customSpecRaw.json` with your all the other validators in the network. -**Note**: Because Rust -> Wasm optimized builds aren't reproducible, each person will get a slightly different Wasm blob which will break consensus if each participant generates the file themselves.For the curious, learn more about this issue in [this blog post](https://dev.to/gnunicorn/hunting-down-a-non-determinism-bug-in-our-rust-wasm-build-4fk1). +**Note**: Because Rust -> Wasm optimized builds aren't reproducible, each person will get a slightly different Wasm blob which will break consensus if each participant generates the file themselves. For the curious, learn more about this issue in [this blog post](https://dev.to/gnunicorn/hunting-down-a-non-determinism-bug-in-our-rust-wasm-build-4fk1). + +### Connect with Polkadot-JS Apps Front-end + +Once the node is running locally, you can connect it to the **Polkadot-JS Apps** front-end +to interact with your chain. [Click here](https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944) to connect your node. + +## Project Structure + +### Node + +- Networking: Mintlayer uses [libp2p](https://libp2p.io/) as its native networking stack for all inter-node communication. +- Bootnodes: Mintlayer has [bootnodes](https://github.com/mintlayer/core/blob/master/assets/bootnodes.json) that a new node will attempt to connect to, unless a specific node is specified by the user. +- Consensus: Mintlayer uses [AURA](https://docs.rs/sc-consensus-aura/0.9.0/sc_consensus_aura/) as its base consensus algorithm for the time being. There will be an update to introduce [DSA](https://www.mintlayer.org/docs/DSA-consensus-paper-draft.pdf) in the future. +- Finality: Since we are using AURA for consensus, we currently rely on [GRANDPA](https://docs.rs/sc-finality-grandpa/0.9.0/sc_finality_grandpa/) for finality. +- Chain Spec: You can find our chain specification in [chain_spec.rs](https://github.com/mintlayer/core/blob/master/node/src/chain_spec.rs). It defines the basics of the chain such as the genesis block and the bootnodes. +- Services: [service.rs](https://github.com/mintlayer/core/blob/master/node/src/service.rs) defines the node implementation itself. It is here you'll find the consensus setup. + +### Runtime + +For more information on what a [runtime](https://substrate.dev/docs/en/knowledgebase/getting-started/glossary#runtime) is follow the link. +Code in the runtime must be written in `no_std` Rust since it compiles to Wasm. + +- lib.rs: The main file in Mintlayer's runtime. Here you'll find the Mintlayer-specific code for block production such as the block production period. +- staking.rs: Mintlayer's staking implementation. + + +### Pallets + +Mintlayer relies on a host of Substrate pallets as well as a few Mintlayer-specific pallets: +- pp: The implementation of programmable pools on Mintlayer. These are essentially Wasm smart contracts. +- utxo: Mintlayer's UTXO system. + +### Libs + +The `libs` is home to code that Mintlayer relies on but isn't technically a pallet: +- chainscript: Mintlayer's Bitcoin script implementation. +- bech32: code for handling transactions with destinations encoded using bech32 + +### Testing + +Unit tests for particular modules are scattered throughout the codebase. +The `test` directory is home to Mintlayer's functional test framework, which includes system-level tests. Heavily based on Bitcoin's functional test framework. + +### Crypto +Currently, Mintlayer uses Schnorr signatures for crypto-related needs. We plan to move to our BLS implementation in the near future. + +### Contributing +[See this guide](https://github.com/mintlayer/core/blob/master/CONTRIBUTING.md) + +### Branches +The key branches of this repository are `master` and `staging`. + +`staging` is used as the development branch, while `master` is used for fully tested. + +To submit a fix or a feature, code your changes on a new branch based on `staging` and create a pull request. This will trigger CI checks and notify the Minlayer team that a new pull request is awaiting review. Once all CI checks have passed and the pull request has been reviewed and approved by a member of the Mintlayer team, the pull request can be merged into `staging`. + +Periodically, the Mintlayer team will merge `staging` into `master`. +Only a select few members of the Mintlayer team have push access to `master` + +## Security issues +If you find an issue related to the security of Mintlayer then please contact us at security@mintlayer.org so we can address the issue. Mintlayer has a [bug bounty program](https://www.mintlayer.org/bug-bounties) so if your security issue is valid you are elligble for a reward paid in MLT. Do not disclose the security issue publicly until the core Mintlayer team has agreed the issue can be disclosed. See [SECURITY.md](https://github.com/mintlayer/core/blob/master/SECURITY.md) for more info. + +## Bugs +Non-security related bugs should be opened as [issues](https://github.com/mintlayer/core/issues/new) in the core Mintlayer repo. Give as much detail as possible. If you want to fix a bug then see our guidelines for [contributing](https://github.com/mintlayer/core/blob/master/CONTRIBUTING.md). ## Firewall rules diff --git a/docs/tokens.md b/docs/tokens.md index a0d349b..b66a396 100644 --- a/docs/tokens.md +++ b/docs/tokens.md @@ -1,25 +1,23 @@ # Mintlayer Tokens -**TODO Do we want Rust code in this doc?** -Each transaction output must carry a data field. This field describes the purpose of the transaction. +This document describes the structure of transactions involving tokens on Mintlayer. Currently, two types of tokens are supported: -We must highlight the following at the moment: +1. *MLS-01 tokens*: MLS-01 is the "basic" Mintlayer token standard, analogous to, say, ERC-20 tokens on Ethereum. -- Transfer Tokens or NFT -- Token Issuance -- Burning tokens -- NFT creation +2. NFTs + +A transaction involving transaction output carries a (possibly empty) `data` field specifying the purpose of the transaction. -All transactions must be signed on the client side. This means we cannot sign transactions on the node side. -**TODO what is the connection between these two sentences?** -In the future, there will be changes to the structures of transactions and we will be required to track them. -**TODO by structures above to we mean rust structs? Why is this interesting to the user?** +The data field can be any of the following: + +- Transfer of MLS-01 tokens or NFTs +- Token issuance +- Token burning +- NFT creation -## Transfer Tokens +## Transferring Tokens -**TODO sentence fragment, I don't understand...** -**TODO When do we NOT use the TxData field?** -For transfering funds to another person in a given UTXO. To send MLT we will use the MLT token ID, which is equal to 0. If the token ID is equal to the ID of the MLS-01 (**TODO what is MLS-01**) token, then the amount of token is transferred to the recipient. The commission is taken only in MLT (**TODO what is this commission**. If the token ID is equal to the ID of any NFT, then the data of this NFT is transferred to the recipient without changing the creator field. The UTXO model itself allows to determine the owner of the NFT. +To send MLT we use the MLT token ID, which is equal to 0. If the token ID is equal to the ID of an MLS-01 token, then the amount of the token is transferred to the recipient. The transaction fee is taken only in MLT. If the token ID is equal to the ID of any NFT, then the data of this NFT is transferred to the recipient without changing the creator field. The UTXO model itself allows to determine the owner of the NFT. ```rust TxData { @@ -30,27 +28,32 @@ TxData { } ``` -## Issue Tokens -When issuing a new token, we specify the data for creating a new token in the transaction input, where the `token_id` is a hash of the inputs. **TODO which inputs?** -**TODO explain remaining fields** +## Issuing Tokens -**TODO understand the comment** -```rust +To issue a new token, we specify the data for creating the token in the transaction output's `data` field: + + ```rust TxData { TokenIssuanceV1 { - token_id: TokenID, - token_ticker: Vec, - amount_to_issue: Value, - // Should be not more than 18 numbers - number_of_decimals: u8, - metadata_URI: Vec, - } + token_ticker: Vec, + amount_to_issue: Value, + // Should not be more than 18 + number_of_decimals: u8, + metadata_URI: Vec, + } } -``` + ``` + +Here, `token_ticker` is a short name given to the token (up to 5 chararcters long). + +The `metatdata_URI` is a web link to a JSON file where we can store additional information about the token + +The _token ID_ is defined as a hash of the _first input_ of the issuance transaction. + + +### Burning Tokens -### Burn Tokens -**TODO verify - the input should be a utxo that contains tokens, the output should contain the TokenBurn arm** -A token burning - as an input is used by UTXO that contains tokens. As an output, the data field should contain the TokenBurn arm. If the amount in burning the output is less than in the input then there should exist at least one output for returning the funds change. In this case, you can burn any existing number of tokens. After this operation, you can use UTXO for the remaining amount of tokens. +The input for a token-burning transaction should be a UTXO containing tokens. In the output, the data field should contain the _TokenBurn_ variant. If the `amount_to_burn` in the output is less than the amount in the input, then there should exist at least one output for returning the difference. In this way, any existing number of tokens can be burned. ```rust TxData { @@ -61,18 +64,17 @@ TxData { } ``` ### NFT -TO DO +**TODO** ## Wallet +**TODO** -TO DO -## Issue and Transfer Tokens +## Issuing and Transferring Tokens -**TODO who are these examples meant for?** ```rust /* Transfer and Issuance in one Tx */ -// Alice issues 1_000_000_000 MLS-01, and send them to Karl +// Alice issues 1_000_000_000 MLS-01 tokens, and sends them to Karl let (utxo0, input0) = tx_input_gen_no_signature(); let tx = Transaction { inputs: vec![input0], @@ -96,13 +98,17 @@ let tx = Transaction { } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); -let first_issuance_token_id = TokenId::new(&tx.inputs[0]); assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + +let first_issuance_token_id = TokenId::new(&tx.inputs[0]); + +// The newly issued token is represented by the TransactionOutput at index 1 +// "Outoint" here refers to the hash of the TransactionOutput struct. let token_utxo_hash = tx.outpoint(1); let token_utxo = tx.outputs[1].clone(); -// Let's send 300_000_000 and rest back and create another token +// Let's send alice 300_000_000 and the rest back to Karl, andalso create another token, "KarlToken" let tx = Transaction { inputs: vec![TransactionInput::new_empty(token_utxo_hash)], outputs: vec![ @@ -126,7 +132,7 @@ let tx = Transaction { 0, H256::from(karl_pub_key), OutputData::TokenIssuanceV1 { - token_ticker: "Token".as_bytes().to_vec(), + token_ticker: "KarlToken".as_bytes().to_vec(), amount_to_issue: 5_000_000_000, // Should be not more than 18 numbers number_of_decimals: 12, @@ -137,10 +143,13 @@ let tx = Transaction { time_lock: Default::default(), } .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); + assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + let alice_transfer_utxo_hash = tx.outpoint(0); let karl_transfer_utxo_hash = tx.outpoint(1); let karl_issuance_utxo_hash = tx.outpoint(2); + assert!(!UtxoStore::::contains_key(H256::from( token_utxo_hash ))); @@ -148,8 +157,6 @@ assert!(UtxoStore::::contains_key(alice_transfer_utxo_hash)); assert!(UtxoStore::::contains_key(karl_transfer_utxo_hash)); assert!(UtxoStore::::contains_key(karl_issuance_utxo_hash)); - - // Let's check token transfer UtxoStore::::get(alice_transfer_utxo_hash) .unwrap() @@ -165,8 +172,6 @@ UtxoStore::::get(alice_transfer_utxo_hash) }) .unwrap(); - - UtxoStore::::get(karl_transfer_utxo_hash) .unwrap() .data @@ -181,8 +186,6 @@ UtxoStore::::get(karl_transfer_utxo_hash) }) .unwrap(); - - // Let's check token issuance UtxoStore::::get(karl_issuance_utxo_hash) .unwrap() diff --git a/docs/transaction.md b/docs/transaction.md index 164b0af..1da057d 100644 --- a/docs/transaction.md +++ b/docs/transaction.md @@ -2,17 +2,11 @@ ## UTXO overview -**TODO maybe system/model instead of structure?** -Mintlayer uses the Bitcoin UTXO structure instead of the account-based models of Ethereum, Ripple, Stellar, and others. -**TODO need explanation of this sentence, it is not clear to me** -Since each transaction's output is stored separately (even when sent to a single address), it is only possible to spend the entire transaction’s output. +Mintlayer uses the a UTXO system similar to Bitcoin's, instead of the account-based models of Ethereum, Ripple, Stellar, and others. There are three essential reasons for this: -There are three essential reasons for choosing the UTXO model: - -- It is compatible with technologies already implemented in Bitcoin, such atomic swaps and the Lightning Network. +- The utxo model is compatible with technologies already implemented in Bitcoin, such atomic swaps and the Lightning Network. -**TODO - why does this improve privacy?** -- It is more privacy-oriented: a single wallet usually utilizes multiple addresses, making it difficult and sometimes impossible to determine which addresses belong to whichs user. +- The utxo model is more privacy-oriented: a single wallet can utilize multiple addresses, making it difficult and sometimes impossible to determine which addresses belong to which user. - Payments can be batched together (aggregated) in a single transaction, saving a considerable amount of the space otherwise required for making a single transaction per payment. @@ -24,10 +18,10 @@ There are three destination types for transaction outputs : A general Mintlayer transaction looks something like this: -**TODO Not sure we want this in Rust code. Too developer specific. Not clear what H256 is, witness, lock** -**TODO possibly add a link to information about the utxo system** -**TODO if we go for the rust struct, then we need the data field. Also, what is this field?** +**TODO if we go for the rust struct, then we need the data field in output** + **TODO timelock is not a string..."** + ```rust Transaction { inputs: [ @@ -66,17 +60,23 @@ In Mintlayer, as Substrate, transanctions need to be signed before being submitt - The timelock **TODO Explain what we are showing here** + **TODO We need to document the python mintlayer crate** + **TODO what is utxos[0][0]? Utxos is a two-dimentsional array?** + **TODO I want to see the Transaction python class. What is the utxo[0][1] in the signature?** -**In the second transaction's signature, outpoints instead of outputs** + +**TODO In the second transaction's signature, outpoints instead of outputs** ### Python ```python from substrateinterface import Keypair import mintlayer.utxo as utxo -client = self.nodes[0].rpc_client +#... + +account = Account(args) alice = Keypair.create_from_uri('//Alice') bob = Keypair.create_from_uri('//Bob') @@ -85,7 +85,7 @@ bob = Keypair.create_from_uri('//Bob') utxos = list(client.utxos_for(alice)) tx1 = utxo.Transaction( - client, + account.client, inputs=[ utxo.Input(utxos[0][0]), ], diff --git a/node/Cargo.toml b/node/Cargo.toml index 0988d79..cc4e01e 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -21,7 +21,7 @@ substrate-build-script-utils = {version = '3.0.0', git = 'https://github.com/par [dependencies] jsonrpc-core = '18.0.0' structopt = '0.3.8' -node-template-runtime = {version = '3.0.0', path = '../runtime'} +mintlayer-runtime = {version = '3.0.0', path = '../runtime'} pallet-utxo-rpc = { path = "../pallets/utxo/rpc" } pallet-utxo-rpc-runtime-api = { path = "../pallets/utxo/rpc/runtime-api" } log = "0.4.8" @@ -227,4 +227,4 @@ branch = "master" [features] default = [] -runtime-benchmarks = ['node-template-runtime/runtime-benchmarks'] +runtime-benchmarks = ['mintlayer-runtime/runtime-benchmarks'] diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 1711f10..ec295a6 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,4 +1,4 @@ -use node_template_runtime::{ +use mintlayer_runtime::{ pallet_utxo, AccountId, BalancesConfig, GenesisConfig, PpConfig, SessionConfig, Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, UtxoConfig, MINIMUM_STAKE, NUM_OF_VALIDATOR_SLOTS, WASM_BINARY, @@ -222,7 +222,7 @@ fn testnet_genesis( session_keys.push(( auth_keys.stash_account_id(), auth_keys.stash_account_id(), - node_template_runtime::opaque::SessionKeys { + mintlayer_runtime::opaque::SessionKeys { aura: AuraId::from(auth_keys.sr25519_public_controller), grandpa: GrandpaId::from(auth_keys.ed25519_public), }, diff --git a/node/src/command.rs b/node/src/command.rs index 4513b27..02afffd 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -18,7 +18,7 @@ use crate::chain_spec::MltKeysInfo; use crate::cli::{Cli, Subcommand}; use crate::{chain_spec, service}; -use node_template_runtime::{pallet_utxo, Block, TEST_NET_MLT_ORIG_SUPPLY}; +use mintlayer_runtime::{pallet_utxo, Block, TEST_NET_MLT_ORIG_SUPPLY}; use sc_cli::{ChainSpec, Role, RuntimeVersion, SubstrateCli}; use sc_network::config::MultiaddrWithPeerId; use sc_service::PartialComponents; @@ -155,7 +155,7 @@ impl SubstrateCli for Cli { } fn native_runtime_version(_: &Box) -> &'static RuntimeVersion { - &node_template_runtime::VERSION + &mintlayer_runtime::VERSION } } diff --git a/node/src/main.rs b/node/src/main.rs index a4182cd..0f4fdaa 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,4 +1,4 @@ -//! Substrate Node Template CLI library. +//! Mintlayer Node CLI library. #![warn(missing_docs)] mod chain_spec; diff --git a/node/src/rpc.rs b/node/src/rpc.rs index e46071d..7a13715 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -7,7 +7,7 @@ use std::sync::Arc; -use node_template_runtime::{opaque::Block, AccountId, Balance, BlockNumber, Hash, Index}; +use mintlayer_runtime::{opaque::Block, AccountId, Balance, BlockNumber, Hash, Index}; pub use sc_rpc_api::DenyUnsafe; use sc_transaction_pool_api::TransactionPool; use sp_api::ProvideRuntimeApi; diff --git a/node/src/service.rs b/node/src/service.rs index 826dc56..fa8f0c1 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,4 +1,4 @@ -use node_template_runtime::{self, opaque::Block, RuntimeApi}; +use mintlayer_runtime::{self, opaque::Block, RuntimeApi}; use sc_client_api::{ExecutorProvider, RemoteBackend}; use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams}; pub use sc_executor::NativeElseWasmExecutor; @@ -17,11 +17,11 @@ impl sc_executor::NativeExecutionDispatch for ExecutorDispatch { type ExtendHostFunctions = frame_benchmarking::benchmarking::HostFunctions; fn dispatch(method: &str, data: &[u8]) -> Option> { - node_template_runtime::api::dispatch(method, data) + mintlayer_runtime::api::dispatch(method, data) } fn native_version() -> sc_executor::NativeVersion { - node_template_runtime::native_version() + mintlayer_runtime::native_version() } } diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index 36055b8..c92495f 100644 --- a/pallets/pp/src/lib.rs +++ b/pallets/pp/src/lib.rs @@ -27,7 +27,7 @@ mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -use codec::Encode; +use codec::{Decode, Encode}; pub use frame_support::{ construct_runtime, dispatch::Vec, @@ -144,7 +144,12 @@ fn send_p2pk_tx( ensure!(fund_info.funds >= value, "Caller doesn't have enough funds"); let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::send_conscrit_p2pk(caller, dest, value, &outpoints) + T::Utxo::submit_c2pk_tx(caller, dest, value, &outpoints).map(|_| { + >::mutate(&caller, |info| { + info.as_mut().unwrap().utxos = Vec::new(); + info.as_mut().unwrap().funds = 0; + }); + }) } /// Create Contract-to-Contract transfer that allows smart contracts to @@ -169,7 +174,12 @@ fn send_c2c_tx( ))?; let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::send_conscrit_c2c(caller, dest, fund_info.funds, data, &outpoints) + T::Utxo::submit_c2c_tx(caller, dest, fund_info.funds, data, &outpoints).map(|_| { + >::mutate(&caller, |info| { + info.as_mut().unwrap().utxos = Vec::new(); + info.as_mut().unwrap().funds = 0; + }); + }) } impl ProgrammablePoolApi for Pallet @@ -267,10 +277,18 @@ impl ChainExtension for Pallet< where ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, { + // Fetch AccountId of the caller from the ChainExtension's memory + // This way the progrmmable pool can force the caller of the ChainExtension + // to only spend their own funds as `ContractBalances` will be queried + // using `acc_id` and user cannot control the value of this variable + let mut env = env.buf_in_buf_out(); + let acc_id = env.ext().address().encode(); + let acc_id: T::AccountId = T::AccountId::decode(&mut &acc_id[..]) + .map_err(|_| "Failed to get smart contract's AccountId")?; + match func_id { x if x == ChainExtensionCall::Transfer as u32 => { - let mut env = env.buf_in_buf_out(); - let (acc_id, dest, value): (T::AccountId, T::AccountId, u128) = env.read_as()?; + let (dest, value): (T::AccountId, u128) = env.read_as()?; if !>::get(&dest).is_none() { return Err(DispatchError::Other( @@ -281,9 +299,6 @@ impl ChainExtension for Pallet< send_p2pk_tx::(&acc_id, &dest, value)? } x if x == ChainExtensionCall::Balance as u32 => { - let mut env = env.buf_in_buf_out(); - let acc_id: T::AccountId = env.read_as()?; - let fund_info = >::get(&acc_id).ok_or(DispatchError::Other( "Contract doesn't own any UTXO or it doesn't exist!", ))?; @@ -294,18 +309,17 @@ impl ChainExtension for Pallet< x if x == ChainExtensionCall::Call as u32 => { // `read_as_unbounded()` has to be used here because the size of `data` // is only known during runtime - let mut env = env.buf_in_buf_out(); - let (acc_id, dest, selector, mut data): ( - T::AccountId, - T::AccountId, - [u8; 4], - Vec, - ) = env.read_as_unbounded(env.in_len())?; + let (dest, selector, mut data): (T::AccountId, [u8; 4], Vec) = + env.read_as_unbounded(env.in_len())?; if >::get(&dest).is_none() { return Err(DispatchError::Other("Destination doesn't exist")); } + if acc_id == dest { + return Err(DispatchError::Other("Contract cannot call itself")); + } + // append data to the selector so the final data // passed on to the contract is in correct format let mut selector = selector.to_vec(); diff --git a/pallets/template/Cargo.toml b/pallets/template/Cargo.toml deleted file mode 100644 index 971f543..0000000 --- a/pallets/template/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -authors = ['Substrate DevHub '] -description = 'FRAME pallet template for defining custom runtime logic.' -edition = '2018' -homepage = 'https://substrate.dev' -license = 'Unlicense' -name = 'pallet-template' -readme = 'README.md' -repository = 'https://github.com/substrate-developer-hub/substrate-node-template/' -version = '3.0.0' - -[package.metadata.docs.rs] -targets = ['x86_64-unknown-linux-gnu'] - -[dependencies.codec] -default-features = false -features = ['derive'] -package = 'parity-scale-codec' -version = '2.0.0' - -[dependencies.frame-support] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -version = '4.0.0-dev' -branch = "master" - -[dependencies.frame-system] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -version = '4.0.0-dev' -branch = "master" - -[dependencies.frame-benchmarking] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -optional = true -version = '4.0.0-dev' -branch = "master" - -[dev-dependencies.serde] -version = '1.0.126' - -[dev-dependencies.sp-runtime] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -version = '4.0.0-dev' -branch = "master" - -[dev-dependencies.sp-core] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -version = '4.0.0-dev' -branch = "master" - -[dev-dependencies.sp-io] -default-features = false -git = 'https://github.com/paritytech/substrate.git' -version = '4.0.0-dev' -branch = "master" - -[features] -default = ['std'] -runtime-benchmarks = ['frame-benchmarking'] -std = [ - 'codec/std', - 'frame-support/std', - 'frame-system/std', - 'frame-benchmarking/std', -] -try-runtime = ['frame-support/try-runtime'] diff --git a/pallets/template/README.md b/pallets/template/README.md deleted file mode 100644 index 8d751a4..0000000 --- a/pallets/template/README.md +++ /dev/null @@ -1 +0,0 @@ -License: Unlicense \ No newline at end of file diff --git a/pallets/template/src/benchmarking.rs b/pallets/template/src/benchmarking.rs deleted file mode 100644 index 26cf63d..0000000 --- a/pallets/template/src/benchmarking.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Benchmarking setup for pallet-template - -use super::*; - -#[allow(unused)] -use crate::Pallet as Template; -use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, whitelisted_caller}; -use frame_system::RawOrigin; - -benchmarks! { - do_something { - let s in 0 .. 100; - let caller: T::AccountId = whitelisted_caller(); - }: _(RawOrigin::Signed(caller), s) - verify { - assert_eq!(Something::::get(), Some(s)); - } -} - -impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test,); diff --git a/pallets/template/src/lib.rs b/pallets/template/src/lib.rs deleted file mode 100644 index 76344c0..0000000 --- a/pallets/template/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -/// Edit this file to define custom logic or remove it if it is not needed. -/// Learn more about FRAME and the core library of Substrate FRAME pallets: -/// -pub use pallet::*; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - -#[frame_support::pallet] -pub mod pallet { - use frame_support::{dispatch::DispatchResult, pallet_prelude::*}; - use frame_system::pallet_prelude::*; - - /// Configure the pallet by specifying the parameters and types on which it depends. - #[pallet::config] - pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's definition of an event. - type Event: From> + IsType<::Event>; - } - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - // The pallet's runtime storage items. - // https://substrate.dev/docs/en/knowledgebase/runtime/storage - #[pallet::storage] - #[pallet::getter(fn something)] - // Learn more about declaring storage items: - // https://substrate.dev/docs/en/knowledgebase/runtime/storage#declaring-storage-items - pub type Something = StorageValue<_, u32>; - - // Pallets use events to inform users when important changes are made. - // https://substrate.dev/docs/en/knowledgebase/runtime/events - #[pallet::event] - #[pallet::metadata(T::AccountId = "AccountId")] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Event documentation should end with an array that provides descriptive names for event - /// parameters. [something, who] - SomethingStored(u32, T::AccountId), - } - - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - /// Error names should be descriptive. - NoneValue, - /// Errors should have helpful documentation associated with them. - StorageOverflow, - } - - #[pallet::hooks] - impl Hooks> for Pallet {} - - // Dispatchable functions allows users to interact with the pallet and invoke state changes. - // These functions materialize as "extrinsics", which are often compared to transactions. - // Dispatchable functions must be annotated with a weight and must return a DispatchResult. - #[pallet::call] - impl Pallet { - /// An example dispatchable that takes a singles value as a parameter, writes the value to - /// storage and emits an event. This function must be dispatched by a signed extrinsic. - #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] - pub fn do_something(origin: OriginFor, something: u32) -> DispatchResult { - // Check that the extrinsic was signed and get the signer. - // This function will return an error if the extrinsic is not signed. - // https://substrate.dev/docs/en/knowledgebase/runtime/origin - let who = ensure_signed(origin)?; - - // Update storage. - >::put(something); - - // Emit an event. - Self::deposit_event(Event::SomethingStored(something, who)); - // Return a successful DispatchResultWithPostInfo - Ok(()) - } - - /// An example dispatchable that may throw a custom error. - #[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))] - pub fn cause_error(origin: OriginFor) -> DispatchResult { - let _who = ensure_signed(origin)?; - - // Read a value from storage. - match >::get() { - // Return an error if the value has not been set. - None => Err(Error::::NoneValue)?, - Some(old) => { - // Increment the value read from storage; will error in the event of overflow. - let new = old.checked_add(1).ok_or(Error::::StorageOverflow)?; - // Update the value in storage with the incremented result. - >::put(new); - Ok(()) - } - } - } - } -} diff --git a/pallets/template/src/mock.rs b/pallets/template/src/mock.rs deleted file mode 100644 index c3c0b5b..0000000 --- a/pallets/template/src/mock.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate as pallet_template; -use frame_support::parameter_types; -use frame_system as system; -use sp_core::H256; -use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, -}; - -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -// Configure a mock runtime to test the pallet. -frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - TemplateModule: pallet_template::{Pallet, Call, Storage, Event}, - } -); - -parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 42; -} - -impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type Origin = Origin; - type Call = Call; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = Event; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); -} - -impl pallet_template::Config for Test { - type Event = Event; -} - -// Build genesis storage according to the mock runtime. -pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::default().build_storage::().unwrap().into() -} diff --git a/pallets/template/src/tests.rs b/pallets/template/src/tests.rs deleted file mode 100644 index 39c2180..0000000 --- a/pallets/template/src/tests.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{mock::*, Error}; -use frame_support::{assert_noop, assert_ok}; - -#[test] -fn it_works_for_default_value() { - new_test_ext().execute_with(|| { - // Dispatch a signed extrinsic. - assert_ok!(TemplateModule::do_something(Origin::signed(1), 42)); - // Read pallet storage and assert an expected result. - assert_eq!(TemplateModule::something(), Some(42)); - }); -} - -#[test] -fn correct_error_for_none_value() { - new_test_ext().execute_with(|| { - // Ensure the expected error is thrown when no value is present. - assert_noop!( - TemplateModule::cause_error(Origin::signed(1)), - Error::::NoneValue - ); - }); -} diff --git a/pallets/utxo/README.md b/pallets/utxo/README.md index b98bce9..f0763f4 100644 --- a/pallets/utxo/README.md +++ b/pallets/utxo/README.md @@ -216,7 +216,7 @@ Make sure the _Option_ input box is still empty, then click the **+** butt 4. In node's [chain_spec.rs](https://github.com/mintlayer/mintlayer-node/blob/master/node/src/chain_spec.rs): 4.1. Import the ff: ```rust - use node_template_runtime::{UtxoConfig, pallet_utxo}; + use mintlayer_runtime::{UtxoConfig, pallet_utxo}; use sp_core:H256; ``` 4.2. add one more param on function `testnet_genesis()`: @@ -259,7 +259,7 @@ Make sure the _Option_ input box is still empty, then click the **+** butt 6. Go back to the workspace directory `$> cd ..` and run: ```bash RUST_LOG=runtime=debug - target/release/node-template benchmark + target/release/mintlayer-core benchmark --chain dev --execution=wasm --wasm-execution=compiled diff --git a/pallets/utxo/src/lib.rs b/pallets/utxo/src/lib.rs index 1b16303..f011638 100644 --- a/pallets/utxo/src/lib.rs +++ b/pallets/utxo/src/lib.rs @@ -515,6 +515,10 @@ pub mod pallet { pub(super) type StakingCount = StorageMap<_, Identity, T::AccountId, (u64, Value), OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn ectl_store)] + pub(super) type EctlStore = StorageMap<_, Identity, H256, bool>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::metadata(T::AccountId = "AccountId")] @@ -566,13 +570,10 @@ pub mod pallet { _utxo_hash: H256, _utxo_value: u128, _data: &Vec, - ) { + ) -> Result<(), &'static str> { // let weight: Weight = 6000000000; - - // match T::ProgrammablePool::create(caller, weight, code, utxo_hash, utxo_value, data) { - // Ok(_) => log::info!("success!"), - // Err(e) => log::error!("failure: {:#?}", e), - // } + // T::ProgrammablePool::create(caller, weight, code, utxo_hash, utxo_value, data) + Ok(()) } pub fn call( @@ -582,10 +583,9 @@ pub mod pallet { _utxo_value: u128, _fund_contract: bool, _data: &Vec, - ) { + ) -> Result<(), &'static str> { // let weight: Weight = 6000000000; - - // match T::ProgrammablePool::call( + // T::ProgrammablePool::call( // caller, // dest, // weight, @@ -593,10 +593,8 @@ pub mod pallet { // utxo_value, // fund_contract, // data, - // ) { - // Ok(_) => log::info!("success!"), - // Err(e) => log::error!("failure: {:#?}", e), - // } + // ) + Ok(()) } pub fn validate_transaction( @@ -1040,13 +1038,22 @@ pub mod pallet { log::info!("TODO validate spending of OP_CREATE"); } Destination::CallPP(_, _, _) => { - let spend = - u16::from_le_bytes(input.witness[1..].try_into().or_else(|_| { - Err(DispatchError::Other( - "Failed to convert witness to an opcode", - )) - })?); - ensure!(spend == 0x1337, "OP_SPEND not found"); + // 32-byte hash + 1 byte length + ensure!( + input.witness.len() == 33, + "Witness field doesn't contain valid data" + ); + + let hash: [u8; 32] = input.witness[1..] + .try_into() + .map_err(|_| DispatchError::Other("Failed to convert the slice"))?; + + ensure!( + >::get(&H256::from(hash)).is_some(), + "Transaction does not have access to smart contract outputs" + ); + + >::remove(&H256::from(hash)); } Destination::ScriptHash(_hash) => { let witness = input.witness.clone(); @@ -1082,7 +1089,6 @@ pub mod pallet { /// Update storage to reflect changes made by transaction /// Where each utxo key is a hash of the entire transaction and its order in the TransactionOutputs vector pub fn update_storage( - caller: &T::AccountId, tx: &TransactionFor, reward: Value, ) -> DispatchResultWithPostInfo { @@ -1136,15 +1142,15 @@ pub mod pallet { Some(OutputData::TokenTransferV1 { .. }) | None => continue, } } - Destination::CreatePP(script, data) => { - log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); - >::insert(hash, output); - create::(caller, script, hash, output.value, &data); + Destination::CreatePP(_script, _data) => { + //log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); + //>::insert(hash, output); + //create::(caller, script, hash, output.value, &data)?; } - Destination::CallPP(acct_id, fund, data) => { - log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); - >::insert(hash, output); - call::(caller, acct_id, hash, output.value, *fund, data); + Destination::CallPP(_acct_id, _fund, _data) => { + //log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); + //>::insert(hash, output); + //call::(caller, acct_id, hash, output.value, *fund, data)?; } Destination::LockForStaking { .. } => { staking::lock_for_staking::(hash, output)?; @@ -1159,12 +1165,11 @@ pub mod pallet { } pub fn spend( - caller: &T::AccountId, tx: &TransactionFor, ) -> DispatchResultWithPostInfo { let tx_validity = validate_transaction::(tx)?; ensure!(tx_validity.requires.is_empty(), "missing inputs"); - update_storage::(caller, tx, tx_validity.priority as Value)?; + update_storage::(tx, tx_validity.priority as Value)?; Ok(().into()) } @@ -1211,7 +1216,8 @@ pub mod pallet { origin: OriginFor, tx: Transaction, ) -> DispatchResultWithPostInfo { - spend::(&ensure_signed(origin)?, &tx)?; + ensure_none(origin)?; + spend::(&tx)?; Self::deposit_event(Event::::TransactionSuccess(tx)); Ok(().into()) } @@ -1280,7 +1286,7 @@ pub mod pallet { .ok_or(DispatchError::Other("Failed to sign the transaction"))?; } - spend::(&signer, &tx) + spend::(&tx) } /// unlock the stake using the STASH ACCOUNT. Stops validating, and allow access to withdraw. @@ -1371,10 +1377,11 @@ impl crate::Pallet { // } } -fn coin_picker(outpoints: &Vec) -> Result, DispatchError> { +fn construct_inputs( + outpoints: &Vec, +) -> Result, DispatchError> { let mut inputs: Vec = Vec::new(); - // consensus-critical sorting function... let mut outpoints = outpoints.clone(); outpoints.sort(); @@ -1385,8 +1392,13 @@ fn coin_picker(outpoints: &Vec) -> Result inputs.push(TransactionInput::new_script( *outpoint, Builder::new().into_script(), - Builder::new().push_int(0x1337).into_script(), + Builder::new().push_slice(&outpoint.encode()).into_script(), )); + + // save the outpoint hash of the input UTXO to ECTL + // from which it can be fetched for validation when + // the node receives a TX that tries to spend OP_CALLs + >::insert(outpoint, true); } _ => { return Err(DispatchError::Other("Only CallPP vouts can be spent!")); @@ -1404,14 +1416,13 @@ where type AccountId = T::AccountId; fn spend( - caller: &T::AccountId, + _caller: &T::AccountId, value: u128, address: H256, utxo: H256, sig: H512, ) -> DispatchResultWithPostInfo { spend::( - caller, &Transaction { inputs: vec![TransactionInput::new_with_signature(utxo, sig)], outputs: vec![TransactionOutputFor::::new_pubkey(value, address)], @@ -1430,8 +1441,8 @@ where staking::withdraw::(stash_account_caller.clone()) } - fn send_conscrit_p2pk( - caller: &T::AccountId, + fn submit_c2pk_tx( + _caller: &T::AccountId, dest: &T::AccountId, value: u128, outpoints: &Vec, @@ -1439,39 +1450,30 @@ where let pubkey_raw: [u8; 32] = dest.encode().try_into().map_err(|_| "Failed to get caller's public key")?; - spend::( - caller, - &Transaction { - inputs: coin_picker::(outpoints)?, - outputs: vec![TransactionOutput::new_pubkey(value, H256::from(pubkey_raw))], - time_lock: Default::default(), - }, - ) - .map_err(|_| "Failed to spend the transaction!")?; + let tx = Transaction { + inputs: construct_inputs::(outpoints)?, + outputs: vec![TransactionOutput::new_pubkey(value, H256::from(pubkey_raw))], + time_lock: Default::default(), + }; + + spend::(&tx).map_err(|_| "Failed to spend the transaction!")?; Ok(()) } - fn send_conscrit_c2c( - caller: &Self::AccountId, + fn submit_c2c_tx( + _caller: &Self::AccountId, dest: &Self::AccountId, value: u128, data: &Vec, outpoints: &Vec, ) -> Result<(), DispatchError> { - spend::( - caller, - &Transaction { - inputs: coin_picker::(outpoints)?, - outputs: vec![TransactionOutput::new_call_pp( - value, - dest.clone(), - true, - data.clone(), - )], - time_lock: Default::default(), - }, - ) - .map_err(|_| "Failed to spend the transaction!")?; + let tx = Transaction { + inputs: construct_inputs::(outpoints)?, + outputs: vec![TransactionOutput::new_call_pp(value, dest.clone(), true, data.clone())], + time_lock: Default::default(), + }; + + spend::(&tx).map_err(|_| "Failed to spend the transaction!")?; Ok(()) } } diff --git a/pallets/utxo/src/staking_tests.rs b/pallets/utxo/src/staking_tests.rs index 45a59f1..503a2dd 100644 --- a/pallets/utxo/src/staking_tests.rs +++ b/pallets/utxo/src/staking_tests.rs @@ -42,7 +42,7 @@ fn staking_first_time() { .sign(&[utxo], 0, &karl_pub_key) .expect("karl's pub key not found"); let utxo = &tx1.outputs[0]; - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx1.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx1.clone())); let tx2 = Transaction { inputs: vec![TransactionInput::new_empty(tx1.outpoint(0))], @@ -66,7 +66,7 @@ fn staking_first_time() { .expect("Alice's pub key not found"); let new_utxo_hash = tx2.outpoint(1); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx2)); + assert_ok!(Utxo::spend(Origin::none(), tx2)); assert!(UtxoStore::::contains_key(new_utxo_hash)); assert!(StakingCount::::contains_key(H256::from(greg_pub_key))); assert!(StakingCount::::contains_key(H256::from( @@ -109,7 +109,7 @@ fn simple_staking() { let locked_utxo_hash = tx.outpoint(0); let new_utxo_hash = tx.outpoint(1); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); assert!(UtxoStore::::contains_key(new_utxo_hash)); assert!(LockedUtxos::::contains_key(locked_utxo_hash)); assert!(StakingCount::::contains_key(H256::from( @@ -148,7 +148,7 @@ fn less_than_minimum_stake() { tx.inputs[0].witness = karl_sig.0.to_vec(); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "output value must be equal or more than the minimum stake" ); }) @@ -193,7 +193,7 @@ fn non_mlt_staking() { .expect("karl's pub key not found"); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "only MLT tokens are supported for staking" ); }) @@ -224,7 +224,7 @@ fn controller_staking_again() { .expect(" tom's pub key not found"); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "StashAccountAlreadyRegistered" ); }) @@ -256,7 +256,7 @@ fn stash_account_is_staking() { .expect("alice's public key not found"); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "StashAccountAlreadyRegistered" ); }) @@ -288,7 +288,7 @@ fn simple_staking_extra() { let locked_utxo_hash = tx.outpoint(0); let new_utxo_hash = tx.outpoint(1); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); assert!(UtxoStore::::contains_key(new_utxo_hash)); assert!(LockedUtxos::::contains_key(locked_utxo_hash)); assert_eq!( @@ -324,7 +324,7 @@ fn non_validator_staking_extra() { .expect("greg's pub key not found"); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "StashAccountNotFound" ); }) diff --git a/pallets/utxo/src/tests.rs b/pallets/utxo/src/tests.rs index 9cd3445..50c64cd 100644 --- a/pallets/utxo/src/tests.rs +++ b/pallets/utxo/src/tests.rs @@ -99,8 +99,8 @@ fn test_script_preimage() { time_lock: Default::default(), }; - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx1)); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx2)); + assert_ok!(Utxo::spend(Origin::none(), tx1)); + assert_ok!(Utxo::spend(Origin::none(), tx2)); }) } @@ -131,7 +131,7 @@ fn test_unchecked_2nd_output() { UtxoStore::::insert(utxo1_hash, &tx1.outputs[1]); // When adding a transaction, the output should be reported as already present. assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx1), + Utxo::spend(Origin::none(), tx1), "output already exists" ); }) @@ -156,7 +156,7 @@ fn test_simple_tx() { let (_, init_utxo) = genesis_utxo(); assert!(UtxoStore::::contains_key(H256::from(init_utxo))); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); assert!(!UtxoStore::::contains_key(H256::from(init_utxo))); assert!(UtxoStore::::contains_key(new_utxo_hash)); assert_eq!( @@ -181,7 +181,7 @@ fn attack_with_sending_to_own_account() { tx.inputs[0].witness = karl_sig.0.to_vec(); assert_noop!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "missing inputs" ); }); @@ -193,13 +193,13 @@ fn attack_with_empty_transactions() { // We should use the real input because. Otherwise, appears another error let (_, input) = tx_input_gen_no_signature(); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), Transaction::default()), // empty tx + Utxo::spend(Origin::none(), Transaction::default()), // empty tx "no inputs" ); assert_err!( Utxo::spend( - Origin::signed(H256::zero()), + Origin::none(), Transaction { inputs: vec![input], // an empty tx outputs: vec![], @@ -226,7 +226,7 @@ fn attack_by_double_counting_input() { .sign_unchecked(&utxos[..], 1, &alice_pub_key); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "each input should be used only once" ); }); @@ -244,7 +244,7 @@ fn attack_with_invalid_signature() { }; assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "signature must be valid" ); }); @@ -263,7 +263,7 @@ fn attack_by_permanently_sinking_outputs() { .sign_unchecked(&[utxo0], 0, &alice_pub_key); assert_noop!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "output value must be nonzero" ); }); @@ -285,7 +285,7 @@ fn attack_by_overflowing_value() { .sign_unchecked(&[utxo0], 0, &alice_pub_key); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "output value overflow" ); }); @@ -307,7 +307,7 @@ fn attack_by_overspending() { .sign_unchecked(&[utxo0], 0, &alice_pub_key); assert_noop!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "output value must not exceed input value" ); }) @@ -334,7 +334,7 @@ fn tx_from_alice_to_karl() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let new_utxo_hash = tx.outpoint(1); let new_utxo = tx.outputs[1].clone(); @@ -349,7 +349,7 @@ fn tx_from_alice_to_karl() { } .sign_unchecked(&[new_utxo], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); }); } @@ -376,7 +376,7 @@ fn test_reward() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let utxo_hash = tx.outpoint(0); // if the previous spend succeeded, there should be one utxo @@ -413,7 +413,7 @@ fn test_reward_overflow() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "reward exceed allowed amount" ); }) @@ -433,7 +433,7 @@ fn test_script() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); }) } @@ -451,7 +451,7 @@ fn test_time_lock_tx() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "Time lock restrictions not satisfied", ); }) @@ -473,7 +473,7 @@ fn test_time_lock_script_fail() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); let outpoint = tx1.outpoint(0); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx1)); + assert_ok!(Utxo::spend(Origin::none(), tx1)); // The following should fail because the transaction-level time lock does not conform to // the time lock restrictions imposed by the scripting system. @@ -486,7 +486,7 @@ fn test_time_lock_script_fail() { time_lock: Default::default(), }; assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx2), + Utxo::spend(Origin::none(), tx2), "script verification failed" ); }) @@ -508,7 +508,7 @@ fn attack_double_spend_by_tweaking_input() { time_lock: Default::default(), } .sign_unchecked(&[utxo0], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx0.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx0.clone())); // Create a transaction that spends the same input 10 times by slightly modifying the // redeem script. @@ -527,7 +527,7 @@ fn attack_double_spend_by_tweaking_input() { time_lock: Default::default(), }; assert_err!( - Utxo::spend(Origin::signed(H256::zero()), tx1), + Utxo::spend(Origin::none(), tx1), "each input should be used only once" ); }); @@ -630,7 +630,17 @@ fn test_send_to_address() { }) } +// Proptest config to decrease the number of test runs by the factor of 16 (for expensive tests). +fn proptest_expensive() -> proptest::test_runner::Config { + let mut config = proptest::test_runner::Config::default(); + config.cases /= 16; + config +} + proptest! { + // These tests are fairly expensive, run fewer of them. + #![proptest_config(proptest_expensive())] + #[test] fn prop_gen_block_time_real_works(bt in gen_block_time_real()) { // This generator should not sample block-based time. @@ -660,14 +670,14 @@ proptest! { } .sign_unchecked(&[utxo0], 0, &alice); let outpoint = tx1.outpoint(0); - assert!(Utxo::spend(Origin::signed(H256::zero()), tx1).is_ok()); + assert!(Utxo::spend(Origin::none(), tx1).is_ok()); let tx2 = Transaction { inputs: vec![TransactionInput::new_script(outpoint, script, Default::default())], outputs: vec![TransactionOutput::new_pubkey(ALICE_GENESIS_BALANCE - u32::MAX as Value, H256::from(alice))], time_lock: tx_lock_time, }; - Utxo::spend(Origin::signed(H256::zero()), tx2) + Utxo::spend(Origin::none(), tx2) }); // The transaction should be accepted if and only if: @@ -808,7 +818,7 @@ fn test_token_issuance() { // After calling `Utxo::spend`, we should check that Storages successfully changed. // If it successfully wrote a new UTXO in the Storage, tx goes through all verifications correctly. assert!(UtxoStore::::contains_key(H256::from(init_utxo))); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); assert!(!UtxoStore::::contains_key(H256::from(init_utxo))); // Checking a new UTXO assert!(UtxoStore::::contains_key(new_utxo_hash)); @@ -858,7 +868,7 @@ fn test_token_issuance() { // let new_utxo_hash = tx.outpoint(0); // let (_, init_utxo) = genesis_utxo(); // assert!(UtxoStore::::contains_key(H256::from(init_utxo))); -// assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); +// assert_ok!(Utxo::spend(Origin::none(), tx)); // assert!(!UtxoStore::::contains_key(H256::from(init_utxo))); // assert!(UtxoStore::::contains_key(new_utxo_hash)); // assert_eq!( @@ -904,7 +914,7 @@ fn test_token_issuance() { // let (_, init_utxo) = genesis_utxo(); // // Submit // assert!(UtxoStore::::contains_key(H256::from(init_utxo))); -// assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); +// assert_ok!(Utxo::spend(Origin::none(), tx.clone())); // assert!(!UtxoStore::::contains_key(H256::from(init_utxo))); // // Checking a new UTXO // assert!(UtxoStore::::contains_key(new_utxo_hash)); @@ -929,7 +939,7 @@ fn test_token_issuance() { // // Submit // assert!(UtxoStore::::contains_key(H256::from(new_utxo_hash))); // frame_support::assert_err_ignore_postinfo!( -// Utxo::spend(Origin::signed(H256::zero()), tx), +// Utxo::spend(Origin::none(), tx), // "digital data has already been minted" // ); // }); @@ -958,14 +968,14 @@ macro_rules! test_tx { // We can check what error we are expecting if stringify!($checking) == "Err" { frame_support::assert_err_ignore_postinfo!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), $err ); assert!(UtxoStore::::contains_key(H256::from(init_utxo))); assert!(!UtxoStore::::contains_key(new_utxo_hash)); } else if stringify!($checking) == "Ok" { // We can check is that success - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx)); + assert_ok!(Utxo::spend(Origin::none(), tx)); assert!(!UtxoStore::::contains_key(H256::from(init_utxo))); assert!(UtxoStore::::contains_key(new_utxo_hash)); } @@ -1105,7 +1115,7 @@ fn test_two_token_creation_in_one_tx() { } .sign_unchecked(&[utxo0], 0, &alice_pub_key); frame_support::assert_err_ignore_postinfo!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "this id can't be used for a new token" ); }); @@ -1143,7 +1153,7 @@ where } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); let token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let token_utxo_hash = tx.outpoint(1); let token_utxo: TransactionOutput = tx.outputs[1].clone(); @@ -1156,7 +1166,7 @@ where token_utxo, ); frame_support::assert_err_ignore_postinfo!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), expecting_err_msg ); }); @@ -1299,7 +1309,7 @@ fn test_token_transfer() { } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); let token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let token_utxo_hash = tx.outpoint(1); let token_utxo = tx.outputs[1].clone(); @@ -1327,7 +1337,7 @@ fn test_token_transfer() { time_lock: Default::default(), } .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let alice_tokens_utxo_hash = tx.outpoint(0); let karl_tokens_utxo_hash = tx.outpoint(1); let karl_tokens_utxo = tx.outputs[1].clone(); @@ -1361,7 +1371,7 @@ fn test_token_transfer() { time_lock: Default::default(), } .sign_unchecked(&[karl_tokens_utxo], 0, &karl_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); assert_eq!( 300_000_000, UtxoStore::::get(alice_tokens_utxo_hash) @@ -1416,7 +1426,7 @@ fn test_token_transfer() { // time_lock: Default::default(), // } // .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); -// assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); +// assert_ok!(Utxo::spend(Origin::none(), tx.clone())); // let token_utxo_hash = tx.outpoint(1); // let token_utxo = tx.outputs[1].clone(); // @@ -1435,7 +1445,7 @@ fn test_token_transfer() { // } // .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); // frame_support::assert_err_ignore_postinfo!( -// Utxo::spend(Origin::signed(H256::zero()), tx), +// Utxo::spend(Origin::none(), tx), // "input for the token not found" // ); // // Let's fail on exceed token amount @@ -1453,7 +1463,7 @@ fn test_token_transfer() { // } // .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); // frame_support::assert_err_ignore_postinfo!( -// Utxo::spend(Origin::signed(H256::zero()), tx), +// Utxo::spend(Origin::none(), tx), // "output value must not exceed input value" // ); // @@ -1472,7 +1482,7 @@ fn test_token_transfer() { // } // .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); // frame_support::assert_err_ignore_postinfo!( -// Utxo::spend(Origin::signed(H256::zero()), tx), +// Utxo::spend(Origin::none(), tx), // "output value must not exceed input value" // ); // @@ -1490,7 +1500,7 @@ fn test_token_transfer() { // time_lock: Default::default(), // } // .sign_unchecked(&[token_utxo], 0, &karl_pub_key); -// assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); +// assert_ok!(Utxo::spend(Origin::none(), tx.clone())); // let nft_utxo_hash = tx.outpoint(0); // assert!(!UtxoStore::::contains_key(H256::from( // token_utxo_hash @@ -1539,7 +1549,7 @@ fn test_token_creation_with_insufficient_fee() { } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let token_utxo_hash = tx.outpoint(1); let token_utxo = tx.outputs[1].clone(); let tx = Transaction { @@ -1561,7 +1571,7 @@ fn test_token_creation_with_insufficient_fee() { } .sign_unchecked(&[token_utxo], 0, &karl_pub_key); frame_support::assert_err_ignore_postinfo!( - Utxo::spend(Origin::signed(H256::zero()), tx), + Utxo::spend(Origin::none(), tx), "insufficient fee" ); }); @@ -1595,7 +1605,7 @@ fn test_transfer_and_issuance_in_one_tx() { } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); let first_issuance_token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let token_utxo_hash = tx.outpoint(1); let token_utxo = tx.outputs[1].clone(); @@ -1634,7 +1644,7 @@ fn test_transfer_and_issuance_in_one_tx() { time_lock: Default::default(), } .sign_unchecked(&[token_utxo.clone()], 0, &karl_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let alice_transfer_utxo_hash = tx.outpoint(0); let karl_transfer_utxo_hash = tx.outpoint(1); let karl_issuance_utxo_hash = tx.outpoint(2); @@ -1722,7 +1732,7 @@ fn test_transfer_for_multiple_tokens() { } .sign_unchecked(&[utxo0.clone()], 0, &alice_pub_key); let tkn1_token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let tkn1_utxo_hash = tx.outpoint(0); let tkn1_utxo = tx.outputs[0].clone(); // @@ -1755,7 +1765,7 @@ fn test_transfer_for_multiple_tokens() { } .sign_unchecked(&[tkn1_utxo.clone()], 0, &karl_pub_key); let tkn2_token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let tkn1_utxo_hash = tx.outpoint(0); let tkn2_utxo_hash = tx.outpoint(1); // @@ -1799,7 +1809,7 @@ fn test_transfer_for_multiple_tokens() { .sign_unchecked(&prev_utxos, 0, &alice_pub_key) .sign_unchecked(&prev_utxos, 1, &alice_pub_key); let tkn3_token_id = TokenId::new(&tx.inputs[0]); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let tkn1_utxo_hash = tx.outpoint(0); let tkn2_utxo_hash = tx.outpoint(1); let tkn3_utxo_hash = tx.outpoint(2); @@ -1844,7 +1854,7 @@ fn test_transfer_for_multiple_tokens() { .sign_unchecked(&prev_utxos, 0, &karl_pub_key) .sign_unchecked(&prev_utxos, 1, &karl_pub_key) .sign_unchecked(&prev_utxos, 2, &karl_pub_key); - assert_ok!(Utxo::spend(Origin::signed(H256::zero()), tx.clone())); + assert_ok!(Utxo::spend(Origin::none(), tx.clone())); let tkn1_utxo_hash = tx.outpoint(0); let tkn2_utxo_hash = tx.outpoint(1); let tkn3_utxo_hash = tx.outpoint(2); diff --git a/pallets/utxo/src/weights.rs b/pallets/utxo/src/weights.rs index 47806f1..b3086aa 100644 --- a/pallets/utxo/src/weights.rs +++ b/pallets/utxo/src/weights.rs @@ -5,7 +5,7 @@ //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 // Executed Command: -// target/release/node-template +// target/release/mintlayer-core // benchmark // --chain // dev diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 1c0c972..d843826 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -3,8 +3,8 @@ authors = ['Substrate DevHub '] edition = '2018' homepage = 'https://substrate.dev' license = 'Unlicense' -name = 'node-template-runtime' -repository = 'https://github.com/substrate-developer-hub/substrate-node-template/' +name = 'mintlayer-runtime' +repository = 'https://github.com/mintlayer/core' version = '3.0.0' [package.metadata.docs.rs] @@ -229,11 +229,6 @@ git = 'https://github.com/paritytech/substrate.git' version = '4.0.0-dev' branch = "master" -[dependencies.pallet-template] -default-features = false -version = '3.0.0' -path = '../pallets/template' - [dependencies.pallet-utxo] default-features = false path = "../pallets/utxo" @@ -248,6 +243,7 @@ path = "../pallets/pp" [features] default = ['std'] +short-block-time = [] runtime-benchmarks = [ 'frame-benchmarking', 'frame-support/runtime-benchmarks', @@ -255,7 +251,6 @@ runtime-benchmarks = [ 'frame-system/runtime-benchmarks', 'hex-literal', 'pallet-balances/runtime-benchmarks', - 'pallet-template/runtime-benchmarks', 'pallet-timestamp/runtime-benchmarks', 'pallet-utxo/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', @@ -274,7 +269,6 @@ std = [ 'pallet-session/std', 'pallet-staking/std', 'pallet-sudo/std', - 'pallet-template/std', 'pallet-timestamp/std', 'pallet-transaction-payment-rpc-runtime-api/std', 'pallet-transaction-payment/std', diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 31fa527..4bf2451 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -47,9 +47,6 @@ use pallet_transaction_payment::CurrencyAdapter; pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Percent, Permill}; -/// Import the template pallet. -pub use pallet_template; - pub use pallet_pp; pub use pallet_utxo; use pallet_utxo::MLT_UNIT; @@ -125,7 +122,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { /// up by `pallet_aura` to implement `fn slot_duration()`. /// /// Change this to adjust the block time. -pub const MILLISECS_PER_BLOCK: u64 = 60_000; //1 min +#[cfg(not(feature = "short-block-time"))] +pub const MILLISECS_PER_BLOCK: u64 = 60_000; // 1 min +#[cfg(feature = "short-block-time")] +pub const MILLISECS_PER_BLOCK: u64 = 3_000; // 3 sec // NOTE: Currently it is not possible to change the slot duration after the chain has started. // Attempting to do so will brick block production. @@ -307,11 +307,6 @@ impl pallet_sudo::Config for Runtime { type Call = Call; } -/// Configure the pallet-template in pallets/template. -impl pallet_template::Config for Runtime { - type Event = Event; -} - parameter_types! { pub const MinimumStake: u128 = MINIMUM_STAKE; pub const StakeWithdrawalFee: u128 = 1 * MLT_UNIT; @@ -508,8 +503,6 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, TransactionPayment: pallet_transaction_payment::{Pallet, Storage}, Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event}, - // Include the custom logic from the pallet-template in the runtime. - TemplateModule: pallet_template::{Pallet, Call, Storage, Event}, Utxo: pallet_utxo::{Pallet, Call, Config, Storage, Event}, Pp: pallet_pp::{Pallet, Call, Config, Storage, Event}, Contracts: pallet_contracts::{Pallet, Call, Storage, Event}, @@ -769,7 +762,6 @@ impl_runtime_apis! { add_benchmark!(params, batches, frame_system, SystemBench::); add_benchmark!(params, batches, pallet_balances, Balances); add_benchmark!(params, batches, pallet_timestamp, Timestamp); - add_benchmark!(params, batches, pallet_template, TemplateModule); add_benchmark!(params, batches, pallet_utxo, Utxo); add_benchmark!(params, batches, pallet_pp, Pp); diff --git a/scripts/docker_run.sh b/scripts/docker_run.sh index 61e6b0f..51581ca 100755 --- a/scripts/docker_run.sh +++ b/scripts/docker_run.sh @@ -2,9 +2,9 @@ set -e -echo "*** Start Substrate node template ***" +echo "*** Start Mintlayer node ***" cd $(dirname ${BASH_SOURCE[0]})/.. docker-compose down --remove-orphans -docker-compose run --rm --service-ports dev $@ \ No newline at end of file +docker-compose run --rm --service-ports dev $@ diff --git a/scripts/wallet.py b/scripts/wallet.py index c317256..2bb0f47 100755 --- a/scripts/wallet.py +++ b/scripts/wallet.py @@ -53,10 +53,37 @@ def withdrawal_era(self): def current_era(self): return self.client.current_era() + def submit(self, tx): + call = self.client.substrate.compose_call( + call_module = 'Utxo', + call_function = 'spend', + call_params = { 'tx': tx.json() }, + ) + extrinsic = self.client.substrate.create_unsigned_extrinsic(call=call) + + def result_handler(message, update_nr, sub_id): + if 'params' in message and type(message['params']['result']) is dict: + res = message['params']['result'] + if 'inBlock' in res: + return { + 'block_hash': res['inBlock'] + } + + result = self.client.substrate.rpc_request( + "author_submitAndWatchExtrinsic", + [str(extrinsic.data)], + result_handler=result_handler + ) + + if 'block_hash' in result: + print('Transaction included in block', result['block_hash']) + + return result + def balance(args): print('Total Free:', Account(args).mlt_balance(), 'MLT') - print('Total Locked:', Account(args).locked_mlt_balance(), 'MLT' ) + print('Total Locked:', Account(args).locked_mlt_balance(), 'MLT') def print_key_info(keypair): print('Seed hex :', keypair.seed_hex or 'UNKNOWN') @@ -128,7 +155,7 @@ def lock(args): ), ] ).sign(acct.keypair, [u for (_, u) in utxos]) - acct.client.submit(acct.keypair, tx) + acct.submit(tx) def lock_extra(args): @@ -136,6 +163,7 @@ def lock_extra(args): utxo_value = Decimal() utxos = [] amount = int(args.amount * MLT_UNIT) + fee = int() if amount < 0: raise Exception('Sending a negative amount') @@ -171,7 +199,7 @@ def lock_extra(args): ), ] ).sign(acct.keypair, [u for (_, u) in utxos]) - acct.client.submit(acct.keypair, tx) + acct.submit(tx) def pay(args): @@ -179,6 +207,11 @@ def pay(args): utxo_value = Decimal() utxos = [] amount = int(args.amount * MLT_UNIT) + fee = int(args.fee * MLT_UNIT) + total = amount + fee + + if fee >= 2 ** 64: + raise Exception('Fee too high') if amount < 0: raise Exception('Sending a negative amount') @@ -186,18 +219,14 @@ def pay(args): for (h, u) in acct.mlt_utxos(): utxos.append((h, u)) utxo_value += u.value - if utxo_value >= amount: + if utxo_value >= total: break - if utxo_value < amount: - raise Exception('Not enough funds') - - fee = int(MLT_UNIT / 10) # TODO - if fee >= 2 ** 64: - raise Exception('Fee too large') - change = utxo_value - amount - fee + if change < 0: + raise Exception('Not enough funds') + tx = mint.Transaction( acct.client, inputs=[ mint.Input(h) for (h, _) in utxos ], @@ -214,7 +243,7 @@ def pay(args): ), ] ).sign(acct.keypair, [u for (_, u) in utxos]) - acct.client.submit(acct.keypair, tx) + acct.submit(tx) def parse_command_line(): ap = argparse.ArgumentParser(description='Mintlayer command line interface') @@ -248,6 +277,8 @@ def parse_command_line(): pay_cmd = sub.add_parser('pay', help='Submit a payment') pay_cmd.set_defaults(func=pay) + pay_cmd.add_argument('--fee', type=Decimal, default=Decimal('0.1'), metavar='AMOUNT', + help='Specify the amount of MLT paid in fees') pay_cmd.add_argument('key', type=str, metavar='SENDER_KEY', help='Sender private key') pay_cmd.add_argument('to', type=str, metavar='RECEPIENT_PUBKEY', @@ -295,6 +326,9 @@ def parse_command_line(): def main(): try: cmd = parse_command_line() + if hasattr(cmd, 'key') and os.path.isfile(cmd.key): + with open(cmd.key) as f: + cmd.key = f.readline().strip() cmd.func(cmd) except Exception as e: print(e) diff --git a/test/functional/metadata.json b/test/functional/assets/c2c_tester.json similarity index 56% rename from test/functional/metadata.json rename to test/functional/assets/c2c_tester.json index cf24b11..bde799b 100644 --- a/test/functional/metadata.json +++ b/test/functional/assets/c2c_tester.json @@ -1,46 +1,22 @@ { "metadataVersion": "0.1.0", "source": { - "hash": "0xd2d4276c5864e736fe51fd70c2e76ae600520dbead60bf3b8054f6d3d13e3dd7", + "hash": "0x241e86037e1803891112031bb03b7816d4b00d50effc920dae39c2004efb2ca4", "language": "ink! 3.0.0-rc4", "compiler": "rustc 1.56.0-nightly" }, "contract": { - "name": "pooltest", + "name": "c2c_tester", "version": "0.1.0", "authors": [ - "[your_name] <[your_email]>" + "RBB S.r.l" ] }, "spec": { "constructors": [ - { - "args": [ - { - "name": "init_value", - "type": { - "displayName": [ - "bool" - ], - "type": 1 - } - } - ], - "docs": [ - "Constructor that initializes the `bool` value to the given `init_value`." - ], - "name": [ - "new" - ], - "selector": "0x9bae9d5e" - }, { "args": [], - "docs": [ - "Constructor that initializes the `bool` value to `false`.", - "", - "Constructors can delegate to other constructors." - ], + "docs": [], "name": [ "default" ], @@ -51,25 +27,29 @@ "events": [], "messages": [ { - "args": [], - "docs": [ - " A message that can be called on instantiated contracts.", - " This one flips the value of the stored `bool` from `true`", - " to `false` and vice versa." + "args": [ + { + "name": "value", + "type": { + "displayName": [ + "u32" + ], + "type": 1 + } + } ], + "docs": [], "mutates": true, "name": [ - "flip" + "set_value" ], "payable": false, "returnType": null, - "selector": "0x633aa551" + "selector": "0xc6298215" }, { "args": [], - "docs": [ - " Simply returns the current value of our `bool`." - ], + "docs": [], "mutates": false, "name": [ "get" @@ -77,7 +57,7 @@ "payable": false, "returnType": { "displayName": [ - "bool" + "u32" ], "type": 1 }, @@ -103,7 +83,7 @@ "types": [ { "def": { - "primitive": "bool" + "primitive": "u32" } } ] diff --git a/test/functional/assets/c2c_tester.wasm b/test/functional/assets/c2c_tester.wasm new file mode 100644 index 0000000..764fe2b Binary files /dev/null and b/test/functional/assets/c2c_tester.wasm differ diff --git a/test/functional/assets/pooltester.json b/test/functional/assets/pooltester.json new file mode 100644 index 0000000..6501429 --- /dev/null +++ b/test/functional/assets/pooltester.json @@ -0,0 +1,216 @@ +{ + "metadataVersion": "0.1.0", + "source": { + "hash": "0x2d1e1ed093dfb95c258342e80e7e1800c8c87cf4459837291a35245d23d15038", + "language": "ink! 3.0.0-rc4", + "compiler": "rustc 1.56.0-nightly" + }, + "contract": { + "name": "pooltester", + "version": "0.1.0", + "authors": [ + "RBB S.r.l" + ] + }, + "spec": { + "constructors": [ + { + "args": [ + { + "name": "value", + "type": { + "displayName": [ + "i64" + ], + "type": 1 + } + } + ], + "docs": [], + "name": [ + "new" + ], + "selector": "0x9bae9d5e" + }, + { + "args": [], + "docs": [], + "name": [ + "default" + ], + "selector": "0xed4b9d1b" + } + ], + "docs": [], + "events": [], + "messages": [ + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "get" + ], + "payable": false, + "returnType": { + "displayName": [ + "i64" + ], + "type": 1 + }, + "selector": "0x2f865bd9" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "flip" + ], + "payable": false, + "returnType": null, + "selector": "0x633aa551" + }, + { + "args": [ + { + "name": "dest", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "send_to_pubkey" + ], + "payable": false, + "returnType": null, + "selector": "0xd10be299" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "fund" + ], + "payable": false, + "returnType": null, + "selector": "0x4aafa343" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "send_to_self" + ], + "payable": false, + "returnType": null, + "selector": "0xba6ee83a" + }, + { + "args": [ + { + "name": "dest", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + }, + { + "name": "selector", + "type": { + "displayName": [], + "type": 5 + } + }, + { + "name": "value", + "type": { + "displayName": [ + "i64" + ], + "type": 1 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "call_contract" + ], + "payable": false, + "returnType": null, + "selector": "0xc7c0b7ca" + } + ] + }, + "storage": { + "struct": { + "fields": [ + { + "layout": { + "cell": { + "key": "0x0000000000000000000000000000000000000000000000000000000000000000", + "ty": 1 + } + }, + "name": "value" + } + ] + } + }, + "types": [ + { + "def": { + "primitive": "i64" + } + }, + { + "def": { + "composite": { + "fields": [ + { + "type": 3, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_env", + "types", + "AccountId" + ] + }, + { + "def": { + "array": { + "len": 32, + "type": 4 + } + } + }, + { + "def": { + "primitive": "u8" + } + }, + { + "def": { + "array": { + "len": 4, + "type": 4 + } + } + } + ] +} \ No newline at end of file diff --git a/test/functional/assets/pooltester.wasm b/test/functional/assets/pooltester.wasm new file mode 100644 index 0000000..515b1d4 Binary files /dev/null and b/test/functional/assets/pooltester.wasm differ diff --git a/test/functional/code.wasm b/test/functional/code.wasm deleted file mode 100644 index d3c74df..0000000 Binary files a/test/functional/code.wasm and /dev/null differ diff --git a/test/functional/feature_alice_bob_test.py b/test/functional/feature_alice_bob_test.py index 1de62a7..b13d26c 100755 --- a/test/functional/feature_alice_bob_test.py +++ b/test/functional/feature_alice_bob_test.py @@ -17,6 +17,7 @@ connect_nodes, wait_until, ) +from test_framework.messages import COIN class ExampleTest(MintlayerTestFramework): @@ -65,6 +66,8 @@ def run_test(self): # fetch the genesis utxo from storage utxos = list(client.utxos_for(alice)) + print("how are you: ",utxos[0][1].json()) + tx1 = utxo.Transaction( client, inputs=[ @@ -72,10 +75,15 @@ def run_test(self): ], outputs=[ utxo.Output( - value=50, + value=50 * COIN, destination=utxo.DestPubkey(bob.public_key), data=None ), + utxo.Output( + value=39999999940 * COIN, + destination=utxo.DestPubkey(alice.public_key), + data=None + ) ] ).sign(alice, [utxos[0][1]]) client.submit(alice, tx1) @@ -87,17 +95,17 @@ def run_test(self): ], outputs=[ utxo.Output( - value=30, + value=30 * COIN, destination=utxo.DestPubkey(alice.public_key), data=None ), utxo.Output( - value=20, + value=15 * COIN, destination=utxo.DestPubkey(bob.public_key), data=None ), ] - ).sign(bob, tx1.outputs) + ).sign(bob, [tx1.outputs[0]]) client.submit(bob, tx2) diff --git a/test/functional/feature_alice_rain_test.py b/test/functional/feature_alice_rain_test.py new file mode 100755 index 0000000..9aaf4fe --- /dev/null +++ b/test/functional/feature_alice_rain_test.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 RBB S.r.l +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""An example functional test + +Send a transaction from Alice to Bob, then spend Bob's transaction +""" + +from substrateinterface import Keypair +import test_framework.mintlayer.utxo as utxo +from test_framework.messages import COIN + +from test_framework.test_framework import MintlayerTestFramework +from test_framework.util import ( + assert_equal, + connect_nodes, + wait_until, +) + + +class ExampleTest(MintlayerTestFramework): + # Each functional test is a subclass of the MintlayerTestFramework class. + + # Override the set_test_params(), add_options(), setup_chain(), setup_network() + # and setup_nodes() methods to customize the test setup as required. + + def set_test_params(self): + """Override test parameters for your individual test. + + This method must be overridden and num_nodes must be exlicitly set.""" + self.setup_clean_chain = True + self.num_nodes = 3 + # Use self.extra_args to change command-line arguments for the nodes + self.extra_args = [["--validator=false"], [], []] + + # self.log.info("I've finished set_test_params") # Oops! Can't run self.log before run_test() + + def setup_network(self): + """Setup the test network topology + + Often you won't need to override this, since the standard network topology + (linear: node0 <-> node1 <-> node2 <-> ...) is fine for most tests. + + If you do override this method, remember to start the nodes, assign + them to self.nodes, connect them and then sync.""" + + self.setup_nodes() + + def custom_method(self): + """Do some custom behaviour for this test + + Define it in a method here because you're going to use it repeatedly. + If you think it's useful in general, consider moving it to the base + MintlayerTestFramework class so other tests can use it.""" + + self.log.info("Running custom_method") + + def run_test(self): + client = self.nodes[0].rpc_client + + alice = Keypair.create_from_uri('//Alice') + rain = Keypair.create_from_uri('//Rain') + rude = Keypair.create_from_uri('//Rude') + + # fetch the genesis utxo from storage + utxos = list(client.utxos_for(alice)) + + tx1 = utxo.Transaction( + client, + inputs=[ + utxo.Input(utxos[0][0]), + ], + outputs=[ + utxo.Output( + value=50 * COIN, + destination=utxo.DestPubkey(rain.public_key), + data=None + ), + utxo.Output( + value=39999999949 * COIN, + destination=utxo.DestPubkey(alice.public_key), + data=None + ) + ] + ).sign(alice, [utxos[0][1]]) + client.submit(alice, tx1) + + tx2 = utxo.Transaction( + client, + inputs=[ + utxo.Input(tx1.outpoint(0)), + ], + outputs=[ + utxo.Output( + value=30 * COIN, + destination=utxo.DestPubkey(rude.public_key), + data=None + ), + utxo.Output( + value=19 * COIN, + destination=utxo.DestPubkey(rain.public_key), + data=None + ), + ] + ).sign(rain, [tx1.outputs[0]]) + client.submit(rain, tx2) + +if __name__ == '__main__': + ExampleTest().main() diff --git a/test/functional/feature_smart_contract_test.py b/test/functional/feature_smart_contract_test.py index d155788..487c841 100755 --- a/test/functional/feature_smart_contract_test.py +++ b/test/functional/feature_smart_contract_test.py @@ -3,9 +3,9 @@ # Copyright (c) 2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Smart contract test -""" +""" Smart contract test """ + # Imports should be in PEP8 ordering (std library first, then third party # libraries then local imports). from collections import defaultdict @@ -23,6 +23,23 @@ import os +# helper function to reduce code duplication +def submit_pp_tx(client, input_utxo, alice, value, output): + tx = utxo.Transaction( + client, + inputs=[ + utxo.Input(input_utxo.outpoint(0)), + ], + outputs=[ + utxo.Output( + value=value, + destination=utxo.DestPubkey(alice.public_key), + data=None, + ), + output + ] + ).sign(alice, [input_utxo.outputs[0]], [0]) + return tx, client.submit(alice, tx) class ExampleTest(MintlayerTestFramework): # Each functional test is a subclass of the MintlayerTestFramework class. @@ -56,97 +73,290 @@ def setup_network(self): # sync_all() should not include node2, since we're not expecting it to # sync. connect_nodes(self.nodes[0], self.nodes[1]) - # self.sync_all([self.nodes[0:1]]) def run_test(self): """Main test logic""" client = self.nodes[0].rpc_client - substrate = client.substrate - alice = Keypair.create_from_uri('//Alice') + bob = Keypair.create_from_uri('//Erin') - # Find a suitable UTXO initial_utxo = [x for x in client.utxos_for(alice) if x[1].value >= 50][0] + value = initial_utxo[1].json()["value"] + + self.log.error(initial_utxo) - tx0 = utxo.Transaction( + tx = utxo.Transaction( client, inputs=[ utxo.Input(initial_utxo[0]), ], outputs=[ utxo.Output( - value=50, + value=value, destination=utxo.DestPubkey(alice.public_key), - data=None + data=None, ), - utxo.Output( - value=10, - destination=utxo.DestCreatePP( - code=os.path.join(os.path.dirname(__file__), "code.wasm"), - data=[0xed, 0x4b, 0x9d, 0x1b], # default() constructor selector - ), - data=None - ), - # This output prevent reward overflow - utxo.Output( - value=3981553255926290448385, # = genesis amount - u64::MAX - destination=utxo.DestPubkey(alice.public_key), - data=None - ) ] ).sign(alice, [initial_utxo[1]]) + client.submit(alice, tx) - # submit transaction and get the extrinsic and block hashes - (ext, blk,_) = client.submit(alice, tx0) + # invalid bytecode + value -= 1 - # each new smart contract instantiation creates a new account - # fetch this SS58-formatted account address and return it - # and the hex-encoded account id - (ss58, acc_id) = contract.getContractAddresses(substrate, blk) + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCreatePP( + code=[0x00], + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + assert_equal(contract.getContractAddresses(substrate, blk), None) + + # invalid value + (invalid_tx, res) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=0, + destination=utxo.DestCreatePP( + code=os.path.join(os.path.dirname(__file__), "assets/pooltester.wasm"), + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + assert_equal(res, None) - # create new contract instance which can be used to interact - # with the instantiated contract + # valid data + value -= 1 + + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCreatePP( + code=os.path.join(os.path.dirname(__file__), "assets/pooltester.wasm"), + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + + (ss58, acc_id) = contract.getContractAddresses(substrate, blk) contractInstance = contract.ContractInstance( ss58, - os.path.join(os.path.dirname(__file__), "metadata.json"), + os.path.join(os.path.dirname(__file__), "assets/pooltester.json"), substrate ) - # read the value of the flipper contract + # verify the initial state of the smart contract result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, False) + assert_equal(result.contract_result_data.value, 1337) + # valid contract call + value -= 1 msg_data = contractInstance.generate_message_data("flip", {}) - self.log.info("Contract msg_data: {}, {}, {}".format(ss58, acc_id, msg_data)) - tx1 = utxo.Transaction( + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCallPP( + dest_account=acc_id, + fund=False, + input_data=bytes.fromhex(msg_data.to_hex()[2:]), + ), + data=None, + )) + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + # invalid `value` given + msg_data = contractInstance.generate_message_data("flip", {}) + + (invalid_tx, res) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=0, + destination=utxo.DestCallPP( + dest_account=alice.public_key, + fund=False, + input_data=bytes.fromhex(msg_data.to_hex()[2:]), + ), + data=None, + )) + assert_equal(res, None) + + # test contract-to-p2k transfer from alice to bob + # + # `send_to_pubkey()` first funds the smart contract from alice's funds + # and when the wasm code is executed, the funds are transferred to bob + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": bob.public_key }) + value -= 555 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 555, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that bob actually received the utxo + bobs_utxos = [x for x in client.utxos_for(bob)] + assert_equal(len(bobs_utxos), 1) + assert_equal(bobs_utxos[0][1].json()['value'], 555) + + # test contract-to-p2pk again but this time don't fund the contract + # meaning that after the TX, bob only has the UTXO he received in the previous test case + # and the contract has a UTXO with value 666 + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": bob.public_key }) + value -= 666 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 666, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = False, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that bob still has the same amount of UTXOs + utxos = [x for x in client.utxos_for(bob)] + assert_equal(len(utxos), 1) + + # verify that the contract has one utxo with value 666 + utxos = [x for x in client.utxos_for(acc_id[2:])] + assert_equal(len(utxos), 1) + assert_equal(utxos[0][1].json()["value"], 666) + + # try to call a contract that doesn't exist (alice's public key + # doesn't point to a valid smart contract) + # + # TODO: because we don't have gas refunding, the money is still + # spent, i.e., if the UTXO set is queried, you'll find a UTXO + # with value 888 meaning user just lost his money which is + # not the correct behavior but the implementation is still under way + msg_data = contractInstance.generate_message_data("fund", {}) + value -= 888 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 888, + destination = utxo.DestCallPP( + dest_account = alice.public_key, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + # Test cross-contract calls + # + # First instantiate another smart contract and verify it has + # been created correctly by querying its value. + # + # Then call the `set_value()` method of newly instantiated contract + # indirectly by creating a UTXO that calls the pooltester's + # `call_contract()` method which dispatches the call to `set_value()` + # + # When all that's done, query the value again and verify that it has been updated + value -= 111 + + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 111, + destination = utxo.DestCreatePP( + code = os.path.join(os.path.dirname(__file__), "assets/c2c_tester.wasm"), + data = [0xed, 0x4b, 0x9d, 0x1b], + ), + data = None, + )) + + (ss58_c2c, acc_id_c2c) = contract.getContractAddresses(substrate, blk) + c2cInstance = contract.ContractInstance( + ss58_c2c, + os.path.join(os.path.dirname(__file__), "assets/c2c_tester.json"), + substrate + ) + + # verify the initial state of the smart contract + result = c2cInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 555) + + msg_data = contractInstance.generate_message_data("call_contract", { + "dest": acc_id_c2c, + "selector": "0xc6298215", + "value": 999, + }) + value -= 222 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 222, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that the call succeeded + result = c2cInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 999) + + # Try to spend the funds of a contract + # + # First fund the contract with some amount of UTXO, + # verify that the fund worked (updated state variable) + # and then try to spend those funds and verify that the + # spend is rejected by the local PP node because the + # smart contract has not spent them and thus the outpoint + # hash is not in the local storage + # + # NOTE: spending the DestCallPP UTXOs doesn't require signatures + # but instead the outpoint hash of the UTXO. This is queried + # from the runtime storage as the smart contract has not transferred + # these funds, the outpoint hash is **not** found from the storage + # and this TX is rejected as invalid + msg_data = contractInstance.generate_message_data("fund", {}) + value -= 555 + + self.log.info("here") + self.log.error(tx) + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 555, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 1338) + + utxos = [x for x in client.utxos_for(acc_id[2:])] + assert_equal(len(utxos), 1) + assert_equal(utxos[0][1].json()["value"], 555) + + invalid_tx = utxo.Transaction( client, - inputs=[ - utxo.Input(tx0.outpoint(0)), + inputs = [ + utxo.Input(utxos[0][0]), ], outputs=[ utxo.Output( - value=49, + value=555, destination=utxo.DestPubkey(alice.public_key), - data=None - ), - utxo.Output( - value=1, - destination=utxo.DestCallPP( - dest_account=acc_id, - fund=False, - input_data=bytes.fromhex(msg_data.to_hex()[2:]), - ), - data=None + data=None, ), ] - ).sign(alice, [tx0.outputs[0]], [0]) - (ext_hash, blk_hash,_) = client.submit(alice, tx1) - - result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, True) + ) + # size of the outpoint (32 bytes, 0x10) + the outpoint itself + # the outpoint in the witness field is valid but because the + # smart contract has not spent the funds, the TX is rejected + tx.inputs[0].witness = bytearray.fromhex("10" + str(utxos[0][0])[2:]) + assert_equal(client.submit(alice, invalid_tx), None) if __name__ == '__main__': ExampleTest().main() diff --git a/test/functional/feature_staking_extra.py b/test/functional/feature_staking_extra.py index ad45f0d..5850366 100755 --- a/test/functional/feature_staking_extra.py +++ b/test/functional/feature_staking_extra.py @@ -91,13 +91,15 @@ def run_test(self): ) ] ).sign(alice_stash, [utxos[0][1]]) - (_,_,events) = client.submit(alice_stash, tx1) + (_, block_hash, _events) = client.submit(alice_stash, tx1) - assert_equal(events[0].value['module_id'],'Staking') - assert_equal(events[0].value['event_id'], 'Bonded') + events = client.substrate.get_events(block_hash = block_hash) - assert_equal(events[1].value['module_id'],'Utxo') - assert_equal(events[1].value['event_id'], 'TransactionSuccess') + assert_equal(events[1].value['module_id'],'Staking') + assert_equal(events[1].value['event_id'], 'Bonded') + + assert_equal(events[2].value['module_id'],'Utxo') + assert_equal(events[2].value['event_id'], 'TransactionSuccess') # Get Alice stash new_count = list(client.get_staking_count(alice_stash))[0][1] diff --git a/test/functional/feature_staking_extra_not_validator.py b/test/functional/feature_staking_extra_not_validator.py index 7aa0b9c..8907822 100755 --- a/test/functional/feature_staking_extra_not_validator.py +++ b/test/functional/feature_staking_extra_not_validator.py @@ -14,6 +14,7 @@ from test_framework.test_framework import MintlayerTestFramework from test_framework.util import ( assert_equal, + assert_raises_substrate_exception, connect_nodes, wait_until, ) @@ -90,7 +91,7 @@ def run_test(self): ), ] ).sign(charlie_stash, [utxos[0][1]]) - client.submit(charlie_stash, tx1) + assert_raises_substrate_exception(client.submit, charlie_stash, tx1) new_count = list(client.staking_count()) # there should only be 2 count of staking, which are Alice and Bob diff --git a/test/functional/feature_staking_extra_wrong_controller.py b/test/functional/feature_staking_extra_wrong_controller.py index b46d52e..9cdbdcb 100755 --- a/test/functional/feature_staking_extra_wrong_controller.py +++ b/test/functional/feature_staking_extra_wrong_controller.py @@ -13,6 +13,7 @@ from test_framework.test_framework import MintlayerTestFramework from test_framework.util import ( assert_equal, + assert_raises_substrate_exception, connect_nodes, wait_until, ) @@ -86,7 +87,7 @@ def run_test(self): ] ).sign(alice_stash, [utxos[0][1]]) - client.submit(alice_stash, tx1) + assert_raises_substrate_exception(client.submit, alice_stash, tx1) # Get Bob's stash new_count = list(client.get_staking_count(alice_stash))[0][1] diff --git a/test/functional/feature_staking_less_than_minimum.py b/test/functional/feature_staking_less_than_minimum.py index e23d78f..c928c35 100755 --- a/test/functional/feature_staking_less_than_minimum.py +++ b/test/functional/feature_staking_less_than_minimum.py @@ -13,6 +13,7 @@ from test_framework.test_framework import MintlayerTestFramework from test_framework.util import ( assert_equal, + assert_raises_substrate_exception, connect_nodes, wait_until, ) @@ -80,6 +81,11 @@ def run_test(self): destination=utxo.DestPubkey(charlie_stash.public_key), data=None ), + utxo.Output( + value=39999949950 * COIN, + destination=utxo.DestPubkey(alice.public_key), + data=None + ), ] ).sign(alice, [utxos[0][1]]) client.submit(alice, tx1) @@ -101,8 +107,8 @@ def run_test(self): data=None ), ] - ).sign(charlie_stash, tx1.outputs) - client.submit(charlie_stash, tx2) + ).sign(charlie_stash, [tx1.outputs[0]]) + assert_raises_substrate_exception(client.submit, charlie_stash, tx2) # there should only be 2 still, because Charlie failed on the staking. assert_equal(len(list(client.staking_count())), 2 ) diff --git a/test/functional/feature_staking_unlock_and_withdraw.py b/test/functional/feature_staking_unlock_and_withdraw.py index c467002..7edff73 100755 --- a/test/functional/feature_staking_unlock_and_withdraw.py +++ b/test/functional/feature_staking_unlock_and_withdraw.py @@ -65,6 +65,7 @@ def run_test(self): assert_equal(len(ledger[0][1]['unlocking']),0) alice_stash = Keypair.create_from_uri('//Alice//stash') + alice = Keypair.create_from_uri('//Alice') # fetch the genesis utxo from storage utxos = list(client.utxos_for(alice_stash)) @@ -87,9 +88,17 @@ def run_test(self): assert_equal(events[2].value['event_id'], 'StakeUnlocked') ledger = list(client.get_staking_ledger()) + assert_equal(len(ledger),2) - time.sleep(500) + time_elapsed = 0 + max_wait = 1200 + time_step = 1 + while client.current_era() < client.withdrawal_era(alice): + if time_elapsed > max_wait: + raise Exception('Timeout: waited too long for withdrawal_era') + time.sleep(time_step) + time_elapsed +=time_step (_, _, w_events) = client.withdraw_stake(alice_stash) diff --git a/test/functional/test_framework/mintlayer/_staking.py b/test/functional/test_framework/mintlayer/_staking.py deleted file mode 100644 index ef74968..0000000 --- a/test/functional/test_framework/mintlayer/_staking.py +++ /dev/null @@ -1,13 +0,0 @@ -import substrateinterface - - -class Staking(object): - - """ Query the node for the staking ledger """ - def get_staking_ledger(self): - query = self.substrate.query_map( - module='Staking', - storage_function='Ledger' - ) - - return ((h, o.value) for (h, o) in query) diff --git a/test/functional/test_framework/mintlayer/staking.py b/test/functional/test_framework/mintlayer/staking.py new file mode 100644 index 0000000..cdfec7a --- /dev/null +++ b/test/functional/test_framework/mintlayer/staking.py @@ -0,0 +1,45 @@ +import substrateinterface + + +class Staking(object): + def __init__(self, substrate): + self.substrate = substrate + + """ Query the node for the staking ledger """ + def get_staking_ledger(self): + query = self.substrate.query_map( + module='Staking', + storage_function='Ledger' + ) + + return ((h, o.value) for (h, o) in query) + + """ accesses current era """ + def current_era(self): + query = self.substrate.query( + module='Staking', + storage_function='CurrentEra' + ) + # this query returns an object of type scalecodec.types.U32 + # so we convert it to an integer + # TODO find a more elegant way to do conversion + return int("{}".format(query)) + + """ gets the staking ledger of the given key """ + def get_ledger(self, keypair): + query = self.substrate.query_map( + module='Staking', + storage_function='Ledger' + ) + + matching = lambda e: e[0].value == keypair.ss58_address + + return filter(matching, ((h, o.value) for (h, o) in query)) + + """ returns what era for the user to be able to withdraw funds """ + def withdrawal_era(self, keypair): + ledger = list(self.get_ledger(keypair)) + if ledger: + return ledger[0][1]['unlocking'][0]['era'] + else: + print("no funds to withdraw") diff --git a/test/functional/test_framework/mintlayer/utxo.py b/test/functional/test_framework/mintlayer/utxo.py index a8bf3a5..cea7e00 100644 --- a/test/functional/test_framework/mintlayer/utxo.py +++ b/test/functional/test_framework/mintlayer/utxo.py @@ -3,9 +3,11 @@ import substrateinterface from substrateinterface import SubstrateInterface, Keypair from substrateinterface.exceptions import SubstrateRequestException +from substrateinterface.utils.ss58 import ss58_decode import scalecodec import os import logging +from staking import Staking """ Client. A thin wrapper over SubstrateInterface """ class Client(): @@ -22,6 +24,8 @@ def __init__(self, url="ws://127.0.0.1", port=9944): type_registry=custom_type_registry ) + self.staking = Staking(self.substrate) + """ SCALE-encode given object in JSON format """ def encode_obj(self, ty, obj): return self.substrate.encode_scale(ty, obj) @@ -47,7 +51,10 @@ def utxos(self, storage_name): """ Get UTXOs for given key """ def utxos_for(self, keypair): - matching = lambda e: e[1].destination.get_pubkey() == keypair.public_key + if type(keypair) == str: + matching = lambda e: e[1].destination.get_pubkey() == keypair + else: + matching = lambda e: e[1].destination.get_pubkey() == keypair.public_key return filter(matching, self.utxos('UtxoStore')) """ Get UTXOs for given key """ @@ -72,47 +79,42 @@ def get_staking_count(self, stash_keypair): return filter(matching , staking_count) - # TODO: move to a separate file """ accesses pallet-staking to retrieve the ledger """ def get_staking_ledger(self): - query = self.substrate.query_map( - module='Staking', - storage_function='Ledger' - ) - - return ((h, o.value) for (h, o) in query) + return self.staking.get_staking_ledger() - # TODO: move to a separate file """ accesses current era """ def current_era(self): - query = self.substrate.query( - module='Staking', - storage_function='CurrentEra' - ) - return query + return self.staking.current_era() - # TODO: move to a separate file """ gets the staking ledger of the given key """ def get_ledger(self, keypair): - query = self.substrate.query_map( - module='Staking', - storage_function='Ledger' - ) - - matching = lambda e: e[0].value == keypair.ss58_address - - return filter(matching, ((h, o.value) for (h, o) in query)) + return self.staking.get_ledger() - # TODO: move to a separate file """ returns what era for the user to be able to withdraw funds """ def withdrawal_era(self, keypair): - ledger = list(self.get_ledger(keypair)) - if ledger: - return ledger[0][1]['unlocking'][0]['era'] - else: - print("no funds to withdraw") - + return self.staking.withdrawal_era(keypair) + + def submit_extrinsic(self, extrinsic): + extrinsic.extrinsic_hash = extrinsic.generate_hash() + def result_handler(message, update_nr, sub_id): + print('Msg:', message) + if 'params' in message and type(message['params']['result']) is dict: + res = message['params']['result'] + if 'inBlock' in res: + return substrateinterface.ExtrinsicReceipt( + substrate = self.substrate, + block_hash = res['inBlock'], + extrinsic_hash = '0x{}'.format(extrinsic.extrinsic_hash) + ) + + result = self.substrate.rpc_request( + "author_submitAndWatchExtrinsic", + [str(extrinsic.data)], + result_handler=result_handler + ) + return result """ Submit a transaction onto the blockchain """ def submit(self, keypair, tx): @@ -121,15 +123,16 @@ def submit(self, keypair, tx): call_function = 'spend', call_params = { 'tx': tx.json() }, ) - extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=keypair) - self.log.debug("extrinsic submitted...") + extrinsic = self.substrate.create_unsigned_extrinsic(call=call) try: - receipt = self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) + self.log.debug("Submitting extrinsic") + receipt = self.submit_extrinsic(extrinsic) self.log.debug("Extrinsic '{}' sent and included in block '{}'".format(receipt.extrinsic_hash, receipt.block_hash)) - return (receipt.extrinsic_hash, receipt.block_hash, receipt.triggered_events) + return (receipt.extrinsic_hash, receipt.block_hash, None) except SubstrateRequestException as e: self.log.debug("Failed to send: {}".format(e)) + raise e """ Submit a transaction onto the blockchain: unlock """ def unlock_request_for_withdrawal(self, keypair): @@ -235,11 +238,14 @@ def __init__(self, dest_account, fund, input_data): @staticmethod def load(obj): - return DestCallPP(obj['dest_account'], obj['fund'], obj['input_data']) + return DestCallPP(ss58_decode(obj['dest_account']), obj['fund'], obj['input_data']) def json(self): return { 'CallPP': { 'dest_account': self.acct, 'fund': self.fund, 'input_data': self.data } } + def get_pubkey(self): + return str(self.acct) + class DestLockForStaking(Destination): def __init__(self, stash_account, controller_account, session_key): self.stash = stash_account @@ -271,7 +277,6 @@ def json(self): def get_ss58_address(self): return self.stash - class Output(): def __init__(self, value, destination, data): self.value = value diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index a457e96..e737f9a 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -52,8 +52,8 @@ def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mo if timewait: self.rpc_timeout = timewait else: - # Wait for up to 600 seconds for the RPC server to respond - self.rpc_timeout = 600 + # Wait for up to 120 seconds for the RPC server to respond + self.rpc_timeout = 120 if binary is None: self.binary = os.getenv("NODEEXE", "mintlayer-core") else: @@ -65,6 +65,7 @@ def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mo port_rpc = rpc_port(i) port_p2p = p2p_port(i) self.args = [self.binary, "--dev", + "--database", "paritydb-experimental", "--base-path", self.datadir, "--log", "trace", "--name", "testnode%d" % i, diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 840cdbc..2d5ce34 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -5,6 +5,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Helpful routines for regression testing.""" +from substrateinterface.exceptions import SubstrateRequestException from base64 import b64encode from binascii import hexlify, unhexlify from decimal import Decimal, ROUND_DOWN @@ -68,6 +69,14 @@ def assert_raises_message(exc, message, fun, *args, **kwds): else: raise AssertionError("No exception raised") +def assert_raises_substrate_exception(fun, *args, **kwds): + try: + fun(*args, **kwds) + except SubstrateRequestException as e: + return True + else: + raise AssertionError("No SubstrateRequestException raised") + def assert_raises_process_error(returncode, output, fun, *args, **kwds): """Execute a process and asserts the process return code and output. diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 0ce8171..0d61947 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -56,6 +56,7 @@ BASE_SCRIPTS= [ 'example_test.py', 'feature_alice_bob_test.py', + 'feature_alice_rain_test.py', 'feature_staking_extra.py', 'feature_staking_extra_not_validator.py', 'feature_staking_extra_wrong_controller.py', @@ -64,9 +65,9 @@ 'feature_staking_diff_addresses.py', 'feature_staking_unlock_not_validator.py', 'feature_staking_withdraw_no_unlock.py', - 'feature_staking_withdraw_not_validator.py' -# 'feature_staking_unlock_and_withdraw.py' ## should be ran on 20 secs - # 'feature_smart_contract_test.py', + 'feature_staking_withdraw_not_validator.py', + 'feature_staking_unlock_and_withdraw.py' ## should be ran on 20 secs + # 'feature_smart_contract_test.py', ## fix this when we have the time. # Don't append tests at the end to avoid merge conflicts # Put them in a random line within the section that fits their approximate run-time ] @@ -326,6 +327,13 @@ def get_next(self): log_stderr)) if not self.jobs: raise IndexError('pop from empty list') + + # print test names that are running + running_test_names = [test_data[0] for test_data in self.jobs] + running_test_names_list = ["{}{}{}".format(BOLD[1], nm, BOLD[0]) for nm in running_test_names] + running_test_names_list = ", ".join(running_test_names_list) + logging.debug("Test%s currently running: %s", "s" if len(running_test_names) > 1 else "", running_test_names_list) + while True: # Return first proc that finishes time.sleep(.5) @@ -336,6 +344,7 @@ def get_next(self): # providing useful output. proc.send_signal(signal.SIGINT) if proc.poll() is not None: + # check if the test has finished log_out.seek(0), log_err.seek(0) [stdout, stderr] = [l.read().decode('utf-8') for l in (log_out, log_err)] log_out.close(), log_err.close() @@ -403,7 +412,7 @@ def check_script_list(src_dir): python_files = set([t for t in os.listdir(script_dir) if t[-3:] == ".py"]) missed_tests = list(python_files - set(map(lambda x: x.split()[0], ALL_SCRIPTS + NON_SCRIPTS))) if len(missed_tests) != 0: - print("%sWARNING!%s The following scripts are not being run: %s. Check the test lists in test_runner.py." % (BOLD[1], BOLD[0], str(missed_tests))) + print("%sWARNING!%s The following scripts are not being run: %s. Check the test lists in test_runner.py." % (BOLD[1], BOLD[0], str(missed_tests)), flush=True) if os.getenv('TRAVIS') == 'true': # On travis this warning is an error to prevent merging incomplete commits into master sys.exit(1) diff --git a/traits/utxo-api/src/lib.rs b/traits/utxo-api/src/lib.rs index e345ae9..b2de034 100644 --- a/traits/utxo-api/src/lib.rs +++ b/traits/utxo-api/src/lib.rs @@ -39,14 +39,14 @@ pub trait UtxoApi { fn withdraw_stake(stash_account_caller: &Self::AccountId) -> DispatchResultWithPostInfo; - fn send_conscrit_p2pk( + fn submit_c2pk_tx( caller: &Self::AccountId, destination: &Self::AccountId, value: u128, outpoints: &Vec, ) -> Result<(), DispatchError>; - fn send_conscrit_c2c( + fn submit_c2c_tx( caller: &Self::AccountId, destination: &Self::AccountId, value: u128,