├── .DS_Store ├── .github └── workflows │ ├── build-push-binary.yml │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── aleo ├── credits.aleo ├── hello.aleo ├── records.aleo ├── roulette.aleo └── token.aleo ├── doc ├── abci.png └── architecture.png ├── docker-compose.yml ├── src ├── blockchain │ ├── application.rs │ ├── genesis.rs │ ├── main.rs │ ├── program_store.rs │ ├── record_store.rs │ └── validator_set.rs ├── client │ ├── account.rs │ ├── commands.rs │ ├── main.rs │ └── tendermint.rs └── lib │ ├── mod.rs │ ├── program_file.rs │ ├── query.rs │ ├── transaction.rs │ ├── validator.rs │ └── vm │ ├── lambdavm │ └── mod.rs │ ├── mod.rs │ └── snarkvm │ ├── mod.rs │ └── stack.rs └── tests └── client.rs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/aleo_lambda_blockchain/HEAD/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/build-push-binary.yml: -------------------------------------------------------------------------------- 1 | name: Build and push aleo_abci binary 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-binary: 8 | name: Build and push aleo_abci binary 9 | 10 | runs-on: [self-hosted, nomad] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: nightly 16 | profile: minimal 17 | 18 | - name: Build aleo_abci 19 | run: cargo build --release 20 | 21 | - name: Compress and upload binary to S3 22 | run: | 23 | cd target/release/ 24 | COMPRESSED_FILE=aleo_abci-$(TZ=America/Buenos_Aires date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD).tgz 25 | tar -cz aleo_abci -f ${COMPRESSED_FILE} 26 | aws s3 cp ${COMPRESSED_FILE} s3://entropy-releases/aleo_abci/${COMPRESSED_FILE} 27 | env: 28 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 29 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 30 | AWS_DEFAULT_REGION: us-west-2 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: mOStropy build checks 2 | on: push 3 | jobs: 4 | integration-tests: 5 | runs-on: [self-hosted, nomad] 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions-rs/toolchain@v1 9 | with: 10 | toolchain: nightly 11 | profile: minimal 12 | # Rust cache temporarily disabled until issue (https://github.com/actions/cache/issues/810) is fixed 13 | # - uses: Swatinem/rust-cache@v2 14 | # with: 15 | # key: "mostropy" 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version: '1.16.1' 19 | - run: rustup update; ulimit -n 4864; cargo clean 20 | - run: cargo build --release --features lambdavm_backend 21 | - run: make reset 22 | - run: | 23 | VM_FEATURE=lambdavm_backend make node > /dev/null & 24 | VM_FEATURE=lambdavm_backend make abci /dev/null & 25 | while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:26657)" != "200" ]]; do sleep 2; done 26 | cargo test --release --features lambdavm_backend -- --nocapture --test-threads=1 27 | - run: pkill make; 28 | make reset 29 | - run: rm -rf ~/.aleo/cache 30 | - run: cargo build --release --features snarkvm_backend 31 | - run: | 32 | VM_FEATURE=snarkvm_backend make node > /dev/null & 33 | VM_FEATURE=snarkvm_backend make abci > /dev/null & 34 | while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:26657)" != "200" ]]; do sleep 2; done 35 | cargo test --release --features snarkvm_backend -- --nocapture --test-threads=4 36 | clippy: 37 | runs-on: [self-hosted, nomad] 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: nightly 43 | components: clippy 44 | # Rust cache temporarily disabled until issue (https://github.com/actions/cache/issues/810) is fixed 45 | # - uses: Swatinem/rust-cache@v2 46 | # with: 47 | # key: "mostropy" 48 | - run: apt install -y clang libclang1 49 | - run: cargo +nightly clippy --all-targets --features snarkvm_backend -- -D warnings 50 | - run: cargo +nightly clippy --all-targets --features lambdavm_backend -- -D warnings 51 | format: 52 | runs-on: [self-hosted, nomad] 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: nightly 58 | components: rustfmt 59 | - run: cargo fmt -- --check 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | target/ 5 | 6 | .vscode/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | hello/build 15 | bin/ 16 | .db_test 17 | *.db 18 | abci.* 19 | testnet/ 20 | *.avm 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aleo_client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | default-run = "client" 6 | 7 | [profile.test] 8 | # use optimizations even in testing, otherwise snarkvm makes it too slow 9 | opt-level = 3 10 | debug-assertions = true 11 | 12 | [lib] 13 | path = "src/lib/mod.rs" 14 | doctest = false 15 | name = "lib" 16 | 17 | [[bin]] 18 | path = "src/client/main.rs" 19 | doctest = false 20 | name = "client" 21 | 22 | [[bin]] 23 | name = "genesis" 24 | path = "src/blockchain/genesis.rs" 25 | test = false 26 | 27 | [[bin]] 28 | name = "aleo_abci" 29 | path = "src/blockchain/main.rs" 30 | 31 | [dependencies] 32 | anyhow = "1.0.66" 33 | bincode = "1.3.3" 34 | bytes = { version = "1.0", default-features = false } 35 | clap = { version = "4.0.5", features = ["derive", "env"] } 36 | flex-error = { version = "0.4.4", default-features = false } 37 | log = "0.4.14" 38 | prost = { version = "0.11", default-features = false } 39 | rand = "0.8.5" 40 | serde = "1.0" 41 | serde_json = { version = "1.0", features = ["raw_value"] } 42 | simple_logger = "2.3.0" 43 | tendermint = "0.25.0" 44 | tendermint-abci = "0.25.0" 45 | tendermint-proto = { version = "0.25.0", default-features = false } 46 | tendermint-rpc = { version = "0.25.0", features = ["http-client"] } 47 | tokio = { version = "1.15.0", features = ["full"] } 48 | tracing = "0.1" 49 | tracing-subscriber = {version = "0.3", features = ["env-filter", "fmt", "std"]} 50 | uuid = { version = "1.2.1", features = ["v4"] } 51 | parking_lot = { version = "0.12.1" } 52 | dirs = "4.0.0" 53 | rocksdb = "0.19.0" 54 | hex = "0.4.3" 55 | rand_chacha = "0.3.1" 56 | indexmap = "1.9.2" 57 | itertools = "0.10.5" 58 | sha2 = "0.10.6" 59 | base64 = "0.20.0" 60 | sha3 = "0.10.6" 61 | cfg-if = "1" 62 | 63 | [dependencies.lambdavm] 64 | git = "https://github.com/lambdaclass/aleo_lambda_vm.git" 65 | branch = "main" 66 | optional = true 67 | 68 | [dependencies.snarkvm] 69 | git = "https://github.com/lambdaclass/snarkVM.git" 70 | branch = "entropy_fork" 71 | features = ["circuit", "console", "parallel", "parameters"] 72 | optional = true 73 | 74 | [profile.bench] 75 | debug = true 76 | 77 | 78 | [dev-dependencies] 79 | assert_fs = "1.0.9" 80 | snarkvm = { git = "https://github.com/Lambdaclass/snarkVM.git", branch = "entropy_fork" } # used for creating records in tests 81 | assert_cmd = "2.0.6" 82 | retry = "2.0.0" 83 | serial_test = "1.0.0" 84 | ctor = "0.1.23" 85 | 86 | [features] 87 | #default = ["lambdavm_backend"] 88 | #default = ["snarkvm_backend"] 89 | snarkvm_backend = ["snarkvm"] 90 | lambdavm_backend = ["lambdavm"] 91 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.65 AS builder 2 | COPY . . 3 | RUN apt-get update && apt install -y clang libclang1 4 | RUN cargo build --release 5 | 6 | FROM debian:bullseye-slim 7 | COPY --from=builder ./target/release/aleo_abci ./target/release/aleo_abci 8 | RUN apt-get update && apt install -y libcurl4 9 | CMD ["/target/release/aleo_abci"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tendermint reset abci build cli genesis tendermint_config testnet tendermint_install 2 | 3 | OS := $(shell uname | tr '[:upper:]' '[:lower:]') 4 | 5 | ifeq ($(shell uname -p), arm) 6 | ARCH=arm64 7 | else 8 | ARCH=amd64 9 | endif 10 | 11 | TENDERMINT_HOME=~/.tendermint/ 12 | VM_FEATURE ?= lambdavm_backend 13 | 14 | # Build the client program and put it in bin/aleo 15 | cli: 16 | mkdir -p bin && cargo build --release --features $(VM_FEATURE) && cp target/release/client bin/aleo 17 | 18 | # Installs tendermint for current OS and puts it in bin/ 19 | bin/tendermint: 20 | make tendermint_install 21 | mv tendermint-install/tendermint bin/ && rm -rf tendermint-install 22 | 23 | # Internal phony target to install tendermint for an arbitrary OS 24 | tendermint_install: 25 | mkdir -p tendermint-install bin && cd tendermint-install &&\ 26 | wget https://github.com/tendermint/tendermint/releases/download/v0.34.22/tendermint_0.34.22_$(OS)_$(ARCH).tar.gz &&\ 27 | tar -xzvf tendermint_0.34.22_$(OS)_$(ARCH).tar.gz 28 | 29 | # initialize tendermint and write a genesis file for a local testnet. 30 | genesis: bin/tendermint cli 31 | test -f $(TENDERMINT_HOME)/account.json || ALEO_HOME=$(TENDERMINT_HOME) bin/aleo account new 32 | bin/tendermint init 33 | cargo run --bin genesis --release --features $(VM_FEATURE) -- $(TENDERMINT_HOME) 34 | 35 | # Run a tendermint node, installing it if necessary 36 | node: genesis tendermint_config 37 | bin/tendermint node --consensus.create_empty_blocks_interval="8s" 38 | 39 | # Override a tendermint node's default configuration. NOTE: we should do something more declarative if we need to update more settings. 40 | tendermint_config: 41 | sed -i.bak 's/max_body_bytes = 1000000/max_body_bytes = 12000000/g' $(TENDERMINT_HOME)/config/config.toml 42 | sed -i.bak 's/max_tx_bytes = 1048576/max_tx_bytes = 10485770/g' $(TENDERMINT_HOME)/config/config.toml 43 | sed -i.bak 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://0.0.0.0:26657"#g' $(TENDERMINT_HOME)/config/config.toml 44 | 45 | # Initialize the tendermint configuration for a testnet of the given amount of validators 46 | testnet: VALIDATORS:=4 47 | testnet: ADDRESS:=192.167.10.2 48 | testnet: HOMEDIR:=testnet 49 | testnet: bin/tendermint cli 50 | rm -rf $(HOMEDIR)/ 51 | bin/tendermint testnet --v $(VALIDATORS) --o ./$(HOMEDIR) --starting-ip-address $(ADDRESS) 52 | for node in $(HOMEDIR)/*/ ; do \ 53 | ALEO_HOME=$$node bin/aleo account new ; \ 54 | make tendermint_config TENDERMINT_HOME=$$node ; \ 55 | done 56 | cargo run --bin genesis --release --features $(VM_FEATURE) -- $(HOMEDIR)/* 57 | 58 | # Initialize the tendermint configuration for a localnet of the given amount of validators 59 | localnet: VALIDATORS:=4 60 | localnet: ADDRESS:=127.0.0.1 61 | localnet: HOMEDIR:=localnet 62 | localnet: bin/tendermint cli 63 | rm -rf $(HOMEDIR)/ 64 | bin/tendermint testnet --v $(VALIDATORS) --o ./$(HOMEDIR) --starting-ip-address $(ADDRESS) 65 | for n in $$(seq 0 $$(($(VALIDATORS)-1))) ; do \ 66 | ALEO_HOME=$(HOMEDIR)/node$$n bin/aleo account new ; \ 67 | make localnet_config TENDERMINT_HOME=$(HOMEDIR)/node$$n NODE=$$n VALIDATORS=$(VALIDATORS); \ 68 | mkdir $(HOMEDIR)/node$$n/abci ; \ 69 | done 70 | cargo run --bin genesis --release --features $(VM_FEATURE) -- $(HOMEDIR)/* 71 | .PHONY: localnet 72 | 73 | localnet_config: 74 | sed -i.bak 's/max_body_bytes = 1000000/max_body_bytes = 12000000/g' $(TENDERMINT_HOME)/config/config.toml 75 | sed -i.bak 's/max_tx_bytes = 1048576/max_tx_bytes = 10485770/g' $(TENDERMINT_HOME)/config/config.toml 76 | for n in $$(seq 0 $$(($(VALIDATORS)-1))) ; do \ 77 | eval "sed -i.bak 's/127.0.0.$$(($${n}+1)):26656/127.0.0.1:26$${n}56/g' $(TENDERMINT_HOME)/config/config.toml" ;\ 78 | done 79 | sed -i.bak 's#laddr = "tcp://0.0.0.0:26656"#laddr = "tcp://0.0.0.0:26$(NODE)56"#g' $(TENDERMINT_HOME)/config/config.toml 80 | sed -i.bak 's#laddr = "tcp://127.0.0.1:26657"#laddr = "tcp://0.0.0.0:26$(NODE)57"#g' $(TENDERMINT_HOME)/config/config.toml 81 | sed -i.bak 's#proxy_app = "tcp://127.0.0.1:26658"#proxy_app = "tcp://127.0.0.1:26$(NODE)58"#g' $(TENDERMINT_HOME)/config/config.toml 82 | .PHONY: localnet_config 83 | 84 | # run both the abci application and the tendermint node 85 | # assumes config for each node has been done previously 86 | localnet_start: NODE:=0 87 | localnet_start: HOMEDIR:=localnet 88 | localnet_start: 89 | bin/tendermint node --home ./$(HOMEDIR)/node$(NODE) --consensus.create_empty_blocks_interval="90s" & 90 | cd ./$(HOMEDIR)/node$(NODE)/abci; cargo run --release --bin aleo_abci --features $(VM_FEATURE) -- --port 26$(NODE)58 91 | .PHONY: localnet_start 92 | 93 | # remove the blockchain data 94 | reset: bin/tendermint 95 | rm -rf ~/.tendermint 96 | rm -rf *.db/ 97 | rm -f abci.* 98 | bin/tendermint unsafe_reset_all 99 | 100 | # run the snarkvm tendermint application 101 | abci: 102 | cargo run --release --bin aleo_abci --features $(VM_FEATURE) 103 | 104 | # run tests on release mode (default VM backend) to ensure there is no extra printing to stdout 105 | test: 106 | RUST_BACKTRACE=full cargo test --release --features $(VM_FEATURE) -- --nocapture --test-threads=4 107 | 108 | 109 | dockernet-build-abci: 110 | docker build -t aleo_abci . 111 | .PHONY: dockernet-build-abci 112 | 113 | # Run a 4-node testnet locally 114 | dockernet-start: HOMEDIR:=dockernet 115 | dockernet-start: dockernet-stop 116 | make testnet HOMEDIR=$(HOMEDIR) 117 | make tendermint_install OS=linux ARCH=amd64 118 | mv tendermint-install/tendermint $(HOMEDIR)/ && rm -rf tendermint-install 119 | docker-compose up 120 | .PHONY: dockernet-start 121 | 122 | # Stop testnet 123 | dockernet-stop: 124 | docker-compose down 125 | .PHONY: dockernet-stop 126 | 127 | # Reset the testnet data 128 | dockernet-reset: HOMEDIR:=dockernet 129 | dockernet-reset: 130 | rm -Rf $(HOMEDIR) 131 | .PHONY: dockernet-reset 132 | -------------------------------------------------------------------------------- /aleo/credits.aleo: -------------------------------------------------------------------------------- 1 | program credits.aleo; 2 | 3 | record credits: 4 | owner as address.private; 5 | gates as u64.private; 6 | 7 | // validator_{n} is the tendermint public key (32 bytes) 8 | // split into 4 u64s for compatibility with VM backends 9 | // 0 is the bigger section, and 3 the lowest 10 | record staked_credits: 11 | owner as address.private; 12 | gates as u64.private; 13 | validator_0 as u64.public; 14 | validator_1 as u64.public; 15 | validator_2 as u64.public; 16 | validator_3 as u64.public; 17 | 18 | function transfer: 19 | input r0 as credits.record; 20 | input r1 as address.private; 21 | input r2 as u64.private; 22 | sub r0.gates r2 into r3; 23 | cast r1 r2 into r4 as credits.record; 24 | cast r0.owner r3 into r5 as credits.record; 25 | output r4 as credits.record; 26 | output r5 as credits.record; 27 | 28 | function combine: 29 | input r0 as credits.record; 30 | input r1 as credits.record; 31 | add r0.gates r1.gates into r2; 32 | cast r0.owner r2 into r3 as credits.record; 33 | output r3 as credits.record; 34 | 35 | function split: 36 | input r0 as credits.record; 37 | input r1 as u64.private; 38 | sub r0.gates r1 into r2; 39 | cast r0.owner r1 into r3 as credits.record; 40 | cast r0.owner r2 into r4 as credits.record; 41 | output r3 as credits.record; 42 | output r4 as credits.record; 43 | 44 | function fee: 45 | input r0 as credits.record; 46 | input r1 as u64.private; 47 | sub r0.gates r1 into r2; 48 | cast r0.owner r2 into r3 as credits.record; 49 | output r3 as credits.record; 50 | 51 | function stake: 52 | input r0 as credits.record; 53 | input r1 as u64.private; 54 | input r2 as u64.public; 55 | input r3 as u64.public; 56 | input r4 as u64.public; 57 | input r5 as u64.public; 58 | sub r0.gates r1 into r6; 59 | cast r0.owner r6 into r7 as credits.record; 60 | cast r0.owner r1 r2 r3 r4 r5 into r8 as staked_credits.record; 61 | add 0u64 r1 into r9; 62 | output r7 as credits.record; 63 | output r8 as staked_credits.record; 64 | output r9 as u64.public; 65 | output r7.owner as address.public; 66 | output r8.validator_0 as u64.public; 67 | output r8.validator_1 as u64.public; 68 | output r8.validator_2 as u64.public; 69 | output r8.validator_3 as u64.public; 70 | 71 | function unstake: 72 | input r0 as staked_credits.record; 73 | input r1 as u64.private; 74 | sub r0.gates r1 into r2; 75 | cast r0.owner r2 r0.validator_0 r0.validator_1 r0.validator_2 r0.validator_3 into r3 as staked_credits.record; 76 | cast r0.owner r1 into r4 as credits.record; 77 | add 0u64 r1 into r5; 78 | output r4 as credits.record; 79 | output r3 as staked_credits.record; 80 | output r5 as u64.public; 81 | output r3.owner as address.public; 82 | output r3.validator_0 as u64.public; 83 | output r3.validator_1 as u64.public; 84 | output r3.validator_2 as u64.public; 85 | output r3.validator_3 as u64.public; 86 | -------------------------------------------------------------------------------- /aleo/hello.aleo: -------------------------------------------------------------------------------- 1 | // The 'hello.aleo' program. 2 | program hello.aleo; 3 | 4 | function hello: 5 | input r0 as u32.public; 6 | input r1 as u32.private; 7 | add r0 r1 into r2; 8 | output r2 as u32.public; -------------------------------------------------------------------------------- /aleo/records.aleo: -------------------------------------------------------------------------------- 1 | program records.aleo; 2 | 3 | record token: 4 | owner as address.private; 5 | gates as u64.private; 6 | amount as u64.public; 7 | 8 | function mint: 9 | input r0 as u64.public; 10 | input r1 as address.public; 11 | cast r1 0u64 r0 into r2 as token.record; 12 | output r2 as token.record; 13 | 14 | function consume: 15 | input r0 as token.record; 16 | add r0.amount 0u64 into r1; 17 | output r1 as u64.public; 18 | 19 | function consume_b: 20 | input r0 as token.record; 21 | add r0.amount 0u64 into r1; 22 | output r1 as u64.public; 23 | 24 | function consume_two: 25 | input r0 as token.record; 26 | input r1 as token.record; 27 | add r0.amount 0u64 into r2; 28 | add r1.amount 0u64 into r3; 29 | output r2 as u64.public; 30 | 31 | // this function is mainly used for testing that this is not possible 32 | function mint_credits: 33 | input r0 as u64.public; 34 | input r1 as address.public; 35 | cast r1 r0 r0 into r2 as token.record; 36 | output r2 as token.record; 37 | -------------------------------------------------------------------------------- /aleo/roulette.aleo: -------------------------------------------------------------------------------- 1 | // The 'bets.aleo' program. 2 | 3 | program bets.aleo; 4 | 5 | record token: 6 | owner as address.private; 7 | gates as u64.private; 8 | amount as u64.private; 9 | 10 | function psd_hash: 11 | input r0 as u32.public; 12 | hash.psd2 r0 into r1; 13 | output r1 as field.private; 14 | 15 | function mint_casino_token_record: 16 | // casino address 17 | input r0 as address.private; 18 | // casino amount of tokens 19 | input r1 as u64.private; 20 | cast r0 0u64 r1 into r2 as token.record; 21 | output r2 as token.record; 22 | 23 | function make_bet: 24 | // casino token record 25 | input r0 as token.record; 26 | // player address 27 | input r1 as address.private; 28 | // Random roulette spin result 29 | input r2 as u8.private; 30 | // Player bet number 31 | input r3 as u8.private; 32 | // Player bet amount of tokens 33 | input r4 as u64.private; 34 | // Player amount of available tokens 35 | input r5 as u64.private; 36 | 37 | //r6 is true if the player wins 38 | is.eq r3 r2 into r6; 39 | 40 | //Reward 41 | mul r4 35u64 into r7; 42 | 43 | //Casino amount of tokens if it wins 44 | add r0.amount r4 into r8; 45 | //Casino amount of tokens if it loses 46 | sub r0.amount r7 into r9; 47 | 48 | //Player amount of tokens if it wins 49 | add r5 r7 into r10; 50 | //Player amount of tokens if it loses 51 | sub r5 r4 into r11; 52 | 53 | //r6 is true if player wins 54 | //r12 casino money after game 55 | ternary r6 r9 r8 into r12; 56 | //r13 player money after game 57 | ternary r6 r10 r11 into r13; 58 | 59 | // Casino token record after the bet 60 | cast r0.owner r0.gates r12 into r14 as token.record; 61 | // Player token record after the bet 62 | cast r1 0u64 r13 into r15 as token.record; 63 | 64 | //Casino new token record 65 | output r14 as token.record; 66 | //Player new token record 67 | output r15 as token.record; 68 | 69 | function psd_bits_mod: 70 | input r0 as boolean.public; 71 | input r1 as boolean.public; 72 | input r2 as boolean.public; 73 | input r3 as boolean.public; 74 | input r4 as boolean.public; 75 | input r5 as boolean.public; 76 | input r6 as u16.public; 77 | 78 | add 1u16 0u16 into r7; 79 | add 2u16 0u16 into r8; 80 | add 4u16 0u16 into r9; 81 | add 8u16 0u16 into r10; 82 | add 16u16 0u16 into r11; 83 | add 32u16 0u16 into r12; 84 | 85 | ternary r5 r7 0u16 into r13; 86 | ternary r4 r8 0u16 into r14; 87 | ternary r3 r9 0u16 into r15; 88 | ternary r2 r10 0u16 into r16; 89 | ternary r1 r11 0u16 into r17; 90 | ternary r0 r12 0u16 into r18; 91 | 92 | add r13 r14 into r19; 93 | add r15 r19 into r20; 94 | add r16 r20 into r21; 95 | add r17 r21 into r22; 96 | add r18 r22 into r23; 97 | 98 | div r23 37u16 into r24; 99 | mul 37u16 r24 into r25; 100 | sub r23 r25 into r26; 101 | 102 | is.eq r26 r6 into r27; 103 | 104 | output r27 as boolean.public; 105 | -------------------------------------------------------------------------------- /aleo/token.aleo: -------------------------------------------------------------------------------- 1 | program token.aleo; 2 | 3 | record token: 4 | owner as address.private; 5 | gates as u64.private; 6 | amount as u64.private; 7 | 8 | function mint: 9 | // amount to mint 10 | input r0 as u64.private; 11 | // address 12 | input r1 as address.private; 13 | // create new record 14 | cast r1 0u64 r0 into r2 as token.record; 15 | // output the new record 16 | output r2 as token.record; 17 | 18 | function transfer_amount: 19 | // sender token record 20 | input r0 as token.record; 21 | // receiver address 22 | input r1 as address.private; 23 | // amount to transfer 24 | input r2 as u64.private; 25 | // final balance of sender 26 | sub r0.amount r2 into r3; 27 | // final balance of receiver 28 | add 0u64 r2 into r4; 29 | // sender token record after the transfer 30 | cast r0.owner r0.gates r3 into r5 as token.record; 31 | // receiver token record after the transfer 32 | cast r1 0u64 r4 into r6 as token.record; 33 | // sender new token record 34 | output r5 as token.record; 35 | // receiver new token record 36 | output r6 as token.record; 37 | 38 | -------------------------------------------------------------------------------- /doc/abci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/aleo_lambda_blockchain/HEAD/doc/abci.png -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/aleo_lambda_blockchain/HEAD/doc/architecture.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node0: 5 | container_name: node0 6 | image: "tendermint/localnode" 7 | ports: 8 | - "26656-26657:26656-26657" 9 | environment: 10 | - ID=0 11 | - LOG=${LOG:-tendermint.log} 12 | volumes: 13 | - ./testnet:/tendermint:Z 14 | depends_on: ["abci0"] 15 | command: node --proxy_app=tcp://abci0:26658 --log_level=error --consensus.create_empty_blocks_interval="10s" 16 | networks: 17 | localnet: 18 | ipv4_address: 192.167.10.2 19 | 20 | node1: 21 | container_name: node1 22 | image: "tendermint/localnode" 23 | ports: 24 | - "26659-26660:26656-26657" 25 | environment: 26 | - ID=1 27 | - LOG=${LOG:-tendermint.log} 28 | volumes: 29 | - ./testnet:/tendermint:Z 30 | depends_on: ["abci1"] 31 | command: node --proxy_app=tcp://abci1:26658 --log_level=error --consensus.create_empty_blocks_interval="10s" 32 | networks: 33 | localnet: 34 | ipv4_address: 192.167.10.3 35 | 36 | node2: 37 | container_name: node2 38 | image: "tendermint/localnode" 39 | environment: 40 | - ID=2 41 | - LOG=${LOG:-tendermint.log} 42 | ports: 43 | - "26662-26663:26656-26657" 44 | volumes: 45 | - ./testnet:/tendermint:Z 46 | depends_on: ["abci2"] 47 | command: node --proxy_app=tcp://abci2:26658 --log_level=error --consensus.create_empty_blocks_interval="10s" 48 | networks: 49 | localnet: 50 | ipv4_address: 192.167.10.4 51 | 52 | node3: 53 | container_name: node3 54 | image: "tendermint/localnode" 55 | environment: 56 | - ID=3 57 | - LOG=${LOG:-tendermint.log} 58 | ports: 59 | - "26665-26666:26656-26657" 60 | volumes: 61 | - ./testnet:/tendermint:Z 62 | depends_on: ["abci3"] 63 | command: node --proxy_app=tcp://abci3:26658 --log_level=error --consensus.create_empty_blocks_interval="10s" 64 | networks: 65 | localnet: 66 | ipv4_address: 192.167.10.5 67 | 68 | abci0: 69 | container_name: abci0 70 | image: "aleo_abci" 71 | expose: 72 | - "26658" 73 | volumes: 74 | - ./testnet/node0/root:/root:Z 75 | - ~/.aleo/resources:/root/.aleo/resources:Z 76 | - ~/.aleo/cache:/root/.aleo/cache:Z 77 | command: "/target/release/aleo_abci --host 192.167.10.6 --verbose" 78 | networks: 79 | localnet: 80 | ipv4_address: 192.167.10.6 81 | 82 | abci1: 83 | container_name: abci1 84 | image: "aleo_abci" 85 | expose: 86 | - "26658" 87 | volumes: 88 | - ./testnet/node1/root:/root:Z 89 | - ~/.aleo/resources:/root/.aleo/resources:Z 90 | - ~/.aleo/cache:/root/.aleo/cache:Z 91 | command: "/target/release/aleo_abci --host 192.167.10.7" 92 | networks: 93 | localnet: 94 | ipv4_address: 192.167.10.7 95 | 96 | abci2: 97 | container_name: abci2 98 | image: "aleo_abci" 99 | expose: 100 | - "26658" 101 | volumes: 102 | - ./testnet/node2/root:/root:Z 103 | - ~/.aleo/resources:/root/.aleo/resources:Z 104 | - ~/.aleo/cache:/root/.aleo/cache:Z 105 | command: "/target/release/aleo_abci --host 192.167.10.8" 106 | networks: 107 | localnet: 108 | ipv4_address: 192.167.10.8 109 | 110 | abci3: 111 | container_name: abci3 112 | image: "aleo_abci" 113 | expose: 114 | - "26658" 115 | volumes: 116 | - ./testnet/node3/root:/root:Z 117 | - ~/.aleo/resources:/root/.aleo/resources:Z 118 | - ~/.aleo/cache:/root/.aleo/cache:Z 119 | command: "/target/release/aleo_abci --host 192.167.10.9" 120 | networks: 121 | localnet: 122 | ipv4_address: 192.167.10.9 123 | 124 | networks: 125 | localnet: 126 | driver: bridge 127 | ipam: 128 | driver: default 129 | config: 130 | - 131 | subnet: 192.167.10.0/16 132 | -------------------------------------------------------------------------------- /src/blockchain/application.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use crate::program_store::ProgramStore; 5 | use crate::record_store::RecordStore; 6 | use crate::validator_set::ValidatorSet; 7 | use anyhow::{bail, ensure, Result}; 8 | use itertools::Itertools; 9 | use lib::validator::GenesisState; 10 | use lib::{query::AbciQuery, transaction::Transaction, vm}; 11 | use tendermint_abci::Application; 12 | use tendermint_proto::abci; 13 | 14 | use tracing::{debug, error, info}; 15 | 16 | /// An Tendermint ABCI application that works with a SnarkVM backend. 17 | /// This struct implements the ABCI application hooks, forwarding commands through 18 | /// a channel for the parts that require knowledge of the application state and the SnarkVM details. 19 | /// For reference see https://docs.tendermint.com/v0.34/introduction/what-is-tendermint.html#abci-overview 20 | #[derive(Debug, Clone)] 21 | pub struct SnarkVMApp { 22 | records: RecordStore, 23 | programs: ProgramStore, 24 | 25 | // NOTE: Wrapping in mutex here because we need mut access to ValidatorSet and the alternative to setup 26 | // a channel was overkilll for this particular case. Also, at the moment we only ever access these field 27 | // from a single tendermint abci connection (the consensus connection), but using Rc instead of Arc would 28 | // introduce subtle bugs should that ever change. 29 | validators: Arc>, 30 | } 31 | 32 | impl Application for SnarkVMApp { 33 | /// This hook is called once upon genesis. It's used to load a default set of records which 34 | /// make the initial distribution of credits in the system. 35 | fn init_chain(&self, request: abci::RequestInitChain) -> abci::ResponseInitChain { 36 | info!("Loading genesis"); 37 | 38 | // the app_state_bytes come from the app_state field of the tendermint genesis.json generated by genesis.rs 39 | let state: GenesisState = 40 | serde_json::from_slice(&request.app_state_bytes).expect("invalid genesis state"); 41 | 42 | for (commitment, record) in state.records { 43 | debug!("Storing genesis record {}", commitment); 44 | self.records 45 | .add(commitment, record) 46 | .expect("failure adding genesis records"); 47 | } 48 | 49 | self.validators.lock().unwrap().replace(state.validators); 50 | Default::default() 51 | } 52 | 53 | /// This hook provides information about the ABCI application. 54 | fn info(&self, request: abci::RequestInfo) -> abci::ResponseInfo { 55 | debug!( 56 | "Got info request. Tendermint version: {}; Block version: {}; P2P version: {}", 57 | request.version, request.block_version, request.p2p_version 58 | ); 59 | 60 | abci::ResponseInfo { 61 | data: "snarkvm-app".to_string(), 62 | version: "0.1.0".to_string(), 63 | app_version: 1, 64 | last_block_height: HeightFile::read_or_create(), 65 | 66 | // using a fixed hash, see the commit() hook 67 | last_block_app_hash: vec![], 68 | } 69 | } 70 | 71 | /// This hook is to query the application for data at the current or past height. 72 | fn query(&self, request: abci::RequestQuery) -> abci::ResponseQuery { 73 | let query_result = match bincode::deserialize(&request.data) { 74 | Ok(AbciQuery::GetRecords) => { 75 | debug!("Fetching records"); 76 | // TODO: This fetches all the records from the RecordStore to filter here the 77 | // owned ones. With a large database this will involve a lot of data/time 78 | // so we should think of a better way to handle this. (eg. pagination or asynchronous 79 | // querying) 80 | // https://trello.com/c/bP8Nbs7C/170-handle-record-querying-properly-in-recordstore 81 | self.records 82 | .scan(None, None) 83 | .map(|result| bincode::serialize(&result).unwrap()) 84 | } 85 | Ok(AbciQuery::GetSpentSerialNumbers) => { 86 | debug!("Fetching spent records's serial numbers"); 87 | 88 | self.records 89 | .scan_spent() 90 | .map(|result| bincode::serialize(&result).unwrap()) 91 | } 92 | Ok(AbciQuery::GetProgram { program_id }) => { 93 | debug!("Fetching {}", program_id); 94 | self.programs.get(&program_id).map(|result| { 95 | bincode::serialize(&result.map(|(program, _keys)| program)).unwrap() 96 | }) 97 | } 98 | Err(e) => Err(e.into()), 99 | }; 100 | 101 | match query_result { 102 | Ok(value) => abci::ResponseQuery { 103 | value, 104 | ..Default::default() 105 | }, 106 | Err(e) => abci::ResponseQuery { 107 | code: 1, 108 | log: format!("Error running query: {e}"), 109 | info: format!("Error running query: {e}"), 110 | ..Default::default() 111 | }, 112 | } 113 | } 114 | 115 | /// This ABCI hook validates an incoming transaction before inserting it in the 116 | /// mempool and relaying it to other nodes. 117 | fn check_tx(&self, request: abci::RequestCheckTx) -> abci::ResponseCheckTx { 118 | let tx: Transaction = bincode::deserialize(&request.tx).unwrap(); 119 | info!("Check Tx ID: {}", tx.id()); 120 | 121 | let result = self 122 | .check_no_duplicate_records(&tx) 123 | .and_then(|_| self.check_inputs_are_unspent(&tx)) 124 | .and_then(|_| self.validate_transaction(&tx)); 125 | 126 | // by making the priority equal to the fees we give more priority to higher-paying transactions 127 | // NOTE: we haven't thoroughly tested tendermint prioritized mempool, see for background 128 | // https://github.com/tendermint/tendermint/discussions/9772 129 | let priority = tx.fees(); 130 | 131 | if let Err(err) = result { 132 | abci::ResponseCheckTx { 133 | code: 1, 134 | log: format!("Could not verify transaction: {err}"), 135 | info: format!("Could not verify transaction: {err}"), 136 | ..Default::default() 137 | } 138 | } else { 139 | abci::ResponseCheckTx { 140 | priority, 141 | ..Default::default() 142 | } 143 | } 144 | } 145 | 146 | /// This hook is called before the app starts processing transactions on a block. 147 | /// Used to store current proposer and the previous block's voters to assign fees and coinbase 148 | /// credits when the block is committed. 149 | fn begin_block(&self, request: abci::RequestBeginBlock) -> abci::ResponseBeginBlock { 150 | // a call to begin block without header doesn't seem to make sense, verify it can happen 151 | // supporting this case is cumbersome, assuming it won't happen until proven wrong 152 | let header = request 153 | .header 154 | .expect("received block without header, aborting"); 155 | 156 | // store current block proposer and previous block voters in the validator set 157 | // NOTE: because of how tendermint makes information available to this hook, 158 | // the block rewards go to this block's porposer and the **previous** block voters. 159 | // This could be revisited if it's a problem. 160 | let votes = request 161 | .last_commit_info 162 | .map(|last_commit| last_commit.votes) 163 | .unwrap_or_default() 164 | .iter() 165 | .filter_map(|vote_info| { 166 | if !vote_info.signed_last_block { 167 | // don't count validators that didn't participate in previous round 168 | return None; 169 | } 170 | 171 | if let Some(validator) = vote_info.validator.clone() { 172 | if validator.power < 0 { 173 | error!("received negative validator vote"); 174 | None 175 | } else { 176 | Some((validator.address, validator.power as u64)) 177 | } 178 | } else { 179 | // If there's no associated validator data, we can't use this vote 180 | None 181 | } 182 | }) 183 | .collect(); 184 | 185 | self.validators.lock().unwrap().begin_block( 186 | &header.proposer_address, 187 | votes, 188 | header.height as u64, 189 | ); 190 | 191 | Default::default() 192 | } 193 | 194 | /// This ABCI hook validates a transaction and applies it to the application state, 195 | /// for example storing the program verifying keys upon a valid deployment. 196 | /// Here is also where transactions are indexed for querying the blockchain. 197 | fn deliver_tx(&self, request: abci::RequestDeliverTx) -> abci::ResponseDeliverTx { 198 | info!("Deliver Tx"); 199 | 200 | let tx: Transaction = bincode::deserialize(&request.tx).unwrap(); 201 | 202 | // we need to repeat the same validations as deliver_tx and only, because the protocol can't 203 | // guarantee that a bynzantine validator won't propose a block with invalid transactions. 204 | // if validation they pass apply (but not commit) the application state changes. 205 | // Note that we check for duplicate records within the transaction before attempting to spend them 206 | // so we don't end up with a half-applied transaction in the record store. 207 | let result = self 208 | .check_no_duplicate_records(&tx) 209 | .and_then(|_| self.check_inputs_are_unspent(&tx)) 210 | .and_then(|_| self.validate_transaction(&tx)) 211 | .map(|_| self.update_validators(&tx)) 212 | .and_then(|_| self.spend_input_records(&tx)) 213 | .and_then(|_| self.add_output_records(&tx)) 214 | .and_then(|_| self.store_program(&tx)); 215 | 216 | match result { 217 | Ok(_) => { 218 | // prepare this transaction to be queried by app.tx_id 219 | let index_event = abci::Event { 220 | r#type: "app".to_string(), 221 | attributes: vec![abci::EventAttribute { 222 | key: "tx_id".to_string().into_bytes(), 223 | value: tx.id().to_string().into_bytes(), 224 | index: true, 225 | }], 226 | }; 227 | 228 | abci::ResponseDeliverTx { 229 | events: vec![index_event], 230 | ..Default::default() 231 | } 232 | } 233 | Err(e) => abci::ResponseDeliverTx { 234 | code: 1, 235 | log: format!("Error delivering transaction: {e}"), 236 | info: format!("Error delivering transaction: {e}"), 237 | ..Default::default() 238 | }, 239 | } 240 | } 241 | 242 | /// Applies validator set updates based on staking transactions included in the block. 243 | /// For details about validator set update semantics see: 244 | /// https://github.com/tendermint/tendermint/blob/v0.34.x/spec/abci/apps.md#endblock 245 | fn end_block(&self, _request: abci::RequestEndBlock) -> abci::ResponseEndBlock { 246 | let validator_set = self.validators.lock().unwrap(); 247 | let validator_updates = validator_set 248 | .pending_updates() 249 | .iter() 250 | .map(|validator| abci::ValidatorUpdate { 251 | pub_key: Some(validator.pub_key.into()), 252 | power: validator.voting_power as i64, 253 | }) 254 | .collect(); 255 | 256 | abci::ResponseEndBlock { 257 | validator_updates, 258 | ..Default::default() 259 | } 260 | } 261 | 262 | /// This hook commits is called when the block is comitted (after deliver_tx has been called for each transaction). 263 | /// Changes to application should take effect here. Tendermint guarantees that no transaction is processed while this 264 | /// hook is running. 265 | /// The result includes a hash of the application state which will be included in the block header. 266 | /// This hash should be deterministic, different app state hashes will produce blockchain forks. 267 | /// New credits records are created to assign validator rewards. 268 | fn commit(&self) -> abci::ResponseCommit { 269 | // the app hash is intended to capture the state of the application that's not contained directly 270 | // in the blockchain transactions (as tendermint already accounts for that with other hashes). 271 | // we could do something in the RecordStore and ProgramStore to track state changes there and 272 | // calculate a hash based on that, if we expected some aspect of that data not to be completely 273 | // determined by the list of committed transactions (for example if we expected different versions 274 | // of the app with differing logic to coexist). At this stage it seems overkill to add support for that 275 | // scenario so we just to use a fixed hash. See below for more discussion on the use of app hash: 276 | // https://github.com/tendermint/tendermint/issues/1179 277 | // https://github.com/tendermint/tendermint/blob/v0.34.x/spec/abci/apps.md#query-proofs 278 | let app_hash = vec![]; 279 | 280 | // apply pending changes in the record store: mark used records as spent, add inputs as unspent 281 | if let Err(err) = self.records.commit() { 282 | error!("Failure while committing the record store {}", err); 283 | } 284 | 285 | let height = HeightFile::increment(); 286 | 287 | let mut validators = self.validators.lock().unwrap(); 288 | for (commitment, record) in validators.block_rewards() { 289 | if let Err(err) = self.records.add(commitment, record) { 290 | error!("Failed to add reward record to store {}", err); 291 | } 292 | } 293 | validators 294 | .commit() 295 | .unwrap_or_else(|e| error!("failed to save validators: {e}")); 296 | 297 | info!("Committing height {}", height); 298 | abci::ResponseCommit { 299 | data: app_hash, 300 | retain_height: 0, 301 | } 302 | } 303 | } 304 | 305 | impl SnarkVMApp { 306 | /// Constructor. 307 | pub fn new() -> Self { 308 | let validators_path = Path::new("abci.validators"); 309 | Self { 310 | // we rather crash than start with badly initialized stores 311 | programs: ProgramStore::new("programs").expect("could not create a program store"), 312 | records: RecordStore::new("records").expect("could not create a record store"), 313 | validators: Arc::new(Mutex::new(ValidatorSet::load_or_create(validators_path))), 314 | } 315 | } 316 | 317 | /// Fail if the same record appears more than once as a function input in the transaction. 318 | fn check_no_duplicate_records(&self, transaction: &Transaction) -> Result<()> { 319 | let serial_numbers = transaction.record_serial_numbers(); 320 | if let Some(serial_number) = serial_numbers.iter().duplicates().next() { 321 | bail!( 322 | "record with serial number {} in transaction {} is duplicate", 323 | serial_number, 324 | transaction.id() 325 | ); 326 | } 327 | Ok(()) 328 | } 329 | 330 | /// the transaction should be rejected if its input records don't exist 331 | /// or they aren't known to be unspent either in the ledger or in an unconfirmed transaction output 332 | fn check_inputs_are_unspent(&self, transaction: &Transaction) -> Result<()> { 333 | let serial_numbers = transaction.record_serial_numbers(); 334 | let already_spent = serial_numbers 335 | .iter() 336 | .find(|serial_number| !self.records.is_unspent(serial_number).unwrap_or(true)); 337 | 338 | if let Some(serial_number) = already_spent { 339 | bail!( 340 | "input record serial number {} is unknown or already spent", 341 | serial_number 342 | ) 343 | } 344 | Ok(()) 345 | } 346 | 347 | /// Mark all input records as spent in the record store. This operation could fail if the records are unknown or already spent, 348 | /// but it's assumed the that was validated before as to prevent half-applied transactions in the block. 349 | fn spend_input_records(&self, transaction: &Transaction) -> Result<()> { 350 | transaction 351 | .record_serial_numbers() 352 | .iter() 353 | .map(|serial_number| self.records.spend(serial_number)) 354 | .find(|result| result.is_err()) 355 | .unwrap_or(Ok(())) 356 | } 357 | 358 | /// Add the tranasction output records as unspent in the record store. 359 | fn add_output_records(&self, transaction: &Transaction) -> Result<()> { 360 | #[allow(clippy::clone_on_copy)] 361 | transaction 362 | .output_records() 363 | .iter() 364 | .map(|(commitment, record)| self.records.add(commitment.clone(), record.clone())) 365 | .find(|result| result.is_err()) 366 | .unwrap_or(Ok(())) 367 | } 368 | 369 | /// Apply validator set side-effects of the transaction: collecting fees and changing 370 | /// the voting power based on staking transactions. 371 | fn update_validators(&self, transaction: &Transaction) -> Result<()> { 372 | let mut validator_set = self.validators.lock().unwrap(); 373 | validator_set.collect(transaction.fees() as u64); 374 | transaction 375 | .stake_updates()? 376 | .into_iter() 377 | .for_each(|update| validator_set.apply(update)); 378 | 379 | Ok(()) 380 | } 381 | 382 | fn validate_transaction(&self, transaction: &Transaction) -> Result<()> { 383 | transaction.verify()?; 384 | 385 | let result = match transaction { 386 | Transaction::Deployment { 387 | ref program, 388 | verifying_keys, 389 | fee, 390 | .. 391 | } => { 392 | ensure!( 393 | !self.programs.exists(program.id()), 394 | format!("Program already exists: {}", program.id()) 395 | ); 396 | 397 | if let Some(transition) = fee { 398 | self.verify_transition(transition)?; 399 | } 400 | 401 | // verify deployment is correct and keys are valid 402 | vm::verify_deployment(program, verifying_keys.clone()) 403 | } 404 | Transaction::Execution { transitions, .. } => { 405 | ensure!( 406 | !transitions.is_empty(), 407 | "There are no transitions in the execution" 408 | ); 409 | 410 | let validator_set = self.validators.lock().unwrap(); 411 | for update in transaction.stake_updates()? { 412 | validator_set.validate(&update)? 413 | } 414 | 415 | for transition in transitions { 416 | self.verify_transition(transition)?; 417 | } 418 | Ok(()) 419 | } 420 | }; 421 | 422 | match result { 423 | Err(ref e) => error!("Transaction {} verification failed: {}", transaction, e), 424 | _ => info!("Transaction {} verification successful", transaction), 425 | }; 426 | result 427 | } 428 | 429 | /// Check the given execution transition with the verifying keys from the program store 430 | fn verify_transition(&self, transition: &vm::Transition) -> Result<()> { 431 | let stored_keys = self.programs.get(transition.program_id())?; 432 | 433 | // only verify if we have the program available 434 | if let Some((_program, keys)) = stored_keys { 435 | vm::verify_execution(transition, &keys) 436 | } else { 437 | bail!(format!( 438 | "Program {} does not exist", 439 | transition.program_id() 440 | )) 441 | } 442 | } 443 | 444 | fn store_program(&self, transaction: &Transaction) -> Result<()> { 445 | if let Transaction::Deployment { 446 | program, 447 | verifying_keys, 448 | .. 449 | } = transaction 450 | { 451 | self.programs.add(program.id(), program, verifying_keys)? 452 | } 453 | Ok(()) 454 | } 455 | } 456 | 457 | /// Local file used to track the last block height seen by the abci application. 458 | struct HeightFile; 459 | 460 | impl HeightFile { 461 | const PATH: &str = "abci.height"; 462 | 463 | fn read_or_create() -> i64 { 464 | // if height file is missing or unreadable, create a new one from zero height 465 | if let Ok(bytes) = std::fs::read(Self::PATH) { 466 | // if contents are not readable, crash intentionally 467 | bincode::deserialize(&bytes).expect("Contents of height file are not readable") 468 | } else { 469 | std::fs::write(Self::PATH, bincode::serialize(&0i64).unwrap()).unwrap(); 470 | 0i64 471 | } 472 | } 473 | 474 | fn increment() -> i64 { 475 | // if the file is missing or contents are unexpected, we crash intentionally; 476 | let mut height: i64 = bincode::deserialize(&std::fs::read(Self::PATH).unwrap()).unwrap(); 477 | height += 1; 478 | std::fs::write(Self::PATH, bincode::serialize(&height).unwrap()).unwrap(); 479 | height 480 | } 481 | } 482 | 483 | // just covering a few special cases here. lower level test are done in record store and program store, higher level in integration tests. 484 | #[cfg(test)] 485 | mod tests { 486 | use lib::{ 487 | transaction::Transaction, 488 | vm::{self, Identifier}, 489 | }; 490 | use serde_json::json; 491 | use std::{ 492 | path::Path, 493 | str::FromStr, 494 | sync::{Arc, Mutex}, 495 | }; 496 | use tendermint_abci::Application; 497 | use tendermint_proto::abci::{RequestCheckTx, RequestDeliverTx}; 498 | 499 | use crate::{ 500 | program_store::ProgramStore, record_store::RecordStore, validator_set::ValidatorSet, 501 | }; 502 | 503 | use super::SnarkVMApp; 504 | 505 | #[test] 506 | fn test_abci_hooks() { 507 | let app = SnarkVMApp { 508 | programs: ProgramStore::new("programs_test").expect("could not create a program store"), 509 | records: RecordStore::new("records_test").expect("could not create a record store"), 510 | validators: Arc::new(Mutex::new(ValidatorSet::load_or_create(Path::new("void")))), 511 | }; 512 | 513 | let private_key = vm::PrivateKey::new(&mut rand::thread_rng()).unwrap(); 514 | let view_key = vm::ViewKey::try_from(&private_key).unwrap(); 515 | let address = vm::Address::try_from(&view_key).unwrap(); 516 | 517 | let program = vm::generate_program(include_str!("../../aleo/records.aleo")).unwrap(); 518 | 519 | // deploy the program to the app 520 | let deployment_transaction = 521 | Transaction::deployment(Path::new("aleo/records.aleo"), &private_key, None).unwrap(); 522 | 523 | let _ = app.store_program(&deployment_transaction); 524 | 525 | // normal execution to mint a record, validations should succeed 526 | let transaction = Transaction::execution( 527 | program.clone(), 528 | Identifier::from_str("mint").unwrap(), 529 | &[ 530 | vm::u64_to_value(10), 531 | vm::UserInputValueType::from_str(&address.to_string()).unwrap(), 532 | ], 533 | &private_key, 534 | None, 535 | ) 536 | .unwrap(); 537 | 538 | let check_tx_req = check_request(&transaction); 539 | assert!(app.check_tx(check_tx_req).code == 0); 540 | 541 | let transaction_json = json!(transaction); 542 | 543 | #[cfg(feature = "lambdavm_backend")] 544 | let pointer_path = "/Execution/transitions/0/outputs/0/EncryptedRecord/1/ciphertext"; 545 | #[cfg(feature = "snarkvm_backend")] 546 | let pointer_path = "/Execution/transitions/0/outputs/0/value"; 547 | 548 | // extract the record to use in upcoming transactions 549 | let output_record = transaction_json 550 | .pointer(pointer_path) 551 | .unwrap() 552 | .as_str() 553 | .unwrap(); 554 | 555 | let ciphertext = vm::EncryptedRecord::from_str(output_record).unwrap(); 556 | let record = ciphertext 557 | .decrypt(&view_key) 558 | .map(vm::UserInputValueType::Record) 559 | .unwrap(); 560 | 561 | // utilize the same record twice 562 | let consume_two_transaction = Transaction::execution( 563 | program.clone(), 564 | Identifier::from_str("consume_two").unwrap(), 565 | &[record.clone(), record.clone()], 566 | &private_key, 567 | None, 568 | ) 569 | .unwrap(); 570 | 571 | // both check_tx and deliver_tx validate that inputs are not being spent twice 572 | let check_tx_req = check_request(&consume_two_transaction); 573 | let deliver_tx_req = deliver_request(&consume_two_transaction); 574 | assert!(app.check_tx(check_tx_req).code != 0); 575 | assert!(app.deliver_tx(deliver_tx_req).code != 0); 576 | 577 | // because validations failed, inputs should not be spent in the store 578 | app.check_inputs_are_unspent(&consume_two_transaction) 579 | .unwrap(); 580 | 581 | // consume the record 582 | let consume_transaction = Transaction::execution( 583 | program, 584 | Identifier::from_str("consume").unwrap(), 585 | &[record], 586 | &private_key, 587 | None, 588 | ) 589 | .unwrap(); 590 | 591 | let check_tx_req = check_request(&consume_transaction); 592 | let deliver_tx_req = deliver_request(&consume_transaction); 593 | 594 | // because the transaction is valid, check and deliver should succeed 595 | assert!(app.check_tx(check_tx_req.clone()).code == 0); 596 | assert!(app.deliver_tx(deliver_tx_req.clone()).code == 0); 597 | 598 | // because deliver_tx() spends the records, further validations should fail 599 | assert!(app.check_tx(check_tx_req).code != 0); 600 | assert!(app.deliver_tx(deliver_tx_req).code != 0); 601 | } 602 | 603 | fn check_request(transaction: &Transaction) -> RequestCheckTx { 604 | RequestCheckTx { 605 | tx: bincode::serialize(transaction).unwrap(), 606 | r#type: 0, 607 | } 608 | } 609 | 610 | fn deliver_request(transaction: &Transaction) -> RequestDeliverTx { 611 | RequestDeliverTx { 612 | tx: bincode::serialize(transaction).unwrap(), 613 | } 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /src/blockchain/genesis.rs: -------------------------------------------------------------------------------- 1 | /// Binary that walks a list of tendermint node directories (like the default ~/.tendermint or a testnet generated node dir), 2 | /// assuming they also contain an aleo account credentials file, and updates their genesis files to include the genesis state 3 | /// expected by our abci app. 4 | use std::{collections::HashMap, path::PathBuf}; 5 | 6 | use anyhow::Result; 7 | use clap::Parser; 8 | use lib::{validator, vm}; 9 | 10 | /// Takes a list of node directories and updates the genesis files on each of them 11 | /// to include records to assign default credits to each validator and a mapping 12 | /// of tendermint validator pubkey to aleo account address. 13 | #[derive(Debug, Parser)] 14 | #[clap()] 15 | pub struct Cli { 16 | /// List of node directories. 17 | /// Each one is expected to contain a config/genesis.json (with a tendermint genesis) 18 | /// a config/priv_validator_key.json (with tendermint validator credentials) 19 | /// and a account.json (with aleo credentials) 20 | #[clap()] 21 | node_dirs: Vec, 22 | 23 | /// The amount of gates to assign to each validator 24 | #[clap(long, default_value = "1000")] 25 | amount: u64, 26 | } 27 | 28 | fn main() -> Result<()> { 29 | let cli: Cli = Cli::parse(); 30 | 31 | // update the genesis JSON with the calculated app state 32 | let genesis_path = cli 33 | .node_dirs 34 | .first() 35 | .expect("need at least one directory") 36 | .join("config/genesis.json"); 37 | let mut genesis: serde_json::Value = 38 | serde_json::from_str(&std::fs::read_to_string(genesis_path)?)?; 39 | let voting_powers: HashMap = genesis["validators"] 40 | .as_array() 41 | .unwrap() 42 | .iter() 43 | .map(|validator| { 44 | ( 45 | validator["pub_key"]["value"].as_str().unwrap().to_string(), 46 | validator["power"].as_str().unwrap().parse().unwrap(), 47 | ) 48 | }) 49 | .collect(); 50 | 51 | // for each node in the testnet, map its tendermint pubkey to its aleo account address 52 | // and generate records for initial validator credits 53 | let mut validators = Vec::new(); 54 | let mut genesis_records = Vec::new(); 55 | for node_dir in cli.node_dirs.clone() { 56 | println!("processing {}", node_dir.to_string_lossy()); 57 | 58 | let aleo_account_path = node_dir.join("account.json"); 59 | let aleo_account: serde_json::Value = 60 | serde_json::from_str(&std::fs::read_to_string(aleo_account_path)?)?; 61 | let aleo_address = aleo_account["address"].as_str().unwrap(); 62 | 63 | let tmint_account_path = node_dir.join("config/priv_validator_key.json"); 64 | let tmint_account: serde_json::Value = 65 | serde_json::from_str(&std::fs::read_to_string(tmint_account_path)?)?; 66 | let tmint_pubkey = tmint_account["pub_key"]["value"] 67 | .as_str() 68 | .expect("couldn't extract pubkey from json"); 69 | let voting_power = *voting_powers.get(tmint_pubkey).unwrap(); 70 | let validator = validator::Validator::from_str(tmint_pubkey, aleo_address, voting_power)?; 71 | 72 | println!("Generating record for {aleo_address}"); 73 | // NOTE: using a hardcoded seed, not for production! 74 | #[allow(unused_mut)] 75 | let mut record = vm::mint_record( 76 | "credits.aleo", 77 | "credits", 78 | &validator.aleo_address, 79 | cli.amount, 80 | 1234, 81 | )?; 82 | 83 | genesis_records.push(record); 84 | validators.push(validator); 85 | } 86 | 87 | // update the genesis JSON with the calculated app state 88 | let genesis_state = validator::GenesisState { 89 | records: genesis_records, 90 | validators, 91 | }; 92 | genesis.as_object_mut().unwrap().insert( 93 | "app_state".to_string(), 94 | serde_json::to_value(genesis_state)?, 95 | ); 96 | let genesis_json = serde_json::to_string_pretty(&genesis)?; 97 | 98 | // set the same genesis file in all nodes of the testnet 99 | for node_dir in cli.node_dirs { 100 | let node_genesis_path = node_dir.join("config/genesis.json"); 101 | println!("Writing genesis to {}", node_genesis_path.to_string_lossy()); 102 | std::fs::write(node_genesis_path, &genesis_json)?; 103 | } 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /src/blockchain/main.rs: -------------------------------------------------------------------------------- 1 | //! In-memory key/value store application for Tendermint. 2 | 3 | use application::SnarkVMApp; 4 | use clap::Parser; 5 | use tendermint_abci::ServerBuilder; 6 | use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt}; 7 | 8 | mod application; 9 | mod program_store; 10 | mod record_store; 11 | mod validator_set; 12 | 13 | #[derive(Debug, Parser)] 14 | #[clap(author, version, about)] 15 | struct Cli { 16 | /// Bind the TCP server to this host. 17 | #[clap(long, default_value = "127.0.0.1")] 18 | host: String, 19 | 20 | /// Bind the TCP server to this port. 21 | #[clap(short, long, default_value = "26658")] 22 | port: u16, 23 | 24 | /// The default server read buffer size, in bytes, for each incoming client 25 | /// connection. 26 | #[clap(short, long, default_value = "1048576")] 27 | read_buf_size: usize, 28 | 29 | /// Increase output logging verbosity to DEBUG level. 30 | #[clap(short, long)] 31 | verbose: bool, 32 | 33 | /// Suppress all output logging (overrides --verbose). 34 | #[clap(short, long)] 35 | quiet: bool, 36 | } 37 | 38 | fn main() { 39 | let cli: Cli = Cli::parse(); 40 | let log_level = if cli.quiet { 41 | LevelFilter::OFF 42 | } else if cli.verbose { 43 | LevelFilter::DEBUG 44 | } else { 45 | LevelFilter::INFO 46 | }; 47 | 48 | let subscriber = tracing_subscriber::fmt() 49 | // Use a more compact, abbreviated log format 50 | .compact() 51 | .with_max_level(log_level) 52 | // Display the thread ID an event was recorded on 53 | .with_thread_ids(true) 54 | // Don't display the event's target (module path) 55 | .with_target(false) 56 | // Build the subscriber 57 | .finish(); 58 | 59 | subscriber.init(); 60 | 61 | let app = SnarkVMApp::new(); 62 | let server = ServerBuilder::new(cli.read_buf_size) 63 | .bind(format!("{}:{}", cli.host, cli.port), app) 64 | .unwrap(); 65 | 66 | server.listen().unwrap(); 67 | } 68 | -------------------------------------------------------------------------------- /src/blockchain/program_store.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use lib::vm::{self, VerifyingKeyMap}; 3 | use log::{debug, error}; 4 | use std::sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}; 5 | use std::thread; 6 | 7 | pub type StoredProgram = (vm::Program, vm::VerifyingKeyMap); 8 | 9 | type Key = vm::ProgramID; 10 | type Value = StoredProgram; 11 | 12 | /// The program store tracks programs that have been deployed to the OS 13 | #[derive(Clone, Debug)] 14 | pub struct ProgramStore { 15 | /// Channel used to send operations to the task that manages the store state. 16 | command_sender: Sender, 17 | } 18 | 19 | #[derive(Debug)] 20 | enum Command { 21 | Add(Key, Box, SyncSender>), 22 | Get(Key, SyncSender>>), 23 | Exists(Key, SyncSender), 24 | } 25 | 26 | impl ProgramStore { 27 | /// Start a new record store on a new thread 28 | pub fn new(path: &str) -> Result { 29 | let db_programs = rocksdb::DB::open_default(format!("{path}.deployed.db"))?; 30 | 31 | let (command_sender, command_receiver): (Sender, Receiver) = channel(); 32 | 33 | thread::spawn(move || { 34 | while let Ok(command) = command_receiver.recv() { 35 | match command { 36 | Command::Add(program_id, program_keys, reply_to) => { 37 | let result = if db_programs 38 | .get(program_id.to_string().as_bytes()) 39 | .unwrap_or(None) 40 | .is_some() 41 | { 42 | Err(anyhow!( 43 | "Program {} already exists in the store", 44 | &program_id, 45 | )) 46 | } else { 47 | let program_keys = bincode::serialize(&program_keys); 48 | Ok(db_programs 49 | .put(program_id.to_string().as_bytes(), program_keys.unwrap()) 50 | .unwrap_or_else(|e| error!("failed to write to db {}", e))) 51 | }; 52 | 53 | reply_to.send(result).unwrap_or_else(|e| error!("{}", e)); 54 | } 55 | Command::Get(program_id, reply_to) => { 56 | let result = db_programs 57 | .get(program_id.to_string().as_bytes()) 58 | .unwrap_or(None) 59 | .map(|value| bincode::deserialize::(&value).unwrap()); 60 | 61 | reply_to 62 | .send(Ok(result)) 63 | .unwrap_or_else(|e| error!("{}", e)); 64 | } 65 | Command::Exists(program_id, reply_to) => { 66 | let result = db_programs.key_may_exist(program_id.to_string().as_bytes()); 67 | reply_to.send(result).unwrap_or_else(|e| error!("{}", e)); 68 | } 69 | }; 70 | } 71 | }); 72 | let program_store = Self { command_sender }; 73 | 74 | program_store.load_credits()?; 75 | Ok(program_store) 76 | } 77 | 78 | /// Returns a program 79 | pub fn get(&self, program_id: &vm::ProgramID) -> Result> { 80 | let (reply_sender, reply_receiver) = sync_channel(0); 81 | 82 | self.command_sender 83 | .send(Command::Get(program_id.to_owned(), reply_sender))?; 84 | 85 | reply_receiver.recv()? 86 | } 87 | 88 | /// Adds a program to the store 89 | pub fn add( 90 | &self, 91 | program_id: &vm::ProgramID, 92 | program: &vm::Program, 93 | verifying_keys: &vm::VerifyingKeyMap, 94 | ) -> Result<()> { 95 | let (reply_sender, reply_receiver) = sync_channel(0); 96 | 97 | self.command_sender.send(Command::Add( 98 | program_id.to_owned(), 99 | Box::new((program.clone(), verifying_keys.clone())), 100 | reply_sender, 101 | ))?; 102 | 103 | reply_receiver.recv()? 104 | } 105 | 106 | /// Returns whether a program ID is already stored 107 | pub fn exists(&self, program_id: &vm::ProgramID) -> bool { 108 | let (reply_sender, reply_receiver) = sync_channel(0); 109 | 110 | self.command_sender 111 | .send(Command::Exists(program_id.to_owned(), reply_sender)) 112 | .unwrap(); 113 | 114 | reply_receiver.recv().unwrap_or(false) 115 | } 116 | 117 | fn load_credits(&self) -> Result<()> { 118 | let (credits_program, keys) = lib::load_credits(); 119 | 120 | if self.exists(credits_program.id()) { 121 | debug!("Credits program already exists in program store"); 122 | Ok(()) 123 | } else { 124 | debug!("Loading credits.aleo as part of Program Store initialization"); 125 | 126 | let key_map = keys 127 | .map 128 | .into_iter() 129 | .map(|(i, (_, verifying_key))| (i, verifying_key)) 130 | .collect(); 131 | 132 | self.add( 133 | credits_program.id(), 134 | &credits_program, 135 | &VerifyingKeyMap { map: key_map }, 136 | )?; 137 | 138 | Ok(()) 139 | } 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use super::*; 146 | use lib::vm; 147 | use lib::vm::Program; 148 | use std::{fs, str::FromStr}; 149 | 150 | #[ctor::ctor] 151 | fn init() { 152 | // todo: this fails with error because it's already initialised 153 | /* simple_logger::SimpleLogger::new() 154 | .env() 155 | .with_level(log::LevelFilter::Info) 156 | .init() 157 | .unwrap(); */ 158 | 159 | fs::remove_dir_all(db_path("")).unwrap_or_default(); 160 | } 161 | 162 | fn db_path(suffix: &str) -> String { 163 | format!(".db_test/{suffix}") 164 | } 165 | 166 | #[test] 167 | fn add_program() { 168 | let store = ProgramStore::new(&db_path("program")).unwrap(); 169 | 170 | let program_path = format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/aleo/hello.aleo"); 171 | let program = 172 | Program::from_str(fs::read_to_string(program_path).unwrap().as_str()).unwrap(); 173 | 174 | let get_program = store.get(program.id()); 175 | 176 | assert!(get_program.unwrap().is_none()); 177 | 178 | let storage_attempt = store_program(&store, "/aleo/hello.aleo"); 179 | assert!(storage_attempt.is_ok() && store.exists(storage_attempt.unwrap().id())); 180 | 181 | // FIXME patching rocksdb weird behavior 182 | std::mem::forget(store); 183 | } 184 | 185 | #[test] 186 | fn credits_loaded() { 187 | let program = Program::credits().expect("Problem loading Credits"); 188 | 189 | { 190 | let store = rocksdb::DB::open_default(db_path("credits")).unwrap(); 191 | let get_program = store.get(program.id().to_string().into_bytes()); 192 | assert!(get_program.unwrap().is_none()); 193 | } 194 | let store = ProgramStore::new(&db_path("credits")).unwrap(); 195 | 196 | assert!(store.exists(program.id())); 197 | } 198 | 199 | fn store_program(program_store: &ProgramStore, path: &str) -> Result { 200 | let program_path = format!("{}{}", env!("CARGO_MANIFEST_DIR"), path); 201 | 202 | let program_string = fs::read_to_string(program_path).unwrap(); 203 | 204 | // generate program keys (proving and verifying) and keep the verifying one for the store 205 | let (program, program_build) = vm::build_program(&program_string)?; 206 | 207 | let keys = program_build 208 | .map 209 | .into_iter() 210 | .map(|(i, (_, verifying_key))| (i, verifying_key)) 211 | .collect(); 212 | 213 | program_store.add(program.id(), &program, &VerifyingKeyMap { map: keys })?; 214 | 215 | Ok(program) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/blockchain/record_store.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use lib::vm::{self, EncryptedRecord, Field}; 3 | use log::error; 4 | use rocksdb::{Direction, IteratorMode, WriteBatch}; 5 | use std::collections::{HashMap, HashSet}; 6 | use std::str::FromStr; 7 | use std::sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}; 8 | use std::thread; 9 | 10 | // because both serial numbers and Commitments are really fields, define types to differentiate them 11 | type SerialNumber = Field; 12 | type Commitment = Field; 13 | 14 | // TODO: Key and Value types should be concrete types instead of serialized data like in 15 | // program store, so that type errors bubble up asap (ie from the interaction with the DB) 16 | type Key = Vec; 17 | type Value = Vec; 18 | 19 | /// Internal channel reply for the scan command 20 | type ScanReply = (Vec<(Key, Value)>, Option); 21 | /// Public return type for the scan command. 22 | type ScanResult = (Vec<(Commitment, vm::EncryptedRecord)>, Option); 23 | 24 | /// The record store tracks the known unspent and spent record sets (similar to bitcoin's UTXO set) 25 | /// according to the transactions that are committed to the ledger. 26 | /// Because of how Tendermint ABCI applications are structured, this store is prepared to buffer 27 | /// updates (new unspent record additions and spending of known records) while transactions are being 28 | /// processed, and apply them together when the block is committed. 29 | #[derive(Clone, Debug)] 30 | pub struct RecordStore { 31 | /// Channel used to send operations to the task that manages the store state. 32 | command_sender: Sender, 33 | } 34 | 35 | #[derive(Debug)] 36 | enum Command { 37 | Add(Key, Value, SyncSender>), 38 | Spend(Key, SyncSender>), 39 | IsUnspent(Key, SyncSender), 40 | Commit, 41 | ScanSpentRecords(SyncSender>), 42 | ScanRecords { 43 | from: Option, 44 | limit: Option, 45 | reply_sender: SyncSender, 46 | }, 47 | } 48 | 49 | impl RecordStore { 50 | /// Start a new record store on a new thread 51 | pub fn new(path: &str) -> Result { 52 | // TODO review column families, may be a more natural way to separate spent/unspent on the same db and still get the benefits 53 | // https://github.com/EighteenZi/rocksdb_wiki/blob/master/Column-Families.md 54 | // we may also like to try something other than rocksdb here, e.g. sqlite 55 | 56 | // TODO: comment on this 57 | let db_records = rocksdb::DB::open_default(format!("{path}.records.db"))?; 58 | 59 | // DB to track spent record serial_numbers. These are tracked to ensure that records aren't spent more than once 60 | // (without having to _know_ the actual record contents). 61 | let db_spent = rocksdb::DB::open_default(format!("{path}.spent.db"))?; 62 | 63 | // map to store temporary unspent record additions until a block is comitted. 64 | let mut record_buffer = HashMap::new(); 65 | 66 | // map to store temporary spent record additions until a block is comitted. 67 | let mut spent_buffer = HashMap::new(); 68 | 69 | let (command_sender, command_receiver): (Sender, Receiver) = channel(); 70 | 71 | thread::spawn(move || { 72 | while let Ok(command) = command_receiver.recv() { 73 | match command { 74 | Command::Add(commitment, ciphertext, reply_to) => { 75 | // TODO: Remove/change this into something secure (merkle path to valid records exists) 76 | // Because tracking existence and spent status leads to security concerns, existence of records will 77 | // have to be proven by the execution. Until this is implemented, return Ok by default here and assume the record exists. 78 | let result = if record_buffer.contains_key(&commitment) 79 | || key_exists_or_fails(&db_records, &commitment) 80 | { 81 | Err(anyhow!( 82 | "record {} already exists", 83 | String::from_utf8_lossy(&commitment) 84 | )) 85 | } else { 86 | record_buffer.insert(commitment, ciphertext); 87 | Ok(()) 88 | }; 89 | reply_to.send(result).unwrap_or_else(|e| error!("{}", e)); 90 | } 91 | Command::Spend(serial_number, reply_to) => { 92 | // TODO: [related to above] implement record existence check and handle case where it exists and it doesn't 93 | let result = if key_exists_or_fails(&db_spent, &serial_number) 94 | || spent_buffer.contains_key(&serial_number) 95 | { 96 | Err(anyhow!("record already spent")) 97 | } else { 98 | spent_buffer.insert(serial_number, "1".as_bytes()); 99 | Ok(()) 100 | }; 101 | 102 | reply_to.send(result).unwrap_or_else(|e| error!("{}", e)); 103 | } 104 | Command::IsUnspent(serial_number, reply_to) => { 105 | // TODO: [related to above] handle record existence scenarios 106 | let is_unspent = !key_exists_or_fails(&db_spent, &serial_number) 107 | && !spent_buffer.contains_key(&serial_number); 108 | reply_to 109 | .send(is_unspent) 110 | .unwrap_or_else(|e| error!("{}", e)); 111 | } 112 | Command::Commit => { 113 | // add new records to store 114 | let mut batch = WriteBatch::default(); 115 | for (key, value) in record_buffer.iter() { 116 | batch.put(key, value); 117 | } 118 | db_records 119 | .write(batch) 120 | .unwrap_or_else(|e| error!("failed to write to db {}", e)); 121 | 122 | // add all buffer spent to db spent, i.e. persisted consumed records (as a serial number for security) 123 | let mut batch = WriteBatch::default(); 124 | for (key, value) in spent_buffer.iter() { 125 | batch.put(key.clone(), value); 126 | } 127 | 128 | db_spent 129 | .write(batch) 130 | .unwrap_or_else(|e| error!("failed to write to db {}", e)); 131 | 132 | // remove all buffer spent from db unspent, i.e. consumed records should only be kept in spent db 133 | let mut batch = WriteBatch::default(); 134 | for key in spent_buffer.keys() { 135 | batch.delete(key); 136 | } 137 | spent_buffer.clear(); 138 | } 139 | Command::ScanRecords { 140 | from, 141 | limit, 142 | reply_sender: reply_to, 143 | } => { 144 | let iterator_mode = from.as_ref().map_or(IteratorMode::Start, |key| { 145 | IteratorMode::From(key, Direction::Forward) 146 | }); 147 | let mut records = vec![]; 148 | let mut last_key = None; 149 | for item in db_records.iterator(iterator_mode) { 150 | if limit.map_or(false, |l| records.len() >= l) { 151 | break; 152 | } 153 | if let Ok((key, record)) = item { 154 | records.push((key.to_vec(), record.to_vec())); 155 | last_key = Some(key.to_vec()); 156 | } 157 | } 158 | reply_to 159 | .send((records, last_key)) 160 | .unwrap_or_else(|e| error!("{}", e)); 161 | } 162 | Command::ScanSpentRecords(reply_sender) => { 163 | let spent_records = db_spent 164 | .iterator(IteratorMode::Start) 165 | .filter_map(|s| { 166 | s.map(|(k, _)| { 167 | SerialNumber::from_str(&String::from_utf8_lossy(&k)).unwrap() 168 | }) 169 | .ok() 170 | }) 171 | .collect(); 172 | reply_sender 173 | .send(spent_records) 174 | .unwrap_or_else(|e| error!("{}", e)); 175 | } 176 | }; 177 | } 178 | }); 179 | Ok(Self { command_sender }) 180 | } 181 | 182 | /// Saves a new unspent record to the write buffer 183 | #[allow(clippy::redundant_clone)] // commitments/serial numbers are strings on lambdavm and so clippy generates a warning for `.to_string()` 184 | pub fn add(&self, commitment: Commitment, record: vm::EncryptedRecord) -> Result<()> { 185 | let (reply_sender, reply_receiver) = sync_channel(0); 186 | 187 | let commitment = commitment.to_string().into_bytes(); 188 | let ciphertext = record.to_string().into_bytes(); 189 | 190 | self.command_sender 191 | .send(Command::Add(commitment, ciphertext, reply_sender))?; 192 | reply_receiver.recv()? 193 | } 194 | 195 | /// Marks a record as spent in the write buffer. 196 | /// Fails if the record is not found or was already spent. 197 | pub fn spend(&self, serial_number: &SerialNumber) -> Result<()> { 198 | let (reply_sender, reply_receiver) = sync_channel(0); 199 | 200 | let serial_number = serial_number.to_string().into_bytes(); 201 | self.command_sender 202 | .send(Command::Spend(serial_number, reply_sender))?; 203 | reply_receiver.recv()? 204 | } 205 | 206 | /// Commit write buffer changes to persistent storage and empty the buffer. 207 | pub fn commit(&self) -> Result<()> { 208 | Ok(self.command_sender.send(Command::Commit)?) 209 | } 210 | 211 | /// Returns whether a record by the given serial_number is known and not spent 212 | pub fn is_unspent(&self, serial_number: &SerialNumber) -> Result { 213 | let (reply_sender, reply_receiver) = sync_channel(0); 214 | 215 | let serial_number = serial_number.to_string().into_bytes(); 216 | self.command_sender 217 | .send(Command::IsUnspent(serial_number, reply_sender))?; 218 | Ok(reply_receiver.recv()?) 219 | } 220 | 221 | /// Return up to `limit` record ciphertexts 222 | #[allow(clippy::redundant_clone)] // commitments/serial numbers are strings on lambdavm and so clippy generates a warning for `.to_string()` 223 | pub fn scan(&self, from: Option, limit: Option) -> Result { 224 | let from = from.map(|commitment| commitment.to_string().into_bytes()); 225 | let (reply_sender, reply_receiver) = sync_channel(0); 226 | 227 | self.command_sender.send(Command::ScanRecords { 228 | from, 229 | limit, 230 | reply_sender, 231 | })?; 232 | 233 | let (results, last_key) = reply_receiver.recv()?; 234 | let last_key = last_key 235 | .map(|commitment| Commitment::from_str(&String::from_utf8_lossy(&commitment)).unwrap()); 236 | let results = results 237 | .iter() 238 | .map(|(commitment, record)| { 239 | let commitment = 240 | Commitment::from_str(&String::from_utf8_lossy(commitment)).unwrap(); 241 | 242 | let record = EncryptedRecord::from_str(&String::from_utf8_lossy(record)).unwrap(); 243 | 244 | (commitment, record) 245 | }) 246 | .collect(); 247 | Ok((results, last_key)) 248 | } 249 | 250 | // TODO: implement way of limiting response size/count or optimization for better scaling 251 | /// Return all serial numbers 252 | pub fn scan_spent(&self) -> Result> { 253 | let (reply_sender, reply_receiver) = sync_channel(0); 254 | 255 | self.command_sender 256 | .send(Command::ScanSpentRecords(reply_sender))?; 257 | 258 | let results = reply_receiver.recv()?; 259 | Ok(results) 260 | } 261 | } 262 | 263 | /// TODO explain the need for this 264 | fn key_exists_or_fails(db: &rocksdb::DB, key: &Key) -> bool { 265 | !matches!(db.get(key), Ok(None)) 266 | } 267 | 268 | #[cfg(test)] 269 | mod tests { 270 | use std::fs; 271 | 272 | use super::*; 273 | #[allow(unused_imports)] 274 | use indexmap::IndexMap; 275 | #[allow(unused_imports)] 276 | use lib::vm::{compute_serial_number, PrivateKey, Record, ViewKey}; 277 | 278 | #[ctor::ctor] 279 | fn init() { 280 | simple_logger::SimpleLogger::new() 281 | .env() 282 | .with_level(log::LevelFilter::Info) 283 | .init() 284 | .unwrap(); 285 | 286 | fs::remove_dir_all(db_path("")).unwrap_or_default(); 287 | } 288 | 289 | fn db_path(suffix: &str) -> String { 290 | format!(".db_test/{suffix}") 291 | } 292 | 293 | #[test] 294 | fn add_and_spend_record() { 295 | let store = RecordStore::new(&db_path("records1")).unwrap(); 296 | let (record, commitment, serial_number) = new_record(); 297 | store.add(commitment, record).unwrap(); 298 | assert!(store.is_unspent(&serial_number).unwrap()); 299 | store.commit().unwrap(); 300 | assert!(store.is_unspent(&serial_number).unwrap()); 301 | store.spend(&serial_number).unwrap(); 302 | assert!(!store.is_unspent(&serial_number).unwrap()); 303 | store.commit().unwrap(); 304 | assert!(!store.is_unspent(&serial_number).unwrap()); 305 | 306 | let msg = store 307 | .spend(&serial_number) 308 | .unwrap_err() 309 | .root_cause() 310 | .to_string(); 311 | assert_eq!("record already spent", msg); 312 | 313 | // FIXME patching rocksdb weird behavior 314 | std::mem::forget(store); 315 | } 316 | 317 | #[test] 318 | #[allow(clippy::clone_on_copy)] 319 | fn no_double_add_record() { 320 | let store = RecordStore::new(&db_path("records2")).unwrap(); 321 | 322 | let (record, commitment, _) = new_record(); 323 | store.add(commitment.clone(), record.clone()).unwrap(); 324 | let msg = store 325 | .add(commitment.clone(), record) 326 | .unwrap_err() 327 | .root_cause() 328 | .to_string(); 329 | assert_eq!(format!("record {commitment} already exists"), msg); 330 | store.commit().unwrap(); 331 | 332 | let (record, commitment, _) = new_record(); 333 | store.add(commitment.clone(), record.clone()).unwrap(); 334 | store.commit().unwrap(); 335 | let msg = store 336 | .add(commitment.clone(), record) 337 | .unwrap_err() 338 | .root_cause() 339 | .to_string(); 340 | assert_eq!(format!("record {commitment} already exists"), msg); 341 | 342 | // FIXME patching rocksdb weird behavior 343 | std::mem::forget(store); 344 | } 345 | 346 | #[test] 347 | fn spend_before_commit() { 348 | let store = RecordStore::new(&db_path("records3")).unwrap(); 349 | 350 | let (record, commitment, serial_number) = new_record(); 351 | store.add(commitment, record).unwrap(); 352 | assert!(store.is_unspent(&serial_number).unwrap()); 353 | store.spend(&serial_number).unwrap(); 354 | assert!(!store.is_unspent(&serial_number).unwrap()); 355 | store.commit().unwrap(); 356 | assert!(!store.is_unspent(&serial_number).unwrap()); 357 | 358 | // FIXME patching rocksdb weird behavior 359 | std::mem::forget(store); 360 | } 361 | 362 | #[test] 363 | fn no_double_spend_record() { 364 | let store = RecordStore::new(&db_path("records4")).unwrap(); 365 | 366 | // add, commit, spend, commit, fail spend 367 | let (record, commitment, serial_number) = new_record(); 368 | store.add(commitment, record).unwrap(); 369 | store.commit().unwrap(); 370 | assert!(store.is_unspent(&serial_number).unwrap()); 371 | store.spend(&serial_number).unwrap(); 372 | store.commit().unwrap(); 373 | assert!(!store.is_unspent(&serial_number).unwrap()); 374 | let msg = store 375 | .spend(&serial_number) 376 | .unwrap_err() 377 | .root_cause() 378 | .to_string(); 379 | assert_eq!("record already spent", msg); 380 | 381 | // add, commit, spend, fail spend, commit, fail spend 382 | let (record, commitment, serial_number) = new_record(); 383 | store.add(commitment, record).unwrap(); 384 | store.commit().unwrap(); 385 | assert!(store.is_unspent(&serial_number).unwrap()); 386 | store.spend(&serial_number).unwrap(); 387 | let msg = store 388 | .spend(&serial_number) 389 | .unwrap_err() 390 | .root_cause() 391 | .to_string(); 392 | assert_eq!("record already spent", msg); 393 | store.commit().unwrap(); 394 | assert!(!store.is_unspent(&serial_number).unwrap()); 395 | let msg = store 396 | .spend(&serial_number) 397 | .unwrap_err() 398 | .root_cause() 399 | .to_string(); 400 | assert_eq!("record already spent", msg); 401 | 402 | // add, spend, fail spend, commit 403 | let (record, commitment, serial_number) = new_record(); 404 | store.add(commitment, record).unwrap(); 405 | store.spend(&serial_number).unwrap(); 406 | let msg = store 407 | .spend(&serial_number) 408 | .unwrap_err() 409 | .root_cause() 410 | .to_string(); 411 | assert_eq!("record already spent", msg); 412 | store.commit().unwrap(); 413 | assert!(!store.is_unspent(&serial_number).unwrap()); 414 | 415 | // FIXME patching rocksdb weird behavior 416 | std::mem::forget(store); 417 | } 418 | 419 | // TODO: (check if it's possible) make a test for validating behavior related to spending a non-existant record 420 | 421 | #[cfg(feature = "lambdavm_backend")] 422 | fn new_record() -> (EncryptedRecord, Commitment, SerialNumber) { 423 | use snarkvm::prelude::{Scalar, Uniform}; 424 | 425 | let address = 426 | String::from("aleo1330ghze6tqvc0s9vd43mnetxlnyfypgf6rw597gn4723lp2wt5gqfk09ry"); 427 | let mut record = Record::new_from_aleo_address(address, 5, IndexMap::new(), None); 428 | 429 | let private_key = PrivateKey::new(&mut rand::thread_rng()).unwrap(); 430 | let rng = &mut rand::thread_rng(); 431 | let randomizer = Scalar::rand(rng); 432 | let record_ciphertext = record.encrypt(randomizer).unwrap(); 433 | let commitment = record.commitment().unwrap(); 434 | let serial_number = compute_serial_number(private_key, commitment.clone()).unwrap(); 435 | 436 | (record_ciphertext, commitment, serial_number) 437 | } 438 | 439 | #[cfg(feature = "snarkvm_backend")] 440 | fn new_record() -> (EncryptedRecord, Commitment, SerialNumber) { 441 | use lib::vm::{Identifier, ProgramID}; 442 | use snarkvm::prelude::{Network, Testnet3, Uniform}; 443 | 444 | let rng = &mut rand::thread_rng(); 445 | let randomizer = Uniform::rand(rng); 446 | let nonce = Testnet3::g_scalar_multiply(&randomizer); 447 | let record = lib::vm::Record::from_str( 448 | &format!("{{ owner: aleo1330ghze6tqvc0s9vd43mnetxlnyfypgf6rw597gn4723lp2wt5gqfk09ry.private, gates: 5u64.private, token_amount: 100u64.private, _nonce: {nonce}.public }}"), 449 | ).unwrap(); 450 | let program_id = ProgramID::from_str("foo.aleo").unwrap(); 451 | let name = Identifier::from_str("bar").unwrap(); 452 | let commitment = record.to_commitment(&program_id, &name).unwrap(); 453 | let record_ciphertext = record.encrypt(randomizer).unwrap(); 454 | 455 | // compute serial number to check for spending status 456 | let pk = 457 | PrivateKey::from_str("APrivateKey1zkpCT3zCj49nmVoeBXa21EGLjTUc7AKAcMNKLXzP7kc4cgx") 458 | .unwrap(); 459 | let serial_number = vm::compute_serial_number(pk, commitment).unwrap(); 460 | 461 | (record_ciphertext, commitment, serial_number) 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/blockchain/validator_set.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use lib::vm; 7 | use log::{debug, error, warn}; 8 | 9 | use anyhow::{anyhow, Result}; 10 | use lib::validator::{Address, Stake, Validator, VotingPower}; 11 | 12 | type Fee = u64; 13 | 14 | /// There's a baseline for the credits distributed among validators, in addition to fees. 15 | /// For now it's constant, but it could be made to decrease based on height to control inflation. 16 | const BASELINE_BLOCK_REWARD: Fee = 100; 17 | /// The portion of the total block rewards that is given to the block proposer. The rest is distributed 18 | /// among voters weighted by their voting power. 19 | const PROPOSER_REWARD_PERCENTAGE: u64 = 50; 20 | 21 | /// Tracks the network validator set, particularly how the tendermint addresses map to 22 | /// aleo account addresses needed to assign credits records for validator rewards. 23 | /// The ValidatorSet exposes methods to collect fees and has logic to distribute them 24 | /// (in addition to a baseline reward), based on block proposer and voting power. 25 | /// There are also methods to apply voting power changes on staking transactions. 26 | #[derive(Debug)] 27 | pub struct ValidatorSet { 28 | /// Path to the file used to persist the currently known validator list of validator, so the app works across restarts. 29 | path: PathBuf, 30 | /// The currently known validator set, including the terndermint pub key/address to aleo account mapping 31 | /// and their last known voting power. 32 | validators: HashMap, 33 | /// The fees collected for the current block. 34 | fees: Fee, 35 | /// The proposer of the current block. 36 | current_proposer: Option
, 37 | /// The previous round block votes, to be considered to distribute this block's rewards. 38 | current_votes: HashMap, 39 | /// The current block's height, used as a seed to generate reward records deterministically across nodes. 40 | current_height: u64, 41 | /// The list of validators that had voting power changes during the current block, including added or removed ones. 42 | updated_validators: HashSet
, 43 | } 44 | 45 | impl ValidatorSet { 46 | /// Create a new validator set. If a previous validators file is found, populate the set with its contents, 47 | /// otherwise start with an empty one. 48 | pub fn load_or_create(path: &Path) -> Self { 49 | let validators = if let Ok(json) = std::fs::read_to_string(path) { 50 | serde_json::from_str::>(&json) 51 | .expect("validators file content is invalid") 52 | .into_iter() 53 | .map(|validator| { 54 | debug!("loading validator {}", validator); 55 | (validator.address(), validator) 56 | }) 57 | .collect() 58 | } else { 59 | HashMap::new() 60 | }; 61 | 62 | Self { 63 | path: path.into(), 64 | validators, 65 | current_height: 0, 66 | fees: 0, 67 | current_proposer: None, 68 | current_votes: HashMap::new(), 69 | updated_validators: HashSet::new(), 70 | } 71 | } 72 | 73 | pub fn replace(&mut self, validators: Vec) { 74 | self.validators = validators 75 | .into_iter() 76 | .map(|validator| (validator.address(), validator)) 77 | .collect() 78 | } 79 | 80 | /// Updates state based on previous commit votes, to know how awards should be assigned. 81 | pub fn begin_block( 82 | &mut self, 83 | proposer: &Address, 84 | votes: HashMap, 85 | height: u64, 86 | ) { 87 | if !self.validators.contains_key(proposer) { 88 | error!( 89 | "received unknown address as proposer {}", 90 | hex::encode_upper(proposer) 91 | ); 92 | } 93 | 94 | for voter in votes.keys() { 95 | if !self.validators.contains_key(voter) { 96 | error!( 97 | "received unknown address as voter {}", 98 | hex::encode_upper(voter) 99 | ); 100 | } 101 | } 102 | 103 | self.updated_validators = HashSet::new(); 104 | self.current_height = height; 105 | self.current_proposer = Some(proposer.to_vec()); 106 | // note that we rely on voting power for a given round as informed by tendermint as opposed to 107 | // using the one tracked in self.validators. This is because the voting power on the informed round 108 | // may not be the same as the last known one (e.g. there could be staking changes already applied 109 | // to self.validators that will take some rounds before affecting the consensus voting). 110 | self.current_votes = votes; 111 | self.fees = BASELINE_BLOCK_REWARD; 112 | } 113 | 114 | /// Return whether is valid to apply the given validator update, e.g. 115 | /// there's enough voting power to unstake and the tendermint and aleo addresses 116 | /// the known mappings. This takes into account pending updates if any, so it's safe 117 | /// to use both during lightweight mempool checks (check_tx) and transaction delivery (deliver_tx). 118 | pub fn validate(&self, update: &Stake) -> Result<()> { 119 | if let Some(validator) = self.validators.get(&update.validator_address()) { 120 | // this is an already known validator, try to apply the staking update and see if it succeeds 121 | validator.clone().apply(update)?; 122 | } else { 123 | // this is a new validator 124 | Validator::from_stake(update)?; 125 | }; 126 | Ok(()) 127 | } 128 | 129 | /// Add or update the given validator and its voting power. 130 | /// Assumes this update has been validated previously with is_valid_update. 131 | pub fn apply(&mut self, update: Stake) { 132 | // mark as updated so its included in the pending updates result 133 | self.updated_validators.insert(update.validator_address()); 134 | 135 | // note that this could leave a validator with zero voting power, which will instruct 136 | // tendermint to remove it, but we still need to keep it around since we can receive 137 | // votes from that validator on subsequent rounds. 138 | self.validators 139 | .entry(update.validator_address()) 140 | .and_modify(|validator| { 141 | validator 142 | .apply(&update) 143 | .expect("attempted to apply an invalid update") 144 | }) 145 | .or_insert_with(|| { 146 | Validator::from_stake(&update).expect("attempted to apply an invalid update") 147 | }); 148 | } 149 | 150 | /// Add the given amount to the current block collected fees. 151 | pub fn collect(&mut self, fee: u64) { 152 | self.fees += fee; 153 | } 154 | 155 | /// Return the list of validators that have been updated by transactions in the current block. 156 | pub fn pending_updates(&self) -> Vec { 157 | self.updated_validators 158 | .iter() 159 | .fold(Vec::new(), |mut acc, address| { 160 | acc.push( 161 | self.validators 162 | .get(address) 163 | .expect("missing updated validator") 164 | .clone(), 165 | ); 166 | acc 167 | }) 168 | } 169 | 170 | /// Distributes the sum of the block fees plus some baseline block credits 171 | /// according to some rule, e.g. 50% for the proposer and 50% for validators 172 | /// weighted by their voting power (which is assumed to be proportional to its stake). 173 | /// If there are credits left because of rounding errors when dividing by voting power, 174 | /// they are assigned to the proposer. 175 | pub fn block_rewards(&self) -> Vec<(vm::Field, vm::EncryptedRecord)> { 176 | if let Some(proposer) = &self.current_proposer { 177 | // first calculate which part of the total belongs to voters 178 | let voter_reward_percentage = 100 - PROPOSER_REWARD_PERCENTAGE; 179 | let total_voter_reward = (self.fees * voter_reward_percentage) / 100; 180 | let total_voting_power = self 181 | .current_votes 182 | .iter() 183 | .fold(0, |accum, (_address, power)| accum + power); 184 | debug!( 185 | "total block rewards: {}, total voting power: {}, total voter rewards: {}", 186 | self.fees, total_voting_power, total_voter_reward 187 | ); 188 | 189 | // calculate how much belongs to each validator, proportional to its voting power 190 | let mut remaining_fees = self.fees; 191 | let mut rewards = HashMap::new(); 192 | for (address, voting_power) in &self.current_votes { 193 | let credits = (*voting_power * total_voter_reward) / total_voting_power; 194 | remaining_fees -= credits; 195 | rewards.insert(address, credits); 196 | } 197 | 198 | // What's left of the fees, goes to the proposer. 199 | // This should be roughly PROPOSER_REWARD_PERCENTAGE plus some leftover because 200 | // of rounding errors when distributing based on voting power above 201 | debug!( 202 | "{} is current round proposer", 203 | self.validators 204 | .get(proposer) 205 | .expect("proposer not found in address map") 206 | ); 207 | *rewards.entry(proposer).or_default() += remaining_fees; 208 | 209 | assert_eq!( 210 | self.fees, 211 | rewards.values().sum::(), 212 | "the sum of rewarded credits is different than the fees: {rewards:?}" 213 | ); 214 | 215 | // generate credits records based on the rewards 216 | let mut output_records = Vec::new(); 217 | for (address, credits) in rewards { 218 | let validator = self 219 | .validators 220 | .get(address) 221 | .expect("validator address not found"); 222 | 223 | debug!( 224 | "Assigning {credits} credits to {validator} (voting power {})", 225 | self.current_votes.get(address).unwrap_or(&0) 226 | ); 227 | 228 | let record = vm::mint_record( 229 | "credits.aleo", 230 | "credits", 231 | &validator.aleo_address, 232 | credits, 233 | self.current_height, 234 | ) 235 | .expect("Couldn't mint credit records for reward"); 236 | 237 | output_records.push(record); 238 | } 239 | 240 | output_records 241 | } else { 242 | warn!("no proposer on this round, skipping rewards"); 243 | Vec::new() 244 | } 245 | } 246 | 247 | /// Saves the currently known list of validators to disk. 248 | pub fn commit(&mut self) -> Result<()> { 249 | let validators_vec: Vec = self.validators.values().cloned().collect(); 250 | let json = serde_json::to_string(&validators_vec).expect("couldn't serialize validators"); 251 | std::fs::write(&self.path, json) 252 | .map_err(|e| anyhow!("failed to write validators file {:?} {e}", self.path)) 253 | } 254 | } 255 | 256 | #[cfg(test)] 257 | mod tests { 258 | use super::*; 259 | use assert_fs::NamedTempFile; 260 | use lib::vm; 261 | 262 | #[test] 263 | fn generate_rewards() { 264 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 265 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 266 | let tmint3 = "TtJ9B7yGXANFIJqH2LJO8JN6M2WOn2w7sRN0HHi14UE="; 267 | let tmint4 = "uHC9buPyVi5GT8dohO1OQ+HlfKQ1HwUHAyv3AjKKsZQ="; 268 | 269 | let aleo1 = account_keys(); 270 | let aleo2 = account_keys(); 271 | let aleo3 = account_keys(); 272 | let aleo4 = account_keys(); 273 | 274 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 1).unwrap(); 275 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 1).unwrap(); 276 | let validator3 = Validator::from_str(tmint3, &aleo3.1.to_string(), 1).unwrap(); 277 | let validator4 = Validator::from_str(tmint4, &aleo4.1.to_string(), 1).unwrap(); 278 | 279 | // create validator set, set validators with voting power 280 | let tempfile = NamedTempFile::new("validators").unwrap(); 281 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 282 | set.replace(vec![ 283 | validator1.clone(), 284 | validator2.clone(), 285 | validator3.clone(), 286 | validator4.clone(), 287 | ]); 288 | 289 | // tmint1 is proposer, tmint3 doesn't vote 290 | let mut votes = HashMap::new(); 291 | votes.insert(validator1.address(), 10); 292 | votes.insert(validator2.address(), 15); 293 | votes.insert(validator3.address(), 25); 294 | let voting_power = 10 + 15 + 25; 295 | set.begin_block(&validator1.address(), votes, 1); 296 | 297 | // add fees 298 | set.collect(20); 299 | set.collect(35); 300 | let fees = 20 + 35; 301 | 302 | // get rewards 303 | let records = set.block_rewards(); 304 | let rewards1 = decrypt_rewards(&aleo1, &records); 305 | let rewards2 = decrypt_rewards(&aleo2, &records); 306 | let rewards3 = decrypt_rewards(&aleo3, &records); 307 | let rewards4 = decrypt_rewards(&aleo4, &records); 308 | 309 | // check proposer gets 50% and the rest is distributed according to vote power 310 | let total_rewards = BASELINE_BLOCK_REWARD + fees; 311 | let voter_rewards = total_rewards * PROPOSER_REWARD_PERCENTAGE / 100; 312 | 313 | // ensure the no credits are lost in the process 314 | assert_eq!(total_rewards, rewards1 + rewards2 + rewards3); 315 | 316 | // non-proposers receive credits proportional to their voting power 317 | assert_eq!(voter_rewards * 15 / voting_power, rewards2); 318 | assert_eq!(voter_rewards * 25 / voting_power, rewards3); 319 | assert_eq!(0, rewards4); 320 | 321 | // proposer gets PROPOSER_REWARD_PERCENTAGE + a part proportional to their voting power + what's left because of rounding 322 | // so, basically, all the rest 323 | assert_eq!(total_rewards - rewards2 - rewards3, rewards1); 324 | 325 | // run another block with different votes, rewards start from scratch 326 | let mut votes = HashMap::new(); 327 | votes.insert(validator4.address(), 10); 328 | set.begin_block(&validator4.address(), votes, 2); 329 | set.collect(10); 330 | 331 | let records = set.block_rewards(); 332 | let rewards1 = decrypt_rewards(&aleo1, &records); 333 | let rewards2 = decrypt_rewards(&aleo2, &records); 334 | let rewards3 = decrypt_rewards(&aleo3, &records); 335 | let rewards4 = decrypt_rewards(&aleo4, &records); 336 | assert_eq!(0, rewards1); 337 | assert_eq!(0, rewards2); 338 | assert_eq!(0, rewards3); 339 | assert_eq!(BASELINE_BLOCK_REWARD + 10, rewards4); 340 | } 341 | 342 | #[test] 343 | fn current_proposer_hadnt_vote() { 344 | // the current round proposer for some reason may not have voted on the previous round 345 | // we've seen this happening at cluster start. This test exercises that case to make 346 | // sure we don't rely on the proposer address being included in the current votes 347 | 348 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 349 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 350 | let aleo1 = account_keys(); 351 | let aleo2 = account_keys(); 352 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 1).unwrap(); 353 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 1).unwrap(); 354 | 355 | // create validator set, set validators with voting power 356 | let tempfile = NamedTempFile::new("validators").unwrap(); 357 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 358 | set.replace(vec![validator1.clone(), validator2.clone()]); 359 | 360 | // tmint1 is proposer and didn't vote 361 | let mut votes = HashMap::new(); 362 | votes.insert(validator2.address(), 15); 363 | let voting_power = 15; 364 | set.begin_block(&validator1.address(), votes, 1); 365 | 366 | // add fees 367 | set.collect(35); 368 | let fees = 35; 369 | 370 | // get rewards 371 | let records = set.block_rewards(); 372 | let rewards1 = decrypt_rewards(&aleo1, &records); 373 | let rewards2 = decrypt_rewards(&aleo2, &records); 374 | 375 | // check proposer gets 50% and the rest is distributed according to vote power 376 | let total_rewards = BASELINE_BLOCK_REWARD + fees; 377 | let voter_rewards = total_rewards * PROPOSER_REWARD_PERCENTAGE / 100; 378 | 379 | // ensure the no credits are lost in the process 380 | assert_eq!(total_rewards, rewards1 + rewards2); 381 | 382 | // non-proposers receive credits proportional to their voting power 383 | assert_eq!(voter_rewards * 15 / voting_power, rewards2); 384 | assert_eq!(total_rewards - rewards2, rewards1); 385 | } 386 | 387 | #[test] 388 | #[allow(clippy::clone_on_copy)] 389 | fn rewards_are_deterministic() { 390 | // create 2 different validators with the same amounts 391 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 392 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 393 | let aleo1 = account_keys(); 394 | let aleo2 = account_keys(); 395 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 1).unwrap(); 396 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 1).unwrap(); 397 | let validators = vec![validator1.clone(), validator2.clone()]; 398 | 399 | let tempfile1 = NamedTempFile::new("validators").unwrap(); 400 | let tempfile2 = NamedTempFile::new("validators").unwrap(); 401 | let mut set1 = ValidatorSet::load_or_create(tempfile1.path()); 402 | let mut set2 = ValidatorSet::load_or_create(tempfile2.path()); 403 | set1.replace(validators.clone()); 404 | set2.replace(validators); 405 | 406 | let mut votes = HashMap::new(); 407 | votes.insert(validator1.address(), 10); 408 | votes.insert(validator2.address(), 15); 409 | set1.begin_block(&validator1.address(), votes.clone(), 1); 410 | set2.begin_block(&validator1.address(), votes.clone(), 1); 411 | set1.collect(100); 412 | set2.collect(100); 413 | 414 | let mut records11 = set1.block_rewards(); 415 | let mut records21 = set2.block_rewards(); 416 | records11.sort_by_key(|k| k.0.clone()); 417 | records21.sort_by_key(|k| k.0.clone()); 418 | 419 | // check that the records generated by both validators are the same 420 | // regardless of the nonce component of the records 421 | assert_eq!(records11, records21); 422 | 423 | // prepare another block with the same fees, verify that even though 424 | // the record amounts are the same, the records themselves are not 425 | set1.begin_block(&validator1.address(), votes.clone(), 2); 426 | set2.begin_block(&validator1.address(), votes.clone(), 2); 427 | set1.collect(100); 428 | set2.collect(100); 429 | 430 | let mut records12 = set1.block_rewards(); 431 | let mut records22 = set2.block_rewards(); 432 | records12.sort_by_key(|k| k.0.clone()); 433 | records22.sort_by_key(|k| k.0.clone()); 434 | 435 | // both validators see the same for this round 436 | assert_eq!(records12, records22); 437 | // but the records are not equal to the previous one 438 | assert_ne!(records11, records12); 439 | assert_ne!(records21, records22); 440 | 441 | // the gates inside the records are the same 442 | let rewards111 = decrypt_rewards(&aleo1, &records11); 443 | let rewards121 = decrypt_rewards(&aleo1, &records12); 444 | assert_eq!(rewards111, rewards121); 445 | } 446 | 447 | #[test] 448 | fn genesis_rewards() { 449 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 450 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 451 | let aleo1 = account_keys(); 452 | let aleo2 = account_keys(); 453 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 1).unwrap(); 454 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 1).unwrap(); 455 | 456 | // create validator set, set validators with voting power 457 | let tempfile = NamedTempFile::new("validators").unwrap(); 458 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 459 | set.replace(vec![validator1.clone(), validator2]); 460 | 461 | // in genesis there won't be any previous block votes 462 | let votes = HashMap::new(); 463 | set.begin_block(&validator1.address(), votes, 1); 464 | 465 | set.collect(20); 466 | set.collect(35); 467 | let fees = 20 + 35; 468 | 469 | let records = set.block_rewards(); 470 | let rewards1 = decrypt_rewards(&aleo1, &records); 471 | let rewards2 = decrypt_rewards(&aleo2, &records); 472 | let total_rewards = BASELINE_BLOCK_REWARD + fees; 473 | 474 | // proposer takes all 475 | assert_eq!(total_rewards, rewards1); 476 | assert_eq!(0, rewards2); 477 | } 478 | 479 | #[test] 480 | fn add_update_validators() { 481 | // create set and setup initial 2 validators 482 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 483 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 484 | let tmint3 = "TtJ9B7yGXANFIJqH2LJO8JN6M2WOn2w7sRN0HHi14UE="; 485 | let aleo1 = account_keys(); 486 | let aleo2 = account_keys(); 487 | let aleo3 = account_keys(); 488 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 1).unwrap(); 489 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 1).unwrap(); 490 | 491 | // create validator set, set validators with voting power 492 | let tempfile = NamedTempFile::new("validators").unwrap(); 493 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 494 | set.replace(vec![validator1.clone(), validator2]); 495 | 496 | // votes/begin block/commit 497 | let mut votes = HashMap::new(); 498 | votes.insert(validator1.address(), 15); 499 | set.begin_block(&validator1.address(), votes, 1); 500 | // no updates on this round (should ignore default ones from before begin block) 501 | assert_eq!(0, set.pending_updates().len()); 502 | let _records = set.block_rewards(); 503 | set.commit().unwrap(); 504 | 505 | // votes/begin block 506 | let mut votes = HashMap::new(); 507 | votes.insert(validator1.address(), 15); 508 | set.begin_block(&validator1.address(), votes, 1); 509 | 510 | // add a new validator, update voting power of a previous one 511 | let stake3 = Stake::new(tmint3, aleo3.1, 1).unwrap(); 512 | let stake2 = Stake::new(tmint2, aleo2.1, 5).unwrap(); 513 | set.apply(stake3.clone()); 514 | set.apply(stake2.clone()); 515 | 516 | // pending updates includes the two given 517 | let mut updates = set.pending_updates(); 518 | updates.sort_by_key(|v| v.voting_power); 519 | assert_eq!(2, updates.len()); 520 | assert_eq!(stake3.validator_address(), updates[0].address()); 521 | assert_eq!(1, updates[0].voting_power); 522 | assert_eq!(stake2.validator_address(), updates[1].address()); 523 | assert_eq!(6, updates[1].voting_power); 524 | 525 | let _records = set.block_rewards(); 526 | set.commit().unwrap(); 527 | } 528 | 529 | #[test] 530 | fn remove_validators() { 531 | // create set and setup initial 2 validators 532 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 533 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 534 | let aleo1 = account_keys(); 535 | let aleo2 = account_keys(); 536 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 5).unwrap(); 537 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 5).unwrap(); 538 | 539 | let tempfile = NamedTempFile::new("validators").unwrap(); 540 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 541 | set.replace(vec![validator1, validator2.clone()]); 542 | 543 | // votes/begin block 544 | let mut votes = HashMap::new(); 545 | votes.insert(validator2.address(), 5); 546 | set.begin_block(&validator2.address(), votes, 1); 547 | 548 | // remove stake but not enough to remove validator 549 | let stake2 = Stake::new(tmint2, aleo2.1, -3).unwrap(); 550 | set.apply(stake2.clone()); 551 | 552 | // pending updates includes the updated 553 | let updates = set.pending_updates(); 554 | assert_eq!(1, updates.len()); 555 | assert_eq!(stake2.validator_address(), updates[0].address()); 556 | assert_eq!(2, updates[0].voting_power); 557 | 558 | let _records = set.block_rewards(); 559 | set.commit().unwrap(); 560 | 561 | // votes/begin block 562 | let mut votes = HashMap::new(); 563 | votes.insert(validator2.address(), 5); 564 | set.begin_block(&validator2.address(), votes, 1); 565 | 566 | // remove remaining stake 567 | let stake2 = Stake::new(tmint2, aleo2.1, -2).unwrap(); 568 | set.apply(stake2.clone()); 569 | 570 | // pending updates includes the removed 571 | let updates = set.pending_updates(); 572 | assert_eq!(1, updates.len()); 573 | assert_eq!(stake2.validator_address(), updates[0].address()); 574 | assert_eq!(0, updates[0].voting_power); 575 | 576 | // get rewards check as expected, include removed 577 | let _records = set.block_rewards(); 578 | set.commit().unwrap(); 579 | 580 | // votes/begin block, shouldn't fail even if it includes votes from removed one 581 | let mut votes = HashMap::new(); 582 | votes.insert(validator2.address(), 5); 583 | set.begin_block(&validator2.address(), votes, 1); 584 | assert_eq!(0, set.pending_updates().len()); 585 | let _records = set.block_rewards(); 586 | set.commit().unwrap(); 587 | } 588 | 589 | #[test] 590 | fn validators_update_validations() { 591 | let tmint1 = "vM+mkdPMvplfxO7wM57z4FXy0TlBC2Onb+MaqcXE8ig="; 592 | let tmint2 = "2HWbuGk04WQm/CrI/0HxoEtjGY0DXp8oMY6RsyrWwbU="; 593 | let aleo1 = account_keys(); 594 | let aleo2 = account_keys(); 595 | let validator1 = Validator::from_str(tmint1, &aleo1.1.to_string(), 5).unwrap(); 596 | let validator2 = Validator::from_str(tmint2, &aleo2.1.to_string(), 5).unwrap(); 597 | 598 | let tempfile = NamedTempFile::new("validators").unwrap(); 599 | let mut set = ValidatorSet::load_or_create(tempfile.path()); 600 | let validators = vec![validator1, validator2]; 601 | set.replace(validators); 602 | 603 | // invalid when aleo address doesn't match previously known one 604 | let aleo2_fake = account_keys(); 605 | let validator2_fake = Stake::new(tmint2, aleo2_fake.1, 5).unwrap(); 606 | let error = set.validate(&validator2_fake).unwrap_err(); 607 | assert!(error 608 | .to_string() 609 | .contains("attempted to apply a staking update on a different aleo account")); 610 | 611 | // invalid on new one and negative voting 612 | let tmint3 = "TtJ9B7yGXANFIJqH2LJO8JN6M2WOn2w7sRN0HHi14UE="; 613 | let aleo3 = account_keys(); 614 | let validator3 = Stake::new(tmint3, aleo3.1, -5).unwrap(); 615 | let error = set.validate(&validator3).unwrap_err(); 616 | assert_eq!( 617 | "cannot create a validator with negative voting power", 618 | error.to_string() 619 | ); 620 | 621 | // invalid on zero power new 622 | let error = Stake::new(tmint3, aleo3.1, 0).unwrap_err(); 623 | assert_eq!("can't stake zero credits", error.to_string()); 624 | 625 | // invalid on negative voting more than available 626 | let validator2 = Stake::new(tmint2, aleo2.1, -6).unwrap(); 627 | let error = set.validate(&validator2).unwrap_err(); 628 | assert!(error 629 | .to_string() 630 | .contains("attempted to unstake more voting power than available")); 631 | } 632 | 633 | pub fn account_keys() -> (vm::ViewKey, vm::Address) { 634 | let private_key = vm::PrivateKey::new(&mut rand::thread_rng()).unwrap(); 635 | let view_key = vm::ViewKey::try_from(&private_key).unwrap(); 636 | let address = vm::Address::try_from(&view_key).unwrap(); 637 | (view_key, address) 638 | } 639 | 640 | fn decrypt_rewards( 641 | owner: &(vm::ViewKey, vm::Address), 642 | rewards: &[(vm::Field, vm::EncryptedRecord)], 643 | ) -> u64 { 644 | rewards 645 | .iter() 646 | .filter(|(_, record)| record.is_owner(&owner.1, &owner.0)) 647 | .fold(0, |acc, (_, record)| { 648 | let decrypted = record.decrypt(&owner.0).unwrap(); 649 | #[cfg(feature = "snarkvm_backend")] 650 | let gates = ***decrypted.gates(); 651 | #[cfg(feature = "lambdavm_backend")] 652 | let gates = decrypted.gates; 653 | acc + gates 654 | }) 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /src/client/account.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use lib::vm; 3 | use log::debug; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | /// File that stores the public and private keys associated with an account. 8 | /// Stores it at $ALEO_HOME/account.json, with ~/.aleo as the default ALEO_HOME. 9 | #[derive(Serialize, Deserialize)] 10 | pub struct Credentials { 11 | pub private_key: vm::PrivateKey, 12 | pub view_key: vm::ViewKey, 13 | pub address: vm::Address, 14 | } 15 | 16 | impl Credentials { 17 | pub fn new() -> Result { 18 | let private_key = vm::PrivateKey::new(&mut rand::thread_rng())?; 19 | let view_key = vm::ViewKey::try_from(&private_key)?; 20 | let address = vm::Address::try_from(&view_key)?; 21 | Ok(Self { 22 | private_key, 23 | view_key, 24 | address, 25 | }) 26 | } 27 | 28 | pub fn save(&self) -> Result { 29 | let file = Self::path(); 30 | let dir = file.parent().unwrap(); 31 | fs::create_dir_all(dir)?; 32 | debug!("Saving credentials to {}", file.to_string_lossy()); 33 | let account_json = serde_json::to_string(&self)?; 34 | fs::write(file.clone(), account_json)?; 35 | Ok(file) 36 | } 37 | 38 | pub fn load() -> Result { 39 | let account_json = fs::read_to_string(Self::path())?; 40 | serde_json::from_str(&account_json).map_err(|e| anyhow!(e)) 41 | } 42 | 43 | fn path() -> PathBuf { 44 | lib::aleo_home().join("account.json") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::{account, tendermint}; 2 | use anyhow::{anyhow, bail, Result}; 3 | use clap::Parser; 4 | use itertools::Itertools; 5 | use lib::program_file::ProgramFile; 6 | use lib::query::AbciQuery; 7 | use lib::transaction::Transaction; 8 | use lib::vm::{self, compute_serial_number}; 9 | #[allow(unused_imports)] 10 | use lib::vm::{EncryptedRecord, ProgramID}; 11 | use log::debug; 12 | use serde_json::json; 13 | use std::collections::HashSet; 14 | use std::fs; 15 | use std::path::PathBuf; 16 | use std::str::FromStr; 17 | 18 | #[derive(Debug, Parser)] 19 | pub enum Command { 20 | #[clap(subcommand)] 21 | Account(Account), 22 | #[clap(subcommand)] 23 | Credits(Credits), 24 | #[clap(subcommand)] 25 | Program(Program), 26 | #[clap(name = "get")] 27 | Get(Get), 28 | } 29 | 30 | /// Commands to manage accounts. 31 | #[derive(Debug, Parser)] 32 | pub enum Account { 33 | New, 34 | /// Fetches the unspent records owned by the given account. 35 | Records, 36 | /// Fetches the unspent records owned by the given account and calculates the final credits balance. 37 | Balance, 38 | } 39 | 40 | #[derive(Debug, Parser)] 41 | pub enum Credits { 42 | /// Transfer credtis to recipient_address from address that owns the input record 43 | Transfer { 44 | #[clap(value_parser=parse_input_record)] 45 | input_record: vm::UserInputValueType, 46 | #[clap(value_parser=parse_input_value)] 47 | recipient_address: vm::UserInputValueType, 48 | #[clap()] 49 | amount: u64, 50 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 51 | #[clap(long)] 52 | fee: Option, 53 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 54 | #[clap(long, value_parser=parse_input_record)] 55 | fee_record: Option, 56 | }, 57 | /// Split input record by amount 58 | Split { 59 | #[clap(value_parser=parse_input_record)] 60 | input_record: vm::UserInputValueType, 61 | amount: u64, 62 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 63 | #[clap(long)] 64 | fee: Option, 65 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 66 | #[clap(long, value_parser=parse_input_record)] 67 | fee_record: Option, 68 | }, 69 | /// Combine two records into one 70 | Combine { 71 | #[clap(value_parser=parse_input_record)] 72 | first_record: vm::UserInputValueType, 73 | #[clap(value_parser=parse_input_record)] 74 | second_record: vm::UserInputValueType, 75 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 76 | #[clap(long)] 77 | fee: Option, 78 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 79 | #[clap(long, value_parser=parse_input_record)] 80 | fee_record: Option, 81 | }, 82 | /// Take credits out from a credits record and stake them as a blockchain validator. This will execute a program and output a 83 | /// stake record that can be later used to reclaim the staked credits. 84 | Stake { 85 | /// The amount of gates to stake. 86 | #[clap()] 87 | amount: u64, 88 | /// The credits record to subtract the staked amount from. 89 | #[clap(value_parser=parse_input_record)] 90 | record: vm::UserInputValueType, 91 | /// The tendermint address of the validator that will stake the credits. 92 | #[clap()] 93 | validator: String, 94 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 95 | #[clap(long)] 96 | fee: Option, 97 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 98 | #[clap(long, value_parser=parse_input_record)] 99 | fee_record: Option, 100 | }, 101 | /// Take credits out of a stake record, reducing the voting power of the validator. 102 | Unstake { 103 | /// The amount of gates to unstake. Should at most what this validator has already staked. 104 | #[clap()] 105 | amount: u64, 106 | /// The stake record to recover the staked amount from. 107 | #[clap(value_parser=parse_input_record)] 108 | record: vm::UserInputValueType, 109 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 110 | #[clap(long)] 111 | fee: Option, 112 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 113 | #[clap(long, value_parser=parse_input_record)] 114 | fee_record: Option, 115 | }, 116 | } 117 | 118 | /// Commands to manage program transactions. 119 | #[derive(Debug, Parser)] 120 | pub enum Program { 121 | /// Builds and sends a deployment transaction to the Blockchain, returning the Transaction ID 122 | Deploy { 123 | /// Path where the aleo program file resides. 124 | #[clap(value_parser)] 125 | path: PathBuf, 126 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 127 | #[clap(long)] 128 | fee: Option, 129 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 130 | #[clap(long, value_parser=parse_input_record)] 131 | fee_record: Option, 132 | }, 133 | /// Runs locally and sends an execution transaction to the blockchain, returning the Transaction ID 134 | Execute { 135 | /// Program to execute (path or program_id). 136 | #[clap(value_parser)] 137 | program: String, 138 | /// The function name. 139 | #[clap(value_parser)] 140 | function: vm::Identifier, 141 | /// The function inputs. 142 | #[clap(value_parser=parse_input_value)] 143 | inputs: Vec, 144 | /// Amount of gates to pay as fee for this execution. If omitted not fee is paid. 145 | #[clap(long)] 146 | fee: Option, 147 | /// The record to use to subtract the fee amount. If omitted, the record with most gates in the account is used. 148 | #[clap(long, value_parser=parse_input_record)] 149 | fee_record: Option, 150 | /// Run the input code locally, generating the execution proof but without sending it over to the blockchain. Displays execution and decrypted records. 151 | #[clap(long, short, default_value_t = false)] 152 | dry_run: bool, 153 | }, 154 | /// Builds an .aleo program's keys and saves them to an .avm file 155 | Build { 156 | /// Path to the .aleo program to build 157 | #[clap(value_parser)] 158 | path: PathBuf, 159 | }, 160 | } 161 | 162 | /// Return the status of a Transaction: Type, whether it is committed to the ledger, and the program name. 163 | /// In the case of execution transactions, it also outputs the function's inputs and outputs. 164 | #[derive(Debug, Parser)] 165 | pub struct Get { 166 | /// Transaction ID from which to retrieve information 167 | #[clap(value_parser)] 168 | pub transaction_id: String, 169 | 170 | /// Whether to decrypt the incoming transaction private records 171 | #[clap(short, long, default_value_t = false)] 172 | pub decrypt: bool, 173 | } 174 | 175 | impl Command { 176 | pub async fn run(self, url: String) -> Result { 177 | let output = if let Command::Account(Account::New) = self { 178 | let credentials = account::Credentials::new()?; 179 | let path = credentials.save()?; 180 | 181 | json!({"path": path, "account": credentials}) 182 | } else { 183 | let credentials = 184 | account::Credentials::load().map_err(|_| anyhow!("credentials not found"))?; 185 | 186 | match self { 187 | Command::Account(Account::New) => { 188 | bail!("this shouldn't be reachable, the account new is a special case handled elsewhere") 189 | } 190 | Command::Account(Account::Balance) => { 191 | let balance = get_records(&credentials, &url).await?.iter().fold( 192 | 0, 193 | |acc, (_, _, record)| { 194 | #[cfg(feature = "snarkvm_backend")] 195 | let gates = ***record.gates(); 196 | #[cfg(feature = "lambdavm_backend")] 197 | let gates = record.gates; 198 | acc + gates 199 | }, 200 | ); 201 | 202 | json!({ "balance": balance }) 203 | } 204 | Command::Account(Account::Records) => { 205 | let records: Vec = get_records(&credentials, &url) 206 | .await? 207 | .iter() 208 | .map(|(commitment, ciphertext, plaintext)| { 209 | json!({ 210 | "commitment": commitment, 211 | "ciphertext": ciphertext, 212 | "record": plaintext 213 | }) 214 | }) 215 | .collect(); 216 | json!(&records) 217 | } 218 | Command::Program(Program::Deploy { 219 | path, 220 | fee, 221 | fee_record, 222 | }) => { 223 | let fee = choose_fee_record(&credentials, &url, &fee, &fee_record, &[]).await?; 224 | let transaction = 225 | Transaction::deployment(&path, &credentials.private_key, fee)?; 226 | let transaction_serialized = bincode::serialize(&transaction).unwrap(); 227 | tendermint::broadcast(transaction_serialized, &url).await?; 228 | json!(transaction) 229 | } 230 | Command::Program(Program::Execute { 231 | program, 232 | function, 233 | inputs, 234 | fee, 235 | fee_record, 236 | dry_run, 237 | }) => { 238 | let fee = 239 | choose_fee_record(&credentials, &url, &fee, &fee_record, &inputs).await?; 240 | let program = match get_program(&url, &program).await? { 241 | Some(program) => program, 242 | None => bail!("Could not find program {}", program), 243 | }; 244 | let transaction = Transaction::execution( 245 | program, 246 | function, 247 | &inputs, 248 | &credentials.private_key, 249 | fee, 250 | )?; 251 | 252 | let mut transaction_json = json!(transaction); 253 | if !dry_run { 254 | let mut transaction_json = json!(transaction); 255 | if !dry_run { 256 | let transaction_serialized = bincode::serialize(&transaction).unwrap(); 257 | tendermint::broadcast(transaction_serialized, &url).await?; 258 | } else { 259 | let records = Self::decrypt_records(&transaction, credentials); 260 | 261 | if !records.is_empty() { 262 | transaction_json 263 | .as_object_mut() 264 | .unwrap() 265 | .insert("decrypted_records".to_string(), json!(records)); 266 | } 267 | } 268 | } else { 269 | let records = Self::decrypt_records(&transaction, credentials); 270 | 271 | if !records.is_empty() { 272 | transaction_json 273 | .as_object_mut() 274 | .unwrap() 275 | .insert("decrypted_records".to_string(), json!(records)); 276 | } 277 | } 278 | json!(transaction_json) 279 | } 280 | Command::Program(Program::Build { path }) => { 281 | let program_source = std::fs::read_to_string(&path)?; 282 | let program_file = ProgramFile::build(&program_source)?; 283 | let output_path = path.with_extension("avm"); 284 | program_file.save(&output_path)?; 285 | json!({ "path": output_path }) 286 | } 287 | Command::Credits(Credits::Transfer { 288 | input_record, 289 | recipient_address, 290 | amount, 291 | fee, 292 | fee_record, 293 | }) => { 294 | let inputs = [ 295 | input_record.clone(), 296 | recipient_address.clone(), 297 | vm::u64_to_value(amount), 298 | ]; 299 | run_credits_command(&credentials, &url, "transfer", &inputs, &fee, &fee_record) 300 | .await? 301 | } 302 | Command::Credits(Credits::Combine { 303 | first_record, 304 | second_record, 305 | fee, 306 | fee_record, 307 | }) => { 308 | let inputs = [first_record.clone(), second_record.clone()]; 309 | run_credits_command(&credentials, &url, "combine", &inputs, &fee, &fee_record) 310 | .await? 311 | } 312 | Command::Credits(Credits::Split { 313 | input_record, 314 | amount, 315 | fee, 316 | fee_record, 317 | }) => { 318 | let inputs = [input_record.clone(), vm::u64_to_value(amount)]; 319 | run_credits_command(&credentials, &url, "split", &inputs, &fee, &fee_record) 320 | .await? 321 | } 322 | Command::Credits(Credits::Stake { 323 | amount, 324 | record, 325 | validator, 326 | fee, 327 | fee_record, 328 | }) => { 329 | let validator_split = 330 | Transaction::validator_key_as_u64s(&base64::decode(validator)?)?; 331 | 332 | let inputs = [ 333 | record.clone(), 334 | vm::u64_to_value(amount), 335 | vm::u64_to_value(validator_split[0]), 336 | vm::u64_to_value(validator_split[1]), 337 | vm::u64_to_value(validator_split[2]), 338 | vm::u64_to_value(validator_split[3]), 339 | ]; 340 | 341 | run_credits_command(&credentials, &url, "stake", &inputs, &fee, &fee_record) 342 | .await? 343 | } 344 | Command::Credits(Credits::Unstake { 345 | amount, 346 | record, 347 | fee, 348 | fee_record, 349 | }) => { 350 | let inputs = [record.clone(), vm::u64_to_value(amount)]; 351 | run_credits_command(&credentials, &url, "unstake", &inputs, &fee, &fee_record) 352 | .await? 353 | } 354 | Command::Get(Get { 355 | transaction_id, 356 | decrypt, 357 | }) => { 358 | let transaction = tendermint::get_transaction(&transaction_id, &url).await?; 359 | let transaction: Transaction = bincode::deserialize(&transaction)?; 360 | 361 | if !decrypt { 362 | json!(transaction) 363 | } else { 364 | let records = Self::decrypt_records(&transaction, credentials); 365 | 366 | json!({ 367 | "execution": transaction, 368 | "decrypted_records": records 369 | }) 370 | } 371 | } 372 | } 373 | }; 374 | 375 | Ok(output) 376 | } 377 | 378 | fn decrypt_records( 379 | transaction: &Transaction, 380 | credentials: account::Credentials, 381 | ) -> Vec { 382 | transaction 383 | .output_records() 384 | .iter() 385 | .filter(|(_commitment, record)| { 386 | record.is_owner(&credentials.address, &credentials.view_key) 387 | }) 388 | .filter_map(|(_commitment, record)| record.decrypt(&credentials.view_key).ok()) 389 | .collect() 390 | } 391 | } 392 | 393 | async fn run_credits_command( 394 | credentials: &account::Credentials, 395 | url: &str, 396 | function: &str, 397 | inputs: &[vm::UserInputValueType], 398 | fee_amount: &Option, 399 | fee_record: &Option, 400 | ) -> Result { 401 | let fee = choose_fee_record(credentials, url, fee_amount, fee_record, inputs).await?; 402 | let function_identifier = vm::Identifier::from_str(function)?; 403 | let transaction = 404 | Transaction::credits_execution(function_identifier, inputs, &credentials.private_key, fee)?; 405 | let transaction_serialized = bincode::serialize(&transaction).unwrap(); 406 | tendermint::broadcast(transaction_serialized, url).await?; 407 | Ok(json!(transaction)) 408 | } 409 | 410 | /// Extends the snarkvm's default argument parsing to support using record ciphertexts as record inputs 411 | fn parse_input_value(input: &str) -> Result { 412 | // try parsing an encrypted record string 413 | if input.starts_with("record") { 414 | return parse_input_record(input); 415 | } 416 | 417 | // %account is a syntactic sugar for current user address 418 | if input == "%account" { 419 | let credentials = account::Credentials::load()?; 420 | let address = credentials.address.to_string(); 421 | return vm::UserInputValueType::from_str(&address); 422 | } 423 | 424 | // try parsing a jsonified plaintext record 425 | if let Ok(record) = serde_json::from_str::(input) { 426 | return Ok(vm::UserInputValueType::Record(record)); 427 | } 428 | // otherwise fallback to parsing a snarkvm literal 429 | vm::UserInputValueType::from_str(input) 430 | } 431 | 432 | pub fn parse_input_record(input: &str) -> Result { 433 | let encrypted_record = vm::EncryptedRecord::from_str(input)?; 434 | 435 | let credentials = account::Credentials::load()?; 436 | encrypted_record 437 | .decrypt(&credentials.view_key) 438 | .map(vm::UserInputValueType::Record) 439 | } 440 | 441 | /// Retrieves all records from the blockchain, and only those that are correctly decrypted 442 | /// (i.e, are owned by the ssed credentials) and have not been spent are returned 443 | async fn get_records( 444 | credentials: &account::Credentials, 445 | url: &str, 446 | ) -> Result> { 447 | let get_records_response = tendermint::query(AbciQuery::GetRecords.into(), url).await?; 448 | let get_spent_records_response = 449 | tendermint::query(AbciQuery::GetSpentSerialNumbers.into(), url).await?; 450 | 451 | let records: Vec<(vm::Field, vm::EncryptedRecord)> = 452 | bincode::deserialize(&get_records_response)?; 453 | let spent_records: HashSet = bincode::deserialize(&get_spent_records_response)?; 454 | 455 | debug!("Records: {:?}", records); 456 | #[allow(clippy::clone_on_copy)] 457 | let records = records 458 | .into_iter() 459 | .filter_map(|(commitment, ciphertext)| { 460 | ciphertext 461 | .decrypt(&credentials.view_key) 462 | .map(|decrypted_record| (commitment.clone(), ciphertext, decrypted_record)) 463 | .ok() 464 | .filter(|(_, _ciphertext, _decrypted_record)| { 465 | let serial_number = compute_serial_number(credentials.private_key, commitment); 466 | serial_number.is_ok() && !spent_records.contains(&serial_number.unwrap()) 467 | }) 468 | }) 469 | .collect(); 470 | Ok(records) 471 | } 472 | 473 | /// Given a desired amount of fee to pay, find the record on this account with the biggest 474 | /// amount of gates that can be used to pay the fee, and that isn't already being used as 475 | /// an execution input. If a record is already provided, use that, otherwise select a default 476 | /// record from the account. 477 | async fn choose_fee_record( 478 | credentials: &account::Credentials, 479 | url: &str, 480 | amount: &Option, 481 | record: &Option, 482 | inputs: &[vm::UserInputValueType], 483 | ) -> Result> { 484 | if amount.is_none() { 485 | return Ok(None); 486 | } 487 | let amount = amount.unwrap(); 488 | 489 | if let Some(vm::UserInputValueType::Record(record_value)) = record { 490 | return Ok(Some((amount, record_value.clone()))); 491 | } 492 | 493 | let account_records: Vec = get_records(credentials, url) 494 | .await? 495 | .into_iter() 496 | .map(|(_, _, record)| record) 497 | .collect(); 498 | 499 | select_default_fee_record(amount, inputs, &account_records).map(|record| Some((amount, record))) 500 | } 501 | 502 | async fn get_program(url: &str, program: &str) -> Result> { 503 | match fs::read_to_string(PathBuf::from(program)) { 504 | Ok(program_string) => vm::generate_program(&program_string).map(Some), 505 | Err(_) => get_program_from_blockchain(url, ProgramID::from_str(program)?).await, 506 | } 507 | } 508 | 509 | async fn get_program_from_blockchain( 510 | url: &str, 511 | program_id: vm::ProgramID, 512 | ) -> Result> { 513 | let result = tendermint::query(AbciQuery::GetProgram { program_id }.into(), url).await?; 514 | let program: Option = bincode::deserialize(&result)?; 515 | Ok(program) 516 | } 517 | 518 | /// Select one of the records to be used to pay the requested fee, 519 | /// that is not already being used as input to the execution. 520 | /// The biggest record is chosen as the default under the assumption 521 | /// that choosing the best fit would lead to record fragmentation. 522 | fn select_default_fee_record( 523 | amount: u64, 524 | inputs: &[vm::UserInputValueType], 525 | account_records: &[vm::Record], 526 | ) -> Result { 527 | // save the input records to make sure that we don't use one of the other execution inputs as the fee 528 | let input_records: HashSet = inputs 529 | .iter() 530 | .filter_map(|value| { 531 | if let vm::UserInputValueType::Record(record) = value { 532 | Some(record.to_string()) 533 | } else { 534 | None 535 | } 536 | }) 537 | .collect(); 538 | 539 | account_records 540 | .iter() 541 | .sorted_by_key(|record| { 542 | #[cfg(feature = "snarkvm_backend")] 543 | let gates = ***record.gates(); 544 | #[cfg(feature = "lambdavm_backend")] 545 | let gates = record.gates; 546 | 547 | // negate to get bigger records first 548 | -(gates as i64) 549 | }) 550 | .find(|record| { 551 | #[cfg(feature = "snarkvm_backend")] 552 | let gates = ***record.gates(); 553 | #[cfg(feature = "lambdavm_backend")] 554 | let gates = record.gates; 555 | // note that here we require that the amount of the record be more than the requested fee 556 | // even though there may be implicit fees in the execution that make the actual amount to be subtracted 557 | // less that that amount, but since we don't have the execution transitions yet, we can't know at this point 558 | // so we make this stricter requirement. 559 | !input_records.contains(&record.to_string()) && gates >= amount 560 | }) 561 | .ok_or_else(|| { 562 | anyhow!("there are not records with enough credits for a {amount} gates fee") 563 | }) 564 | .cloned() 565 | } 566 | 567 | #[cfg(test)] 568 | mod tests { 569 | use super::*; 570 | use vm::Address; 571 | 572 | #[test] 573 | fn select_default_record() { 574 | let private_key = vm::PrivateKey::new(&mut rand::thread_rng()).unwrap(); 575 | let view_key = vm::ViewKey::try_from(&private_key).unwrap(); 576 | 577 | let record10 = mint_record(&view_key, 10); 578 | let record5 = mint_record(&view_key, 5); 579 | let record6 = mint_record(&view_key, 6); 580 | 581 | // if no records in account, fail 582 | let error = select_default_fee_record(10, &[], &[]).unwrap_err(); 583 | assert_eq!( 584 | "there are not records with enough credits for a 10 gates fee", 585 | error.to_string() 586 | ); 587 | 588 | // if several records but none big enough, fail 589 | let error = 590 | select_default_fee_record(10, &[], &[record5.clone(), record6.clone()]).unwrap_err(); 591 | assert_eq!( 592 | "there are not records with enough credits for a 10 gates fee", 593 | error.to_string() 594 | ); 595 | 596 | // if one record no input, choose it 597 | let result = select_default_fee_record(5, &[], &[record6.clone()]).unwrap(); 598 | assert_eq!(record6, result); 599 | 600 | // if one record but also input, fail 601 | let error = select_default_fee_record( 602 | 5, 603 | &[vm::UserInputValueType::Record(record6.clone())], 604 | &[record6.clone()], 605 | ) 606 | .unwrap_err(); 607 | assert_eq!( 608 | "there are not records with enough credits for a 5 gates fee", 609 | error.to_string() 610 | ); 611 | 612 | // if several records, choose the biggest one 613 | let result = select_default_fee_record( 614 | 5, 615 | &[], 616 | &[record5.clone(), record10.clone(), record6.clone()], 617 | ) 618 | .unwrap(); 619 | assert_eq!(record10, result); 620 | 621 | let result = select_default_fee_record( 622 | 5, 623 | &[vm::UserInputValueType::Record(record10.clone())], 624 | &[record5, record10, record6.clone()], 625 | ) 626 | .unwrap(); 627 | assert_eq!(record6, result); 628 | } 629 | 630 | fn mint_record(view_key: &vm::ViewKey, amount: u64) -> vm::Record { 631 | let address = Address::try_from(view_key).unwrap(); 632 | vm::mint_record("credits.aleo", "credits", &address, amount, 123) 633 | .unwrap() 634 | .1 635 | .decrypt(view_key) 636 | .unwrap() 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /src/client/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use serde_json::json; 3 | use tracing_subscriber::util::SubscriberInitExt; 4 | use tracing_subscriber::EnvFilter; 5 | 6 | mod account; 7 | mod commands; 8 | mod tendermint; 9 | 10 | /// Default tendermint url 11 | const LOCAL_BLOCKCHAIN_URL: &str = "http://127.0.0.1:26657"; 12 | 13 | #[derive(Debug, Parser)] 14 | #[clap()] 15 | pub struct Cli { 16 | /// Specify a subcommand. 17 | #[clap(subcommand)] 18 | pub command: commands::Command, 19 | 20 | /// Output log lines to stdout based on the desired log level (RUST_LOG env var). 21 | #[clap(short, long, global = false, default_value_t = false)] 22 | pub verbose: bool, 23 | 24 | /// tendermint node url 25 | #[clap(short, long, env = "BLOCKCHAIN_URL", default_value = LOCAL_BLOCKCHAIN_URL)] 26 | pub url: String, 27 | } 28 | 29 | #[tokio::main()] 30 | async fn main() { 31 | let cli = Cli::parse(); 32 | 33 | if cli.verbose { 34 | tracing_subscriber::fmt() 35 | // Use a more compact, abbreviated log format 36 | .compact() 37 | .with_env_filter(EnvFilter::from_default_env()) 38 | // Build and init the subscriber 39 | .finish() 40 | .init(); 41 | } 42 | 43 | let (exit_code, output) = match cli.command.run(cli.url).await { 44 | Ok(output) => (0, output), 45 | Err(err) => (1, json!({"error": err.to_string()})), 46 | }; 47 | 48 | println!("{output:#}"); 49 | std::process::exit(exit_code); 50 | } 51 | -------------------------------------------------------------------------------- /src/client/tendermint.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, ensure, Result}; 2 | use log::debug; 3 | use tendermint_rpc::query::Query; 4 | use tendermint_rpc::{Client, HttpClient, Order}; 5 | 6 | pub async fn get_transaction(tx_id: &str, url: &str) -> Result> { 7 | let client = HttpClient::new(url)?; 8 | // todo: this index key might have to be a part of the shared lib so that both the CLI and the ABCI can be in sync 9 | let query = Query::contains("app.tx_id", tx_id); 10 | 11 | let response = client 12 | .tx_search(query, false, 1, 1, Order::Ascending) 13 | .await?; 14 | 15 | // early return with error if no transaction has been indexed for that tx id 16 | ensure!( 17 | response.total_count > 0, 18 | "Transaction ID {} is invalid or has not yet been committed to the blockchain", 19 | tx_id 20 | ); 21 | 22 | let tx_bytes: Vec = response.txs.into_iter().next().unwrap().tx.into(); 23 | 24 | Ok(tx_bytes) 25 | } 26 | 27 | pub async fn broadcast(transaction: Vec, url: &str) -> Result<()> { 28 | let client = HttpClient::new(url).unwrap(); 29 | 30 | let tx: tendermint::abci::Transaction = transaction.into(); 31 | 32 | let response = client.broadcast_tx_sync(tx).await?; 33 | 34 | debug!("Response from CheckTx: {:?}", response); 35 | match response.code { 36 | tendermint::abci::Code::Ok => Ok(()), 37 | tendermint::abci::Code::Err(code) => { 38 | bail!("Error executing transaction {}: {}", code, response.log) 39 | } 40 | } 41 | } 42 | 43 | pub async fn query(query: Vec, url: &str) -> Result> { 44 | let client = HttpClient::new(url).unwrap(); 45 | 46 | let response = client.abci_query(None, query, None, true).await?; 47 | 48 | debug!("Response from Query: {:?}", response); 49 | match response.code { 50 | tendermint::abci::Code::Ok => Ok(response.value), 51 | tendermint::abci::Code::Err(code) => { 52 | bail!("Error executing transaction {}: {}", code, response.log) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | pub mod program_file; 4 | pub mod query; 5 | pub mod transaction; 6 | pub mod validator; 7 | pub mod vm; 8 | 9 | /// Directory to store aleo related files (e.g. account, cached programs). Typically ~/.aleo/ 10 | pub fn aleo_home() -> PathBuf { 11 | std::env::var("ALEO_HOME") 12 | .map(|path| PathBuf::from_str(&path).unwrap()) 13 | .unwrap_or_else(|_| dirs::home_dir().unwrap().join(".aleo")) 14 | } 15 | 16 | /// Get the credits program. This is a special built-in program of the system, which contains 17 | /// functions to move aleo money. Since it's required for most uses in clients and servers, it's 18 | /// cached to only be built once. 19 | pub fn load_credits() -> (vm::Program, vm::ProgramBuild) { 20 | // TODO: move this to lambdaVM-specific module or to the crate 21 | // currently, lambda VM does not check whether the params are created on disk before using them 22 | // so if they do not exist, make sure they are generated 23 | #[cfg(feature = "lambdavm_backend")] 24 | vm::ensure_srs_file_exists().expect("Error reading or creating Universal SRS file"); 25 | 26 | // try to fetch from cache 27 | let cache_path = aleo_home().join("cache/credits.avm"); 28 | if let Ok(program) = program_file::ProgramFile::load(&cache_path) { 29 | log::debug!("found credits program in {cache_path:?}"); 30 | return program; 31 | } 32 | 33 | // else build keys and cache for future use 34 | log::debug!("cached credits not found, building and saving to {cache_path:?}"); 35 | let source = include_str!("../../aleo/credits.aleo"); 36 | let file = program_file::ProgramFile::build(source).expect("couldn't build credits program"); 37 | std::fs::create_dir_all(aleo_home().join("cache")).expect("couldn't create cache dir"); 38 | file.save(&cache_path) 39 | .expect("couldn't save credits program"); 40 | 41 | (file.program, file.keys) 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/program_file.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::vm; 4 | use anyhow::{anyhow, Result}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// This helper struct provides methods to dump programs and their proving/verifying keys into 8 | /// files to support vm "built-in" programs, i.e. programs that come already built and can be 9 | /// shared between the network and clients without extra work, like the credits program. 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct ProgramFile { 12 | pub program: vm::Program, 13 | pub keys: vm::ProgramBuild, 14 | } 15 | 16 | impl ProgramFile { 17 | pub fn build(program_source: &str) -> Result { 18 | let (program, keys) = vm::build_program(program_source)?; 19 | 20 | Ok(Self { program, keys }) 21 | } 22 | 23 | pub fn save(&self, output_path: &Path) -> Result<()> { 24 | let json = serde_json::to_string(self)?; 25 | std::fs::write(output_path, json).map_err(|e| anyhow!(e)) 26 | } 27 | 28 | pub fn load(path: &Path) -> Result<(vm::Program, vm::ProgramBuild)> { 29 | let json = std::fs::read_to_string(path) 30 | .map_err(|e| anyhow!("couldn't find stored program: {e}"))?; 31 | let stored: Self = serde_json::from_str(&json)?; 32 | Ok((stored.program, stored.keys)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/query.rs: -------------------------------------------------------------------------------- 1 | use crate::vm::ProgramID; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Serialize, Deserialize, Debug)] 5 | pub enum AbciQuery { 6 | /// Returns all records's ciphertexts from the blockchain 7 | GetRecords, 8 | /// Returns all spent records's serial numbers 9 | GetSpentSerialNumbers, 10 | /// Returns the program struct given it's id 11 | GetProgram { program_id: ProgramID }, 12 | } 13 | 14 | impl From for Vec { 15 | fn from(q: AbciQuery) -> Vec { 16 | // bincoding an enum should not fail ever so unwrap() here should be fine 17 | bincode::serialize(&q).unwrap() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::load_credits; 2 | use crate::validator; 3 | use crate::vm::{self, VerifyingKeyMap}; 4 | use anyhow::{anyhow, ensure, Result}; 5 | use itertools::Itertools; 6 | use log::debug; 7 | use serde::{Deserialize, Serialize}; 8 | use sha2::{Digest, Sha256}; 9 | use std::fs; 10 | use std::path::Path; 11 | use std::str::FromStr; 12 | 13 | #[derive(Clone, Serialize, Deserialize, Debug)] 14 | pub enum Transaction { 15 | Deployment { 16 | id: String, 17 | program: Box, 18 | verifying_keys: vm::VerifyingKeyMap, 19 | fee: Option, 20 | }, 21 | Execution { 22 | id: String, 23 | transitions: Vec, 24 | }, 25 | } 26 | 27 | impl Transaction { 28 | // Used to generate deployment of a new program in path 29 | pub fn deployment( 30 | path: &Path, 31 | private_key: &vm::PrivateKey, 32 | fee: Option<(u64, vm::Record)>, 33 | ) -> Result { 34 | let program_string = fs::read_to_string(path)?; 35 | debug!("Deploying program {}", program_string); 36 | 37 | // generate program keys (proving and verifying) and keep the verifying one for the deploy 38 | let (program, program_build) = vm::build_program(&program_string)?; 39 | 40 | let verifying_keys = program_build 41 | .map 42 | .into_iter() 43 | .map(|(i, keys)| (i, keys.1)) 44 | .collect(); 45 | 46 | let fee = Self::execute_fee(private_key, fee, 0)?; 47 | 48 | Transaction::Deployment { 49 | id: "not known yet".to_string(), 50 | fee, 51 | program: Box::new(program), 52 | verifying_keys: VerifyingKeyMap { 53 | map: verifying_keys, 54 | }, 55 | } 56 | .set_hashed_id() 57 | } 58 | 59 | // Used to generate an execution of a program in path or an execution of the credits program 60 | pub fn execution( 61 | program: vm::Program, 62 | function_name: vm::Identifier, 63 | inputs: &[vm::UserInputValueType], 64 | private_key: &vm::PrivateKey, 65 | requested_fee: Option<(u64, vm::Record)>, 66 | ) -> Result { 67 | let mut transitions = vm::execution(program, function_name, inputs, private_key, None)?; 68 | 69 | // some amount of fees may be implicit if the execution drops credits. in that case, those credits are 70 | // subtracted from the fees that were requested to be paid. 71 | let implicit_fees = transitions.iter().map(|transition| transition.fee()).sum(); 72 | if let Some(transition) = Self::execute_fee(private_key, requested_fee, implicit_fees)? { 73 | transitions.push(transition); 74 | } 75 | 76 | Self::Execution { 77 | id: "not known yet".to_string(), 78 | transitions, 79 | } 80 | .set_hashed_id() 81 | } 82 | 83 | pub fn credits_execution( 84 | function_name: vm::Identifier, 85 | inputs: &[vm::UserInputValueType], 86 | private_key: &vm::PrivateKey, 87 | requested_fee: Option<(u64, vm::Record)>, 88 | ) -> Result { 89 | let mut transitions = 90 | Self::execute_credits(&function_name.to_string(), inputs, private_key)?; 91 | 92 | // some amount of fees may be implicit if the execution drops credits. in that case, those credits are 93 | // subtracted from the fees that were requested to be paid. 94 | let implicit_fees = transitions.iter().map(|transition| transition.fee()).sum(); 95 | if let Some(transition) = Self::execute_fee(private_key, requested_fee, implicit_fees)? { 96 | transitions.push(transition); 97 | } 98 | 99 | Self::Execution { 100 | id: "not known yet".to_string(), 101 | transitions, 102 | } 103 | .set_hashed_id() 104 | } 105 | 106 | pub fn id(&self) -> &str { 107 | match self { 108 | Transaction::Deployment { id, .. } => id, 109 | Transaction::Execution { id, .. } => id, 110 | } 111 | } 112 | 113 | pub fn output_records(&self) -> Vec<(vm::Field, vm::EncryptedRecord)> { 114 | #[cfg(feature = "snarkvm_backend")] 115 | return self 116 | .transitions() 117 | .iter() 118 | .flat_map(|transition| transition.output_records()) 119 | .map(|(commitment, record)| (*commitment, record.clone())) 120 | .collect(); 121 | 122 | #[cfg(feature = "lambdavm_backend")] 123 | return self 124 | .transitions() 125 | .iter() 126 | .flat_map(|transition| transition.output_records()) 127 | .map(|(commitment, record)| (commitment, record)) 128 | .collect(); 129 | } 130 | 131 | /// If the transaction is an execution, return the list of input record serial numbers 132 | pub fn record_serial_numbers(&self) -> Vec { 133 | #[cfg(feature = "snarkvm_backend")] 134 | return self 135 | .transitions() 136 | .iter() 137 | .flat_map(|transition| transition.serial_numbers().copied()) 138 | .collect(); 139 | 140 | #[cfg(feature = "lambdavm_backend")] 141 | return self 142 | .transitions() 143 | .iter() 144 | .flat_map(|transition| transition.serial_numbers()) 145 | .collect(); 146 | } 147 | 148 | fn transitions(&self) -> Vec { 149 | match self { 150 | Transaction::Deployment { fee, .. } => { 151 | if let Some(transition) = fee { 152 | vec![transition.clone()] 153 | } else { 154 | vec![] 155 | } 156 | } 157 | Transaction::Execution { transitions, .. } => transitions.clone(), 158 | } 159 | } 160 | 161 | /// Return the sum of the transition fees contained in this transition. 162 | /// For deployments it's the fee of the fee specific transition, if present. 163 | /// For executions, it's the sum of the fees of all the execution transitions. 164 | pub fn fees(&self) -> i64 { 165 | match self { 166 | Transaction::Deployment { fee, .. } => { 167 | fee.as_ref().map_or(0, |transition| *transition.fee()) 168 | } 169 | Transaction::Execution { transitions, .. } => transitions 170 | .iter() 171 | .fold(0, |acc, transition| acc + transition.fee()), 172 | } 173 | } 174 | 175 | /// Extract a list of validator updates that result from the current execution. 176 | /// This will return a non-empty vector in case some of the transitions are of the 177 | /// stake or unstake functions in the credits program. 178 | pub fn stake_updates(&self) -> Result> { 179 | let mut result = Vec::new(); 180 | if let Self::Execution { transitions, .. } = self { 181 | for transition in transitions { 182 | if transition.program_id().to_string() == "credits.aleo" { 183 | let extract_output = |index: usize| { 184 | transition 185 | .outputs() 186 | .get(index) 187 | .ok_or_else(|| anyhow!("couldn't find staking output in transition")) 188 | }; 189 | 190 | let amount = match transition.function_name().to_string().as_str() { 191 | "stake" => vm::int_from_output::(extract_output(2)?)? as i64, 192 | "unstake" => -(vm::int_from_output::(extract_output(2)?)? as i64), 193 | _ => continue, 194 | }; 195 | 196 | // TODO: Factor out the following extraction and test it as with the original conversion 197 | 198 | let validator_key: [u64; 4] = [ 199 | vm::int_from_output(extract_output(4)?)?, 200 | vm::int_from_output(extract_output(5)?)?, 201 | vm::int_from_output(extract_output(6)?)?, 202 | vm::int_from_output(extract_output(7)?)?, 203 | ]; 204 | 205 | let validator = Transaction::validator_key_from_u64s(&validator_key)?; 206 | 207 | let aleo_address = vm::address_from_output(extract_output(3)?)?; 208 | let validator = validator::Stake::new(&validator, aleo_address, amount)?; 209 | 210 | result.push(validator); 211 | } 212 | } 213 | } 214 | Ok(result) 215 | } 216 | 217 | /// If there is some required fee, return the transition resulting of executing 218 | /// the fee function of the credits program for the requested amount. 219 | /// The fee function just burns the desired amount of credits, so its effect is just 220 | /// to produce a difference between the input/output records of its transition. 221 | fn execute_fee( 222 | private_key: &vm::PrivateKey, 223 | requested_fee: Option<(u64, vm::Record)>, 224 | implicit_fee: i64, 225 | ) -> Result> { 226 | if let Some((gates, record)) = requested_fee { 227 | ensure!( 228 | implicit_fee >= 0, 229 | "execution produced a negative fee, cannot create credits" 230 | ); 231 | 232 | if implicit_fee > gates as i64 { 233 | // already covered by implicit fee, don't spend the record 234 | return Ok(None); 235 | } 236 | 237 | let gates = gates as i64 - implicit_fee; 238 | #[cfg(feature = "lambdavm_backend")] 239 | let inputs = [ 240 | vm::UserInputValueType::Record(crate::vm::Record { 241 | owner: record.owner, 242 | gates: record.gates, 243 | data: record.data, 244 | nonce: record.nonce, 245 | }), 246 | // TODO: Revisit the cast below. 247 | vm::UserInputValueType::U64(gates as u64), 248 | ]; 249 | 250 | #[cfg(feature = "snarkvm_backend")] 251 | let inputs = [ 252 | vm::UserInputValueType::Record(record), 253 | vm::UserInputValueType::from_str(&format!("{gates}u64"))?, 254 | ]; 255 | 256 | let transitions = Self::execute_credits("fee", &inputs, private_key)?; 257 | Ok(Some(transitions.first().unwrap().clone())) 258 | } else { 259 | Ok(None) 260 | } 261 | } 262 | 263 | fn execute_credits( 264 | function: &str, 265 | inputs: &[vm::UserInputValueType], 266 | private_key: &vm::PrivateKey, 267 | ) -> Result> { 268 | let function = vm::Identifier::from_str(function)?; 269 | let (program, keys) = load_credits(); 270 | let (proving_key, _) = keys 271 | .map 272 | .get(&function) 273 | .ok_or_else(|| anyhow!("credits function not found"))?; 274 | 275 | vm::execution( 276 | program, 277 | function, 278 | inputs, 279 | private_key, 280 | Some(proving_key.clone()), 281 | ) 282 | } 283 | 284 | /// Verify that the transaction id is consistent with its contents, by checking it's sha256 hash. 285 | pub fn verify(&self) -> Result<()> { 286 | ensure!( 287 | self.id() == self.hash()?, 288 | "Corrupted transaction: Inconsistent transaction id" 289 | ); 290 | 291 | Ok(()) 292 | } 293 | 294 | /// Hash the contents of the given enum and return it with the hash as its id. 295 | fn set_hashed_id(mut self) -> Result { 296 | let new_id = self.hash()?; 297 | match self { 298 | Transaction::Deployment { ref mut id, .. } => *id = new_id, 299 | Transaction::Execution { ref mut id, .. } => *id = new_id, 300 | }; 301 | Ok(self) 302 | } 303 | 304 | /// Calculate a sha256 hash of the contents of the transaction (dependent on the transaction type) 305 | fn hash(&self) -> Result { 306 | let mut hasher = Sha256::new(); 307 | 308 | let variant_code: u8 = match self { 309 | Transaction::Deployment { .. } => 0, 310 | Transaction::Execution { .. } => 1, 311 | }; 312 | hasher.update(variant_code.to_be_bytes()); 313 | 314 | match self { 315 | Transaction::Deployment { 316 | id: _id, 317 | program, 318 | verifying_keys, 319 | fee, 320 | } => { 321 | hasher.update(program.id().to_string()); 322 | 323 | for (key, value) in verifying_keys.map.clone().into_iter() { 324 | hasher.update(key.to_string()); 325 | #[cfg(feature = "snarkvm_backend")] 326 | let serialization = serde_json::to_string(&value)?; 327 | #[cfg(feature = "lambdavm_backend")] 328 | let serialization = lambdavm::serialize_verifying_key(value)?; 329 | hasher.update(serialization); 330 | } 331 | 332 | if let Some(fee) = fee { 333 | hasher.update(serde_json::to_string(fee)?); 334 | } 335 | } 336 | Transaction::Execution { 337 | id: _id, 338 | transitions, 339 | } => { 340 | for transition in transitions.iter() { 341 | hasher.update(serde_json::to_string(transition)?); 342 | } 343 | } 344 | } 345 | 346 | let hash = hasher.finalize().as_slice().to_owned(); 347 | Ok(hex::encode(hash)) 348 | } 349 | 350 | // TODO: Move this to validator set/use tendermint-rs structs for pub keys? 351 | pub fn validator_key_as_u128s(bytes: &[u8]) -> Result<(u128, u128)> { 352 | ensure!( 353 | bytes.len() == 32, 354 | "Input validator address is not 32 bytes long" 355 | ); 356 | let high_part: [u8; 16] = bytes[0..16].try_into()?; 357 | let low_part: [u8; 16] = bytes[16..].try_into()?; 358 | 359 | Ok(( 360 | u128::from_be_bytes(high_part), 361 | u128::from_be_bytes(low_part), 362 | )) 363 | } 364 | 365 | pub fn validator_key_from_u128s(higher: u128, lower: u128) -> Result { 366 | let mut address = higher.to_be_bytes().to_vec(); 367 | 368 | address.append(&mut lower.to_be_bytes().to_vec()); 369 | Ok(base64::encode(address)) 370 | } 371 | 372 | /// Returns a slice of 32 bytes (the size of a Tendermint Public Key) as 4 sections of `u64`s 373 | /// where the order of the `u64s` is from the most significant to the least significant 374 | pub fn validator_key_as_u64s(bytes: &[u8]) -> Result> { 375 | ensure!( 376 | bytes.len() == 32, 377 | "Input validator address is not 32 bytes long" 378 | ); 379 | 380 | let sections: Vec = bytes 381 | .chunks_exact(8) 382 | .map(|x| u64::from_be_bytes(x.try_into().expect("error converting address into u64s"))) 383 | .collect(); 384 | 385 | ensure!( 386 | sections.len() == 4, 387 | "Input validator address was incorrectly converted" 388 | ); 389 | 390 | Ok(sections) 391 | } 392 | 393 | /// Returns a Tendermint Public Key from a slice of 4 `u64`s, where the first `u64` 394 | /// corresponds to the most significant section of bytes and the order of significance 395 | /// is descending 396 | pub fn validator_key_from_u64s(sections: &[u64]) -> Result { 397 | ensure!( 398 | sections.len() == 4, 399 | "Input validator address does not have 4 sections" 400 | ); 401 | 402 | let sections = sections.iter().flat_map(|x| x.to_be_bytes()).collect_vec(); 403 | Ok(base64::encode(sections)) 404 | } 405 | } 406 | 407 | impl std::fmt::Display for Transaction { 408 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 409 | match self { 410 | Transaction::Deployment { id, program, .. } => { 411 | write!(f, "Deployment({},{})", id, program.id()) 412 | } 413 | Transaction::Execution { id, transitions } => { 414 | let transition = transitions.first().unwrap(); 415 | write!(f, "Execution({},{id})", transition.program_id()) 416 | } 417 | } 418 | } 419 | } 420 | 421 | #[cfg(test)] 422 | mod tests { 423 | use crate::transaction::Transaction; 424 | 425 | #[test] 426 | fn convert_validator_address_u128() { 427 | let pub_key = "KvYujhwQVoCOH1B3FrmtjSN5GgKUjarOKDNIbWfA8hc="; 428 | let key_encoded = base64::decode(pub_key).unwrap(); 429 | let (h, l) = Transaction::validator_key_as_u128s(&key_encoded).unwrap(); 430 | assert!(h == 57105825100092210844007095251039268237u128); 431 | assert!(l == 47151775319435836265997973510082851351u128); 432 | 433 | assert!(Transaction::validator_key_from_u128s(h, l).unwrap() == pub_key); 434 | } 435 | 436 | #[test] 437 | fn convert_validator_address_u64() { 438 | let pub_key = "KvYujhwQVoCOH1B3FrmtjSN5GgKUjarOKDNIbWfA8hc="; 439 | let key_encoded = base64::decode(pub_key).unwrap(); 440 | let key_sections = Transaction::validator_key_as_u64s(&key_encoded).unwrap(); 441 | 442 | let expected_slice = [ 443 | 3095712981754861184u64, 444 | 10240992550076394893u64, 445 | 2556102861894036174u64, 446 | 2896738620058694167u64, 447 | ]; 448 | 449 | assert_eq!(key_sections, expected_slice); 450 | assert!(Transaction::validator_key_from_u64s(&key_sections).unwrap() == pub_key); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/lib/validator.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::{anyhow, ensure, Result}; 4 | use log::debug; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::vm; 8 | 9 | pub type VotingPower = u64; 10 | pub type Address = Vec; 11 | 12 | /// Represents a validator node in the blockchain with a given voting power for the consensus 13 | /// protocol. Each validator has an associated tendermint public key and an aleo account. 14 | #[derive(Debug, Deserialize, Serialize, Clone)] 15 | pub struct Validator { 16 | pub aleo_address: vm::Address, 17 | pub pub_key: tendermint::PublicKey, 18 | pub voting_power: VotingPower, 19 | } 20 | 21 | /// Represents an amount of credits (positive or negative) that are staked on a specific validator. 22 | #[derive(Debug, Deserialize, Serialize, Clone)] 23 | pub struct Stake { 24 | aleo_address: vm::Address, 25 | pub_key: tendermint::PublicKey, 26 | gates_delta: i64, 27 | } 28 | 29 | #[derive(Deserialize, Serialize)] 30 | pub struct GenesisState { 31 | pub records: Vec<(vm::Field, vm::EncryptedRecord)>, 32 | pub validators: Vec, 33 | } 34 | 35 | impl Validator { 36 | /// Construct a new validator update from a base64 encoded ed25519 public key string (as it appears in tendermint JSON files) 37 | /// And an Aleo address string. 38 | pub fn from_str(pub_key: &str, aleo_address: &str, voting_power: VotingPower) -> Result { 39 | let aleo_address = vm::Address::from_str(aleo_address)?; 40 | Ok(Self { 41 | pub_key: parse_pub_key(pub_key)?, 42 | aleo_address, 43 | voting_power, 44 | }) 45 | } 46 | 47 | /// Instantiate a validator from the given initial stake, which should be positive. 48 | pub fn from_stake(stake: &Stake) -> Result { 49 | ensure!( 50 | stake.gates_delta > 0, 51 | "cannot create a validator with negative voting power" 52 | ); 53 | Ok(Self { 54 | aleo_address: stake.aleo_address, 55 | pub_key: stake.pub_key, 56 | voting_power: stake.gates_delta as u64, 57 | }) 58 | } 59 | 60 | /// Update the validator voting power based on the given change in stake. 61 | /// It will fail if the stake belongs to a different validator or if more stake than 62 | /// available is attempted to be removed. 63 | pub fn apply(&mut self, stake: &Stake) -> Result<()> { 64 | ensure!( 65 | self.address() == stake.validator_address(), 66 | "attempted to apply a staking update on a different validator. expected {} received {}", 67 | self, 68 | stake 69 | ); 70 | 71 | ensure!(self.aleo_address == stake.aleo_address, 72 | "attempted to apply a staking update on a different aleo account. expected {} received {}", 73 | self.aleo_address, stake.aleo_address); 74 | 75 | let new_power = self.voting_power as i64 + stake.gates_delta; 76 | ensure!( 77 | new_power >= 0, 78 | "attempted to unstake more voting power than available for {self}" 79 | ); 80 | self.voting_power = new_power as u64; 81 | 82 | Ok(()) 83 | } 84 | 85 | /// Return the tendermint validator address (which is derived from its public key) as bytes. 86 | pub fn address(&self) -> Address { 87 | pub_key_to_address(&self.pub_key) 88 | } 89 | } 90 | 91 | impl Stake { 92 | /// Construct a stake of a given amount (positive or negative) for a specific validator. 93 | /// identified by its base64 encoded ed25519 public key string and aleo address. 94 | pub fn new(pub_key: &str, aleo_address: vm::Address, gates_delta: i64) -> Result { 95 | ensure!(gates_delta != 0, "can't stake zero credits"); 96 | Ok(Self { 97 | pub_key: parse_pub_key(pub_key)?, 98 | aleo_address, 99 | gates_delta, 100 | }) 101 | } 102 | 103 | /// Return the tendermint validator address (which is derived from its public key) as bytes. 104 | pub fn validator_address(&self) -> Address { 105 | pub_key_to_address(&self.pub_key) 106 | } 107 | } 108 | 109 | impl std::hash::Hash for Validator { 110 | fn hash(&self, state: &mut H) { 111 | state.write(&self.address()) 112 | } 113 | } 114 | 115 | impl std::fmt::Display for Validator { 116 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | write!( 118 | f, 119 | "{}/{}", 120 | hex::encode_upper(self.address()), 121 | self.aleo_address 122 | ) 123 | } 124 | } 125 | 126 | impl std::fmt::Display for Stake { 127 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 128 | write!( 129 | f, 130 | "{}/{}", 131 | hex::encode_upper(self.validator_address()), 132 | self.aleo_address 133 | ) 134 | } 135 | } 136 | 137 | fn parse_pub_key(key: &str) -> Result { 138 | debug!("key: {}", key); 139 | tendermint::PublicKey::from_raw_ed25519(&base64::decode(key)?) 140 | .ok_or_else(|| anyhow!("failed to generate tendermint public key")) 141 | } 142 | 143 | fn pub_key_to_address(key: &tendermint::PublicKey) -> Address { 144 | tendermint::account::Id::from(key.ed25519().expect("unsupported public key type")) 145 | .as_bytes() 146 | .to_vec() 147 | } 148 | -------------------------------------------------------------------------------- /src/lib/vm/lambdavm/mod.rs: -------------------------------------------------------------------------------- 1 | /// Library for interfacing with the VM, and generating Transactions 2 | /// 3 | use std::str::FromStr; 4 | 5 | use anyhow::{anyhow, bail, ensure, Result}; 6 | pub use lambdavm::build_program; 7 | pub use lambdavm::jaleo::{get_credits_key, mint_credits}; 8 | pub use lambdavm::jaleo::{Itertools, UserInputValueType}; 9 | use lambdavm::VariableType; 10 | use log::debug; 11 | use sha3::{Digest, Sha3_256}; 12 | 13 | const MAX_INPUTS: usize = 8; 14 | const MAX_OUTPUTS: usize = 8; 15 | 16 | pub type Address = lambdavm::jaleo::Address; 17 | pub type Identifier = lambdavm::jaleo::Identifier; 18 | pub type Program = lambdavm::jaleo::Program; 19 | pub type ProgramBuild = lambdavm::ProgramBuild; 20 | pub type Record = lambdavm::jaleo::Record; 21 | pub type EncryptedRecord = lambdavm::jaleo::EncryptedRecord; 22 | pub type ViewKey = lambdavm::jaleo::ViewKey; 23 | pub type PrivateKey = lambdavm::jaleo::PrivateKey; 24 | pub type Field = lambdavm::jaleo::Field; 25 | pub type ProgramID = lambdavm::jaleo::ProgramID; 26 | pub type VerifyingKey = lambdavm::jaleo::VerifyingKey; 27 | pub type ProvingKey = lambdavm::jaleo::ProvingKey; 28 | pub type Deployment = lambdavm::jaleo::Deployment; 29 | pub type Transition = lambdavm::jaleo::Transition; 30 | pub type VerifyingKeyMap = lambdavm::jaleo::VerifyingKeyMap; 31 | 32 | /// Basic deployment validations 33 | pub fn verify_deployment(program: &Program, verifying_keys: VerifyingKeyMap) -> Result<()> { 34 | // Ensure the deployment contains verifying keys. 35 | let program_id = program.id(); 36 | ensure!( 37 | !verifying_keys.map.is_empty(), 38 | "No verifying keys present in the deployment for program '{program_id}'" 39 | ); 40 | 41 | // Ensure the number of verifying keys matches the number of program functions. 42 | if verifying_keys.map.len() != program.functions().len() { 43 | bail!("The number of verifying keys does not match the number of program functions"); 44 | } 45 | 46 | // Ensure the program functions are in the same order as the verifying keys. 47 | for ((function_name, function), candidate_name) in 48 | program.functions().iter().zip_eq(verifying_keys.map.keys()) 49 | { 50 | // Ensure the function name is correct. 51 | if function_name != function.name() { 52 | bail!( 53 | "The function key is '{function_name}', but the function name is '{}'", 54 | function.name() 55 | ) 56 | } 57 | // Ensure the function name with the verifying key is correct. 58 | if candidate_name != function.name() { 59 | bail!( 60 | "The verifier key is '{candidate_name}', but the function name is '{}'", 61 | function.name() 62 | ) 63 | } 64 | } 65 | Ok(()) 66 | } 67 | 68 | pub fn ensure_srs_file_exists() -> Result<()> { 69 | let (_, srs_file_path) = lambdavm::universal_srs::get_universal_srs_dir_and_filepath()?; 70 | if std::fs::File::open(srs_file_path).is_err() { 71 | let _ = lambdavm::universal_srs::generate_universal_srs_and_write_to_file()?; 72 | } 73 | Ok(()) 74 | } 75 | 76 | pub fn verify_execution( 77 | transition: &Transition, 78 | verifying_key_map: &VerifyingKeyMap, 79 | ) -> Result<()> { 80 | // Verify each transition. 81 | log::debug!( 82 | "Verifying transition for {}/{}...", 83 | transition.program_id, 84 | transition.function_name 85 | ); 86 | 87 | // this check also rules out coinbase executions (e.g. credits genesis function) 88 | ensure!( 89 | transition.fee >= 0, 90 | "The execution fee is negative, cannot create credits" 91 | ); 92 | 93 | // Ensure an external execution isn't attempting to create credits 94 | // The assumption at this point is that credits can only be created in the genesis block 95 | // We may revisit if we add validator rewards, at which point some credits may be minted, although 96 | // still not by external function calls 97 | ensure!( 98 | !program_is_coinbase( 99 | &transition.program_id.to_string(), 100 | &transition.function_name.to_string() 101 | ), 102 | "Coinbase functions cannot be called" 103 | ); 104 | // // Ensure the transition ID is correct. 105 | // ensure!( 106 | // **transition == transition.to_root()?, 107 | // "The transition ID is incorrect" 108 | // ); 109 | // Ensure the number of inputs is within the allowed range. 110 | ensure!( 111 | transition.inputs.len() <= MAX_INPUTS, 112 | "Transition exceeded maximum number of inputs" 113 | ); 114 | // Ensure the number of outputs is within the allowed range. 115 | ensure!( 116 | transition.outputs.len() <= MAX_OUTPUTS, 117 | "Transition exceeded maximum number of outputs" 118 | ); 119 | // // Ensure each input is valid. 120 | // if transition 121 | // .inputs 122 | // .iter() 123 | // .enumerate() 124 | // .any(|(index, input)| !input.verify(transition.tcm(), index)) 125 | // { 126 | // bail!("Failed to verify a transition input") 127 | // } 128 | // // Ensure each output is valid. 129 | // let num_inputs = transition.inputs.len(); 130 | // if transition 131 | // .outputs 132 | // .iter() 133 | // .enumerate() 134 | // .any(|(index, output)| !output.verify(transition.tcm(), num_inputs + index)) 135 | // { 136 | // bail!("Failed to verify a transition output") 137 | // } 138 | 139 | // Retrieve the verifying key. 140 | let verifying_key = verifying_key_map 141 | .map 142 | .get(&transition.function_name) 143 | .ok_or_else(|| anyhow!("missing verifying key"))?; 144 | // Decode and deserialize the proof. 145 | let proof_bytes = hex::decode(&transition.proof)?; 146 | 147 | // TODO: Fix this by making proofs derive the deserialize trait instead of this. 148 | let proof = lambdavm::jaleo::deserialize_proof(proof_bytes)?; 149 | 150 | let inputs: Vec = transition 151 | .inputs 152 | .iter() 153 | .filter_map(|i| match i { 154 | lambdavm::VariableType::Public(value) => Some(value.clone()), 155 | _ => None, 156 | }) 157 | .collect(); 158 | 159 | // Ensure the proof is valid. 160 | ensure!( 161 | lambdavm::verify_proof(verifying_key.clone(), &inputs, &proof)?, 162 | "Transition is invalid" 163 | ); 164 | 165 | Ok(()) 166 | } 167 | 168 | pub fn program_is_coinbase(program_id: &str, function_name: &str) -> bool { 169 | (function_name == "mint" || function_name == "genesis") && program_id == "credits.aleo" 170 | } 171 | 172 | // Generates a program deployment for source transactions 173 | pub fn generate_program(program_string: &str) -> Result { 174 | // Verify program is valid by parsing it and returning it 175 | Program::from_str(program_string) 176 | } 177 | 178 | pub fn execution( 179 | program: Program, 180 | function_name: Identifier, 181 | inputs: &[UserInputValueType], 182 | private_key: &PrivateKey, 183 | _proving_key: Option, 184 | ) -> Result> { 185 | ensure!( 186 | !program_is_coinbase(&program.id().to_string(), &function_name.to_string()), 187 | "Coinbase functions cannot be called" 188 | ); 189 | 190 | debug!( 191 | "executing program {} function {} inputs {:?}", 192 | program, function_name, inputs 193 | ); 194 | 195 | let function = program 196 | .get_function(&function_name) 197 | .map_err(|e| anyhow!("{}", e))?; 198 | 199 | let (compiled_function_variables, proof) = 200 | lambdavm::execute_function(&program, &function_name.to_string(), inputs)?; 201 | 202 | let inputs = lambdavm::jaleo::process_circuit_inputs( 203 | &function, 204 | &compiled_function_variables, 205 | private_key, 206 | )?; 207 | let outputs = 208 | lambdavm::jaleo::process_circuit_outputs(&function, &compiled_function_variables)?; 209 | 210 | let bytes_proof = lambdavm::jaleo::serialize_proof(proof)?; 211 | let encoded_proof = hex::encode(bytes_proof); 212 | 213 | let transition = Transition { 214 | program_id: *program.id(), 215 | function_name, 216 | inputs: inputs.into_values().collect::>(), 217 | outputs: outputs.into_values().collect::>(), 218 | proof: encoded_proof, 219 | fee: 0, 220 | }; 221 | 222 | Ok(vec![transition]) 223 | } 224 | 225 | /// Extract the record gates (the minimal credits unit) as a u64 integer, instead of a snarkvm internal type. 226 | pub fn gates(record: &Record) -> u64 { 227 | record.gates 228 | } 229 | 230 | /// This is temporary. We should be using the `serial_number` method in the Record struct, but 231 | /// we are doing this to conform to the current API. 232 | pub fn compute_serial_number(_private_key: PrivateKey, commitment: Field) -> Result { 233 | Ok(sha3_hash(&hex::decode(commitment)?)) 234 | } 235 | 236 | fn sha3_hash(input: &[u8]) -> String { 237 | let mut hasher = Sha3_256::new(); 238 | hasher.update(input); 239 | let bytes = hasher.finalize().to_vec(); 240 | hex::encode(bytes) 241 | } 242 | 243 | /// Generate a record for a specific program with the given attributes, 244 | /// by using the given seed to deterministically generate a nonce. 245 | /// This could be replaced by a more user-friendly record constructor. 246 | pub fn mint_record( 247 | _program_id: &str, 248 | _record_name: &str, 249 | owner_address: &Address, 250 | gates: u64, 251 | seed: u64, 252 | ) -> Result<(Field, EncryptedRecord)> { 253 | // For now calling mint_credits is enough; on the snarkVM backend the program_id 254 | // and record_name are just used to calculate the commitment, but we don't do things that way 255 | // The seed is used for instantiating a randomizer, which is used to generate the nonce 256 | // and encrypt the record. Once again, we don't really do things that way for now. 257 | 258 | mint_credits(owner_address, gates, seed) 259 | } 260 | 261 | /// Matches types of literals (that we know are numbers) and turns them into u128 before trying to downcast to the desired type 262 | // TODO: Once https://trello.com/c/vtHu588B/77-handle-inputs-and-outputs-visibility-encryption is merged, fix this 263 | pub fn int_from_output>(output: &VariableType) -> Result 264 | where 265 | >::Error: std::fmt::Debug, 266 | { 267 | match output { 268 | VariableType::Private(user_input_value_type) 269 | | VariableType::Public(user_input_value_type) => { 270 | let value = match user_input_value_type { 271 | UserInputValueType::U8(v) => *v as u128, 272 | UserInputValueType::U16(v) => *v as u128, 273 | UserInputValueType::U32(v) => *v as u128, 274 | UserInputValueType::U64(v) => *v as u128, 275 | UserInputValueType::U128(v) => *v, 276 | _ => todo!(), 277 | }; 278 | Ok(T::try_from(value).expect("issue casting literal to desired type")) 279 | } 280 | _ => { 281 | bail!("output type extraction not supported"); 282 | } 283 | } 284 | } 285 | 286 | // same as above 287 | pub fn address_from_output(output: &VariableType) -> Result
{ 288 | if let VariableType::Public(UserInputValueType::Address(address)) = output { 289 | let address_string = std::str::from_utf8(address)?; 290 | let address = Address::from_str(address_string)?; 291 | return Ok(address); 292 | }; 293 | 294 | if let VariableType::Private(UserInputValueType::Address(address)) = output { 295 | let address_string = std::str::from_utf8(address)?; 296 | let address = Address::from_str(address_string)?; 297 | return Ok(address); 298 | }; 299 | 300 | bail!("output type extraction not supported"); 301 | } 302 | 303 | pub fn u64_to_value(amount: u64) -> UserInputValueType { 304 | UserInputValueType::from_str(&format!("{amount}u64")).expect("couldn't parse amount") 305 | } 306 | -------------------------------------------------------------------------------- /src/lib/vm/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "snarkvm_backend")] 2 | mod snarkvm; 3 | #[cfg(feature = "snarkvm_backend")] 4 | pub use self::snarkvm::*; 5 | 6 | #[cfg(feature = "lambdavm_backend")] 7 | mod lambdavm; 8 | #[cfg(feature = "lambdavm_backend")] 9 | pub use self::lambdavm::*; 10 | -------------------------------------------------------------------------------- /src/lib/vm/snarkvm/mod.rs: -------------------------------------------------------------------------------- 1 | /// Library for interfacing with the VM, and generating Transactions 2 | /// 3 | use std::{ops::Deref, str::FromStr, sync::Arc}; 4 | 5 | use anyhow::{anyhow, bail, ensure, Result}; 6 | use indexmap::IndexMap; 7 | use log::debug; 8 | use parking_lot::{lock_api::RwLock, RawRwLock}; 9 | use rand::{rngs::ThreadRng, SeedableRng}; 10 | use rand_chacha::ChaCha8Rng; 11 | use serde::{Deserialize, Serialize}; 12 | use snarkvm::prelude::Parser; 13 | use snarkvm::{ 14 | circuit::AleoV0, 15 | console::types::string::Integer, 16 | prelude::{ 17 | Balance, CallStack, Environment, Itertools, Literal, Network, One, Owner, Plaintext, 18 | Testnet3, ToBits, ToField, Uniform, I64, 19 | }, 20 | }; 21 | 22 | mod stack; 23 | 24 | pub type Address = snarkvm::prelude::Address; 25 | pub type Identifier = snarkvm::prelude::Identifier; 26 | pub type UserInputValueType = snarkvm::prelude::Value; 27 | pub type Program = snarkvm::prelude::Program; 28 | pub type Ciphertext = snarkvm::prelude::Ciphertext; 29 | pub type Record = snarkvm::prelude::Record>; 30 | type Execution = snarkvm::prelude::Execution; 31 | pub type EncryptedRecord = snarkvm::prelude::Record; 32 | pub type ViewKey = snarkvm::prelude::ViewKey; 33 | pub type PrivateKey = snarkvm::prelude::PrivateKey; 34 | pub type Field = snarkvm::prelude::Field; 35 | pub type Origin = snarkvm::prelude::Origin; 36 | pub type Output = snarkvm::prelude::Output; 37 | pub type ProgramID = snarkvm::prelude::ProgramID; 38 | pub type VerifyingKey = snarkvm::prelude::VerifyingKey; 39 | pub type ProvingKey = snarkvm::prelude::ProvingKey; 40 | pub type Deployment = snarkvm::prelude::Deployment; 41 | pub type Transition = snarkvm::prelude::Transition; 42 | 43 | /// These structs are nothing more than a wrapper around the actual IndexMap that is used 44 | /// for the verifying keys map. Why does it exist? The problem comes from the lambdavm backend. 45 | /// Arkworks' verifying keys do not implement the regular `Serialize`/`Deserialize` traits, 46 | /// as they use their own custom `CanonicalSerialize`/`CanonicalDeserialize` ones. To implement 47 | /// the regular `Serialize`/`Deserialize` traits, we wrapped the IndexMap around this struct. 48 | /// To preserve APIs across the two backends, we did the same here. 49 | #[derive(Clone, Debug, Serialize, Deserialize)] 50 | pub struct VerifyingKeyMap { 51 | pub map: IndexMap, 52 | } 53 | 54 | #[derive(Debug, Serialize, Deserialize)] 55 | pub struct ProgramBuild { 56 | pub map: IndexMap, 57 | } 58 | 59 | /// Basic deployment validations 60 | pub fn verify_deployment(program: &Program, verifying_keys: VerifyingKeyMap) -> Result<()> { 61 | // Ensure the deployment contains verifying keys. 62 | let program_id = program.id(); 63 | ensure!( 64 | !verifying_keys.map.is_empty(), 65 | "No verifying keys present in the deployment for program '{program_id}'" 66 | ); 67 | 68 | // Ensure the number of verifying keys matches the number of program functions. 69 | if verifying_keys.map.len() != program.functions().len() { 70 | bail!("The number of verifying keys does not match the number of program functions"); 71 | } 72 | 73 | // Ensure the program functions are in the same order as the verifying keys. 74 | for ((function_name, function), candidate_name) in 75 | program.functions().iter().zip_eq(verifying_keys.map.keys()) 76 | { 77 | // Ensure the function name is correct. 78 | if function_name != function.name() { 79 | bail!( 80 | "The function key is '{function_name}', but the function name is '{}'", 81 | function.name() 82 | ) 83 | } 84 | // Ensure the function name with the verifying key is correct. 85 | if candidate_name != function.name() { 86 | bail!( 87 | "The verifier key is '{candidate_name}', but the function name is '{}'", 88 | function.name() 89 | ) 90 | } 91 | } 92 | Ok(()) 93 | } 94 | 95 | pub fn verify_execution(transition: &Transition, verifying_keys: &VerifyingKeyMap) -> Result<()> { 96 | // Verify each transition. 97 | log::debug!( 98 | "Verifying transition for {}/{}...", 99 | transition.program_id(), 100 | transition.function_name() 101 | ); 102 | 103 | // this check also rules out coinbase executions (e.g. credits genesis function) 104 | ensure!( 105 | *transition.fee() >= 0, 106 | "The execution fee is negative, cannot create credits" 107 | ); 108 | 109 | // Ensure the transition ID is correct. 110 | ensure!( 111 | **transition.id() == transition.to_root()?, 112 | "The transition ID is incorrect" 113 | ); 114 | // Ensure the number of inputs is within the allowed range. 115 | ensure!( 116 | transition.inputs().len() <= Testnet3::MAX_INPUTS, 117 | "Transition exceeded maximum number of inputs" 118 | ); 119 | // Ensure the number of outputs is within the allowed range. 120 | ensure!( 121 | transition.outputs().len() <= Testnet3::MAX_INPUTS, 122 | "Transition exceeded maximum number of outputs" 123 | ); 124 | // Ensure each input is valid. 125 | if transition 126 | .inputs() 127 | .iter() 128 | .enumerate() 129 | .any(|(index, input)| !input.verify(transition.tcm(), index)) 130 | { 131 | bail!("Failed to verify a transition input") 132 | } 133 | // Ensure each output is valid. 134 | let num_inputs = transition.inputs().len(); 135 | if transition 136 | .outputs() 137 | .iter() 138 | .enumerate() 139 | .any(|(index, output)| !output.verify(transition.tcm(), num_inputs + index)) 140 | { 141 | bail!("Failed to verify a transition output") 142 | } 143 | // Compute the x- and y-coordinate of `tpk`. 144 | let (tpk_x, tpk_y) = transition.tpk().to_xy_coordinate(); 145 | // [Inputs] Construct the verifier inputs to verify the proof. 146 | let mut inputs = vec![ 147 | ::Field::one(), 148 | *tpk_x, 149 | *tpk_y, 150 | **transition.tcm(), 151 | ]; 152 | // [Inputs] Extend the verifier inputs with the input IDs. 153 | inputs.extend( 154 | transition 155 | .inputs() 156 | .iter() 157 | .flat_map(|input| input.verifier_inputs()), 158 | ); 159 | 160 | // [Inputs] Extend the verifier inputs with the output IDs. 161 | inputs.extend( 162 | transition 163 | .outputs() 164 | .iter() 165 | .flat_map(|output| output.verifier_inputs()), 166 | ); 167 | // [Inputs] Extend the verifier inputs with the fee. 168 | inputs.push(*I64::::new(*transition.fee()).to_field()?); 169 | 170 | log::debug!( 171 | "Transition public inputs ({} elements): {:#?}", 172 | inputs.len(), 173 | inputs 174 | ); 175 | 176 | // Retrieve the verifying key. 177 | let verifying_key = verifying_keys 178 | .map 179 | .get(transition.function_name()) 180 | .ok_or_else(|| anyhow!("missing verifying key"))?; 181 | // Ensure the proof is valid. 182 | ensure!( 183 | verifying_key.verify(transition.function_name(), &inputs, transition.proof()), 184 | "Transition is invalid" 185 | ); 186 | Ok(()) 187 | } 188 | 189 | /// Generate proving and verifying keys for each function in the given program, 190 | /// and return them in a function name -> (proving key, verifying key) map. 191 | pub fn build_program(program_string: &str) -> Result<(Program, ProgramBuild)> { 192 | let (_, program) = Program::parse(program_string).map_err(|e| anyhow!("{}", e))?; 193 | 194 | let mut verifying_keys = IndexMap::new(); 195 | 196 | for function_name in program.functions().keys() { 197 | let rng = &mut rand::thread_rng(); 198 | verifying_keys.insert( 199 | *function_name, 200 | synthesize_function_keys(&program, rng, function_name)?, 201 | ); 202 | } 203 | 204 | Ok(( 205 | program, 206 | ProgramBuild { 207 | map: verifying_keys, 208 | }, 209 | )) 210 | } 211 | 212 | /// Generate proving and verifying keys for the given function. 213 | pub fn synthesize_function_keys( 214 | program: &Program, 215 | rng: &mut ThreadRng, 216 | function_name: &Identifier, 217 | ) -> Result<(ProvingKey, VerifyingKey)> { 218 | let stack = stack::new_init(program)?; 219 | stack.synthesize_key::(function_name, rng)?; 220 | let proving_key = stack.proving_keys.read().get(function_name).cloned(); 221 | let proving_key = proving_key.ok_or_else(|| anyhow!("proving key not found for identifier"))?; 222 | 223 | let verifying_key = stack.verifying_keys.read().get(function_name).cloned(); 224 | let verifying_key = 225 | verifying_key.ok_or_else(|| anyhow!("verifying key not found for identifier"))?; 226 | 227 | Ok((proving_key, verifying_key)) 228 | } 229 | 230 | // Generates a program deployment for source transactions 231 | pub fn generate_program(program_string: &str) -> Result { 232 | // Verify program is valid by parsing it and returning it 233 | Program::from_str(program_string) 234 | } 235 | 236 | pub fn execution( 237 | program: Program, 238 | function_name: Identifier, 239 | inputs: &[UserInputValueType], 240 | private_key: &PrivateKey, 241 | proving_key: Option, 242 | ) -> Result> { 243 | ensure!( 244 | !Program::is_coinbase(program.id(), &function_name), 245 | "Coinbase functions cannot be called" 246 | ); 247 | 248 | ensure!( 249 | program.contains_function(&function_name), 250 | "Function '{function_name}' does not exist." 251 | ); 252 | 253 | debug!( 254 | "executing program {} function {} inputs {:?}", 255 | program, function_name, inputs 256 | ); 257 | 258 | let rng = &mut rand::thread_rng(); 259 | 260 | let stack = stack::new_init(&program)?; 261 | 262 | let proving_key = match proving_key { 263 | Some(v) => v, 264 | None => synthesize_function_keys(&program, rng, &function_name)?.0, 265 | }; 266 | stack.insert_proving_key(&function_name, proving_key)?; 267 | 268 | let authorization = stack.authorize::(private_key, function_name, inputs, rng)?; 269 | let execution: Arc> = Arc::new(RwLock::new(Execution::new())); 270 | 271 | // Execute the circuit. 272 | let _ = stack.execute_function::( 273 | CallStack::execute(authorization, execution.clone())?, 274 | rng, 275 | )?; 276 | 277 | let execution = execution.read().clone(); 278 | 279 | Ok(execution.into_transitions().collect()) 280 | } 281 | 282 | /// Extract the record gates (the minimal credits unit) as a u64 integer, instead of a snarkvm internal type. 283 | pub fn gates(record: &Record) -> u64 { 284 | *record.gates().deref().deref() 285 | } 286 | 287 | /// A helper method to derive the serial number from the private key and commitment. 288 | pub fn compute_serial_number(private_key: PrivateKey, commitment: Field) -> Result { 289 | // Compute the generator `H` as `HashToGroup(commitment)`. 290 | let h = Testnet3::hash_to_group_psd2(&[Testnet3::serial_number_domain(), commitment])?; 291 | // Compute `gamma` as `sk_sig * H`. 292 | let gamma = h * private_key.sk_sig(); 293 | // Compute `sn_nonce` as `Hash(COFACTOR * gamma)`. 294 | let sn_nonce = Testnet3::hash_to_scalar_psd2(&[ 295 | Testnet3::serial_number_domain(), 296 | gamma.mul_by_cofactor().to_x_coordinate(), 297 | ])?; 298 | // Compute `serial_number` as `Commit(commitment, sn_nonce)`. 299 | Testnet3::commit_bhp512( 300 | &(Testnet3::serial_number_domain(), commitment).to_bits_le(), 301 | &sn_nonce, 302 | ) 303 | } 304 | 305 | /// Generate a record for a specific program with the given attributes, 306 | /// by using the given seed to deterministically generate a nonce. 307 | /// This could be replaced by a more user-friendly record constructor. 308 | pub fn mint_record( 309 | program_id: &str, 310 | record_name: &str, 311 | owner_address: &Address, 312 | gates: u64, 313 | seed: u64, 314 | ) -> Result<(Field, EncryptedRecord)> { 315 | // TODO have someone verify/audit this, probably it's unsafe or breaks cryptographic assumptions 316 | 317 | let owner = Owner::Private(Plaintext::Literal( 318 | Literal::Address(*owner_address), 319 | Default::default(), 320 | )); 321 | let amount = Integer::new(gates); 322 | let gates = Balance::Private(Plaintext::Literal(Literal::U64(amount), Default::default())); 323 | let empty_data = IndexMap::new(); 324 | 325 | let mut rng = ChaCha8Rng::seed_from_u64(seed); 326 | let randomizer = Uniform::rand(&mut rng); 327 | let nonce = Testnet3::g_scalar_multiply(&randomizer); 328 | 329 | let public_record = Record::from_plaintext(owner, gates, empty_data, nonce)?; 330 | let record_name = Identifier::from_str(record_name)?; 331 | let program_id = ProgramID::from_str(program_id)?; 332 | let commitment = public_record.to_commitment(&program_id, &record_name)?; 333 | let encrypted_record = public_record.encrypt(randomizer)?; 334 | Ok((commitment, encrypted_record)) 335 | } 336 | 337 | // This function might be too hacky, consider generalizing better and moving it to a proper place 338 | /// Matches types of literals (that we know are numbers) and turns them into u128 before trying to downcast to the desired type 339 | pub fn int_from_output>(output: &Output) -> Result 340 | where 341 | >::Error: std::fmt::Debug, 342 | { 343 | if let Output::Public(_, Some(Plaintext::Literal(literal, _))) = output { 344 | let value = match literal { 345 | Literal::U32(v) => *v.deref() as u128, 346 | Literal::U64(v) => *v.deref() as u128, 347 | Literal::U128(v) => *v.deref(), 348 | _ => todo!(), 349 | }; 350 | return Ok(T::try_from(value).expect("issue casting literal to desired type")); 351 | }; 352 | bail!("output type extraction not supported"); 353 | } 354 | 355 | // same as above 356 | pub fn address_from_output(output: &Output) -> Result
{ 357 | if let Output::Public(_, Some(Plaintext::Literal(Literal::Address(value), _))) = output { 358 | return Ok(*value); 359 | }; 360 | 361 | bail!("output type extraction not supported"); 362 | } 363 | 364 | pub fn u64_to_value(amount: u64) -> UserInputValueType { 365 | UserInputValueType::from_str(&format!("{amount}u64")).expect("couldn't parse amount") 366 | } 367 | 368 | #[allow(non_snake_case)] 369 | pub fn u128_to_UserInputValueType(amount: u128) -> UserInputValueType { 370 | UserInputValueType::from_str(&format!("{amount}u128")).expect("couldn't parse amount") 371 | } 372 | 373 | pub fn u128_to_value(amount: u128) -> UserInputValueType { 374 | UserInputValueType::from_str(&format!("{amount}u128")).expect("couldn't parse amount") 375 | } 376 | -------------------------------------------------------------------------------- /src/lib/vm/snarkvm/stack.rs: -------------------------------------------------------------------------------- 1 | use super::Program; 2 | use anyhow::{ensure, Result}; 3 | use snarkvm::prelude::{RegisterTypes, Testnet3, UniversalSRS}; 4 | /// This module includes helper functions initially taken from SnarkVM's Stack struct. 5 | /// The goal is to progressively remove the dependency on that struct. 6 | use std::sync::Arc; 7 | 8 | pub type Stack = snarkvm::prelude::Stack; 9 | 10 | /// This function creates and initializes a `Stack` struct for a given program on the fly, providing functionality 11 | /// related to Programs (deploy, executions, key synthesis) without the need of a `Process`. It essentially combines 12 | /// Stack::new() and Stack::init() 13 | pub fn new_init(program: &Program) -> Result { 14 | // Retrieve the program ID. 15 | let program_id = program.id(); 16 | 17 | // Ensure the program contains functions. 18 | ensure!( 19 | !program.functions().is_empty(), 20 | "No functions present in the deployment for program '{program_id}'" 21 | ); 22 | 23 | // Construct the stack for the program. 24 | let universal_srs = Arc::new(UniversalSRS::::load()?); 25 | 26 | let mut stack = Stack { 27 | program: program.clone(), 28 | external_stacks: Default::default(), 29 | register_types: Default::default(), 30 | finalize_types: Default::default(), 31 | universal_srs, 32 | proving_keys: Default::default(), 33 | verifying_keys: Default::default(), 34 | }; 35 | 36 | // Add the program functions to the stack. 37 | for function in program.functions().values() { 38 | let name = function.name(); 39 | // Ensure the function name is not already added. 40 | ensure!( 41 | !stack.register_types.contains_key(name), 42 | "Function '{name}' already exists" 43 | ); 44 | 45 | // Compute the register types. 46 | let register_types = RegisterTypes::from_function(&stack, function)?; 47 | // Add the function name and register types to the stack. 48 | stack.register_types.insert(*name, register_types); 49 | } 50 | 51 | // Return the stack. 52 | Ok(stack) 53 | } 54 | --------------------------------------------------------------------------------