From f737069a75b283c9fc008387a827f7bec9f0d424 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 29 Nov 2022 16:48:48 +0100 Subject: [PATCH 001/208] Add integration testing library --- contract-testing/Cargo.lock | 1806 +++++++++++++++++++++++++++++++++++ contract-testing/Cargo.toml | 13 + contract-testing/src/lib.rs | 320 +++++++ 3 files changed, 2139 insertions(+) create mode 100644 contract-testing/Cargo.lock create mode 100644 contract-testing/Cargo.toml create mode 100644 contract-testing/src/lib.rs diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock new file mode 100644 index 00000000..bdf0039c --- /dev/null +++ b/contract-testing/Cargo.lock @@ -0,0 +1,1806 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aggregate_sig" +version = "0.1.0" +dependencies = [ + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "generic-array", + "id", + "pairing", + "rand 0.7.3", + "random_oracle", + "rayon", + "serde", + "sha2 0.10.6", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] + +[[package]] +name = "bulletproofs" +version = "0.1.0" +dependencies = [ + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "group", + "pairing", + "pedersen_scheme", + "rand 0.7.3", + "random_oracle", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytecheck" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" +dependencies = [ + "bytecheck_derive", + "ptr_meta", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "concordium-contracts-common" +version = "4.1.0" +dependencies = [ + "bs58", + "chrono", + "concordium-contracts-common-derive", + "fnv", + "hashbrown 0.11.2", + "hex", + "num-bigint 0.4.3", + "num-integer", + "num-traits", + "rust_decimal", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "concordium-contracts-common-derive" +version = "1.0.1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "concordium-smart-contract-testing" +version = "0.1.0" +dependencies = [ + "concordium-contracts-common", + "concordium_base", + "sha2 0.10.6", + "wasm-chain-integration", + "wasm-transform", +] + +[[package]] +name = "concordium_base" +version = "0.1.0" +dependencies = [ + "aggregate_sig", + "anyhow", + "byteorder", + "chrono", + "concordium-contracts-common", + "crypto_common", + "derive_more", + "ecvrf", + "ed25519-dalek", + "eddsa_ed25519", + "either", + "encrypted_transfers", + "ff", + "hex", + "id", + "itertools", + "libc", + "num", + "num-bigint 0.4.3", + "num-traits", + "pairing", + "rand 0.7.3", + "rand_core 0.5.1", + "random_oracle", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.6", + "thiserror", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto_common" +version = "0.1.0" +dependencies = [ + "aes", + "anyhow", + "base64", + "byteorder", + "cbc", + "concordium-contracts-common", + "crypto_common_derive", + "derive_more", + "ed25519-dalek", + "either", + "ff", + "group", + "hex", + "hmac", + "libc", + "pairing", + "pbkdf2", + "rand 0.7.3", + "serde", + "sha2 0.10.6", + "thiserror", +] + +[[package]] +name = "crypto_common_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve_arithmetic" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "crypto_common", + "crypto_common_derive", + "ff", + "group", + "pairing", + "rand 0.7.3", + "serde", + "sha2 0.10.6", + "thiserror", +] + +[[package]] +name = "cxx" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", + "subtle", +] + +[[package]] +name = "dodis_yampolskiy_prf" +version = "0.1.0" +dependencies = [ + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "pairing", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "thiserror", +] + +[[package]] +name = "ecvrf" +version = "0.0.1" +dependencies = [ + "crypto_common", + "crypto_common_derive", + "curve25519-dalek", + "libc", + "rand 0.7.3", + "sha2 0.10.6", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "eddsa_ed25519" +version = "0.1.0" +dependencies = [ + "anyhow", + "crypto_common", + "crypto_common_derive", + "curve25519-dalek", + "ed25519-dalek", + "libc", + "rand 0.7.3", + "random_oracle", + "serde", + "sha2 0.10.6", + "thiserror", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "elgamal" +version = "0.1.0" +dependencies = [ + "anyhow", + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "libc", + "pairing", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "thiserror", +] + +[[package]] +name = "encrypted_transfers" +version = "0.1.0" +dependencies = [ + "bulletproofs", + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "elgamal", + "ff", + "id", + "itertools", + "libc", + "pairing", + "pedersen_scheme", + "rand 0.7.3", + "random_oracle", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "ff" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4530da57967e140ee0b44e0143aa66b5cb42bd9c503dbe316a15d5b0be65713e" +dependencies = [ + "byteorder", + "ff_derive", + "rand_core 0.5.1", +] + +[[package]] +name = "ff_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5796e7d62ca01a00ed3a649b0da1ffa1ac8f06bcad40339df09dbdd69a05ba9" +dependencies = [ + "num-bigint 0.2.6", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ffi_helpers" +version = "0.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "group" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cbdfc48f95bef47e3daf3b9d552a1dde6311e3a5fefa43e16c59f651d56fe5b" +dependencies = [ + "ff", + "rand 0.7.3", + "rand_xorshift", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "id" +version = "0.1.0" +dependencies = [ + "anyhow", + "bulletproofs", + "byteorder", + "chrono", + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "derive_more", + "dodis_yampolskiy_prf", + "ed25519-dalek", + "either", + "elgamal", + "ff", + "hex", + "itertools", + "libc", + "pairing", + "pedersen_scheme", + "ps_sig", + "rand 0.7.3", + "rand_core 0.5.1", + "random_oracle", + "serde", + "serde_json", + "sha2 0.10.6", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint 0.4.3", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint 0.4.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pairing" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c40534479a28199cd5109da27fe2fc4a4728e4fc701d9e9c1bded78f3271e4" +dependencies = [ + "byteorder", + "ff", + "group", + "rand_core 0.5.1", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.6", + "hmac", + "password-hash", + "sha2 0.10.6", +] + +[[package]] +name = "pedersen_scheme" +version = "0.1.0" +dependencies = [ + "byteorder", + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "pairing", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "thiserror", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ps_sig" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "ff", + "libc", + "pairing", + "pedersen_scheme", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "thiserror", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "random_oracle" +version = "0.1.0" +dependencies = [ + "crypto_common", + "crypto_common_derive", + "curve_arithmetic", + "rand 0.7.3", + "sha3", +] + +[[package]] +name = "rayon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +dependencies = [ + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rust_decimal" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9" +dependencies = [ + "arrayvec", + "borsh", + "bytecheck", + "byteorder", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.6", + "keccak", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-chain-integration" +version = "0.2.0" +dependencies = [ + "anyhow", + "byteorder", + "concordium-contracts-common", + "derive_more", + "ed25519-zebra", + "ffi_helpers", + "libc", + "num_enum", + "rand 0.8.5", + "secp256k1", + "serde", + "sha2 0.10.6", + "sha3", + "slab", + "thiserror", + "tinyvec", + "wasm-transform", +] + +[[package]] +name = "wasm-transform" +version = "0.1.1" +dependencies = [ + "anyhow", + "concordium-contracts-common", + "derive_more", + "leb128", + "num_enum", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml new file mode 100644 index 00000000..41aa77fa --- /dev/null +++ b/contract-testing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "concordium-smart-contract-testing" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +concordium-contracts-common = {path = "../concordium-base/concordium-contracts-common/concordium-contracts-common"} +concordium_base = {path = "../concordium-base/rust-src/concordium_base"} +wasm-chain-integration = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} +wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} +sha2 = "0.10" diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs new file mode 100644 index 00000000..292c9ce7 --- /dev/null +++ b/contract-testing/src/lib.rs @@ -0,0 +1,320 @@ +use concordium_base::base::{Energy, ExchangeRate}; +use concordium_contracts_common::*; +use sha2::{Digest, Sha256}; +use std::{ + collections::{ + btree_map::Entry::{Occupied, Vacant}, + BTreeMap, + }, + path::{Path, PathBuf}, +}; +use wasm_chain_integration::v1::ConcordiumAllowedImports; +use wasm_transform::artifact; + +pub struct Chain { + /// The slot time viewable inside the smart contracts. + /// An error is thrown if this is `None` and the contract tries to + /// access it. + slot_time: Option, + /// Energy per microCCD. + energy_per_micro_ccd: ExchangeRate, + /// Accounts and info about them. + accounts: BTreeMap, + /// Smart contract modules. + modules: BTreeMap< + ModuleReference, + artifact::Artifact< + wasm_transform::artifact::ArtifactNamedImport, + artifact::CompiledFunction, + >, + >, +} + +impl Chain { + pub fn new(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { + Self { + slot_time: Some(slot_time), + energy_per_micro_ccd, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + } + } + + pub fn module_deploy>( + &mut self, + sender: AccountAddress, + module_path: P, + ) -> Result { + // Load file + let module = std::fs::read(module_path)?; + + // Deserialize as wasm module (artifact) + let artifact = wasm_transform::utils::instantiate_with_metering::< + wasm_transform::artifact::ArtifactNamedImport, + _, + >( + &ConcordiumAllowedImports { + support_upgrade: true, + }, + &module[8..], // skip the 4 version bytes and 4 len bytes + ) + .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; + + // Calculate transaction fee of deployment + let energy = Energy::from(module.len() as u64 / 10); // From: Concordium/Const/deployModuleCost.hs + let transaction_fee = self.calculate_energy_cost(energy); + + // Try to subtract cost for account + match self.accounts.entry(sender) { + Vacant(_) => return Err(DeployModuleError::AccountDoesNotExist), + Occupied(mut acc) => { + let acc = acc.get_mut(); + if acc.balance < transaction_fee { + return Err(DeployModuleError::InsufficientFunds); + } + acc.balance -= transaction_fee; + } + } + + // Save the module + let module_reference = { + let mut hasher = Sha256::new(); + hasher.update(module); + let hash: [u8; 32] = hasher.finalize().into(); + ModuleReference::from(hash) + }; + self.modules.insert(module_reference, artifact); + Ok(SuccessfulModuleDeployment { + module_reference, + energy, + transaction_fee, + }) + } + + pub fn contract_init( + &mut self, + _sender: AccountAddress, + _module: ModuleReference, + _contract_name: ContractName, + _parameter: ContractParameter, + _amount: Amount, + _energy: Energy, + ) -> Result { + todo!() + } + + /// Can we get the return value here? + pub fn contract_update( + &mut self, + _sender: AccountAddress, + _address: ContractAddress, + _entrypoint: EntrypointName, + _parameter: ContractParameter, + _amount: Amount, + _energy: Energy, + ) -> Result { + todo!() + } + + /// If `None` is provided, address 0 will be used, which will have + /// sufficient funds. + pub fn contract_invoke( + &mut self, + _sender: Option, + _address: ContractAddress, + _entrypoint: EntrypointName, + _parameter: ContractParameter, + _amount: Amount, + _energy: Option, // Defaults to 100000 if `None`. + ) -> Result { + todo!() + } + + /// Create an account. Will override existing account if already present. + pub fn create_account(&mut self, account: AccountAddress, account_info: AccountInfo) { + self.accounts.insert(account, account_info); + } + + /// Creates a contract address with an index one above the highest + /// currently used. Next call to `contract_init` will skip this + /// address. + pub fn create_contract_address(&mut self) -> ContractAddress { todo!() } + + pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = Some(slot_time); } + + pub fn set_energy_per_micro_ccd(&mut self, energy_per_micro_ccd: ExchangeRate) { + self.energy_per_micro_ccd = energy_per_micro_ccd; + } + + /// Returns the balance of an account if it exists. + pub fn account_balance(&self, address: AccountAddress) -> Option { + self.accounts.get(&address).and_then(|ai| Some(ai.balance)) + } + + pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { + Amount::from_micro_ccd( + energy.energy * self.energy_per_micro_ccd.numerator() + / self.energy_per_micro_ccd.denominator(), + ) + } +} + +pub struct AccountInfo { + /// The account balance. TODO: Do we need the three types of balances? + pub balance: Amount, + /// Optional test policies. + policies: Option, +} + +impl AccountInfo { + pub fn new(balance: Amount) -> Self { + Self { + balance, + policies: None, + } + } +} + +#[derive(Debug)] +pub struct FailedContractInteraction { + /// Energy spent. + pub energy: Energy, + /// Error returned. + pub error: ContractError, + /// Events emitted before the interaction failed. Events from failed + /// updates are not stored on the chain, but can be useful for + /// debugging. + pub events: Vec, +} + +#[derive(Debug)] +pub struct ContractError(Vec); + +// TODO: Can we get Eq for this when using io::Error? +// TODO: Should this also have the energy used? +#[derive(Debug)] +pub enum DeployModuleError { + ReadFileError(std::io::Error), + InvalidModule(String), + InsufficientFunds, + AccountDoesNotExist, +} + +impl From for DeployModuleError { + fn from(e: std::io::Error) -> Self { DeployModuleError::ReadFileError(e) } +} + +impl ContractError { + pub fn deserial(&self) -> Result { todo!() } +} + +#[derive(PartialEq, Eq, Debug)] +pub struct Event(String); + +#[derive(PartialEq, Eq, Debug)] +pub enum ChainEvent { + Interrupted { + address: ContractAddress, + events: Vec, + }, + Resumed { + address: ContractAddress, + success: bool, + }, + Upgraded { + address: ContractAddress, + from: ModuleReference, + to: ModuleReference, + }, + Updated { + address: ContractAddress, + contract: OwnedContractName, + entrypoing: OwnedEntrypointName, + amount: Amount, + }, +} + +pub struct SuccessfulContractUpdate { + /// Host events that occured. This includes interrupts, resumes, and + /// upgrades. + pub host_events: Vec, + pub transfers: Vec<(AccountAddress, Amount)>, + /// Energy used. + pub energy: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, + /// The returned value. + pub return_value: ContractReturnValue, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SuccessfulModuleDeployment { + pub module_reference: ModuleReference, + pub energy: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SuccessfulContractInit { + /// The address of the new instance. + pub contract_address: ContractAddress, + /// Events produced during initialization. + pub events: Vec, + /// Energy used. + pub energy: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, +} + +pub struct Policies; + +pub struct ContractReturnValue(Vec); + +#[derive(Debug, PartialEq, Eq)] +pub enum ParsingError { + /// Thrown by `deserial` on failure. + ParsingFailed, +} + +impl ContractReturnValue { + pub fn deserial(&self) -> Result { todo!() } +} + +pub struct ContractParameter(Vec); + +impl ContractParameter { + pub fn empty() -> Self { Self(Vec::new()) } + + pub fn from_bytes(bytes: Vec) -> Self { Self(bytes) } + + // TODO: add version with serde json + pub fn from_typed(parameter: &T) -> Self { Self(to_bytes(parameter)) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deploying_valid_module_works() { + let mut chain = Chain::new( + Timestamp::from_timestamp_millis(0), + ExchangeRate::new_unchecked(10, 1), + ); + let acc_0 = AccountAddress([0; 32]); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(acc_0, AccountInfo::new(initial_balance)); + + let res = chain + .module_deploy(acc_0, "icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + assert_eq!(chain.accounts.len(), 1); + assert_eq!(chain.modules.len(), 1); + assert_eq!( + chain.account_balance(acc_0), + Some(initial_balance - res.transaction_fee) + ); + } +} From 450f939464d16e634564f48dcb7275110308ea5a Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 30 Nov 2022 17:20:17 +0100 Subject: [PATCH 002/208] Implement bare-bones contract initialization --- contract-testing/Cargo.lock | 1 + contract-testing/Cargo.toml | 1 + contract-testing/src/lib.rs | 331 ++++++++++++++++++++++++++++++------ 3 files changed, 282 insertions(+), 51 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index bdf0039c..15f138be 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -305,6 +305,7 @@ dependencies = [ name = "concordium-smart-contract-testing" version = "0.1.0" dependencies = [ + "anyhow", "concordium-contracts-common", "concordium_base", "sha2 0.10.6", diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 41aa77fa..99b37550 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -11,3 +11,4 @@ concordium_base = {path = "../concordium-base/rust-src/concordium_base"} wasm-chain-integration = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" +anyhow = "1" diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 292c9ce7..e22a46de 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,14 +1,19 @@ +use anyhow::anyhow; use concordium_base::base::{Energy, ExchangeRate}; use concordium_contracts_common::*; use sha2::{Digest, Sha256}; use std::{ collections::{ btree_map::Entry::{Occupied, Vacant}, - BTreeMap, + BTreeMap, LinkedList, }, - path::{Path, PathBuf}, + path::Path, +}; +use wasm_chain_integration::{ + v0, + v1::{self, ConcordiumAllowedImports, ReturnValue}, + ExecResult, InterpreterEnergy, }; -use wasm_chain_integration::v1::ConcordiumAllowedImports; use wasm_transform::artifact; pub struct Chain { @@ -16,27 +21,37 @@ pub struct Chain { /// An error is thrown if this is `None` and the contract tries to /// access it. slot_time: Option, - /// Energy per microCCD. - energy_per_micro_ccd: ExchangeRate, + /// MicroCCD per Energy. + micro_ccd_per_energy: ExchangeRate, /// Accounts and info about them. accounts: BTreeMap, /// Smart contract modules. modules: BTreeMap< ModuleReference, - artifact::Artifact< - wasm_transform::artifact::ArtifactNamedImport, - artifact::CompiledFunction, - >, + artifact::Artifact, >, + /// Smart contract instances. + contracts: BTreeMap, + /// Next contract index to use when creating a new instance. + next_contract_index: u64, } impl Chain { - pub fn new(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { + pub fn new_with_time(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { Self { slot_time: Some(slot_time), - energy_per_micro_ccd, - accounts: BTreeMap::new(), - modules: BTreeMap::new(), + ..Self::new(energy_per_micro_ccd) + } + } + + pub fn new(energy_per_micro_ccd: ExchangeRate) -> Self { + Self { + slot_time: None, + micro_ccd_per_energy: energy_per_micro_ccd, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + contracts: BTreeMap::new(), + next_contract_index: 0, } } @@ -49,10 +64,7 @@ impl Chain { let module = std::fs::read(module_path)?; // Deserialize as wasm module (artifact) - let artifact = wasm_transform::utils::instantiate_with_metering::< - wasm_transform::artifact::ArtifactNamedImport, - _, - >( + let artifact = wasm_transform::utils::instantiate_with_metering::( &ConcordiumAllowedImports { support_upgrade: true, }, @@ -93,14 +105,147 @@ impl Chain { pub fn contract_init( &mut self, - _sender: AccountAddress, - _module: ModuleReference, - _contract_name: ContractName, - _parameter: ContractParameter, - _amount: Amount, - _energy: Energy, - ) -> Result { - todo!() + sender: AccountAddress, + module: ModuleReference, + contract_name: ContractName, + parameter: ContractParameter, + amount: Amount, + energy_limit: Energy, + ) -> Result { + // Lookup artifact + let artifact = self + .modules + .get(&module) + .ok_or_else(|| ContractInitError::ModuleDoesNotExist)?; + // Get the account and check that it has sufficient balance to pay for the + // energy. + let account_info = self + .accounts + .get(&sender) + .ok_or_else(|| ContractInitError::AccountDoesNotExist)?; + if account_info.balance < self.calculate_energy_cost(energy_limit) { + return Err(ContractInitError::InsufficientFunds); + } + // Construct the context. + let init_ctx = InitContextOpt { + metadata: ChainMetadataOpt { + slot_time: self.slot_time, + }, + init_origin: sender, + sender_policies: account_info.policies.clone(), + }; + // Initialize contract + let loader = v1::trie::Loader::new(&[][..]); + let res = v1::invoke_init( + artifact, + init_ctx, + v1::InitInvocation { + amount, + init_name: contract_name.get_chain_name(), + parameter: ¶meter.0, + energy: InterpreterEnergy { + // TODO: Why do we have two separate energy types? + energy: energy_limit.energy, + }, + }, + false, + loader, + ) + .map_err(|e| ContractInitError::StringInitError(e.to_string()))?; + // Charge account for cost + let (res, transaction_fee) = match res { + v1::InitResult::Success { + logs, + return_value: _, /* Ignore return value for now, since our tools do not support + * it for inits, currently. */ + remaining_energy, + state, + } => { + let contract_address = self.create_contract_address(); + let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + + // Save the state + self.contracts.insert(contract_address, state); // Ignore the return value since we created a new contract address for this. + + ( + Ok(SuccessfulContractInit { + contract_address, + logs, + energy_used, + transaction_fee, + }), + transaction_fee, + ) + } + v1::InitResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs { + // TODO: Get logs on failures. + logs: LinkedList::new(), + }, + }, + )), + transaction_fee, + ) + } + v1::InitResult::Trap { + error, + remaining_energy, + } => { + let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs { + // TODO: Get logs on failures. + logs: LinkedList::new(), + }, + }, + )), + transaction_fee, + ) + } + v1::InitResult::OutOfEnergy => { + let transaction_fee = self.calculate_energy_cost(energy_limit); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_limit, + transaction_fee, + logs: v0::Logs { + // TODO: Get logs on failures. + logs: LinkedList::new(), + }, + }, + )), + transaction_fee, + ) + } + }; + // Charge the account. + // We have to get the account info again because of the borrow checker. + self.accounts + .get_mut(&sender) + .ok_or_else(|| ContractInitError::AccountDoesNotExist)? + .balance -= transaction_fee; + res } /// Can we get the return value here? @@ -138,12 +283,17 @@ impl Chain { /// Creates a contract address with an index one above the highest /// currently used. Next call to `contract_init` will skip this /// address. - pub fn create_contract_address(&mut self) -> ContractAddress { todo!() } + pub fn create_contract_address(&mut self) -> ContractAddress { + let index = self.next_contract_index; + let subindex = 0; + self.next_contract_index += 1; + ContractAddress::new(index, subindex) + } pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = Some(slot_time); } pub fn set_energy_per_micro_ccd(&mut self, energy_per_micro_ccd: ExchangeRate) { - self.energy_per_micro_ccd = energy_per_micro_ccd; + self.micro_ccd_per_energy = energy_per_micro_ccd; } /// Returns the balance of an account if it exists. @@ -153,17 +303,69 @@ impl Chain { pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { Amount::from_micro_ccd( - energy.energy * self.energy_per_micro_ccd.numerator() - / self.energy_per_micro_ccd.denominator(), + energy.energy * self.micro_ccd_per_energy.numerator() + / self.micro_ccd_per_energy.denominator(), ) } } +/// A chain metadata with an optional field. +/// Used when simulating contracts to allow the user to only specify the +/// necessary context fields. +/// The default value is `None` for all `Option` fields. +pub(crate) struct ChainMetadataOpt { + slot_time: Option, +} + +impl v0::HasChainMetadata for ChainMetadataOpt { + fn slot_time(&self) -> ExecResult { + unwrap_ctx_field(self.slot_time, "metadata.slotTime") + } +} + +/// An init context with optional fields. +/// Used when simulating contracts to allow the user to only specify the +/// context fields used by the contract. +/// The default value is `None` for all `Option` fields and the default of +/// `ChainMetadataOpt` for `metadata`. +pub(crate) struct InitContextOpt { + metadata: ChainMetadataOpt, + init_origin: AccountAddress, + sender_policies: Option, +} + +impl v0::HasInitContext for InitContextOpt { + type MetadataType = ChainMetadataOpt; + + fn metadata(&self) -> &Self::MetadataType { &self.metadata } + + fn init_origin(&self) -> ExecResult<&AccountAddress> { Ok(&self.init_origin) } + + fn sender_policies(&self) -> ExecResult<&[u8]> { + unwrap_ctx_field( + self.sender_policies.as_ref().map(Vec::as_ref), + "senderPolicies", + ) + } +} + +// Error handling when unwrapping. +fn unwrap_ctx_field(opt: Option, name: &str) -> ExecResult { + match opt { + Some(v) => Ok(v), + None => Err(anyhow!( + "Missing field '{}' in the context. Make sure to provide all the fields that the \ + contract uses.", + name, + )), + } +} + pub struct AccountInfo { /// The account balance. TODO: Do we need the three types of balances? pub balance: Amount, /// Optional test policies. - policies: Option, + policies: Option, } impl AccountInfo { @@ -175,16 +377,48 @@ impl AccountInfo { } } +pub enum ContractInitError { + /// Initialization failed for a reason that also exists on the chain. + ValidChainError(FailedContractInteraction), + /// TODO: Can we get a better error than the anyhow? + StringInitError(String), + /// Module has not been deployed in test environment. + ModuleDoesNotExist, + /// Account has not been created in test environment. + AccountDoesNotExist, + /// The account does not have enough funds to pay for the energy. + InsufficientFunds, +} + #[derive(Debug)] -pub struct FailedContractInteraction { - /// Energy spent. - pub energy: Energy, - /// Error returned. - pub error: ContractError, - /// Events emitted before the interaction failed. Events from failed - /// updates are not stored on the chain, but can be useful for - /// debugging. - pub events: Vec, +pub enum FailedContractInteraction { + Reject { + reason: i32, + return_value: ReturnValue, + energy_used: Energy, + transaction_fee: Amount, + /// Logs emitted before the interaction failed. Logs from failed + /// updates are not stored on the chain, but can be useful for + /// debugging. + logs: v0::Logs, + }, + Trap { + error: anyhow::Error, + energy_used: Energy, + transaction_fee: Amount, + /// Logs emitted before the interaction failed. Logs from failed + /// updates are not stored on the chain, but can be useful for + /// debugging. + logs: v0::Logs, + }, + OutOFEnergy { + energy_used: Energy, + transaction_fee: Amount, + /// Logs emitted before the interaction failed. Logs from failed + /// updates are not stored on the chain, but can be useful for + /// debugging. + logs: v0::Logs, + }, } #[derive(Debug)] @@ -255,20 +489,18 @@ pub struct SuccessfulModuleDeployment { pub transaction_fee: Amount, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub struct SuccessfulContractInit { /// The address of the new instance. pub contract_address: ContractAddress, - /// Events produced during initialization. - pub events: Vec, + /// Logs produced during initialization. + pub logs: v0::Logs, /// Energy used. - pub energy: Energy, + pub energy_used: Energy, /// Cost of transaction. pub transaction_fee: Amount, } -pub struct Policies; - pub struct ContractReturnValue(Vec); #[derive(Debug, PartialEq, Eq)] @@ -281,7 +513,7 @@ impl ContractReturnValue { pub fn deserial(&self) -> Result { todo!() } } -pub struct ContractParameter(Vec); +pub struct ContractParameter(pub Vec); impl ContractParameter { pub fn empty() -> Self { Self(Vec::new()) } @@ -298,16 +530,13 @@ mod tests { #[test] fn deploying_valid_module_works() { - let mut chain = Chain::new( - Timestamp::from_timestamp_millis(0), - ExchangeRate::new_unchecked(10, 1), - ); + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); let acc_0 = AccountAddress([0; 32]); let initial_balance = Amount::from_ccd(10000); chain.create_account(acc_0, AccountInfo::new(initial_balance)); let res = chain - .module_deploy(acc_0, "icecream/icecream.wasm.v1") + .module_deploy(acc_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); assert_eq!(chain.accounts.len(), 1); From 6f5414b5da48042df3e47bfa6c57648b9a3e2f61 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 1 Dec 2022 10:24:18 +0100 Subject: [PATCH 003/208] Add tests for contract init --- contract-testing/src/lib.rs | 111 ++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 6 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index e22a46de..4bf6b3b2 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -166,7 +166,7 @@ impl Chain { let transaction_fee = self.calculate_energy_cost(energy_used); // Save the state - self.contracts.insert(contract_address, state); // Ignore the return value since we created a new contract address for this. + self.contracts.insert(contract_address, state); ( Ok(SuccessfulContractInit { @@ -377,6 +377,7 @@ impl AccountInfo { } } +#[derive(Debug)] pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. ValidChainError(FailedContractInteraction), @@ -421,6 +422,30 @@ pub enum FailedContractInteraction { }, } +impl FailedContractInteraction { + pub fn transaction_fee(&self) -> Amount { + match self { + FailedContractInteraction::Reject { + transaction_fee, .. + } => *transaction_fee, + FailedContractInteraction::Trap { + transaction_fee, .. + } => *transaction_fee, + FailedContractInteraction::OutOFEnergy { + transaction_fee, .. + } => *transaction_fee, + } + } + + pub fn logs(&self) -> &v0::Logs { + match self { + FailedContractInteraction::Reject { logs, .. } => logs, + FailedContractInteraction::Trap { logs, .. } => logs, + FailedContractInteraction::OutOFEnergy { logs, .. } => logs, + } + } +} + #[derive(Debug)] pub struct ContractError(Vec); @@ -528,22 +553,96 @@ impl ContractParameter { mod tests { use super::*; + const ACC_0: AccountAddress = AccountAddress([0; 32]); + const ACC_1: AccountAddress = AccountAddress([1; 32]); + + #[test] + fn creating_accounts_work() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); + chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); + + assert_eq!(chain.accounts.len(), 2); + assert_eq!(chain.account_balance(ACC_0), Some(Amount::from_ccd(1))); + assert_eq!(chain.account_balance(ACC_1), Some(Amount::from_ccd(2))); + } + #[test] fn deploying_valid_module_works() { let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); - let acc_0 = AccountAddress([0; 32]); let initial_balance = Amount::from_ccd(10000); - chain.create_account(acc_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res = chain - .module_deploy(acc_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); - assert_eq!(chain.accounts.len(), 1); assert_eq!(chain.modules.len(), 1); assert_eq!( - chain.account_balance(acc_0), + chain.account_balance(ACC_0), Some(initial_balance - res.transaction_fee) ); } + + #[test] + fn initializing_valid_contract_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + ContractParameter::from_bytes(vec![0u8]), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + assert_eq!( + chain.account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) + ); + assert_eq!(chain.contracts.len(), 1); + } + + #[test] + fn initializing_with_invalid_parameter_fails() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + ContractParameter::from_bytes(vec![99u8]), // Invalid param + Amount::zero(), + Energy::from(10000u64), + ) + .expect_err("Initializing with invalid params should fail"); + + assert!(matches!(res_init, ContractInitError::ValidChainError(_))); + match res_init { + // Failed in the right way and account is still charged. + ContractInitError::ValidChainError(FailedContractInteraction::Reject { + transaction_fee, + .. + }) => assert_eq!( + chain.account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - transaction_fee) + ), + _ => panic!("Expected valid chain error."), + }; + } } From 5779fac6f321aae531cbc8eb965c70a639958779 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 1 Dec 2022 11:50:45 +0100 Subject: [PATCH 004/208] Implement most of bare-bones smart contract update --- contract-testing/src/lib.rs | 189 +++++++++++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 26 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 4bf6b3b2..79267935 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -31,11 +31,20 @@ pub struct Chain { artifact::Artifact, >, /// Smart contract instances. - contracts: BTreeMap, + // TODO: Value should also hold module ref, name etc. + contracts: BTreeMap, /// Next contract index to use when creating a new instance. next_contract_index: u64, } +pub struct ContractInstance { + module_reference: ModuleReference, + contract_name: OwnedContractName, + state: v1::trie::MutableState, + owner: AccountAddress, + self_balance: Amount, +} + impl Chain { pub fn new_with_time(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { Self { @@ -106,16 +115,16 @@ impl Chain { pub fn contract_init( &mut self, sender: AccountAddress, - module: ModuleReference, + module_reference: ModuleReference, contract_name: ContractName, parameter: ContractParameter, amount: Amount, - energy_limit: Energy, + energy_reserved: Energy, ) -> Result { // Lookup artifact let artifact = self .modules - .get(&module) + .get(&module_reference) .ok_or_else(|| ContractInitError::ModuleDoesNotExist)?; // Get the account and check that it has sufficient balance to pay for the // energy. @@ -123,7 +132,7 @@ impl Chain { .accounts .get(&sender) .ok_or_else(|| ContractInitError::AccountDoesNotExist)?; - if account_info.balance < self.calculate_energy_cost(energy_limit) { + if account_info.balance < self.calculate_energy_cost(energy_reserved) { return Err(ContractInitError::InsufficientFunds); } // Construct the context. @@ -145,7 +154,7 @@ impl Chain { parameter: ¶meter.0, energy: InterpreterEnergy { // TODO: Why do we have two separate energy types? - energy: energy_limit.energy, + energy: energy_reserved.energy, }, }, false, @@ -162,11 +171,19 @@ impl Chain { state, } => { let contract_address = self.create_contract_address(); - let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); - // Save the state - self.contracts.insert(contract_address, state); + let contract_instance = ContractInstance { + module_reference, + contract_name: contract_name.to_owned(), + state, + owner: sender, + self_balance: amount, + }; + + // Save the contract instance + self.contracts.insert(contract_address, contract_instance); ( Ok(SuccessfulContractInit { @@ -183,7 +200,7 @@ impl Chain { return_value, remaining_energy, } => { - let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractInitError::ValidChainError( @@ -205,7 +222,7 @@ impl Chain { error, remaining_energy, } => { - let energy_used = Energy::from(energy_limit.energy - remaining_energy); + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractInitError::ValidChainError( @@ -223,11 +240,11 @@ impl Chain { ) } v1::InitResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_limit); + let transaction_fee = self.calculate_energy_cost(energy_reserved); ( Err(ContractInitError::ValidChainError( FailedContractInteraction::OutOFEnergy { - energy_used: energy_limit, + energy_used: energy_reserved, transaction_fee, logs: v0::Logs { // TODO: Get logs on failures. @@ -251,13 +268,75 @@ impl Chain { /// Can we get the return value here? pub fn contract_update( &mut self, - _sender: AccountAddress, - _address: ContractAddress, - _entrypoint: EntrypointName, - _parameter: ContractParameter, - _amount: Amount, - _energy: Energy, - ) -> Result { + invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? + address: ContractAddress, + entrypoint: EntrypointName, + parameter: ContractParameter, + amount: Amount, + energy_reserved: Energy, + ) -> Result { + // Find contract instance and artifact + let instance = self + .contracts + .get(&address) + .ok_or_else(|| ContractUpdateError::InstanceDoesNotExist)?; + let artifact = self + .modules + .get(&instance.module_reference) + .ok_or_else(|| ContractUpdateError::ModuleDoesNotExist)?; + + // Ensure account exists and can pay for the reserved energy + let account_info = self + .accounts + .get(&invoker) + .ok_or_else(|| ContractUpdateError::AccountDoesNotExist)?; + if account_info.balance < self.calculate_energy_cost(energy_reserved) { + return Err(ContractUpdateError::InsufficientFunds); + } + // Construct the context + let receive_ctx = ReceiveContextOpt { + metadata: ChainMetadataOpt { + slot_time: self.slot_time, + }, + invoker, + self_address: address, + self_balance: instance.self_balance + amount, + sender: Address::Account(invoker), + owner: instance.owner, + sender_policies: account_info.policies.clone(), + entrypoint: entrypoint.to_owned(), + }; + let receive_name = OwnedReceiveName::new_unchecked(format!( + "{}.{}", + instance.contract_name.as_contract_name().contract_name(), + entrypoint + )); + + let mut loader = v1::trie::Loader::new(&[][..]); + let mut mutable_state = instance.state; + let inner = mutable_state.get_inner(&mut loader); + let instance_state = v1::InstanceState::new(loader, inner); + + // Update the contract + let res = v1::invoke_receive( + *artifact, + receive_ctx, + v1::ReceiveInvocation { + amount, + receive_name: receive_name.as_receive_name(), + parameter: ¶meter.0, + energy: InterpreterEnergy { + energy: energy_reserved.energy, + }, + }, + instance_state, + v1::ReceiveParams { + max_parameter_size: 65535, + limit_logs_and_return_values: false, + support_queries: true, + }, + ); + // Charge the transaction fee todo!() } @@ -270,7 +349,7 @@ impl Chain { _entrypoint: EntrypointName, _parameter: ContractParameter, _amount: Amount, - _energy: Option, // Defaults to 100000 if `None`. + _energy_reserved: Option, // Defaults to 100000 if `None`. ) -> Result { todo!() } @@ -349,6 +428,51 @@ impl v0::HasInitContext for InitContextOpt { } } +/// A receive context with optional fields. +/// Used when simulating contracts to allow the user to only specify the +/// context fields used by the contract. +/// The default value is `None` for all `Option` fields and the default of +/// `ChainMetadataOpt` for `metadata`. +pub(crate) struct ReceiveContextOpt { + metadata: ChainMetadataOpt, + invoker: AccountAddress, + self_address: ContractAddress, + self_balance: Amount, + sender: Address, + owner: AccountAddress, + sender_policies: Option, + /// The entrypoint name invoked. Only relevant for fallback receive + /// functions. + entrypoint: OwnedEntrypointName, +} + +impl v0::HasReceiveContext for ReceiveContextOpt { + type MetadataType = ChainMetadataOpt; + + fn metadata(&self) -> &Self::MetadataType { &self.metadata } + + fn invoker(&self) -> ExecResult<&AccountAddress> { Ok(&self.invoker) } + + fn self_address(&self) -> ExecResult<&ContractAddress> { Ok(&self.self_address) } + + fn self_balance(&self) -> ExecResult { Ok(self.self_balance) } + + fn sender(&self) -> ExecResult<&Address> { Ok(&self.sender) } + + fn owner(&self) -> ExecResult<&AccountAddress> { Ok(&self.owner) } + + fn sender_policies(&self) -> ExecResult<&[u8]> { + unwrap_ctx_field( + self.sender_policies.as_ref().map(Vec::as_ref), + "senderPolicies", + ) + } +} + +impl v1::HasReceiveContext for ReceiveContextOpt { + fn entrypoint(&self) -> ExecResult { Ok(self.entrypoint.as_entrypoint_name()) } +} + // Error handling when unwrapping. fn unwrap_ctx_field(opt: Option, name: &str) -> ExecResult { match opt { @@ -391,6 +515,22 @@ pub enum ContractInitError { InsufficientFunds, } +#[derive(Debug)] +pub enum ContractUpdateError { + /// Update failed for a reason that also exists on the chain. + ValidChainError(FailedContractInteraction), + /// TODO: Can we get a better error than the anyhow? + StringInitError(String), + /// Module has not been deployed in test environment. + ModuleDoesNotExist, + /// Contract instance has not been initialized in test environment. + InstanceDoesNotExist, + /// Account has not been created in test environment. + AccountDoesNotExist, + /// The account does not have enough funds to pay for the energy. + InsufficientFunds, +} + #[derive(Debug)] pub enum FailedContractInteraction { Reject { @@ -467,14 +607,11 @@ impl ContractError { pub fn deserial(&self) -> Result { todo!() } } -#[derive(PartialEq, Eq, Debug)] -pub struct Event(String); - -#[derive(PartialEq, Eq, Debug)] +#[derive(Debug)] pub enum ChainEvent { Interrupted { address: ContractAddress, - events: Vec, + logs: v0::Logs, }, Resumed { address: ContractAddress, From 02e3d58e8dcf247503a7d4fb9f540f63c3485233 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 1 Dec 2022 15:03:10 +0100 Subject: [PATCH 005/208] Finish up basic updates and add test --- contract-testing/Cargo.lock | 2 +- contract-testing/src/lib.rs | 189 +++++++++++++++++++++++++++++++----- 2 files changed, 164 insertions(+), 27 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index 15f138be..01d12070 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -275,7 +275,7 @@ dependencies = [ [[package]] name = "concordium-contracts-common" -version = "4.1.0" +version = "5.0.0" dependencies = [ "bs58", "chrono", diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 79267935..17dd6707 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -5,7 +5,7 @@ use sha2::{Digest, Sha256}; use std::{ collections::{ btree_map::Entry::{Occupied, Vacant}, - BTreeMap, LinkedList, + BTreeMap, }, path::Path, }; @@ -40,7 +40,7 @@ pub struct Chain { pub struct ContractInstance { module_reference: ModuleReference, contract_name: OwnedContractName, - state: v1::trie::MutableState, + state: v1::trie::PersistentState, owner: AccountAddress, self_balance: Amount, } @@ -144,7 +144,7 @@ impl Chain { sender_policies: account_info.policies.clone(), }; // Initialize contract - let loader = v1::trie::Loader::new(&[][..]); + let mut loader = v1::trie::Loader::new(&[][..]); let res = v1::invoke_init( artifact, init_ctx, @@ -160,7 +160,7 @@ impl Chain { false, loader, ) - .map_err(|e| ContractInitError::StringInitError(e.to_string()))?; + .map_err(|e| ContractInitError::StringError(e.to_string()))?; // Charge account for cost let (res, transaction_fee) = match res { v1::InitResult::Success { @@ -168,16 +168,19 @@ impl Chain { return_value: _, /* Ignore return value for now, since our tools do not support * it for inits, currently. */ remaining_energy, - state, + mut state, } => { let contract_address = self.create_contract_address(); let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); + let mut collector = v1::trie::SizeCollector::default(); + let contract_instance = ContractInstance { module_reference, contract_name: contract_name.to_owned(), - state, + state: state.freeze(&mut loader, &mut collector), /* TODO: Do we need to + * charge to storage? */ owner: sender, self_balance: amount, }; @@ -209,10 +212,7 @@ impl Chain { return_value, energy_used, transaction_fee, - logs: v0::Logs { - // TODO: Get logs on failures. - logs: LinkedList::new(), - }, + logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )), transaction_fee, @@ -230,10 +230,7 @@ impl Chain { error, energy_used, transaction_fee, - logs: v0::Logs { - // TODO: Get logs on failures. - logs: LinkedList::new(), - }, + logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )), transaction_fee, @@ -246,10 +243,7 @@ impl Chain { FailedContractInteraction::OutOFEnergy { energy_used: energy_reserved, transaction_fee, - logs: v0::Logs { - // TODO: Get logs on failures. - logs: LinkedList::new(), - }, + logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )), transaction_fee, @@ -313,13 +307,14 @@ impl Chain { )); let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = instance.state; + let mut mutable_state = instance.state.thaw(); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); // Update the contract - let res = v1::invoke_receive( - *artifact, + let res = v1::invoke_receive::<_, _, _, _, ReceiveContextOpt, ReceiveContextOpt>( + std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable + * for this to work. */ receive_ctx, v1::ReceiveInvocation { amount, @@ -335,9 +330,100 @@ impl Chain { limit_logs_and_return_values: false, support_queries: true, }, - ); + ) + .map_err(|e| ContractUpdateError::StringError(e.to_string()))?; + + let (res, transaction_fee) = match res { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + if state_changed { + let instance = self + .contracts + .get_mut(&address) + .ok_or_else(|| ContractUpdateError::InstanceDoesNotExist)?; + let mut collector = v1::trie::SizeCollector::default(); + instance.state = mutable_state.freeze(&mut loader, &mut collector); + // TODO: Do we need to charge for storage? + } + ( + Ok(SuccessfulContractUpdate { + host_events: Vec::new(), // TODO: add host events + transfers: Vec::new(), /* TODO: add transfers (or add fn to compute + * based on host events) */ + energy_used, + transaction_fee, + return_value: ContractReturnValue(return_value), + state_changed, + logs, + }), + transaction_fee, + ) + } + v1::ReceiveResult::Interrupt { .. } => todo!("Handle interrupts"), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )), + transaction_fee, + ) + } + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )), + transaction_fee, + ) + } + v1::ReceiveResult::OutOfEnergy => { + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_reserved, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )), + transaction_fee, + ) + } + }; // Charge the transaction fee - todo!() + self.accounts + .get_mut(&invoker) + .ok_or_else(|| ContractUpdateError::AccountDoesNotExist)? + .balance -= transaction_fee; + res } /// If `None` is provided, address 0 will be used, which will have @@ -506,7 +592,7 @@ pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. ValidChainError(FailedContractInteraction), /// TODO: Can we get a better error than the anyhow? - StringInitError(String), + StringError(String), /// Module has not been deployed in test environment. ModuleDoesNotExist, /// Account has not been created in test environment. @@ -520,7 +606,7 @@ pub enum ContractUpdateError { /// Update failed for a reason that also exists on the chain. ValidChainError(FailedContractInteraction), /// TODO: Can we get a better error than the anyhow? - StringInitError(String), + StringError(String), /// Module has not been deployed in test environment. ModuleDoesNotExist, /// Contract instance has not been initialized in test environment. @@ -630,17 +716,22 @@ pub enum ChainEvent { }, } +// TODO: Consider adding function to aggregate all logs from the host_events. pub struct SuccessfulContractUpdate { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. pub host_events: Vec, pub transfers: Vec<(AccountAddress, Amount)>, /// Energy used. - pub energy: Energy, + pub energy_used: Energy, /// Cost of transaction. pub transaction_fee: Amount, /// The returned value. pub return_value: ContractReturnValue, + /// Whether the state was changed. + pub state_changed: bool, + /// The logs produced before?/after? the last interrupt if any. + pub logs: v0::Logs, } #[derive(Debug, PartialEq, Eq)] @@ -782,4 +873,50 @@ mod tests { _ => panic!("Expected valid chain error."), }; } + + #[test] + fn updating_valid_contract_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + ContractParameter::from_bytes(vec![0u8]), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("set"), + ContractParameter::from_bytes(vec![1u8]), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + // TODO: Assert the new state value. + } } From bb7b26614840757ecf964af2f9e514b63e62299b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 5 Dec 2022 16:37:19 +0100 Subject: [PATCH 006/208] Add invoke method, helper methods, and begin handling interrupts --- contract-testing/src/lib.rs | 380 +++++++++++++++++++++++++++++++----- 1 file changed, 333 insertions(+), 47 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 17dd6707..d47729bf 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -11,7 +11,7 @@ use std::{ }; use wasm_chain_integration::{ v0, - v1::{self, ConcordiumAllowedImports, ReturnValue}, + v1::{self, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, ExecResult, InterpreterEnergy, }; use wasm_transform::artifact; @@ -122,16 +122,10 @@ impl Chain { energy_reserved: Energy, ) -> Result { // Lookup artifact - let artifact = self - .modules - .get(&module_reference) - .ok_or_else(|| ContractInitError::ModuleDoesNotExist)?; + let artifact = self.get_artifact(module_reference)?; // Get the account and check that it has sufficient balance to pay for the // energy. - let account_info = self - .accounts - .get(&sender) - .ok_or_else(|| ContractInitError::AccountDoesNotExist)?; + let account_info = self.get_account(sender)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) { return Err(ContractInitError::InsufficientFunds); } @@ -252,10 +246,7 @@ impl Chain { }; // Charge the account. // We have to get the account info again because of the borrow checker. - self.accounts - .get_mut(&sender) - .ok_or_else(|| ContractInitError::AccountDoesNotExist)? - .balance -= transaction_fee; + self.get_account_mut(sender)?.balance -= transaction_fee; res } @@ -270,20 +261,12 @@ impl Chain { energy_reserved: Energy, ) -> Result { // Find contract instance and artifact - let instance = self - .contracts - .get(&address) - .ok_or_else(|| ContractUpdateError::InstanceDoesNotExist)?; - let artifact = self - .modules - .get(&instance.module_reference) - .ok_or_else(|| ContractUpdateError::ModuleDoesNotExist)?; + let instance = self.get_instance(address)?; + // TODO: Charge energy for module lookup. + let artifact = self.get_artifact(instance.module_reference)?; // Ensure account exists and can pay for the reserved energy - let account_info = self - .accounts - .get(&invoker) - .ok_or_else(|| ContractUpdateError::AccountDoesNotExist)?; + let account_info = self.get_account(invoker)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) { return Err(ContractUpdateError::InsufficientFunds); } @@ -343,10 +326,7 @@ impl Chain { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); if state_changed { - let instance = self - .contracts - .get_mut(&address) - .ok_or_else(|| ContractUpdateError::InstanceDoesNotExist)?; + let instance = self.get_instance_mut(address)?; let mut collector = v1::trie::SizeCollector::default(); instance.state = mutable_state.freeze(&mut loader, &mut collector); // TODO: Do we need to charge for storage? @@ -365,7 +345,55 @@ impl Chain { transaction_fee, ) } - v1::ReceiveResult::Interrupt { .. } => todo!("Handle interrupts"), + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => { + if let v1::Interrupt::Transfer { to, amount } = interrupt { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + + let response = { + // Check if receiver account exists + if !self.accounts.contains_key(&to) { + InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + } + } else if self.get_instance(address)?.self_balance < amount { + InvokeResponse::Failure { + kind: v1::InvokeFailure::InsufficientAmount, + } + } else { + // Move the CCD + self.get_account_mut(to)?.balance += amount; + let instance = self.get_instance_mut(address)?; + instance.self_balance -= amount; + + InvokeResponse::Success { + new_balance: instance.self_balance, + data: None, + } + } + }; + + // Resume execution + let resume_res = v1::resume_receive( + config, // TODO: Need to change some things so it can use the ReceiveCtxOpt + response, + InterpreterEnergy::from(remaining_energy), + &mut mutable_state, + state_changed, + loader, + ); + // TODO: Do we need to check energy here? + } else { + todo!("Handle other interrupts") + } + todo!() + } v1::ReceiveResult::Reject { reason, return_value, @@ -419,25 +447,139 @@ impl Chain { } }; // Charge the transaction fee - self.accounts - .get_mut(&invoker) - .ok_or_else(|| ContractUpdateError::AccountDoesNotExist)? - .balance -= transaction_fee; + self.get_account_mut(invoker)?.balance -= transaction_fee; res } - /// If `None` is provided, address 0 will be used, which will have - /// sufficient funds. + /// TODO: Should we make invoker and energy optional? pub fn contract_invoke( &mut self, - _sender: Option, - _address: ContractAddress, - _entrypoint: EntrypointName, - _parameter: ContractParameter, - _amount: Amount, - _energy_reserved: Option, // Defaults to 100000 if `None`. - ) -> Result { - todo!() + invoker: AccountAddress, + address: ContractAddress, + entrypoint: EntrypointName, + parameter: ContractParameter, + amount: Amount, + energy_reserved: Energy, + ) -> Result { + // Find contract instance and artifact + let instance = self.get_instance(address)?; + let artifact = self.get_artifact(instance.module_reference)?; + + // Ensure account exists and can pay for the reserved energy + let account_info = self.get_account(invoker)?; + if account_info.balance < self.calculate_energy_cost(energy_reserved) { + return Err(ContractUpdateError::InsufficientFunds); + } + // Construct the context + let receive_ctx = ReceiveContextOpt { + metadata: ChainMetadataOpt { + slot_time: self.slot_time, + }, + invoker, + self_address: address, + self_balance: instance.self_balance + amount, + sender: Address::Account(invoker), + owner: instance.owner, + sender_policies: account_info.policies.clone(), + entrypoint: entrypoint.to_owned(), + }; + let receive_name = OwnedReceiveName::new_unchecked(format!( + "{}.{}", + instance.contract_name.as_contract_name().contract_name(), + entrypoint + )); + + let mut loader = v1::trie::Loader::new(&[][..]); + let mut mutable_state = instance.state.thaw(); + let inner = mutable_state.get_inner(&mut loader); + let instance_state = v1::InstanceState::new(loader, inner); + + // Update the contract + let res = v1::invoke_receive::<_, _, _, _, ReceiveContextOpt, ReceiveContextOpt>( + std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable + * for this to work. */ + receive_ctx, + v1::ReceiveInvocation { + amount, + receive_name: receive_name.as_receive_name(), + parameter: ¶meter.0, + energy: InterpreterEnergy { + energy: energy_reserved.energy, + }, + }, + instance_state, + v1::ReceiveParams { + max_parameter_size: 65535, + limit_logs_and_return_values: false, + support_queries: true, + }, + ) + .map_err(|e| ContractUpdateError::StringError(e.to_string()))?; + + match res { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + Ok(SuccessfulContractUpdate { + host_events: Vec::new(), // TODO: add host events + transfers: Vec::new(), /* TODO: add transfers (or add fn to compute + * based on host events) */ + energy_used, + transaction_fee, + return_value: ContractReturnValue(return_value), + state_changed, // TODO: Should we always set this to false? + logs, + }) + } + v1::ReceiveResult::Interrupt { .. } => todo!("Handle interrupts"), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) + } + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) + } + v1::ReceiveResult::OutOfEnergy => { + let transaction_fee = self.calculate_energy_cost(energy_reserved); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_reserved, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) + } + } } /// Create an account. Will override existing account if already present. @@ -472,6 +614,70 @@ impl Chain { / self.micro_ccd_per_energy.denominator(), ) } + + fn get_artifact( + &self, + module_ref: ModuleReference, + ) -> Result<&artifact::Artifact, ModuleMissing> + { + self.modules.get(&module_ref).ok_or(ModuleMissing) + } + + fn get_instance( + &self, + address: ContractAddress, + ) -> Result<&ContractInstance, ContractInstanceMissing> { + self.contracts.get(&address).ok_or(ContractInstanceMissing) + } + + fn get_instance_mut( + &mut self, + address: ContractAddress, + ) -> Result<&mut ContractInstance, ContractInstanceMissing> { + self.contracts + .get_mut(&address) + .ok_or(ContractInstanceMissing) + } + + fn get_account(&self, address: AccountAddress) -> Result<&AccountInfo, AccountMissing> { + self.accounts.get(&address).ok_or(AccountMissing) + } + + fn get_account_mut( + &mut self, + address: AccountAddress, + ) -> Result<&mut AccountInfo, AccountMissing> { + self.accounts.get_mut(&address).ok_or(AccountMissing) + } +} + +#[derive(Debug)] +struct ModuleMissing; + +impl From for ContractInitError { + fn from(_: ModuleMissing) -> Self { Self::ModuleDoesNotExist } +} + +impl From for ContractUpdateError { + fn from(_: ModuleMissing) -> Self { Self::ModuleDoesNotExist } +} + +#[derive(Debug)] +struct ContractInstanceMissing; + +impl From for ContractUpdateError { + fn from(_: ContractInstanceMissing) -> Self { Self::InstanceDoesNotExist } +} + +#[derive(Debug)] +struct AccountMissing; + +impl From for ContractInitError { + fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } +} + +impl From for ContractUpdateError { + fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } } /// A chain metadata with an optional field. @@ -889,7 +1095,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - ContractParameter::from_bytes(vec![0u8]), + ContractParameter::from_bytes(vec![0u8]), // Starts as 0 Amount::zero(), Energy::from(10000u64), ) @@ -900,12 +1106,24 @@ mod tests { ACC_0, res_init.contract_address, EntrypointName::new_unchecked("set"), - ContractParameter::from_bytes(vec![1u8]), + ContractParameter::from_bytes(vec![1u8]), // Updated to 1 Amount::zero(), Energy::from(10000u64), ) .expect("Updating valid contract should work"); + let res_invoke_get = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. assert_eq!( chain.account_balance(ACC_0), Some( @@ -917,6 +1135,74 @@ mod tests { ); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); - // TODO: Assert the new state value. + // Assert that the updated state is persisted. + assert_eq!(res_invoke_get.return_value.0, [1u8]); + } + + #[test] + fn update_with_account_transfer_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(123); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + ContractParameter::from_typed(&ACC_1), + transfer_amount, + Energy::from(10000u64), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + - transfer_amount + ) + ); + assert_eq!( + chain.account_balance(ACC_1), + Some(initial_balance + transfer_amount) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + assert_eq!(res_update.return_value.0, [2u8]); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, [2u8]); } } From ab37629e828020d475648f479f6b5a40b0125037 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 6 Dec 2022 16:06:09 +0100 Subject: [PATCH 007/208] Remove optional contexts, implement naive handling of transfers --- contract-testing/src/lib.rs | 354 ++++++++++++++++++------------------ 1 file changed, 175 insertions(+), 179 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d47729bf..b2a1ee10 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use concordium_base::base::{Energy, ExchangeRate}; use concordium_contracts_common::*; use sha2::{Digest, Sha256}; @@ -12,15 +11,14 @@ use std::{ use wasm_chain_integration::{ v0, v1::{self, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, - ExecResult, InterpreterEnergy, + InterpreterEnergy, }; use wasm_transform::artifact; pub struct Chain { /// The slot time viewable inside the smart contracts. - /// An error is thrown if this is `None` and the contract tries to - /// access it. - slot_time: Option, + /// Defaults to `0`. + slot_time: SlotTime, /// MicroCCD per Energy. micro_ccd_per_energy: ExchangeRate, /// Accounts and info about them. @@ -31,7 +29,6 @@ pub struct Chain { artifact::Artifact, >, /// Smart contract instances. - // TODO: Value should also hold module ref, name etc. contracts: BTreeMap, /// Next contract index to use when creating a new instance. next_contract_index: u64, @@ -48,14 +45,15 @@ pub struct ContractInstance { impl Chain { pub fn new_with_time(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { Self { - slot_time: Some(slot_time), + slot_time, ..Self::new(energy_per_micro_ccd) } } + /// Create a new [`Self`] where the `slot_time` defaults to `0`. pub fn new(energy_per_micro_ccd: ExchangeRate) -> Self { Self { - slot_time: None, + slot_time: Timestamp::from_timestamp_millis(0), micro_ccd_per_energy: energy_per_micro_ccd, accounts: BTreeMap::new(), modules: BTreeMap::new(), @@ -130,8 +128,8 @@ impl Chain { return Err(ContractInitError::InsufficientFunds); } // Construct the context. - let init_ctx = InitContextOpt { - metadata: ChainMetadataOpt { + let init_ctx = v0::InitContext { + metadata: ChainMetadata { slot_time: self.slot_time, }, init_origin: sender, @@ -261,6 +259,8 @@ impl Chain { energy_reserved: Energy, ) -> Result { // Find contract instance and artifact + // Update balance.... + self.get_instance_mut(address)?.self_balance += amount; let instance = self.get_instance(address)?; // TODO: Charge energy for module lookup. let artifact = self.get_artifact(instance.module_reference)?; @@ -271,17 +271,19 @@ impl Chain { return Err(ContractUpdateError::InsufficientFunds); } // Construct the context - let receive_ctx = ReceiveContextOpt { - metadata: ChainMetadataOpt { - slot_time: self.slot_time, - }, - invoker, - self_address: address, - self_balance: instance.self_balance + amount, - sender: Address::Account(invoker), - owner: instance.owner, - sender_policies: account_info.policies.clone(), + let receive_ctx = v1::ReceiveContext { entrypoint: entrypoint.to_owned(), + common: v0::ReceiveContext { + metadata: ChainMetadata { + slot_time: self.slot_time, + }, + invoker, + self_address: address, + self_balance: instance.self_balance, + sender: Address::Account(invoker), + owner: instance.owner, + sender_policies: account_info.policies.clone(), + }, }; let receive_name = OwnedReceiveName::new_unchecked(format!( "{}.{}", @@ -295,7 +297,14 @@ impl Chain { let instance_state = v1::InstanceState::new(loader, inner); // Update the contract - let res = v1::invoke_receive::<_, _, _, _, ReceiveContextOpt, ReceiveContextOpt>( + let res = v1::invoke_receive::< + _, + _, + _, + _, + v1::ReceiveContext, + v1::ReceiveContext, + >( std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable * for this to work. */ receive_ctx, @@ -348,51 +357,113 @@ impl Chain { v1::ReceiveResult::Interrupt { remaining_energy, state_changed, - logs, + logs: interrupt_logs, config, interrupt, } => { - if let v1::Interrupt::Transfer { to, amount } = interrupt { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - - let response = { - // Check if receiver account exists - if !self.accounts.contains_key(&to) { - InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - } - } else if self.get_instance(address)?.self_balance < amount { - InvokeResponse::Failure { - kind: v1::InvokeFailure::InsufficientAmount, + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + + let response = { + // Check if receiver account exists + if !self.accounts.contains_key(&to) { + InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + } + } else if self.get_instance(address)?.self_balance < amount { + InvokeResponse::Failure { + kind: v1::InvokeFailure::InsufficientAmount, + } + } else { + // Move the CCD + self.get_account_mut(to)?.balance += amount; + let instance = self.get_instance_mut(address)?; + instance.self_balance -= amount; + + InvokeResponse::Success { + new_balance: instance.self_balance, + data: None, + } } - } else { - // Move the CCD - self.get_account_mut(to)?.balance += amount; - let instance = self.get_instance_mut(address)?; - instance.self_balance -= amount; - - InvokeResponse::Success { - new_balance: instance.self_balance, - data: None, - } - } - }; - - // Resume execution - let resume_res = v1::resume_receive( - config, // TODO: Need to change some things so it can use the ReceiveCtxOpt - response, - InterpreterEnergy::from(remaining_energy), - &mut mutable_state, - state_changed, - loader, - ); - // TODO: Do we need to check energy here? - } else { - todo!("Handle other interrupts") + }; + + // Resume execution + let res = match v1::resume_receive( + config, + response, + InterpreterEnergy::from(remaining_energy), + &mut mutable_state, + state_changed, + loader, + ) { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs: resume_logs, + state_changed, + return_value, + remaining_energy, + } => { + let energy_used = + Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + if state_changed { + let instance = self.get_instance_mut(address)?; + let mut collector = v1::trie::SizeCollector::default(); + instance.state = + mutable_state.freeze(&mut loader, &mut collector); + // TODO: Do we need to charge for + // storage? + } + ( + Ok(SuccessfulContractUpdate { + host_events: vec![ + ChainEvent::Resumed { + address, + success: true, + }, + ChainEvent::Interrupted { + address, + logs: interrupt_logs, + }, + ], + transfers: vec![(to, amount)], + energy_used, + transaction_fee, + return_value: ContractReturnValue(return_value), + state_changed, /* TODO: Which state_changed should be + * returned here? */ + logs: resume_logs, + }), + transaction_fee, + ) + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => todo!(), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => todo!("reason: {}", reason), + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => todo!(), + v1::ReceiveResult::OutOfEnergy => todo!(), + }, + Err(_) => todo!(), + }; + res + // TODO: Do we need to check energy here? + } + _ => todo!("Handle other interrupts"), } - todo!() } v1::ReceiveResult::Reject { reason, @@ -446,8 +517,8 @@ impl Chain { ) } }; - // Charge the transaction fee - self.get_account_mut(invoker)?.balance -= transaction_fee; + // Charge the transaction fee and amount + self.get_account_mut(invoker)?.balance -= transaction_fee + amount; res } @@ -471,17 +542,19 @@ impl Chain { return Err(ContractUpdateError::InsufficientFunds); } // Construct the context - let receive_ctx = ReceiveContextOpt { - metadata: ChainMetadataOpt { - slot_time: self.slot_time, - }, - invoker, - self_address: address, - self_balance: instance.self_balance + amount, - sender: Address::Account(invoker), - owner: instance.owner, - sender_policies: account_info.policies.clone(), + let receive_ctx = v1::ReceiveContext { entrypoint: entrypoint.to_owned(), + common: v0::ReceiveContext { + metadata: ChainMetadata { + slot_time: self.slot_time, + }, + invoker, + self_address: address, + self_balance: instance.self_balance + amount, + sender: Address::Account(invoker), + owner: instance.owner, + sender_policies: account_info.policies.clone(), + }, }; let receive_name = OwnedReceiveName::new_unchecked(format!( "{}.{}", @@ -495,7 +568,14 @@ impl Chain { let instance_state = v1::InstanceState::new(loader, inner); // Update the contract - let res = v1::invoke_receive::<_, _, _, _, ReceiveContextOpt, ReceiveContextOpt>( + let res = v1::invoke_receive::< + _, + _, + _, + _, + v1::ReceiveContext, + v1::ReceiveContext, + >( std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable * for this to work. */ receive_ctx, @@ -597,7 +677,7 @@ impl Chain { ContractAddress::new(index, subindex) } - pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = Some(slot_time); } + pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } pub fn set_energy_per_micro_ccd(&mut self, energy_per_micro_ccd: ExchangeRate) { self.micro_ccd_per_energy = energy_per_micro_ccd; @@ -680,117 +760,33 @@ impl From for ContractUpdateError { fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } } -/// A chain metadata with an optional field. -/// Used when simulating contracts to allow the user to only specify the -/// necessary context fields. -/// The default value is `None` for all `Option` fields. -pub(crate) struct ChainMetadataOpt { - slot_time: Option, -} - -impl v0::HasChainMetadata for ChainMetadataOpt { - fn slot_time(&self) -> ExecResult { - unwrap_ctx_field(self.slot_time, "metadata.slotTime") - } -} - -/// An init context with optional fields. -/// Used when simulating contracts to allow the user to only specify the -/// context fields used by the contract. -/// The default value is `None` for all `Option` fields and the default of -/// `ChainMetadataOpt` for `metadata`. -pub(crate) struct InitContextOpt { - metadata: ChainMetadataOpt, - init_origin: AccountAddress, - sender_policies: Option, -} - -impl v0::HasInitContext for InitContextOpt { - type MetadataType = ChainMetadataOpt; - - fn metadata(&self) -> &Self::MetadataType { &self.metadata } - - fn init_origin(&self) -> ExecResult<&AccountAddress> { Ok(&self.init_origin) } - - fn sender_policies(&self) -> ExecResult<&[u8]> { - unwrap_ctx_field( - self.sender_policies.as_ref().map(Vec::as_ref), - "senderPolicies", - ) - } -} - -/// A receive context with optional fields. -/// Used when simulating contracts to allow the user to only specify the -/// context fields used by the contract. -/// The default value is `None` for all `Option` fields and the default of -/// `ChainMetadataOpt` for `metadata`. -pub(crate) struct ReceiveContextOpt { - metadata: ChainMetadataOpt, - invoker: AccountAddress, - self_address: ContractAddress, - self_balance: Amount, - sender: Address, - owner: AccountAddress, - sender_policies: Option, - /// The entrypoint name invoked. Only relevant for fallback receive - /// functions. - entrypoint: OwnedEntrypointName, -} - -impl v0::HasReceiveContext for ReceiveContextOpt { - type MetadataType = ChainMetadataOpt; - - fn metadata(&self) -> &Self::MetadataType { &self.metadata } - - fn invoker(&self) -> ExecResult<&AccountAddress> { Ok(&self.invoker) } - - fn self_address(&self) -> ExecResult<&ContractAddress> { Ok(&self.self_address) } - - fn self_balance(&self) -> ExecResult { Ok(self.self_balance) } - - fn sender(&self) -> ExecResult<&Address> { Ok(&self.sender) } - - fn owner(&self) -> ExecResult<&AccountAddress> { Ok(&self.owner) } - - fn sender_policies(&self) -> ExecResult<&[u8]> { - unwrap_ctx_field( - self.sender_policies.as_ref().map(Vec::as_ref), - "senderPolicies", - ) - } +pub struct AccountInfo { + /// The account balance. TODO: Do we need the three types of balances? + pub balance: Amount, + /// Account policies. + policies: v0::OwnedPolicyBytes, } -impl v1::HasReceiveContext for ReceiveContextOpt { - fn entrypoint(&self) -> ExecResult { Ok(self.entrypoint.as_entrypoint_name()) } -} +pub struct TestPolicies(v0::OwnedPolicyBytes); -// Error handling when unwrapping. -fn unwrap_ctx_field(opt: Option, name: &str) -> ExecResult { - match opt { - Some(v) => Ok(v), - None => Err(anyhow!( - "Missing field '{}' in the context. Make sure to provide all the fields that the \ - contract uses.", - name, - )), - } -} +impl TestPolicies { + // TODO: Make correctly structured policies ~= Vec>. + pub fn empty() -> Self { Self(v0::OwnedPolicyBytes::new()) } -pub struct AccountInfo { - /// The account balance. TODO: Do we need the three types of balances? - pub balance: Amount, - /// Optional test policies. - policies: Option, + // TODO: Add helper functions for creating arbitrary valid policies. } impl AccountInfo { - pub fn new(balance: Amount) -> Self { + pub fn new_with_policy(balance: Amount, policies: TestPolicies) -> Self { Self { balance, - policies: None, + policies: policies.0, } } + + /// Create new account info with empty account policies. + pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } } #[derive(Debug)] @@ -1143,7 +1139,7 @@ mod tests { fn update_with_account_transfer_works() { let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(123); + let transfer_amount = Amount::from_ccd(1); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); @@ -1169,7 +1165,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), transfer_amount, - Energy::from(10000u64), + Energy::from(4000000u64), ) .expect("Updating valid contract should work"); @@ -1201,8 +1197,8 @@ mod tests { ); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); - assert_eq!(res_update.return_value.0, [2u8]); + assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, [2u8]); + assert_eq!(res_view.return_value.0, [2, 0, 0, 0]); } } From 9a1b247a5fc92a79a10abf81a9712bf465394d93 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 7 Dec 2022 14:29:00 +0100 Subject: [PATCH 008/208] Begin work on recursive handling of contracts --- contract-testing/src/lib.rs | 519 +++++++++++++++++++++--------------- 1 file changed, 310 insertions(+), 209 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index b2a1ee10..a51b4054 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -10,8 +10,8 @@ use std::{ }; use wasm_chain_integration::{ v0, - v1::{self, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, - InterpreterEnergy, + v1::{self, trie::MutableState, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, + ExecResult, InterpreterEnergy, }; use wasm_transform::artifact; @@ -262,14 +262,13 @@ impl Chain { // Update balance.... self.get_instance_mut(address)?.self_balance += amount; let instance = self.get_instance(address)?; - // TODO: Charge energy for module lookup. - let artifact = self.get_artifact(instance.module_reference)?; - // Ensure account exists and can pay for the reserved energy let account_info = self.get_account(invoker)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) { return Err(ContractUpdateError::InsufficientFunds); } + // TODO: Check if entrypoint exists or if the fallback should be used. + // Construct the context let receive_ctx = v1::ReceiveContext { entrypoint: entrypoint.to_owned(), @@ -291,22 +290,154 @@ impl Chain { entrypoint )); + // TODO: Charge energy for module lookup. + let artifact = self.get_artifact(instance.module_reference)?; + let mut loader = v1::trie::Loader::new(&[][..]); let mut mutable_state = instance.state.thaw(); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); - // Update the contract - let res = v1::invoke_receive::< - _, - _, - _, - _, - v1::ReceiveContext, - v1::ReceiveContext, - >( - std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable - * for this to work. */ + struct ProcessReceiveData<'a, 'b, 'c> { + address: ContractAddress, + contract_name: OwnedContractName, + amount: Amount, + entrypoint: OwnedEntrypointName, + chain: &'a mut Chain, + chain_events: Vec, + mutable_state: &'b mut MutableState, + loader: v1::trie::Loader<&'c [u8]>, + } + + fn process( + data: &mut ProcessReceiveData, + res: ExecResult>, + ) -> ExecResult> { + match res { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let update_event = ChainEvent::Updated { + address: data.address, + contract: data.contract_name.clone(), + entrypoint: data.entrypoint.clone(), + amount: data.amount, + }; + if state_changed { + let instance = data + .chain + .get_instance_mut(data.address) + .expect("Instance known to exist"); + let mut collector = v1::trie::SizeCollector::default(); + instance.state = + data.mutable_state.freeze(&mut data.loader, &mut collector); + // TODO: Charge energy for this + } + // Add update event + data.chain_events.push(update_event); + Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + }) + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed: _, + logs, + config, + interrupt, + } => { + let interrupt_event = ChainEvent::Interrupted { + address: data.address, + logs, + }; + data.chain_events.push(interrupt_event); + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + let response = { + // Check if receiver account exists + if !data.chain.accounts.contains_key(&to) { + InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + } + } else if data + .chain + .get_instance(data.address) + .expect("Contract is known to exist") + .self_balance + < amount + { + InvokeResponse::Failure { + kind: v1::InvokeFailure::InsufficientAmount, + } + } else { + // Move the CCD + data.chain + .get_account_mut(to) + .expect("Account is already known to exist") + .balance += amount; + let instance = data + .chain + .get_instance_mut(data.address) + .expect("Contract is known to exist"); + instance.self_balance -= amount; + + // Add transfer event + data.chain_events.push(ChainEvent::Transferred { + from: data.address, + amount, + to, + }); + + InvokeResponse::Success { + new_balance: instance.self_balance, + data: None, + } + } + }; + let success = matches!(response, InvokeResponse::Success { .. }); + // Add resume event + data.chain_events.push(ChainEvent::Resumed { + address: data.address, + success, + }); + + // Resume + v1::resume_receive( + config, + response, + InterpreterEnergy::from(remaining_energy), + data.mutable_state, + false, // never changes on transfers + data.loader, + ) + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => todo!(), + v1::Interrupt::Upgrade { module_ref } => todo!(), + v1::Interrupt::QueryAccountBalance { address } => todo!(), + v1::Interrupt::QueryContractBalance { address } => todo!(), + v1::Interrupt::QueryExchangeRates => todo!(), + } + } + x => Ok(x), + }, + Err(e) => Err(e), + } + } + + let mut res = v1::invoke_receive( + std::sync::Arc::new(artifact.clone()), receive_ctx, v1::ReceiveInvocation { amount, @@ -322,201 +453,116 @@ impl Chain { limit_logs_and_return_values: false, support_queries: true, }, - ) - .map_err(|e| ContractUpdateError::StringError(e.to_string()))?; + ); - let (res, transaction_fee) = match res { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - if state_changed { - let instance = self.get_instance_mut(address)?; - let mut collector = v1::trie::SizeCollector::default(); - instance.state = mutable_state.freeze(&mut loader, &mut collector); - // TODO: Do we need to charge for storage? - } - ( - Ok(SuccessfulContractUpdate { - host_events: Vec::new(), // TODO: add host events - transfers: Vec::new(), /* TODO: add transfers (or add fn to compute - * based on host events) */ - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, - logs, - }), - transaction_fee, - ) + let mut data = ProcessReceiveData { + address, + contract_name: instance.contract_name.clone(), + amount, + entrypoint: entrypoint.to_owned(), + chain: self, + chain_events: Vec::new(), + mutable_state: &mut mutable_state, + loader, + }; + + // Is false if there are still interrupts to process. + let mut last_processing_round = !matches!(res, Ok(v1::ReceiveResult::Interrupt { .. })); + + loop { + res = process(&mut data, res); + if last_processing_round { + break; + } else { + last_processing_round = !matches!(res, Ok(v1::ReceiveResult::Interrupt { .. })); } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs: interrupt_logs, - config, - interrupt, - } => { - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - - let response = { - // Check if receiver account exists - if !self.accounts.contains_key(&to) { - InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - } - } else if self.get_instance(address)?.self_balance < amount { - InvokeResponse::Failure { - kind: v1::InvokeFailure::InsufficientAmount, - } - } else { - // Move the CCD - self.get_account_mut(to)?.balance += amount; - let instance = self.get_instance_mut(address)?; - instance.self_balance -= amount; - - InvokeResponse::Success { - new_balance: instance.self_balance, - data: None, - } - } - }; + } + let chain_events = data.chain_events; - // Resume execution - let res = match v1::resume_receive( - config, - response, - InterpreterEnergy::from(remaining_energy), - &mut mutable_state, + let (res, transaction_fee) = match res { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Ok(SuccessfulContractUpdate { + chain_events, + transfers: Vec::new(), /* TODO: add transfers (or + * add fn to compute + * based on host events) */ + energy_used, + transaction_fee, + return_value: ContractReturnValue(return_value), state_changed, - loader, - ) { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs: resume_logs, - state_changed, - return_value, - remaining_energy, - } => { - let energy_used = - Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - if state_changed { - let instance = self.get_instance_mut(address)?; - let mut collector = v1::trie::SizeCollector::default(); - instance.state = - mutable_state.freeze(&mut loader, &mut collector); - // TODO: Do we need to charge for - // storage? - } - ( - Ok(SuccessfulContractUpdate { - host_events: vec![ - ChainEvent::Resumed { - address, - success: true, - }, - ChainEvent::Interrupted { - address, - logs: interrupt_logs, - }, - ], - transfers: vec![(to, amount)], - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, /* TODO: Which state_changed should be - * returned here? */ - logs: resume_logs, - }), - transaction_fee, - ) - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs, - config, - interrupt, - } => todo!(), - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => todo!("reason: {}", reason), - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => todo!(), - v1::ReceiveResult::OutOfEnergy => todo!(), + logs, + }), + transaction_fee, + ) + } + v1::ReceiveResult::Interrupt { .. } => panic!("Should never happen"), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. }, - Err(_) => todo!(), - }; - res - // TODO: Do we need to check energy here? - } - _ => todo!("Handle other interrupts"), + )), + transaction_fee, + ) } - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) - } - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) - } - v1::ReceiveResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOFEnergy { - energy_used: energy_reserved, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) - } + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => { + let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )), + transaction_fee, + ) + } + v1::ReceiveResult::OutOfEnergy => { + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_reserved, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )), + transaction_fee, + ) + } + }, + Err(e) => ( + Err(ContractUpdateError::StringError(e.to_string())), + self.calculate_energy_cost(energy_reserved), + ), }; + // Charge the transaction fee and amount self.get_account_mut(invoker)?.balance -= transaction_fee + amount; res @@ -606,9 +652,9 @@ impl Chain { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); let transaction_fee = self.calculate_energy_cost(energy_used); Ok(SuccessfulContractUpdate { - host_events: Vec::new(), // TODO: add host events - transfers: Vec::new(), /* TODO: add transfers (or add fn to compute - * based on host events) */ + chain_events: Vec::new(), // TODO: add host events + transfers: Vec::new(), /* TODO: add transfers (or add fn to compute + * based on host events) */ energy_used, transaction_fee, return_value: ContractReturnValue(return_value), @@ -913,16 +959,22 @@ pub enum ChainEvent { Updated { address: ContractAddress, contract: OwnedContractName, - entrypoing: OwnedEntrypointName, + entrypoint: OwnedEntrypointName, amount: Amount, }, + Transferred { + from: ContractAddress, + amount: Amount, + to: AccountAddress, + }, } // TODO: Consider adding function to aggregate all logs from the host_events. +#[derive(Debug)] pub struct SuccessfulContractUpdate { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. - pub host_events: Vec, + pub chain_events: Vec, pub transfers: Vec<(AccountAddress, Amount)>, /// Energy used. pub energy_used: Energy, @@ -956,6 +1008,7 @@ pub struct SuccessfulContractInit { pub transaction_fee: Amount, } +#[derive(Debug)] pub struct ContractReturnValue(Vec); #[derive(Debug, PartialEq, Eq)] @@ -1201,4 +1254,52 @@ mod tests { // Assert that the updated state is persisted. assert_eq!(res_view.return_value.0, [2, 0, 0, 0]); } + + #[test] + fn update_with_account_transfer_to_missing_account_fails() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(1); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain.contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. + transfer_amount, + Energy::from(4000000u64), + ); + + assert!(matches!(res_update, Err(_))); + + // assert_eq!( + // chain.account_balance(ACC_0), + // Some( + // initial_balance + // - res_deploy.transaction_fee + // - res_init.transaction_fee + // - res_update.??? + // ) + // ); + // assert_eq!(chain.contracts.len(), 1); + // assert!(!res_update.state_changed); + // assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); + // Assert that the updated state is persisted. + } } From 225e347189871d7f9907767997fabedd271f69f0 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 8 Dec 2022 16:01:13 +0100 Subject: [PATCH 009/208] Refactor processing of receive calls --- contract-testing/src/lib.rs | 290 ++++++++++++++++++------------------ 1 file changed, 141 insertions(+), 149 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index a51b4054..36417b97 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -298,144 +298,6 @@ impl Chain { let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); - struct ProcessReceiveData<'a, 'b, 'c> { - address: ContractAddress, - contract_name: OwnedContractName, - amount: Amount, - entrypoint: OwnedEntrypointName, - chain: &'a mut Chain, - chain_events: Vec, - mutable_state: &'b mut MutableState, - loader: v1::trie::Loader<&'c [u8]>, - } - - fn process( - data: &mut ProcessReceiveData, - res: ExecResult>, - ) -> ExecResult> { - match res { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - let update_event = ChainEvent::Updated { - address: data.address, - contract: data.contract_name.clone(), - entrypoint: data.entrypoint.clone(), - amount: data.amount, - }; - if state_changed { - let instance = data - .chain - .get_instance_mut(data.address) - .expect("Instance known to exist"); - let mut collector = v1::trie::SizeCollector::default(); - instance.state = - data.mutable_state.freeze(&mut data.loader, &mut collector); - // TODO: Charge energy for this - } - // Add update event - data.chain_events.push(update_event); - Ok(v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - }) - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed: _, - logs, - config, - interrupt, - } => { - let interrupt_event = ChainEvent::Interrupted { - address: data.address, - logs, - }; - data.chain_events.push(interrupt_event); - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - let response = { - // Check if receiver account exists - if !data.chain.accounts.contains_key(&to) { - InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - } - } else if data - .chain - .get_instance(data.address) - .expect("Contract is known to exist") - .self_balance - < amount - { - InvokeResponse::Failure { - kind: v1::InvokeFailure::InsufficientAmount, - } - } else { - // Move the CCD - data.chain - .get_account_mut(to) - .expect("Account is already known to exist") - .balance += amount; - let instance = data - .chain - .get_instance_mut(data.address) - .expect("Contract is known to exist"); - instance.self_balance -= amount; - - // Add transfer event - data.chain_events.push(ChainEvent::Transferred { - from: data.address, - amount, - to, - }); - - InvokeResponse::Success { - new_balance: instance.self_balance, - data: None, - } - } - }; - let success = matches!(response, InvokeResponse::Success { .. }); - // Add resume event - data.chain_events.push(ChainEvent::Resumed { - address: data.address, - success, - }); - - // Resume - v1::resume_receive( - config, - response, - InterpreterEnergy::from(remaining_energy), - data.mutable_state, - false, // never changes on transfers - data.loader, - ) - } - v1::Interrupt::Call { - address, - parameter, - name, - amount, - } => todo!(), - v1::Interrupt::Upgrade { module_ref } => todo!(), - v1::Interrupt::QueryAccountBalance { address } => todo!(), - v1::Interrupt::QueryContractBalance { address } => todo!(), - v1::Interrupt::QueryExchangeRates => todo!(), - } - } - x => Ok(x), - }, - Err(e) => Err(e), - } - } - let mut res = v1::invoke_receive( std::sync::Arc::new(artifact.clone()), receive_ctx, @@ -466,17 +328,7 @@ impl Chain { loader, }; - // Is false if there are still interrupts to process. - let mut last_processing_round = !matches!(res, Ok(v1::ReceiveResult::Interrupt { .. })); - - loop { - res = process(&mut data, res); - if last_processing_round { - break; - } else { - last_processing_round = !matches!(res, Ok(v1::ReceiveResult::Interrupt { .. })); - } - } + res = data.process(res); let chain_events = data.chain_events; let (res, transaction_fee) = match res { @@ -777,6 +629,146 @@ impl Chain { } } +struct ProcessReceiveData<'a, 'b, 'c> { + address: ContractAddress, + contract_name: OwnedContractName, + amount: Amount, + entrypoint: OwnedEntrypointName, + chain: &'a mut Chain, + chain_events: Vec, + mutable_state: &'b mut MutableState, + loader: v1::trie::Loader<&'c [u8]>, +} + +impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { + fn process( + &mut self, + res: ExecResult>, + ) -> ExecResult> { + match res { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let update_event = ChainEvent::Updated { + address: self.address, + contract: self.contract_name.clone(), + entrypoint: self.entrypoint.clone(), + amount: self.amount, + }; + if state_changed { + let instance = self + .chain + .get_instance_mut(self.address) + .expect("Instance known to exist"); + let mut collector = v1::trie::SizeCollector::default(); + instance.state = + self.mutable_state.freeze(&mut self.loader, &mut collector); + // TODO: Charge energy for this + } + // Add update event + self.chain_events.push(update_event); + Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + }) + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed: _, + logs, + config, + interrupt, + } => { + let interrupt_event = ChainEvent::Interrupted { + address: self.address, + logs, + }; + self.chain_events.push(interrupt_event); + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + let response = { + // Check if receiver account exists + if !self.chain.accounts.contains_key(&to) { + InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + } + } else if self + .chain + .get_instance(self.address) + .expect("Contract is known to exist") + .self_balance + < amount + { + InvokeResponse::Failure { + kind: v1::InvokeFailure::InsufficientAmount, + } + } else { + // Move the CCD + self.chain + .get_account_mut(to) + .expect("Account is already known to exist") + .balance += amount; + let instance = self + .chain + .get_instance_mut(self.address) + .expect("Contract is known to exist"); + instance.self_balance -= amount; + + // Add transfer event + self.chain_events.push(ChainEvent::Transferred { + from: self.address, + amount, + to, + }); + + InvokeResponse::Success { + new_balance: instance.self_balance, + data: None, + } + } + }; + let success = matches!(response, InvokeResponse::Success { .. }); + // Add resume event + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + // Resume + self.process(v1::resume_receive( + config, + response, + InterpreterEnergy::from(remaining_energy), + &mut self.mutable_state.clone(), + false, // never changes on transfers + self.loader, + )) + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => todo!(), + v1::Interrupt::Upgrade { module_ref } => todo!(), + v1::Interrupt::QueryAccountBalance { address } => todo!(), + v1::Interrupt::QueryContractBalance { address } => todo!(), + v1::Interrupt::QueryExchangeRates => todo!(), + } + } + x => Ok(x), + }, + Err(e) => Err(e), + } + } +} + #[derive(Debug)] struct ModuleMissing; From 0820cefdff69c79c10867fd5722a2bc794f05780 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 8 Dec 2022 17:41:43 +0100 Subject: [PATCH 010/208] Handle fallback entrypoints and add more checks in test --- contract-testing/src/lib.rs | 110 ++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 30 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 36417b97..9ecec907 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -34,6 +34,7 @@ pub struct Chain { next_contract_index: u64, } +#[derive(Clone)] pub struct ContractInstance { module_reference: ModuleReference, contract_name: OwnedContractName, @@ -248,8 +249,7 @@ impl Chain { res } - /// Can we get the return value here? - pub fn contract_update( + fn contract_update( &mut self, invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? address: ContractAddress, @@ -267,7 +267,21 @@ impl Chain { if account_info.balance < self.calculate_energy_cost(energy_reserved) { return Err(ContractUpdateError::InsufficientFunds); } - // TODO: Check if entrypoint exists or if the fallback should be used. + // Construct the receive name (or fallback receive name) and ensure its presence + // in the contract. + let receive_name = { + let artifact = self.get_artifact(instance.module_reference)?; + let contract_name = instance.contract_name.as_contract_name().contract_name(); + let receive_name = format!("{}.{}", contract_name, entrypoint); + let fallback_receive_name = format!("{}.", contract_name); + if artifact.has_entrypoint(receive_name.as_str()) { + OwnedReceiveName::new_unchecked(receive_name) + } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { + OwnedReceiveName::new_unchecked(fallback_receive_name) + } else { + return Err(ContractUpdateError::EntrypointDoesNotExist); + } + }; // Construct the context let receive_ctx = v1::ReceiveContext { @@ -284,11 +298,6 @@ impl Chain { sender_policies: account_info.policies.clone(), }, }; - let receive_name = OwnedReceiveName::new_unchecked(format!( - "{}.{}", - instance.contract_name.as_contract_name().contract_name(), - entrypoint - )); // TODO: Charge energy for module lookup. let artifact = self.get_artifact(instance.module_reference)?; @@ -331,6 +340,10 @@ impl Chain { res = data.process(res); let chain_events = data.chain_events; + // Used for rollback. + let accounts_backup = self.accounts.clone(); + let contracts_backup = self.contracts.clone(); + let (res, transaction_fee) = match res { Ok(r) => match r { v1::ReceiveResult::Success { @@ -344,9 +357,6 @@ impl Chain { ( Ok(SuccessfulContractUpdate { chain_events, - transfers: Vec::new(), /* TODO: add transfers (or - * add fn to compute - * based on host events) */ energy_used, transaction_fee, return_value: ContractReturnValue(return_value), @@ -412,7 +422,7 @@ impl Chain { Err(e) => ( Err(ContractUpdateError::StringError(e.to_string())), self.calculate_energy_cost(energy_reserved), - ), + ), // TODO: Incorrect cost }; // Charge the transaction fee and amount @@ -505,8 +515,6 @@ impl Chain { let transaction_fee = self.calculate_energy_cost(energy_used); Ok(SuccessfulContractUpdate { chain_events: Vec::new(), // TODO: add host events - transfers: Vec::new(), /* TODO: add transfers (or add fn to compute - * based on host events) */ energy_used, transaction_fee, return_value: ContractReturnValue(return_value), @@ -641,6 +649,10 @@ struct ProcessReceiveData<'a, 'b, 'c> { } impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { + /// Process a receive function until completion. + /// + /// *Preconditions*: + /// - Contract instance exists in `chain.contracts`. fn process( &mut self, res: ExecResult>, @@ -798,6 +810,7 @@ impl From for ContractUpdateError { fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } } +#[derive(Clone)] pub struct AccountInfo { /// The account balance. TODO: Do we need the three types of balances? pub balance: Amount, @@ -851,6 +864,8 @@ pub enum ContractUpdateError { ModuleDoesNotExist, /// Contract instance has not been initialized in test environment. InstanceDoesNotExist, + /// Entrypoint does not exist and neither does the fallback entrypoint. + EntrypointDoesNotExist, /// Account has not been created in test environment. AccountDoesNotExist, /// The account does not have enough funds to pay for the energy. @@ -967,7 +982,6 @@ pub struct SuccessfulContractUpdate { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. pub chain_events: Vec, - pub transfers: Vec<(AccountAddress, Amount)>, /// Energy used. pub energy_used: Energy, /// Cost of transaction. @@ -980,6 +994,32 @@ pub struct SuccessfulContractUpdate { pub logs: v0::Logs, } +#[derive(Debug, PartialEq, Eq)] +pub struct Transfer { + pub from: ContractAddress, + pub amount: Amount, + pub to: AccountAddress, +} + +impl SuccessfulContractUpdate { + pub fn transfers(&self) -> Vec { + self.chain_events + .iter() + .filter_map(|e| { + if let ChainEvent::Transferred { from, amount, to } = e { + Some(Transfer { + from: *from, + amount: *amount, + to: *to, + }) + } else { + None + } + }) + .collect() + } +} + #[derive(Debug, PartialEq, Eq)] pub struct SuccessfulModuleDeployment { pub module_reference: ModuleReference, @@ -1240,6 +1280,11 @@ mod tests { chain.account_balance(ACC_1), Some(initial_balance + transfer_amount) ); + assert_eq!(res_update.transfers(), [Transfer { + from: res_init.contract_address, + amount: transfer_amount, + to: ACC_1, + }]); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); @@ -1278,20 +1323,25 @@ mod tests { Energy::from(4000000u64), ); - assert!(matches!(res_update, Err(_))); - - // assert_eq!( - // chain.account_balance(ACC_0), - // Some( - // initial_balance - // - res_deploy.transaction_fee - // - res_init.transaction_fee - // - res_update.??? - // ) - // ); - // assert_eq!(chain.contracts.len(), 1); - // assert!(!res_update.state_changed); - // assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); - // Assert that the updated state is persisted. + match res_update { + Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { + reason, + transaction_fee, + .. + })) => { + assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing + assert_eq!( + // TODO: Handle rollback of account balances. + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - transaction_fee + ) + ); + } + _ => panic!("Expected contract update to fail"), + } } } From b2fafc3397b940bb3a00f9d55edba96377dfb436 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 12 Dec 2022 10:41:05 +0100 Subject: [PATCH 011/208] Handle rollbacks of top-level transactions --- contract-testing/src/lib.rs | 50 ++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 9ecec907..3b9c1238 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -258,15 +258,25 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - // Find contract instance and artifact - // Update balance.... - self.get_instance_mut(address)?.self_balance += amount; - let instance = self.get_instance(address)?; - // Ensure account exists and can pay for the reserved energy + // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) { + // Save policies for later + let account_policies = account_info.policies.clone(); + if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractUpdateError::InsufficientFunds); } + + // Save backups of accounts and contracts in case of rollbacks. + let accounts_backup = self.accounts.clone(); + let contracts_backup = self.contracts.clone(); + + // Move the amount from the invoker to the contract. + self.get_account_mut(invoker)?.balance -= amount; + self.get_instance_mut(address)?.self_balance += amount; + + // Get the instance for its info, which will be used further down. + let instance = self.get_instance(address)?; + // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. let receive_name = { @@ -283,7 +293,7 @@ impl Chain { } }; - // Construct the context + // Construct the receive context let receive_ctx = v1::ReceiveContext { entrypoint: entrypoint.to_owned(), common: v0::ReceiveContext { @@ -295,18 +305,20 @@ impl Chain { self_balance: instance.self_balance, sender: Address::Account(invoker), owner: instance.owner, - sender_policies: account_info.policies.clone(), + sender_policies: account_policies, }, }; // TODO: Charge energy for module lookup. let artifact = self.get_artifact(instance.module_reference)?; + // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); let mut mutable_state = instance.state.thaw(); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); + // Get the initial result from invoking receive let mut res = v1::invoke_receive( std::sync::Arc::new(artifact.clone()), receive_ctx, @@ -326,6 +338,8 @@ impl Chain { }, ); + // Set up some data needed for recursively processing the receive until the end, + // i.e. beyond interrupts. let mut data = ProcessReceiveData { address, contract_name: instance.contract_name.clone(), @@ -337,13 +351,12 @@ impl Chain { loader, }; + // Process the receive invocation to the end. res = data.process(res); let chain_events = data.chain_events; - // Used for rollback. - let accounts_backup = self.accounts.clone(); - let contracts_backup = self.contracts.clone(); - + // Convert the wasm_chain_integration result to the one used here and find + // the transaction fee. let (res, transaction_fee) = match res { Ok(r) => match r { v1::ReceiveResult::Success { @@ -425,8 +438,14 @@ impl Chain { ), // TODO: Incorrect cost }; - // Charge the transaction fee and amount - self.get_account_mut(invoker)?.balance -= transaction_fee + amount; + // Handle rollback + if res.is_err() { + self.accounts = accounts_backup; + self.contracts = contracts_backup; + } + + // Charge the transaction fee irrespective of the result + self.get_account_mut(invoker)?.balance -= transaction_fee; res } @@ -653,6 +672,7 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { /// /// *Preconditions*: /// - Contract instance exists in `chain.contracts`. + /// - Account exists in `chain.accounts`. fn process( &mut self, res: ExecResult>, @@ -724,7 +744,7 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { // Move the CCD self.chain .get_account_mut(to) - .expect("Account is already known to exist") + .expect("Account is known to exist") .balance += amount; let instance = self .chain From c909d3c6e9a4a99409448fa98b1d41e8abcae218 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 12 Dec 2022 11:02:20 +0100 Subject: [PATCH 012/208] Charge for module lookup --- contract-testing/src/lib.rs | 218 +++++++++++++++++------------------- 1 file changed, 101 insertions(+), 117 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 3b9c1238..636d8948 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -15,6 +15,9 @@ use wasm_chain_integration::{ }; use wasm_transform::artifact; +/// A V1 artifact, with concrete types for the generic parameters. +type ArtifactV1 = artifact::Artifact; + pub struct Chain { /// The slot time viewable inside the smart contracts. /// Defaults to `0`. @@ -24,10 +27,7 @@ pub struct Chain { /// Accounts and info about them. accounts: BTreeMap, /// Smart contract modules. - modules: BTreeMap< - ModuleReference, - artifact::Artifact, - >, + modules: BTreeMap, /// Smart contract instances. contracts: BTreeMap, /// Next contract index to use when creating a new instance. @@ -122,10 +122,11 @@ impl Chain { ) -> Result { // Lookup artifact let artifact = self.get_artifact(module_reference)?; + let mut transaction_fee = self.lookup_module_cost(&artifact); // Get the account and check that it has sufficient balance to pay for the - // energy. + // reserved_energy and amount. let account_info = self.get_account(sender)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) { + if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } // Construct the context. @@ -154,8 +155,8 @@ impl Chain { loader, ) .map_err(|e| ContractInitError::StringError(e.to_string()))?; - // Charge account for cost - let (res, transaction_fee) = match res { + // Handle the result and update the transaction fee. + let res = match res { v1::InitResult::Success { logs, return_value: _, /* Ignore return value for now, since our tools do not support @@ -165,7 +166,7 @@ impl Chain { } => { let contract_address = self.create_contract_address(); let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); + transaction_fee += self.calculate_energy_cost(energy_used); let mut collector = v1::trie::SizeCollector::default(); @@ -180,16 +181,15 @@ impl Chain { // Save the contract instance self.contracts.insert(contract_address, contract_instance); + // Subtract the from the invoker. + self.get_account_mut(sender)?.balance -= amount; - ( - Ok(SuccessfulContractInit { - contract_address, - logs, - energy_used, - transaction_fee, - }), + Ok(SuccessfulContractInit { + contract_address, + logs, + energy_used, transaction_fee, - ) + }) } v1::InitResult::Reject { reason, @@ -197,50 +197,41 @@ impl Chain { remaining_energy, } => { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get Logs on failures. + }, + )) } v1::InitResult::Trap { error, remaining_energy, } => { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get Logs on failures. + }, + )) } v1::InitResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOFEnergy { - energy_used: energy_reserved, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_reserved); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_reserved, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get Logs on failures. + }, + )) } }; // Charge the account. @@ -274,13 +265,15 @@ impl Chain { self.get_account_mut(invoker)?.balance -= amount; self.get_instance_mut(address)?.self_balance += amount; - // Get the instance for its info, which will be used further down. + // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; + let artifact = self.get_artifact(instance.module_reference)?; + // Calculate cost of looking up module. + let mut transaction_fee = self.lookup_module_cost(&artifact); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. let receive_name = { - let artifact = self.get_artifact(instance.module_reference)?; let contract_name = instance.contract_name.as_contract_name().contract_name(); let receive_name = format!("{}.{}", contract_name, entrypoint); let fallback_receive_name = format!("{}.", contract_name); @@ -309,9 +302,6 @@ impl Chain { }, }; - // TODO: Charge energy for module lookup. - let artifact = self.get_artifact(instance.module_reference)?; - // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); let mut mutable_state = instance.state.thaw(); @@ -355,9 +345,9 @@ impl Chain { res = data.process(res); let chain_events = data.chain_events; - // Convert the wasm_chain_integration result to the one used here and find - // the transaction fee. - let (res, transaction_fee) = match res { + // Convert the wasm_chain_integration result to the one used here and + // update the transaction fee. + let res = match res { Ok(r) => match r { v1::ReceiveResult::Success { logs, @@ -366,18 +356,15 @@ impl Chain { remaining_energy, } => { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Ok(SuccessfulContractUpdate { - chain_events, - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, - logs, - }), + transaction_fee += self.calculate_energy_cost(energy_used); + Ok(SuccessfulContractUpdate { + chain_events, + energy_used, transaction_fee, - ) + return_value: ContractReturnValue(return_value), + state_changed, + logs, + }) } v1::ReceiveResult::Interrupt { .. } => panic!("Should never happen"), v1::ReceiveResult::Reject { @@ -386,56 +373,48 @@ impl Chain { remaining_energy, } => { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) } v1::ReceiveResult::Trap { error, remaining_energy, } => { let energy_used = Energy::from(energy_reserved.energy - remaining_energy); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) } v1::ReceiveResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOFEnergy { - energy_used: energy_reserved, - transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. - }, - )), - transaction_fee, - ) + transaction_fee += self.calculate_energy_cost(energy_reserved); + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOFEnergy { + energy_used: energy_reserved, + transaction_fee, + logs: v0::Logs::default(), // TODO: Get logs on failures. + }, + )) } }, - Err(e) => ( - Err(ContractUpdateError::StringError(e.to_string())), - self.calculate_energy_cost(energy_reserved), - ), // TODO: Incorrect cost + Err(e) => { + // TODO: what is the correct cost here? + transaction_fee += self.calculate_energy_cost(energy_reserved); + Err(ContractUpdateError::StringError(e.to_string())) + } }; // Handle rollback @@ -620,11 +599,13 @@ impl Chain { ) } - fn get_artifact( - &self, - module_ref: ModuleReference, - ) -> Result<&artifact::Artifact, ModuleMissing> - { + pub fn lookup_module_cost(&self, artifact: &ArtifactV1) -> Amount { + // TODO: Is it just the `.code`? + let energy = Energy::from(artifact.code.len() as u64 / 50); // Comes from Concordium/Cost.hs::lookupModule + self.calculate_energy_cost(energy) + } + + fn get_artifact(&self, module_ref: ModuleReference) -> Result<&ArtifactV1, ModuleMissing> { self.modules.get(&module_ref).ok_or(ModuleMissing) } @@ -1364,4 +1345,7 @@ mod tests { _ => panic!("Expected contract update to fail"), } } + + // TODO: Add tests that check: + // - Correct account balances after init / update failures (when Amount > 0) } From 47b30c6d64f78b71de17809fa261fa19a46afe2b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 12 Dec 2022 17:12:10 +0100 Subject: [PATCH 013/208] Restructure to use an internal aux function A step towards handling invoke_contract interrupts --- contract-testing/Cargo.lock | 1 + contract-testing/Cargo.toml | 1 + contract-testing/src/lib.rs | 118 +++++++++++++++++++++++++----------- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index 01d12070..ddda7b30 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -309,6 +309,7 @@ dependencies = [ "concordium-contracts-common", "concordium_base", "sha2 0.10.6", + "thiserror", "wasm-chain-integration", "wasm-transform", ] diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 99b37550..283e1151 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -12,3 +12,4 @@ wasm-chain-integration = {path = "../concordium-base/smart-contracts/wasm-chain- wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" +thiserror = "1.0" diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 636d8948..15024494 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::bail; use concordium_base::base::{Energy, ExchangeRate}; use concordium_contracts_common::*; use sha2::{Digest, Sha256}; @@ -8,6 +9,7 @@ use std::{ }, path::Path, }; +use thiserror::Error; use wasm_chain_integration::{ v0, v1::{self, trie::MutableState, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, @@ -122,7 +124,7 @@ impl Chain { ) -> Result { // Lookup artifact let artifact = self.get_artifact(module_reference)?; - let mut transaction_fee = self.lookup_module_cost(&artifact); + let mut transaction_fee = self.calculate_energy_cost(self.lookup_module_cost(&artifact)); // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.get_account(sender)?; @@ -240,36 +242,41 @@ impl Chain { res } - fn contract_update( + /// Used for handling contract invokes internally. + /// + /// Precondition: + /// - `invoker` exists + /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// + /// TODO: Use proper error types instead of anyhow. + fn contract_update_aux( &mut self, - invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? + invoker: AccountAddress, + sender: Address, address: ContractAddress, - entrypoint: EntrypointName, - parameter: ContractParameter, + entrypoint: OwnedEntrypointName, + parameter: Vec, amount: Amount, - energy_reserved: Energy, - ) -> Result { - // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.get_account(invoker)?; - // Save policies for later - let account_policies = account_info.policies.clone(); - if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { - return Err(ContractUpdateError::InsufficientFunds); + remaining_energy: &mut Energy, + chain_events: &mut Vec, + ) -> ExecResult> { + // Move the amount from the sender to the contract. + match sender { + Address::Account(addr) => { + self.get_account_mut(addr)?.balance -= amount; + } + Address::Contract(addr) => { + self.get_instance_mut(addr)?.self_balance -= amount; + } } - - // Save backups of accounts and contracts in case of rollbacks. - let accounts_backup = self.accounts.clone(); - let contracts_backup = self.contracts.clone(); - - // Move the amount from the invoker to the contract. - self.get_account_mut(invoker)?.balance -= amount; self.get_instance_mut(address)?.self_balance += amount; // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; let artifact = self.get_artifact(instance.module_reference)?; - // Calculate cost of looking up module. - let mut transaction_fee = self.lookup_module_cost(&artifact); + // Subtract the cost of looking up the module + *remaining_energy = + Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -282,7 +289,7 @@ impl Chain { } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { OwnedReceiveName::new_unchecked(fallback_receive_name) } else { - return Err(ContractUpdateError::EntrypointDoesNotExist); + bail!("Entrypoint does not exist."); } }; @@ -296,9 +303,9 @@ impl Chain { invoker, self_address: address, self_balance: instance.self_balance, - sender: Address::Account(invoker), + sender, owner: instance.owner, - sender_policies: account_policies, + sender_policies: self.get_account(invoker)?.policies.clone(), }, }; @@ -309,15 +316,15 @@ impl Chain { let instance_state = v1::InstanceState::new(loader, inner); // Get the initial result from invoking receive - let mut res = v1::invoke_receive( + let res = v1::invoke_receive( std::sync::Arc::new(artifact.clone()), receive_ctx, v1::ReceiveInvocation { amount, receive_name: receive_name.as_receive_name(), - parameter: ¶meter.0, + parameter: ¶meter, energy: InterpreterEnergy { - energy: energy_reserved.energy, + energy: remaining_energy.energy, }, }, instance_state, @@ -342,8 +349,44 @@ impl Chain { }; // Process the receive invocation to the end. - res = data.process(res); - let chain_events = data.chain_events; + let res = data.process(res); + // Get the chain events out. TODO: Find a better way. + *chain_events = data.chain_events; + res + } + + pub fn contract_update( + &mut self, + invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? + address: ContractAddress, + entrypoint: EntrypointName, + parameter: ContractParameter, + amount: Amount, + mut energy_reserved: Energy, + ) -> Result { + // Save backups of accounts and contracts in case of rollbacks. + let accounts_backup = self.accounts.clone(); + let contracts_backup = self.contracts.clone(); + + // Ensure account exists and can pay for the reserved energy and amount + let account_info = self.get_account(invoker)?; + if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { + return Err(ContractUpdateError::InsufficientFunds); + } + + let mut chain_events = Vec::new(); + let res = self.contract_update_aux( + invoker, + Address::Account(invoker), + address, + entrypoint.to_owned(), + parameter.0, + amount, + &mut energy_reserved, + &mut chain_events, + ); + + let mut transaction_fee = Amount::zero(); // Convert the wasm_chain_integration result to the one used here and // update the transaction fee. @@ -599,10 +642,10 @@ impl Chain { ) } - pub fn lookup_module_cost(&self, artifact: &ArtifactV1) -> Amount { + pub fn lookup_module_cost(&self, artifact: &ArtifactV1) -> Energy { // TODO: Is it just the `.code`? - let energy = Energy::from(artifact.code.len() as u64 / 50); // Comes from Concordium/Cost.hs::lookupModule - self.calculate_energy_cost(energy) + // Comes from Concordium/Cost.hs::lookupModule + Energy::from(artifact.code.len() as u64 / 50) } fn get_artifact(&self, module_ref: ModuleReference) -> Result<&ArtifactV1, ModuleMissing> { @@ -782,7 +825,8 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { } } -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Module has not been deployed.")] struct ModuleMissing; impl From for ContractInitError { @@ -793,14 +837,16 @@ impl From for ContractUpdateError { fn from(_: ModuleMissing) -> Self { Self::ModuleDoesNotExist } } -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Contract instance has not been instantiated.")] struct ContractInstanceMissing; impl From for ContractUpdateError { fn from(_: ContractInstanceMissing) -> Self { Self::InstanceDoesNotExist } } -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Account has not been created.")] struct AccountMissing; impl From for ContractInitError { From 5268cf46e84e56c978bc1dc5f8156ffce5f5e965 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 13 Dec 2022 16:04:58 +0100 Subject: [PATCH 014/208] Handle recursive invokes (but with state-related bugs) --- contract-testing/src/lib.rs | 219 +++++++++++++++++++++++++++++++++--- 1 file changed, 204 insertions(+), 15 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 15024494..e662ffb5 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -12,7 +12,11 @@ use std::{ use thiserror::Error; use wasm_chain_integration::{ v0, - v1::{self, trie::MutableState, ConcordiumAllowedImports, InvokeResponse, ReturnValue}, + v1::{ + self, + trie::{MutableState, SizeCollector}, + ConcordiumAllowedImports, InvokeResponse, ReturnValue, + }, ExecResult, InterpreterEnergy, }; use wasm_transform::artifact; @@ -244,7 +248,7 @@ impl Chain { /// Used for handling contract invokes internally. /// - /// Precondition: + /// Preconditions: /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// @@ -257,7 +261,7 @@ impl Chain { entrypoint: OwnedEntrypointName, parameter: Vec, amount: Amount, - remaining_energy: &mut Energy, + mut remaining_energy: Energy, chain_events: &mut Vec, ) -> ExecResult> { // Move the amount from the sender to the contract. @@ -275,7 +279,7 @@ impl Chain { let instance = self.get_instance(address)?; let artifact = self.get_artifact(instance.module_reference)?; // Subtract the cost of looking up the module - *remaining_energy = + remaining_energy = Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); // Construct the receive name (or fallback receive name) and ensure its presence @@ -338,13 +342,14 @@ impl Chain { // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. let mut data = ProcessReceiveData { + invoker, address, contract_name: instance.contract_name.clone(), amount, entrypoint: entrypoint.to_owned(), chain: self, chain_events: Vec::new(), - mutable_state: &mut mutable_state, + mutable_state, loader, }; @@ -362,7 +367,7 @@ impl Chain { entrypoint: EntrypointName, parameter: ContractParameter, amount: Amount, - mut energy_reserved: Energy, + energy_reserved: Energy, ) -> Result { // Save backups of accounts and contracts in case of rollbacks. let accounts_backup = self.accounts.clone(); @@ -382,7 +387,7 @@ impl Chain { entrypoint.to_owned(), parameter.0, amount, - &mut energy_reserved, + energy_reserved, &mut chain_events, ); @@ -680,18 +685,19 @@ impl Chain { } } -struct ProcessReceiveData<'a, 'b, 'c> { +struct ProcessReceiveData<'a, 'b> { + invoker: AccountAddress, address: ContractAddress, contract_name: OwnedContractName, amount: Amount, entrypoint: OwnedEntrypointName, chain: &'a mut Chain, chain_events: Vec, - mutable_state: &'b mut MutableState, - loader: v1::trie::Loader<&'c [u8]>, + mutable_state: MutableState, + loader: v1::trie::Loader<&'b [u8]>, } -impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { +impl<'a, 'b> ProcessReceiveData<'a, 'b> { /// Process a receive function until completion. /// /// *Preconditions*: @@ -709,6 +715,7 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { return_value, remaining_energy, } => { + println!("Updated contract {}", self.address); let update_event = ChainEvent::Updated { address: self.address, contract: self.contract_name.clone(), @@ -736,11 +743,12 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { } v1::ReceiveResult::Interrupt { remaining_energy, - state_changed: _, + state_changed, logs, config, interrupt, } => { + println!("Interrupting contract {}", self.address); let interrupt_event = ChainEvent::Interrupted { address: self.address, logs, @@ -811,7 +819,128 @@ impl<'a, 'b, 'c> ProcessReceiveData<'a, 'b, 'c> { parameter, name, amount, - } => todo!(), + } => { + println!( + "Calling contract {}\n\twith parameter: {:?}", + address, parameter + ); + if state_changed { + println!("Saving state"); + let mut collector = SizeCollector::default(); + let persistent_state = + self.mutable_state.freeze(&mut self.loader, &mut collector); + // TODO: Charge for size of new state. + self.chain.get_instance_mut(address)?.state = persistent_state; + } + + let res = self.chain.contract_update_aux( + self.invoker, + Address::Contract(self.address), + address, + name, + parameter, + amount, + Energy::from(remaining_energy), + &mut self.chain_events, + ); + + let (success, response, energy_after_invoke, state_changed) = match res + { + Ok(r) => match r { + v1::ReceiveResult::Success { + return_value, + remaining_energy, + state_changed, + .. + } => ( + true, + InvokeResponse::Success { + new_balance: self + .chain + .get_instance(self.address)? + .self_balance, + data: Some(return_value), + }, + remaining_energy, + state_changed, + ), + v1::ReceiveResult::Interrupt { .. } => { + panic!("Internal error: Should never return on interrupts.") + } + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => ( + false, + InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + remaining_energy, + false, + ), + v1::ReceiveResult::Trap { + remaining_energy, .. + } => ( + false, + InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + remaining_energy, + false, + ), + v1::ReceiveResult::OutOfEnergy => ( + false, + InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + 0, + false, + ), // TODO: What is the correct error here? + }, + Err(e) => ( + false, + InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + 0, + false, + ), // TODO: Correct energy here? + }; + + // Add resume event + let resume_event = ChainEvent::Resumed { + address: self.address, + success, + }; + + println!( + "Resuming contract {}\n\tafter {}", + self.address, + if success { + "succesful invocation" + } else { + "failed invocation" + } + ); + self.chain_events.push(resume_event); + + let mut new_mutable_state = + self.chain.get_instance(self.address)?.state.thaw(); + self.mutable_state = new_mutable_state.clone(); + + self.process(v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut new_mutable_state, + state_changed, + self.loader, + )) + } v1::Interrupt::Upgrade { module_ref } => todo!(), v1::Interrupt::QueryAccountBalance { address } => todo!(), v1::Interrupt::QueryContractBalance { address } => todo!(), @@ -1297,7 +1426,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), transfer_amount, - Energy::from(4000000u64), + Energy::from(10000u64), ) .expect("Updating valid contract should work"); @@ -1367,7 +1496,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. transfer_amount, - Energy::from(4000000u64), + Energy::from(10000u64), ); match res_update { @@ -1394,4 +1523,64 @@ mod tests { // TODO: Add tests that check: // - Correct account balances after init / update failures (when Amount > 0) + // + #[test] + fn update_with_reentry_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "fib/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_fib"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + ContractParameter::from_typed(&3u64), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + assert_eq!(res_update.return_value.0, u64::to_le_bytes(3)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, u64::to_le_bytes(3)); + } } From 69ca9e21252220872fe5afd9bd4f2f6f936770c2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 14 Dec 2022 11:38:23 +0100 Subject: [PATCH 015/208] Handle state on recursion without rollbacks --- contract-testing/src/lib.rs | 142 +++++++++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 35 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index e662ffb5..08580b2e 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -252,6 +252,9 @@ impl Chain { /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// + /// Returns: + /// - Everything the types can encode apart from + /// `Ok(v1::ReceiveResult::Interrupt)` /// TODO: Use proper error types instead of anyhow. fn contract_update_aux( &mut self, @@ -804,15 +807,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { success, }); - // Resume - self.process(v1::resume_receive( + let resume_res = v1::resume_receive( config, response, InterpreterEnergy::from(remaining_energy), - &mut self.mutable_state.clone(), + &mut self.mutable_state, false, // never changes on transfers self.loader, - )) + ); + + // Resume + self.process(resume_res) } v1::Interrupt::Call { address, @@ -825,7 +830,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { address, parameter ); if state_changed { - println!("Saving state"); + println!("Saving state prior to invoke"); let mut collector = SizeCollector::default(); let persistent_state = self.mutable_state.freeze(&mut self.loader, &mut collector); @@ -852,18 +857,21 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { remaining_energy, state_changed, .. - } => ( - true, - InvokeResponse::Success { - new_balance: self - .chain - .get_instance(self.address)? - .self_balance, - data: Some(return_value), - }, - remaining_energy, - state_changed, - ), + } => { + println!("Invoke returned with value: {:?}", return_value); + ( + true, + InvokeResponse::Success { + new_balance: self + .chain + .get_instance(self.address)? + .self_balance, + data: Some(return_value), + }, + remaining_energy, + state_changed, + ) + } v1::ReceiveResult::Interrupt { .. } => { panic!("Internal error: Should never return on interrupts.") } @@ -928,18 +936,21 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { ); self.chain_events.push(resume_event); - let mut new_mutable_state = + // Update the mutable state, since it might have been changed on + // reentry. + self.mutable_state = self.chain.get_instance(self.address)?.state.thaw(); - self.mutable_state = new_mutable_state.clone(); - self.process(v1::resume_receive( + let resume_res = v1::resume_receive( config, response, InterpreterEnergy::from(energy_after_invoke), - &mut new_mutable_state, + &mut self.mutable_state, state_changed, self.loader, - )) + ); + + self.process(resume_res) } v1::Interrupt::Upgrade { module_ref } => todo!(), v1::Interrupt::QueryAccountBalance { address } => todo!(), @@ -1265,7 +1276,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res = chain - .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); assert_eq!(chain.modules.len(), 1); @@ -1282,7 +1293,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1309,7 +1320,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1344,7 +1355,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1405,7 +1416,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1476,7 +1487,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1496,7 +1507,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. transfer_amount, - Energy::from(10000u64), + Energy::from(100000u64), ); match res_update { @@ -1507,7 +1518,6 @@ mod tests { })) => { assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing assert_eq!( - // TODO: Handle rollback of account balances. chain.account_balance(ACC_0), Some( initial_balance @@ -1525,13 +1535,13 @@ mod tests { // - Correct account balances after init / update failures (when Amount > 0) // #[test] - fn update_with_reentry_works() { + fn update_with_fib_reentry_works() { let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "fib/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/fib/a.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -1550,7 +1560,68 @@ mod tests { ACC_0, res_init.contract_address, EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&3u64), + ContractParameter::from_typed(&6u64), + Amount::zero(), + Energy::from(4000000u64), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + let expected_res = u64::to_le_bytes(13); + assert_eq!(res_update.return_value.0, expected_res); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, expected_res); + } + + #[test] + fn update_with_integrate_reentry_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("recurse"), + ContractParameter::from_typed(&10u32), Amount::zero(), Energy::from(1000000u64), ) @@ -1579,8 +1650,9 @@ mod tests { ); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); - assert_eq!(res_update.return_value.0, u64::to_le_bytes(3)); + let expected_res = 10 + 7 + 11 + 3 + 7 + 11; + assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, u64::to_le_bytes(3)); + assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } } From 2efbbea07821d3f72b8f4a725147c22f3d350ef5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 15 Dec 2022 10:38:12 +0100 Subject: [PATCH 016/208] Collect chain events, improve debug printing, add rollback test --- contract-testing/src/lib.rs | 93 +++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 08580b2e..2b7f815f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -359,7 +359,7 @@ impl Chain { // Process the receive invocation to the end. let res = data.process(res); // Get the chain events out. TODO: Find a better way. - *chain_events = data.chain_events; + chain_events.append(&mut data.chain_events); res } @@ -376,6 +376,11 @@ impl Chain { let accounts_backup = self.accounts.clone(); let contracts_backup = self.contracts.clone(); + println!( + "Updating contract {}, with parameter: {:?}", + address, parameter.0 + ); + // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { @@ -493,6 +498,11 @@ impl Chain { let instance = self.get_instance(address)?; let artifact = self.get_artifact(instance.module_reference)?; + println!( + "Invoking contract {} with parameter: {:?}", + address, parameter.0 + ); + // Ensure account exists and can pay for the reserved energy let account_info = self.get_account(invoker)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) { @@ -718,7 +728,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { return_value, remaining_energy, } => { - println!("Updated contract {}", self.address); + println!("\tSuccessful contract update {}", self.address); let update_event = ChainEvent::Updated { address: self.address, contract: self.contract_name.clone(), @@ -751,7 +761,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, interrupt, } => { - println!("Interrupting contract {}", self.address); + println!("\tInterrupting contract {}", self.address); let interrupt_event = ChainEvent::Interrupted { address: self.address, logs, @@ -825,12 +835,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { name, amount, } => { - println!( - "Calling contract {}\n\twith parameter: {:?}", - address, parameter - ); if state_changed { - println!("Saving state prior to invoke"); + println!("\t\tState was changed. Saving prior to another call."); let mut collector = SizeCollector::default(); let persistent_state = self.mutable_state.freeze(&mut self.loader, &mut collector); @@ -838,6 +844,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain.get_instance_mut(address)?.state = persistent_state; } + println!( + "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", + address, parameter + ); + let res = self.chain.contract_update_aux( self.invoker, Address::Contract(self.address), @@ -858,7 +869,10 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { state_changed, .. } => { - println!("Invoke returned with value: {:?}", return_value); + println!( + "\t\tInvoke returned with value: {:?}", + return_value + ); ( true, InvokeResponse::Success { @@ -926,7 +940,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }; println!( - "Resuming contract {}\n\tafter {}", + "\tResuming contract {}\n\t\tafter {}", self.address, if success { "succesful invocation" @@ -1655,4 +1669,63 @@ mod tests { // Assert that the updated state is persisted. assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } + + #[test] + fn update_with_rollback_and_reentry_works() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("inc-fail-on-zero"), + ContractParameter::from_typed(&3u32), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Invoking get should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(res_update.state_changed); + let expected_res = 7; + assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); + } } From 9250ed3987e8e9bbee780f6becc6fa5090a00557 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 15 Dec 2022 11:03:16 +0100 Subject: [PATCH 017/208] Only save chain events for successful sub calls --- contract-testing/src/lib.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 2b7f815f..11d59e3c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -358,8 +358,10 @@ impl Chain { // Process the receive invocation to the end. let res = data.process(res); - // Get the chain events out. TODO: Find a better way. - chain_events.append(&mut data.chain_events); + // Append the new chain events if the invocation succeeded. + if matches!(res, Ok(v1::ReceiveResult::Success { .. })) { + chain_events.append(&mut data.chain_events); + } res } @@ -923,7 +925,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { false, ), // TODO: What is the correct error here? }, - Err(e) => ( + Err(_e) => ( false, InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, @@ -1673,13 +1675,15 @@ mod tests { #[test] fn update_with_rollback_and_reentry_works() { let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); - let initial_balance = Amount::from_ccd(10000); + let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); + let input_param: u32 = 8; + let res_init = chain .contract_init( ACC_0, @@ -1696,9 +1700,9 @@ mod tests { ACC_0, res_init.contract_address, EntrypointName::new_unchecked("inc-fail-on-zero"), - ContractParameter::from_typed(&3u32), + ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(1000000u64), + Energy::from(100000000u64), ) .expect("Updating valid contract should work"); @@ -1723,7 +1727,7 @@ mod tests { ) ); assert!(res_update.state_changed); - let expected_res = 7; + let expected_res = 2u32.pow(input_param) - 1; assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); // Assert that the updated state is persisted. assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); From 9db1a179cfb07890a31c0af3f9dbfaa7fc1fcfd1 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 9 Jan 2023 16:31:26 +0100 Subject: [PATCH 018/208] Implement the account balance query --- contract-testing/src/lib.rs | 83 ++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 11d59e3c..cf70091f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -24,6 +24,11 @@ use wasm_transform::artifact; /// A V1 artifact, with concrete types for the generic parameters. type ArtifactV1 = artifact::Artifact; +// Energy constants from Cost.hs in concordium-base. +const QUERY_ACCOUNT_BALANCE_COST: u64 = 200; +const QUERY_CONTRACT_BALANCE_COST: u64 = 200; +const QUERY_EXCHANGE_RATE_COST: u64 = 100; + pub struct Chain { /// The slot time viewable inside the smart contracts. /// Defaults to `0`. @@ -264,6 +269,10 @@ impl Chain { entrypoint: OwnedEntrypointName, parameter: Vec, amount: Amount, + // The CCD amount reserved from the invoker account. While the amount + // is reserved, it is not subtracted in the chain.accounts map. + // Used to handle account balance queries for the invoker account. + invoker_amount_reserved: Amount, mut remaining_energy: Energy, chain_events: &mut Vec, ) -> ExecResult> { @@ -349,6 +358,7 @@ impl Chain { address, contract_name: instance.contract_name.clone(), amount, + invoker_amount_reserved, entrypoint: entrypoint.to_owned(), chain: self, chain_events: Vec::new(), @@ -385,7 +395,8 @@ impl Chain { // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { + let invoker_amount_reserved = self.calculate_energy_cost(energy_reserved) + amount; + if account_info.balance < invoker_amount_reserved { return Err(ContractUpdateError::InsufficientFunds); } @@ -397,6 +408,7 @@ impl Chain { entrypoint.to_owned(), parameter.0, amount, + invoker_amount_reserved, energy_reserved, &mut chain_events, ); @@ -701,15 +713,19 @@ impl Chain { } struct ProcessReceiveData<'a, 'b> { - invoker: AccountAddress, - address: ContractAddress, - contract_name: OwnedContractName, - amount: Amount, - entrypoint: OwnedEntrypointName, - chain: &'a mut Chain, - chain_events: Vec, - mutable_state: MutableState, - loader: v1::trie::Loader<&'b [u8]>, + invoker: AccountAddress, + address: ContractAddress, + contract_name: OwnedContractName, + amount: Amount, + /// The CCD amount reserved from the invoker account. While the amount is + /// reserved, it is not subtracted in the chain.accounts map. + /// Used to handle account balance queries for the invoker account. + invoker_amount_reserved: Amount, + entrypoint: OwnedEntrypointName, + chain: &'a mut Chain, + chain_events: Vec, + mutable_state: MutableState, + loader: v1::trie::Loader<&'b [u8]>, } impl<'a, 'b> ProcessReceiveData<'a, 'b> { @@ -858,6 +874,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { name, parameter, amount, + self.invoker_amount_reserved, Energy::from(remaining_energy), &mut self.chain_events, ); @@ -969,7 +986,51 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.process(resume_res) } v1::Interrupt::Upgrade { module_ref } => todo!(), - v1::Interrupt::QueryAccountBalance { address } => todo!(), + v1::Interrupt::QueryAccountBalance { address } => { + // When querying an account, the amounts from any `invoke_transfer`s + // should be included. That is handled by + // the `chain` struct already. transaction. + // However, that is hand + let response = match self.chain.account_balance(address) { + Some(acc_bal) => { + // If you query the invoker account, it should also + // take into account the send-amount and the amount reserved for + // the reserved max energy. The value of which is held in + // `self.invoker_amount_reserved`. + let acc_bal = if address == self.invoker { + acc_bal - self.invoker_amount_reserved + } else { + acc_bal + }; + // TODO: Do we need non-zero staked and shielded balances? + let balances = + to_bytes(&(acc_bal, Amount::zero(), Amount::zero())); + InvokeResponse::Success { + new_balance: self + .chain + .get_instance(self.address)? + .self_balance, + data: Some(balances), + } + } + None => InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + + let energy_after_invoke = remaining_energy - QUERY_ACCOUNT_BALANCE_COST; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.mutable_state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } v1::Interrupt::QueryContractBalance { address } => todo!(), v1::Interrupt::QueryExchangeRates => todo!(), } From 2bb7a2f36d8daa4ee7afd96f15ae0709b5dd980f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 10 Jan 2023 10:20:09 +0100 Subject: [PATCH 019/208] Add `module_deploy_raw` which takes a raw wasm module --- contract-testing/src/lib.rs | 42 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index cf70091f..72201ebc 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -74,20 +74,24 @@ impl Chain { } } - pub fn module_deploy>( + /// Helper function that handles the actual logic of deploying the module + /// bytes. + /// + /// Parameters: + /// - `sender`: the sender account. + /// - `module_bytes`: the module **without** the contract version and + /// module length bytes (8 bytes total). + fn module_deploy_aux( &mut self, sender: AccountAddress, - module_path: P, + module: &[u8], ) -> Result { - // Load file - let module = std::fs::read(module_path)?; - // Deserialize as wasm module (artifact) let artifact = wasm_transform::utils::instantiate_with_metering::( &ConcordiumAllowedImports { support_upgrade: true, }, - &module[8..], // skip the 4 version bytes and 4 len bytes + &module, ) .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; @@ -122,6 +126,32 @@ impl Chain { }) } + /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version + /// bytes and 4 module length bytes. + pub fn module_deploy_raw>( + &mut self, + sender: AccountAddress, + module_path: P, + ) -> Result { + // Load file + let module = std::fs::read(module_path)?; + self.module_deploy_aux(sender, &module) + } + + /// Deploy a wasm module as it is output from `cargo concordium build`, i.e. + /// **including** the prefix of 4 version bytes and 4 module length bytes. + pub fn module_deploy>( + &mut self, + sender: AccountAddress, + module_path: P, + ) -> Result { + // Load file + let module = std::fs::read(module_path)?; + // Here, we skip the 8 bytes that encode the smart contract version and module + // length + self.module_deploy_aux(sender, &module[8..]) + } + pub fn contract_init( &mut self, sender: AccountAddress, From af8ae0b15f1fedfb6f7ab7efeaa6ff7efa4c179b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 10 Jan 2023 10:20:54 +0100 Subject: [PATCH 020/208] Only add interrupt events for transfers and calls --- contract-testing/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 72201ebc..38174497 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -810,13 +810,19 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { interrupt, } => { println!("\tInterrupting contract {}", self.address); + + // Create the interrupt event, which will be included for transfers and calls, + // but not for the remaining interrupts. + // TODO: Or is it included in upgrades as well? let interrupt_event = ChainEvent::Interrupted { address: self.address, logs, }; - self.chain_events.push(interrupt_event); match interrupt { v1::Interrupt::Transfer { to, amount } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + let response = { // Check if receiver account exists if !self.chain.accounts.contains_key(&to) { @@ -883,6 +889,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { name, amount, } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + if state_changed { println!("\t\tState was changed. Saving prior to another call."); let mut collector = SizeCollector::default(); From 7f286eaa0cdb4b3bbc2f75b76335fe3a47783249 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 10 Jan 2023 11:31:29 +0100 Subject: [PATCH 021/208] Add tests for account balance (from scheduler tests) The tests should be moved to concordium-base so that they can be used here. --- contract-testing/src/lib.rs | 395 ++++++++++++++++++++++++++++++++---- 1 file changed, 354 insertions(+), 41 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 38174497..7764c9e3 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -302,7 +302,7 @@ impl Chain { // The CCD amount reserved from the invoker account. While the amount // is reserved, it is not subtracted in the chain.accounts map. // Used to handle account balance queries for the invoker account. - invoker_amount_reserved: Amount, + invoker_amount_reserved_for_nrg: Amount, mut remaining_energy: Energy, chain_events: &mut Vec, ) -> ExecResult> { @@ -388,7 +388,7 @@ impl Chain { address, contract_name: instance.contract_name.clone(), amount, - invoker_amount_reserved, + invoker_amount_reserved_for_nrg, entrypoint: entrypoint.to_owned(), chain: self, chain_events: Vec::new(), @@ -425,8 +425,8 @@ impl Chain { // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; - let invoker_amount_reserved = self.calculate_energy_cost(energy_reserved) + amount; - if account_info.balance < invoker_amount_reserved { + let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } @@ -438,7 +438,7 @@ impl Chain { entrypoint.to_owned(), parameter.0, amount, - invoker_amount_reserved, + invoker_amount_reserved_for_nrg, energy_reserved, &mut chain_events, ); @@ -743,19 +743,19 @@ impl Chain { } struct ProcessReceiveData<'a, 'b> { - invoker: AccountAddress, - address: ContractAddress, - contract_name: OwnedContractName, - amount: Amount, - /// The CCD amount reserved from the invoker account. While the amount is - /// reserved, it is not subtracted in the chain.accounts map. - /// Used to handle account balance queries for the invoker account. - invoker_amount_reserved: Amount, - entrypoint: OwnedEntrypointName, - chain: &'a mut Chain, - chain_events: Vec, - mutable_state: MutableState, - loader: v1::trie::Loader<&'b [u8]>, + invoker: AccountAddress, + address: ContractAddress, + contract_name: OwnedContractName, + amount: Amount, + /// The CCD amount reserved from the invoker account for the energy. While + /// the amount is reserved, it is not subtracted in the chain.accounts + /// map. Used to handle account balance queries for the invoker account. + invoker_amount_reserved_for_nrg: Amount, + entrypoint: OwnedEntrypointName, + chain: &'a mut Chain, + chain_events: Vec, + mutable_state: MutableState, + loader: v1::trie::Loader<&'b [u8]>, } impl<'a, 'b> ProcessReceiveData<'a, 'b> { @@ -823,6 +823,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); + println!("Transferring {} CCD to {}", amount, to); + let response = { // Check if receiver account exists if !self.chain.accounts.contains_key(&to) { @@ -913,7 +915,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { name, parameter, amount, - self.invoker_amount_reserved, + self.invoker_amount_reserved_for_nrg, Energy::from(remaining_energy), &mut self.chain_events, ); @@ -1026,6 +1028,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } v1::Interrupt::Upgrade { module_ref } => todo!(), v1::Interrupt::QueryAccountBalance { address } => { + println!("Querying account balance of {}", address); // When querying an account, the amounts from any `invoke_transfer`s // should be included. That is handled by // the `chain` struct already. transaction. @@ -1034,13 +1037,15 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { Some(acc_bal) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for - // the reserved max energy. The value of which is held in - // `self.invoker_amount_reserved`. + // the reserved max energy. The former is handled in + // `contract_update_aux`, but the latter is represented in + // `self.invoker_amount_reserved_for_nrg`. let acc_bal = if address == self.invoker { - acc_bal - self.invoker_amount_reserved + acc_bal - self.invoker_amount_reserved_for_nrg } else { acc_bal }; + // TODO: Do we need non-zero staked and shielded balances? let balances = to_bytes(&(acc_bal, Amount::zero(), Amount::zero())); @@ -1371,12 +1376,15 @@ impl ContractParameter { mod tests { use super::*; + const NRG_PER_MICRO_CCD: u64 = 2404; const ACC_0: AccountAddress = AccountAddress([0; 32]); const ACC_1: AccountAddress = AccountAddress([1; 32]); + const WASM_TEST_FOLDER: &str = + "../../concordium-node/concordium-consensus/testdata/contracts/v1"; #[test] fn creating_accounts_work() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); @@ -1387,12 +1395,12 @@ mod tests { #[test] fn deploying_valid_module_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); assert_eq!(chain.modules.len(), 1); @@ -1404,12 +1412,12 @@ mod tests { #[test] fn initializing_valid_contract_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1431,12 +1439,12 @@ mod tests { #[test] fn initializing_with_invalid_parameter_fails() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1466,12 +1474,12 @@ mod tests { #[test] fn updating_valid_contract_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1525,14 +1533,14 @@ mod tests { #[test] fn update_with_account_transfer_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1597,13 +1605,13 @@ mod tests { #[test] fn update_with_account_transfer_to_missing_account_fails() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1652,12 +1660,12 @@ mod tests { // #[test] fn update_with_fib_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/fib/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/fib/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1713,12 +1721,12 @@ mod tests { #[test] fn update_with_integrate_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1774,12 +1782,12 @@ mod tests { #[test] fn update_with_rollback_and_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(2404, 1)); + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") // TODO: Add wasm files to the repo for tests. + .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let input_param: u32 = 8; @@ -1832,4 +1840,309 @@ mod tests { // Assert that the updated state is persisted. assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } + + mod account_balance { + use super::*; + + /// Queries the balance of another account and asserts that it is as + /// expected. + #[test] + fn test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to + // get around it here. + // The contract will query the balance of ACC_1 and assert that the three + // balances match this input. + let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Queries the balance of the invoker account, which will have have the + /// expected balance of: + /// prior_balance - amount_sent - amount_to_cover_reserved_NRG. + #[test] + fn invoker_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let update_amount = Amount::from_ccd(123); + let energy_limit = Energy::from(100000u64); + let invoker_reserved_amount = update_amount + chain.calculate_energy_cost(energy_limit); + + // The contract will query the balance of ACC_1, which is also the invoker, and + // assert that the three balances match this input. + let expected_balance = initial_balance - invoker_reserved_amount; + let input_param = (ACC_1, (expected_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_1, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + update_amount, + energy_limit, + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) + ); + assert_eq!( + chain.account_balance(ACC_1), + // Differs from `expected_balance` as it only includes the actual amount charged + // for the NRG use. Not the reserved amount. + Some(initial_balance - res_update.transaction_fee - update_amount) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Makes a transfer to an account, then queries its balance and asserts + /// that it is as expected. + #[test] + fn transfer_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let amount_to_send = Amount::from_ccd(123); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + amount_to_send, // Make sure the contract has CCD to transfer. + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let amount_to_send = Amount::from_ccd(123); + let expected_balance = initial_balance + amount_to_send; + let input_param = ( + ACC_1, + amount_to_send, + (expected_balance, Amount::zero(), Amount::zero()), + ); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + - amount_to_send + ) + ); + assert_eq!( + chain.account_balance(ACC_1), + Some(initial_balance + amount_to_send) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Transferred { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. } + ])); + } + + #[test] + fn balance_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to + // get around it here. + // The contract will query the balance of ACC_1 and assert that the three + // balances match this input. + let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Queries the balance of a missing account and asserts that it returns + /// the correct error. + #[test] + fn missing_account_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!( + "{}/queries-account-balance-missing-account.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // The account to query, which doesn't exist in this test case. + let input_param = ACC_1; + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + } } From f0c72dfb84bbad87dec12273d19dff44fab23f5b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 10 Jan 2023 16:33:45 +0100 Subject: [PATCH 022/208] Implement contract balance query --- contract-testing/src/lib.rs | 257 +++++++++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 1 deletion(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 7764c9e3..7ffce470 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -529,6 +529,8 @@ impl Chain { } /// TODO: Should we make invoker and energy optional? + /// TODO: Only works with basic update functions. Rewrite to use + /// `contract_update_aux`. pub fn contract_invoke( &mut self, invoker: AccountAddress, @@ -697,6 +699,13 @@ impl Chain { self.accounts.get(&address).and_then(|ai| Some(ai.balance)) } + /// Returns the balance of an contract if it exists. + pub fn contract_balance(&self, address: ContractAddress) -> Option { + self.contracts + .get(&address) + .and_then(|ci| Some(ci.self_balance)) + } + pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { Amount::from_micro_ccd( energy.energy * self.micro_ccd_per_energy.numerator() @@ -1075,7 +1084,36 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.process(resume_res) } - v1::Interrupt::QueryContractBalance { address } => todo!(), + v1::Interrupt::QueryContractBalance { address } => { + println!("Querying contract balance of {}", address); + + let response = match self.chain.contract_balance(address) { + Some(balance) => InvokeResponse::Success { + new_balance: self + .chain + .get_instance(self.address)? + .self_balance, + data: Some(to_bytes(&balance)), + }, + None => InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + }; + + let energy_after_invoke = + remaining_energy - QUERY_CONTRACT_BALANCE_COST; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.mutable_state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } v1::Interrupt::QueryExchangeRates => todo!(), } } @@ -2145,4 +2183,221 @@ mod tests { ])); } } + + mod contract_balance { + use super::*; + + /// Test querying the balance of another contract, which exists. Asserts + /// that the balance is as expected. + #[test] + fn test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_init_other = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + init_amount, // Set up another contract with `init_amount` balance + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // check that the other contract has `self_balance == init_amount`. + let input_param = (res_init_other.contract_address, init_amount); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance of the contract instance itself. This + /// should include the amount sent to it in the update transaction. + #[test] + fn query_self_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + let update_amount = Amount::from_ccd(456); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + init_amount, + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // check that the other contract has `self_balance == init_amount`. + let input_param = (res_init.contract_address, init_amount + update_amount); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + update_amount, + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance after a transfer of CCD. + #[test] + fn query_self_after_transfer_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + let update_amount = Amount::from_ccd(456); + let transfer_amount = Amount::from_ccd(78); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!( + "{}/queries-contract-balance-transfer.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + init_amount, + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let input_param = ( + ACC_0, + transfer_amount, + ( + res_init.contract_address, + init_amount + update_amount - transfer_amount, + ), + ); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + update_amount, + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Transferred { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance of a contract that doesn't exist. + #[test] + fn missing_contract_test() { + let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!( + "{}/queries-contract-balance-missing-contract.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // Non-existent contract address. + let input_param = ContractAddress::new(123, 456); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + } } From fad50624a5e99ef5bae8386aa2498226310c6c07 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 10 Jan 2023 17:16:01 +0100 Subject: [PATCH 023/208] Add actual exchange rates used by chain --- contract-testing/src/lib.rs | 107 ++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 7ffce470..d18a613f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -32,17 +32,19 @@ const QUERY_EXCHANGE_RATE_COST: u64 = 100; pub struct Chain { /// The slot time viewable inside the smart contracts. /// Defaults to `0`. - slot_time: SlotTime, - /// MicroCCD per Energy. - micro_ccd_per_energy: ExchangeRate, + slot_time: SlotTime, + /// MicroCCD per Euro ratio. + micro_ccd_per_euro: ExchangeRate, + /// Euro per Energy ratio. + euro_per_energy: ExchangeRate, /// Accounts and info about them. - accounts: BTreeMap, + accounts: BTreeMap, /// Smart contract modules. - modules: BTreeMap, + modules: BTreeMap, /// Smart contract instances. - contracts: BTreeMap, + contracts: BTreeMap, /// Next contract index to use when creating a new instance. - next_contract_index: u64, + next_contract_index: u64, } #[derive(Clone)] @@ -55,25 +57,42 @@ pub struct ContractInstance { } impl Chain { - pub fn new_with_time(slot_time: SlotTime, energy_per_micro_ccd: ExchangeRate) -> Self { + pub fn new_with_time_and_rates( + slot_time: SlotTime, + micro_ccd_per_euro: ExchangeRate, + euro_per_energy: ExchangeRate, + ) -> Self { Self { slot_time, - ..Self::new(energy_per_micro_ccd) + micro_ccd_per_euro, + euro_per_energy, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + contracts: BTreeMap::new(), + next_contract_index: 0, } } - /// Create a new [`Self`] where the `slot_time` defaults to `0`. - pub fn new(energy_per_micro_ccd: ExchangeRate) -> Self { + pub fn new_with_time(slot_time: SlotTime) -> Self { Self { - slot_time: Timestamp::from_timestamp_millis(0), - micro_ccd_per_energy: energy_per_micro_ccd, - accounts: BTreeMap::new(), - modules: BTreeMap::new(), - contracts: BTreeMap::new(), - next_contract_index: 0, + slot_time, + ..Self::new() } } + /// Create a new [`Self`] where + /// - `slot_time` defaults to `0`, + /// - `micro_ccd_per_euro` defaults to `16036807715944130560 / + /// 108919627567`, + /// - `euro_per_energy` defaults to `1 / 50000`. + pub fn new() -> Self { + Self::new_with_time_and_rates( + Timestamp::from_timestamp_millis(0), + ExchangeRate::new_unchecked(16036807715944130560, 108919627567), + ExchangeRate::new_unchecked(1, 50000), + ) + } + /// Helper function that handles the actual logic of deploying the module /// bytes. /// @@ -690,8 +709,12 @@ impl Chain { pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } - pub fn set_energy_per_micro_ccd(&mut self, energy_per_micro_ccd: ExchangeRate) { - self.micro_ccd_per_energy = energy_per_micro_ccd; + pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { + self.euro_per_energy = euro_per_energy; + } + + pub fn set_micro_ccd_per_euro(&mut self, micro_ccd_per_euro: ExchangeRate) { + self.micro_ccd_per_euro = micro_ccd_per_euro; } /// Returns the balance of an account if it exists. @@ -706,10 +729,14 @@ impl Chain { .and_then(|ci| Some(ci.self_balance)) } + // FIXME: Compute without overflow pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { + let micro_ccd_per_energy_numerator = + self.euro_per_energy.numerator() * self.micro_ccd_per_euro.numerator(); + let micro_ccd_per_energy_denominator = + self.euro_per_energy.denominator() * self.micro_ccd_per_euro.denominator(); Amount::from_micro_ccd( - energy.energy * self.micro_ccd_per_energy.numerator() - / self.micro_ccd_per_energy.denominator(), + energy.energy * micro_ccd_per_energy_numerator / micro_ccd_per_energy_denominator, ) } @@ -1422,7 +1449,7 @@ mod tests { #[test] fn creating_accounts_work() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); @@ -1433,7 +1460,7 @@ mod tests { #[test] fn deploying_valid_module_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1450,7 +1477,7 @@ mod tests { #[test] fn initializing_valid_contract_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1477,7 +1504,7 @@ mod tests { #[test] fn initializing_with_invalid_parameter_fails() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1512,7 +1539,7 @@ mod tests { #[test] fn updating_valid_contract_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1571,7 +1598,7 @@ mod tests { #[test] fn update_with_account_transfer_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1643,7 +1670,7 @@ mod tests { #[test] fn update_with_account_transfer_to_missing_account_fails() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1698,7 +1725,7 @@ mod tests { // #[test] fn update_with_fib_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1759,7 +1786,7 @@ mod tests { #[test] fn update_with_integrate_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1820,7 +1847,7 @@ mod tests { #[test] fn update_with_rollback_and_reentry_works() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -1886,7 +1913,7 @@ mod tests { /// expected. #[test] fn test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); @@ -1945,7 +1972,7 @@ mod tests { /// prior_balance - amount_sent - amount_to_cover_reserved_NRG. #[test] fn invoker_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); @@ -2004,7 +2031,7 @@ mod tests { /// that it is as expected. #[test] fn transfer_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); @@ -2072,7 +2099,7 @@ mod tests { #[test] fn balance_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); chain.create_account(ACC_1, AccountInfo::new(initial_balance)); @@ -2130,7 +2157,7 @@ mod tests { /// the correct error. #[test] fn missing_account_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -2191,7 +2218,7 @@ mod tests { /// that the balance is as expected. #[test] fn test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -2249,7 +2276,7 @@ mod tests { /// should include the amount sent to it in the update transaction. #[test] fn query_self_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -2296,7 +2323,7 @@ mod tests { /// Test querying the balance after a transfer of CCD. #[test] fn query_self_after_transfer_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); @@ -2356,7 +2383,7 @@ mod tests { /// Test querying the balance of a contract that doesn't exist. #[test] fn missing_contract_test() { - let mut chain = Chain::new(ExchangeRate::new_unchecked(NRG_PER_MICRO_CCD, 1)); + let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); From 803b35b36b488d266a5c5df73d5e7856cd40ba78 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 11 Jan 2023 13:34:21 +0100 Subject: [PATCH 024/208] Use simpler exchange rate ratio to avoid overflow --- contract-testing/src/lib.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d18a613f..49831c5c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -88,7 +88,7 @@ impl Chain { pub fn new() -> Self { Self::new_with_time_and_rates( Timestamp::from_timestamp_millis(0), - ExchangeRate::new_unchecked(16036807715944130560, 108919627567), + ExchangeRate::new_unchecked(147235241, 1), ExchangeRate::new_unchecked(1, 50000), ) } @@ -729,7 +729,23 @@ impl Chain { .and_then(|ci| Some(ci.self_balance)) } - // FIXME: Compute without overflow + // Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange rates + // available: + // + // To find the mCCD/NRG exchange rate: + // + // euro mCCD euro * mCCD mCCD + // ---- * ---- = ----------- = ---- + // NRG euro NRG * euro NRG + // + // To convert the `energy` parameter to mCCD: + // + // mCCD NRG * mCCD + // NRG * ---- = ---------- = mCCD + // NRG NRG + // + // TODO: If using a mCCD/euro exchange rate with large numbers, then this can + // overflow. pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { let micro_ccd_per_energy_numerator = self.euro_per_energy.numerator() * self.micro_ccd_per_euro.numerator(); @@ -1726,7 +1742,7 @@ mod tests { #[test] fn update_with_fib_reentry_works() { let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); + let initial_balance = Amount::from_ccd(1000000); chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain From 82e4eda0b99fe448566d1b8c71507ba085094002 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 11 Jan 2023 13:42:00 +0100 Subject: [PATCH 025/208] Implement querying exchange rates --- contract-testing/src/lib.rs | 78 +++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 49831c5c..3d6118f1 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1157,7 +1157,30 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.process(resume_res) } - v1::Interrupt::QueryExchangeRates => todo!(), + v1::Interrupt::QueryExchangeRates => { + println!("Querying exchange rates"); + + let exchange_rates = + (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); + + let response = InvokeResponse::Success { + new_balance: self.chain.get_instance(self.address)?.self_balance, + data: Some(to_bytes(&exchange_rates)), + }; + + let energy_after_invoke = remaining_energy - QUERY_EXCHANGE_RATE_COST; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.mutable_state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } } } x => Ok(x), @@ -1922,7 +1945,7 @@ mod tests { assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } - mod account_balance { + mod query_account_balance { use super::*; /// Queries the balance of another account and asserts that it is as @@ -2227,7 +2250,7 @@ mod tests { } } - mod contract_balance { + mod query_contract_balance { use super::*; /// Test querying the balance of another contract, which exists. Asserts @@ -2443,4 +2466,53 @@ mod tests { ])); } } + + mod query_exchange_rates { + + use super::*; + + /// Test querying the exchange rates. + #[test] + fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // Non-existent contract address. + let input_param = (chain.euro_per_energy, chain.micro_ccd_per_euro); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("query"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + } } From b0180066efa562d1316aab3ad2a95689b0517460 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 11 Jan 2023 13:55:35 +0100 Subject: [PATCH 026/208] Improve deployment cost calculation However, it still doesn't exactly match concordium-client or the node (which differ internally). --- contract-testing/src/lib.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 3d6118f1..44ef8ff9 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,5 +1,8 @@ use anyhow::bail; -use concordium_base::base::{Energy, ExchangeRate}; +use concordium_base::{ + base::{Energy, ExchangeRate}, + transactions::{self, cost}, +}; use concordium_contracts_common::*; use sha2::{Digest, Sha256}; use std::{ @@ -115,8 +118,31 @@ impl Chain { .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; // Calculate transaction fee of deployment - let energy = Energy::from(module.len() as u64 / 10); // From: Concordium/Const/deployModuleCost.hs + // TODO: This is still slightly off. + // For the fib module + // - This results in 18259 NRG + // - Concordium-client says 18262 NRG + // - The node charges for 18261 NRG + let energy = { + let payload_size = + 1 + module.len() as u64 + transactions::construct::TRANSACTION_HEADER_SIZE; // +1 for the tag + let number_of_sigs = 1; // Accounts always have one signature here. TODO: Should we allow changing that? + let base_cost = cost::base_cost(payload_size, number_of_sigs); + let deploy_module_cost = cost::deploy_module(payload_size); + let total = base_cost + deploy_module_cost; + println!( + "Deploying module \ + cost:\n\tmodule_size:{}\n\tbase_cost:{}\n\tdeploy_module_cost:{}\n\ttotal:{}", + payload_size, base_cost, deploy_module_cost, total + ); + total + }; let transaction_fee = self.calculate_energy_cost(energy); + println!( + "Deploying module with size {}, resulting in {} NRG.", + module.len(), + energy + ); // Try to subtract cost for account match self.accounts.entry(sender) { From cef63a68fa5d9c92648a491558a6f3e2329cc930 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 11 Jan 2023 17:00:25 +0100 Subject: [PATCH 027/208] Implement contract upgrades --- contract-testing/src/lib.rs | 205 +++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 44ef8ff9..aaf3884d 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -31,6 +31,8 @@ type ArtifactV1 = artifact::Artifact ProcessReceiveData<'a, 'b> { self.process(resume_res) } - v1::Interrupt::Upgrade { module_ref } => todo!(), + v1::Interrupt::Upgrade { module_ref } => { + println!("Upgrading contract to {:?}", module_ref); + + // Add the interrupt event. + self.chain_events.push(interrupt_event); + + // Charge a base cost. + let mut energy_after_invoke = + remaining_energy - INITIALIZE_CONTRACT_INSTANCE_BASE_COST; + + let response = match self.chain.modules.get(&module_ref) { + None => InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(artifact) => { + // Charge for the module lookup. + energy_after_invoke -= + self.chain.lookup_module_cost(&artifact).energy; + + if artifact.export.contains_key( + self.contract_name.as_contract_name().get_chain_name(), + ) { + let instance = self.chain.get_instance_mut(self.address)?; + let old_module_ref = instance.module_reference; + // Update module reference for the instance. + instance.module_reference = module_ref; + + // Charge for the initialization cost. + energy_after_invoke -= + INITIALIZE_CONTRACT_INSTANCE_CREATE_COST; + + let upgrade_event = ChainEvent::Upgraded { + address: self.address, + from: old_module_ref, + to: module_ref, + }; + + self.chain_events.push(upgrade_event); + + InvokeResponse::Success { + new_balance: instance.self_balance, + data: None, + } + } else { + InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, + } + } + } + }; + + let success = matches!(response, InvokeResponse::Success { .. }); + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.mutable_state, + state_changed, + self.loader, + ); + + self.process(resume_res) + } v1::Interrupt::QueryAccountBalance { address } => { println!("Querying account balance of {}", address); // When querying an account, the amounts from any `invoke_transfer`s @@ -1506,7 +1575,6 @@ impl ContractParameter { mod tests { use super::*; - const NRG_PER_MICRO_CCD: u64 = 2404; const ACC_0: AccountAddress = AccountAddress([0; 32]); const ACC_1: AccountAddress = AccountAddress([1; 32]); const WASM_TEST_FOLDER: &str = @@ -2541,4 +2609,137 @@ mod tests { ])); } } + + mod contract_upgrade { + + use super::*; + + /// Test a basic upgrade, ensuring that the new module is in place by + /// checking the available entrypoints. + #[test] + fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + // Deploy the two modules `upgrading_0`, `upgrading_1` + let res_deploy_0 = chain + .module_deploy_raw(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_raw(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + // Initialize `upgrading_0`. + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_a"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + // Upgrade the contract to the `upgrading_1` module by calling the `bump` + // entrypoint. + let res_update_upgrade = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("bump"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + // Call the `newfun` entrypoint which only exists in `upgrading_1`. + let res_update_new = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("newfun"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating the `newfun` from the `upgrading_1` module should work"); + + println!("{:?}", res_update_upgrade.chain_events); + assert!(matches!(res_update_upgrade.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. }, + ])); + assert!(matches!(res_update_new.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// The contract in this test, triggers an upgrade and then in the same + /// invocation, calls a function in the upgraded module. + /// Checking the new module is being used. + #[test] + fn test_self_invoke() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + let res_deploy_1 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + // Invoking `contract.name` + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Making the upgrade + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { .. }, + ChainEvent::Resumed { .. }, + // Invoking contract.name again + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // The successful update + ChainEvent::Updated { .. }, + ])); + } + } } From b9fe56256a78d728ba3355508aba233d7cbbf480 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 12 Jan 2023 15:45:35 +0100 Subject: [PATCH 028/208] Add remaining contract upgrade tests --- contract-testing/src/lib.rs | 403 +++++++++++++++++++++++++++++++++++- 1 file changed, 400 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index aaf3884d..35f514b6 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1379,6 +1379,8 @@ pub enum ContractUpdateError { InsufficientFunds, } +// TODO: Implementing (Partial)Eq for this and the other error/success types +// would be nice. But `anyhow::Error` does not implement (Partial)Eq. #[derive(Debug)] pub enum FailedContractInteraction { Reject { @@ -2668,13 +2670,12 @@ mod tests { ) .expect("Updating the `newfun` from the `upgrading_1` module should work"); - println!("{:?}", res_update_upgrade.chain_events); assert!(matches!(res_update_upgrade.chain_events[..], [ ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { .. }, + ChainEvent::Upgraded { from, to, .. }, ChainEvent::Resumed { .. }, ChainEvent::Updated { .. }, - ])); + ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference)); assert!(matches!(res_update_new.chain_events[..], [ ChainEvent::Updated { .. } ])); @@ -2741,5 +2742,401 @@ mod tests { ChainEvent::Updated { .. }, ])); } + + /// Test upgrading to a module that doesn't exist (it uses module + /// `[0u8;32]` inside the contract). The contract checks whether + /// the expected error is returned. + #[test] + fn test_missing_module() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + // No upgrade event, as it is supposed to fail. + ChainEvent::Resumed { success, .. }, + ChainEvent::Updated { .. }, + ] if success == false)); + } + + /// Test upgrading to a module where there isn't a matching contract + /// name. The contract checks whether the expected error is + /// returned. + #[test] + fn test_missing_contract() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + // No upgrade event, as it is supposed to fail. + ChainEvent::Resumed { success, .. }, + ChainEvent::Updated { .. }, + ] if success == false)); + } + + /// Test upgrading twice in the same transaction. The effect of the + /// second upgrade should be in effect at the end. + #[test] + fn test_twice_in_one_transaction() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_raw(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_raw(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_2 = chain + .module_deploy_raw(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000u64), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + // Invoke the contract itself to check the name entrypoint return value. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Upgrade from module 0 to 1 + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { from: first_from, to: first_to, .. }, + ChainEvent::Resumed { .. }, + // Invoke the contract itself to check the name again. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Upgrade again + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { from: second_from, to: second_to, .. }, + ChainEvent::Resumed { .. }, + // Invoke itself again to check name a final time. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Final update event + ChainEvent::Updated { .. }, + ] if first_from == res_deploy_0.module_reference + && first_to == res_deploy_1.module_reference + && second_from == res_deploy_1.module_reference + && second_to == res_deploy_2.module_reference)); + } + + /// Test upgrading to a module where there isn't a matching contract + /// name. The contract checks whether the expected error is + /// returned. + #[test] + fn test_chained_contract() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let number_of_upgrades: u32 = 93; // TODO: Stack will overflow if larger than 93. + let input_param = (number_of_upgrades, res_deploy.module_reference); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Updating valid contract should work"); + + // Per upgrade: 3 events for invoking itself + 3 events for the upgrade. + // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful + // update. + assert_eq!( + res_update.chain_events.len() as u32, + 6 * number_of_upgrades + 4 + ) + } + + /// Tests whether a contract which triggers a succesful upgrade, + /// but rejects the transaction from another cause, rollbacks the + /// upgrade as well. + #[test] + fn test_reject() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update_upgrade = chain.contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(1000000u64), + ); + + let res_update_new_feature = chain.contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(1000000u64), + ); + + // Check the return value manually returned by the contract. + assert!(matches!(res_update_upgrade, + Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { reason, ..})) + if reason == -1)); + + // Assert that the new_feature entrypoint doesn't exist since the upgrade + // failed. + assert!(matches!( + res_update_new_feature, + Err(ContractUpdateError::StringError(e)) if e == "Entrypoint does not exist." + )); + } + + /// Tests calling an entrypoint introduced by an upgrade of the module + /// can be called and whether an entrypoint removed by an upgrade fail + /// with the appropriate reject reason. + #[test] + fn test_changing_entrypoint() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_raw( + ACC_0, + format!("{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000u64), + ) + .expect("Initializing valid contract should work"); + + let res_update_old_feature_0 = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("old_feature"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Updating old_feature on old module should work."); + + let res_update_new_feature_0 = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect_err("Updating new_feature on old module should _not_ work"); + + let res_update_upgrade = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + ContractParameter::from_typed(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Upgrading contract should work."); + + let res_update_old_feature_1 = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("old_feature"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect_err("Updating old_feature on _new_ module should _not_ work."); + + let res_update_new_feature_1 = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(1000000u64), + ) + .expect("Updating new_feature on _new_ module should work"); + + assert!(matches!(res_update_old_feature_0.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + assert!( + matches!(res_update_new_feature_0, ContractUpdateError::StringError(e) if e == "Entrypoint does not exist.") + ); + assert!(matches!(res_update_upgrade.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. }, + ])); + assert!( + matches!(res_update_old_feature_1, ContractUpdateError::StringError(e) if e == "Entrypoint does not exist.") + ); + assert!(matches!(res_update_new_feature_1.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } } } From cc1ddcf2c3e4ed56f926181df05647abcd11bb00 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 12 Jan 2023 16:31:46 +0100 Subject: [PATCH 029/208] Remove direct CCC dependency, make fields pub, add docs --- contract-testing/Cargo.lock | 1 - contract-testing/Cargo.toml | 1 - contract-testing/src/lib.rs | 68 +++++++++++++++++++++---------------- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index ddda7b30..a40871c0 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -306,7 +306,6 @@ name = "concordium-smart-contract-testing" version = "0.1.0" dependencies = [ "anyhow", - "concordium-contracts-common", "concordium_base", "sha2 0.10.6", "thiserror", diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 283e1151..2f4a5602 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium-contracts-common = {path = "../concordium-base/concordium-contracts-common/concordium-contracts-common"} concordium_base = {path = "../concordium-base/rust-src/concordium_base"} wasm-chain-integration = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 35f514b6..be099c9b 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,17 +1,11 @@ use anyhow::bail; use concordium_base::{ base::{Energy, ExchangeRate}, + contracts_common::*, transactions::{self, cost}, }; -use concordium_contracts_common::*; use sha2::{Digest, Sha256}; -use std::{ - collections::{ - btree_map::Entry::{Occupied, Vacant}, - BTreeMap, - }, - path::Path, -}; +use std::{collections::BTreeMap, path::Path}; use thiserror::Error; use wasm_chain_integration::{ v0, @@ -28,40 +22,50 @@ use wasm_transform::artifact; type ArtifactV1 = artifact::Artifact; // Energy constants from Cost.hs in concordium-base. -const QUERY_ACCOUNT_BALANCE_COST: u64 = 200; -const QUERY_CONTRACT_BALANCE_COST: u64 = 200; -const QUERY_EXCHANGE_RATE_COST: u64 = 100; + +/// Cost of querying the account balance from a within smart contract instance. +const CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST: u64 = 200; +/// Cost of querying the contract balance from a within smart contract instance. +const CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST: u64 = 200; +/// Cost of querying the current exchange rates from a within smart contract +/// instance. +const CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST: u64 = 100; +/// The base cost of initializing a contract instance to cover administrative +/// costs. Even if no code is run and no instance created. const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: u64 = 300; +/// Cost of creating an empty smart contract instance. const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: u64 = 200; pub struct Chain { /// The slot time viewable inside the smart contracts. /// Defaults to `0`. - slot_time: SlotTime, + pub slot_time: SlotTime, /// MicroCCD per Euro ratio. - micro_ccd_per_euro: ExchangeRate, + pub micro_ccd_per_euro: ExchangeRate, /// Euro per Energy ratio. - euro_per_energy: ExchangeRate, + pub euro_per_energy: ExchangeRate, /// Accounts and info about them. - accounts: BTreeMap, + pub accounts: BTreeMap, /// Smart contract modules. - modules: BTreeMap, + pub modules: BTreeMap, /// Smart contract instances. - contracts: BTreeMap, + pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. - next_contract_index: u64, + pub next_contract_index: u64, } #[derive(Clone)] pub struct ContractInstance { - module_reference: ModuleReference, - contract_name: OwnedContractName, - state: v1::trie::PersistentState, - owner: AccountAddress, - self_balance: Amount, + pub module_reference: ModuleReference, + pub contract_name: OwnedContractName, + pub state: v1::trie::PersistentState, + pub owner: AccountAddress, + pub self_balance: Amount, } impl Chain { + /// Create a new [`Self`] where all the configurable parameters are + /// provided. pub fn new_with_time_and_rates( slot_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -78,6 +82,9 @@ impl Chain { } } + /// Create a new [`Self`] with a specified `slot_time` where + /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(slot_time: SlotTime) -> Self { Self { slot_time, @@ -87,8 +94,7 @@ impl Chain { /// Create a new [`Self`] where /// - `slot_time` defaults to `0`, - /// - `micro_ccd_per_euro` defaults to `16036807715944130560 / - /// 108919627567`, + /// - `micro_ccd_per_euro` defaults to `147235241 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new() -> Self { Self::new_with_time_and_rates( @@ -103,8 +109,8 @@ impl Chain { /// /// Parameters: /// - `sender`: the sender account. - /// - `module_bytes`: the module **without** the contract version and - /// module length bytes (8 bytes total). + /// - `module`: the raw wasm module (i.e. **without** the contract version + /// and module length bytes (8 bytes total)). fn module_deploy_aux( &mut self, sender: AccountAddress, @@ -1209,7 +1215,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, }; - let energy_after_invoke = remaining_energy - QUERY_ACCOUNT_BALANCE_COST; + let energy_after_invoke = + remaining_energy - CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST; let resume_res = v1::resume_receive( config, @@ -1239,7 +1246,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }; let energy_after_invoke = - remaining_energy - QUERY_CONTRACT_BALANCE_COST; + remaining_energy - CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST; let resume_res = v1::resume_receive( config, @@ -1263,7 +1270,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { data: Some(to_bytes(&exchange_rates)), }; - let energy_after_invoke = remaining_energy - QUERY_EXCHANGE_RATE_COST; + let energy_after_invoke = + remaining_energy - CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST; let resume_res = v1::resume_receive( config, From bf277a9d65385a1952554a1a5a6607641ac7740c Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 12 Jan 2023 16:32:43 +0100 Subject: [PATCH 030/208] Fix deploy module NRG calculation and allow custom acc sig count --- contract-testing/src/lib.rs | 75 ++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index be099c9b..4fe28b07 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -126,24 +126,14 @@ impl Chain { .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; // Calculate transaction fee of deployment - // TODO: This is still slightly off. - // For the fib module - // - This results in 18259 NRG - // - Concordium-client says 18262 NRG - // - The node charges for 18261 NRG let energy = { + // +1 for the tag, +8 for size and version let payload_size = - 1 + module.len() as u64 + transactions::construct::TRANSACTION_HEADER_SIZE; // +1 for the tag - let number_of_sigs = 1; // Accounts always have one signature here. TODO: Should we allow changing that? + 1 + 8 + module.len() as u64 + transactions::construct::TRANSACTION_HEADER_SIZE; + let number_of_sigs = self.get_account(sender)?.signature_count; let base_cost = cost::base_cost(payload_size, number_of_sigs); - let deploy_module_cost = cost::deploy_module(payload_size); - let total = base_cost + deploy_module_cost; - println!( - "Deploying module \ - cost:\n\tmodule_size:{}\n\tbase_cost:{}\n\tdeploy_module_cost:{}\n\ttotal:{}", - payload_size, base_cost, deploy_module_cost, total - ); - total + let deploy_module_cost = cost::deploy_module(module.len() as u64); + base_cost + deploy_module_cost }; let transaction_fee = self.calculate_energy_cost(energy); println!( @@ -153,16 +143,11 @@ impl Chain { ); // Try to subtract cost for account - match self.accounts.entry(sender) { - Vacant(_) => return Err(DeployModuleError::AccountDoesNotExist), - Occupied(mut acc) => { - let acc = acc.get_mut(); - if acc.balance < transaction_fee { - return Err(DeployModuleError::InsufficientFunds); - } - acc.balance -= transaction_fee; - } + let account = self.get_account_mut(sender)?; + if account.balance < transaction_fee { + return Err(DeployModuleError::InsufficientFunds); } + account.balance -= transaction_fee; // Save the module let module_reference = { @@ -1328,9 +1313,12 @@ impl From for ContractUpdateError { #[derive(Clone)] pub struct AccountInfo { /// The account balance. TODO: Do we need the three types of balances? - pub balance: Amount, + pub balance: Amount, /// Account policies. - policies: v0::OwnedPolicyBytes, + policies: v0::OwnedPolicyBytes, + /// The number of signatures. The number of signatures affect the cost of + /// every transaction for the account. + signature_count: u32, } pub struct TestPolicies(v0::OwnedPolicyBytes); @@ -1344,14 +1332,43 @@ impl TestPolicies { } impl AccountInfo { + /// Create a new [`Self`] with the provided parameters. + /// The `signature_count` must be >= 1 for transaction costs to be + /// realistic. + pub fn new_with_policy_and_signature_count( + balance: Amount, + policies: TestPolicies, + signature_count: u32, + ) -> Self { + Self { + balance, + policies: policies.0, + signature_count, + } + } + + /// Create new [`Self`] with empty account policies but the provided + /// `signature_count`. The `signature_count` must be >= 1 for transaction + /// costs to be realistic. + pub fn new_with_signature_count(balance: Amount, signature_count: u32) -> Self { + Self { + signature_count, + ..Self::new(balance) + } + } + + /// Create new [`Self`] with empty account policies and a signature + /// count of `1`. pub fn new_with_policy(balance: Amount, policies: TestPolicies) -> Self { Self { balance, policies: policies.0, + signature_count: 1, } } - /// Create new account info with empty account policies. + /// Create new [`Self`] with empty account policies and a signature + /// count of `1`. pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } } @@ -1461,6 +1478,10 @@ impl From for DeployModuleError { fn from(e: std::io::Error) -> Self { DeployModuleError::ReadFileError(e) } } +impl From for DeployModuleError { + fn from(_: AccountMissing) -> Self { DeployModuleError::AccountDoesNotExist } +} + impl ContractError { pub fn deserial(&self) -> Result { todo!() } } From 7b96b9f6c6777988c33b6c783f59bf2578b3f79f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 13 Jan 2023 11:16:51 +0100 Subject: [PATCH 031/208] Use WasmModule and add more docs --- contract-testing/src/lib.rs | 147 ++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 56 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 4fe28b07..184ee28d 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,7 +1,9 @@ use anyhow::bail; use concordium_base::{ base::{Energy, ExchangeRate}, + common, contracts_common::*, + smart_contracts::{ModuleRef, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost}, }; use sha2::{Digest, Sha256}; @@ -114,45 +116,48 @@ impl Chain { fn module_deploy_aux( &mut self, sender: AccountAddress, - module: &[u8], + wasm_module: WasmModule, ) -> Result { // Deserialize as wasm module (artifact) let artifact = wasm_transform::utils::instantiate_with_metering::( &ConcordiumAllowedImports { support_upgrade: true, }, - &module, + wasm_module.source.as_ref(), ) .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; // Calculate transaction fee of deployment let energy = { // +1 for the tag, +8 for size and version - let payload_size = - 1 + 8 + module.len() as u64 + transactions::construct::TRANSACTION_HEADER_SIZE; + let payload_size = 1 + + 8 + + wasm_module.source.size() as u64 + + transactions::construct::TRANSACTION_HEADER_SIZE; let number_of_sigs = self.get_account(sender)?.signature_count; let base_cost = cost::base_cost(payload_size, number_of_sigs); - let deploy_module_cost = cost::deploy_module(module.len() as u64); + let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); base_cost + deploy_module_cost }; let transaction_fee = self.calculate_energy_cost(energy); println!( "Deploying module with size {}, resulting in {} NRG.", - module.len(), + wasm_module.source.size(), energy ); // Try to subtract cost for account let account = self.get_account_mut(sender)?; - if account.balance < transaction_fee { - return Err(DeployModuleError::InsufficientFunds); - } - account.balance -= transaction_fee; + account + .balance + .checked_sub(transaction_fee) + .ok_or(DepoyModuleError::InsufficientFunds)?; - // Save the module + // Save the module TODO: Use wasm_module.get_module_ref() and find a proper way + // to convert ModuleRef to ModuleReference. let module_reference = { let mut hasher = Sha256::new(); - hasher.update(module); + hasher.update(wasm_module.source.as_ref()); let hash: [u8; 32] = hasher.finalize().into(); ModuleReference::from(hash) }; @@ -166,30 +171,59 @@ impl Chain { /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version /// bytes and 4 module length bytes. - pub fn module_deploy_raw>( + /// The module still has to a valid V1 smart contract module. + /// + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. + pub fn module_deploy_wasm_v1>( &mut self, sender: AccountAddress, module_path: P, ) -> Result { // Load file - let module = std::fs::read(module_path)?; - self.module_deploy_aux(sender, &module) + let file_contents = std::fs::read(module_path)?; + let wasm_module = WasmModule { + version: WasmVersion::V1, + source: ModuleSource::from(file_contents), + }; + self.module_deploy_aux(sender, wasm_module) } - /// Deploy a wasm module as it is output from `cargo concordium build`, i.e. - /// **including** the prefix of 4 version bytes and 4 module length bytes. - pub fn module_deploy>( + /// Deploy a v1 wasm module as it is output from `cargo concordium build`, + /// i.e. **including** the prefix of 4 version bytes and 4 module length + /// bytes. + /// + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. + pub fn module_deploy_v1>( &mut self, sender: AccountAddress, module_path: P, ) -> Result { // Load file - let module = std::fs::read(module_path)?; - // Here, we skip the 8 bytes that encode the smart contract version and module - // length - self.module_deploy_aux(sender, &module[8..]) + let file_contents = std::fs::read(module_path)?; + let mut cursor = std::io::Cursor::new(file_contents); + let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor) + .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; + + if wasm_module.version != WasmVersion::V1 { + return Err(DeployModuleError::UnsupportedModuleVersion); + } + self.module_deploy_aux(sender, wasm_module) } + /// Initialize a contract. + /// + /// - `sender`: The account paying for the transaction. Will also become the + /// owner of the instance created. + /// - `module_reference`: The reference to the a module that has already + /// been deployed. + /// - `contract_name`: Name of the contract to initialize. + /// - `parameter`: Parameter provided to the init method. + /// - `amount`: The initial balance of the contract. Subtracted from the + /// `sender` account. + /// - `energy_reserved`: Amount of energy reserved for executing the init + /// method. pub fn contract_init( &mut self, sender: AccountAddress, @@ -1472,6 +1506,7 @@ pub enum DeployModuleError { InvalidModule(String), InsufficientFunds, AccountDoesNotExist, + UnsupportedModuleVersion, } impl From for DeployModuleError { @@ -1629,7 +1664,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); assert_eq!(chain.modules.len(), 1); @@ -1646,7 +1681,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1673,7 +1708,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1708,7 +1743,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1769,7 +1804,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1840,7 +1875,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1894,7 +1929,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/fib/a.wasm.v1") + .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -1955,7 +1990,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let res_init = chain @@ -2016,7 +2051,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") .expect("Deploying valid module should work"); let input_param: u32 = 8; @@ -2083,7 +2118,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), ) @@ -2142,7 +2177,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. + .module_deploy_wasm_v1(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. .expect("Deploying valid module should work"); let res_init = chain @@ -2201,7 +2236,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER), ) @@ -2269,7 +2304,7 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), ) @@ -2326,7 +2361,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!( "{}/queries-account-balance-missing-account.wasm", @@ -2389,7 +2424,7 @@ mod tests { let init_amount = Amount::from_ccd(123); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), ) @@ -2448,7 +2483,7 @@ mod tests { let update_amount = Amount::from_ccd(456); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), ) @@ -2496,7 +2531,7 @@ mod tests { let transfer_amount = Amount::from_ccd(78); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!( "{}/queries-contract-balance-transfer.wasm", @@ -2552,7 +2587,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!( "{}/queries-contract-balance-missing-contract.wasm", @@ -2604,7 +2639,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER), ) @@ -2655,11 +2690,11 @@ mod tests { // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain - .module_deploy_raw(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) .expect("Deploying valid module should work"); // Initialize `upgrading_0`. @@ -2720,13 +2755,13 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER), ) @@ -2782,7 +2817,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER), ) @@ -2828,14 +2863,14 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER), ) @@ -2880,15 +2915,15 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_raw(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) .expect("Deploying valid module should work"); let res_deploy_2 = chain - .module_deploy_raw(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) .expect("Deploying valid module should work"); let res_init = chain @@ -2954,7 +2989,7 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER), ) @@ -3004,14 +3039,14 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER), ) @@ -3069,14 +3104,14 @@ mod tests { chain.create_account(ACC_0, AccountInfo::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_raw( + .module_deploy_wasm_v1( ACC_0, format!("{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER), ) From baffc84db119123b8dd4303d3a7fbb5b1b742ebf Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 13 Jan 2023 13:27:43 +0100 Subject: [PATCH 032/208] Fix mixed use of Energy and InterpreterEnergy --- contract-testing/src/lib.rs | 259 ++++++++++++++++++++++-------------- 1 file changed, 156 insertions(+), 103 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 184ee28d..7e03fa32 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -3,7 +3,7 @@ use concordium_base::{ base::{Energy, ExchangeRate}, common, contracts_common::*, - smart_contracts::{ModuleRef, ModuleSource, WasmModule, WasmVersion}, + smart_contracts::{ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost}, }; use sha2::{Digest, Sha256}; @@ -26,17 +26,17 @@ type ArtifactV1 = artifact::Artifact { let contract_address = self.create_contract_address(); - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); let mut collector = v1::trie::SizeCollector::default(); @@ -309,7 +309,10 @@ impl Chain { return_value, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); Err(ContractInitError::ValidChainError( FailedContractInteraction::Reject { @@ -325,7 +328,10 @@ impl Chain { error, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); Err(ContractInitError::ValidChainError( FailedContractInteraction::Trap { @@ -441,9 +447,7 @@ impl Chain { amount, receive_name: receive_name.as_receive_name(), parameter: ¶meter, - energy: InterpreterEnergy { - energy: remaining_energy.energy, - }, + energy: Chain::to_interpreter_energy(remaining_energy), }, instance_state, v1::ReceiveParams { @@ -525,9 +529,13 @@ impl Chain { logs, state_changed, return_value, - remaining_energy, + remaining_energy, /* TODO: Could we change this from `u64` to + * `InterpreterEnergy` in chain_integration? */ } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); Ok(SuccessfulContractUpdate { chain_events, @@ -544,7 +552,10 @@ impl Chain { return_value, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); Err(ContractUpdateError::ValidChainError( FailedContractInteraction::Reject { @@ -560,7 +571,10 @@ impl Chain { error, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); transaction_fee += self.calculate_energy_cost(energy_used); Err(ContractUpdateError::ValidChainError( FailedContractInteraction::Trap { @@ -668,9 +682,7 @@ impl Chain { amount, receive_name: receive_name.as_receive_name(), parameter: ¶meter.0, - energy: InterpreterEnergy { - energy: energy_reserved.energy, - }, + energy: Chain::to_interpreter_energy(energy_reserved), }, instance_state, v1::ReceiveParams { @@ -688,7 +700,10 @@ impl Chain { return_value, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); let transaction_fee = self.calculate_energy_cost(energy_used); Ok(SuccessfulContractUpdate { chain_events: Vec::new(), // TODO: add host events @@ -705,7 +720,10 @@ impl Chain { return_value, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); let transaction_fee = self.calculate_energy_cost(energy_used); Err(ContractUpdateError::ValidChainError( FailedContractInteraction::Reject { @@ -721,7 +739,10 @@ impl Chain { error, remaining_energy, } => { - let energy_used = Energy::from(energy_reserved.energy - remaining_energy); + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); let transaction_fee = self.calculate_energy_cost(energy_used); Err(ContractUpdateError::ValidChainError( FailedContractInteraction::Trap { @@ -809,6 +830,20 @@ impl Chain { ) } + /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. + fn to_interpreter_energy(energy: Energy) -> InterpreterEnergy { + InterpreterEnergy { + energy: energy.energy * 1000, + } + } + + /// Convert [`InterpreterEnergy`] to [`Energy`] by dividing by `1000`. + fn from_interpreter_energy(interpreter_energy: InterpreterEnergy) -> Energy { + Energy { + energy: interpreter_energy.energy / 1000, + } + } + pub fn lookup_module_cost(&self, artifact: &ArtifactV1) -> Energy { // TODO: Is it just the `.code`? // Comes from Concordium/Cost.hs::lookupModule @@ -1021,7 +1056,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { parameter, amount, self.invoker_amount_reserved_for_nrg, - Energy::from(remaining_energy), + Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }), &mut self.chain_events, ); @@ -1138,8 +1175,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain_events.push(interrupt_event); // Charge a base cost. - let mut energy_after_invoke = - remaining_energy - INITIALIZE_CONTRACT_INSTANCE_BASE_COST; + let mut energy_after_invoke = remaining_energy + - Chain::to_interpreter_energy( + INITIALIZE_CONTRACT_INSTANCE_BASE_COST, + ) + .energy; let response = match self.chain.modules.get(&module_ref) { None => InvokeResponse::Failure { @@ -1147,8 +1187,10 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, Some(artifact) => { // Charge for the module lookup. - energy_after_invoke -= - self.chain.lookup_module_cost(&artifact).energy; + energy_after_invoke -= Chain::to_interpreter_energy( + self.chain.lookup_module_cost(&artifact), + ) + .energy; if artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), @@ -1159,8 +1201,10 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { instance.module_reference = module_ref; // Charge for the initialization cost. - energy_after_invoke -= - INITIALIZE_CONTRACT_INSTANCE_CREATE_COST; + energy_after_invoke -= Chain::to_interpreter_energy( + INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + ) + .energy; let upgrade_event = ChainEvent::Upgraded { address: self.address, @@ -1234,8 +1278,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, }; - let energy_after_invoke = - remaining_energy - CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST; + let energy_after_invoke = remaining_energy + - Chain::to_interpreter_energy( + CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, + ) + .energy; let resume_res = v1::resume_receive( config, @@ -1264,8 +1311,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, }; - let energy_after_invoke = - remaining_energy - CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST; + let energy_after_invoke = remaining_energy + - Chain::to_interpreter_energy( + CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + ) + .energy; let resume_res = v1::resume_receive( config, @@ -1289,8 +1339,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { data: Some(to_bytes(&exchange_rates)), }; - let energy_after_invoke = - remaining_energy - CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST; + let energy_after_invoke = remaining_energy + - Chain::to_interpreter_energy( + CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, + ) + .energy; let resume_res = v1::resume_receive( config, @@ -1691,7 +1744,7 @@ mod tests { ContractName::new_unchecked("init_weather"), ContractParameter::from_bytes(vec![0u8]), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); assert_eq!( @@ -1718,7 +1771,7 @@ mod tests { ContractName::new_unchecked("init_weather"), ContractParameter::from_bytes(vec![99u8]), // Invalid param Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect_err("Initializing with invalid params should fail"); @@ -1753,7 +1806,7 @@ mod tests { ContractName::new_unchecked("init_weather"), ContractParameter::from_bytes(vec![0u8]), // Starts as 0 Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -1764,7 +1817,7 @@ mod tests { EntrypointName::new_unchecked("set"), ContractParameter::from_bytes(vec![1u8]), // Updated to 1 Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Updating valid contract should work"); @@ -1814,7 +1867,7 @@ mod tests { ContractName::new_unchecked("init_integrate"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -1825,7 +1878,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), transfer_amount, - Energy::from(10000u64), + Energy::from(10000), ) .expect("Updating valid contract should work"); @@ -1836,7 +1889,7 @@ mod tests { EntrypointName::new_unchecked("view"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Invoking get should work"); @@ -1885,7 +1938,7 @@ mod tests { ContractName::new_unchecked("init_integrate"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -1895,7 +1948,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. transfer_amount, - Energy::from(100000u64), + Energy::from(100000), ); match res_update { @@ -1939,7 +1992,7 @@ mod tests { ContractName::new_unchecked("init_fib"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -1950,7 +2003,7 @@ mod tests { EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&6u64), Amount::zero(), - Energy::from(4000000u64), + Energy::from(4000000), ) .expect("Updating valid contract should work"); @@ -1961,7 +2014,7 @@ mod tests { EntrypointName::new_unchecked("view"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Invoking get should work"); @@ -2000,7 +2053,7 @@ mod tests { ContractName::new_unchecked("init_integrate"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2011,7 +2064,7 @@ mod tests { EntrypointName::new_unchecked("recurse"), ContractParameter::from_typed(&10u32), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect("Updating valid contract should work"); @@ -2022,7 +2075,7 @@ mod tests { EntrypointName::new_unchecked("view"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Invoking get should work"); @@ -2063,7 +2116,7 @@ mod tests { ContractName::new_unchecked("init_integrate"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2074,7 +2127,7 @@ mod tests { EntrypointName::new_unchecked("inc-fail-on-zero"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000000u64), + Energy::from(100000000), ) .expect("Updating valid contract should work"); @@ -2085,7 +2138,7 @@ mod tests { EntrypointName::new_unchecked("view"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Invoking get should work"); @@ -2131,7 +2184,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2148,7 +2201,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2187,12 +2240,12 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); let update_amount = Amount::from_ccd(123); - let energy_limit = Energy::from(100000u64); + let energy_limit = Energy::from(100000); let invoker_reserved_amount = update_amount + chain.calculate_energy_cost(energy_limit); // The contract will query the balance of ACC_1, which is also the invoker, and @@ -2251,7 +2304,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), amount_to_send, // Make sure the contract has CCD to transfer. - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2317,7 +2370,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2334,7 +2387,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2377,7 +2430,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2391,7 +2444,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2437,7 +2490,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2448,7 +2501,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), init_amount, // Set up another contract with `init_amount` balance - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2462,7 +2515,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2496,7 +2549,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), init_amount, - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2510,7 +2563,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), update_amount, - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2547,7 +2600,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), init_amount, - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2567,7 +2620,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), update_amount, - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2603,7 +2656,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2617,7 +2670,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2652,7 +2705,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2666,7 +2719,7 @@ mod tests { EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2705,7 +2758,7 @@ mod tests { ContractName::new_unchecked("init_a"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2718,7 +2771,7 @@ mod tests { EntrypointName::new_unchecked("bump"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2730,7 +2783,7 @@ mod tests { EntrypointName::new_unchecked("newfun"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating the `newfun` from the `upgrading_1` module should work"); @@ -2774,7 +2827,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2785,7 +2838,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2830,7 +2883,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2841,7 +2894,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::empty(), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2883,7 +2936,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2894,7 +2947,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -2933,7 +2986,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -2946,7 +2999,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(100000u64), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -3002,7 +3055,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -3016,7 +3069,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&input_param), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect("Updating valid contract should work"); @@ -3059,7 +3112,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -3069,7 +3122,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ); let res_update_new_feature = chain.contract_update( @@ -3078,7 +3131,7 @@ mod tests { EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ); // Check the return value manually returned by the contract. @@ -3124,7 +3177,7 @@ mod tests { ContractName::new_unchecked("init_contract"), ContractParameter::empty(), Amount::zero(), - Energy::from(10000u64), + Energy::from(10000), ) .expect("Initializing valid contract should work"); @@ -3135,7 +3188,7 @@ mod tests { EntrypointName::new_unchecked("old_feature"), ContractParameter::empty(), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect("Updating old_feature on old module should work."); @@ -3146,7 +3199,7 @@ mod tests { EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect_err("Updating new_feature on old module should _not_ work"); @@ -3157,7 +3210,7 @@ mod tests { EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect("Upgrading contract should work."); @@ -3168,7 +3221,7 @@ mod tests { EntrypointName::new_unchecked("old_feature"), ContractParameter::empty(), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect_err("Updating old_feature on _new_ module should _not_ work."); @@ -3179,7 +3232,7 @@ mod tests { EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), Amount::zero(), - Energy::from(1000000u64), + Energy::from(1000000), ) .expect("Updating new_feature on _new_ module should work"); From f85dab6aaf9a8b3a95795d5723e28500f9e650bd Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 13 Jan 2023 14:03:16 +0100 Subject: [PATCH 033/208] Add additional checks and clarify comments --- contract-testing/src/lib.rs | 46 ++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 7e03fa32..ace71f31 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -161,6 +161,10 @@ impl Chain { let hash: [u8; 32] = hasher.finalize().into(); ModuleReference::from(hash) }; + // Ensure module hasn't been deployed before. + if self.modules.contains_key(&module_reference) { + return Err(DeployModuleError::DuplicateModule); + } self.modules.insert(module_reference, artifact); Ok(SuccessfulModuleDeployment { module_reference, @@ -286,8 +290,7 @@ impl Chain { let contract_instance = ContractInstance { module_reference, contract_name: contract_name.to_owned(), - state: state.freeze(&mut loader, &mut collector), /* TODO: Do we need to - * charge to storage? */ + state: state.freeze(&mut loader, &mut collector), // TODO: Charge for storage. owner: sender, self_balance: amount, }; @@ -375,7 +378,7 @@ impl Chain { sender: Address, address: ContractAddress, entrypoint: OwnedEntrypointName, - parameter: Vec, + parameter: Parameter, amount: Amount, // The CCD amount reserved from the invoker account. While the amount // is reserved, it is not subtracted in the chain.accounts map. @@ -446,7 +449,7 @@ impl Chain { v1::ReceiveInvocation { amount, receive_name: receive_name.as_receive_name(), - parameter: ¶meter, + parameter: ¶meter.0, energy: Chain::to_interpreter_energy(remaining_energy), }, instance_state, @@ -512,7 +515,7 @@ impl Chain { Address::Account(invoker), address, entrypoint.to_owned(), - parameter.0, + Parameter(¶meter.0), amount, invoker_amount_reserved_for_nrg, energy_reserved, @@ -951,9 +954,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } => { println!("\tInterrupting contract {}", self.address); - // Create the interrupt event, which will be included for transfers and calls, - // but not for the remaining interrupts. - // TODO: Or is it included in upgrades as well? + // Create the interrupt event, which will be included for transfers, calls, and + // upgrades, but not for the remaining interrupts. let interrupt_event = ChainEvent::Interrupted { address: self.address, logs, @@ -983,15 +985,20 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } else { // Move the CCD - self.chain - .get_account_mut(to) - .expect("Account is known to exist") - .balance += amount; - let instance = self - .chain - .get_instance_mut(self.address) - .expect("Contract is known to exist"); - instance.self_balance -= amount; + let instance_new_balance = match ( + self.chain.accounts.get_mut(&to), + self.chain.contracts.get_mut(&self.address), + ) { + (Some(acc), Some(contr)) => { + contr.self_balance -= amount; + acc.balance += amount; + contr.self_balance + } + _ => anyhow::bail!( + "Contract or Account missing when they are known to \ + exist." + ), + }; // Add transfer event self.chain_events.push(ChainEvent::Transferred { @@ -1001,7 +1008,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }); InvokeResponse::Success { - new_balance: instance.self_balance, + new_balance: instance_new_balance, data: None, } } @@ -1053,7 +1060,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { Address::Contract(self.address), address, name, - parameter, + Parameter(¶meter), amount, self.invoker_amount_reserved_for_nrg, Chain::from_interpreter_energy(InterpreterEnergy { @@ -1560,6 +1567,7 @@ pub enum DeployModuleError { InsufficientFunds, AccountDoesNotExist, UnsupportedModuleVersion, + DuplicateModule, } impl From for DeployModuleError { From 82a3c85df75dcd16f3ba21c9f450632f72bb08a9 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 16 Jan 2023 11:00:34 +0100 Subject: [PATCH 034/208] Use Rc for module artifacts --- contract-testing/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index ace71f31..be0b43aa 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -7,7 +7,7 @@ use concordium_base::{ transactions::{self, cost}, }; use sha2::{Digest, Sha256}; -use std::{collections::BTreeMap, path::Path}; +use std::{collections::BTreeMap, path::Path, rc::Rc}; use thiserror::Error; use wasm_chain_integration::{ v0, @@ -49,7 +49,7 @@ pub struct Chain { /// Accounts and info about them. pub accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap, + pub modules: BTreeMap>, /// Smart contract instances. pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. @@ -165,7 +165,7 @@ impl Chain { if self.modules.contains_key(&module_reference) { return Err(DeployModuleError::DuplicateModule); } - self.modules.insert(module_reference, artifact); + self.modules.insert(module_reference, Rc::new(artifact)); Ok(SuccessfulModuleDeployment { module_reference, energy, @@ -854,7 +854,10 @@ impl Chain { } fn get_artifact(&self, module_ref: ModuleReference) -> Result<&ArtifactV1, ModuleMissing> { - self.modules.get(&module_ref).ok_or(ModuleMissing) + match self.modules.get(&module_ref) { + Some(artifact) => Ok(artifact.as_ref()), + None => Err(ModuleMissing), + } } fn get_instance( From eab9e69ad5ad1e7dfa5d38517ca25ce94839d0a7 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 16 Jan 2023 12:34:41 +0100 Subject: [PATCH 035/208] Improve error types and simplify with thiserror --- contract-testing/src/lib.rs | 149 +++++++++++++++++------------------- 1 file changed, 70 insertions(+), 79 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index be0b43aa..10cba06f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -124,8 +124,7 @@ impl Chain { support_upgrade: true, }, wasm_module.source.as_ref(), - ) - .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; + )?; // Calculate transaction fee of deployment let energy = { @@ -163,7 +162,7 @@ impl Chain { }; // Ensure module hasn't been deployed before. if self.modules.contains_key(&module_reference) { - return Err(DeployModuleError::DuplicateModule); + return Err(DeployModuleError::DuplicateModule(module_reference)); } self.modules.insert(module_reference, Rc::new(artifact)); Ok(SuccessfulModuleDeployment { @@ -207,11 +206,12 @@ impl Chain { // Load file let file_contents = std::fs::read(module_path)?; let mut cursor = std::io::Cursor::new(file_contents); - let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor) - .map_err(|e| DeployModuleError::InvalidModule(e.to_string()))?; + let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor)?; if wasm_module.version != WasmVersion::V1 { - return Err(DeployModuleError::UnsupportedModuleVersion); + return Err(DeployModuleError::UnsupportedModuleVersion( + wasm_module.version, + )); } self.module_deploy_aux(sender, wasm_module) } @@ -267,8 +267,7 @@ impl Chain { }, false, loader, - ) - .map_err(|e| ContractInitError::StringError(e.to_string()))?; + )?; // Handle the result and update the transaction fee. let res = match res { v1::InitResult::Success { @@ -348,7 +347,7 @@ impl Chain { v1::InitResult::OutOfEnergy => { transaction_fee += self.calculate_energy_cost(energy_reserved); Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOFEnergy { + FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, logs: v0::Logs::default(), // TODO: Get Logs on failures. @@ -591,7 +590,7 @@ impl Chain { v1::ReceiveResult::OutOfEnergy => { transaction_fee += self.calculate_energy_cost(energy_reserved); Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOFEnergy { + FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, logs: v0::Logs::default(), // TODO: Get logs on failures. @@ -602,7 +601,7 @@ impl Chain { Err(e) => { // TODO: what is the correct cost here? transaction_fee += self.calculate_energy_cost(energy_reserved); - Err(ContractUpdateError::StringError(e.to_string())) + Err(ContractUpdateError::InterpreterError(e)) } }; @@ -693,8 +692,7 @@ impl Chain { limit_logs_and_return_values: false, support_queries: true, }, - ) - .map_err(|e| ContractUpdateError::StringError(e.to_string()))?; + )?; match res { v1::ReceiveResult::Success { @@ -759,7 +757,7 @@ impl Chain { v1::ReceiveResult::OutOfEnergy => { let transaction_fee = self.calculate_energy_cost(energy_reserved); Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOFEnergy { + FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, logs: v0::Logs::default(), // TODO: Get logs on failures. @@ -856,7 +854,7 @@ impl Chain { fn get_artifact(&self, module_ref: ModuleReference) -> Result<&ArtifactV1, ModuleMissing> { match self.modules.get(&module_ref) { Some(artifact) => Ok(artifact.as_ref()), - None => Err(ModuleMissing), + None => Err(ModuleMissing(module_ref)), } } @@ -864,7 +862,9 @@ impl Chain { &self, address: ContractAddress, ) -> Result<&ContractInstance, ContractInstanceMissing> { - self.contracts.get(&address).ok_or(ContractInstanceMissing) + self.contracts + .get(&address) + .ok_or(ContractInstanceMissing(address)) } fn get_instance_mut( @@ -873,18 +873,20 @@ impl Chain { ) -> Result<&mut ContractInstance, ContractInstanceMissing> { self.contracts .get_mut(&address) - .ok_or(ContractInstanceMissing) + .ok_or(ContractInstanceMissing(address)) } fn get_account(&self, address: AccountAddress) -> Result<&AccountInfo, AccountMissing> { - self.accounts.get(&address).ok_or(AccountMissing) + self.accounts.get(&address).ok_or(AccountMissing(address)) } fn get_account_mut( &mut self, address: AccountAddress, ) -> Result<&mut AccountInfo, AccountMissing> { - self.accounts.get_mut(&address).ok_or(AccountMissing) + self.accounts + .get_mut(&address) + .ok_or(AccountMissing(address)) } } @@ -1376,36 +1378,16 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } #[derive(Debug, Error)] -#[error("Module has not been deployed.")] -struct ModuleMissing; - -impl From for ContractInitError { - fn from(_: ModuleMissing) -> Self { Self::ModuleDoesNotExist } -} - -impl From for ContractUpdateError { - fn from(_: ModuleMissing) -> Self { Self::ModuleDoesNotExist } -} +#[error("Module {:?} does not exist.", 0)] +pub struct ModuleMissing(ModuleReference); #[derive(Debug, Error)] -#[error("Contract instance has not been instantiated.")] -struct ContractInstanceMissing; - -impl From for ContractUpdateError { - fn from(_: ContractInstanceMissing) -> Self { Self::InstanceDoesNotExist } -} +#[error("Contract instance {0} does not exist.")] +pub struct ContractInstanceMissing(ContractAddress); #[derive(Debug, Error)] -#[error("Account has not been created.")] -struct AccountMissing; - -impl From for ContractInitError { - fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } -} - -impl From for ContractUpdateError { - fn from(_: AccountMissing) -> Self { Self::AccountDoesNotExist } -} +#[error("Account {0} does not exist.")] +pub struct AccountMissing(AccountAddress); #[derive(Clone)] pub struct AccountInfo { @@ -1469,35 +1451,47 @@ impl AccountInfo { pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. + #[error("failed due to a valid chain error: {:?}", 0)] ValidChainError(FailedContractInteraction), - /// TODO: Can we get a better error than the anyhow? - StringError(String), + /// An error thrown by the interpreter. + #[error("an error occured in the interpreter: {:?}", 0)] + InterpreterError(#[from] anyhow::Error), /// Module has not been deployed in test environment. - ModuleDoesNotExist, + #[error("module {:?} does not exist", 0.0)] + ModuleDoesNotExist(#[from] ModuleMissing), /// Account has not been created in test environment. - AccountDoesNotExist, + #[error("account {} does not exist", 0.0)] + AccountDoesNotExist(#[from] AccountMissing), /// The account does not have enough funds to pay for the energy. + #[error("account does not have enough funds to pay for the energy")] InsufficientFunds, } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ContractUpdateError { /// Update failed for a reason that also exists on the chain. + #[error("failed due to a valid chain error: {:?}", 0)] ValidChainError(FailedContractInteraction), - /// TODO: Can we get a better error than the anyhow? - StringError(String), + /// An error thrown by the wasm interpreter + #[error("an error occured in the interpreter: {:?}", 0)] + InterpreterError(#[from] anyhow::Error), /// Module has not been deployed in test environment. - ModuleDoesNotExist, + #[error("module {:?} does not exist", 0.0)] + ModuleDoesNotExist(#[from] ModuleMissing), /// Contract instance has not been initialized in test environment. - InstanceDoesNotExist, + #[error("instance {} does not exist", 0.0)] + InstanceDoesNotExist(#[from] ContractInstanceMissing), /// Entrypoint does not exist and neither does the fallback entrypoint. + #[error("entrypoint does not exist")] EntrypointDoesNotExist, /// Account has not been created in test environment. - AccountDoesNotExist, + #[error("account {} does not exist", 0.0)] + AccountDoesNotExist(#[from] AccountMissing), /// The account does not have enough funds to pay for the energy. + #[error("account does not have enough funds to pay for the energy")] InsufficientFunds, } @@ -1524,7 +1518,7 @@ pub enum FailedContractInteraction { /// debugging. logs: v0::Logs, }, - OutOFEnergy { + OutOfEnergy { energy_used: Energy, transaction_fee: Amount, /// Logs emitted before the interaction failed. Logs from failed @@ -1543,7 +1537,7 @@ impl FailedContractInteraction { FailedContractInteraction::Trap { transaction_fee, .. } => *transaction_fee, - FailedContractInteraction::OutOFEnergy { + FailedContractInteraction::OutOfEnergy { transaction_fee, .. } => *transaction_fee, } @@ -1553,7 +1547,7 @@ impl FailedContractInteraction { match self { FailedContractInteraction::Reject { logs, .. } => logs, FailedContractInteraction::Trap { logs, .. } => logs, - FailedContractInteraction::OutOFEnergy { logs, .. } => logs, + FailedContractInteraction::OutOfEnergy { logs, .. } => logs, } } } @@ -1563,22 +1557,20 @@ pub struct ContractError(Vec); // TODO: Can we get Eq for this when using io::Error? // TODO: Should this also have the energy used? -#[derive(Debug)] +#[derive(Debug, Error)] pub enum DeployModuleError { - ReadFileError(std::io::Error), - InvalidModule(String), + #[error("could not read the file due to: {0}")] + ReadFileError(#[from] std::io::Error), + #[error("module is invalid due to: {0}")] + InvalidModule(#[from] anyhow::Error), + #[error("account does not have sufficient funds to pay for the energy")] InsufficientFunds, - AccountDoesNotExist, - UnsupportedModuleVersion, - DuplicateModule, -} - -impl From for DeployModuleError { - fn from(e: std::io::Error) -> Self { DeployModuleError::ReadFileError(e) } -} - -impl From for DeployModuleError { - fn from(_: AccountMissing) -> Self { DeployModuleError::AccountDoesNotExist } + #[error("account {} does not exist", 0.0)] + AccountDoesNotExist(#[from] AccountMissing), + #[error("wasm version {0} is not supported")] + UnsupportedModuleVersion(WasmVersion), + #[error("module with reference {:?} already exists", 0)] + DuplicateModule(ModuleReference), } impl ContractError { @@ -1627,7 +1619,7 @@ pub struct SuccessfulContractUpdate { pub return_value: ContractReturnValue, /// Whether the state was changed. pub state_changed: bool, - /// The logs produced before?/after? the last interrupt if any. + /// The logs produced since the last interrupt. pub logs: v0::Logs, } @@ -1697,7 +1689,6 @@ impl ContractParameter { pub fn from_bytes(bytes: Vec) -> Self { Self(bytes) } - // TODO: add version with serde json pub fn from_typed(parameter: &T) -> Self { Self(to_bytes(parameter)) } } @@ -3070,7 +3061,7 @@ mod tests { ) .expect("Initializing valid contract should work"); - let number_of_upgrades: u32 = 93; // TODO: Stack will overflow if larger than 93. + let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain @@ -3154,7 +3145,7 @@ mod tests { // failed. assert!(matches!( res_update_new_feature, - Err(ContractUpdateError::StringError(e)) if e == "Entrypoint does not exist." + Err(ContractUpdateError::InterpreterError(e)) if e.to_string() == "Entrypoint does not exist." )); } @@ -3251,7 +3242,7 @@ mod tests { ChainEvent::Updated { .. } ])); assert!( - matches!(res_update_new_feature_0, ContractUpdateError::StringError(e) if e == "Entrypoint does not exist.") + matches!(res_update_new_feature_0, ContractUpdateError::InterpreterError(e) if e.to_string() == "Entrypoint does not exist.") ); assert!(matches!(res_update_upgrade.chain_events[..], [ ChainEvent::Interrupted { .. }, @@ -3260,7 +3251,7 @@ mod tests { ChainEvent::Updated { .. }, ])); assert!( - matches!(res_update_old_feature_1, ContractUpdateError::StringError(e) if e == "Entrypoint does not exist.") + matches!(res_update_old_feature_1, ContractUpdateError::InterpreterError(e) if e.to_string() == "Entrypoint does not exist.") ); assert!(matches!(res_update_new_feature_1.chain_events[..], [ ChainEvent::Updated { .. } From 94133f3e9a682adeb87da314965b701f02068ae3 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 18 Jan 2023 16:16:55 +0100 Subject: [PATCH 036/208] Initial work on changesets --- contract-testing/src/lib.rs | 93 ++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 10cba06f..87a765b1 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -54,6 +54,7 @@ pub struct Chain { pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. pub next_contract_index: u64, + pub instance_changesets: BTreeMap, } #[derive(Clone)] @@ -65,6 +66,32 @@ pub struct ContractInstance { pub self_balance: Amount, } +#[derive(Clone, Copy)] +pub enum AmountDelta { + Positive(Amount), + Negative(Amount), +} +impl AmountDelta { + pub fn new() -> Self { Self::Positive(Amount::zero()) } +} + +/// Data held for a contract instance during the execution of a transaction. +#[derive(Clone)] +pub struct InstanceChangeSet { + // modification_index ? + amount_changed: AmountDelta, + state: MutableState, +} + +impl InstanceChangeSet { + fn from_state(state: MutableState) -> Self { + Self { + amount_changed: AmountDelta::new(), + state, + } + } +} + impl Chain { /// Create a new [`Self`] where all the configurable parameters are /// provided. @@ -81,6 +108,7 @@ impl Chain { modules: BTreeMap::new(), contracts: BTreeMap::new(), next_contract_index: 0, + instance_changesets: BTreeMap::new(), } } @@ -399,7 +427,7 @@ impl Chain { // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; - let artifact = self.get_artifact(instance.module_reference)?; + let artifact = self.get_artifact(instance.module_reference)?.clone(); // Subtract the cost of looking up the module remaining_energy = Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); @@ -435,15 +463,18 @@ impl Chain { }, }; + let contract_name = instance.contract_name.clone(); + // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = instance.state.thaw(); + let mut changeset = self.instance_changeset(address); + let mut mutable_state = changeset.state.make_fresh_generation(&mut loader); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); // Get the initial result from invoking receive let res = v1::invoke_receive( - std::sync::Arc::new(artifact.clone()), + std::sync::Arc::new(artifact), receive_ctx, v1::ReceiveInvocation { amount, @@ -464,7 +495,7 @@ impl Chain { let mut data = ProcessReceiveData { invoker, address, - contract_name: instance.contract_name.clone(), + contract_name, amount, invoker_amount_reserved_for_nrg, entrypoint: entrypoint.to_owned(), @@ -611,6 +642,15 @@ impl Chain { self.contracts = contracts_backup; } + let mut loader = v1::trie::Loader::new(&[][..]); + let mut collector = SizeCollector::default(); + for (addr, changeset) in self.instance_changesets.iter_mut() { + self.contracts.get_mut(addr).expect("Must exist").state = + changeset.state.freeze(&mut loader, &mut collector); + } + self.instance_changesets.clear(); + // TODO: Charge for size in collector; + // Charge the transaction fee irrespective of the result self.get_account_mut(invoker)?.balance -= transaction_fee; res @@ -782,6 +822,16 @@ impl Chain { ContractAddress::new(index, subindex) } + /// Precondition: `address` exists in state.contracts. + fn instance_changeset(&mut self, address: ContractAddress) -> InstanceChangeSet { + self.instance_changesets + .entry(address) + .or_insert(InstanceChangeSet::from_state( + self.contracts[&address].state.thaw(), + )) + .clone() + } + pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { @@ -932,14 +982,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { amount: self.amount, }; if state_changed { - let instance = self - .chain - .get_instance_mut(self.address) - .expect("Instance known to exist"); - let mut collector = v1::trie::SizeCollector::default(); - instance.state = - self.mutable_state.freeze(&mut self.loader, &mut collector); - // TODO: Charge energy for this + // let instance = self + // .chain + // .get_instance_mut(self.address) + // .expect("Instance known to exist"); + // let mut collector = v1::trie::SizeCollector::default(); + self.chain.instance_changeset(self.address).state = + self.mutable_state.clone(); + // instance.state = + // self.mutable_state.freeze(&mut self.loader, &mut + // collector); TODO: Charge + // energy for this } // Add update event self.chain_events.push(update_event); @@ -1048,11 +1101,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { if state_changed { println!("\t\tState was changed. Saving prior to another call."); - let mut collector = SizeCollector::default(); - let persistent_state = - self.mutable_state.freeze(&mut self.loader, &mut collector); + // let mut collector = SizeCollector::default(); + let mut loader = v1::trie::Loader::new(&[][..]); + // Make new generation so that we might roll back. + self.chain.instance_changeset(self.address).state = + self.mutable_state.make_fresh_generation(&mut loader); + // let persistent_state = + // self.mutable_state.freeze(&mut + // self.loader, &mut collector); // TODO: Charge for size of new state. - self.chain.get_instance_mut(address)?.state = persistent_state; + // self.chain.get_instance_mut(address)?.state = + // persistent_state; } println!( @@ -1167,7 +1226,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Update the mutable state, since it might have been changed on // reentry. self.mutable_state = - self.chain.get_instance(self.address)?.state.thaw(); + self.chain.instance_changeset(self.address).state.clone(); let resume_res = v1::resume_receive( config, From 2923bd9c4e960ae1aa2f358935f7051189722700 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 19 Jan 2023 09:42:33 +0100 Subject: [PATCH 037/208] Use Arc instead of Rc --- contract-testing/src/lib.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 87a765b1..db22bbb5 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -7,7 +7,7 @@ use concordium_base::{ transactions::{self, cost}, }; use sha2::{Digest, Sha256}; -use std::{collections::BTreeMap, path::Path, rc::Rc}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; use thiserror::Error; use wasm_chain_integration::{ v0, @@ -49,7 +49,7 @@ pub struct Chain { /// Accounts and info about them. pub accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap>, + pub modules: BTreeMap>, /// Smart contract instances. pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. @@ -192,7 +192,7 @@ impl Chain { if self.modules.contains_key(&module_reference) { return Err(DeployModuleError::DuplicateModule(module_reference)); } - self.modules.insert(module_reference, Rc::new(artifact)); + self.modules.insert(module_reference, Arc::new(artifact)); Ok(SuccessfulModuleDeployment { module_reference, energy, @@ -427,7 +427,7 @@ impl Chain { // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; - let artifact = self.get_artifact(instance.module_reference)?.clone(); + let artifact = self.get_artifact(instance.module_reference)?; // Subtract the cost of looking up the module remaining_energy = Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); @@ -474,7 +474,7 @@ impl Chain { // Get the initial result from invoking receive let res = v1::invoke_receive( - std::sync::Arc::new(artifact), + artifact, receive_ctx, v1::ReceiveInvocation { amount, @@ -717,8 +717,7 @@ impl Chain { v1::ReceiveContext, v1::ReceiveContext, >( - std::sync::Arc::new(artifact.clone()), /* TODO: I made ProcessedImports cloneable - * for this to work. */ + artifact, receive_ctx, v1::ReceiveInvocation { amount, @@ -901,11 +900,13 @@ impl Chain { Energy::from(artifact.code.len() as u64 / 50) } - fn get_artifact(&self, module_ref: ModuleReference) -> Result<&ArtifactV1, ModuleMissing> { - match self.modules.get(&module_ref) { - Some(artifact) => Ok(artifact.as_ref()), - None => Err(ModuleMissing(module_ref)), - } + /// Returns an Arc clone of the artifact. + fn get_artifact(&self, module_ref: ModuleReference) -> Result, ModuleMissing> { + let artifact = self + .modules + .get(&module_ref) + .ok_or(ModuleMissing(module_ref))?; + Ok(Arc::clone(artifact)) } fn get_instance( From 097bf348ea01892ff2f198a48ee394a62f435ab4 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 20 Jan 2023 16:13:04 +0100 Subject: [PATCH 038/208] Add stack of states in changeset and add tests from scheduler --- contract-testing/src/lib.rs | 390 +++++++++++++++++++++++++++++++----- 1 file changed, 341 insertions(+), 49 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index db22bbb5..789bc178 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -66,7 +66,7 @@ pub struct ContractInstance { pub self_balance: Amount, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum AmountDelta { Positive(Amount), Negative(Amount), @@ -76,20 +76,42 @@ impl AmountDelta { } /// Data held for a contract instance during the execution of a transaction. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct InstanceChangeSet { // modification_index ? amount_changed: AmountDelta, - state: MutableState, + state_stack: Vec, } impl InstanceChangeSet { fn from_state(state: MutableState) -> Self { Self { amount_changed: AmountDelta::new(), - state, + state_stack: vec![state], + } + } + + /// This assumes that there always is /at least/ one state in the + /// `state_stack`, which should always be true. + /// Return InstanceState directly here? + fn get_last_state(&mut self) -> &mut MutableState { + let last_elem_pos = self.state_stack.len() - 1; + &mut self.state_stack[last_elem_pos] + } + + /// Will remove the last state added to the stack. But will never remove the + /// initial state. + fn remove_last_state(&mut self) { + if self.state_stack.len() > 1 { + self.state_stack.pop(); } } + + fn add_new_checkpoint(&mut self) { + let mut loader = v1::trie::Loader::new(&[][..]); + let new_mutable_state = self.get_last_state().make_fresh_generation(&mut loader); + self.state_stack.push(new_mutable_state); + } } impl Chain { @@ -467,8 +489,7 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut changeset = self.instance_changeset(address); - let mut mutable_state = changeset.state.make_fresh_generation(&mut loader); + let mutable_state = &self.instance_changeset(address).get_last_state(); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -501,7 +522,6 @@ impl Chain { entrypoint: entrypoint.to_owned(), chain: self, chain_events: Vec::new(), - mutable_state, loader, }; @@ -644,10 +664,15 @@ impl Chain { let mut loader = v1::trie::Loader::new(&[][..]); let mut collector = SizeCollector::default(); + // Apply changes from changesets to the persisted data. + // TODO: We should make sure that only valid changesets are kept. I.e. if it + // made changes and failed, then it shouldn't be frozen. for (addr, changeset) in self.instance_changesets.iter_mut() { - self.contracts.get_mut(addr).expect("Must exist").state = - changeset.state.freeze(&mut loader, &mut collector); + self.contracts.get_mut(addr).expect("Must exist").state = changeset + .get_last_state() + .freeze(&mut loader, &mut collector); } + // Clear the changeset. self.instance_changesets.clear(); // TODO: Charge for size in collector; @@ -822,13 +847,12 @@ impl Chain { } /// Precondition: `address` exists in state.contracts. - fn instance_changeset(&mut self, address: ContractAddress) -> InstanceChangeSet { + fn instance_changeset(&mut self, address: ContractAddress) -> &mut InstanceChangeSet { self.instance_changesets .entry(address) .or_insert(InstanceChangeSet::from_state( self.contracts[&address].state.thaw(), )) - .clone() } pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } @@ -949,11 +973,12 @@ struct ProcessReceiveData<'a, 'b> { /// The CCD amount reserved from the invoker account for the energy. While /// the amount is reserved, it is not subtracted in the chain.accounts /// map. Used to handle account balance queries for the invoker account. + /// TODO: We could use a changeset for accounts -> balance, and then look up + /// the "chain.accounts" values for chain queries. invoker_amount_reserved_for_nrg: Amount, entrypoint: OwnedEntrypointName, chain: &'a mut Chain, chain_events: Vec, - mutable_state: MutableState, loader: v1::trie::Loader<&'b [u8]>, } @@ -982,19 +1007,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { entrypoint: self.entrypoint.clone(), amount: self.amount, }; - if state_changed { - // let instance = self - // .chain - // .get_instance_mut(self.address) - // .expect("Instance known to exist"); - // let mut collector = v1::trie::SizeCollector::default(); - self.chain.instance_changeset(self.address).state = - self.mutable_state.clone(); - // instance.state = - // self.mutable_state.freeze(&mut self.loader, &mut - // collector); TODO: Charge - // energy for this - } // Add update event self.chain_events.push(update_event); Ok(v1::ReceiveResult::Success { @@ -1083,7 +1095,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(remaining_energy), - &mut self.mutable_state, + &mut self.chain.instance_changeset(self.address).get_last_state(), false, // never changes on transfers self.loader, ); @@ -1100,20 +1112,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); - if state_changed { - println!("\t\tState was changed. Saving prior to another call."); - // let mut collector = SizeCollector::default(); - let mut loader = v1::trie::Loader::new(&[][..]); - // Make new generation so that we might roll back. - self.chain.instance_changeset(self.address).state = - self.mutable_state.make_fresh_generation(&mut loader); - // let persistent_state = - // self.mutable_state.freeze(&mut - // self.loader, &mut collector); - // TODO: Charge for size of new state. - // self.chain.get_instance_mut(address)?.state = - // persistent_state; - } + // Make a checkpoint before calling another contract so that we may roll + // back. + self.chain + .instance_changeset(self.address) + .add_new_checkpoint(); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", @@ -1224,16 +1227,19 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { ); self.chain_events.push(resume_event); - // Update the mutable state, since it might have been changed on - // reentry. - self.mutable_state = - self.chain.instance_changeset(self.address).state.clone(); + // Remove the last state changes if the invocation failed. + if !success { + println!("---- Removing last state due to !success"); + self.chain + .instance_changeset(self.address) + .remove_last_state(); + }; let resume_res = v1::resume_receive( config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.mutable_state, + self.chain.instance_changeset(self.address).get_last_state(), state_changed, self.loader, ); @@ -1308,7 +1314,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.mutable_state, + self.chain.instance_changeset(self.address).get_last_state(), state_changed, self.loader, ); @@ -1360,7 +1366,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.mutable_state, + self.chain.instance_changeset(self.address).get_last_state(), false, // State never changes on queries. self.loader, ); @@ -1393,7 +1399,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.mutable_state, + self.chain.instance_changeset(self.address).get_last_state(), false, // State never changes on queries. self.loader, ); @@ -1421,7 +1427,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.mutable_state, + self.chain.instance_changeset(self.address).get_last_state(), false, // State never changes on queries. self.loader, ); @@ -3318,4 +3324,290 @@ mod tests { ])); } } + + /// Tests related to checkpoints and rollbacks of the contract state. + mod checkpointing { + use super::*; + + /// This test has the following call pattern: + /// A + /// --> B + /// --> A + /// <-- + /// B(trap) + /// A <-- + /// The state at A should be left unchanged by the changes of the + /// 'inner' invocation on contract A. A correctly perceives B's + /// trapping signal. Only V1 contracts are being used. + #[test] + fn test_case_1() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward_crash"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + ContractParameter::from_typed(¶meter), + // We supply one microCCD as we expect a trap + // (see contract for details). + Amount::from_micro_ccd(1), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + } + + /// This test has the following call pattern: + /// A + /// --> B + /// --> A (no modification, but bump iterator) + /// <-- + /// B + /// A <-- + /// + /// The state at A should be left unchanged. + /// The iterator initialized at the outer A should point to the same + /// entry as before the call. That is, the iterator should not + /// be affected by the inner iterator. Only V1 contracts are + /// being used. + #[test] + fn test_case_2() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_no_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + ContractParameter::from_typed(¶meter), + // We supply zero microCCD as we're instructing the contract to not expect + // state modifications. Also, the contract does not expect + // errors, i.e., a trap signal from underlying invocations. + // The 'inner' call to contract A does not modify the state. + // See the contract for details. + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + } + + /// This test has the following call pattern: + /// A + /// --> Transfer + /// A <-- + /// + /// The state at A should be left unchanged. + /// The iterator initialized at A should after the call point to the + /// same entry as before the call. Only V1 contracts are being + /// used. + #[test] + fn test_case_3() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_update( + ACC_0, + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + ContractParameter::from_typed(&ACC_1), + // We supply three micro CCDs as we're instructing the contract to carry out a + // transfer instead of a call. See the contract for + // details. + Amount::from_micro_ccd(3), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + } + + /// This test has the following call pattern: + /// A + /// --> B + /// --> A modify + /// <-- + /// B + /// A <-- + /// + /// The state at A should have changed according to the 'inner' + /// invocation on contract A. Only V1 contracts are being used. + #[test] + fn test_case_4() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + ContractParameter::from_typed(¶meter), + // We supply four CCDs as we're instructing the contract to expect state + // modifications being made from the 'inner' contract A + // call to be in effect when returned to the caller (a.a_modify_proxy). + // See the contract for details. + Amount::from_micro_ccd(4), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + } + } } From c9b58df3e79fc7ba77cccf5669e7af8bbaa52813 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 13:41:13 +0100 Subject: [PATCH 039/208] Add subtract/add methods to AmountDelta --- contract-testing/src/lib.rs | 69 +++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 789bc178..31eba9da 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -66,21 +66,47 @@ pub struct ContractInstance { pub self_balance: Amount, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AmountDelta { Positive(Amount), Negative(Amount), } impl AmountDelta { - pub fn new() -> Self { Self::Positive(Amount::zero()) } + fn new() -> Self { Self::Positive(Amount::zero()) } + + fn subtract(&mut self, amount: Amount) { + match self { + Self::Positive(current) => { + if *current >= amount { + current.subtract_micro_ccd(amount.micro_ccd); + } else { + *self = Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)); + } + } + Self::Negative(current) => { + *self = Self::Negative(current.add_micro_ccd(amount.micro_ccd)); + } + } + } + + fn add(&mut self, amount: Amount) { + match self { + Self::Positive(current) => { + current.add_micro_ccd(amount.micro_ccd); + } + Self::Negative(current) => { + if *current >= amount { + *self = Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)); + } else { + *self = Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)); + } + } + } + } } -/// Data held for a contract instance during the execution of a transaction. -#[derive(Clone, Debug)] pub struct InstanceChangeSet { - // modification_index ? - amount_changed: AmountDelta, - state_stack: Vec, + stack: Vec, } impl InstanceChangeSet { @@ -3610,4 +3636,33 @@ mod tests { .expect("Updating contract should succeed"); } } + + mod amount_delta { + use super::*; + + #[test] + fn test() { + let mut x = AmountDelta::new(); + assert_eq!(x, AmountDelta::Positive(Amount::zero())); + + let one = Amount::from_ccd(1); + let two = Amount::from_ccd(2); + let three = Amount::from_ccd(3); + let five = Amount::from_ccd(5); + + x.subtract(one); // -1 CCD + x.subtract(one); // -2 CCD + assert_eq!(x, AmountDelta::Negative(two)); + x.add(five); // +3 CCD + assert_eq!(x, AmountDelta::Positive(three)); + x.subtract(five); // -2 CCD + assert_eq!(x, AmountDelta::Negative(two)); + x.add(two); // 0 + + x.add(Amount::from_micro_ccd(1)); // 1 mCCD + assert_eq!(x, AmountDelta::Positive(Amount::from_micro_ccd(1))); + x.subtract(Amount::from_micro_ccd(2)); // -1 mCCD + assert_eq!(x, AmountDelta::Negative(Amount::from_micro_ccd(1))); + } + } } From ac0ea2e8f0877f6c594a3a20fa52b43e7d2370e8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 13:43:06 +0100 Subject: [PATCH 040/208] Add stack of changesets --- contract-testing/src/lib.rs | 131 +++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 39 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 31eba9da..43808209 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -111,33 +111,51 @@ pub struct InstanceChangeSet { impl InstanceChangeSet { fn from_state(state: MutableState) -> Self { - Self { + let initial_changeset = InstanceChanges { + modification_index: 0, amount_changed: AmountDelta::new(), - state_stack: vec![state], + state, + }; + Self { + stack: vec![initial_changeset], } } - /// This assumes that there always is /at least/ one state in the - /// `state_stack`, which should always be true. - /// Return InstanceState directly here? - fn get_last_state(&mut self) -> &mut MutableState { - let last_elem_pos = self.state_stack.len() - 1; - &mut self.state_stack[last_elem_pos] + /// Get a mutable reference to the last instance changeset. + /// Will panic if the stack is empty. + fn get_last(&mut self) -> &mut InstanceChanges { + let last_index = self.stack.len() - 1; + &mut self.stack[last_index] } - /// Will remove the last state added to the stack. But will never remove the - /// initial state. - fn remove_last_state(&mut self) { - if self.state_stack.len() > 1 { - self.state_stack.pop(); - } - } + /// Remove from the stack until the index is reached, i.e. not including the + /// index. Will panic if the index is less than the number of elements + /// in the stack. + fn remove_until_index(&mut self, index: u32) { self.stack.truncate(index as usize + 1) } - fn add_new_checkpoint(&mut self) { + /// Make a new checkpoint. + /// Will panic if the stack is empty. + fn checkpoint(&mut self) { let mut loader = v1::trie::Loader::new(&[][..]); - let new_mutable_state = self.get_last_state().make_fresh_generation(&mut loader); - self.state_stack.push(new_mutable_state); + let last = self.get_last(); + let new_checkpoint = InstanceChanges { + modification_index: last.modification_index + 1, + amount_changed: last.amount_changed, + state: last.state.make_fresh_generation(&mut loader), + }; + self.stack.push(new_checkpoint); } + + /// Returns whether there are any changes to the instance, which means that + /// there are more than the original changeset element on the stack. + fn has_changes(&mut self) -> bool { self.stack.len() > 1 } +} +/// Data held for a contract instance during the execution of a transaction. +#[derive(Clone, Debug)] +pub struct InstanceChanges { + modification_index: u32, + amount_changed: AmountDelta, + state: MutableState, } impl Chain { @@ -515,7 +533,15 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mutable_state = &self.instance_changeset(address).get_last_state(); + let instance_changeset = self.instance_changeset(address); + let modification_index_before_invoke = instance_changeset.get_last().modification_index; + println!( + "Checkpointing {} from {}", + address, modification_index_before_invoke, + ); + // Make a checkpoint. + instance_changeset.checkpoint(); + let mutable_state = &mut instance_changeset.get_last().state; let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -556,6 +582,16 @@ impl Chain { // Append the new chain events if the invocation succeeded. if matches!(res, Ok(v1::ReceiveResult::Success { .. })) { chain_events.append(&mut data.chain_events); + } else { + let instance_changeset = self.instance_changeset(address); + println!( + "Removing checkpoints [{}..{}] for contract {}", + modification_index_before_invoke + 1, + instance_changeset.get_last().modification_index, + address, + ); + // Pop a changeset if the invocation failed. + instance_changeset.remove_until_index(modification_index_before_invoke); } res } @@ -694,9 +730,17 @@ impl Chain { // TODO: We should make sure that only valid changesets are kept. I.e. if it // made changes and failed, then it shouldn't be frozen. for (addr, changeset) in self.instance_changesets.iter_mut() { - self.contracts.get_mut(addr).expect("Must exist").state = changeset - .get_last_state() - .freeze(&mut loader, &mut collector); + if changeset.has_changes() { + println!( + "Freezing changeset {} of contract {}", + changeset.get_last().modification_index, + addr + ); + self.contracts.get_mut(addr).expect("Must exist").state = changeset + .get_last() + .state + .freeze(&mut loader, &mut collector); + } } // Clear the changeset. self.instance_changesets.clear(); @@ -1121,7 +1165,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(remaining_energy), - &mut self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, false, // never changes on transfers self.loader, ); @@ -1140,9 +1184,15 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Make a checkpoint before calling another contract so that we may roll // back. - self.chain - .instance_changeset(self.address) - .add_new_checkpoint(); + let changeset = self.chain.instance_changeset(self.address); + let modification_index_before_invoke = + changeset.get_last().modification_index; + println!( + "\t\tCheckpointing addr {}, from index: {}", + self.address, + changeset.get_last().modification_index + ); + changeset.checkpoint(); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", @@ -1242,6 +1292,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { success, }; + // Remove the last state changes if the invocation failed. + if !success { + println!( + "\t\tRemove checkpoint due to !success for contract: {}", + self.address + ); + self.chain + .instance_changeset(self.address) + .remove_until_index(modification_index_before_invoke); + }; + println!( "\tResuming contract {}\n\t\tafter {}", self.address, @@ -1253,19 +1314,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { ); self.chain_events.push(resume_event); - // Remove the last state changes if the invocation failed. - if !success { - println!("---- Removing last state due to !success"); - self.chain - .instance_changeset(self.address) - .remove_last_state(); - }; - let resume_res = v1::resume_receive( config, response, InterpreterEnergy::from(energy_after_invoke), - self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, state_changed, self.loader, ); @@ -1340,7 +1393,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, state_changed, self.loader, ); @@ -1392,7 +1445,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, false, // State never changes on queries. self.loader, ); @@ -1425,7 +1478,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, false, // State never changes on queries. self.loader, ); @@ -1453,7 +1506,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - self.chain.instance_changeset(self.address).get_last_state(), + &mut self.chain.instance_changeset(self.address).get_last().state, false, // State never changes on queries. self.loader, ); From ba715e8a059ad60bd1e00bf1b66b0c61aaec785b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 13:59:18 +0100 Subject: [PATCH 041/208] Remove changesets if no state changes occurred --- contract-testing/src/lib.rs | 45 +++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 43808209..6dd9c734 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -464,6 +464,8 @@ impl Chain { /// Returns: /// - Everything the types can encode apart from /// `Ok(v1::ReceiveResult::Interrupt)` + /// + /// TODO: Change return type, so it can't return Interrupt. /// TODO: Use proper error types instead of anyhow. fn contract_update_aux( &mut self, @@ -580,19 +582,34 @@ impl Chain { // Process the receive invocation to the end. let res = data.process(res); // Append the new chain events if the invocation succeeded. - if matches!(res, Ok(v1::ReceiveResult::Success { .. })) { - chain_events.append(&mut data.chain_events); - } else { - let instance_changeset = self.instance_changeset(address); - println!( - "Removing checkpoints [{}..{}] for contract {}", - modification_index_before_invoke + 1, - instance_changeset.get_last().modification_index, - address, - ); - // Pop a changeset if the invocation failed. - instance_changeset.remove_until_index(modification_index_before_invoke); + + match res { + Ok(v1::ReceiveResult::Success { state_changed, .. }) => { + chain_events.append(&mut data.chain_events); + if !state_changed { + let instance_changeset = self.instance_changeset(address); + println!( + "Removing checkpoints [{}..{}] for contract {}", + modification_index_before_invoke + 1, + instance_changeset.get_last().modification_index, + address, + ); + instance_changeset.remove_until_index(modification_index_before_invoke); + } + } + _ => { + let instance_changeset = self.instance_changeset(address); + println!( + "Removing checkpoints [{}..{}] for contract {}", + modification_index_before_invoke + 1, + instance_changeset.get_last().modification_index, + address, + ); + // Pop a changeset if the invocation failed. + instance_changeset.remove_until_index(modification_index_before_invoke); + } } + res } @@ -1293,7 +1310,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }; // Remove the last state changes if the invocation failed. - if !success { + if !success || !state_changed { println!( "\t\tRemove checkpoint due to !success for contract: {}", self.address @@ -3589,7 +3606,7 @@ mod tests { ) .expect("Initializing valid contract should work"); - let res_init_b = chain + chain .contract_init( ACC_0, res_deploy.module_reference, From bed4d4ae11e708e1beb7f69573fad4b07efb9d1f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 14:12:02 +0100 Subject: [PATCH 042/208] Remove logs for failures The plan is to reintroduce these at a later point when the wasm interpreter can return them. --- contract-testing/src/lib.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 6dd9c734..0a2dee7c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -416,7 +416,6 @@ impl Chain { return_value, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )) } @@ -434,7 +433,6 @@ impl Chain { error, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )) } @@ -444,7 +442,6 @@ impl Chain { FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, - logs: v0::Logs::default(), // TODO: Get Logs on failures. }, )) } @@ -695,7 +692,6 @@ impl Chain { return_value, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -713,7 +709,6 @@ impl Chain { error, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -723,7 +718,6 @@ impl Chain { FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -883,7 +877,6 @@ impl Chain { return_value, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -901,7 +894,6 @@ impl Chain { error, energy_used, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -911,7 +903,6 @@ impl Chain { FailedContractInteraction::OutOfEnergy { energy_used: energy_reserved, transaction_fee, - logs: v0::Logs::default(), // TODO: Get logs on failures. }, )) } @@ -1666,27 +1657,15 @@ pub enum FailedContractInteraction { return_value: ReturnValue, energy_used: Energy, transaction_fee: Amount, - /// Logs emitted before the interaction failed. Logs from failed - /// updates are not stored on the chain, but can be useful for - /// debugging. - logs: v0::Logs, }, Trap { error: anyhow::Error, energy_used: Energy, transaction_fee: Amount, - /// Logs emitted before the interaction failed. Logs from failed - /// updates are not stored on the chain, but can be useful for - /// debugging. - logs: v0::Logs, }, OutOfEnergy { energy_used: Energy, transaction_fee: Amount, - /// Logs emitted before the interaction failed. Logs from failed - /// updates are not stored on the chain, but can be useful for - /// debugging. - logs: v0::Logs, }, } @@ -1704,14 +1683,6 @@ impl FailedContractInteraction { } => *transaction_fee, } } - - pub fn logs(&self) -> &v0::Logs { - match self { - FailedContractInteraction::Reject { logs, .. } => logs, - FailedContractInteraction::Trap { logs, .. } => logs, - FailedContractInteraction::OutOfEnergy { logs, .. } => logs, - } - } } #[derive(Debug)] From 12471eabe1351b5b03bb9599133dbd2abab9b0d8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 15:24:26 +0100 Subject: [PATCH 043/208] Support recursive contracts calls in contract_invoke By reusing the contract_update_aux function --- contract-testing/src/lib.rs | 341 ++++++++++++++---------------------- 1 file changed, 136 insertions(+), 205 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 0a2dee7c..d8d7d78c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -636,7 +636,7 @@ impl Chain { } let mut chain_events = Vec::new(); - let res = self.contract_update_aux( + let receive_result = self.contract_update_aux( invoker, Address::Account(invoker), address, @@ -648,86 +648,8 @@ impl Chain { &mut chain_events, ); - let mut transaction_fee = Amount::zero(); - - // Convert the wasm_chain_integration result to the one used here and - // update the transaction fee. - let res = match res { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, /* TODO: Could we change this from `u64` to - * `InterpreterEnergy` in chain_integration? */ - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - Ok(SuccessfulContractUpdate { - chain_events, - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, - logs, - }) - } - v1::ReceiveResult::Interrupt { .. } => panic!("Should never happen"), - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - }, - )) - } - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - }, - )) - } - v1::ReceiveResult::OutOfEnergy => { - transaction_fee += self.calculate_energy_cost(energy_reserved); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )) - } - }, - Err(e) => { - // TODO: what is the correct cost here? - transaction_fee += self.calculate_energy_cost(energy_reserved); - Err(ContractUpdateError::InterpreterError(e)) - } - }; + let (res, transaction_fee) = + self.convert_receive_result(receive_result, chain_events, energy_reserved); // Handle rollback if res.is_err() { @@ -738,8 +660,6 @@ impl Chain { let mut loader = v1::trie::Loader::new(&[][..]); let mut collector = SizeCollector::default(); // Apply changes from changesets to the persisted data. - // TODO: We should make sure that only valid changesets are kept. I.e. if it - // made changes and failed, then it shouldn't be frozen. for (addr, changeset) in self.instance_changesets.iter_mut() { if changeset.has_changes() { println!( @@ -774,139 +694,46 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - // Find contract instance and artifact - let instance = self.get_instance(address)?; - let artifact = self.get_artifact(instance.module_reference)?; + // Save backups of accounts and contracts so we can reset them after the invoke. + let accounts_backup = self.accounts.clone(); + let contracts_backup = self.contracts.clone(); println!( - "Invoking contract {} with parameter: {:?}", + "Invoking contract {}, with parameter: {:?}", address, parameter.0 ); - // Ensure account exists and can pay for the reserved energy + // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) { + let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } - // Construct the context - let receive_ctx = v1::ReceiveContext { - entrypoint: entrypoint.to_owned(), - common: v0::ReceiveContext { - metadata: ChainMetadata { - slot_time: self.slot_time, - }, - invoker, - self_address: address, - self_balance: instance.self_balance + amount, - sender: Address::Account(invoker), - owner: instance.owner, - sender_policies: account_info.policies.clone(), - }, - }; - let receive_name = OwnedReceiveName::new_unchecked(format!( - "{}.{}", - instance.contract_name.as_contract_name().contract_name(), - entrypoint - )); - let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = instance.state.thaw(); - let inner = mutable_state.get_inner(&mut loader); - let instance_state = v1::InstanceState::new(loader, inner); + let mut chain_events = Vec::new(); + let receive_result = self.contract_update_aux( + invoker, + Address::Account(invoker), + address, + entrypoint.to_owned(), + Parameter(¶meter.0), + amount, + invoker_amount_reserved_for_nrg, + energy_reserved, + &mut chain_events, + ); - // Update the contract - let res = v1::invoke_receive::< - _, - _, - _, - _, - v1::ReceiveContext, - v1::ReceiveContext, - >( - artifact, - receive_ctx, - v1::ReceiveInvocation { - amount, - receive_name: receive_name.as_receive_name(), - parameter: ¶meter.0, - energy: Chain::to_interpreter_energy(energy_reserved), - }, - instance_state, - v1::ReceiveParams { - max_parameter_size: 65535, - limit_logs_and_return_values: false, - support_queries: true, - }, - )?; + let (res, _) = self.convert_receive_result(receive_result, chain_events, energy_reserved); - match res { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - let transaction_fee = self.calculate_energy_cost(energy_used); - Ok(SuccessfulContractUpdate { - chain_events: Vec::new(), // TODO: add host events - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, // TODO: Should we always set this to false? - logs, - }) - } - v1::ReceiveResult::Interrupt { .. } => todo!("Handle interrupts"), - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - let transaction_fee = self.calculate_energy_cost(energy_used); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - }, - )) - } - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - let transaction_fee = self.calculate_energy_cost(energy_used); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - }, - )) - } - v1::ReceiveResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )) - } - } + // Roll back any changes. + self.accounts = accounts_backup; + self.contracts = contracts_backup; + // Clear the changeset. + self.instance_changesets.clear(); + + // TODO: Add cost for size in collector to transaction_fee + + res } /// Create an account. Will override existing account if already present. @@ -1041,6 +868,110 @@ impl Chain { .get_mut(&address) .ok_or(AccountMissing(address)) } + + /// Convert the wasm_chain_integration result to the one used here and + /// calculate the transaction fee. + fn convert_receive_result( + &self, + receive_result: ExecResult>, + chain_events: Vec, + energy_reserved: Energy, + ) -> ( + Result, + Amount, + ) { + match receive_result { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, /* TODO: Could we change this from `u64` to + * `InterpreterEnergy` in chain_integration? */ + } => { + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Ok(SuccessfulContractUpdate { + chain_events, + energy_used, + transaction_fee, + return_value: ContractReturnValue(return_value), + state_changed, + logs, + }), + transaction_fee, + ) + } + v1::ReceiveResult::Interrupt { .. } => panic!("Should never happen"), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + }, + )), + transaction_fee, + ) + } + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => { + let energy_used = energy_reserved + - Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::Trap { + error, + energy_used, + transaction_fee, + }, + )), + transaction_fee, + ) + } + v1::ReceiveResult::OutOfEnergy => { + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee, + }, + )), + transaction_fee, + ) + } + }, + Err(e) => { + // TODO: what is the correct cost here? + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractUpdateError::InterpreterError(e)), + transaction_fee, + ) + } + } + } } struct ProcessReceiveData<'a, 'b> { From b80290440650f3e4735ebc033b0e60308b53faff Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 23 Jan 2023 15:58:06 +0100 Subject: [PATCH 044/208] Handle overflows in energy to ccd calculation --- contract-testing/Cargo.lock | 1 + contract-testing/Cargo.toml | 1 + contract-testing/src/lib.rs | 32 ++++++++++++++++++++++---------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index a40871c0..d84c0455 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -307,6 +307,7 @@ version = "0.1.0" dependencies = [ "anyhow", "concordium_base", + "num-bigint 0.4.3", "sha2 0.10.6", "thiserror", "wasm-chain-integration", diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 2f4a5602..99e9e988 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -12,3 +12,4 @@ wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" +num-bigint = "0.4.3" diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d8d7d78c..b91779c7 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -6,6 +6,7 @@ use concordium_base::{ smart_contracts::{ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost}, }; +use num_bigint::BigUint; use sha2::{Digest, Sha256}; use std::{collections::BTreeMap, path::Path, sync::Arc}; use thiserror::Error; @@ -796,17 +797,16 @@ impl Chain { // mCCD NRG * mCCD // NRG * ---- = ---------- = mCCD // NRG NRG - // - // TODO: If using a mCCD/euro exchange rate with large numbers, then this can - // overflow. pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { - let micro_ccd_per_energy_numerator = - self.euro_per_energy.numerator() * self.micro_ccd_per_euro.numerator(); - let micro_ccd_per_energy_denominator = - self.euro_per_energy.denominator() * self.micro_ccd_per_euro.denominator(); - Amount::from_micro_ccd( - energy.energy * micro_ccd_per_energy_numerator / micro_ccd_per_energy_denominator, - ) + let micro_ccd_per_energy_numerator: BigUint = + BigUint::from(self.euro_per_energy.numerator()) * self.micro_ccd_per_euro.numerator(); + let micro_ccd_per_energy_denominator: BigUint = + BigUint::from(self.euro_per_energy.denominator()) + * self.micro_ccd_per_euro.denominator(); + let cost: BigUint = + (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; + let cost: u64 = u64::try_from(cost).expect("Should never overflow due to use of BigUint"); + Amount::from_micro_ccd(cost) } /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. @@ -2224,6 +2224,18 @@ mod tests { assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } + #[test] + fn calculate_cost_will_not_overflow() { + let chain = Chain::new_with_time_and_rates( + SlotTime::from_timestamp_millis(0), + ExchangeRate::new_unchecked(u64::MAX, u64::MAX - 1), + ExchangeRate::new_unchecked(u64::MAX - 2, u64::MAX - 3), + ); + + let energy = Energy::from(u64::MAX - 4); + chain.calculate_energy_cost(energy); + } + mod query_account_balance { use super::*; From d0b96deed1d95d6c0dfcaf14bb78128e33023d0e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 24 Jan 2023 11:03:32 +0100 Subject: [PATCH 045/208] Add self_balance to instance changeset --- contract-testing/src/lib.rs | 135 +++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 56 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index b91779c7..44e2339a 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -111,10 +111,10 @@ pub struct InstanceChangeSet { } impl InstanceChangeSet { - fn from_state(state: MutableState) -> Self { + fn new(self_balance: Amount, state: MutableState) -> Self { let initial_changeset = InstanceChanges { modification_index: 0, - amount_changed: AmountDelta::new(), + self_balance, state, }; Self { @@ -141,7 +141,7 @@ impl InstanceChangeSet { let last = self.get_last(); let new_checkpoint = InstanceChanges { modification_index: last.modification_index + 1, - amount_changed: last.amount_changed, + self_balance: last.self_balance, state: last.state.make_fresh_generation(&mut loader), }; self.stack.push(new_checkpoint); @@ -155,7 +155,8 @@ impl InstanceChangeSet { #[derive(Clone, Debug)] pub struct InstanceChanges { modification_index: u32, - amount_changed: AmountDelta, + // amount_changed: AmountDelta, + self_balance: Amount, state: MutableState, } @@ -481,15 +482,28 @@ impl Chain { chain_events: &mut Vec, ) -> ExecResult> { // Move the amount from the sender to the contract. - match sender { + let contract_sender_info = match sender { Address::Account(addr) => { self.get_account_mut(addr)?.balance -= amount; + None } Address::Contract(addr) => { - self.get_instance_mut(addr)?.self_balance -= amount; + let instance_changeset = self.instance_changeset(addr); + let modification_index = instance_changeset.get_last().modification_index; + instance_changeset.checkpoint(); + instance_changeset.get_last().self_balance -= amount; + Some((addr, modification_index)) } - } - self.get_instance_mut(address)?.self_balance += amount; + }; + + let instance_changeset = self.instance_changeset(address); + let modification_index_before_invoke = instance_changeset.get_last().modification_index; + println!( + "Checkpointing {} from {}", + address, modification_index_before_invoke, + ); + instance_changeset.checkpoint(); + instance_changeset.get_last().self_balance += amount; // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; @@ -533,15 +547,7 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let instance_changeset = self.instance_changeset(address); - let modification_index_before_invoke = instance_changeset.get_last().modification_index; - println!( - "Checkpointing {} from {}", - address, modification_index_before_invoke, - ); - // Make a checkpoint. - instance_changeset.checkpoint(); - let mutable_state = &mut instance_changeset.get_last().state; + let mutable_state = &mut self.instance_changeset(address).get_last().state; let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -605,6 +611,12 @@ impl Chain { ); // Pop a changeset if the invocation failed. instance_changeset.remove_until_index(modification_index_before_invoke); + + // If the sender was a contract, remove the potential balance changes. + if let Some((addr, modification_index)) = contract_sender_info { + self.instance_changeset(addr) + .remove_until_index(modification_index) + } } } @@ -668,10 +680,10 @@ impl Chain { changeset.get_last().modification_index, addr ); - self.contracts.get_mut(addr).expect("Must exist").state = changeset - .get_last() - .state - .freeze(&mut loader, &mut collector); + let changes = changeset.get_last(); + let contract = self.contracts.get_mut(&addr).expect("Known to exist."); + contract.state = changes.state.freeze(&mut loader, &mut collector); + contract.self_balance = changes.self_balance; } } // Clear the changeset. @@ -684,8 +696,6 @@ impl Chain { } /// TODO: Should we make invoker and energy optional? - /// TODO: Only works with basic update functions. Rewrite to use - /// `contract_update_aux`. pub fn contract_invoke( &mut self, invoker: AccountAddress, @@ -754,11 +764,10 @@ impl Chain { /// Precondition: `address` exists in state.contracts. fn instance_changeset(&mut self, address: ContractAddress) -> &mut InstanceChangeSet { - self.instance_changesets - .entry(address) - .or_insert(InstanceChangeSet::from_state( - self.contracts[&address].state.thaw(), - )) + self.instance_changesets.entry(address).or_insert({ + let contract = &self.contracts[&address]; + InstanceChangeSet::new(contract.self_balance, contract.state.thaw()) + }) } pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } @@ -1055,8 +1064,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } else if self .chain - .get_instance(self.address) - .expect("Contract is known to exist") + .instance_changeset(self.address) + .get_last() .self_balance < amount { @@ -1065,20 +1074,14 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } else { // Move the CCD - let instance_new_balance = match ( - self.chain.accounts.get_mut(&to), - self.chain.contracts.get_mut(&self.address), - ) { - (Some(acc), Some(contr)) => { - contr.self_balance -= amount; - acc.balance += amount; - contr.self_balance - } - _ => anyhow::bail!( - "Contract or Account missing when they are known to \ - exist." - ), - }; + self.chain + .accounts + .get_mut(&to) + .expect("Known to exist") + .balance += amount; + let instance_changes = + self.chain.instance_changeset(self.address).get_last(); + instance_changes.self_balance -= amount; // Add transfer event self.chain_events.push(ChainEvent::Transferred { @@ -1088,7 +1091,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }); InvokeResponse::Success { - new_balance: instance_new_balance, + new_balance: instance_changes.self_balance, data: None, } } @@ -1121,9 +1124,10 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); + // TODO: Check that contract has sufficient balance here. + let changeset = self.chain.instance_changeset(self.address); // Make a checkpoint before calling another contract so that we may roll // back. - let changeset = self.chain.instance_changeset(self.address); let modification_index_before_invoke = changeset.get_last().modification_index; println!( @@ -1170,7 +1174,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { InvokeResponse::Success { new_balance: self .chain - .get_instance(self.address)? + .instance_changeset(address) + .get_last() .self_balance, data: Some(return_value), }, @@ -1270,6 +1275,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event. self.chain_events.push(interrupt_event); + // TODO: Add module to changeset. + // Charge a base cost. let mut energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( @@ -1364,7 +1371,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { InvokeResponse::Success { new_balance: self .chain - .get_instance(self.address)? + .instance_changeset(self.address) + .get_last() .self_balance, data: Some(balances), } @@ -1394,17 +1402,28 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { v1::Interrupt::QueryContractBalance { address } => { println!("Querying contract balance of {}", address); - let response = match self.chain.contract_balance(address) { - Some(balance) => InvokeResponse::Success { + let response = if self.chain.contracts.contains_key(&address) { + InvokeResponse::Success { + // Balance of contract querying. Won't change. new_balance: self .chain - .get_instance(self.address)? + .instance_changeset(self.address) + .get_last() .self_balance, - data: Some(to_bytes(&balance)), - }, - None => InvokeResponse::Failure { + // Balance of the queried contract. Notice the `address` vs + // `self.address`. + data: Some(to_bytes( + &self + .chain + .instance_changeset(address) + .get_last() + .self_balance, + )), + } + } else { + InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, - }, + } }; let energy_after_invoke = remaining_energy @@ -1431,7 +1450,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); let response = InvokeResponse::Success { - new_balance: self.chain.get_instance(self.address)?.self_balance, + new_balance: self + .chain + .instance_changeset(self.address) + .get_last() + .self_balance, data: Some(to_bytes(&exchange_rates)), }; From 1e541bc24bf116ce01433d3f8618c21db0d012a0 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 25 Jan 2023 10:01:32 +0100 Subject: [PATCH 046/208] Group tests using the integrate contract --- contract-testing/src/lib.rs | 466 ++++++++++++++++++------------------ 1 file changed, 236 insertions(+), 230 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 44e2339a..7b539729 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1937,130 +1937,258 @@ mod tests { assert_eq!(res_invoke_get.return_value.0, [1u8]); } - #[test] - fn update_with_account_transfer_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + /// Tests using the integrate contract defined in + /// concordium-rust-smart-contract on the 'kb/sc-integration-testing' + /// branch. + mod integrate_contract { + use super::*; - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); + #[test] + fn update_with_account_transfer_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(1); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); - let res_update = chain - .contract_update( + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + ContractParameter::from_typed(&ACC_1), + transfer_amount, + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + - transfer_amount + ) + ); + assert_eq!( + chain.account_balance(ACC_1), + Some(initial_balance + transfer_amount) + ); + assert_eq!(res_update.transfers(), [Transfer { + from: res_init.contract_address, + amount: transfer_amount, + to: ACC_1, + }]); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, [2, 0, 0, 0]); + } + + #[test] + fn update_with_account_transfer_to_missing_account_fails() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(1); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain.contract_update( ACC_0, res_init.contract_address, EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&ACC_1), + ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. transfer_amount, - Energy::from(10000), - ) - .expect("Updating valid contract should work"); + Energy::from(100000), + ); - let res_view = chain - .contract_invoke( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("view"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); + match res_update { + Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { + reason, + transaction_fee, + .. + })) => { + assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - transaction_fee + ) + ); + } + _ => panic!("Expected contract update to fail"), + } + } - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - - transfer_amount - ) - ); - assert_eq!( - chain.account_balance(ACC_1), - Some(initial_balance + transfer_amount) - ); - assert_eq!(res_update.transfers(), [Transfer { - from: res_init.contract_address, - amount: transfer_amount, - to: ACC_1, - }]); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, [2, 0, 0, 0]); - } + #[test] + fn update_with_integrate_reentry_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - #[test] - fn update_with_account_transfer_to_missing_account_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("recurse"), + ContractParameter::from_typed(&10u32), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Updating valid contract should work"); - let res_update = chain.contract_update( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. - transfer_amount, - Energy::from(100000), - ); + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); - match res_update { - Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { - reason, - transaction_fee, - .. - })) => { - assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing - assert_eq!( - chain.account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - transaction_fee - ) - ); - } - _ => panic!("Expected contract update to fail"), + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + let expected_res = 10 + 7 + 11 + 3 + 7 + 11; + assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } - } + #[test] + fn update_with_rollback_and_reentry_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let input_param: u32 = 8; + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("inc-fail-on-zero"), + ContractParameter::from_typed(&input_param), + Amount::zero(), + Energy::from(100000000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + res_init.contract_address, + EntrypointName::new_unchecked("view"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + assert_eq!( + chain.account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(res_update.state_changed); + let expected_res = 2u32.pow(input_param) - 1; + assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); + } + } // TODO: Add tests that check: // - Correct account balances after init / update failures (when Amount > 0) // @@ -2125,128 +2253,6 @@ mod tests { assert_eq!(res_view.return_value.0, expected_res); } - #[test] - fn update_with_integrate_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("recurse"), - ContractParameter::from_typed(&10u32), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("view"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - let expected_res = 10 + 7 + 11 + 3 + 7 + 11; - assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); - } - - #[test] - fn update_with_rollback_and_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let input_param: u32 = 8; - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("inc-fail-on-zero"), - ContractParameter::from_typed(&input_param), - Amount::zero(), - Energy::from(100000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - res_init.contract_address, - EntrypointName::new_unchecked("view"), - ContractParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - assert_eq!( - chain.account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(res_update.state_changed); - let expected_res = 2u32.pow(input_param) - 1; - assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); - } - #[test] fn calculate_cost_will_not_overflow() { let chain = Chain::new_with_time_and_rates( From 8b2a712fe7924fcf2c07e347777e555529655454 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 25 Jan 2023 12:00:49 +0100 Subject: [PATCH 047/208] Do not clone contracts (handled with changesets instead) --- contract-testing/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 7b539729..6c7a3d6d 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -634,7 +634,6 @@ impl Chain { ) -> Result { // Save backups of accounts and contracts in case of rollbacks. let accounts_backup = self.accounts.clone(); - let contracts_backup = self.contracts.clone(); println!( "Updating contract {}, with parameter: {:?}", @@ -667,7 +666,6 @@ impl Chain { // Handle rollback if res.is_err() { self.accounts = accounts_backup; - self.contracts = contracts_backup; } let mut loader = v1::trie::Loader::new(&[][..]); @@ -707,7 +705,6 @@ impl Chain { ) -> Result { // Save backups of accounts and contracts so we can reset them after the invoke. let accounts_backup = self.accounts.clone(); - let contracts_backup = self.contracts.clone(); println!( "Invoking contract {}, with parameter: {:?}", @@ -738,7 +735,6 @@ impl Chain { // Roll back any changes. self.accounts = accounts_backup; - self.contracts = contracts_backup; // Clear the changeset. self.instance_changesets.clear(); From 2156d120287a99d8dc1aaf33700a4dcf58da9ebd Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 8 Feb 2023 16:46:21 +0100 Subject: [PATCH 048/208] Rework checkpointing system --- contract-testing/src/lib.rs | 1095 ++++++++++++++++++++++++++--------- 1 file changed, 834 insertions(+), 261 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 6c7a3d6d..c83b125c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -8,7 +8,11 @@ use concordium_base::{ }; use num_bigint::BigUint; use sha2::{Digest, Sha256}; -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::{ + collections::{btree_map, BTreeMap}, + path::Path, + sync::Arc, +}; use thiserror::Error; use wasm_chain_integration::{ v0, @@ -55,7 +59,7 @@ pub struct Chain { pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. pub next_contract_index: u64, - pub instance_changesets: BTreeMap, + changeset: ChangeSet, } #[derive(Clone)] @@ -72,94 +76,657 @@ pub enum AmountDelta { Positive(Amount), Negative(Amount), } + impl AmountDelta { fn new() -> Self { Self::Positive(Amount::zero()) } - fn subtract(&mut self, amount: Amount) { + fn subtract_amount(self, amount: Amount) -> Self { match self { Self::Positive(current) => { - if *current >= amount { - current.subtract_micro_ccd(amount.micro_ccd); + if current >= amount { + Self::Positive(current.subtract_micro_ccd(amount.micro_ccd)) } else { - *self = Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)); + Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)) } } - Self::Negative(current) => { - *self = Self::Negative(current.add_micro_ccd(amount.micro_ccd)); - } + Self::Negative(current) => Self::Negative(current.add_micro_ccd(amount.micro_ccd)), } } - fn add(&mut self, amount: Amount) { + fn add_amount(self, amount: Amount) -> Self { match self { - Self::Positive(current) => { - current.add_micro_ccd(amount.micro_ccd); - } + Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), Self::Negative(current) => { - if *current >= amount { - *self = Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)); + if current >= amount { + Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)) } else { - *self = Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)); + Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)) } } } } + + fn add_delta(self, delta: AmountDelta) -> Self { + match delta { + AmountDelta::Positive(d) => self.add_amount(d), + AmountDelta::Negative(d) => self.subtract_amount(d), + } + } + + fn is_zero(&self) -> bool { + match self { + AmountDelta::Positive(d) => d.micro_ccd == 0, + AmountDelta::Negative(d) => d.micro_ccd == 0, + } + } + + /// Returns a new balance with the `AmountDelta` applied, or, an + /// error if `balance + self < 0`. + /// + /// Panics if an overflow occurs. + fn apply_to_balance(&self, balance: Amount) -> Result { + match self { + AmountDelta::Positive(d) => Ok(balance + .checked_add(*d) + .expect("Overflow occured when adding Amounts.")), // TODO: Should we return an + // error for this? + AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(UnderflowError), + } + } } -pub struct InstanceChangeSet { - stack: Vec, +/// Data held for a contract instance during the execution of a transaction. +#[derive(Clone, Debug)] +pub struct InstanceChanges { + modification_index: u32, + balance_delta: AmountDelta, + /// Should never be modified. + original_balance: Amount, + state: Option, + module: Option, } -impl InstanceChangeSet { - fn new(self_balance: Amount, state: MutableState) -> Self { - let initial_changeset = InstanceChanges { +impl InstanceChanges { + /// Create a new `Self`. The original balance must be provided, all other + /// fields take on default values. + fn new(original_balance: Amount) -> Self { + Self { modification_index: 0, - self_balance, - state, - }; + balance_delta: AmountDelta::new(), + original_balance, + state: None, + module: None, + } + } + + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// Preconditions: + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.balance_delta + .apply_to_balance(self.original_balance) + .expect("Precondition violation: invalid `balance_delta`.") + } +} + +#[derive(Clone, Debug)] +pub struct AccountChanges { + /// Should never be modified. + original_balance: Amount, + balance_delta: AmountDelta, +} + +impl AccountChanges { + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// Preconditions: + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.balance_delta + .apply_to_balance(self.original_balance) + .expect("Precondition violation: invalid `balance_delta`.") + } +} + +#[derive(Clone, Debug)] +struct Changes { + contracts: BTreeMap, + accounts: BTreeMap, +} + +struct ChangeSet { + stack: Vec, +} + +#[derive(PartialEq, Eq, Debug)] +struct EmptyChainSetError; + +impl ChangeSet { + /// Creates a new stack with one empty changeset. + fn new() -> Self { Self { - stack: vec![initial_changeset], + stack: vec![Changes { + contracts: BTreeMap::new(), + accounts: BTreeMap::new(), + }], + } + } + + /// Add a clone of the top-most element to the stack. + fn checkpoint(&mut self) -> Result<(), EmptyChainSetError> { + let cloned_top_element = self.get()?.clone(); + self.stack.push(cloned_top_element); + Ok(()) + } + + /// Pop the last top-most element of the stack. + fn pop(&mut self) -> Result<(), EmptyChainSetError> { + if self.stack.is_empty() { + return Err(EmptyChainSetError); + } + self.stack.pop(); + Ok(()) + } + + /// Get an immutable reference the last checkpoint. + fn get(&self) -> Result<&Changes, EmptyChainSetError> { + self.stack.last().ok_or(EmptyChainSetError) + } + + /// Get a mutable reference to the last checkpoint. + fn get_mut(&mut self) -> Result<&mut Changes, EmptyChainSetError> { + self.stack.last_mut().ok_or(EmptyChainSetError) + } + + /// Clear all changes. This replaces the `Self` with a completely new + /// `Self`. + fn clear(&mut self) { *self = Self::new() } +} + +// Private methods +impl Chain { + /// Check whether an account exists. + fn account_exists(&self, address: AccountAddress) -> bool { + self.accounts.contains_key(&address) + } + + /// Check whether a contract exists. + fn contract_exists(&self, address: ContractAddress) -> bool { + self.contracts.contains_key(&address) + } + + /// Make a transfer from a contract to an account in the changeset. + /// Returns the new balances of both. + /// + /// Precondition: + /// - Assumes that `from` contract exists. + fn transfer_contract_to_account( + &mut self, + amount: Amount, + from: ContractAddress, + to: AccountAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.account_exists(to) { + return Err(ContractTransferError::ToMissing); + } + + // Make the transfer. + let new_balance_from = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance_to = self + .change_account_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + + Ok(NewBalances { + new_balance_from, + new_balance_to, + }) + } + + /// Make a transfer between contracts in the changeset. + /// Returns the new balances of both. + /// + /// Precondition: + /// - Assumes that `from` contract exists. + fn transfer_contract_to_contract( + &mut self, + amount: Amount, + from: ContractAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` contract exists. + if !self.contract_exists(to) { + return Err(ContractTransferError::ToMissing); + } + + // Make the transfer. + let new_balance_from = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance_to = self + .change_contract_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + + Ok(NewBalances { + new_balance_from, + new_balance_to, + }) + } + + /// Make a transfer from an account to a contract in the changeset. + /// Returns the new balance of `from`. + /// + /// Precondition: + /// - Assumes that `from` account exists. + fn transfer_account_to_contract( + &mut self, + amount: Amount, + from: AccountAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.contract_exists(to) { + return Err(AccountTransferError::ToMissing); + } + + // Make the transfer. + let new_balance_from = self.change_account_balance(from, AmountDelta::Negative(amount))?; + let new_balance_to = self + .change_contract_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + + Ok(NewBalances { + new_balance_from, + new_balance_to, + }) + } + + // TODO: Should we handle overflows explicitly? + /// Changes the contract balance in the topmost checkpoint on the changeset. + /// If contract isn't already present in the changeset, it is added. + /// Returns the new balance. + /// + /// Precondition: + /// - Contract must exist. + fn change_contract_balance( + &mut self, + address: ContractAddress, + delta: AmountDelta, + ) -> Result { + match self + .changeset + .get_mut() + .expect("change set should never be empty.") + .contracts + .entry(address) + { + btree_map::Entry::Vacant(vac) => { + // get original balance + let original_balance = self + .contracts + .get(&address) + .expect("Precondition violation: contract assumed to exist") + .self_balance; + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(InstanceChanges { + balance_delta: delta, + ..InstanceChanges::new(original_balance) + }); + return Ok(new_contract_balance); + } + btree_map::Entry::Occupied(mut occ) => { + let contract_changes = occ.get_mut(); + let new_delta = contract_changes.balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = + new_delta.apply_to_balance(contract_changes.original_balance)?; + contract_changes.balance_delta = new_delta; + return Ok(new_contract_balance); + } } } - /// Get a mutable reference to the last instance changeset. - /// Will panic if the stack is empty. - fn get_last(&mut self) -> &mut InstanceChanges { - let last_index = self.stack.len() - 1; - &mut self.stack[last_index] + // TODO: Should we handle overflows explicitly? + /// Changes the account balance in the topmost checkpoint on the changeset. + /// If account isn't already present in the changeset, it is added. + /// Returns the new balance. + /// + /// Precondition: + /// - Account must exist. + fn change_account_balance( + &mut self, + address: AccountAddress, + delta: AmountDelta, + ) -> Result { + match self + .changeset + .get_mut() + .expect("change set should never be empty.") + .accounts + .entry(address) + { + btree_map::Entry::Vacant(vac) => { + // get original balance + let original_balance = self + .accounts + .get(&address) + .expect("Precondition violation: account assumed to exist") + .balance; + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(AccountChanges { + original_balance, + balance_delta: delta, + }); + return Ok(new_account_balance); + } + btree_map::Entry::Occupied(mut occ) => { + let account_changes = occ.get_mut(); + let new_delta = account_changes.balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = + new_delta.apply_to_balance(account_changes.original_balance)?; + account_changes.balance_delta = new_delta; + return Ok(new_account_balance); + } + } } - /// Remove from the stack until the index is reached, i.e. not including the - /// index. Will panic if the index is less than the number of elements - /// in the stack. - fn remove_until_index(&mut self, index: u32) { self.stack.truncate(index as usize + 1) } + /// Returns the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + /// + /// TODO: Find better names for these methods. Perhaps moving them to an + /// inner struct. + /// + /// Preconditions: + /// - Contract must exist. + fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { + self.changeset_contract_balance(address) + .expect("Precondition violation: contract must exist") + } - /// Make a new checkpoint. - /// Will panic if the stack is empty. - fn checkpoint(&mut self) { + /// Looks up the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + fn changeset_contract_balance(&self, address: ContractAddress) -> Option { + match self + .changeset + .get() + .expect("change set should never be empty.") + .contracts + .get(&address) + { + Some(changes) => Some(changes.current_balance()), + None => self.contracts.get(&address).map(|c| c.self_balance), + } + } + + /// Returns the contract module (artifact) from the topmost checkpoint on + /// the changeset. Or, alternatively, from persistence. + /// + /// Preconditions: + /// - Contract instance must exist (and therefore also the artifact). + /// - If the changeset contains a module reference, then it must refer a + /// deployed module. + fn contract_module(&self, address: ContractAddress) -> Arc { + match self + .changeset + .get() + .expect("change set should never be empty.") + .contracts + .get(&address) + .and_then(|c| c.module) + { + // Contract has been upgrade, new module exists. + Some(new_module) => Arc::clone( + self.modules + .get(&new_module) + .expect("Precondition violation: module must exist."), + ), + // Contract hasn't been upgraded. Use persisted module. + None => { + let module_ref = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .module_reference; + Arc::clone( + self.modules + .get(&module_ref) + .expect("Precondition violation: module must exist."), + ) + } + } + } + + /// Get the contract state, either from the changeset or by thawing it from + /// persistence. TODO: Should this return a &mut MutableState or is cloning + /// ok? + /// + /// Preconditions: + /// - Contract instance must exist. + fn contract_state(&self, address: ContractAddress) -> MutableState { + match self + .changeset + .get() + .expect("change set should never be empty") + .contracts + .get(&address) + .and_then(|c| c.state.clone()) + { + // Contract state has been modified. + Some(modified_state) => modified_state.clone(), + // Contract state hasn't been modified. Thaw from persistence. + None => self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist") + .state + .thaw(), + } + } + + /// Looks up the account balance for an account by first checking the + /// changeset, then the persisted values. + fn account_balance_from_changeset(&self, address: AccountAddress) -> Option { + match self + .changeset + .get() + .expect("Change set should never be empty") + .accounts + .get(&address) + .map(|a| a.current_balance()) + { + // Account exists in changeset. + Some(bal) => Some(bal), + // Account doesn't exist in changeset. + None => self.accounts.get(&address).map(|a| a.balance), + } + } + + /// Persist all changes from the changeset, then clear the changeset. + /// Returns the number of bytes additional bytes added to contract states, + /// to be charged for subsequently. + /// + /// Preconditions: + /// - All contracts, modules, accounts referred must exist in persistence. + /// - All amount deltas must be valid (i.e. not cause underflows when added + /// to balance). + fn persist_changes(&mut self) -> u64 { + let changes = self + .changeset + .get_mut() + .expect("change set should never be empty"); + // Persist contract changes and collect the total increase in states sizes. + let mut collector = SizeCollector::default(); let mut loader = v1::trie::Loader::new(&[][..]); - let last = self.get_last(); - let new_checkpoint = InstanceChanges { - modification_index: last.modification_index + 1, - self_balance: last.self_balance, - state: last.state.make_fresh_generation(&mut loader), - }; - self.stack.push(new_checkpoint); + for (addr, changes) in changes.contracts.iter_mut() { + let mut contract = self + .contracts + .get_mut(&addr) + .expect("Precondition violation: contract must exist"); + // Update balance. + if !changes.balance_delta.is_zero() { + contract.self_balance = changes + .balance_delta + .apply_to_balance(changes.original_balance) + .expect("Precondition violation: amount delta causes underflow"); + } + // Update module reference. + if let Some(new_module_ref) = changes.module { + contract.module_reference = new_module_ref; + } + // Update state. + if let Some(modified_state) = &mut changes.state { + contract.state = modified_state.freeze(&mut loader, &mut collector); + } + } + // Persist account changes. + for (addr, changes) in changes.accounts.iter() { + let mut account = self + .accounts + .get_mut(&addr) + .expect("Precondition violation: account must exist"); + // Update balance. + if !changes.balance_delta.is_zero() { + account.balance = changes + .balance_delta + .apply_to_balance(changes.original_balance) + .expect("Precondition violation: amount delta causes underflow"); + } + } + // Clear the changeset. + self.changeset.clear(); + + // Return the state size increase in bytes. + let state_size_increase_in_bytes = collector.collect(); + state_size_increase_in_bytes + } + + /// Saves the a mutable state for a contract in the changeset. + /// If the contract already has an entry in the changeset, the old state + /// will be replaced. Otherwise, the entry is created and the state is + /// added. + /// + /// Preconditions: + /// - Contract must exist. + fn save_state_changes(&mut self, address: ContractAddress, state: MutableState) { + self.changeset + .get_mut() + .expect("change set should never be empty") + .contracts + .entry(address) + .and_modify(|changes| changes.state = Some(state.clone())) + .or_insert({ + let original_balance = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .self_balance; + InstanceChanges { + state: Some(state), + ..InstanceChanges::new(original_balance) + } + }); } - /// Returns whether there are any changes to the instance, which means that - /// there are more than the original changeset element on the stack. - fn has_changes(&mut self) -> bool { self.stack.len() > 1 } + /// Returns the modification index for a contract. + /// It looks it up in the changeset, and if it isn't there, it will return + /// `0`. + fn modification_index(&self, address: ContractAddress) -> u32 { + self.changeset + .get() + .expect("change set should never be empty") + .contracts + .get(&address) + .map_or(0, |c| c.modification_index) + } + + /// Increments the modification index of a contract in the changeset. + /// If the contract isn't present in the changeset, it is added with index + /// 1. + /// + /// Preconditions: + /// - Contract must exist. + fn increment_modification_index(&mut self, address: ContractAddress) { + self.changeset + .get_mut() + .expect("change set should never be empty") + .contracts + .entry(address) + .and_modify(|changes| changes.modification_index += 1) + .or_insert({ + let original_balance = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .self_balance; + InstanceChanges { + modification_index: 1, /* Contract not present in changeset, default index is + * 0, + * now incremented to 1. */ + ..InstanceChanges::new(original_balance) + } + }); + } } -/// Data held for a contract instance during the execution of a transaction. -#[derive(Clone, Debug)] -pub struct InstanceChanges { - modification_index: u32, - // amount_changed: AmountDelta, - self_balance: Amount, - state: MutableState, + +struct NewBalances { + new_balance_from: Amount, + new_balance_to: Amount, +} + +#[derive(Debug)] +struct InsufficientBalanceError; + +#[derive(Debug)] +struct UnderflowError; + +impl From for InsufficientBalanceError { + fn from(_: UnderflowError) -> Self { InsufficientBalanceError } +} + +impl From for ContractTransferError { + fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } } +impl From for AccountTransferError { + fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } +} + +// checkpoint: clone + put on top of stack (what happens in withRollback in +// scheduler) +// +// rollback: pop +// +// save_to_persistence: +// - Save any changes still on the stack. +// +// get_current_state: from newest changeset or persistence +// -> look on top of stack to see if state = some(State) +// -- same behavior for modules +// +// stack only modified right before invoke (push) or right after entrypoint +// execution (maybe pop) +// +// modification_index: inc if state_changed prior to invoke_contract +// -> on resume: +// - save mod index prior to invoke +// - invoke +// - get newest state + mod index +// - if new mod_idx != old_mod_idx => state_modified (will always be >=) +// - This is where state_modified != state_changed (because of +// intermediate calls) +// - if state_changed then commit the current state to the changeset +// (TODO: find out where this should be handled) (in scheduler: +// withInstanceStateV1) +// +// on any res::Success, commit state to changeset +// on res::Other, stack.pop() checkpoint + impl Chain { /// Create a new [`Self`] where all the configurable parameters are /// provided. @@ -176,7 +743,7 @@ impl Chain { modules: BTreeMap::new(), contracts: BTreeMap::new(), next_contract_index: 0, - instance_changesets: BTreeMap::new(), + changeset: ChangeSet::new(), } } @@ -481,33 +1048,27 @@ impl Chain { mut remaining_energy: Energy, chain_events: &mut Vec, ) -> ExecResult> { - // Move the amount from the sender to the contract. - let contract_sender_info = match sender { - Address::Account(addr) => { - self.get_account_mut(addr)?.balance -= amount; - None - } - Address::Contract(addr) => { - let instance_changeset = self.instance_changeset(addr); - let modification_index = instance_changeset.get_last().modification_index; - instance_changeset.checkpoint(); - instance_changeset.get_last().self_balance -= amount; - Some((addr, modification_index)) + // Move the amount from the sender to the contract, if any. + // And get the new self_balance. + let instance_self_balance = if amount.micro_ccd() > 0 { + match sender { + Address::Account(sender_account) => { + self.transfer_account_to_contract(amount, sender_account, address)? + .new_balance_to + } + Address::Contract(sender_contract) => { + self.transfer_contract_to_contract(amount, sender_contract, address)? + .new_balance_to + } } + } else { + self.changeset_contract_balance_unchecked(address) }; - let instance_changeset = self.instance_changeset(address); - let modification_index_before_invoke = instance_changeset.get_last().modification_index; - println!( - "Checkpointing {} from {}", - address, modification_index_before_invoke, - ); - instance_changeset.checkpoint(); - instance_changeset.get_last().self_balance += amount; - // Get the instance and artifact. To be used in several places. let instance = self.get_instance(address)?; - let artifact = self.get_artifact(instance.module_reference)?; + let artifact = self.contract_module(address); + // Subtract the cost of looking up the module remaining_energy = Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); @@ -536,7 +1097,7 @@ impl Chain { }, invoker, self_address: address, - self_balance: instance.self_balance, + self_balance: instance_self_balance, sender, owner: instance.owner, sender_policies: self.get_account(invoker)?.policies.clone(), @@ -547,7 +1108,7 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mutable_state = &mut self.instance_changeset(address).get_last().state; + let mut mutable_state = self.contract_state(address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -579,45 +1140,17 @@ impl Chain { invoker_amount_reserved_for_nrg, entrypoint: entrypoint.to_owned(), chain: self, + state: mutable_state, chain_events: Vec::new(), loader, }; // Process the receive invocation to the end. let res = data.process(res); - // Append the new chain events if the invocation succeeded. - match res { - Ok(v1::ReceiveResult::Success { state_changed, .. }) => { - chain_events.append(&mut data.chain_events); - if !state_changed { - let instance_changeset = self.instance_changeset(address); - println!( - "Removing checkpoints [{}..{}] for contract {}", - modification_index_before_invoke + 1, - instance_changeset.get_last().modification_index, - address, - ); - instance_changeset.remove_until_index(modification_index_before_invoke); - } - } - _ => { - let instance_changeset = self.instance_changeset(address); - println!( - "Removing checkpoints [{}..{}] for contract {}", - modification_index_before_invoke + 1, - instance_changeset.get_last().modification_index, - address, - ); - // Pop a changeset if the invocation failed. - instance_changeset.remove_until_index(modification_index_before_invoke); - - // If the sender was a contract, remove the potential balance changes. - if let Some((addr, modification_index)) = contract_sender_info { - self.instance_changeset(addr) - .remove_until_index(modification_index) - } - } + // Append the new chain events if the invocation succeeded. + if matches!(res, Ok(v1::ReceiveResult::Success { .. })) { + chain_events.append(&mut data.chain_events); } res @@ -632,21 +1165,21 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - // Save backups of accounts and contracts in case of rollbacks. - let accounts_backup = self.accounts.clone(); - println!( "Updating contract {}, with parameter: {:?}", address, parameter.0 ); // Ensure account exists and can pay for the reserved energy and amount + // TODO: Could we just remove this amount in the changeset and then put back the + // to_ccd(remaining_energy) afterwards? let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } + // TODO: Should chain events be part of the changeset? let mut chain_events = Vec::new(); let receive_result = self.contract_update_aux( invoker, @@ -663,32 +1196,17 @@ impl Chain { let (res, transaction_fee) = self.convert_receive_result(receive_result, chain_events, energy_reserved); - // Handle rollback - if res.is_err() { - self.accounts = accounts_backup; + // Persist changes from changeset. + if res.is_ok() { + let _state_size_increase_in_bytes = self.persist_changes(); + // TODO: Charge for size in collector; + } else { + self.changeset.clear(); } - let mut loader = v1::trie::Loader::new(&[][..]); - let mut collector = SizeCollector::default(); - // Apply changes from changesets to the persisted data. - for (addr, changeset) in self.instance_changesets.iter_mut() { - if changeset.has_changes() { - println!( - "Freezing changeset {} of contract {}", - changeset.get_last().modification_index, - addr - ); - let changes = changeset.get_last(); - let contract = self.contracts.get_mut(&addr).expect("Known to exist."); - contract.state = changes.state.freeze(&mut loader, &mut collector); - contract.self_balance = changes.self_balance; - } - } - // Clear the changeset. - self.instance_changesets.clear(); - // TODO: Charge for size in collector; - - // Charge the transaction fee irrespective of the result + // Charge the transaction fee irrespective of the result. + // TODO: If we charge up front, then we should return to_ccd(remaining_energy) + // here instead. self.get_account_mut(invoker)?.balance -= transaction_fee; res } @@ -703,9 +1221,6 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - // Save backups of accounts and contracts so we can reset them after the invoke. - let accounts_backup = self.accounts.clone(); - println!( "Invoking contract {}, with parameter: {:?}", address, parameter.0 @@ -733,10 +1248,8 @@ impl Chain { let (res, _) = self.convert_receive_result(receive_result, chain_events, energy_reserved); - // Roll back any changes. - self.accounts = accounts_backup; // Clear the changeset. - self.instance_changesets.clear(); + self.changeset.clear(); // TODO: Add cost for size in collector to transaction_fee @@ -758,14 +1271,6 @@ impl Chain { ContractAddress::new(index, subindex) } - /// Precondition: `address` exists in state.contracts. - fn instance_changeset(&mut self, address: ContractAddress) -> &mut InstanceChangeSet { - self.instance_changesets.entry(address).or_insert({ - let contract = &self.contracts[&address]; - InstanceChangeSet::new(contract.self_balance, contract.state.thaw()) - }) - } - pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { @@ -777,11 +1282,13 @@ impl Chain { } /// Returns the balance of an account if it exists. + // This will always be the persisted account balance. pub fn account_balance(&self, address: AccountAddress) -> Option { self.accounts.get(&address).and_then(|ai| Some(ai.balance)) } /// Returns the balance of an contract if it exists. + // This will always be the persisted contract balance. pub fn contract_balance(&self, address: ContractAddress) -> Option { self.contracts .get(&address) @@ -835,6 +1342,7 @@ impl Chain { } /// Returns an Arc clone of the artifact. + /// TODO: Look in changeset first. fn get_artifact(&self, module_ref: ModuleReference) -> Result, ModuleMissing> { let artifact = self .modules @@ -979,6 +1487,25 @@ impl Chain { } } +/// Errors related to transfers from contract to an account. +#[derive(PartialEq, Eq, Debug, Error)] +enum ContractTransferError { + #[error("The receiver does not exist.")] + ToMissing, + #[error("The sender does not have sufficient balance.")] + InsufficientBalance, +} + +/// Errors related to "transfers" (fx when updating with an amount) from an +/// account to a contract. +#[derive(PartialEq, Eq, Debug, Error)] +enum AccountTransferError { + #[error("The receiver does not exist.")] + ToMissing, + #[error("The sender does not have sufficient balance.")] + InsufficientBalance, +} + struct ProcessReceiveData<'a, 'b> { invoker: AccountAddress, address: ContractAddress, @@ -992,6 +1519,7 @@ struct ProcessReceiveData<'a, 'b> { invoker_amount_reserved_for_nrg: Amount, entrypoint: OwnedEntrypointName, chain: &'a mut Chain, + state: MutableState, chain_events: Vec, loader: v1::trie::Loader<&'b [u8]>, } @@ -1023,6 +1551,14 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }; // Add update event self.chain_events.push(update_event); + + // Save changes to changeset. + if state_changed { + self.chain + .save_state_changes(self.address, self.state.clone()); + // TODO: Is it ok to clone here? + } + Ok(v1::ReceiveResult::Success { logs, state_changed, @@ -1050,49 +1586,39 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); - println!("Transferring {} CCD to {}", amount, to); - - let response = { - // Check if receiver account exists - if !self.chain.accounts.contains_key(&to) { - InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - } - } else if self - .chain - .instance_changeset(self.address) - .get_last() - .self_balance - < amount - { - InvokeResponse::Failure { - kind: v1::InvokeFailure::InsufficientAmount, - } - } else { - // Move the CCD - self.chain - .accounts - .get_mut(&to) - .expect("Known to exist") - .balance += amount; - let instance_changes = - self.chain.instance_changeset(self.address).get_last(); - instance_changes.self_balance -= amount; - - // Add transfer event - self.chain_events.push(ChainEvent::Transferred { - from: self.address, - amount, - to, - }); + println!("\t\tTransferring {} CCD to {}", amount, to); - InvokeResponse::Success { - new_balance: instance_changes.self_balance, - data: None, - } + let response = match self.chain.transfer_contract_to_account( + amount, + self.address, + to, + ) { + Ok(new_balances) => InvokeResponse::Success { + new_balance: new_balances.new_balance_from, + data: None, + }, + Err(err) => { + let kind = match err { + ContractTransferError::ToMissing => { + v1::InvokeFailure::NonExistentAccount + } + ContractTransferError::InsufficientBalance => { + v1::InvokeFailure::InsufficientAmount + } + }; + InvokeResponse::Failure { kind } } }; + let success = matches!(response, InvokeResponse::Success { .. }); + if success { + // Add transfer event + self.chain_events.push(ChainEvent::Transferred { + from: self.address, + amount, + to, + }); + } // Add resume event self.chain_events.push(ChainEvent::Resumed { address: self.address, @@ -1103,7 +1629,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(remaining_energy), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, false, // never changes on transfers self.loader, ); @@ -1120,18 +1646,23 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); - // TODO: Check that contract has sufficient balance here. - let changeset = self.chain.instance_changeset(self.address); + if state_changed { + self.chain + .save_state_changes(self.address, self.state.clone()); + // TODO: Is it ok to clone here? + } + // Make a checkpoint before calling another contract so that we may roll // back. - let modification_index_before_invoke = - changeset.get_last().modification_index; - println!( - "\t\tCheckpointing addr {}, from index: {}", - self.address, - changeset.get_last().modification_index - ); - changeset.checkpoint(); + self.chain + .changeset + .checkpoint() + .expect("Change set should never be empty"); + + // Save the modification index before the invoke. + let mod_idx_before_invoke = self.chain.modification_index(self.address); + // Increment the modification index. + self.chain.increment_modification_index(self.address); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", @@ -1152,13 +1683,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { &mut self.chain_events, ); - let (success, response, energy_after_invoke, state_changed) = match res - { + let (success, response, energy_after_invoke) = match res { Ok(r) => match r { v1::ReceiveResult::Success { return_value, remaining_energy, - state_changed, .. } => { println!( @@ -1170,13 +1699,10 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { InvokeResponse::Success { new_balance: self .chain - .instance_changeset(address) - .get_last() - .self_balance, + .changeset_contract_balance_unchecked(address), data: Some(return_value), }, remaining_energy, - state_changed, ) } v1::ReceiveResult::Interrupt { .. } => { @@ -1195,7 +1721,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, }, remaining_energy, - false, ), v1::ReceiveResult::Trap { remaining_energy, .. @@ -1205,7 +1730,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { kind: v1::InvokeFailure::RuntimeError, }, remaining_energy, - false, ), v1::ReceiveResult::OutOfEnergy => ( false, @@ -1213,7 +1737,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { kind: v1::InvokeFailure::RuntimeError, }, 0, - false, ), // TODO: What is the correct error here? }, Err(_e) => ( @@ -1222,7 +1745,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { kind: v1::InvokeFailure::RuntimeError, }, 0, - false, ), // TODO: Correct energy here? }; @@ -1233,14 +1755,26 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }; // Remove the last state changes if the invocation failed. - if !success || !state_changed { - println!( - "\t\tRemove checkpoint due to !success for contract: {}", - self.address - ); + let state_changed = if !success { self.chain - .instance_changeset(self.address) - .remove_until_index(modification_index_before_invoke); + .changeset + .pop() + .expect("Change set should never be empty"); + false // We rolled back, so no changes were made + // to this contract. + } else { + let mod_idx_after_invoke = + self.chain.modification_index(self.address); + let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + self.state = self.chain.contract_state(self.address); + } + // TODO: Notes say that we should commit the state changes to the + // changeset at this point. But the state changes would already + // exist in the changeset at this point. + state_changed }; println!( @@ -1258,7 +1792,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, state_changed, self.loader, ); @@ -1335,7 +1869,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, state_changed, self.loader, ); @@ -1343,12 +1877,14 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.process(resume_res) } v1::Interrupt::QueryAccountBalance { address } => { - println!("Querying account balance of {}", address); + println!("\t\tQuerying account balance of {}", address); // When querying an account, the amounts from any `invoke_transfer`s // should be included. That is handled by // the `chain` struct already. transaction. // However, that is hand - let response = match self.chain.account_balance(address) { + let response = match self.chain.account_balance_from_changeset(address) + { + // TODO: next Some(acc_bal) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for @@ -1360,6 +1896,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } else { acc_bal }; + println!("\t\t\tBalance found to be {}", acc_bal); // TODO: Do we need non-zero staked and shielded balances? let balances = @@ -1367,9 +1904,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { InvokeResponse::Success { new_balance: self .chain - .instance_changeset(self.address) - .get_last() - .self_balance, + .changeset_contract_balance_unchecked(self.address), data: Some(balances), } } @@ -1388,7 +1923,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, false, // State never changes on queries. self.loader, ); @@ -1398,28 +1933,18 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { v1::Interrupt::QueryContractBalance { address } => { println!("Querying contract balance of {}", address); - let response = if self.chain.contracts.contains_key(&address) { - InvokeResponse::Success { - // Balance of contract querying. Won't change. + let response = match self.chain.changeset_contract_balance(address) { + None => InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + Some(bal) => InvokeResponse::Success { + // Balance of contract querying. Won't change. Notice the + // `self.address`. new_balance: self .chain - .instance_changeset(self.address) - .get_last() - .self_balance, - // Balance of the queried contract. Notice the `address` vs - // `self.address`. - data: Some(to_bytes( - &self - .chain - .instance_changeset(address) - .get_last() - .self_balance, - )), - } - } else { - InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - } + .changeset_contract_balance_unchecked(self.address), + data: Some(to_bytes(&bal)), + }, }; let energy_after_invoke = remaining_energy @@ -1432,7 +1957,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, false, // State never changes on queries. self.loader, ); @@ -1448,9 +1973,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let response = InvokeResponse::Success { new_balance: self .chain - .instance_changeset(self.address) - .get_last() - .self_balance, + .changeset_contract_balance_unchecked(self.address), data: Some(to_bytes(&exchange_rates)), }; @@ -1464,7 +1987,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { config, response, InterpreterEnergy::from(energy_after_invoke), - &mut self.chain.instance_changeset(self.address).get_last().state, + &mut self.state, false, // State never changes on queries. self.loader, ); @@ -2184,10 +2707,60 @@ mod tests { // Assert that the updated state is persisted. assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } + + // #[test] + // fn rollback_of_account_balances_after_failed_contract_invoke() { + // let mut chain = Chain::new(); + // let initial_balance = Amount::from_ccd(10000); + // let transfer_amount = Amount::from_ccd(2); + // chain.create_account(ACC_0, + // AccountInfo::new(initial_balance)); chain. + // create_account(ACC_1, AccountInfo::new(initial_balance)); + + // let res_deploy = chain + // .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + // .expect("Deploying valid module should work"); + + // let res_init_0 = chain + // .contract_init( + // ACC_0, + // res_deploy.module_reference, + // ContractName::new_unchecked("init_integrate"), + // ContractParameter::empty(), + // Amount::zero(), + // Energy::from(10000), + // ) + // .expect("Initializing valid contract should work"); + + // let res_init_1 = chain + // .contract_init( + // ACC_0, + // res_deploy.module_reference, + // ContractName::new_unchecked("init_integrate_other"), + // ContractParameter::empty(), + // Amount::zero(), + // Energy::from(10000), + // ) + // .expect("Initializing valid contract should work"); + + // let param = (res_init_1.contract_address, initial_balance, + // ACC_1); + + // let res_update = chain + // .contract_update( + // ACC_0, + // res_init_0.contract_address, + // EntrypointName::new_unchecked("mutate_and_forward"), + // ContractParameter::from_typed(¶m), + // transfer_amount, + // Energy::from(100000), + // ) + // .expect("Update should succeed"); + // } } // TODO: Add tests that check: // - Correct account balances after init / update failures (when Amount > 0) - // + #[test] fn update_with_fib_reentry_works() { let mut chain = Chain::new(); @@ -3659,18 +4232,18 @@ mod tests { let three = Amount::from_ccd(3); let five = Amount::from_ccd(5); - x.subtract(one); // -1 CCD - x.subtract(one); // -2 CCD + x = x.subtract_amount(one); // -1 CCD + x = x.subtract_amount(one); // -2 CCD assert_eq!(x, AmountDelta::Negative(two)); - x.add(five); // +3 CCD + x = x.add_amount(five); // +3 CCD assert_eq!(x, AmountDelta::Positive(three)); - x.subtract(five); // -2 CCD + x = x.subtract_amount(five); // -2 CCD assert_eq!(x, AmountDelta::Negative(two)); - x.add(two); // 0 + x = x.add_amount(two); // 0 - x.add(Amount::from_micro_ccd(1)); // 1 mCCD + x = x.add_amount(Amount::from_micro_ccd(1)); // 1 mCCD assert_eq!(x, AmountDelta::Positive(Amount::from_micro_ccd(1))); - x.subtract(Amount::from_micro_ccd(2)); // -1 mCCD + x = x.subtract_amount(Amount::from_micro_ccd(2)); // -1 mCCD assert_eq!(x, AmountDelta::Negative(Amount::from_micro_ccd(1))); } } From 056e95cca72891df02c7d22effd5587e7a026680 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 9 Feb 2023 15:12:55 +0100 Subject: [PATCH 049/208] Checkpoint state instead of clone and inc mod idx on every save --- contract-testing/src/lib.rs | 166 +++++++++++++++--------------------- 1 file changed, 71 insertions(+), 95 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index c83b125c..d1a0caa9 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -611,15 +611,23 @@ impl Chain { /// will be replaced. Otherwise, the entry is created and the state is /// added. /// + /// This also increments the modification index. It will be set to 1 if the + /// contract has no entry in the changeset. + /// /// Preconditions: /// - Contract must exist. - fn save_state_changes(&mut self, address: ContractAddress, state: MutableState) { + fn save_state_changes(&mut self, address: ContractAddress, state: &mut MutableState) { + println!("Saving state for {}", address); + let mut loader = v1::trie::Loader::new(&[][..]); self.changeset .get_mut() .expect("change set should never be empty") .contracts .entry(address) - .and_modify(|changes| changes.state = Some(state.clone())) + .and_modify(|changes| { + changes.state = Some(state.make_fresh_generation(&mut loader)); + changes.modification_index += 1; + }) .or_insert({ let original_balance = self .contracts @@ -627,7 +635,8 @@ impl Chain { .expect("Precondition violation: contract must exist.") .self_balance; InstanceChanges { - state: Some(state), + state: Some(state.make_fresh_generation(&mut loader)), + modification_index: 1, // Increment from default, 0, to 1. ..InstanceChanges::new(original_balance) } }); @@ -644,34 +653,6 @@ impl Chain { .get(&address) .map_or(0, |c| c.modification_index) } - - /// Increments the modification index of a contract in the changeset. - /// If the contract isn't present in the changeset, it is added with index - /// 1. - /// - /// Preconditions: - /// - Contract must exist. - fn increment_modification_index(&mut self, address: ContractAddress) { - self.changeset - .get_mut() - .expect("change set should never be empty") - .contracts - .entry(address) - .and_modify(|changes| changes.modification_index += 1) - .or_insert({ - let original_balance = self - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .self_balance; - InstanceChanges { - modification_index: 1, /* Contract not present in changeset, default index is - * 0, - * now incremented to 1. */ - ..InstanceChanges::new(original_balance) - } - }); - } } struct NewBalances { @@ -1554,9 +1535,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Save changes to changeset. if state_changed { - self.chain - .save_state_changes(self.address, self.state.clone()); - // TODO: Is it ok to clone here? + self.chain.save_state_changes(self.address, &mut self.state); } Ok(v1::ReceiveResult::Success { @@ -1646,10 +1625,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); + // Save the modification index before the invoke. + let mod_idx_before_invoke = self.chain.modification_index(self.address); + if state_changed { - self.chain - .save_state_changes(self.address, self.state.clone()); - // TODO: Is it ok to clone here? + self.chain.save_state_changes(self.address, &mut self.state); } // Make a checkpoint before calling another contract so that we may roll @@ -1659,10 +1639,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { .checkpoint() .expect("Change set should never be empty"); - // Save the modification index before the invoke. - let mod_idx_before_invoke = self.chain.modification_index(self.address); - // Increment the modification index. - self.chain.increment_modification_index(self.address); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", @@ -1748,11 +1724,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { ), // TODO: Correct energy here? }; - // Add resume event - let resume_event = ChainEvent::Resumed { - address: self.address, - success, - }; // Remove the last state changes if the invocation failed. let state_changed = if !success { @@ -1786,6 +1757,13 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { "failed invocation" } ); + + // Add resume event + let resume_event = ChainEvent::Resumed { + address: self.address, + success, + }; + self.chain_events.push(resume_event); let resume_res = v1::resume_receive( @@ -2708,55 +2686,53 @@ mod tests { assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); } - // #[test] - // fn rollback_of_account_balances_after_failed_contract_invoke() { - // let mut chain = Chain::new(); - // let initial_balance = Amount::from_ccd(10000); - // let transfer_amount = Amount::from_ccd(2); - // chain.create_account(ACC_0, - // AccountInfo::new(initial_balance)); chain. - // create_account(ACC_1, AccountInfo::new(initial_balance)); - - // let res_deploy = chain - // .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - // .expect("Deploying valid module should work"); - - // let res_init_0 = chain - // .contract_init( - // ACC_0, - // res_deploy.module_reference, - // ContractName::new_unchecked("init_integrate"), - // ContractParameter::empty(), - // Amount::zero(), - // Energy::from(10000), - // ) - // .expect("Initializing valid contract should work"); - - // let res_init_1 = chain - // .contract_init( - // ACC_0, - // res_deploy.module_reference, - // ContractName::new_unchecked("init_integrate_other"), - // ContractParameter::empty(), - // Amount::zero(), - // Energy::from(10000), - // ) - // .expect("Initializing valid contract should work"); - - // let param = (res_init_1.contract_address, initial_balance, - // ACC_1); - - // let res_update = chain - // .contract_update( - // ACC_0, - // res_init_0.contract_address, - // EntrypointName::new_unchecked("mutate_and_forward"), - // ContractParameter::from_typed(¶m), - // transfer_amount, - // Energy::from(100000), - // ) - // .expect("Update should succeed"); - // } + #[test] + fn rollback_of_account_balances_after_failed_contract_invoke() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(2); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init_0 = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_1 = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate_other"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let param = (res_init_1.contract_address, initial_balance, ACC_1); + + let res_update = chain + .contract_update( + ACC_0, + res_init_0.contract_address, + EntrypointName::new_unchecked("mutate_and_forward"), + ContractParameter::from_typed(¶m), + transfer_amount, + Energy::from(100000), + ) + .expect("Update should succeed"); + } } // TODO: Add tests that check: // - Correct account balances after init / update failures (when Amount > 0) From cf15589a5b60321d644c588f7074ee257dcf27b3 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 9 Feb 2023 15:44:09 +0100 Subject: [PATCH 050/208] Add extra debugging prints --- contract-testing/src/lib.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d1a0caa9..df3c8594 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -28,6 +28,8 @@ use wasm_transform::artifact; /// A V1 artifact, with concrete types for the generic parameters. type ArtifactV1 = artifact::Artifact; +const VERBOSE_DEBUG: bool = true; + // Energy constants from Cost.hs in concordium-base. /// Cost of querying the account balance from a within smart contract instance. @@ -197,6 +199,7 @@ struct Changes { accounts: BTreeMap, } +#[derive(Debug)] struct ChangeSet { stack: Vec, } @@ -1639,6 +1642,12 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { .checkpoint() .expect("Change set should never be empty"); + if VERBOSE_DEBUG { + println!( + "Before call (after checkpoint): {:#?}", + self.chain.changeset.get().unwrap() + ); + } println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", @@ -1724,9 +1733,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { ), // TODO: Correct energy here? }; - // Remove the last state changes if the invocation failed. let state_changed = if !success { + println!("Rolling back"); self.chain .changeset .pop() @@ -1738,6 +1747,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain.modification_index(self.address); let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; if state_changed { + println!("State change detected via mod_idx"); // Update the state field with the newest value from the // changeset. self.state = self.chain.contract_state(self.address); @@ -1748,6 +1758,13 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { state_changed }; + if VERBOSE_DEBUG { + println!( + "After call (and potential rollback):\n{:#?}", + self.chain.changeset.get().unwrap() + ); + } + println!( "\tResuming contract {}\n\t\tafter {}", self.address, @@ -3988,7 +4005,7 @@ mod tests { /// This test has the following call pattern: /// A /// --> B - /// --> A (no modification, but bump iterator) + /// --> A (no modification, just lookup entry) /// <-- /// B /// A <-- From f79b2463da972a75f1db93690831e653c2ab6e2f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Feb 2023 15:57:00 +0100 Subject: [PATCH 051/208] Fix test case 2 --- contract-testing/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index df3c8594..3a6b2038 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1628,13 +1628,13 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event self.chain_events.push(interrupt_event); - // Save the modification index before the invoke. - let mod_idx_before_invoke = self.chain.modification_index(self.address); - if state_changed { self.chain.save_state_changes(self.address, &mut self.state); } + // Save the modification index before the invoke. + let mod_idx_before_invoke = self.chain.modification_index(self.address); + // Make a checkpoint before calling another contract so that we may roll // back. self.chain From dd99568ed9cd91f0a03e41f1b31072d9435e3b6f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 13 Feb 2023 14:23:30 +0100 Subject: [PATCH 052/208] Handle OutOfEnergy due to state size increase --- contract-testing/src/lib.rs | 266 ++++++++++++++++++++++++++++-------- 1 file changed, 212 insertions(+), 54 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 3a6b2038..9c132ca5 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -10,6 +10,7 @@ use num_bigint::BigUint; use sha2::{Digest, Sha256}; use std::{ collections::{btree_map, BTreeMap}, + fmt, path::Path, sync::Arc, }; @@ -18,7 +19,7 @@ use wasm_chain_integration::{ v0, v1::{ self, - trie::{MutableState, SizeCollector}, + trie::{MutableState, PersistentState, SizeCollector}, ConcordiumAllowedImports, InvokeResponse, ReturnValue, }, ExecResult, InterpreterEnergy, @@ -28,7 +29,7 @@ use wasm_transform::artifact; /// A V1 artifact, with concrete types for the generic parameters. type ArtifactV1 = artifact::Artifact; -const VERBOSE_DEBUG: bool = true; +const VERBOSE_DEBUG: bool = false; // Energy constants from Cost.hs in concordium-base. @@ -550,15 +551,26 @@ impl Chain { } } - /// Persist all changes from the changeset, then clear the changeset. - /// Returns the number of bytes additional bytes added to contract states, - /// to be charged for subsequently. + /// Try to persist all changes from the changeset. /// - /// Preconditions: + /// Always clears the changeset. + /// + /// If the energy needed for storing extra state is larger than the + /// `remaining_energy`, then: + /// - no changes will be persisted, + /// - an [`OutOfEnergy`] error is returned. + /// + /// Otherwise, it returns the [`Energy`] to be charged for the additional + /// bytes added to contract states. + /// + /// *Preconditions:* /// - All contracts, modules, accounts referred must exist in persistence. /// - All amount deltas must be valid (i.e. not cause underflows when added /// to balance). - fn persist_changes(&mut self) -> u64 { + fn persist_changes_and_clear( + &mut self, + remaining_energy: Energy, + ) -> Result { let changes = self .changeset .get_mut() @@ -566,6 +578,34 @@ impl Chain { // Persist contract changes and collect the total increase in states sizes. let mut collector = SizeCollector::default(); let mut loader = v1::trie::Loader::new(&[][..]); + + let mut frozen_states: BTreeMap = BTreeMap::new(); + + // Create frozen versions of all the states, to compute the energy needed. + for (addr, changes) in changes.contracts.iter_mut() { + if let Some(modified_state) = &mut changes.state { + frozen_states.insert(*addr, modified_state.freeze(&mut loader, &mut collector)); + } + } + + // One energy per extra byte of state. + let energy_for_state_increase = Energy::from(collector.collect()); + + println!( + "persist_changes: remaining: {}, state: {}", + remaining_energy, energy_for_state_increase + ); + + // Return an error if out of energy, and clear the changeset. + if remaining_energy + .checked_sub(energy_for_state_increase) + .is_none() + { + self.changeset.clear(); + return Err(OutOfEnergy); + } + + // Then persist all the changes. for (addr, changes) in changes.contracts.iter_mut() { let mut contract = self .contracts @@ -583,8 +623,11 @@ impl Chain { contract.module_reference = new_module_ref; } // Update state. - if let Some(modified_state) = &mut changes.state { - contract.state = modified_state.freeze(&mut loader, &mut collector); + if changes.state.is_some() { + // Replace with the frozen state we created earlier. + contract.state = frozen_states + .remove(addr) + .expect("Known to exist since we just added it."); } } // Persist account changes. @@ -604,9 +647,47 @@ impl Chain { // Clear the changeset. self.changeset.clear(); - // Return the state size increase in bytes. - let state_size_increase_in_bytes = collector.collect(); - state_size_increase_in_bytes + Ok(energy_for_state_increase) + } + + /// Traverses the last checkpoint in the changeset and collects the energy + /// needed to be charged for additional state bytes. + /// + /// Always clears the changeset. + /// + /// Returns an [`OutOfEnergy`] error if the energy needed for storing the + /// extra state is larger than `remaining_energy`. Otherwise, it returns + /// the [`Energy`] needed for storing the extra state. + fn collect_energy_for_extra_state_and_clear( + &mut self, + remaining_energy: Energy, + ) -> Result { + let mut loader = v1::trie::Loader::new(&[][..]); + let mut collector = SizeCollector::default(); + for (_, changes) in self + .changeset + .get_mut() + .expect("change set should never be empty") + .contracts + .iter_mut() + { + if let Some(modified_state) = &mut changes.state { + modified_state.freeze(&mut loader, &mut collector); + } + } + // Clear the changeset. + self.changeset.clear(); + + // One energy per extra byte in the state. + let energy_for_state_increase = Energy::from(collector.collect()); + + if remaining_energy + .checked_sub(energy_for_state_increase) + .is_none() + { + return Err(OutOfEnergy); + } + Ok(energy_for_state_increase) } /// Saves the a mutable state for a contract in the changeset. @@ -1068,7 +1149,7 @@ impl Chain { } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { OwnedReceiveName::new_unchecked(fallback_receive_name) } else { - bail!("Entrypoint does not exist."); + bail!(EntrypointDoesNotExist(entrypoint)); } }; @@ -1177,16 +1258,40 @@ impl Chain { &mut chain_events, ); - let (res, transaction_fee) = - self.convert_receive_result(receive_result, chain_events, energy_reserved); + // Get the energy to be charged for extra state bytes. Or return an error if out + // of energy. + let energy_for_state_increase = match receive_result { + Ok(v1::ReceiveResult::Success { + remaining_energy, .. + }) => { + println!("Remaining energy u64: {}", remaining_energy); + match self.persist_changes_and_clear(Chain::from_interpreter_energy( + InterpreterEnergy::from(remaining_energy), + )) { + Ok(energy) => energy, + Err(_) => { + return Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }, + )) + } + } + } + _ => { + // An error occured, so we don't save the changes. Just clear. + self.changeset.clear(); + Energy::from(0) + } + }; - // Persist changes from changeset. - if res.is_ok() { - let _state_size_increase_in_bytes = self.persist_changes(); - // TODO: Charge for size in collector; - } else { - self.changeset.clear(); - } + let (res, transaction_fee) = self.convert_receive_result( + receive_result, + chain_events, + energy_reserved, + energy_for_state_increase, + ); // Charge the transaction fee irrespective of the result. // TODO: If we charge up front, then we should return to_ccd(remaining_energy) @@ -1195,7 +1300,6 @@ impl Chain { res } - /// TODO: Should we make invoker and energy optional? pub fn contract_invoke( &mut self, invoker: AccountAddress, @@ -1230,12 +1334,35 @@ impl Chain { &mut chain_events, ); - let (res, _) = self.convert_receive_result(receive_result, chain_events, energy_reserved); - - // Clear the changeset. - self.changeset.clear(); + let energy_for_state_increase = match receive_result { + Ok(v1::ReceiveResult::Success { + remaining_energy, .. + }) => match self.collect_energy_for_extra_state_and_clear( + Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), + ) { + Ok(energy) => energy, + Err(_) => { + return Err(ContractUpdateError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }, + )) + } + }, + _ => { + // An error occured, so we just clear the changeset. + self.changeset.clear(); + Energy::from(0) + } + }; - // TODO: Add cost for size in collector to transaction_fee + let (res, _) = self.convert_receive_result( + receive_result, + chain_events, + energy_reserved, + energy_for_state_increase, + ); res } @@ -1368,11 +1495,20 @@ impl Chain { /// Convert the wasm_chain_integration result to the one used here and /// calculate the transaction fee. + /// + /// The `energy_for_state_increase` is only used if the result was a + /// success. + /// + /// *Precondition*: + /// - The `receive_result` should never be + /// `Ok(v1::ReceiveResult::Interrupt(_))`. + /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` fn convert_receive_result( &self, receive_result: ExecResult>, chain_events: Vec, energy_reserved: Energy, + energy_for_state_increase: Energy, ) -> ( Result, Amount, @@ -1386,10 +1522,14 @@ impl Chain { remaining_energy, /* TODO: Could we change this from `u64` to * `InterpreterEnergy` in chain_integration? */ } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); + let remaining_energy = + Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + println!( + "reserved: {}, remaining: {}, state: {}", + energy_reserved, remaining_energy, energy_for_state_increase + ); + let energy_used = + energy_reserved - remaining_energy + energy_for_state_increase; let transaction_fee = self.calculate_energy_cost(energy_used); ( Ok(SuccessfulContractUpdate { @@ -1403,16 +1543,17 @@ impl Chain { transaction_fee, ) } - v1::ReceiveResult::Interrupt { .. } => panic!("Should never happen"), + v1::ReceiveResult::Interrupt { .. } => { + panic!("Precondition violation: Got `ReceiveResult::Interrupt`.") + } v1::ReceiveResult::Reject { reason, return_value, remaining_energy, } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); + let remaining_energy = + Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractUpdateError::ValidChainError( @@ -1430,10 +1571,10 @@ impl Chain { error, remaining_energy, } => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); + let remaining_energy = Chain::from_interpreter_energy(InterpreterEnergy { + energy: remaining_energy, + }); + let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractUpdateError::ValidChainError( @@ -1459,14 +1600,18 @@ impl Chain { ) } }, - Err(e) => { - // TODO: what is the correct cost here? - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractUpdateError::InterpreterError(e)), - transaction_fee, - ) - } + Err(e) => match e.downcast::() { + // The user tried to call `contract_update` or `contract_invoke` with an entrypoint + // which does not exist. This is caught up front and nothing is put + // on chain, so the cost is zero. + Ok(err) => ( + Err(ContractUpdateError::EntrypointDoesNotExist(err)), + Amount::zero(), + ), + // An internal precondition has been violated, which caused an error in the + // interpreter. + Err(e) => panic!("Internal error: {}", e), + }, } } } @@ -1490,6 +1635,19 @@ enum AccountTransferError { InsufficientBalance, } +#[derive(PartialEq, Eq, Debug)] +struct OutOfEnergy; + +/// The entrypoint does not exist. +#[derive(PartialEq, Eq, Debug, Error)] +pub struct EntrypointDoesNotExist(OwnedEntrypointName); + +impl fmt::Display for EntrypointDoesNotExist { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "The entrypoint '{}' does not exist.", self.0) + } +} + struct ProcessReceiveData<'a, 'b> { invoker: AccountAddress, address: ContractAddress, @@ -2107,7 +2265,7 @@ pub enum ContractUpdateError { InstanceDoesNotExist(#[from] ContractInstanceMissing), /// Entrypoint does not exist and neither does the fallback entrypoint. #[error("entrypoint does not exist")] - EntrypointDoesNotExist, + EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), /// Account has not been created in test environment. #[error("account {} does not exist", 0.0)] AccountDoesNotExist(#[from] AccountMissing), @@ -2739,7 +2897,7 @@ mod tests { let param = (res_init_1.contract_address, initial_balance, ACC_1); - let res_update = chain + chain .contract_update( ACC_0, res_init_0.contract_address, @@ -3812,7 +3970,7 @@ mod tests { // failed. assert!(matches!( res_update_new_feature, - Err(ContractUpdateError::InterpreterError(e)) if e.to_string() == "Entrypoint does not exist." + Err(ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint))) if entrypoint.to_string() == "new_feature" )); } @@ -3909,7 +4067,7 @@ mod tests { ChainEvent::Updated { .. } ])); assert!( - matches!(res_update_new_feature_0, ContractUpdateError::InterpreterError(e) if e.to_string() == "Entrypoint does not exist.") + matches!(res_update_new_feature_0, ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint)) if entrypoint.to_string() == "new_feature") ); assert!(matches!(res_update_upgrade.chain_events[..], [ ChainEvent::Interrupted { .. }, @@ -3918,7 +4076,7 @@ mod tests { ChainEvent::Updated { .. }, ])); assert!( - matches!(res_update_old_feature_1, ContractUpdateError::InterpreterError(e) if e.to_string() == "Entrypoint does not exist.") + matches!(res_update_old_feature_1, ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint)) if entrypoint.to_string() == "old_feature") ); assert!(matches!(res_update_new_feature_1.chain_events[..], [ ChainEvent::Updated { .. } From 533c1a411577b7e7962f1b621dac1e6a6b69ef20 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 14 Feb 2023 11:43:34 +0100 Subject: [PATCH 053/208] Document, clarify names, cleanup --- contract-testing/src/lib.rs | 619 ++++++++++++++++++++---------------- 1 file changed, 339 insertions(+), 280 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 9c132ca5..d1256060 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -10,7 +10,6 @@ use num_bigint::BigUint; use sha2::{Digest, Sha256}; use std::{ collections::{btree_map, BTreeMap}, - fmt, path::Path, sync::Arc, }; @@ -27,8 +26,10 @@ use wasm_chain_integration::{ use wasm_transform::artifact; /// A V1 artifact, with concrete types for the generic parameters. -type ArtifactV1 = artifact::Artifact; +type ContractModule = artifact::Artifact; +/// Whether the current [`Changes`] should be printed before and after an +/// internal contract invoke. TODO: Remove before publishing. const VERBOSE_DEBUG: bool = false; // Energy constants from Cost.hs in concordium-base. @@ -46,6 +47,9 @@ const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; /// Cost of creating an empty smart contract instance. const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: Energy = Energy { energy: 200 }; +/// Represents the block chain and supports a number of operations, including +/// creating accounts, deploying modules, initializing contract, updating +/// contracts and invoking contracts. pub struct Chain { /// The slot time viewable inside the smart contracts. /// Defaults to `0`. @@ -57,32 +61,44 @@ pub struct Chain { /// Accounts and info about them. pub accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap>, + pub modules: BTreeMap>, /// Smart contract instances. - pub contracts: BTreeMap, + pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. pub next_contract_index: u64, + /// The changeset used during a contract update or invocation. changeset: ChangeSet, } +/// A smart contract instance along. #[derive(Clone)] -pub struct ContractInstance { +pub struct Contract { + /// The module which contains this contract. pub module_reference: ModuleReference, + /// The name of the contract. pub contract_name: OwnedContractName, + /// The contract state. pub state: v1::trie::PersistentState, + /// The owner of the contract. pub owner: AccountAddress, + /// The balance of the contract. pub self_balance: Amount, } +/// A positive or negative delta in for an [`Amount`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AmountDelta { + /// A posittive delta. Positive(Amount), + /// A negative delta. Negative(Amount), } impl AmountDelta { + /// Create a new [`Self`], with the value `+0`. fn new() -> Self { Self::Positive(Amount::zero()) } + /// Subtract an [`Amount`] from [`Self`]. fn subtract_amount(self, amount: Amount) -> Self { match self { Self::Positive(current) => { @@ -96,6 +112,7 @@ impl AmountDelta { } } + /// Add an [`Amount`] from [`Self`]. fn add_amount(self, amount: Amount) -> Self { match self { Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), @@ -109,6 +126,7 @@ impl AmountDelta { } } + /// Add two [`Self`] to create a new one. fn add_delta(self, delta: AmountDelta) -> Self { match delta { AmountDelta::Positive(d) => self.add_amount(d), @@ -116,6 +134,7 @@ impl AmountDelta { } } + /// Whether the [`Self`] is zero (either `+0` or `-0`). fn is_zero(&self) -> bool { match self { AmountDelta::Positive(d) => d.micro_ccd == 0, @@ -138,27 +157,34 @@ impl AmountDelta { } } -/// Data held for a contract instance during the execution of a transaction. +/// Data held for a contract during the execution of a contract update or +/// invocation. #[derive(Clone, Debug)] -pub struct InstanceChanges { - modification_index: u32, - balance_delta: AmountDelta, - /// Should never be modified. - original_balance: Amount, - state: Option, - module: Option, +pub struct ContractChanges { + /// An index that is used to check whether a caller contract has been + /// modified after invoking another contract (due to reentrancy). + modification_index: u32, + /// Represents how much the contract's self balance has changed. + self_balance_delta: AmountDelta, + /// The original contract balance, i.e. the one that is persisted. Should + /// never be modified. + self_balance_original: Amount, + /// The potentially modified contract state. + state: Option, + /// The potentially changed module. + module: Option, } -impl InstanceChanges { +impl ContractChanges { /// Create a new `Self`. The original balance must be provided, all other /// fields take on default values. fn new(original_balance: Amount) -> Self { Self { - modification_index: 0, - balance_delta: AmountDelta::new(), - original_balance, - state: None, - module: None, + modification_index: 0, + self_balance_delta: AmountDelta::new(), + self_balance_original: original_balance, + state: None, + module: None, } } @@ -168,12 +194,14 @@ impl InstanceChanges { /// Preconditions: /// - `balance_delta + original_balance` must be larger than `0`. fn current_balance(&self) -> Amount { - self.balance_delta - .apply_to_balance(self.original_balance) + self.self_balance_delta + .apply_to_balance(self.self_balance_original) .expect("Precondition violation: invalid `balance_delta`.") } } +/// Data held for an account during the execution of an update or invoke +/// transaction. #[derive(Clone, Debug)] pub struct AccountChanges { /// Should never be modified. @@ -194,22 +222,30 @@ impl AccountChanges { } } +/// Data held for accounts and contracts during the execution of a contract +/// update or invocation. #[derive(Clone, Debug)] struct Changes { - contracts: BTreeMap, + /// The contracts which have changes. + contracts: BTreeMap, + /// The accounts which have changes. accounts: BTreeMap, } +/// The set of [`Changes`] represented as a stack. #[derive(Debug)] struct ChangeSet { + /// The stack of changes. stack: Vec, } -#[derive(PartialEq, Eq, Debug)] -struct EmptyChainSetError; +/// The message to use when an internal error occurs in the changeset. +const INTERNAL_CHANGESET_ERROR_MESSAGE: &str = + "Internal error: change set stack should never be empty."; impl ChangeSet { - /// Creates a new stack with one empty changeset. + /// Creates a new changeset with one empty [`Changes`] element on the + /// stack.. fn new() -> Self { Self { stack: vec![Changes { @@ -219,37 +255,35 @@ impl ChangeSet { } } - /// Add a clone of the top-most element to the stack. - fn checkpoint(&mut self) -> Result<(), EmptyChainSetError> { - let cloned_top_element = self.get()?.clone(); + /// Make a checkpoint by putting a clone of the top element onto the stack. + fn checkpoint(&mut self) { + let cloned_top_element = self.current().clone(); self.stack.push(cloned_top_element); - Ok(()) } - /// Pop the last top-most element of the stack. - fn pop(&mut self) -> Result<(), EmptyChainSetError> { - if self.stack.is_empty() { - return Err(EmptyChainSetError); - } - self.stack.pop(); - Ok(()) - } + /// Perform a rollback by popping the top element of the stack. + fn rollback(&mut self) { self.stack.pop().expect(INTERNAL_CHANGESET_ERROR_MESSAGE); } - /// Get an immutable reference the last checkpoint. - fn get(&self) -> Result<&Changes, EmptyChainSetError> { - self.stack.last().ok_or(EmptyChainSetError) - } + /// Get an immutable reference the current (latest) checkpoint. + fn current(&self) -> &Changes { self.stack.last().expect(INTERNAL_CHANGESET_ERROR_MESSAGE) } - /// Get a mutable reference to the last checkpoint. - fn get_mut(&mut self) -> Result<&mut Changes, EmptyChainSetError> { - self.stack.last_mut().ok_or(EmptyChainSetError) + /// Get a mutable reference to the current (latest) checkpoint. + fn current_mut(&mut self) -> &mut Changes { + self.stack + .last_mut() + .expect(INTERNAL_CHANGESET_ERROR_MESSAGE) } - /// Clear all changes. This replaces the `Self` with a completely new - /// `Self`. + /// Clear all changes. + /// + /// This replaces the `Self` with a completely new `Self`. fn clear(&mut self) { *self = Self::new() } } +impl Default for Chain { + fn default() -> Self { Self::new() } +} + // Private methods impl Chain { /// Check whether an account exists. @@ -267,7 +301,7 @@ impl Chain { /// /// Precondition: /// - Assumes that `from` contract exists. - fn transfer_contract_to_account( + fn changeset_transfer_contract_to_account( &mut self, amount: Amount, from: ContractAddress, @@ -279,9 +313,10 @@ impl Chain { } // Make the transfer. - let new_balance_from = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance_from = + self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; let new_balance_to = self - .change_account_balance(to, AmountDelta::Positive(amount)) + .changeset_change_account_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(NewBalances { @@ -295,7 +330,7 @@ impl Chain { /// /// Precondition: /// - Assumes that `from` contract exists. - fn transfer_contract_to_contract( + fn changeset_transfer_contract_to_contract( &mut self, amount: Amount, from: ContractAddress, @@ -307,9 +342,10 @@ impl Chain { } // Make the transfer. - let new_balance_from = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance_from = + self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; let new_balance_to = self - .change_contract_balance(to, AmountDelta::Positive(amount)) + .changeset_change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(NewBalances { @@ -323,7 +359,7 @@ impl Chain { /// /// Precondition: /// - Assumes that `from` account exists. - fn transfer_account_to_contract( + fn changeset_transfer_account_to_contract( &mut self, amount: Amount, from: AccountAddress, @@ -335,9 +371,10 @@ impl Chain { } // Make the transfer. - let new_balance_from = self.change_account_balance(from, AmountDelta::Negative(amount))?; + let new_balance_from = + self.changeset_change_account_balance(from, AmountDelta::Negative(amount))?; let new_balance_to = self - .change_contract_balance(to, AmountDelta::Positive(amount)) + .changeset_change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(NewBalances { @@ -353,18 +390,12 @@ impl Chain { /// /// Precondition: /// - Contract must exist. - fn change_contract_balance( + fn changeset_change_contract_balance( &mut self, address: ContractAddress, delta: AmountDelta, ) -> Result { - match self - .changeset - .get_mut() - .expect("change set should never be empty.") - .contracts - .entry(address) - { + match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self @@ -375,20 +406,20 @@ impl Chain { // Try to apply the balance or return an error if insufficient funds. let new_contract_balance = delta.apply_to_balance(original_balance)?; // Insert the changes into the changeset. - vac.insert(InstanceChanges { - balance_delta: delta, - ..InstanceChanges::new(original_balance) + vac.insert(ContractChanges { + self_balance_delta: delta, + ..ContractChanges::new(original_balance) }); - return Ok(new_contract_balance); + Ok(new_contract_balance) } btree_map::Entry::Occupied(mut occ) => { let contract_changes = occ.get_mut(); - let new_delta = contract_changes.balance_delta.add_delta(delta); + let new_delta = contract_changes.self_balance_delta.add_delta(delta); // Try to apply the balance or return an error if insufficient funds. let new_contract_balance = - new_delta.apply_to_balance(contract_changes.original_balance)?; - contract_changes.balance_delta = new_delta; - return Ok(new_contract_balance); + new_delta.apply_to_balance(contract_changes.self_balance_original)?; + contract_changes.self_balance_delta = new_delta; + Ok(new_contract_balance) } } } @@ -400,18 +431,12 @@ impl Chain { /// /// Precondition: /// - Account must exist. - fn change_account_balance( + fn changeset_change_account_balance( &mut self, address: AccountAddress, delta: AmountDelta, ) -> Result { - match self - .changeset - .get_mut() - .expect("change set should never be empty.") - .accounts - .entry(address) - { + match self.changeset.current_mut().accounts.entry(address) { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self @@ -426,7 +451,7 @@ impl Chain { original_balance, balance_delta: delta, }); - return Ok(new_account_balance); + Ok(new_account_balance) } btree_map::Entry::Occupied(mut occ) => { let account_changes = occ.get_mut(); @@ -435,7 +460,7 @@ impl Chain { let new_account_balance = new_delta.apply_to_balance(account_changes.original_balance)?; account_changes.balance_delta = new_delta; - return Ok(new_account_balance); + Ok(new_account_balance) } } } @@ -443,9 +468,6 @@ impl Chain { /// Returns the contract balance from the topmost checkpoint on the /// changeset. Or, alternatively, from persistence. /// - /// TODO: Find better names for these methods. Perhaps moving them to an - /// inner struct. - /// /// Preconditions: /// - Contract must exist. fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { @@ -456,30 +478,23 @@ impl Chain { /// Looks up the contract balance from the topmost checkpoint on the /// changeset. Or, alternatively, from persistence. fn changeset_contract_balance(&self, address: ContractAddress) -> Option { - match self - .changeset - .get() - .expect("change set should never be empty.") - .contracts - .get(&address) - { + match self.changeset.current().contracts.get(&address) { Some(changes) => Some(changes.current_balance()), None => self.contracts.get(&address).map(|c| c.self_balance), } } - /// Returns the contract module (artifact) from the topmost checkpoint on + /// Returns the contract module from the topmost checkpoint on /// the changeset. Or, alternatively, from persistence. /// /// Preconditions: /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. - fn contract_module(&self, address: ContractAddress) -> Arc { + fn changeset_contract_module(&self, address: ContractAddress) -> Arc { match self .changeset - .get() - .expect("change set should never be empty.") + .current() .contracts .get(&address) .and_then(|c| c.module) @@ -507,22 +522,20 @@ impl Chain { } /// Get the contract state, either from the changeset or by thawing it from - /// persistence. TODO: Should this return a &mut MutableState or is cloning - /// ok? + /// persistence. /// /// Preconditions: /// - Contract instance must exist. - fn contract_state(&self, address: ContractAddress) -> MutableState { + fn changeset_contract_state(&self, address: ContractAddress) -> MutableState { match self .changeset - .get() - .expect("change set should never be empty") + .current() .contracts .get(&address) .and_then(|c| c.state.clone()) { // Contract state has been modified. - Some(modified_state) => modified_state.clone(), + Some(modified_state) => modified_state, // Contract state hasn't been modified. Thaw from persistence. None => self .contracts @@ -535,11 +548,10 @@ impl Chain { /// Looks up the account balance for an account by first checking the /// changeset, then the persisted values. - fn account_balance_from_changeset(&self, address: AccountAddress) -> Option { + fn changeset_account_balance(&self, address: AccountAddress) -> Option { match self .changeset - .get() - .expect("Change set should never be empty") + .current() .accounts .get(&address) .map(|a| a.current_balance()) @@ -567,14 +579,11 @@ impl Chain { /// - All contracts, modules, accounts referred must exist in persistence. /// - All amount deltas must be valid (i.e. not cause underflows when added /// to balance). - fn persist_changes_and_clear( + fn changeset_persist_and_clear( &mut self, remaining_energy: Energy, ) -> Result { - let changes = self - .changeset - .get_mut() - .expect("change set should never be empty"); + let changes = self.changeset.current_mut(); // Persist contract changes and collect the total increase in states sizes. let mut collector = SizeCollector::default(); let mut loader = v1::trie::Loader::new(&[][..]); @@ -601,7 +610,7 @@ impl Chain { .checked_sub(energy_for_state_increase) .is_none() { - self.changeset.clear(); + self.changeset_clear(); return Err(OutOfEnergy); } @@ -609,13 +618,13 @@ impl Chain { for (addr, changes) in changes.contracts.iter_mut() { let mut contract = self .contracts - .get_mut(&addr) + .get_mut(addr) .expect("Precondition violation: contract must exist"); // Update balance. - if !changes.balance_delta.is_zero() { + if !changes.self_balance_delta.is_zero() { contract.self_balance = changes - .balance_delta - .apply_to_balance(changes.original_balance) + .self_balance_delta + .apply_to_balance(changes.self_balance_original) .expect("Precondition violation: amount delta causes underflow"); } // Update module reference. @@ -634,7 +643,7 @@ impl Chain { for (addr, changes) in changes.accounts.iter() { let mut account = self .accounts - .get_mut(&addr) + .get_mut(addr) .expect("Precondition violation: account must exist"); // Update balance. if !changes.balance_delta.is_zero() { @@ -645,7 +654,7 @@ impl Chain { } } // Clear the changeset. - self.changeset.clear(); + self.changeset_clear(); Ok(energy_for_state_increase) } @@ -658,25 +667,19 @@ impl Chain { /// Returns an [`OutOfEnergy`] error if the energy needed for storing the /// extra state is larger than `remaining_energy`. Otherwise, it returns /// the [`Energy`] needed for storing the extra state. - fn collect_energy_for_extra_state_and_clear( + fn changeset_collect_energy_for_state_and_clear( &mut self, remaining_energy: Energy, ) -> Result { let mut loader = v1::trie::Loader::new(&[][..]); let mut collector = SizeCollector::default(); - for (_, changes) in self - .changeset - .get_mut() - .expect("change set should never be empty") - .contracts - .iter_mut() - { + for (_, changes) in self.changeset.current_mut().contracts.iter_mut() { if let Some(modified_state) = &mut changes.state { modified_state.freeze(&mut loader, &mut collector); } } // Clear the changeset. - self.changeset.clear(); + self.changeset_clear(); // One energy per extra byte in the state. let energy_for_state_increase = Energy::from(collector.collect()); @@ -700,12 +703,11 @@ impl Chain { /// /// Preconditions: /// - Contract must exist. - fn save_state_changes(&mut self, address: ContractAddress, state: &mut MutableState) { + fn changeset_save_state_changes(&mut self, address: ContractAddress, state: &mut MutableState) { println!("Saving state for {}", address); let mut loader = v1::trie::Loader::new(&[][..]); self.changeset - .get_mut() - .expect("change set should never be empty") + .current_mut() .contracts .entry(address) .and_modify(|changes| { @@ -718,10 +720,10 @@ impl Chain { .get(&address) .expect("Precondition violation: contract must exist.") .self_balance; - InstanceChanges { + ContractChanges { state: Some(state.make_fresh_generation(&mut loader)), modification_index: 1, // Increment from default, 0, to 1. - ..InstanceChanges::new(original_balance) + ..ContractChanges::new(original_balance) } }); } @@ -729,24 +731,39 @@ impl Chain { /// Returns the modification index for a contract. /// It looks it up in the changeset, and if it isn't there, it will return /// `0`. - fn modification_index(&self, address: ContractAddress) -> u32 { + fn changeset_modification_index(&self, address: ContractAddress) -> u32 { self.changeset - .get() - .expect("change set should never be empty") + .current() .contracts .get(&address) .map_or(0, |c| c.modification_index) } + + /// Clears the changeset. + fn changeset_clear(&mut self) { self.changeset.clear(); } + + /// Makes a new checkpoint. + fn changeset_checkpoint(&mut self) { self.changeset.checkpoint(); } + + /// Roll back to the previous checkpoint. + fn changeset_rollback(&mut self) { self.changeset.rollback(); } } +/// The resulting new balances after making a transfer between an accounts or +/// contracts. struct NewBalances { + /// The new balance of the sender. new_balance_from: Amount, + /// The new balance of the receiver. new_balance_to: Amount, } +/// A transfer of [`Amount`]s failed because the sender had insufficient +/// balance. #[derive(Debug)] struct InsufficientBalanceError; +/// An underflow occurred. #[derive(Debug)] struct UnderflowError; @@ -762,36 +779,6 @@ impl From for AccountTransferError { fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } } -// checkpoint: clone + put on top of stack (what happens in withRollback in -// scheduler) -// -// rollback: pop -// -// save_to_persistence: -// - Save any changes still on the stack. -// -// get_current_state: from newest changeset or persistence -// -> look on top of stack to see if state = some(State) -// -- same behavior for modules -// -// stack only modified right before invoke (push) or right after entrypoint -// execution (maybe pop) -// -// modification_index: inc if state_changed prior to invoke_contract -// -> on resume: -// - save mod index prior to invoke -// - invoke -// - get newest state + mod index -// - if new mod_idx != old_mod_idx => state_modified (will always be >=) -// - This is where state_modified != state_changed (because of -// intermediate calls) -// - if state_changed then commit the current state to the changeset -// (TODO: find out where this should be handled) (in scheduler: -// withInstanceStateV1) -// -// on any res::Success, commit state to changeset -// on res::Other, stack.pop() checkpoint - impl Chain { /// Create a new [`Self`] where all the configurable parameters are /// provided. @@ -859,9 +846,9 @@ impl Chain { // +1 for the tag, +8 for size and version let payload_size = 1 + 8 - + wasm_module.source.size() as u64 + + wasm_module.source.size() + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self.get_account(sender)?.signature_count; + let number_of_sigs = self.persistence_get_account(sender)?.signature_count; let base_cost = cost::base_cost(payload_size, number_of_sigs); let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); base_cost + deploy_module_cost @@ -874,7 +861,7 @@ impl Chain { ); // Try to subtract cost for account - let account = self.get_account_mut(sender)?; + let account = self.persistence_get_account_mut(sender)?; if account.balance < transaction_fee { return Err(DeployModuleError::InsufficientFunds); }; @@ -966,11 +953,11 @@ impl Chain { energy_reserved: Energy, ) -> Result { // Lookup artifact - let artifact = self.get_artifact(module_reference)?; + let artifact = self.persistence_contract_module(module_reference)?; let mut transaction_fee = self.calculate_energy_cost(self.lookup_module_cost(&artifact)); // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. - let account_info = self.get_account(sender)?; + let account_info = self.persistence_get_account(sender)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } @@ -1014,7 +1001,7 @@ impl Chain { let mut collector = v1::trie::SizeCollector::default(); - let contract_instance = ContractInstance { + let contract_instance = Contract { module_reference, contract_name: contract_name.to_owned(), state: state.freeze(&mut loader, &mut collector), // TODO: Charge for storage. @@ -1025,7 +1012,7 @@ impl Chain { // Save the contract instance self.contracts.insert(contract_address, contract_instance); // Subtract the from the invoker. - self.get_account_mut(sender)?.balance -= amount; + self.persistence_get_account_mut(sender)?.balance -= amount; Ok(SuccessfulContractInit { contract_address, @@ -1082,7 +1069,7 @@ impl Chain { }; // Charge the account. // We have to get the account info again because of the borrow checker. - self.get_account_mut(sender)?.balance -= transaction_fee; + self.persistence_get_account_mut(sender)?.balance -= transaction_fee; res } @@ -1118,11 +1105,11 @@ impl Chain { let instance_self_balance = if amount.micro_ccd() > 0 { match sender { Address::Account(sender_account) => { - self.transfer_account_to_contract(amount, sender_account, address)? + self.changeset_transfer_account_to_contract(amount, sender_account, address)? .new_balance_to } Address::Contract(sender_contract) => { - self.transfer_contract_to_contract(amount, sender_contract, address)? + self.changeset_transfer_contract_to_contract(amount, sender_contract, address)? .new_balance_to } } @@ -1131,8 +1118,8 @@ impl Chain { }; // Get the instance and artifact. To be used in several places. - let instance = self.get_instance(address)?; - let artifact = self.contract_module(address); + let instance = self.persistence_get_contract(address)?; + let artifact = self.changeset_contract_module(address); // Subtract the cost of looking up the module remaining_energy = @@ -1165,7 +1152,7 @@ impl Chain { self_balance: instance_self_balance, sender, owner: instance.owner, - sender_policies: self.get_account(invoker)?.policies.clone(), + sender_policies: self.persistence_get_account(invoker)?.policies.clone(), }, }; @@ -1173,7 +1160,7 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = self.contract_state(address); + let mut mutable_state = self.changeset_contract_state(address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -1184,7 +1171,7 @@ impl Chain { v1::ReceiveInvocation { amount, receive_name: receive_name.as_receive_name(), - parameter: ¶meter.0, + parameter: parameter.0, energy: Chain::to_interpreter_energy(remaining_energy), }, instance_state, @@ -1203,7 +1190,7 @@ impl Chain { contract_name, amount, invoker_amount_reserved_for_nrg, - entrypoint: entrypoint.to_owned(), + entrypoint, chain: self, state: mutable_state, chain_events: Vec::new(), @@ -1221,6 +1208,9 @@ impl Chain { res } + /// Update a contract by calling one of its entrypoints. + /// + /// If successful, any changes will be saved. pub fn contract_update( &mut self, invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? @@ -1238,7 +1228,7 @@ impl Chain { // Ensure account exists and can pay for the reserved energy and amount // TODO: Could we just remove this amount in the changeset and then put back the // to_ccd(remaining_energy) afterwards? - let account_info = self.get_account(invoker)?; + let account_info = self.persistence_get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); @@ -1264,8 +1254,7 @@ impl Chain { Ok(v1::ReceiveResult::Success { remaining_energy, .. }) => { - println!("Remaining energy u64: {}", remaining_energy); - match self.persist_changes_and_clear(Chain::from_interpreter_energy( + match self.changeset_persist_and_clear(Chain::from_interpreter_energy( InterpreterEnergy::from(remaining_energy), )) { Ok(energy) => energy, @@ -1281,7 +1270,7 @@ impl Chain { } _ => { // An error occured, so we don't save the changes. Just clear. - self.changeset.clear(); + self.changeset_clear(); Energy::from(0) } }; @@ -1296,10 +1285,14 @@ impl Chain { // Charge the transaction fee irrespective of the result. // TODO: If we charge up front, then we should return to_ccd(remaining_energy) // here instead. - self.get_account_mut(invoker)?.balance -= transaction_fee; + self.persistence_get_account_mut(invoker)?.balance -= transaction_fee; res } + /// Invoke a contract by calling an entrypoint. + /// + /// Similar to [`contract_update`] except that all changes are discarded + /// afterwards. Typically used for "view" functions. pub fn contract_invoke( &mut self, invoker: AccountAddress, @@ -1315,7 +1308,7 @@ impl Chain { ); // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.get_account(invoker)?; + let account_info = self.persistence_get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); @@ -1337,7 +1330,7 @@ impl Chain { let energy_for_state_increase = match receive_result { Ok(v1::ReceiveResult::Success { remaining_energy, .. - }) => match self.collect_energy_for_extra_state_and_clear( + }) => match self.changeset_collect_energy_for_state_and_clear( Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), ) { Ok(energy) => energy, @@ -1352,7 +1345,7 @@ impl Chain { }, _ => { // An error occured, so we just clear the changeset. - self.changeset.clear(); + self.changeset_clear(); Energy::from(0) } }; @@ -1382,33 +1375,34 @@ impl Chain { ContractAddress::new(index, subindex) } + /// Set the chain's slot time. pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } + /// Set the chain's Euro per NRG conversion rate. pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { self.euro_per_energy = euro_per_energy; } + /// Set the chain's microCCD per Euro conversion rate. pub fn set_micro_ccd_per_euro(&mut self, micro_ccd_per_euro: ExchangeRate) { self.micro_ccd_per_euro = micro_ccd_per_euro; } /// Returns the balance of an account if it exists. - // This will always be the persisted account balance. - pub fn account_balance(&self, address: AccountAddress) -> Option { - self.accounts.get(&address).and_then(|ai| Some(ai.balance)) + /// This will always be the persisted account balance. + pub fn persistence_account_balance(&self, address: AccountAddress) -> Option { + self.accounts.get(&address).map(|ai| ai.balance) } /// Returns the balance of an contract if it exists. - // This will always be the persisted contract balance. - pub fn contract_balance(&self, address: ContractAddress) -> Option { - self.contracts - .get(&address) - .and_then(|ci| Some(ci.self_balance)) + /// This will always be the persisted contract balance. + pub fn persistence_contract_balance(&self, address: ContractAddress) -> Option { + self.contracts.get(&address).map(|ci| ci.self_balance) } - // Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange rates - // available: - // + /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange + /// rates available: + // TODO: Find a way to make this parse the doc tests // To find the mCCD/NRG exchange rate: // // euro mCCD euro * mCCD mCCD @@ -1446,45 +1440,55 @@ impl Chain { } } - pub fn lookup_module_cost(&self, artifact: &ArtifactV1) -> Energy { + /// Calculate the energy energy for looking up a [`ContractModule`]. + fn lookup_module_cost(&self, module: &ContractModule) -> Energy { // TODO: Is it just the `.code`? // Comes from Concordium/Cost.hs::lookupModule - Energy::from(artifact.code.len() as u64 / 50) + Energy::from(module.code.len() as u64 / 50) } - /// Returns an Arc clone of the artifact. - /// TODO: Look in changeset first. - fn get_artifact(&self, module_ref: ModuleReference) -> Result, ModuleMissing> { - let artifact = self + /// Returns an Arc clone of the [`ContractModule`] from persistence. + fn persistence_contract_module( + &self, + module_ref: ModuleReference, + ) -> Result, ModuleMissing> { + let module = self .modules .get(&module_ref) .ok_or(ModuleMissing(module_ref))?; - Ok(Arc::clone(artifact)) + Ok(Arc::clone(module)) } - fn get_instance( + /// Returns an immutable reference to a [`Contract`] from persistence. + fn persistence_get_contract( &self, address: ContractAddress, - ) -> Result<&ContractInstance, ContractInstanceMissing> { + ) -> Result<&Contract, ContractInstanceMissing> { self.contracts .get(&address) .ok_or(ContractInstanceMissing(address)) } - fn get_instance_mut( + /// Returns a mutable reference to a [`Contract`] from persistence. + fn persistence_get_contract_mut( &mut self, address: ContractAddress, - ) -> Result<&mut ContractInstance, ContractInstanceMissing> { + ) -> Result<&mut Contract, ContractInstanceMissing> { self.contracts .get_mut(&address) .ok_or(ContractInstanceMissing(address)) } - fn get_account(&self, address: AccountAddress) -> Result<&AccountInfo, AccountMissing> { + /// Returns an immutable reference to [`AccountInfo`] from persistence. + fn persistence_get_account( + &self, + address: AccountAddress, + ) -> Result<&AccountInfo, AccountMissing> { self.accounts.get(&address).ok_or(AccountMissing(address)) } - fn get_account_mut( + /// Returns a mutable reference to [`AccountInfo`] from persistence. + fn persistence_get_account_mut( &mut self, address: AccountAddress, ) -> Result<&mut AccountInfo, AccountMissing> { @@ -1619,8 +1623,10 @@ impl Chain { /// Errors related to transfers from contract to an account. #[derive(PartialEq, Eq, Debug, Error)] enum ContractTransferError { + /// The receiver does not exist. #[error("The receiver does not exist.")] ToMissing, + /// The sender does not have sufficient balance. #[error("The sender does not have sufficient balance.")] InsufficientBalance, } @@ -1629,29 +1635,33 @@ enum ContractTransferError { /// account to a contract. #[derive(PartialEq, Eq, Debug, Error)] enum AccountTransferError { + /// The receiver does not exist. #[error("The receiver does not exist.")] ToMissing, + /// The sender does not have sufficient balance. #[error("The sender does not have sufficient balance.")] InsufficientBalance, } +/// The contract ran out of energy during execution of an update or invocation. #[derive(PartialEq, Eq, Debug)] struct OutOfEnergy; /// The entrypoint does not exist. #[derive(PartialEq, Eq, Debug, Error)] +#[error("The entrypoint '{0}' does not exist.")] pub struct EntrypointDoesNotExist(OwnedEntrypointName); -impl fmt::Display for EntrypointDoesNotExist { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "The entrypoint '{}' does not exist.", self.0) - } -} - +/// Data needed to recursively process a contract update or invocation to +/// completion. struct ProcessReceiveData<'a, 'b> { + /// The invoker. invoker: AccountAddress, + /// The contract being called. address: ContractAddress, + /// The name of the contract. contract_name: OwnedContractName, + /// The amount sent from the sender to the contract. amount: Amount, /// The CCD amount reserved from the invoker account for the energy. While /// the amount is reserved, it is not subtracted in the chain.accounts @@ -1659,10 +1669,15 @@ struct ProcessReceiveData<'a, 'b> { /// TODO: We could use a changeset for accounts -> balance, and then look up /// the "chain.accounts" values for chain queries. invoker_amount_reserved_for_nrg: Amount, + /// The entrypoint to execute. entrypoint: OwnedEntrypointName, + /// A reference to the chain. chain: &'a mut Chain, + /// The current state. state: MutableState, + /// Chain events that have occurred during the execution. chain_events: Vec, + /// loader: v1::trie::Loader<&'b [u8]>, } @@ -1696,7 +1711,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Save changes to changeset. if state_changed { - self.chain.save_state_changes(self.address, &mut self.state); + self.chain + .changeset_save_state_changes(self.address, &mut self.state); } Ok(v1::ReceiveResult::Success { @@ -1728,7 +1744,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { println!("\t\tTransferring {} CCD to {}", amount, to); - let response = match self.chain.transfer_contract_to_account( + let response = match self.chain.changeset_transfer_contract_to_account( amount, self.address, to, @@ -1787,23 +1803,22 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain_events.push(interrupt_event); if state_changed { - self.chain.save_state_changes(self.address, &mut self.state); + self.chain + .changeset_save_state_changes(self.address, &mut self.state); } // Save the modification index before the invoke. - let mod_idx_before_invoke = self.chain.modification_index(self.address); + let mod_idx_before_invoke = + self.chain.changeset_modification_index(self.address); // Make a checkpoint before calling another contract so that we may roll // back. - self.chain - .changeset - .checkpoint() - .expect("Change set should never be empty"); + self.chain.changeset_checkpoint(); if VERBOSE_DEBUG { println!( "Before call (after checkpoint): {:#?}", - self.chain.changeset.get().unwrap() + self.chain.changeset.current() ); } @@ -1893,22 +1908,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Remove the last state changes if the invocation failed. let state_changed = if !success { - println!("Rolling back"); - self.chain - .changeset - .pop() - .expect("Change set should never be empty"); + self.chain.changeset_rollback(); false // We rolled back, so no changes were made // to this contract. } else { let mod_idx_after_invoke = - self.chain.modification_index(self.address); + self.chain.changeset_modification_index(self.address); let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; if state_changed { - println!("State change detected via mod_idx"); // Update the state field with the newest value from the // changeset. - self.state = self.chain.contract_state(self.address); + self.state = self.chain.changeset_contract_state(self.address); } // TODO: Notes say that we should commit the state changes to the // changeset at this point. But the state changes would already @@ -1919,7 +1929,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { if VERBOSE_DEBUG { println!( "After call (and potential rollback):\n{:#?}", - self.chain.changeset.get().unwrap() + self.chain.changeset.current() ); } @@ -1974,14 +1984,16 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { Some(artifact) => { // Charge for the module lookup. energy_after_invoke -= Chain::to_interpreter_energy( - self.chain.lookup_module_cost(&artifact), + self.chain.lookup_module_cost(artifact), ) .energy; if artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), ) { - let instance = self.chain.get_instance_mut(self.address)?; + let instance = self + .chain + .persistence_get_contract_mut(self.address)?; let old_module_ref = instance.module_reference; // Update module reference for the instance. instance.module_reference = module_ref; @@ -2035,9 +2047,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // should be included. That is handled by // the `chain` struct already. transaction. // However, that is hand - let response = match self.chain.account_balance_from_changeset(address) - { - // TODO: next + let response = match self.chain.changeset_account_balance(address) { Some(acc_bal) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for @@ -2049,7 +2059,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } else { acc_bal }; - println!("\t\t\tBalance found to be {}", acc_bal); // TODO: Do we need non-zero staked and shielded balances? let balances = @@ -2156,21 +2165,25 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } +/// The contract module does not exist. #[derive(Debug, Error)] #[error("Module {:?} does not exist.", 0)] pub struct ModuleMissing(ModuleReference); +/// The contract instance does not exist. #[derive(Debug, Error)] #[error("Contract instance {0} does not exist.")] pub struct ContractInstanceMissing(ContractAddress); +/// The account does not exist. #[derive(Debug, Error)] #[error("Account {0} does not exist.")] pub struct AccountMissing(AccountAddress); +/// Data about an [`AccountAddress`]. #[derive(Clone)] pub struct AccountInfo { - /// The account balance. TODO: Do we need the three types of balances? + /// The account balance. TODO: Add all three types of balances. pub balance: Amount, /// Account policies. policies: v0::OwnedPolicyBytes, @@ -2179,6 +2192,7 @@ pub struct AccountInfo { signature_count: u32, } +/// Account policies for testing. pub struct TestPolicies(v0::OwnedPolicyBytes); impl TestPolicies { @@ -2230,6 +2244,7 @@ impl AccountInfo { pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } } +/// Errors that can occur while initializing a contract. #[derive(Debug, Error)] pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. @@ -2249,6 +2264,7 @@ pub enum ContractInitError { InsufficientFunds, } +/// Errors that can occur during a contract update or invocation. #[derive(Debug, Error)] pub enum ContractUpdateError { /// Update failed for a reason that also exists on the chain. @@ -2274,28 +2290,40 @@ pub enum ContractUpdateError { InsufficientFunds, } -// TODO: Implementing (Partial)Eq for this and the other error/success types -// would be nice. But `anyhow::Error` does not implement (Partial)Eq. +/// Represents a failed interaction, i.e. update or invocation, of a contract. #[derive(Debug)] pub enum FailedContractInteraction { + /// The contract rejected. Reject { + /// The error code for why it rejected. reason: i32, + /// The return value. return_value: ReturnValue, + /// The amount of energy used before rejecting. energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. transaction_fee: Amount, }, + /// The contract trapped. Trap { + /// The error message. error: anyhow::Error, + /// The amount of energy used before rejecting. energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. transaction_fee: Amount, }, + /// The contract ran out of energy. OutOfEnergy { + /// The amount of energy used before rejecting. energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. transaction_fee: Amount, }, } impl FailedContractInteraction { + /// Get the transaction fee. pub fn transaction_fee(&self) -> Amount { match self { FailedContractInteraction::Reject { @@ -2311,59 +2339,80 @@ impl FailedContractInteraction { } } -#[derive(Debug)] -pub struct ContractError(Vec); - +/// An error that can occur while deploying a [`ContractModule`]. // TODO: Can we get Eq for this when using io::Error? // TODO: Should this also have the energy used? #[derive(Debug, Error)] pub enum DeployModuleError { + /// Failed to read the module file. #[error("could not read the file due to: {0}")] ReadFileError(#[from] std::io::Error), + /// The module provided is not valid. #[error("module is invalid due to: {0}")] InvalidModule(#[from] anyhow::Error), + /// The account does not have sufficient funds to pay for the deployment. #[error("account does not have sufficient funds to pay for the energy")] InsufficientFunds, + /// The account deploying the module does not exist. #[error("account {} does not exist", 0.0)] AccountDoesNotExist(#[from] AccountMissing), + /// The module version is not supported. #[error("wasm version {0} is not supported")] UnsupportedModuleVersion(WasmVersion), + /// The module has already been deployed. #[error("module with reference {:?} already exists", 0)] DuplicateModule(ModuleReference), } -impl ContractError { - pub fn deserial(&self) -> Result { todo!() } -} - +/// An event that occurred during a contract update or invocation. #[derive(Debug)] pub enum ChainEvent { + /// A contract was interrupted. Interrupted { + /// The contract interrupted. address: ContractAddress, + /// Logs produced prior to being interrupted. logs: v0::Logs, }, + /// A contract was resumed after being interrupted. Resumed { + /// The contract resumed. address: ContractAddress, + /// Whether the action that caused the interrupt succeeded. success: bool, }, + /// A contract was upgraded. Upgraded { + /// The contract upgraded. address: ContractAddress, + /// The old module reference. from: ModuleReference, + /// The new module reference. to: ModuleReference, }, + /// A contract was updated. Updated { + /// The contract updated. address: ContractAddress, + /// The name of the contract. contract: OwnedContractName, + /// The entrypoint called. entrypoint: OwnedEntrypointName, + /// The amount added to the contract. amount: Amount, }, + /// A contract transferred an [`Amount`] to an account. Transferred { + /// The sender contract. from: ContractAddress, + /// The [`Amount`] transferred. amount: Amount, + /// The receiver account. to: AccountAddress, }, } +/// Represents a successful contract update (or invocation). // TODO: Consider adding function to aggregate all logs from the host_events. #[derive(Debug)] pub struct SuccessfulContractUpdate { @@ -2382,14 +2431,19 @@ pub struct SuccessfulContractUpdate { pub logs: v0::Logs, } +/// A transfer from an contract to an account. #[derive(Debug, PartialEq, Eq)] pub struct Transfer { + /// The sender contract. pub from: ContractAddress, + /// The amount transferred. pub amount: Amount, + /// The receive account. pub to: AccountAddress, } impl SuccessfulContractUpdate { + /// Get a list of all transfers that were made from contracts to accounts. pub fn transfers(&self) -> Vec { self.chain_events .iter() @@ -2408,14 +2462,18 @@ impl SuccessfulContractUpdate { } } +/// Represents a successful deployment of a [`ContractModule`]. #[derive(Debug, PartialEq, Eq)] pub struct SuccessfulModuleDeployment { + /// The reference of the module deployed. pub module_reference: ModuleReference, + /// The energy used for deployment. pub energy: Energy, /// Cost of transaction. pub transaction_fee: Amount, } +/// Represents a successful initialization of a contract. #[derive(Debug)] pub struct SuccessfulContractInit { /// The address of the new instance. @@ -2428,26 +2486,21 @@ pub struct SuccessfulContractInit { pub transaction_fee: Amount, } +/// A value returned by a contract. #[derive(Debug)] pub struct ContractReturnValue(Vec); -#[derive(Debug, PartialEq, Eq)] -pub enum ParsingError { - /// Thrown by `deserial` on failure. - ParsingFailed, -} - -impl ContractReturnValue { - pub fn deserial(&self) -> Result { todo!() } -} - +/// The parameter to a contract. pub struct ContractParameter(pub Vec); impl ContractParameter { + /// Create an empty [`Self`]. pub fn empty() -> Self { Self(Vec::new()) } + /// Create a [`Self`] from a byte array. pub fn from_bytes(bytes: Vec) -> Self { Self(bytes) } + /// Create a [`Self`] by serializing a `T`. pub fn from_typed(parameter: &T) -> Self { Self(to_bytes(parameter)) } } @@ -2467,8 +2520,14 @@ mod tests { chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); assert_eq!(chain.accounts.len(), 2); - assert_eq!(chain.account_balance(ACC_0), Some(Amount::from_ccd(1))); - assert_eq!(chain.account_balance(ACC_1), Some(Amount::from_ccd(2))); + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(Amount::from_ccd(1)) + ); + assert_eq!( + chain.persistence_account_balance(ACC_1), + Some(Amount::from_ccd(2)) + ); } #[test] @@ -2483,7 +2542,7 @@ mod tests { assert_eq!(chain.modules.len(), 1); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some(initial_balance - res.transaction_fee) ); } @@ -2509,7 +2568,7 @@ mod tests { ) .expect("Initializing valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!(chain.contracts.len(), 1); @@ -2543,7 +2602,7 @@ mod tests { transaction_fee, .. }) => assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) ), _ => panic!("Expected valid chain error."), @@ -2595,7 +2654,7 @@ mod tests { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -2662,7 +2721,7 @@ mod tests { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -2672,7 +2731,7 @@ mod tests { ) ); assert_eq!( - chain.account_balance(ACC_1), + chain.persistence_account_balance(ACC_1), Some(initial_balance + transfer_amount) ); assert_eq!(res_update.transfers(), [Transfer { @@ -2726,7 +2785,7 @@ mod tests { })) => { assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -2784,7 +2843,7 @@ mod tests { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -2846,7 +2905,7 @@ mod tests { .expect("Invoking get should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -2957,7 +3016,7 @@ mod tests { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -3033,7 +3092,7 @@ mod tests { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -3092,11 +3151,11 @@ mod tests { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!( - chain.account_balance(ACC_1), + chain.persistence_account_balance(ACC_1), // Differs from `expected_balance` as it only includes the actual amount charged // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) @@ -3155,7 +3214,7 @@ mod tests { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -3165,7 +3224,7 @@ mod tests { ) ); assert_eq!( - chain.account_balance(ACC_1), + chain.persistence_account_balance(ACC_1), Some(initial_balance + amount_to_send) ); assert!(matches!(res_update.chain_events[..], [ @@ -3219,7 +3278,7 @@ mod tests { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -3276,7 +3335,7 @@ mod tests { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.persistence_account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee From fa575d5673bd55a3bd9955228e8959ecdb9c88dc Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 14 Feb 2023 15:05:19 +0100 Subject: [PATCH 054/208] Allow contract sender for update/invoke --- contract-testing/src/lib.rs | 171 +++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 14 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d1256060..05849e33 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -287,15 +287,25 @@ impl Default for Chain { // Private methods impl Chain { /// Check whether an account exists. - fn account_exists(&self, address: AccountAddress) -> bool { + fn persistence_account_exists(&self, address: AccountAddress) -> bool { self.accounts.contains_key(&address) } /// Check whether a contract exists. - fn contract_exists(&self, address: ContractAddress) -> bool { + fn persistence_contract_exists(&self, address: ContractAddress) -> bool { self.contracts.contains_key(&address) } + /// Check whether the address exists in persistence. I.e. if it is an + /// account, whether the account exists, and if it is a contract, whether + /// the contract exists. + fn persistence_address_exists(&self, address: Address) -> bool { + match address { + Address::Account(acc) => self.persistence_account_exists(acc), + Address::Contract(contr) => self.persistence_contract_exists(contr), + } + } + /// Make a transfer from a contract to an account in the changeset. /// Returns the new balances of both. /// @@ -308,7 +318,7 @@ impl Chain { to: AccountAddress, ) -> Result { // Ensure the `to` account exists. - if !self.account_exists(to) { + if !self.persistence_account_exists(to) { return Err(ContractTransferError::ToMissing); } @@ -337,7 +347,7 @@ impl Chain { to: ContractAddress, ) -> Result { // Ensure the `to` contract exists. - if !self.contract_exists(to) { + if !self.persistence_contract_exists(to) { return Err(ContractTransferError::ToMissing); } @@ -366,7 +376,7 @@ impl Chain { to: ContractAddress, ) -> Result { // Ensure the `to` account exists. - if !self.contract_exists(to) { + if !self.persistence_contract_exists(to) { return Err(AccountTransferError::ToMissing); } @@ -984,6 +994,7 @@ impl Chain { loader, )?; // Handle the result and update the transaction fee. + // TODO: Extract to helper function. let res = match res { v1::InitResult::Success { logs, @@ -1078,6 +1089,7 @@ impl Chain { /// Preconditions: /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// - `sender` exists /// /// Returns: /// - Everything the types can encode apart from @@ -1213,7 +1225,8 @@ impl Chain { /// If successful, any changes will be saved. pub fn contract_update( &mut self, - invoker: AccountAddress, // TODO: Should we add a sender field and allow contract senders? + invoker: AccountAddress, + sender: Address, address: ContractAddress, entrypoint: EntrypointName, parameter: ContractParameter, @@ -1225,6 +1238,11 @@ impl Chain { address, parameter.0 ); + // Ensure the sender exists. + if !self.persistence_address_exists(sender) { + return Err(ContractUpdateError::SenderDoesNotExist(sender)); + } + // Ensure account exists and can pay for the reserved energy and amount // TODO: Could we just remove this amount in the changeset and then put back the // to_ccd(remaining_energy) afterwards? @@ -1238,7 +1256,7 @@ impl Chain { let mut chain_events = Vec::new(); let receive_result = self.contract_update_aux( invoker, - Address::Account(invoker), + sender, address, entrypoint.to_owned(), Parameter(¶meter.0), @@ -1296,6 +1314,7 @@ impl Chain { pub fn contract_invoke( &mut self, invoker: AccountAddress, + sender: Address, address: ContractAddress, entrypoint: EntrypointName, parameter: ContractParameter, @@ -1307,6 +1326,11 @@ impl Chain { address, parameter.0 ); + // Ensure the sender exists. + if !self.persistence_address_exists(sender) { + return Err(ContractUpdateError::SenderDoesNotExist(sender)); + } + // Ensure account exists and can pay for the reserved energy and amount let account_info = self.persistence_get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); @@ -1317,7 +1341,7 @@ impl Chain { let mut chain_events = Vec::new(); let receive_result = self.contract_update_aux( invoker, - Address::Account(invoker), + sender, address, entrypoint.to_owned(), Parameter(¶meter.0), @@ -1920,9 +1944,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // changeset. self.state = self.chain.changeset_contract_state(self.address); } - // TODO: Notes say that we should commit the state changes to the - // changeset at this point. But the state changes would already - // exist in the changeset at this point. state_changed }; @@ -2282,9 +2303,12 @@ pub enum ContractUpdateError { /// Entrypoint does not exist and neither does the fallback entrypoint. #[error("entrypoint does not exist")] EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), - /// Account has not been created in test environment. - #[error("account {} does not exist", 0.0)] - AccountDoesNotExist(#[from] AccountMissing), + /// The invoker account has not been created in test environment. + #[error("invoker account {} does not exist", 0.0)] + InvokerDoesNotExist(#[from] AccountMissing), + /// The sender does not exist in the test environment. + #[error("sender {0} does not exist")] + SenderDoesNotExist(Address), /// The account does not have enough funds to pay for the energy. #[error("account does not have enough funds to pay for the energy")] InsufficientFunds, @@ -2633,6 +2657,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("set"), ContractParameter::from_bytes(vec![1u8]), // Updated to 1 @@ -2644,6 +2669,7 @@ mod tests { let res_invoke_get = chain .contract_invoke( ACC_0, + Address::Contract(res_init.contract_address), // Invoke with a contract as sender. res_init.contract_address, EntrypointName::new_unchecked("get"), ContractParameter::empty(), @@ -2668,6 +2694,85 @@ mod tests { assert_eq!(res_invoke_get.return_value.0, [1u8]); } + /// Test that updates and invocations where the sender is missing fail. + #[test] + fn updating_and_invoking_with_missing_sender_fails() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let missing_account = Address::Account(ACC_1); + let missing_contract = Address::Contract(ContractAddress::new(100, 0)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + ContractParameter::from_bytes(vec![0u8]), // Starts as 0 + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update_acc = chain.contract_update( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_invoke_acc = chain.contract_invoke( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_update_contr = chain.contract_update( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_invoke_contr = chain.contract_invoke( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + ContractParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + assert!(matches!( + res_update_acc, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + assert!(matches!( + res_invoke_acc, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + assert!(matches!( + res_update_contr, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); + assert!(matches!( + res_invoke_contr, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); + } + /// Tests using the integrate contract defined in /// concordium-rust-smart-contract on the 'kb/sc-integration-testing' /// branch. @@ -2700,6 +2805,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), @@ -2711,6 +2817,7 @@ mod tests { let res_view = chain .contract_invoke( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), ContractParameter::empty(), @@ -2770,6 +2877,7 @@ mod tests { let res_update = chain.contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. @@ -2822,6 +2930,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("recurse"), ContractParameter::from_typed(&10u32), @@ -2833,6 +2942,7 @@ mod tests { let res_view = chain .contract_invoke( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), ContractParameter::empty(), @@ -2885,6 +2995,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("inc-fail-on-zero"), ContractParameter::from_typed(&input_param), @@ -2896,6 +3007,7 @@ mod tests { let res_view = chain .contract_invoke( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), ContractParameter::empty(), @@ -2959,6 +3071,7 @@ mod tests { chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init_0.contract_address, EntrypointName::new_unchecked("mutate_and_forward"), ContractParameter::from_typed(¶m), @@ -2995,6 +3108,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), ContractParameter::from_typed(&6u64), @@ -3006,6 +3120,7 @@ mod tests { let res_view = chain .contract_invoke( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), ContractParameter::empty(), @@ -3083,6 +3198,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3142,6 +3258,7 @@ mod tests { let res_update = chain .contract_update( ACC_1, + Address::Account(ACC_1), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3205,6 +3322,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3269,6 +3387,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3326,6 +3445,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3397,6 +3517,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3445,6 +3566,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3502,6 +3624,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3552,6 +3675,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3601,6 +3725,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), ContractParameter::from_typed(&input_param), @@ -3653,6 +3778,7 @@ mod tests { let res_update_upgrade = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("bump"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -3665,6 +3791,7 @@ mod tests { let res_update_new = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("newfun"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -3720,6 +3847,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -3776,6 +3904,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::empty(), @@ -3829,6 +3958,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -3881,6 +4011,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&input_param), @@ -3951,6 +4082,7 @@ mod tests { let res_update = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&input_param), @@ -4004,6 +4136,7 @@ mod tests { let res_update_upgrade = chain.contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -4013,6 +4146,7 @@ mod tests { let res_update_new_feature = chain.contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), @@ -4070,6 +4204,7 @@ mod tests { let res_update_old_feature_0 = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("old_feature"), ContractParameter::empty(), @@ -4081,6 +4216,7 @@ mod tests { let res_update_new_feature_0 = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), @@ -4092,6 +4228,7 @@ mod tests { let res_update_upgrade = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), ContractParameter::from_typed(&res_deploy_1.module_reference), @@ -4103,6 +4240,7 @@ mod tests { let res_update_old_feature_1 = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("old_feature"), ContractParameter::empty(), @@ -4114,6 +4252,7 @@ mod tests { let res_update_new_feature_1 = chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), ContractParameter::empty(), @@ -4208,6 +4347,7 @@ mod tests { chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), ContractParameter::from_typed(¶meter), @@ -4283,6 +4423,7 @@ mod tests { chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), ContractParameter::from_typed(¶meter), @@ -4342,6 +4483,7 @@ mod tests { chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), ContractParameter::from_typed(&ACC_1), @@ -4415,6 +4557,7 @@ mod tests { chain .contract_update( ACC_0, + Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), ContractParameter::from_typed(¶meter), From 61fdadda94282c81502fa3b64fd5cd96514efe34 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 14 Feb 2023 15:30:39 +0100 Subject: [PATCH 055/208] Use changeset for handling contract upgrades --- contract-testing/src/lib.rs | 98 ++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 05849e33..adcbf082 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -191,7 +191,7 @@ impl ContractChanges { /// Get the current balance by adding the original balance and the balance /// delta. /// - /// Preconditions: + /// *Preconditions:* /// - `balance_delta + original_balance` must be larger than `0`. fn current_balance(&self) -> Amount { self.self_balance_delta @@ -213,7 +213,7 @@ impl AccountChanges { /// Get the current balance by adding the original balance and the balance /// delta. /// - /// Preconditions: + /// *Preconditions:* /// - `balance_delta + original_balance` must be larger than `0`. fn current_balance(&self) -> Amount { self.balance_delta @@ -478,7 +478,7 @@ impl Chain { /// Returns the contract balance from the topmost checkpoint on the /// changeset. Or, alternatively, from persistence. /// - /// Preconditions: + /// *Preconditions:* /// - Contract must exist. fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { self.changeset_contract_balance(address) @@ -497,7 +497,7 @@ impl Chain { /// Returns the contract module from the topmost checkpoint on /// the changeset. Or, alternatively, from persistence. /// - /// Preconditions: + /// *Preconditions:* /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. @@ -534,7 +534,7 @@ impl Chain { /// Get the contract state, either from the changeset or by thawing it from /// persistence. /// - /// Preconditions: + /// *Preconditions:* /// - Contract instance must exist. fn changeset_contract_state(&self, address: ContractAddress) -> MutableState { match self @@ -703,7 +703,8 @@ impl Chain { Ok(energy_for_state_increase) } - /// Saves the a mutable state for a contract in the changeset. + /// Saves a mutable state for a contract in the changeset. + /// /// If the contract already has an entry in the changeset, the old state /// will be replaced. Otherwise, the entry is created and the state is /// added. @@ -711,10 +712,9 @@ impl Chain { /// This also increments the modification index. It will be set to 1 if the /// contract has no entry in the changeset. /// - /// Preconditions: + /// *Preconditions:* /// - Contract must exist. fn changeset_save_state_changes(&mut self, address: ContractAddress, state: &mut MutableState) { - println!("Saving state for {}", address); let mut loader = v1::trie::Loader::new(&[][..]); self.changeset .current_mut() @@ -738,7 +738,55 @@ impl Chain { }); } + /// Saves a new module reference for the contract in the changeset. + /// + /// If the contract already has an entry in the changeset, the old module is + /// replaced. Otherwise, the entry is created and the module is added. + /// + /// Returns the previous module, which is either the one from persistence, + /// or the most recent one from the changeset. + /// + /// *Preconditions:* + /// - Contract must exist. + /// - Module must exist. + fn changeset_save_module_upgrade( + &mut self, + address: ContractAddress, + module_reference: ModuleReference, + ) -> ModuleReference { + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + let contract = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist."); + let old_module_ref = contract.module_reference; + let original_balance = contract.self_balance; + vac.insert(ContractChanges { + module: Some(module_reference), + ..ContractChanges::new(original_balance) + }); + old_module_ref + } + btree_map::Entry::Occupied(mut occ) => { + let changes = occ.get_mut(); + let old_module_ref = match changes.module { + Some(old_module) => old_module, + None => { + self.contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .module_reference + } + }; + changes.module = Some(module_reference); + old_module_ref + } + } + } + /// Returns the modification index for a contract. + /// /// It looks it up in the changeset, and if it isn't there, it will return /// `0`. fn changeset_modification_index(&self, address: ContractAddress) -> u32 { @@ -1086,7 +1134,7 @@ impl Chain { /// Used for handling contract invokes internally. /// - /// Preconditions: + /// *Preconditions:* /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// - `sender` exists @@ -1493,16 +1541,6 @@ impl Chain { .ok_or(ContractInstanceMissing(address)) } - /// Returns a mutable reference to a [`Contract`] from persistence. - fn persistence_get_contract_mut( - &mut self, - address: ContractAddress, - ) -> Result<&mut Contract, ContractInstanceMissing> { - self.contracts - .get_mut(&address) - .ok_or(ContractInstanceMissing(address)) - } - /// Returns an immutable reference to [`AccountInfo`] from persistence. fn persistence_get_account( &self, @@ -2002,22 +2040,22 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { None => InvokeResponse::Failure { kind: v1::InvokeFailure::UpgradeInvalidModuleRef, }, - Some(artifact) => { + Some(module) => { // Charge for the module lookup. energy_after_invoke -= Chain::to_interpreter_energy( - self.chain.lookup_module_cost(artifact), + self.chain.lookup_module_cost(module), ) .energy; - if artifact.export.contains_key( + if module.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), ) { - let instance = self - .chain - .persistence_get_contract_mut(self.address)?; - let old_module_ref = instance.module_reference; - // Update module reference for the instance. - instance.module_reference = module_ref; + // Update module reference in the changeset. + let old_module_ref = + self.chain.changeset_save_module_upgrade( + self.address, + module_ref, + ); // Charge for the initialization cost. energy_after_invoke -= Chain::to_interpreter_energy( @@ -2034,7 +2072,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain_events.push(upgrade_event); InvokeResponse::Success { - new_balance: instance.self_balance, + new_balance: self + .chain + .changeset_contract_balance_unchecked(self.address), data: None, } } else { From 44d8061e89e15a5c80b80557347f47d02eceeca8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Feb 2023 16:06:29 +0100 Subject: [PATCH 056/208] Change return type of `contract_update_aux` to avoid impossible case --- contract-testing/src/lib.rs | 701 ++++++++++++++++++------------------ 1 file changed, 354 insertions(+), 347 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index adcbf082..60f297f6 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,4 +1,3 @@ -use anyhow::bail; use concordium_base::{ base::{Energy, ExchangeRate}, common, @@ -19,7 +18,7 @@ use wasm_chain_integration::{ v1::{ self, trie::{MutableState, PersistentState, SizeCollector}, - ConcordiumAllowedImports, InvokeResponse, ReturnValue, + ConcordiumAllowedImports, InvokeFailure, InvokeResponse, ReturnValue, }, ExecResult, InterpreterEnergy, }; @@ -316,10 +315,10 @@ impl Chain { amount: Amount, from: ContractAddress, to: AccountAddress, - ) -> Result { + ) -> Result { // Ensure the `to` account exists. if !self.persistence_account_exists(to) { - return Err(ContractTransferError::ToMissing); + return Err(TransferError::ToMissing); } // Make the transfer. @@ -345,10 +344,10 @@ impl Chain { amount: Amount, from: ContractAddress, to: ContractAddress, - ) -> Result { + ) -> Result { // Ensure the `to` contract exists. if !self.persistence_contract_exists(to) { - return Err(ContractTransferError::ToMissing); + return Err(TransferError::ToMissing); } // Make the transfer. @@ -374,10 +373,10 @@ impl Chain { amount: Amount, from: AccountAddress, to: ContractAddress, - ) -> Result { + ) -> Result { // Ensure the `to` account exists. if !self.persistence_contract_exists(to) { - return Err(AccountTransferError::ToMissing); + return Err(TransferError::ToMissing); } // Make the transfer. @@ -583,7 +582,8 @@ impl Chain { /// - an [`OutOfEnergy`] error is returned. /// /// Otherwise, it returns the [`Energy`] to be charged for the additional - /// bytes added to contract states. + /// bytes added to contract states. It also returns whether the state of the + /// provided `invoked_contract` was changed. /// /// *Preconditions:* /// - All contracts, modules, accounts referred must exist in persistence. @@ -592,7 +592,9 @@ impl Chain { fn changeset_persist_and_clear( &mut self, remaining_energy: Energy, - ) -> Result { + invoked_contract: ContractAddress, + ) -> Result<(Energy, bool), OutOfEnergy> { + let mut invoked_contract_has_state_changes = false; let changes = self.changeset.current_mut(); // Persist contract changes and collect the total increase in states sizes. let mut collector = SizeCollector::default(); @@ -610,11 +612,6 @@ impl Chain { // One energy per extra byte of state. let energy_for_state_increase = Energy::from(collector.collect()); - println!( - "persist_changes: remaining: {}, state: {}", - remaining_energy, energy_for_state_increase - ); - // Return an error if out of energy, and clear the changeset. if remaining_energy .checked_sub(energy_for_state_increase) @@ -643,6 +640,9 @@ impl Chain { } // Update state. if changes.state.is_some() { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } // Replace with the frozen state we created earlier. contract.state = frozen_states .remove(addr) @@ -666,7 +666,10 @@ impl Chain { // Clear the changeset. self.changeset_clear(); - Ok(energy_for_state_increase) + Ok(( + energy_for_state_increase, + invoked_contract_has_state_changes, + )) } /// Traverses the last checkpoint in the changeset and collects the energy @@ -675,16 +678,24 @@ impl Chain { /// Always clears the changeset. /// /// Returns an [`OutOfEnergy`] error if the energy needed for storing the - /// extra state is larger than `remaining_energy`. Otherwise, it returns - /// the [`Energy`] needed for storing the extra state. + /// extra state is larger than `remaining_energy`. + /// + /// Otherwise, it returns + /// the [`Energy`] needed for storing the extra state. It also returns + /// whether the state of the provided `invoked_contract` has changed. fn changeset_collect_energy_for_state_and_clear( &mut self, remaining_energy: Energy, - ) -> Result { + invoked_contract: ContractAddress, + ) -> Result<(Energy, bool), OutOfEnergy> { + let mut invoked_contract_has_state_changes = false; let mut loader = v1::trie::Loader::new(&[][..]); let mut collector = SizeCollector::default(); - for (_, changes) in self.changeset.current_mut().contracts.iter_mut() { + for (addr, changes) in self.changeset.current_mut().contracts.iter_mut() { if let Some(modified_state) = &mut changes.state { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } modified_state.freeze(&mut loader, &mut collector); } } @@ -700,7 +711,10 @@ impl Chain { { return Err(OutOfEnergy); } - Ok(energy_for_state_increase) + Ok(( + energy_for_state_increase, + invoked_contract_has_state_changes, + )) } /// Saves a mutable state for a contract in the changeset. @@ -829,12 +843,21 @@ impl From for InsufficientBalanceError { fn from(_: UnderflowError) -> Self { InsufficientBalanceError } } -impl From for ContractTransferError { +impl From for TransferError { fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } } -impl From for AccountTransferError { - fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } +struct UpdateAuxResponse { + /// The result from the invoke. + invoke_response: InvokeResponse, + /// Will be `Some` if and only if `invoke_response` is `Success`. + logs: Option, + /// The remaining energy after the invocation. + remaining_energy: InterpreterEnergy, +} + +impl UpdateAuxResponse { + fn is_success(&self) -> bool { matches!(self.invoke_response, InvokeResponse::Success { .. }) } } impl Chain { @@ -1040,17 +1063,17 @@ impl Chain { }, false, loader, - )?; + ); // Handle the result and update the transaction fee. // TODO: Extract to helper function. let res = match res { - v1::InitResult::Success { + Ok(v1::InitResult::Success { logs, return_value: _, /* Ignore return value for now, since our tools do not support * it for inits, currently. */ remaining_energy, mut state, - } => { + }) => { let contract_address = self.create_contract_address(); let energy_used = energy_reserved - Chain::from_interpreter_energy(InterpreterEnergy { @@ -1080,11 +1103,11 @@ impl Chain { transaction_fee, }) } - v1::InitResult::Reject { + Ok(v1::InitResult::Reject { reason, return_value, remaining_energy, - } => { + }) => { let energy_used = energy_reserved - Chain::from_interpreter_energy(InterpreterEnergy { energy: remaining_energy, @@ -1099,10 +1122,10 @@ impl Chain { }, )) } - v1::InitResult::Trap { - error, + Ok(v1::InitResult::Trap { + error: _, // TODO: Should we forward this to the user? remaining_energy, - } => { + }) => { let energy_used = energy_reserved - Chain::from_interpreter_energy(InterpreterEnergy { energy: remaining_energy, @@ -1110,13 +1133,12 @@ impl Chain { transaction_fee += self.calculate_energy_cost(energy_used); Err(ContractInitError::ValidChainError( FailedContractInteraction::Trap { - error, energy_used, transaction_fee, }, )) } - v1::InitResult::OutOfEnergy => { + Ok(v1::InitResult::OutOfEnergy) => { transaction_fee += self.calculate_energy_cost(energy_reserved); Err(ContractInitError::ValidChainError( FailedContractInteraction::OutOfEnergy { @@ -1125,6 +1147,7 @@ impl Chain { }, )) } + Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), }; // Charge the account. // We have to get the account info again because of the borrow checker. @@ -1138,18 +1161,13 @@ impl Chain { /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// - `sender` exists - /// - /// Returns: - /// - Everything the types can encode apart from - /// `Ok(v1::ReceiveResult::Interrupt)` - /// - /// TODO: Change return type, so it can't return Interrupt. - /// TODO: Use proper error types instead of anyhow. + /// - if the contract (`contract_address`) exists, then its `module` must + /// also exist. fn contract_update_aux( &mut self, invoker: AccountAddress, sender: Address, - address: ContractAddress, + contract_address: ContractAddress, entrypoint: OwnedEntrypointName, parameter: Parameter, amount: Amount, @@ -1157,33 +1175,69 @@ impl Chain { // is reserved, it is not subtracted in the chain.accounts map. // Used to handle account balance queries for the invoker account. invoker_amount_reserved_for_nrg: Amount, - mut remaining_energy: Energy, + // Uses [`Interpreter`] energy to avoid rounding issues when converting to and fro + // [`Energy`]. + mut remaining_energy: InterpreterEnergy, chain_events: &mut Vec, - ) -> ExecResult> { + ) -> UpdateAuxResponse { // Move the amount from the sender to the contract, if any. // And get the new self_balance. let instance_self_balance = if amount.micro_ccd() > 0 { - match sender { - Address::Account(sender_account) => { - self.changeset_transfer_account_to_contract(amount, sender_account, address)? - .new_balance_to - } - Address::Contract(sender_contract) => { - self.changeset_transfer_contract_to_contract(amount, sender_contract, address)? - .new_balance_to + let transfer_result = match sender { + Address::Account(sender_account) => self.changeset_transfer_account_to_contract( + amount, + sender_account, + contract_address, + ), + Address::Contract(sender_contract) => self.changeset_transfer_contract_to_contract( + amount, + sender_contract, + contract_address, + ), + }; + match transfer_result { + Ok(new_balances) => new_balances.new_balance_from, + Err(transfer_error) => { + let kind = match transfer_error { + TransferError::InsufficientBalance => InvokeFailure::InsufficientAmount, + TransferError::ToMissing => InvokeFailure::NonExistentContract, + }; + // Return early. + // TODO: Should we charge any energy for this? + return UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { kind }, + logs: None, + remaining_energy, + }; } } } else { - self.changeset_contract_balance_unchecked(address) + match self.changeset_contract_balance(contract_address) { + Some(self_balance) => self_balance, + None => { + // Return early. + // TODO: For the top-most update, we should catch this in `contract_update` and + // return `ContractUpdateError::EntrypointMissing`. + return UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { + kind: InvokeFailure::NonExistentContract, + }, + logs: None, + remaining_energy, + }; + } + } }; // Get the instance and artifact. To be used in several places. - let instance = self.persistence_get_contract(address)?; - let artifact = self.changeset_contract_module(address); + let instance = self + .persistence_get_contract(contract_address) + .expect("Contract known to exist at this point"); + let artifact = self.changeset_contract_module(contract_address); // Subtract the cost of looking up the module - remaining_energy = - Energy::from(remaining_energy.energy - self.lookup_module_cost(&artifact).energy); + remaining_energy = remaining_energy + .subtract(Chain::to_interpreter_energy(self.lookup_module_cost(&artifact)).energy); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -1196,7 +1250,14 @@ impl Chain { } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { OwnedReceiveName::new_unchecked(fallback_receive_name) } else { - bail!(EntrypointDoesNotExist(entrypoint)); + // Return early. + return UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { + kind: InvokeFailure::NonExistentEntrypoint, + }, + logs: None, + remaining_energy, + }; } }; @@ -1208,11 +1269,15 @@ impl Chain { slot_time: self.slot_time, }, invoker, - self_address: address, + self_address: contract_address, self_balance: instance_self_balance, sender, owner: instance.owner, - sender_policies: self.persistence_get_account(invoker)?.policies.clone(), + sender_policies: self + .persistence_get_account(invoker) + .expect("Precondition violation: invoker must exist.") + .policies + .clone(), }, }; @@ -1220,19 +1285,19 @@ impl Chain { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = self.changeset_contract_state(address); + let mut mutable_state = self.changeset_contract_state(contract_address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); // Get the initial result from invoking receive - let res = v1::invoke_receive( + let initial_result = v1::invoke_receive( artifact, receive_ctx, v1::ReceiveInvocation { amount, receive_name: receive_name.as_receive_name(), parameter: parameter.0, - energy: Chain::to_interpreter_energy(remaining_energy), + energy: remaining_energy, }, instance_state, v1::ReceiveParams { @@ -1246,7 +1311,7 @@ impl Chain { // i.e. beyond interrupts. let mut data = ProcessReceiveData { invoker, - address, + address: contract_address, contract_name, amount, invoker_amount_reserved_for_nrg, @@ -1257,15 +1322,85 @@ impl Chain { loader, }; - // Process the receive invocation to the end. - let res = data.process(res); + // Process the receive invocation to the completion. + let result = data.process(initial_result); + let mut new_chain_events = data.chain_events; + + let result = match result { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed: _, /* This only reflects changes since last interrupt, we use + * the changeset later to get a more precise result. */ + return_value, + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: InvokeResponse::Success { + new_balance: self + .changeset_contract_balance_unchecked(contract_address), + data: Some(return_value), + }, + logs: Some(logs), + remaining_energy, + } + } + v1::ReceiveResult::Interrupt { .. } => { + panic!("Internal error: `data.process` returned an interrupt.") + } + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { + kind: InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + logs: None, + remaining_energy, + } + } + v1::ReceiveResult::Trap { + error: _, // TODO: Should we return this to the user? + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { + kind: InvokeFailure::RuntimeError, + }, + logs: None, + remaining_energy, + } + } + v1::ReceiveResult::OutOfEnergy => { + let remaining_energy = InterpreterEnergy::from(0); + UpdateAuxResponse { + invoke_response: InvokeResponse::Failure { + kind: InvokeFailure::RuntimeError, + }, + logs: None, + remaining_energy, + } + } + }, + Err(internal_error) => { + panic!("Internal error: Got interpreter error {}", internal_error) + } + }; // Append the new chain events if the invocation succeeded. - if matches!(res, Ok(v1::ReceiveResult::Success { .. })) { - chain_events.append(&mut data.chain_events); + if result.is_success() { + chain_events.append(&mut new_chain_events); } - res + result } /// Update a contract by calling one of its entrypoints. @@ -1275,7 +1410,7 @@ impl Chain { &mut self, invoker: AccountAddress, sender: Address, - address: ContractAddress, + contract_address: ContractAddress, entrypoint: EntrypointName, parameter: ContractParameter, amount: Amount, @@ -1283,7 +1418,7 @@ impl Chain { ) -> Result { println!( "Updating contract {}, with parameter: {:?}", - address, parameter.0 + contract_address, parameter.0 ); // Ensure the sender exists. @@ -1302,50 +1437,45 @@ impl Chain { // TODO: Should chain events be part of the changeset? let mut chain_events = Vec::new(); - let receive_result = self.contract_update_aux( + let result = self.contract_update_aux( invoker, sender, - address, + contract_address, entrypoint.to_owned(), Parameter(¶meter.0), amount, invoker_amount_reserved_for_nrg, - energy_reserved, + Chain::to_interpreter_energy(energy_reserved), &mut chain_events, ); // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. - let energy_for_state_increase = match receive_result { - Ok(v1::ReceiveResult::Success { - remaining_energy, .. - }) => { - match self.changeset_persist_and_clear(Chain::from_interpreter_energy( - InterpreterEnergy::from(remaining_energy), - )) { - Ok(energy) => energy, - Err(_) => { - return Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }, - )) - } + let (energy_for_state_increase, state_changed) = if result.is_success() { + match self.changeset_persist_and_clear( + Chain::from_interpreter_energy(result.remaining_energy), + contract_address, + ) { + Ok(energy) => energy, + Err(_) => { + return Err(ContractUpdateError::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }); } } - _ => { - // An error occured, so we don't save the changes. Just clear. - self.changeset_clear(); - Energy::from(0) - } + } else { + // An error occured, so we don't save the changes. Just clear. + self.changeset_clear(); + (Energy::from(0), false) }; - let (res, transaction_fee) = self.convert_receive_result( - receive_result, + let (res, transaction_fee) = self.convert_update_aux_response( + result, chain_events, energy_reserved, energy_for_state_increase, + state_changed, ); // Charge the transaction fee irrespective of the result. @@ -1363,7 +1493,7 @@ impl Chain { &mut self, invoker: AccountAddress, sender: Address, - address: ContractAddress, + contract_address: ContractAddress, entrypoint: EntrypointName, parameter: ContractParameter, amount: Amount, @@ -1371,7 +1501,7 @@ impl Chain { ) -> Result { println!( "Invoking contract {}, with parameter: {:?}", - address, parameter.0 + contract_address, parameter.0 ); // Ensure the sender exists. @@ -1387,49 +1517,46 @@ impl Chain { } let mut chain_events = Vec::new(); - let receive_result = self.contract_update_aux( + let result = self.contract_update_aux( invoker, sender, - address, + contract_address, entrypoint.to_owned(), Parameter(¶meter.0), amount, invoker_amount_reserved_for_nrg, - energy_reserved, + Chain::to_interpreter_energy(energy_reserved), &mut chain_events, ); - let energy_for_state_increase = match receive_result { - Ok(v1::ReceiveResult::Success { - remaining_energy, .. - }) => match self.changeset_collect_energy_for_state_and_clear( - Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), + let (energy_for_state_increase, state_changed) = if result.is_success() { + match self.changeset_collect_energy_for_state_and_clear( + Chain::from_interpreter_energy(result.remaining_energy), + contract_address, ) { Ok(energy) => energy, Err(_) => { - return Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }, - )) + return Err(ContractUpdateError::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }); } - }, - _ => { - // An error occured, so we just clear the changeset. - self.changeset_clear(); - Energy::from(0) } + } else { + // An error occured, so we just clear the changeset. + self.changeset_clear(); + (Energy::from(0), false) }; - let (res, _) = self.convert_receive_result( - receive_result, + let (result, _) = self.convert_update_aux_response( + result, chain_events, energy_reserved, energy_for_state_increase, + state_changed, ); - res + result } /// Create an account. Will override existing account if already present. @@ -1565,138 +1692,55 @@ impl Chain { /// The `energy_for_state_increase` is only used if the result was a /// success. /// - /// *Precondition*: - /// - The `receive_result` should never be - /// `Ok(v1::ReceiveResult::Interrupt(_))`. + /// The `state_changed` should refer to whether the state of the top-level + /// contract invoked has changed. + /// + /// *Preconditions*: /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` - fn convert_receive_result( + fn convert_update_aux_response( &self, - receive_result: ExecResult>, + update_aux_response: UpdateAuxResponse, chain_events: Vec, energy_reserved: Energy, energy_for_state_increase: Energy, + state_changed: bool, ) -> ( Result, Amount, ) { - match receive_result { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, + let remaining_energy = Chain::from_interpreter_energy(update_aux_response.remaining_energy); + match update_aux_response.invoke_response { + InvokeResponse::Success { new_balance, data } => { + let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; + let transaction_fee = self.calculate_energy_cost(energy_used); + let result = Ok(SuccessfulContractUpdate { + chain_events, + energy_used, + transaction_fee, + return_value: ContractReturnValue(data.unwrap_or_default()), state_changed, - return_value, - remaining_energy, /* TODO: Could we change this from `u64` to - * `InterpreterEnergy` in chain_integration? */ - } => { - let remaining_energy = - Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - println!( - "reserved: {}, remaining: {}, state: {}", - energy_reserved, remaining_energy, energy_for_state_increase - ); - let energy_used = - energy_reserved - remaining_energy + energy_for_state_increase; - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Ok(SuccessfulContractUpdate { - chain_events, - energy_used, - transaction_fee, - return_value: ContractReturnValue(return_value), - state_changed, - logs, - }), - transaction_fee, - ) - } - v1::ReceiveResult::Interrupt { .. } => { - panic!("Precondition violation: Got `ReceiveResult::Interrupt`.") - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let remaining_energy = - Chain::from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - let energy_used = energy_reserved - remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - }, - )), - transaction_fee, - ) - } - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => { - let remaining_energy = Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - let energy_used = energy_reserved - remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::Trap { - error, - energy_used, - transaction_fee, - }, - )), - transaction_fee, - ) - } - v1::ReceiveResult::OutOfEnergy => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractUpdateError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )), - transaction_fee, - ) - } - }, - Err(e) => match e.downcast::() { - // The user tried to call `contract_update` or `contract_invoke` with an entrypoint - // which does not exist. This is caught up front and nothing is put - // on chain, so the cost is zero. - Ok(err) => ( - Err(ContractUpdateError::EntrypointDoesNotExist(err)), - Amount::zero(), - ), - // An internal precondition has been violated, which caused an error in the - // interpreter. - Err(e) => panic!("Internal error: {}", e), - }, + new_balance, + logs: update_aux_response.logs.unwrap_or_default(), + }); + (result, transaction_fee) + } + InvokeResponse::Failure { kind } => { + let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; + let transaction_fee = self.calculate_energy_cost(energy_used); + let result = Err(ContractUpdateError::ExecutionError { + failure_kind: kind, + energy_used, + transaction_fee, + }); + (result, transaction_fee) + } } } } -/// Errors related to transfers from contract to an account. -#[derive(PartialEq, Eq, Debug, Error)] -enum ContractTransferError { - /// The receiver does not exist. - #[error("The receiver does not exist.")] - ToMissing, - /// The sender does not have sufficient balance. - #[error("The sender does not have sufficient balance.")] - InsufficientBalance, -} - -/// Errors related to "transfers" (fx when updating with an amount) from an -/// account to a contract. +/// Errors related to transfers. #[derive(PartialEq, Eq, Debug, Error)] -enum AccountTransferError { +enum TransferError { /// The receiver does not exist. #[error("The receiver does not exist.")] ToMissing, @@ -1817,11 +1861,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { }, Err(err) => { let kind = match err { - ContractTransferError::ToMissing => { - v1::InvokeFailure::NonExistentAccount + TransferError::ToMissing => { + InvokeFailure::NonExistentAccount } - ContractTransferError::InsufficientBalance => { - v1::InvokeFailure::InsufficientAmount + TransferError::InsufficientBalance => { + InvokeFailure::InsufficientAmount } }; InvokeResponse::Failure { kind } @@ -1897,76 +1941,11 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { Parameter(¶meter), amount, self.invoker_amount_reserved_for_nrg, - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }), + InterpreterEnergy::from(remaining_energy), &mut self.chain_events, ); - let (success, response, energy_after_invoke) = match res { - Ok(r) => match r { - v1::ReceiveResult::Success { - return_value, - remaining_energy, - .. - } => { - println!( - "\t\tInvoke returned with value: {:?}", - return_value - ); - ( - true, - InvokeResponse::Success { - new_balance: self - .chain - .changeset_contract_balance_unchecked(address), - data: Some(return_value), - }, - remaining_energy, - ) - } - v1::ReceiveResult::Interrupt { .. } => { - panic!("Internal error: Should never return on interrupts.") - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => ( - false, - InvokeResponse::Failure { - kind: v1::InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, - }, - remaining_energy, - ), - v1::ReceiveResult::Trap { - remaining_energy, .. - } => ( - false, - InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - remaining_energy, - ), - v1::ReceiveResult::OutOfEnergy => ( - false, - InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - 0, - ), // TODO: What is the correct error here? - }, - Err(_e) => ( - false, - InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - 0, - ), // TODO: Correct energy here? - }; + let success = res.is_success(); // Remove the last state changes if the invocation failed. let state_changed = if !success { @@ -2012,8 +1991,8 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let resume_res = v1::resume_receive( config, - response, - InterpreterEnergy::from(energy_after_invoke), + res.invoke_response, + res.remaining_energy, &mut self.state, state_changed, self.loader, @@ -2027,8 +2006,6 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Add the interrupt event. self.chain_events.push(interrupt_event); - // TODO: Add module to changeset. - // Charge a base cost. let mut energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( @@ -2038,7 +2015,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let response = match self.chain.modules.get(&module_ref) { None => InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + kind: InvokeFailure::UpgradeInvalidModuleRef, }, Some(module) => { // Charge for the module lookup. @@ -2079,7 +2056,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } else { InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidContractName, + kind: InvokeFailure::UpgradeInvalidContractName, } } } @@ -2132,7 +2109,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } None => InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, + kind: InvokeFailure::NonExistentAccount, }, }; @@ -2158,7 +2135,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let response = match self.chain.changeset_contract_balance(address) { None => InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, + kind: InvokeFailure::NonExistentContract, }, Some(bal) => InvokeResponse::Success { // Balance of contract querying. Won't change. Notice the @@ -2311,10 +2288,7 @@ pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. #[error("failed due to a valid chain error: {:?}", 0)] ValidChainError(FailedContractInteraction), - /// An error thrown by the interpreter. - #[error("an error occured in the interpreter: {:?}", 0)] - InterpreterError(#[from] anyhow::Error), - /// Module has not been deployed in test environment. + /// Module has not been deployed in the test environment. #[error("module {:?} does not exist", 0.0)] ModuleDoesNotExist(#[from] ModuleMissing), /// Account has not been created in test environment. @@ -2325,25 +2299,38 @@ pub enum ContractInitError { InsufficientFunds, } -/// Errors that can occur during a contract update or invocation. +/// Errors that can occur during a [`Chain::contract_update]` or +/// [`Chain::contract_invoke`] call. +/// +/// There are two categories of errors here: +/// - `ExecutionError` and `OutOfEnergy` can occur if the preconditions for the +/// function is valid, and a contract is executed. +/// - The rest represent incorrect usage of the function, where some +/// precondition wasn't met. #[derive(Debug, Error)] pub enum ContractUpdateError { /// Update failed for a reason that also exists on the chain. - #[error("failed due to a valid chain error: {:?}", 0)] - ValidChainError(FailedContractInteraction), - /// An error thrown by the wasm interpreter - #[error("an error occured in the interpreter: {:?}", 0)] - InterpreterError(#[from] anyhow::Error), + #[error("failed during execution")] + ExecutionError { + failure_kind: InvokeFailure, + energy_used: Energy, + transaction_fee: Amount, + }, + #[error("ran out of energy")] + OutOfEnergy { + energy_used: Energy, + transaction_fee: Amount, + }, /// Module has not been deployed in test environment. #[error("module {:?} does not exist", 0.0)] ModuleDoesNotExist(#[from] ModuleMissing), - /// Contract instance has not been initialized in test environment. + /// Contract instance has not been initialized in the test environment. #[error("instance {} does not exist", 0.0)] InstanceDoesNotExist(#[from] ContractInstanceMissing), /// Entrypoint does not exist and neither does the fallback entrypoint. #[error("entrypoint does not exist")] EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), - /// The invoker account has not been created in test environment. + /// The invoker account has not been created in the test environment. #[error("invoker account {} does not exist", 0.0)] InvokerDoesNotExist(#[from] AccountMissing), /// The sender does not exist in the test environment. @@ -2354,7 +2341,8 @@ pub enum ContractUpdateError { InsufficientFunds, } -/// Represents a failed interaction, i.e. update or invocation, of a contract. +/// Represents a failed contract interaction, i.e. an initialization, update, or +/// invocation. #[derive(Debug)] pub enum FailedContractInteraction { /// The contract rejected. @@ -2370,8 +2358,6 @@ pub enum FailedContractInteraction { }, /// The contract trapped. Trap { - /// The error message. - error: anyhow::Error, /// The amount of energy used before rejecting. energy_used: Energy, /// The transaction fee to be paid by the invoker for the interaction. @@ -2491,6 +2477,8 @@ pub struct SuccessfulContractUpdate { pub return_value: ContractReturnValue, /// Whether the state was changed. pub state_changed: bool, + /// The new balance of the smart contract. + pub new_balance: Amount, /// The logs produced since the last interrupt. pub logs: v0::Logs, } @@ -2926,12 +2914,12 @@ mod tests { ); match res_update { - Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { - reason, + Err(ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::ContractReject { code, .. }, transaction_fee, .. - })) => { - assert_eq!(reason, -3); // Corresponds to contract error TransactionErrorAccountMissing + }) => { + assert_eq!(code, -3); // The custom contract error code for missing account. assert_eq!( chain.persistence_account_balance(ACC_0), Some( @@ -4195,15 +4183,24 @@ mod tests { ); // Check the return value manually returned by the contract. - assert!(matches!(res_update_upgrade, - Err(ContractUpdateError::ValidChainError(FailedContractInteraction::Reject { reason, ..})) - if reason == -1)); + match res_update_upgrade { + Err(ContractUpdateError::ExecutionError { failure_kind, .. }) => match failure_kind + { + InvokeFailure::ContractReject { code, .. } if code == -1 => (), + _ => panic!("Expected ContractReject with code == -1"), + }, + _ => panic!("Expected Err(ContractUpdateError::ExecutionError)"), + } // Assert that the new_feature entrypoint doesn't exist since the upgrade // failed. assert!(matches!( - res_update_new_feature, - Err(ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint))) if entrypoint.to_string() == "new_feature" + res_update_new_feature, + Err(ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + }) )); } @@ -4304,18 +4301,28 @@ mod tests { assert!(matches!(res_update_old_feature_0.chain_events[..], [ ChainEvent::Updated { .. } ])); - assert!( - matches!(res_update_new_feature_0, ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint)) if entrypoint.to_string() == "new_feature") - ); + assert!(matches!( + res_update_new_feature_0, + ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + } + )); assert!(matches!(res_update_upgrade.chain_events[..], [ ChainEvent::Interrupted { .. }, ChainEvent::Upgraded { .. }, ChainEvent::Resumed { .. }, ChainEvent::Updated { .. }, ])); - assert!( - matches!(res_update_old_feature_1, ContractUpdateError::EntrypointDoesNotExist(EntrypointDoesNotExist(entrypoint)) if entrypoint.to_string() == "old_feature") - ); + assert!(matches!( + res_update_old_feature_1, + ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + } + )); assert!(matches!(res_update_new_feature_1.chain_events[..], [ ChainEvent::Updated { .. } ])); From c8dda1b33e5b46b800e0ad9d93f59bc08fd9945f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Feb 2023 16:14:01 +0100 Subject: [PATCH 057/208] Remove struct with unused field The `new_balance_to` field was never used. --- contract-testing/src/lib.rs | 58 ++++++++++++------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 60f297f6..3ba4d0e4 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -306,7 +306,7 @@ impl Chain { } /// Make a transfer from a contract to an account in the changeset. - /// Returns the new balances of both. + /// Returns the new balance of `from`. /// /// Precondition: /// - Assumes that `from` contract exists. @@ -315,27 +315,23 @@ impl Chain { amount: Amount, from: ContractAddress, to: AccountAddress, - ) -> Result { + ) -> Result { // Ensure the `to` account exists. if !self.persistence_account_exists(to) { return Err(TransferError::ToMissing); } // Make the transfer. - let new_balance_from = + let new_balance = self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - let new_balance_to = self - .changeset_change_account_balance(to, AmountDelta::Positive(amount)) + self.changeset_change_account_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); - Ok(NewBalances { - new_balance_from, - new_balance_to, - }) + Ok(new_balance) } /// Make a transfer between contracts in the changeset. - /// Returns the new balances of both. + /// Returns the new balance of `from`. /// /// Precondition: /// - Assumes that `from` contract exists. @@ -344,23 +340,19 @@ impl Chain { amount: Amount, from: ContractAddress, to: ContractAddress, - ) -> Result { + ) -> Result { // Ensure the `to` contract exists. if !self.persistence_contract_exists(to) { return Err(TransferError::ToMissing); } // Make the transfer. - let new_balance_from = + let new_balance = self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - let new_balance_to = self - .changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); - Ok(NewBalances { - new_balance_from, - new_balance_to, - }) + Ok(new_balance) } /// Make a transfer from an account to a contract in the changeset. @@ -373,23 +365,18 @@ impl Chain { amount: Amount, from: AccountAddress, to: ContractAddress, - ) -> Result { + ) -> Result { // Ensure the `to` account exists. if !self.persistence_contract_exists(to) { return Err(TransferError::ToMissing); } // Make the transfer. - let new_balance_from = + let new_balance = self.changeset_change_account_balance(from, AmountDelta::Negative(amount))?; - let new_balance_to = self - .changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); - - Ok(NewBalances { - new_balance_from, - new_balance_to, - }) + Ok(new_balance) } // TODO: Should we handle overflows explicitly? @@ -821,15 +808,6 @@ impl Chain { fn changeset_rollback(&mut self) { self.changeset.rollback(); } } -/// The resulting new balances after making a transfer between an accounts or -/// contracts. -struct NewBalances { - /// The new balance of the sender. - new_balance_from: Amount, - /// The new balance of the receiver. - new_balance_to: Amount, -} - /// A transfer of [`Amount`]s failed because the sender had insufficient /// balance. #[derive(Debug)] @@ -1196,7 +1174,7 @@ impl Chain { ), }; match transfer_result { - Ok(new_balances) => new_balances.new_balance_from, + Ok(new_balance_from) => new_balance_from, Err(transfer_error) => { let kind = match transfer_error { TransferError::InsufficientBalance => InvokeFailure::InsufficientAmount, @@ -1855,9 +1833,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.address, to, ) { - Ok(new_balances) => InvokeResponse::Success { - new_balance: new_balances.new_balance_from, - data: None, + Ok(new_balance) => InvokeResponse::Success { + new_balance, + data: None, }, Err(err) => { let kind = match err { From cfb3b245f190b5257e3951b10b603399f60ee914 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Feb 2023 16:31:02 +0100 Subject: [PATCH 058/208] Use preexisting types for parameters and return values --- contract-testing/src/lib.rs | 210 +++++++++++++++++------------------- 1 file changed, 96 insertions(+), 114 deletions(-) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 3ba4d0e4..3863339c 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1007,7 +1007,7 @@ impl Chain { sender: AccountAddress, module_reference: ModuleReference, contract_name: ContractName, - parameter: ContractParameter, + parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, ) -> Result { @@ -1390,7 +1390,7 @@ impl Chain { sender: Address, contract_address: ContractAddress, entrypoint: EntrypointName, - parameter: ContractParameter, + parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, ) -> Result { @@ -1473,7 +1473,7 @@ impl Chain { sender: Address, contract_address: ContractAddress, entrypoint: EntrypointName, - parameter: ContractParameter, + parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, ) -> Result { @@ -1695,7 +1695,7 @@ impl Chain { chain_events, energy_used, transaction_fee, - return_value: ContractReturnValue(data.unwrap_or_default()), + return_value: data.unwrap_or_default(), state_changed, new_balance, logs: update_aux_response.logs.unwrap_or_default(), @@ -2452,7 +2452,7 @@ pub struct SuccessfulContractUpdate { /// Cost of transaction. pub transaction_fee: Amount, /// The returned value. - pub return_value: ContractReturnValue, + pub return_value: ReturnValue, /// Whether the state was changed. pub state_changed: bool, /// The new balance of the smart contract. @@ -2516,24 +2516,6 @@ pub struct SuccessfulContractInit { pub transaction_fee: Amount, } -/// A value returned by a contract. -#[derive(Debug)] -pub struct ContractReturnValue(Vec); - -/// The parameter to a contract. -pub struct ContractParameter(pub Vec); - -impl ContractParameter { - /// Create an empty [`Self`]. - pub fn empty() -> Self { Self(Vec::new()) } - - /// Create a [`Self`] from a byte array. - pub fn from_bytes(bytes: Vec) -> Self { Self(bytes) } - - /// Create a [`Self`] by serializing a `T`. - pub fn from_typed(parameter: &T) -> Self { Self(to_bytes(parameter)) } -} - #[cfg(test)] mod tests { use super::*; @@ -2592,7 +2574,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - ContractParameter::from_bytes(vec![0u8]), + OwnedParameter::from_bytes(vec![0u8]), Amount::zero(), Energy::from(10000), ) @@ -2619,7 +2601,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - ContractParameter::from_bytes(vec![99u8]), // Invalid param + OwnedParameter::from_bytes(vec![99u8]), // Invalid param Amount::zero(), Energy::from(10000), ) @@ -2654,7 +2636,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - ContractParameter::from_bytes(vec![0u8]), // Starts as 0 + OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 Amount::zero(), Energy::from(10000), ) @@ -2666,7 +2648,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("set"), - ContractParameter::from_bytes(vec![1u8]), // Updated to 1 + OwnedParameter::from_bytes(vec![1u8]), // Updated to 1 Amount::zero(), Energy::from(10000), ) @@ -2678,7 +2660,7 @@ mod tests { Address::Contract(res_init.contract_address), // Invoke with a contract as sender. res_init.contract_address, EntrypointName::new_unchecked("get"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2697,7 +2679,7 @@ mod tests { assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); // Assert that the updated state is persisted. - assert_eq!(res_invoke_get.return_value.0, [1u8]); + assert_eq!(res_invoke_get.return_value, [1u8]); } /// Test that updates and invocations where the sender is missing fail. @@ -2719,7 +2701,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - ContractParameter::from_bytes(vec![0u8]), // Starts as 0 + OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 Amount::zero(), Energy::from(10000), ) @@ -2730,7 +2712,7 @@ mod tests { missing_account, res_init.contract_address, EntrypointName::new_unchecked("get"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ); @@ -2740,7 +2722,7 @@ mod tests { missing_account, res_init.contract_address, EntrypointName::new_unchecked("get"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ); @@ -2750,7 +2732,7 @@ mod tests { missing_contract, res_init.contract_address, EntrypointName::new_unchecked("get"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ); @@ -2760,7 +2742,7 @@ mod tests { missing_contract, res_init.contract_address, EntrypointName::new_unchecked("get"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ); @@ -2802,7 +2784,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2814,7 +2796,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&ACC_1), + OwnedParameter::new(&ACC_1), transfer_amount, Energy::from(10000), ) @@ -2826,7 +2808,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2854,9 +2836,9 @@ mod tests { }]); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); - assert_eq!(res_update.return_value.0, [2, 0, 0, 0]); + assert_eq!(res_update.return_value, [2, 0, 0, 0]); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, [2, 0, 0, 0]); + assert_eq!(res_view.return_value, [2, 0, 0, 0]); } #[test] @@ -2875,7 +2857,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2886,7 +2868,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&ACC_1), // We haven't created ACC_1. + OwnedParameter::new(&ACC_1), // We haven't created ACC_1. transfer_amount, Energy::from(100000), ); @@ -2927,7 +2909,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2939,7 +2921,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("recurse"), - ContractParameter::from_typed(&10u32), + OwnedParameter::new(&10u32), Amount::zero(), Energy::from(1000000), ) @@ -2951,7 +2933,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -2970,9 +2952,9 @@ mod tests { assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); let expected_res = 10 + 7 + 11 + 3 + 7 + 11; - assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); + assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); + assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); } #[test] @@ -2992,7 +2974,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3004,7 +2986,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("inc-fail-on-zero"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000000), ) @@ -3016,7 +2998,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3033,9 +3015,9 @@ mod tests { ); assert!(res_update.state_changed); let expected_res = 2u32.pow(input_param) - 1; - assert_eq!(res_update.return_value.0, u32::to_le_bytes(expected_res)); + assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, u32::to_le_bytes(expected_res)); + assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); } #[test] @@ -3055,7 +3037,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3066,7 +3048,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_integrate_other"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3080,7 +3062,7 @@ mod tests { Address::Account(ACC_0), res_init_0.contract_address, EntrypointName::new_unchecked("mutate_and_forward"), - ContractParameter::from_typed(¶m), + OwnedParameter::new(¶m), transfer_amount, Energy::from(100000), ) @@ -3105,7 +3087,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_fib"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3117,7 +3099,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - ContractParameter::from_typed(&6u64), + OwnedParameter::new(&6u64), Amount::zero(), Energy::from(4000000), ) @@ -3129,7 +3111,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("view"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3148,9 +3130,9 @@ mod tests { assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); let expected_res = u64::to_le_bytes(13); - assert_eq!(res_update.return_value.0, expected_res); + assert_eq!(res_update.return_value, expected_res); // Assert that the updated state is persisted. - assert_eq!(res_view.return_value.0, expected_res); + assert_eq!(res_view.return_value, expected_res); } #[test] @@ -3189,7 +3171,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3207,7 +3189,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3246,7 +3228,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3267,7 +3249,7 @@ mod tests { Address::Account(ACC_1), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), update_amount, energy_limit, ) @@ -3311,7 +3293,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), amount_to_send, // Make sure the contract has CCD to transfer. Energy::from(10000), ) @@ -3331,7 +3313,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(10000), ) @@ -3378,7 +3360,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3396,7 +3378,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3439,7 +3421,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3454,7 +3436,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3500,7 +3482,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3511,7 +3493,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), init_amount, // Set up another contract with `init_amount` balance Energy::from(10000), ) @@ -3526,7 +3508,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3560,7 +3542,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), init_amount, Energy::from(10000), ) @@ -3575,7 +3557,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), update_amount, Energy::from(100000), ) @@ -3612,7 +3594,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), init_amount, Energy::from(10000), ) @@ -3633,7 +3615,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), update_amount, Energy::from(100000), ) @@ -3669,7 +3651,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3684,7 +3666,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3719,7 +3701,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3734,7 +3716,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -3773,7 +3755,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_a"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3787,7 +3769,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("bump"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(100000), ) @@ -3800,7 +3782,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("newfun"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(100000), ) @@ -3844,7 +3826,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3856,7 +3838,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(100000), ) @@ -3901,7 +3883,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3913,7 +3895,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(100000), ) @@ -3955,7 +3937,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -3967,7 +3949,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(100000), ) @@ -4006,7 +3988,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4020,7 +4002,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(100000), ) @@ -4076,7 +4058,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4091,7 +4073,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&input_param), + OwnedParameter::new(&input_param), Amount::zero(), Energy::from(1000000), ) @@ -4134,7 +4116,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4145,7 +4127,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(1000000), ); @@ -4155,7 +4137,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(1000000), ); @@ -4210,7 +4192,7 @@ mod tests { ACC_0, res_deploy_0.module_reference, ContractName::new_unchecked("init_contract"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4222,7 +4204,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("old_feature"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(1000000), ) @@ -4234,7 +4216,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(1000000), ) @@ -4246,7 +4228,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - ContractParameter::from_typed(&res_deploy_1.module_reference), + OwnedParameter::new(&res_deploy_1.module_reference), Amount::zero(), Energy::from(1000000), ) @@ -4258,7 +4240,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("old_feature"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(1000000), ) @@ -4270,7 +4252,7 @@ mod tests { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("new_feature"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(1000000), ) @@ -4336,7 +4318,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_a"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4347,7 +4329,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_b"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4375,7 +4357,7 @@ mod tests { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - ContractParameter::from_typed(¶meter), + OwnedParameter::new(¶meter), // We supply one microCCD as we expect a trap // (see contract for details). Amount::from_micro_ccd(1), @@ -4412,7 +4394,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_a"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4423,7 +4405,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_b"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4451,7 +4433,7 @@ mod tests { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - ContractParameter::from_typed(¶meter), + OwnedParameter::new(¶meter), // We supply zero microCCD as we're instructing the contract to not expect // state modifications. Also, the contract does not expect // errors, i.e., a trap signal from underlying invocations. @@ -4488,7 +4470,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_a"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4499,7 +4481,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_b"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4511,7 +4493,7 @@ mod tests { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - ContractParameter::from_typed(&ACC_1), + OwnedParameter::new(&ACC_1), // We supply three micro CCDs as we're instructing the contract to carry out a // transfer instead of a call. See the contract for // details. @@ -4546,7 +4528,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_a"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4557,7 +4539,7 @@ mod tests { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_b"), - ContractParameter::empty(), + OwnedParameter::empty(), Amount::zero(), Energy::from(10000), ) @@ -4585,7 +4567,7 @@ mod tests { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - ContractParameter::from_typed(¶meter), + OwnedParameter::new(¶meter), // We supply four CCDs as we're instructing the contract to expect state // modifications being made from the 'inner' contract A // call to be in effect when returned to the caller (a.a_modify_proxy). From 2a39793dab42c48b1a1e49bd3d6f276be918aceb Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Feb 2023 10:25:30 +0100 Subject: [PATCH 059/208] Move constants to separate module --- contract-testing/src/constants.rs | 22 ++++++++++++++++++++++ contract-testing/src/lib.rs | 26 ++++++-------------------- 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 contract-testing/src/constants.rs diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs new file mode 100644 index 00000000..65e86adc --- /dev/null +++ b/contract-testing/src/constants.rs @@ -0,0 +1,22 @@ +//! Various constants. + +use concordium_base::base::Energy; + +// Energy constants from Cost.hs in concordium-base. + +/// Cost of querying the account balance from a within smart contract instance. +pub(crate) const CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST: Energy = Energy { energy: 200 }; + +/// Cost of querying the contract balance from a within smart contract instance. +pub(crate) const CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST: Energy = Energy { energy: 200 }; + +/// Cost of querying the current exchange rates from a within smart contract +/// instance. +pub(crate) const CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST: Energy = Energy { energy: 100 }; + +/// The base cost of initializing a contract instance to cover administrative +/// costs. Even if no code is run and no instance created. +pub(crate) const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; + +/// Cost of creating an empty smart contract instance. +pub(crate) const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: Energy = Energy { energy: 200 }; diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 3863339c..d47c56cb 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -23,6 +23,7 @@ use wasm_chain_integration::{ ExecResult, InterpreterEnergy, }; use wasm_transform::artifact; +mod constants; /// A V1 artifact, with concrete types for the generic parameters. type ContractModule = artifact::Artifact; @@ -31,21 +32,6 @@ type ContractModule = artifact::Artifact ProcessReceiveData<'a, 'b> { // Charge a base cost. let mut energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( - INITIALIZE_CONTRACT_INSTANCE_BASE_COST, + constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST, ) .energy; @@ -2014,7 +2000,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Charge for the initialization cost. energy_after_invoke -= Chain::to_interpreter_energy( - INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, ) .energy; @@ -2093,7 +2079,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( - CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, ) .energy; @@ -2127,7 +2113,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( - CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, ) .energy; @@ -2157,7 +2143,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { let energy_after_invoke = remaining_energy - Chain::to_interpreter_energy( - CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, + constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, ) .energy; From dc5738c184bad41a38f7aa0efd043a0caa451646 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Feb 2023 16:38:54 +0100 Subject: [PATCH 060/208] Split into multiple files - Hide details (types and functions) for executing contract entrypoints in the `invocation` module. - Which is split into three files: `types.rs`, `impls.rs`, `mod.rs` - Split top-level file into `types.rs`, `impls.rs` and `lib.rs`. --- contract-testing/src/impls.rs | 768 +++++++ contract-testing/src/invocation/impls.rs | 1382 ++++++++++++ contract-testing/src/invocation/mod.rs | 18 + contract-testing/src/invocation/types.rs | 121 ++ contract-testing/src/lib.rs | 2519 +--------------------- contract-testing/src/types.rs | 325 +++ 6 files changed, 2620 insertions(+), 2513 deletions(-) create mode 100644 contract-testing/src/impls.rs create mode 100644 contract-testing/src/invocation/impls.rs create mode 100644 contract-testing/src/invocation/mod.rs create mode 100644 contract-testing/src/invocation/types.rs create mode 100644 contract-testing/src/types.rs diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs new file mode 100644 index 00000000..63634cf9 --- /dev/null +++ b/contract-testing/src/impls.rs @@ -0,0 +1,768 @@ +use concordium_base::{ + base::Energy, + common, + contracts_common::{ + AccountAddress, Address, Amount, ChainMetadata, ContractAddress, ContractName, + EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, SlotTime, + Timestamp, + }, + smart_contracts::{ModuleSource, WasmModule, WasmVersion}, + transactions::{self, cost}, +}; +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; +use wasm_chain_integration::{v0, v1, InterpreterEnergy}; + +use crate::{ + invocation::{ContractInvocation, UpdateAuxResponse}, + types::*, +}; + +impl Default for Chain { + fn default() -> Self { Self::new() } +} + +impl Chain { + /// Create a new [`Self`] where all the configurable parameters are + /// provided. + pub fn new_with_time_and_rates( + slot_time: SlotTime, + micro_ccd_per_euro: ExchangeRate, + euro_per_energy: ExchangeRate, + ) -> Self { + Self { + slot_time, + micro_ccd_per_euro, + euro_per_energy, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + contracts: BTreeMap::new(), + next_contract_index: 0, + } + } + + /// Create a new [`Self`] with a specified `slot_time` where + /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. + pub fn new_with_time(slot_time: SlotTime) -> Self { + Self { + slot_time, + ..Self::new() + } + } + + /// Create a new [`Self`] where + /// - `slot_time` defaults to `0`, + /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. + pub fn new() -> Self { + Self::new_with_time_and_rates( + Timestamp::from_timestamp_millis(0), + ExchangeRate::new_unchecked(147235241, 1), + ExchangeRate::new_unchecked(1, 50000), + ) + } + + /// Helper function that handles the actual logic of deploying the module + /// bytes. + /// + /// Parameters: + /// - `sender`: the sender account. + /// - `module`: the raw wasm module (i.e. **without** the contract version + /// and module length bytes (8 bytes total)). + fn module_deploy_aux( + &mut self, + sender: AccountAddress, + wasm_module: WasmModule, + ) -> Result { + // Deserialize as wasm module (artifact) + let artifact = wasm_transform::utils::instantiate_with_metering::( + &v1::ConcordiumAllowedImports { + support_upgrade: true, + }, + wasm_module.source.as_ref(), + )?; + + // Calculate transaction fee of deployment + let energy = { + // +1 for the tag, +8 for size and version + let payload_size = 1 + + 8 + + wasm_module.source.size() + + transactions::construct::TRANSACTION_HEADER_SIZE; + let number_of_sigs = self.persistence_get_account(sender)?.signature_count; + let base_cost = cost::base_cost(payload_size, number_of_sigs); + let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); + base_cost + deploy_module_cost + }; + let transaction_fee = self.calculate_energy_cost(energy); + println!( + "Deploying module with size {}, resulting in {} NRG.", + wasm_module.source.size(), + energy + ); + + // Try to subtract cost for account + let account = self.persistence_get_account_mut(sender)?; + if account.balance < transaction_fee { + return Err(DeployModuleError::InsufficientFunds); + }; + account.balance -= transaction_fee; + + // Save the module TODO: Use wasm_module.get_module_ref() and find a proper way + // to convert ModuleRef to ModuleReference. + let module_reference = { + let mut hasher = Sha256::new(); + hasher.update(wasm_module.source.as_ref()); + let hash: [u8; 32] = hasher.finalize().into(); + ModuleReference::from(hash) + }; + // Ensure module hasn't been deployed before. + if self.modules.contains_key(&module_reference) { + return Err(DeployModuleError::DuplicateModule(module_reference)); + } + self.modules.insert(module_reference, Arc::new(artifact)); + Ok(SuccessfulModuleDeployment { + module_reference, + energy, + transaction_fee, + }) + } + + /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version + /// bytes and 4 module length bytes. + /// The module still has to a valid V1 smart contract module. + /// + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. + pub fn module_deploy_wasm_v1>( + &mut self, + sender: AccountAddress, + module_path: P, + ) -> Result { + // Load file + let file_contents = std::fs::read(module_path)?; + let wasm_module = WasmModule { + version: WasmVersion::V1, + source: ModuleSource::from(file_contents), + }; + self.module_deploy_aux(sender, wasm_module) + } + + /// Deploy a v1 wasm module as it is output from `cargo concordium build`, + /// i.e. **including** the prefix of 4 version bytes and 4 module length + /// bytes. + /// + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. + pub fn module_deploy_v1>( + &mut self, + sender: AccountAddress, + module_path: P, + ) -> Result { + // Load file + let file_contents = std::fs::read(module_path)?; + let mut cursor = std::io::Cursor::new(file_contents); + let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor)?; + + if wasm_module.version != WasmVersion::V1 { + return Err(DeployModuleError::UnsupportedModuleVersion( + wasm_module.version, + )); + } + self.module_deploy_aux(sender, wasm_module) + } + + /// Initialize a contract. + /// + /// - `sender`: The account paying for the transaction. Will also become the + /// owner of the instance created. + /// - `module_reference`: The reference to the a module that has already + /// been deployed. + /// - `contract_name`: Name of the contract to initialize. + /// - `parameter`: Parameter provided to the init method. + /// - `amount`: The initial balance of the contract. Subtracted from the + /// `sender` account. + /// - `energy_reserved`: Amount of energy reserved for executing the init + /// method. + pub fn contract_init( + &mut self, + sender: AccountAddress, + module_reference: ModuleReference, + contract_name: ContractName, + parameter: OwnedParameter, + amount: Amount, + energy_reserved: Energy, + ) -> Result { + // Lookup artifact + let artifact = self.persistence_contract_module(module_reference)?; + let mut transaction_fee = self.calculate_energy_cost(lookup_module_cost(&artifact)); + // Get the account and check that it has sufficient balance to pay for the + // reserved_energy and amount. + let account_info = self.persistence_get_account(sender)?; + if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { + return Err(ContractInitError::InsufficientFunds); + } + // Construct the context. + let init_ctx = v0::InitContext { + metadata: ChainMetadata { + slot_time: self.slot_time, + }, + init_origin: sender, + sender_policies: account_info.policies.clone(), + }; + // Initialize contract + let mut loader = v1::trie::Loader::new(&[][..]); + let res = v1::invoke_init( + artifact, + init_ctx, + v1::InitInvocation { + amount, + init_name: contract_name.get_chain_name(), + parameter: ¶meter.0, + energy: to_interpreter_energy(energy_reserved), + }, + false, + loader, + ); + // Handle the result and update the transaction fee. + // TODO: Extract to helper function. + let res = match res { + Ok(v1::InitResult::Success { + logs, + return_value: _, /* Ignore return value for now, since our tools do not support + * it for inits, currently. */ + remaining_energy, + mut state, + }) => { + let contract_address = self.create_contract_address(); + let energy_used = energy_reserved + - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + transaction_fee += self.calculate_energy_cost(energy_used); + + let mut collector = v1::trie::SizeCollector::default(); + + let contract_instance = Contract { + module_reference, + contract_name: contract_name.to_owned(), + state: state.freeze(&mut loader, &mut collector), // TODO: Charge for storage. + owner: sender, + self_balance: amount, + }; + + // Save the contract instance + self.contracts.insert(contract_address, contract_instance); + // Subtract the from the invoker. + self.persistence_get_account_mut(sender)?.balance -= amount; + + Ok(SuccessfulContractInit { + contract_address, + logs, + energy_used, + transaction_fee, + }) + } + Ok(v1::InitResult::Reject { + reason, + return_value, + remaining_energy, + }) => { + let energy_used = energy_reserved + - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + }, + )) + } + Ok(v1::InitResult::Trap { + error: _, // TODO: Should we forward this to the user? + remaining_energy, + }) => { + let energy_used = energy_reserved + - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + transaction_fee += self.calculate_energy_cost(energy_used); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Trap { + energy_used, + transaction_fee, + }, + )) + } + Ok(v1::InitResult::OutOfEnergy) => { + transaction_fee += self.calculate_energy_cost(energy_reserved); + Err(ContractInitError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee, + }, + )) + } + Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), + }; + // Charge the account. + // We have to get the account info again because of the borrow checker. + self.persistence_get_account_mut(sender)?.balance -= transaction_fee; + res + } + + /// Update a contract by calling one of its entrypoints. + /// + /// If successful, any changes will be saved. + pub fn contract_update( + &mut self, + invoker: AccountAddress, + sender: Address, + contract_address: ContractAddress, + entrypoint: EntrypointName, + parameter: OwnedParameter, + amount: Amount, + energy_reserved: Energy, + ) -> Result { + println!( + "Updating contract {}, with parameter: {:?}", + contract_address, parameter.0 + ); + + // Ensure the sender exists. + if !self.persistence_address_exists(sender) { + return Err(ContractUpdateError::SenderDoesNotExist(sender)); + } + + // Ensure account exists and can pay for the reserved energy and amount + // TODO: Could we just remove this amount in the changeset and then put back the + // to_ccd(remaining_energy) afterwards? + let account_info = self.persistence_get_account(invoker)?; + let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + if account_info.balance < invoker_amount_reserved_for_nrg + amount { + return Err(ContractUpdateError::InsufficientFunds); + } + + // TODO: Should chain events be part of the changeset? + let (changeset, result, chain_events) = ContractInvocation::execute( + &self, + invoker, + sender, + contract_address, + entrypoint.to_owned(), + Parameter(¶meter.0), + amount, + invoker_amount_reserved_for_nrg, + energy_reserved, + ); + + // Get the energy to be charged for extra state bytes. Or return an error if out + // of energy. + let (energy_for_state_increase, state_changed) = if result.is_success() { + match changeset.persist( + from_interpreter_energy(result.remaining_energy), + contract_address, + &mut self.accounts, + &mut self.contracts, + ) { + Ok(energy) => energy, + Err(_) => { + return Err(ContractUpdateError::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }); + } + } + } else { + // An error occured, so no energy should be charged for storing the state. + (Energy::from(0), false) + }; + + let (res, transaction_fee) = self.convert_update_aux_response( + result, + chain_events, + energy_reserved, + energy_for_state_increase, + state_changed, + ); + + // Charge the transaction fee irrespective of the result. + // TODO: If we charge up front, then we should return to_ccd(remaining_energy) + // here instead. + self.persistence_get_account_mut(invoker)?.balance -= transaction_fee; + res + } + + /// Invoke a contract by calling an entrypoint. + /// + /// Similar to [`contract_update`] except that all changes are discarded + /// afterwards. Typically used for "view" functions. + pub fn contract_invoke( + &mut self, + invoker: AccountAddress, + sender: Address, + contract_address: ContractAddress, + entrypoint: EntrypointName, + parameter: OwnedParameter, + amount: Amount, + energy_reserved: Energy, + ) -> Result { + println!( + "Invoking contract {}, with parameter: {:?}", + contract_address, parameter.0 + ); + + // Ensure the sender exists. + if !self.persistence_address_exists(sender) { + return Err(ContractUpdateError::SenderDoesNotExist(sender)); + } + + // Ensure account exists and can pay for the reserved energy and amount + let account_info = self.persistence_get_account(invoker)?; + let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + if account_info.balance < invoker_amount_reserved_for_nrg + amount { + return Err(ContractUpdateError::InsufficientFunds); + } + + let (changeset, result, chain_events) = ContractInvocation::execute( + &self, + invoker, + sender, + contract_address, + entrypoint.to_owned(), + Parameter(¶meter.0), + amount, + invoker_amount_reserved_for_nrg, + energy_reserved, + ); + + let (energy_for_state_increase, state_changed) = if result.is_success() { + match changeset.collect_energy_for_state( + from_interpreter_energy(result.remaining_energy), + contract_address, + ) { + Ok(energy) => energy, + Err(_) => { + return Err(ContractUpdateError::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }); + } + } + } else { + // An error occured, so no energy should be charged for storing state. + (Energy::from(0), false) + }; + + let (result, _) = self.convert_update_aux_response( + result, + chain_events, + energy_reserved, + energy_for_state_increase, + state_changed, + ); + + result + } + + /// Create an account. Will override existing account if already present. + pub fn create_account(&mut self, account: AccountAddress, account_info: AccountInfo) { + self.accounts.insert(account, account_info); + } + + /// Creates a contract address with an index one above the highest + /// currently used. Next call to `contract_init` will skip this + /// address. + pub fn create_contract_address(&mut self) -> ContractAddress { + let index = self.next_contract_index; + let subindex = 0; + self.next_contract_index += 1; + ContractAddress::new(index, subindex) + } + + /// Set the chain's slot time. + pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } + + /// Set the chain's Euro per NRG conversion rate. + pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { + self.euro_per_energy = euro_per_energy; + } + + /// Set the chain's microCCD per Euro conversion rate. + pub fn set_micro_ccd_per_euro(&mut self, micro_ccd_per_euro: ExchangeRate) { + self.micro_ccd_per_euro = micro_ccd_per_euro; + } + + /// Returns the balance of an account if it exists. + /// This will always be the persisted account balance. + pub fn persistence_account_balance(&self, address: AccountAddress) -> Option { + self.accounts.get(&address).map(|ai| ai.balance) + } + + /// Returns the balance of an contract if it exists. + /// This will always be the persisted contract balance. + pub fn persistence_contract_balance(&self, address: ContractAddress) -> Option { + self.contracts.get(&address).map(|ci| ci.self_balance) + } + + /// Returns an Arc clone of the [`ContractModule`] from persistence. + fn persistence_contract_module( + &self, + module_ref: ModuleReference, + ) -> Result, ModuleMissing> { + let module = self + .modules + .get(&module_ref) + .ok_or(ModuleMissing(module_ref))?; + Ok(Arc::clone(module)) + } + + /// Returns an immutable reference to a [`Contract`] from persistence. + fn persistence_get_contract( + &self, + address: ContractAddress, + ) -> Result<&Contract, ContractInstanceMissing> { + self.contracts + .get(&address) + .ok_or(ContractInstanceMissing(address)) + } + + /// Returns an immutable reference to [`AccountInfo`] from persistence. + fn persistence_get_account( + &self, + address: AccountAddress, + ) -> Result<&AccountInfo, AccountMissing> { + self.accounts.get(&address).ok_or(AccountMissing(address)) + } + + /// Returns a mutable reference to [`AccountInfo`] from persistence. + fn persistence_get_account_mut( + &mut self, + address: AccountAddress, + ) -> Result<&mut AccountInfo, AccountMissing> { + self.accounts + .get_mut(&address) + .ok_or(AccountMissing(address)) + } + + /// Check whether an account exists. + fn persistence_account_exists(&self, address: AccountAddress) -> bool { + self.accounts.contains_key(&address) + } + + /// Check whether a contract exists. + fn persistence_contract_exists(&self, address: ContractAddress) -> bool { + self.contracts.contains_key(&address) + } + + /// Check whether the address exists in persistence. I.e. if it is an + /// account, whether the account exists, and if it is a contract, whether + /// the contract exists. + fn persistence_address_exists(&self, address: Address) -> bool { + match address { + Address::Account(acc) => self.persistence_account_exists(acc), + Address::Contract(contr) => self.persistence_contract_exists(contr), + } + } + + /// Convert the wasm_chain_integration result to the one used here and + /// calculate the transaction fee. + /// + /// The `energy_for_state_increase` is only used if the result was a + /// success. + /// + /// The `state_changed` should refer to whether the state of the top-level + /// contract invoked has changed. + /// + /// *Preconditions*: + /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` + fn convert_update_aux_response( + &self, + update_aux_response: UpdateAuxResponse, + chain_events: Vec, + energy_reserved: Energy, + energy_for_state_increase: Energy, + state_changed: bool, + ) -> ( + Result, + Amount, + ) { + let remaining_energy = from_interpreter_energy(update_aux_response.remaining_energy); + match update_aux_response.invoke_response { + v1::InvokeResponse::Success { new_balance, data } => { + let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; + let transaction_fee = self.calculate_energy_cost(energy_used); + let result = Ok(SuccessfulContractUpdate { + chain_events, + energy_used, + transaction_fee, + return_value: data.unwrap_or_default(), + state_changed, + new_balance, + logs: update_aux_response.logs.unwrap_or_default(), + }); + (result, transaction_fee) + } + v1::InvokeResponse::Failure { kind } => { + let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; + let transaction_fee = self.calculate_energy_cost(energy_used); + let result = Err(ContractUpdateError::ExecutionError { + failure_kind: kind, + energy_used, + transaction_fee, + }); + (result, transaction_fee) + } + } + } + + /// Helper function for converting [`Energy`] to [`Amount`] using the two + /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. + fn calculate_energy_cost(&self, energy: Energy) -> Amount { + energy_to_amount(energy, self.euro_per_energy, self.micro_ccd_per_euro) + } +} + +impl TestPolicies { + // TODO: Make correctly structured policies ~= Vec>. + pub fn empty() -> Self { Self(v0::OwnedPolicyBytes::new()) } + + // TODO: Add helper functions for creating arbitrary valid policies. +} + +impl AccountInfo { + /// Create a new [`Self`] with the provided parameters. + /// The `signature_count` must be >= 1 for transaction costs to be + /// realistic. + pub fn new_with_policy_and_signature_count( + balance: Amount, + policies: TestPolicies, + signature_count: u32, + ) -> Self { + Self { + balance, + policies: policies.0, + signature_count, + } + } + + /// Create new [`Self`] with empty account policies but the provided + /// `signature_count`. The `signature_count` must be >= 1 for transaction + /// costs to be realistic. + pub fn new_with_signature_count(balance: Amount, signature_count: u32) -> Self { + Self { + signature_count, + ..Self::new(balance) + } + } + + /// Create new [`Self`] with empty account policies and a signature + /// count of `1`. + pub fn new_with_policy(balance: Amount, policies: TestPolicies) -> Self { + Self { + balance, + policies: policies.0, + signature_count: 1, + } + } + + /// Create new [`Self`] with empty account policies and a signature + /// count of `1`. + pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } +} + +impl FailedContractInteraction { + /// Get the transaction fee. + pub fn transaction_fee(&self) -> Amount { + match self { + FailedContractInteraction::Reject { + transaction_fee, .. + } => *transaction_fee, + FailedContractInteraction::Trap { + transaction_fee, .. + } => *transaction_fee, + FailedContractInteraction::OutOfEnergy { + transaction_fee, .. + } => *transaction_fee, + } + } +} + +impl SuccessfulContractUpdate { + /// Get a list of all transfers that were made from contracts to accounts. + pub fn transfers(&self) -> Vec { + self.chain_events + .iter() + .filter_map(|e| { + if let ChainEvent::Transferred { from, amount, to } = e { + Some(Transfer { + from: *from, + amount: *amount, + to: *to, + }) + } else { + None + } + }) + .collect() + } +} + +/// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. +pub(crate) fn to_interpreter_energy(energy: Energy) -> InterpreterEnergy { + InterpreterEnergy::from(energy.energy * 1000) +} + +/// Convert [`InterpreterEnergy`] to [`Energy`] by dividing by `1000`. +pub(crate) fn from_interpreter_energy(interpreter_energy: InterpreterEnergy) -> Energy { + Energy::from(interpreter_energy.energy / 1000) +} + +/// Calculate the energy energy for looking up a [`ContractModule`]. +pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { + // TODO: Is it just the `.code`? + // Comes from Concordium/Cost.hs::lookupModule + Energy::from(module.code.len() as u64 / 50) +} + +/// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange +/// rates provided. +// TODO: Find a way to make this parse the doc tests +// To find the mCCD/NRG exchange rate: +// +// euro mCCD euro * mCCD mCCD +// ---- * ---- = ----------- = ---- +// NRG euro NRG * euro NRG +// +// To convert the `energy` parameter to mCCD: +// +// mCCD NRG * mCCD +// NRG * ---- = ---------- = mCCD +// NRG NRG +pub(crate) fn energy_to_amount( + energy: Energy, + euro_per_energy: ExchangeRate, + micro_ccd_per_euro: ExchangeRate, +) -> Amount { + let micro_ccd_per_energy_numerator: BigUint = + BigUint::from(euro_per_energy.numerator()) * micro_ccd_per_euro.numerator(); + let micro_ccd_per_energy_denominator: BigUint = + BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); + let cost: BigUint = + (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; + let cost: u64 = u64::try_from(cost).expect("Should never overflow due to use of BigUint"); + Amount::from_micro_ccd(cost) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn calculate_cost_will_not_overflow() { + let micro_ccd_per_euro = ExchangeRate::new_unchecked(u64::MAX, u64::MAX - 1); + let euro_per_energy = ExchangeRate::new_unchecked(u64::MAX - 2, u64::MAX - 3); + let energy = Energy::from(u64::MAX - 4); + energy_to_amount(energy, euro_per_energy, micro_ccd_per_euro); + } +} diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs new file mode 100644 index 00000000..2c59de5d --- /dev/null +++ b/contract-testing/src/invocation/impls.rs @@ -0,0 +1,1382 @@ +use super::types::*; +use crate::{ + constants, + impls::{lookup_module_cost, to_interpreter_energy}, + types::{ + AccountInfo, Chain, ChainEvent, Contract, ContractModule, InsufficientBalanceError, + TransferError, + }, +}; +use concordium_base::{ + base::Energy, + contracts_common::{ + to_bytes, AccountAddress, Address, Amount, ChainMetadata, ContractAddress, ModuleReference, + OwnedEntrypointName, OwnedReceiveName, Parameter, + }, +}; +use std::{ + collections::{btree_map, BTreeMap}, + sync::Arc, +}; +use wasm_chain_integration::{ + v0, + v1::{self, trie}, + ExecResult, InterpreterEnergy, +}; +use wasm_transform::artifact; + +impl ContractInvocation { + /// Execute an entrypoint on a contract. + /// + /// *Preconditions:* + /// - `invoker` exists + /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// - `sender` exists + /// - if the contract (`contract_address`) exists, then its `module` must + /// also exist. + pub(crate) fn execute( + chain: &Chain, + invoker: AccountAddress, + sender: Address, + contract_address: ContractAddress, + entrypoint: OwnedEntrypointName, + parameter: Parameter, + amount: Amount, + // The CCD amount reserved from the invoker account. While the amount + // is reserved, it is not subtracted in the chain.accounts map. + // Used to handle account balance queries for the invoker account. + invoker_amount_reserved_for_nrg: Amount, + reserved_energy: Energy, + ) -> (ChangeSet, UpdateAuxResponse, Vec) { + let mut contract_invocation = Self { + changeset: ChangeSet::new(), + accounts: chain.accounts.clone(), /* TODO: These three maps should ideally + * be + * immutable references. */ + modules: chain.modules.clone(), + contracts: chain.contracts.clone(), + slot_time: chain.slot_time, + euro_per_energy: chain.euro_per_energy, + micro_ccd_per_euro: chain.micro_ccd_per_euro, + }; + + let mut chain_events = Vec::new(); + let response = contract_invocation.contract_update_aux( + invoker, + sender, + contract_address, + entrypoint, + parameter, + amount, + invoker_amount_reserved_for_nrg, + to_interpreter_energy(reserved_energy), + &mut chain_events, + ); + (contract_invocation.changeset, response, chain_events) + } + + /// Used for handling contract invokes internally. + /// + /// TODO: Find a better name for this function. + /// + /// *Preconditions:* + /// - `invoker` exists + /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// - `sender` exists + /// - if the contract (`contract_address`) exists, then its `module` must + /// also exist. + fn contract_update_aux( + &mut self, + invoker: AccountAddress, + sender: Address, + contract_address: ContractAddress, + entrypoint: OwnedEntrypointName, + parameter: Parameter, + amount: Amount, + // The CCD amount reserved from the invoker account. While the amount + // is reserved, it is not subtracted in the chain.accounts map. + // Used to handle account balance queries for the invoker account. + invoker_amount_reserved_for_nrg: Amount, + // Uses [`Interpreter`] energy to avoid rounding issues when converting to and fro + // [`Energy`]. + mut remaining_energy: InterpreterEnergy, + chain_events: &mut Vec, + ) -> UpdateAuxResponse { + // Move the amount from the sender to the contract, if any. + // And get the new self_balance. + let instance_self_balance = if amount.micro_ccd() > 0 { + let transfer_result = match sender { + Address::Account(sender_account) => self.changeset_transfer_account_to_contract( + amount, + sender_account, + contract_address, + ), + Address::Contract(sender_contract) => self.changeset_transfer_contract_to_contract( + amount, + sender_contract, + contract_address, + ), + }; + match transfer_result { + Ok(new_balance_from) => new_balance_from, + Err(transfer_error) => { + let kind = match transfer_error { + TransferError::InsufficientBalance => v1::InvokeFailure::InsufficientAmount, + TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, + }; + // Return early. + // TODO: Should we charge any energy for this? + return UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { kind }, + logs: None, + remaining_energy, + }; + } + } + } else { + match self.changeset_contract_balance(contract_address) { + Some(self_balance) => self_balance, + None => { + // Return early. + // TODO: For the top-most update, we should catch this in `contract_update` and + // return `ContractUpdateError::EntrypointMissing`. + return UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + logs: None, + remaining_energy, + }; + } + } + }; + + // Get the instance and artifact. To be used in several places. + let instance = self + .contracts + .get(&contract_address) + .expect("Contract known to exist at this point"); + let artifact = self.changeset_contract_module(contract_address); + + // Subtract the cost of looking up the module + remaining_energy = + remaining_energy.subtract(to_interpreter_energy(lookup_module_cost(&artifact)).energy); + + // Construct the receive name (or fallback receive name) and ensure its presence + // in the contract. + let receive_name = { + let contract_name = instance.contract_name.as_contract_name().contract_name(); + let receive_name = format!("{}.{}", contract_name, entrypoint); + let fallback_receive_name = format!("{}.", contract_name); + if artifact.has_entrypoint(receive_name.as_str()) { + OwnedReceiveName::new_unchecked(receive_name) + } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { + OwnedReceiveName::new_unchecked(fallback_receive_name) + } else { + // Return early. + return UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentEntrypoint, + }, + logs: None, + remaining_energy, + }; + } + }; + + // Construct the receive context + let receive_ctx = v1::ReceiveContext { + entrypoint: entrypoint.to_owned(), + common: v0::ReceiveContext { + metadata: ChainMetadata { + slot_time: self.slot_time, + }, + invoker, + self_address: contract_address, + self_balance: instance_self_balance, + sender, + owner: instance.owner, + sender_policies: self + .accounts + .get(&invoker) + .expect("Precondition violation: invoker must exist.") + .policies + .clone(), + }, + }; + + let contract_name = instance.contract_name.clone(); + + // Construct the instance state + let mut loader = v1::trie::Loader::new(&[][..]); + let mut mutable_state = self.changeset_contract_state(contract_address); + let inner = mutable_state.get_inner(&mut loader); + let instance_state = v1::InstanceState::new(loader, inner); + + // Get the initial result from invoking receive + let initial_result = v1::invoke_receive( + artifact, + receive_ctx, + v1::ReceiveInvocation { + amount, + receive_name: receive_name.as_receive_name(), + parameter: parameter.0, + energy: remaining_energy, + }, + instance_state, + v1::ReceiveParams { + max_parameter_size: 65535, + limit_logs_and_return_values: false, + support_queries: true, + }, + ); + + // Set up some data needed for recursively processing the receive until the end, + // i.e. beyond interrupts. + let mut data = ProcessReceiveData { + invoker, + address: contract_address, + contract_name, + amount, + invoker_amount_reserved_for_nrg, + entrypoint, + chain: self, + state: mutable_state, + chain_events: Vec::new(), + loader, + }; + + // Process the receive invocation to the completion. + let result = data.process(initial_result); + let mut new_chain_events = data.chain_events; + + let result = match result { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed: _, /* This only reflects changes since last interrupt, we use + * the changeset later to get a more precise result. */ + return_value, + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Success { + new_balance: self + .changeset_contract_balance_unchecked(contract_address), + data: Some(return_value), + }, + logs: Some(logs), + remaining_energy, + } + } + v1::ReceiveResult::Interrupt { .. } => { + panic!("Internal error: `data.process` returned an interrupt.") + } + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + logs: None, + remaining_energy, + } + } + v1::ReceiveResult::Trap { + error: _, // TODO: Should we return this to the user? + remaining_energy, + } => { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: None, + remaining_energy, + } + } + v1::ReceiveResult::OutOfEnergy => { + let remaining_energy = InterpreterEnergy::from(0); + UpdateAuxResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: None, + remaining_energy, + } + } + }, + Err(internal_error) => { + panic!("Internal error: Got interpreter error {}", internal_error) + } + }; + + // Append the new chain events if the invocation succeeded. + if result.is_success() { + chain_events.append(&mut new_chain_events); + } + + result + } + + /// Make a transfer from a contract to an account in the changeset. + /// Returns the new balance of `from`. + /// + /// Precondition: + /// - Assumes that `from` contract exists. + fn changeset_transfer_contract_to_account( + &mut self, + amount: Amount, + from: ContractAddress, + to: AccountAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.accounts.contains_key(&to) { + return Err(TransferError::ToMissing); + } + + // Make the transfer. + let new_balance = + self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; + self.changeset_change_account_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + + Ok(new_balance) + } + + /// Make a transfer between contracts in the changeset. + /// Returns the new balance of `from`. + /// + /// Precondition: + /// - Assumes that `from` contract exists. + fn changeset_transfer_contract_to_contract( + &mut self, + amount: Amount, + from: ContractAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` contract exists. + if !self.contracts.contains_key(&to) { + return Err(TransferError::ToMissing); + } + + // Make the transfer. + let new_balance = + self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; + self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + + Ok(new_balance) + } + + /// Make a transfer from an account to a contract in the changeset. + /// Returns the new balance of `from`. + /// + /// Precondition: + /// - Assumes that `from` account exists. + fn changeset_transfer_account_to_contract( + &mut self, + amount: Amount, + from: AccountAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.contracts.contains_key(&to) { + return Err(TransferError::ToMissing); + } + + // Make the transfer. + let new_balance = + self.changeset_change_account_balance(from, AmountDelta::Negative(amount))?; + self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); + Ok(new_balance) + } + + // TODO: Should we handle overflows explicitly? + /// Changes the contract balance in the topmost checkpoint on the changeset. + /// If contract isn't already present in the changeset, it is added. + /// Returns the new balance. + /// + /// Precondition: + /// - Contract must exist. + fn changeset_change_contract_balance( + &mut self, + address: ContractAddress, + delta: AmountDelta, + ) -> Result { + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + // get original balance + let original_balance = self + .contracts + .get(&address) + .expect("Precondition violation: contract assumed to exist") + .self_balance; + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(ContractChanges { + self_balance_delta: delta, + ..ContractChanges::new(original_balance) + }); + Ok(new_contract_balance) + } + btree_map::Entry::Occupied(mut occ) => { + let contract_changes = occ.get_mut(); + let new_delta = contract_changes.self_balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = + new_delta.apply_to_balance(contract_changes.self_balance_original)?; + contract_changes.self_balance_delta = new_delta; + Ok(new_contract_balance) + } + } + } + + // TODO: Should we handle overflows explicitly? + /// Changes the account balance in the topmost checkpoint on the changeset. + /// If account isn't already present in the changeset, it is added. + /// Returns the new balance. + /// + /// Precondition: + /// - Account must exist. + fn changeset_change_account_balance( + &mut self, + address: AccountAddress, + delta: AmountDelta, + ) -> Result { + match self.changeset.current_mut().accounts.entry(address) { + btree_map::Entry::Vacant(vac) => { + // get original balance + let original_balance = self + .accounts + .get(&address) + .expect("Precondition violation: account assumed to exist") + .balance; + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(AccountChanges { + original_balance, + balance_delta: delta, + }); + Ok(new_account_balance) + } + btree_map::Entry::Occupied(mut occ) => { + let account_changes = occ.get_mut(); + let new_delta = account_changes.balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = + new_delta.apply_to_balance(account_changes.original_balance)?; + account_changes.balance_delta = new_delta; + Ok(new_account_balance) + } + } + } + + /// Returns the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + /// + /// *Preconditions:* + /// - Contract must exist. + fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { + self.changeset_contract_balance(address) + .expect("Precondition violation: contract must exist") + } + + /// Looks up the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + fn changeset_contract_balance(&self, address: ContractAddress) -> Option { + match self.changeset.current().contracts.get(&address) { + Some(changes) => Some(changes.current_balance()), + None => self.contracts.get(&address).map(|c| c.self_balance), + } + } + + /// Returns the contract module from the topmost checkpoint on + /// the changeset. Or, alternatively, from persistence. + /// + /// *Preconditions:* + /// - Contract instance must exist (and therefore also the artifact). + /// - If the changeset contains a module reference, then it must refer a + /// deployed module. + fn changeset_contract_module(&self, address: ContractAddress) -> Arc { + match self + .changeset + .current() + .contracts + .get(&address) + .and_then(|c| c.module) + { + // Contract has been upgrade, new module exists. + Some(new_module) => Arc::clone( + self.modules + .get(&new_module) + .expect("Precondition violation: module must exist."), + ), + // Contract hasn't been upgraded. Use persisted module. + None => { + let module_ref = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .module_reference; + Arc::clone( + self.modules + .get(&module_ref) + .expect("Precondition violation: module must exist."), + ) + } + } + } + + /// Get the contract state, either from the changeset or by thawing it from + /// persistence. + /// + /// *Preconditions:* + /// - Contract instance must exist. + fn changeset_contract_state(&self, address: ContractAddress) -> trie::MutableState { + match self + .changeset + .current() + .contracts + .get(&address) + .and_then(|c| c.state.clone()) + { + // Contract state has been modified. + Some(modified_state) => modified_state, + // Contract state hasn't been modified. Thaw from persistence. + None => self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist") + .state + .thaw(), + } + } + + /// Looks up the account balance for an account by first checking the + /// changeset, then the persisted values. + fn changeset_account_balance(&self, address: AccountAddress) -> Option { + match self + .changeset + .current() + .accounts + .get(&address) + .map(|a| a.current_balance()) + { + // Account exists in changeset. + Some(bal) => Some(bal), + // Account doesn't exist in changeset. + None => self.accounts.get(&address).map(|a| a.balance), + } + } + + /// Saves a mutable state for a contract in the changeset. + /// + /// If the contract already has an entry in the changeset, the old state + /// will be replaced. Otherwise, the entry is created and the state is + /// added. + /// + /// This also increments the modification index. It will be set to 1 if the + /// contract has no entry in the changeset. + /// + /// *Preconditions:* + /// - Contract must exist. + fn changeset_save_state_changes( + &mut self, + address: ContractAddress, + state: &mut trie::MutableState, + ) { + let mut loader = v1::trie::Loader::new(&[][..]); + self.changeset + .current_mut() + .contracts + .entry(address) + .and_modify(|changes| { + changes.state = Some(state.make_fresh_generation(&mut loader)); + changes.modification_index += 1; + }) + .or_insert({ + let original_balance = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .self_balance; + ContractChanges { + state: Some(state.make_fresh_generation(&mut loader)), + modification_index: 1, // Increment from default, 0, to 1. + ..ContractChanges::new(original_balance) + } + }); + } + + /// Saves a new module reference for the contract in the changeset. + /// + /// If the contract already has an entry in the changeset, the old module is + /// replaced. Otherwise, the entry is created and the module is added. + /// + /// Returns the previous module, which is either the one from persistence, + /// or the most recent one from the changeset. + /// + /// *Preconditions:* + /// - Contract must exist. + /// - Module must exist. + fn changeset_save_module_upgrade( + &mut self, + address: ContractAddress, + module_reference: ModuleReference, + ) -> ModuleReference { + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + let contract = self + .contracts + .get(&address) + .expect("Precondition violation: contract must exist."); + let old_module_ref = contract.module_reference; + let original_balance = contract.self_balance; + vac.insert(ContractChanges { + module: Some(module_reference), + ..ContractChanges::new(original_balance) + }); + old_module_ref + } + btree_map::Entry::Occupied(mut occ) => { + let changes = occ.get_mut(); + let old_module_ref = match changes.module { + Some(old_module) => old_module, + None => { + self.contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .module_reference + } + }; + changes.module = Some(module_reference); + old_module_ref + } + } + } + + /// Returns the modification index for a contract. + /// + /// It looks it up in the changeset, and if it isn't there, it will return + /// `0`. + fn changeset_modification_index(&self, address: ContractAddress) -> u32 { + self.changeset + .current() + .contracts + .get(&address) + .map_or(0, |c| c.modification_index) + } + + /// Makes a new checkpoint. + fn changeset_checkpoint(&mut self) { self.changeset.checkpoint(); } + + /// Roll back to the previous checkpoint. + fn changeset_rollback(&mut self) { self.changeset.rollback(); } +} + +impl ChangeSet { + /// Creates a new changeset with one empty [`Changes`] element on the + /// stack.. + fn new() -> Self { + Self { + stack: vec![Changes { + contracts: BTreeMap::new(), + accounts: BTreeMap::new(), + }], + } + } + + /// Make a checkpoint by putting a clone of the top element onto the stack. + fn checkpoint(&mut self) { + let cloned_top_element = self.current().clone(); + self.stack.push(cloned_top_element); + } + + /// Perform a rollback by popping the top element of the stack. + fn rollback(&mut self) { + self.stack + .pop() + .expect("Internal error: change set stack should never be empty."); + } + + /// Get an immutable reference the current (latest) checkpoint. + fn current(&self) -> &Changes { + self.stack + .last() + .expect("Internal error: change set stack should never be empty.") + } + + /// Get a mutable reference to the current (latest) checkpoint. + fn current_mut(&mut self) -> &mut Changes { + self.stack + .last_mut() + .expect("Internal error: change set stack should never be empty.") + } + + /// Try to persist all changes from the changeset. + /// + /// If the energy needed for storing extra state is larger than the + /// `remaining_energy`, then: + /// - no changes will be persisted, + /// - an [`OutOfEnergy`] error is returned. + /// + /// Otherwise, it returns the [`Energy`] to be charged for the additional + /// bytes added to contract states. It also returns whether the state of the + /// provided `invoked_contract` was changed. + /// + /// *Preconditions:* + /// - All contracts, modules, accounts referred must exist in persistence. + /// - All amount deltas must be valid (i.e. not cause underflows when added + /// to balance). + pub(crate) fn persist( + mut self, + remaining_energy: Energy, + invoked_contract: ContractAddress, + persisted_accounts: &mut BTreeMap, + persisted_contracts: &mut BTreeMap, + ) -> Result<(Energy, bool), OutOfEnergy> { + let current = self.current_mut(); + let mut invoked_contract_has_state_changes = false; + // Persist contract changes and collect the total increase in states sizes. + let mut collector = v1::trie::SizeCollector::default(); + let mut loader = v1::trie::Loader::new(&[][..]); + + let mut frozen_states: BTreeMap = BTreeMap::new(); + + // Create frozen versions of all the states, to compute the energy needed. + for (addr, changes) in current.contracts.iter_mut() { + if let Some(modified_state) = &mut changes.state { + frozen_states.insert(*addr, modified_state.freeze(&mut loader, &mut collector)); + } + } + + // One energy per extra byte of state. + let energy_for_state_increase = Energy::from(collector.collect()); + + // Return an error if out of energy, and clear the changeset. + if remaining_energy + .checked_sub(energy_for_state_increase) + .is_none() + { + return Err(OutOfEnergy); + } + + // Then persist all the changes. + for (addr, changes) in current.contracts.iter_mut() { + let mut contract = persisted_contracts + .get_mut(addr) + .expect("Precondition violation: contract must exist"); + // Update balance. + if !changes.self_balance_delta.is_zero() { + contract.self_balance = changes + .self_balance_delta + .apply_to_balance(changes.self_balance_original) + .expect("Precondition violation: amount delta causes underflow"); + } + // Update module reference. + if let Some(new_module_ref) = changes.module { + contract.module_reference = new_module_ref; + } + // Update state. + if changes.state.is_some() { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } + // Replace with the frozen state we created earlier. + contract.state = frozen_states + .remove(addr) + .expect("Known to exist since we just added it."); + } + } + // Persist account changes. + for (addr, changes) in current.accounts.iter() { + let mut account = persisted_accounts + .get_mut(addr) + .expect("Precondition violation: account must exist"); + // Update balance. + if !changes.balance_delta.is_zero() { + account.balance = changes + .balance_delta + .apply_to_balance(changes.original_balance) + .expect("Precondition violation: amount delta causes underflow"); + } + } + + Ok(( + energy_for_state_increase, + invoked_contract_has_state_changes, + )) + } + + /// Traverses the last checkpoint in the changeset and collects the energy + /// needed to be charged for additional state bytes. + /// + /// Returns an [`OutOfEnergy`] error if the energy needed for storing the + /// extra state is larger than `remaining_energy`. + /// + /// Otherwise, it returns + /// the [`Energy`] needed for storing the extra state. It also returns + /// whether the state of the provided `invoked_contract` has changed. + pub(crate) fn collect_energy_for_state( + mut self, + remaining_energy: Energy, + invoked_contract: ContractAddress, + ) -> Result<(Energy, bool), OutOfEnergy> { + let mut invoked_contract_has_state_changes = false; + let mut loader = v1::trie::Loader::new(&[][..]); + let mut collector = v1::trie::SizeCollector::default(); + for (addr, changes) in self.current_mut().contracts.iter_mut() { + if let Some(modified_state) = &mut changes.state { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } + modified_state.freeze(&mut loader, &mut collector); + } + } + + // One energy per extra byte in the state. + let energy_for_state_increase = Energy::from(collector.collect()); + + if remaining_energy + .checked_sub(energy_for_state_increase) + .is_none() + { + return Err(OutOfEnergy); + } + Ok(( + energy_for_state_increase, + invoked_contract_has_state_changes, + )) + } +} + +impl Default for ChangeSet { + fn default() -> Self { Self::new() } +} + +impl AmountDelta { + /// Create a new [`Self`], with the value `+0`. + fn new() -> Self { Self::Positive(Amount::zero()) } + + /// Subtract an [`Amount`] from [`Self`]. + fn subtract_amount(self, amount: Amount) -> Self { + match self { + Self::Positive(current) => { + if current >= amount { + Self::Positive(current.subtract_micro_ccd(amount.micro_ccd)) + } else { + Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)) + } + } + Self::Negative(current) => Self::Negative(current.add_micro_ccd(amount.micro_ccd)), + } + } + + /// Add an [`Amount`] from [`Self`]. + fn add_amount(self, amount: Amount) -> Self { + match self { + Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), + Self::Negative(current) => { + if current >= amount { + Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)) + } else { + Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)) + } + } + } + } + + /// Add two [`Self`] to create a new one. + fn add_delta(self, delta: AmountDelta) -> Self { + match delta { + AmountDelta::Positive(d) => self.add_amount(d), + AmountDelta::Negative(d) => self.subtract_amount(d), + } + } + + /// Whether the [`Self`] is zero (either `+0` or `-0`). + fn is_zero(&self) -> bool { + match self { + AmountDelta::Positive(d) => d.micro_ccd == 0, + AmountDelta::Negative(d) => d.micro_ccd == 0, + } + } + + /// Returns a new balance with the `AmountDelta` applied, or, an + /// error if `balance + self < 0`. + /// + /// Panics if an overflow occurs. + fn apply_to_balance(&self, balance: Amount) -> Result { + match self { + AmountDelta::Positive(d) => Ok(balance + .checked_add(*d) + .expect("Overflow occured when adding Amounts.")), // TODO: Should we return an + // error for this? + AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(UnderflowError), + } + } +} + +impl ContractChanges { + /// Create a new `Self`. The original balance must be provided, all other + /// fields take on default values. + fn new(original_balance: Amount) -> Self { + Self { + modification_index: 0, + self_balance_delta: AmountDelta::new(), + self_balance_original: original_balance, + state: None, + module: None, + } + } + + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// *Preconditions:* + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.self_balance_delta + .apply_to_balance(self.self_balance_original) + .expect("Precondition violation: invalid `balance_delta`.") + } +} + +impl AccountChanges { + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// *Preconditions:* + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.balance_delta + .apply_to_balance(self.original_balance) + .expect("Precondition violation: invalid `balance_delta`.") + } +} + +impl<'a, 'b> ProcessReceiveData<'a, 'b> { + /// Process a receive function until completion. + /// + /// *Preconditions*: + /// - Contract instance exists in `chain.contracts`. + /// - Account exists in `chain.accounts`. + fn process( + &mut self, + res: ExecResult>, + ) -> ExecResult> { + match res { + Ok(r) => match r { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + println!("\tSuccessful contract update {}", self.address); + let update_event = ChainEvent::Updated { + address: self.address, + contract: self.contract_name.clone(), + entrypoint: self.entrypoint.clone(), + amount: self.amount, + }; + // Add update event + self.chain_events.push(update_event); + + // Save changes to changeset. + if state_changed { + self.chain + .changeset_save_state_changes(self.address, &mut self.state); + } + + Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + }) + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => { + println!("\tInterrupting contract {}", self.address); + + // Create the interrupt event, which will be included for transfers, calls, and + // upgrades, but not for the remaining interrupts. + let interrupt_event = ChainEvent::Interrupted { + address: self.address, + logs, + }; + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + + println!("\t\tTransferring {} CCD to {}", amount, to); + + let response = match self.chain.changeset_transfer_contract_to_account( + amount, + self.address, + to, + ) { + Ok(new_balance) => v1::InvokeResponse::Success { + new_balance, + data: None, + }, + Err(err) => { + let kind = match err { + TransferError::ToMissing => { + v1::InvokeFailure::NonExistentAccount + } + TransferError::InsufficientBalance => { + v1::InvokeFailure::InsufficientAmount + } + }; + v1::InvokeResponse::Failure { kind } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + if success { + // Add transfer event + self.chain_events.push(ChainEvent::Transferred { + from: self.address, + amount, + to, + }); + } + // Add resume event + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(remaining_energy), + &mut self.state, + false, // never changes on transfers + self.loader, + ); + + // Resume + self.process(resume_res) + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + + if state_changed { + self.chain + .changeset_save_state_changes(self.address, &mut self.state); + } + + // Save the modification index before the invoke. + let mod_idx_before_invoke = + self.chain.changeset_modification_index(self.address); + + // Make a checkpoint before calling another contract so that we may roll + // back. + self.chain.changeset_checkpoint(); + + println!( + "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", + address, parameter + ); + + let res = self.chain.contract_update_aux( + self.invoker, + Address::Contract(self.address), + address, + name, + Parameter(¶meter), + amount, + self.invoker_amount_reserved_for_nrg, + InterpreterEnergy::from(remaining_energy), + &mut self.chain_events, + ); + + let success = res.is_success(); + + // Remove the last state changes if the invocation failed. + let state_changed = if !success { + self.chain.changeset_rollback(); + false // We rolled back, so no changes were made + // to this contract. + } else { + let mod_idx_after_invoke = + self.chain.changeset_modification_index(self.address); + let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + self.state = self.chain.changeset_contract_state(self.address); + } + state_changed + }; + + println!( + "\tResuming contract {}\n\t\tafter {}", + self.address, + if success { + "succesful invocation" + } else { + "failed invocation" + } + ); + + // Add resume event + let resume_event = ChainEvent::Resumed { + address: self.address, + success, + }; + + self.chain_events.push(resume_event); + + let resume_res = v1::resume_receive( + config, + res.invoke_response, + res.remaining_energy, + &mut self.state, + state_changed, + self.loader, + ); + + self.process(resume_res) + } + v1::Interrupt::Upgrade { module_ref } => { + println!("Upgrading contract to {:?}", module_ref); + + // Add the interrupt event. + self.chain_events.push(interrupt_event); + + // Charge a base cost. + let mut energy_after_invoke = remaining_energy + - to_interpreter_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST, + ) + .energy; + + let response = match self.chain.modules.get(&module_ref) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(module) => { + // Charge for the module lookup. + energy_after_invoke -= + to_interpreter_energy(lookup_module_cost(module)).energy; + + if module.export.contains_key( + self.contract_name.as_contract_name().get_chain_name(), + ) { + // Update module reference in the changeset. + let old_module_ref = + self.chain.changeset_save_module_upgrade( + self.address, + module_ref, + ); + + // Charge for the initialization cost. + energy_after_invoke -= to_interpreter_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + ) + .energy; + + let upgrade_event = ChainEvent::Upgraded { + address: self.address, + from: old_module_ref, + to: module_ref, + }; + + self.chain_events.push(upgrade_event); + + v1::InvokeResponse::Success { + new_balance: self + .chain + .changeset_contract_balance_unchecked(self.address), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, + } + } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.state, + state_changed, + self.loader, + ); + + self.process(resume_res) + } + v1::Interrupt::QueryAccountBalance { address } => { + println!("\t\tQuerying account balance of {}", address); + // When querying an account, the amounts from any `invoke_transfer`s + // should be included. That is handled by + // the `chain` struct already. transaction. + // However, that is hand + let response = match self.chain.changeset_account_balance(address) { + Some(acc_bal) => { + // If you query the invoker account, it should also + // take into account the send-amount and the amount reserved for + // the reserved max energy. The former is handled in + // `contract_update_aux`, but the latter is represented in + // `self.invoker_amount_reserved_for_nrg`. + let acc_bal = if address == self.invoker { + acc_bal - self.invoker_amount_reserved_for_nrg + } else { + acc_bal + }; + + // TODO: Do we need non-zero staked and shielded balances? + let balances = + to_bytes(&(acc_bal, Amount::zero(), Amount::zero())); + v1::InvokeResponse::Success { + new_balance: self + .chain + .changeset_contract_balance_unchecked(self.address), + data: Some(balances), + } + } + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + + let energy_after_invoke = remaining_energy + - to_interpreter_energy( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, + ) + .energy; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } + v1::Interrupt::QueryContractBalance { address } => { + println!("Querying contract balance of {}", address); + + let response = match self.chain.changeset_contract_balance(address) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + Some(bal) => v1::InvokeResponse::Success { + // Balance of contract querying. Won't change. Notice the + // `self.address`. + new_balance: self + .chain + .changeset_contract_balance_unchecked(self.address), + data: Some(to_bytes(&bal)), + }, + }; + + let energy_after_invoke = remaining_energy + - to_interpreter_energy( + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + ) + .energy; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } + v1::Interrupt::QueryExchangeRates => { + println!("Querying exchange rates"); + + let exchange_rates = + (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); + + let response = v1::InvokeResponse::Success { + new_balance: self + .chain + .changeset_contract_balance_unchecked(self.address), + data: Some(to_bytes(&exchange_rates)), + }; + + let energy_after_invoke = remaining_energy + - to_interpreter_energy( + constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, + ) + .energy; + + let resume_res = v1::resume_receive( + config, + response, + InterpreterEnergy::from(energy_after_invoke), + &mut self.state, + false, // State never changes on queries. + self.loader, + ); + + self.process(resume_res) + } + } + } + x => Ok(x), + }, + Err(e) => Err(e), + } + } +} + +impl UpdateAuxResponse { + pub(crate) fn is_success(&self) -> bool { + matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) + } +} + +impl From for InsufficientBalanceError { + fn from(_: UnderflowError) -> Self { InsufficientBalanceError } +} + +impl From for TransferError { + fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } +} diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs new file mode 100644 index 00000000..e56b0254 --- /dev/null +++ b/contract-testing/src/invocation/mod.rs @@ -0,0 +1,18 @@ +//! Functionality and types for invoking a contract either for a contract update +//! or a contract invoke. +//! +//! Contract invocation is effectful and transactional. +//! We therefore keep track of changes during execution in a [`ChangeSet`]. +//! +//! Once the execution (transaction) has finished, the changes can then be +//! persisted (saved) or discarded, dependent on whether it succeeded or not. +//! For contract invokes the changes will always be discarded. +//! +//! The changes that may occur are: +//! - Mutations to contract state, +//! - Contract upgrades (changing the module), +//! - Balances of contracts and accounts. + +mod impls; +mod types; +pub(crate) use types::{ContractInvocation, UpdateAuxResponse}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs new file mode 100644 index 00000000..388f699f --- /dev/null +++ b/contract-testing/src/invocation/types.rs @@ -0,0 +1,121 @@ +use crate::types::{AccountInfo, ChainEvent, Contract, ContractModule}; +use concordium_base::contracts_common::{ + AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, + OwnedEntrypointName, SlotTime, +}; +use std::{collections::BTreeMap, sync::Arc}; +use wasm_chain_integration::{ + v0, + v1::{self, trie::MutableState, InvokeResponse}, + InterpreterEnergy, +}; + +// TODO: Find a better name. +pub(crate) struct UpdateAuxResponse { + /// The result from the invoke. + pub(crate) invoke_response: InvokeResponse, + /// Will be `Some` if and only if `invoke_response` is `Success`. + pub(crate) logs: Option, + /// The remaining energy after the invocation. + pub(crate) remaining_energy: InterpreterEnergy, +} + +pub(crate) struct ContractInvocation { + pub(super) changeset: ChangeSet, + pub(super) accounts: BTreeMap, + pub(super) modules: BTreeMap>, + pub(super) contracts: BTreeMap, + pub(super) slot_time: SlotTime, + pub(super) euro_per_energy: ExchangeRate, + pub(super) micro_ccd_per_euro: ExchangeRate, +} + +/// The set of [`Changes`] represented as a stack. +#[derive(Debug, Clone)] +pub(crate) struct ChangeSet { + /// The stack of changes. + pub(super) stack: Vec, +} + +/// Data held for accounts and contracts during the execution of a contract +/// update or invocation. +#[derive(Clone, Debug)] +pub(super) struct Changes { + /// The contracts which have changes. + pub(super) contracts: BTreeMap, + /// The accounts which have changes. + pub(super) accounts: BTreeMap, +} + +/// Data held for an account during the execution of an update or invoke +/// transaction. +#[derive(Clone, Debug)] +pub(super) struct AccountChanges { + /// Should never be modified. + pub(super) original_balance: Amount, + pub(super) balance_delta: AmountDelta, +} + +/// Data held for a contract during the execution of a contract update or +/// invocation. +#[derive(Clone, Debug)] +pub(super) struct ContractChanges { + /// An index that is used to check whether a caller contract has been + /// modified after invoking another contract (due to reentrancy). + pub(super) modification_index: u32, + /// Represents how much the contract's self balance has changed. + pub(super) self_balance_delta: AmountDelta, + /// The original contract balance, i.e. the one that is persisted. Should + /// never be modified. + pub(super) self_balance_original: Amount, + /// The potentially modified contract state. + pub(super) state: Option, + /// The potentially changed module. + pub(super) module: Option, +} + +/// Data needed to recursively process a contract update or invocation to +/// completion. +pub(super) struct ProcessReceiveData<'a, 'b> { + /// The invoker. + pub(super) invoker: AccountAddress, + /// The contract being called. + pub(super) address: ContractAddress, + /// The name of the contract. + pub(super) contract_name: OwnedContractName, + /// The amount sent from the sender to the contract. + pub(super) amount: Amount, + /// The CCD amount reserved from the invoker account for the energy. While + /// the amount is reserved, it is not subtracted in the chain.accounts + /// map. Used to handle account balance queries for the invoker account. + /// TODO: We could use a changeset for accounts -> balance, and then look up + /// the "chain.accounts" values for chain queries. + pub(super) invoker_amount_reserved_for_nrg: Amount, + /// The entrypoint to execute. + pub(super) entrypoint: OwnedEntrypointName, + /// A reference to the chain. + pub(super) chain: &'a mut ContractInvocation, + /// The current state. + pub(super) state: MutableState, + /// Chain events that have occurred during the execution. + pub(super) chain_events: Vec, + /// + pub(super) loader: v1::trie::Loader<&'b [u8]>, +} + +/// A positive or negative delta in for an [`Amount`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum AmountDelta { + /// A posittive delta. + Positive(Amount), + /// A negative delta. + Negative(Amount), +} + +/// An underflow occurred. +#[derive(Debug)] +pub(super) struct UnderflowError; + +/// The contract ran out of energy during execution of an update or invocation. +#[derive(PartialEq, Eq, Debug)] +pub(crate) struct OutOfEnergy; diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d47c56cb..65383302 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,2509 +1,14 @@ -use concordium_base::{ - base::{Energy, ExchangeRate}, - common, - contracts_common::*, - smart_contracts::{ModuleSource, WasmModule, WasmVersion}, - transactions::{self, cost}, -}; -use num_bigint::BigUint; -use sha2::{Digest, Sha256}; -use std::{ - collections::{btree_map, BTreeMap}, - path::Path, - sync::Arc, -}; -use thiserror::Error; -use wasm_chain_integration::{ - v0, - v1::{ - self, - trie::{MutableState, PersistentState, SizeCollector}, - ConcordiumAllowedImports, InvokeFailure, InvokeResponse, ReturnValue, - }, - ExecResult, InterpreterEnergy, -}; -use wasm_transform::artifact; mod constants; +mod impls; +mod invocation; +mod types; -/// A V1 artifact, with concrete types for the generic parameters. -type ContractModule = artifact::Artifact; - -/// Whether the current [`Changes`] should be printed before and after an -/// internal contract invoke. TODO: Remove before publishing. -const VERBOSE_DEBUG: bool = false; - -/// Represents the block chain and supports a number of operations, including -/// creating accounts, deploying modules, initializing contract, updating -/// contracts and invoking contracts. -pub struct Chain { - /// The slot time viewable inside the smart contracts. - /// Defaults to `0`. - pub slot_time: SlotTime, - /// MicroCCD per Euro ratio. - pub micro_ccd_per_euro: ExchangeRate, - /// Euro per Energy ratio. - pub euro_per_energy: ExchangeRate, - /// Accounts and info about them. - pub accounts: BTreeMap, - /// Smart contract modules. - pub modules: BTreeMap>, - /// Smart contract instances. - pub contracts: BTreeMap, - /// Next contract index to use when creating a new instance. - pub next_contract_index: u64, - /// The changeset used during a contract update or invocation. - changeset: ChangeSet, -} - -/// A smart contract instance along. -#[derive(Clone)] -pub struct Contract { - /// The module which contains this contract. - pub module_reference: ModuleReference, - /// The name of the contract. - pub contract_name: OwnedContractName, - /// The contract state. - pub state: v1::trie::PersistentState, - /// The owner of the contract. - pub owner: AccountAddress, - /// The balance of the contract. - pub self_balance: Amount, -} - -/// A positive or negative delta in for an [`Amount`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AmountDelta { - /// A posittive delta. - Positive(Amount), - /// A negative delta. - Negative(Amount), -} - -impl AmountDelta { - /// Create a new [`Self`], with the value `+0`. - fn new() -> Self { Self::Positive(Amount::zero()) } - - /// Subtract an [`Amount`] from [`Self`]. - fn subtract_amount(self, amount: Amount) -> Self { - match self { - Self::Positive(current) => { - if current >= amount { - Self::Positive(current.subtract_micro_ccd(amount.micro_ccd)) - } else { - Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)) - } - } - Self::Negative(current) => Self::Negative(current.add_micro_ccd(amount.micro_ccd)), - } - } - - /// Add an [`Amount`] from [`Self`]. - fn add_amount(self, amount: Amount) -> Self { - match self { - Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), - Self::Negative(current) => { - if current >= amount { - Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)) - } else { - Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)) - } - } - } - } - - /// Add two [`Self`] to create a new one. - fn add_delta(self, delta: AmountDelta) -> Self { - match delta { - AmountDelta::Positive(d) => self.add_amount(d), - AmountDelta::Negative(d) => self.subtract_amount(d), - } - } - - /// Whether the [`Self`] is zero (either `+0` or `-0`). - fn is_zero(&self) -> bool { - match self { - AmountDelta::Positive(d) => d.micro_ccd == 0, - AmountDelta::Negative(d) => d.micro_ccd == 0, - } - } - - /// Returns a new balance with the `AmountDelta` applied, or, an - /// error if `balance + self < 0`. - /// - /// Panics if an overflow occurs. - fn apply_to_balance(&self, balance: Amount) -> Result { - match self { - AmountDelta::Positive(d) => Ok(balance - .checked_add(*d) - .expect("Overflow occured when adding Amounts.")), // TODO: Should we return an - // error for this? - AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(UnderflowError), - } - } -} - -/// Data held for a contract during the execution of a contract update or -/// invocation. -#[derive(Clone, Debug)] -pub struct ContractChanges { - /// An index that is used to check whether a caller contract has been - /// modified after invoking another contract (due to reentrancy). - modification_index: u32, - /// Represents how much the contract's self balance has changed. - self_balance_delta: AmountDelta, - /// The original contract balance, i.e. the one that is persisted. Should - /// never be modified. - self_balance_original: Amount, - /// The potentially modified contract state. - state: Option, - /// The potentially changed module. - module: Option, -} - -impl ContractChanges { - /// Create a new `Self`. The original balance must be provided, all other - /// fields take on default values. - fn new(original_balance: Amount) -> Self { - Self { - modification_index: 0, - self_balance_delta: AmountDelta::new(), - self_balance_original: original_balance, - state: None, - module: None, - } - } - - /// Get the current balance by adding the original balance and the balance - /// delta. - /// - /// *Preconditions:* - /// - `balance_delta + original_balance` must be larger than `0`. - fn current_balance(&self) -> Amount { - self.self_balance_delta - .apply_to_balance(self.self_balance_original) - .expect("Precondition violation: invalid `balance_delta`.") - } -} - -/// Data held for an account during the execution of an update or invoke -/// transaction. -#[derive(Clone, Debug)] -pub struct AccountChanges { - /// Should never be modified. - original_balance: Amount, - balance_delta: AmountDelta, -} - -impl AccountChanges { - /// Get the current balance by adding the original balance and the balance - /// delta. - /// - /// *Preconditions:* - /// - `balance_delta + original_balance` must be larger than `0`. - fn current_balance(&self) -> Amount { - self.balance_delta - .apply_to_balance(self.original_balance) - .expect("Precondition violation: invalid `balance_delta`.") - } -} - -/// Data held for accounts and contracts during the execution of a contract -/// update or invocation. -#[derive(Clone, Debug)] -struct Changes { - /// The contracts which have changes. - contracts: BTreeMap, - /// The accounts which have changes. - accounts: BTreeMap, -} - -/// The set of [`Changes`] represented as a stack. -#[derive(Debug)] -struct ChangeSet { - /// The stack of changes. - stack: Vec, -} - -/// The message to use when an internal error occurs in the changeset. -const INTERNAL_CHANGESET_ERROR_MESSAGE: &str = - "Internal error: change set stack should never be empty."; - -impl ChangeSet { - /// Creates a new changeset with one empty [`Changes`] element on the - /// stack.. - fn new() -> Self { - Self { - stack: vec![Changes { - contracts: BTreeMap::new(), - accounts: BTreeMap::new(), - }], - } - } - - /// Make a checkpoint by putting a clone of the top element onto the stack. - fn checkpoint(&mut self) { - let cloned_top_element = self.current().clone(); - self.stack.push(cloned_top_element); - } - - /// Perform a rollback by popping the top element of the stack. - fn rollback(&mut self) { self.stack.pop().expect(INTERNAL_CHANGESET_ERROR_MESSAGE); } - - /// Get an immutable reference the current (latest) checkpoint. - fn current(&self) -> &Changes { self.stack.last().expect(INTERNAL_CHANGESET_ERROR_MESSAGE) } - - /// Get a mutable reference to the current (latest) checkpoint. - fn current_mut(&mut self) -> &mut Changes { - self.stack - .last_mut() - .expect(INTERNAL_CHANGESET_ERROR_MESSAGE) - } - - /// Clear all changes. - /// - /// This replaces the `Self` with a completely new `Self`. - fn clear(&mut self) { *self = Self::new() } -} - -impl Default for Chain { - fn default() -> Self { Self::new() } -} - -// Private methods -impl Chain { - /// Check whether an account exists. - fn persistence_account_exists(&self, address: AccountAddress) -> bool { - self.accounts.contains_key(&address) - } - - /// Check whether a contract exists. - fn persistence_contract_exists(&self, address: ContractAddress) -> bool { - self.contracts.contains_key(&address) - } - - /// Check whether the address exists in persistence. I.e. if it is an - /// account, whether the account exists, and if it is a contract, whether - /// the contract exists. - fn persistence_address_exists(&self, address: Address) -> bool { - match address { - Address::Account(acc) => self.persistence_account_exists(acc), - Address::Contract(contr) => self.persistence_contract_exists(contr), - } - } - - /// Make a transfer from a contract to an account in the changeset. - /// Returns the new balance of `from`. - /// - /// Precondition: - /// - Assumes that `from` contract exists. - fn changeset_transfer_contract_to_account( - &mut self, - amount: Amount, - from: ContractAddress, - to: AccountAddress, - ) -> Result { - // Ensure the `to` account exists. - if !self.persistence_account_exists(to) { - return Err(TransferError::ToMissing); - } - - // Make the transfer. - let new_balance = - self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_account_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); - - Ok(new_balance) - } - - /// Make a transfer between contracts in the changeset. - /// Returns the new balance of `from`. - /// - /// Precondition: - /// - Assumes that `from` contract exists. - fn changeset_transfer_contract_to_contract( - &mut self, - amount: Amount, - from: ContractAddress, - to: ContractAddress, - ) -> Result { - // Ensure the `to` contract exists. - if !self.persistence_contract_exists(to) { - return Err(TransferError::ToMissing); - } - - // Make the transfer. - let new_balance = - self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); - - Ok(new_balance) - } - - /// Make a transfer from an account to a contract in the changeset. - /// Returns the new balance of `from`. - /// - /// Precondition: - /// - Assumes that `from` account exists. - fn changeset_transfer_account_to_contract( - &mut self, - amount: Amount, - from: AccountAddress, - to: ContractAddress, - ) -> Result { - // Ensure the `to` account exists. - if !self.persistence_contract_exists(to) { - return Err(TransferError::ToMissing); - } - - // Make the transfer. - let new_balance = - self.changeset_change_account_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); - Ok(new_balance) - } - - // TODO: Should we handle overflows explicitly? - /// Changes the contract balance in the topmost checkpoint on the changeset. - /// If contract isn't already present in the changeset, it is added. - /// Returns the new balance. - /// - /// Precondition: - /// - Contract must exist. - fn changeset_change_contract_balance( - &mut self, - address: ContractAddress, - delta: AmountDelta, - ) -> Result { - match self.changeset.current_mut().contracts.entry(address) { - btree_map::Entry::Vacant(vac) => { - // get original balance - let original_balance = self - .contracts - .get(&address) - .expect("Precondition violation: contract assumed to exist") - .self_balance; - // Try to apply the balance or return an error if insufficient funds. - let new_contract_balance = delta.apply_to_balance(original_balance)?; - // Insert the changes into the changeset. - vac.insert(ContractChanges { - self_balance_delta: delta, - ..ContractChanges::new(original_balance) - }); - Ok(new_contract_balance) - } - btree_map::Entry::Occupied(mut occ) => { - let contract_changes = occ.get_mut(); - let new_delta = contract_changes.self_balance_delta.add_delta(delta); - // Try to apply the balance or return an error if insufficient funds. - let new_contract_balance = - new_delta.apply_to_balance(contract_changes.self_balance_original)?; - contract_changes.self_balance_delta = new_delta; - Ok(new_contract_balance) - } - } - } - - // TODO: Should we handle overflows explicitly? - /// Changes the account balance in the topmost checkpoint on the changeset. - /// If account isn't already present in the changeset, it is added. - /// Returns the new balance. - /// - /// Precondition: - /// - Account must exist. - fn changeset_change_account_balance( - &mut self, - address: AccountAddress, - delta: AmountDelta, - ) -> Result { - match self.changeset.current_mut().accounts.entry(address) { - btree_map::Entry::Vacant(vac) => { - // get original balance - let original_balance = self - .accounts - .get(&address) - .expect("Precondition violation: account assumed to exist") - .balance; - // Try to apply the balance or return an error if insufficient funds. - let new_account_balance = delta.apply_to_balance(original_balance)?; - // Insert the changes into the changeset. - vac.insert(AccountChanges { - original_balance, - balance_delta: delta, - }); - Ok(new_account_balance) - } - btree_map::Entry::Occupied(mut occ) => { - let account_changes = occ.get_mut(); - let new_delta = account_changes.balance_delta.add_delta(delta); - // Try to apply the balance or return an error if insufficient funds. - let new_account_balance = - new_delta.apply_to_balance(account_changes.original_balance)?; - account_changes.balance_delta = new_delta; - Ok(new_account_balance) - } - } - } - - /// Returns the contract balance from the topmost checkpoint on the - /// changeset. Or, alternatively, from persistence. - /// - /// *Preconditions:* - /// - Contract must exist. - fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { - self.changeset_contract_balance(address) - .expect("Precondition violation: contract must exist") - } - - /// Looks up the contract balance from the topmost checkpoint on the - /// changeset. Or, alternatively, from persistence. - fn changeset_contract_balance(&self, address: ContractAddress) -> Option { - match self.changeset.current().contracts.get(&address) { - Some(changes) => Some(changes.current_balance()), - None => self.contracts.get(&address).map(|c| c.self_balance), - } - } - - /// Returns the contract module from the topmost checkpoint on - /// the changeset. Or, alternatively, from persistence. - /// - /// *Preconditions:* - /// - Contract instance must exist (and therefore also the artifact). - /// - If the changeset contains a module reference, then it must refer a - /// deployed module. - fn changeset_contract_module(&self, address: ContractAddress) -> Arc { - match self - .changeset - .current() - .contracts - .get(&address) - .and_then(|c| c.module) - { - // Contract has been upgrade, new module exists. - Some(new_module) => Arc::clone( - self.modules - .get(&new_module) - .expect("Precondition violation: module must exist."), - ), - // Contract hasn't been upgraded. Use persisted module. - None => { - let module_ref = self - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .module_reference; - Arc::clone( - self.modules - .get(&module_ref) - .expect("Precondition violation: module must exist."), - ) - } - } - } - - /// Get the contract state, either from the changeset or by thawing it from - /// persistence. - /// - /// *Preconditions:* - /// - Contract instance must exist. - fn changeset_contract_state(&self, address: ContractAddress) -> MutableState { - match self - .changeset - .current() - .contracts - .get(&address) - .and_then(|c| c.state.clone()) - { - // Contract state has been modified. - Some(modified_state) => modified_state, - // Contract state hasn't been modified. Thaw from persistence. - None => self - .contracts - .get(&address) - .expect("Precondition violation: contract must exist") - .state - .thaw(), - } - } - - /// Looks up the account balance for an account by first checking the - /// changeset, then the persisted values. - fn changeset_account_balance(&self, address: AccountAddress) -> Option { - match self - .changeset - .current() - .accounts - .get(&address) - .map(|a| a.current_balance()) - { - // Account exists in changeset. - Some(bal) => Some(bal), - // Account doesn't exist in changeset. - None => self.accounts.get(&address).map(|a| a.balance), - } - } - - /// Try to persist all changes from the changeset. - /// - /// Always clears the changeset. - /// - /// If the energy needed for storing extra state is larger than the - /// `remaining_energy`, then: - /// - no changes will be persisted, - /// - an [`OutOfEnergy`] error is returned. - /// - /// Otherwise, it returns the [`Energy`] to be charged for the additional - /// bytes added to contract states. It also returns whether the state of the - /// provided `invoked_contract` was changed. - /// - /// *Preconditions:* - /// - All contracts, modules, accounts referred must exist in persistence. - /// - All amount deltas must be valid (i.e. not cause underflows when added - /// to balance). - fn changeset_persist_and_clear( - &mut self, - remaining_energy: Energy, - invoked_contract: ContractAddress, - ) -> Result<(Energy, bool), OutOfEnergy> { - let mut invoked_contract_has_state_changes = false; - let changes = self.changeset.current_mut(); - // Persist contract changes and collect the total increase in states sizes. - let mut collector = SizeCollector::default(); - let mut loader = v1::trie::Loader::new(&[][..]); - - let mut frozen_states: BTreeMap = BTreeMap::new(); - - // Create frozen versions of all the states, to compute the energy needed. - for (addr, changes) in changes.contracts.iter_mut() { - if let Some(modified_state) = &mut changes.state { - frozen_states.insert(*addr, modified_state.freeze(&mut loader, &mut collector)); - } - } - - // One energy per extra byte of state. - let energy_for_state_increase = Energy::from(collector.collect()); - - // Return an error if out of energy, and clear the changeset. - if remaining_energy - .checked_sub(energy_for_state_increase) - .is_none() - { - self.changeset_clear(); - return Err(OutOfEnergy); - } - - // Then persist all the changes. - for (addr, changes) in changes.contracts.iter_mut() { - let mut contract = self - .contracts - .get_mut(addr) - .expect("Precondition violation: contract must exist"); - // Update balance. - if !changes.self_balance_delta.is_zero() { - contract.self_balance = changes - .self_balance_delta - .apply_to_balance(changes.self_balance_original) - .expect("Precondition violation: amount delta causes underflow"); - } - // Update module reference. - if let Some(new_module_ref) = changes.module { - contract.module_reference = new_module_ref; - } - // Update state. - if changes.state.is_some() { - if *addr == invoked_contract { - invoked_contract_has_state_changes = true; - } - // Replace with the frozen state we created earlier. - contract.state = frozen_states - .remove(addr) - .expect("Known to exist since we just added it."); - } - } - // Persist account changes. - for (addr, changes) in changes.accounts.iter() { - let mut account = self - .accounts - .get_mut(addr) - .expect("Precondition violation: account must exist"); - // Update balance. - if !changes.balance_delta.is_zero() { - account.balance = changes - .balance_delta - .apply_to_balance(changes.original_balance) - .expect("Precondition violation: amount delta causes underflow"); - } - } - // Clear the changeset. - self.changeset_clear(); - - Ok(( - energy_for_state_increase, - invoked_contract_has_state_changes, - )) - } - - /// Traverses the last checkpoint in the changeset and collects the energy - /// needed to be charged for additional state bytes. - /// - /// Always clears the changeset. - /// - /// Returns an [`OutOfEnergy`] error if the energy needed for storing the - /// extra state is larger than `remaining_energy`. - /// - /// Otherwise, it returns - /// the [`Energy`] needed for storing the extra state. It also returns - /// whether the state of the provided `invoked_contract` has changed. - fn changeset_collect_energy_for_state_and_clear( - &mut self, - remaining_energy: Energy, - invoked_contract: ContractAddress, - ) -> Result<(Energy, bool), OutOfEnergy> { - let mut invoked_contract_has_state_changes = false; - let mut loader = v1::trie::Loader::new(&[][..]); - let mut collector = SizeCollector::default(); - for (addr, changes) in self.changeset.current_mut().contracts.iter_mut() { - if let Some(modified_state) = &mut changes.state { - if *addr == invoked_contract { - invoked_contract_has_state_changes = true; - } - modified_state.freeze(&mut loader, &mut collector); - } - } - // Clear the changeset. - self.changeset_clear(); - - // One energy per extra byte in the state. - let energy_for_state_increase = Energy::from(collector.collect()); - - if remaining_energy - .checked_sub(energy_for_state_increase) - .is_none() - { - return Err(OutOfEnergy); - } - Ok(( - energy_for_state_increase, - invoked_contract_has_state_changes, - )) - } - - /// Saves a mutable state for a contract in the changeset. - /// - /// If the contract already has an entry in the changeset, the old state - /// will be replaced. Otherwise, the entry is created and the state is - /// added. - /// - /// This also increments the modification index. It will be set to 1 if the - /// contract has no entry in the changeset. - /// - /// *Preconditions:* - /// - Contract must exist. - fn changeset_save_state_changes(&mut self, address: ContractAddress, state: &mut MutableState) { - let mut loader = v1::trie::Loader::new(&[][..]); - self.changeset - .current_mut() - .contracts - .entry(address) - .and_modify(|changes| { - changes.state = Some(state.make_fresh_generation(&mut loader)); - changes.modification_index += 1; - }) - .or_insert({ - let original_balance = self - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .self_balance; - ContractChanges { - state: Some(state.make_fresh_generation(&mut loader)), - modification_index: 1, // Increment from default, 0, to 1. - ..ContractChanges::new(original_balance) - } - }); - } - - /// Saves a new module reference for the contract in the changeset. - /// - /// If the contract already has an entry in the changeset, the old module is - /// replaced. Otherwise, the entry is created and the module is added. - /// - /// Returns the previous module, which is either the one from persistence, - /// or the most recent one from the changeset. - /// - /// *Preconditions:* - /// - Contract must exist. - /// - Module must exist. - fn changeset_save_module_upgrade( - &mut self, - address: ContractAddress, - module_reference: ModuleReference, - ) -> ModuleReference { - match self.changeset.current_mut().contracts.entry(address) { - btree_map::Entry::Vacant(vac) => { - let contract = self - .contracts - .get(&address) - .expect("Precondition violation: contract must exist."); - let old_module_ref = contract.module_reference; - let original_balance = contract.self_balance; - vac.insert(ContractChanges { - module: Some(module_reference), - ..ContractChanges::new(original_balance) - }); - old_module_ref - } - btree_map::Entry::Occupied(mut occ) => { - let changes = occ.get_mut(); - let old_module_ref = match changes.module { - Some(old_module) => old_module, - None => { - self.contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .module_reference - } - }; - changes.module = Some(module_reference); - old_module_ref - } - } - } - - /// Returns the modification index for a contract. - /// - /// It looks it up in the changeset, and if it isn't there, it will return - /// `0`. - fn changeset_modification_index(&self, address: ContractAddress) -> u32 { - self.changeset - .current() - .contracts - .get(&address) - .map_or(0, |c| c.modification_index) - } - - /// Clears the changeset. - fn changeset_clear(&mut self) { self.changeset.clear(); } - - /// Makes a new checkpoint. - fn changeset_checkpoint(&mut self) { self.changeset.checkpoint(); } - - /// Roll back to the previous checkpoint. - fn changeset_rollback(&mut self) { self.changeset.rollback(); } -} - -/// A transfer of [`Amount`]s failed because the sender had insufficient -/// balance. -#[derive(Debug)] -struct InsufficientBalanceError; - -/// An underflow occurred. -#[derive(Debug)] -struct UnderflowError; - -impl From for InsufficientBalanceError { - fn from(_: UnderflowError) -> Self { InsufficientBalanceError } -} - -impl From for TransferError { - fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } -} - -struct UpdateAuxResponse { - /// The result from the invoke. - invoke_response: InvokeResponse, - /// Will be `Some` if and only if `invoke_response` is `Success`. - logs: Option, - /// The remaining energy after the invocation. - remaining_energy: InterpreterEnergy, -} - -impl UpdateAuxResponse { - fn is_success(&self) -> bool { matches!(self.invoke_response, InvokeResponse::Success { .. }) } -} - -impl Chain { - /// Create a new [`Self`] where all the configurable parameters are - /// provided. - pub fn new_with_time_and_rates( - slot_time: SlotTime, - micro_ccd_per_euro: ExchangeRate, - euro_per_energy: ExchangeRate, - ) -> Self { - Self { - slot_time, - micro_ccd_per_euro, - euro_per_energy, - accounts: BTreeMap::new(), - modules: BTreeMap::new(), - contracts: BTreeMap::new(), - next_contract_index: 0, - changeset: ChangeSet::new(), - } - } - - /// Create a new [`Self`] with a specified `slot_time` where - /// - `micro_ccd_per_euro` defaults to `147235241 / 1` - /// - `euro_per_energy` defaults to `1 / 50000`. - pub fn new_with_time(slot_time: SlotTime) -> Self { - Self { - slot_time, - ..Self::new() - } - } - - /// Create a new [`Self`] where - /// - `slot_time` defaults to `0`, - /// - `micro_ccd_per_euro` defaults to `147235241 / 1` - /// - `euro_per_energy` defaults to `1 / 50000`. - pub fn new() -> Self { - Self::new_with_time_and_rates( - Timestamp::from_timestamp_millis(0), - ExchangeRate::new_unchecked(147235241, 1), - ExchangeRate::new_unchecked(1, 50000), - ) - } - - /// Helper function that handles the actual logic of deploying the module - /// bytes. - /// - /// Parameters: - /// - `sender`: the sender account. - /// - `module`: the raw wasm module (i.e. **without** the contract version - /// and module length bytes (8 bytes total)). - fn module_deploy_aux( - &mut self, - sender: AccountAddress, - wasm_module: WasmModule, - ) -> Result { - // Deserialize as wasm module (artifact) - let artifact = wasm_transform::utils::instantiate_with_metering::( - &ConcordiumAllowedImports { - support_upgrade: true, - }, - wasm_module.source.as_ref(), - )?; - - // Calculate transaction fee of deployment - let energy = { - // +1 for the tag, +8 for size and version - let payload_size = 1 - + 8 - + wasm_module.source.size() - + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self.persistence_get_account(sender)?.signature_count; - let base_cost = cost::base_cost(payload_size, number_of_sigs); - let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); - base_cost + deploy_module_cost - }; - let transaction_fee = self.calculate_energy_cost(energy); - println!( - "Deploying module with size {}, resulting in {} NRG.", - wasm_module.source.size(), - energy - ); - - // Try to subtract cost for account - let account = self.persistence_get_account_mut(sender)?; - if account.balance < transaction_fee { - return Err(DeployModuleError::InsufficientFunds); - }; - account.balance -= transaction_fee; - - // Save the module TODO: Use wasm_module.get_module_ref() and find a proper way - // to convert ModuleRef to ModuleReference. - let module_reference = { - let mut hasher = Sha256::new(); - hasher.update(wasm_module.source.as_ref()); - let hash: [u8; 32] = hasher.finalize().into(); - ModuleReference::from(hash) - }; - // Ensure module hasn't been deployed before. - if self.modules.contains_key(&module_reference) { - return Err(DeployModuleError::DuplicateModule(module_reference)); - } - self.modules.insert(module_reference, Arc::new(artifact)); - Ok(SuccessfulModuleDeployment { - module_reference, - energy, - transaction_fee, - }) - } - - /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version - /// bytes and 4 module length bytes. - /// The module still has to a valid V1 smart contract module. - /// - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. - pub fn module_deploy_wasm_v1>( - &mut self, - sender: AccountAddress, - module_path: P, - ) -> Result { - // Load file - let file_contents = std::fs::read(module_path)?; - let wasm_module = WasmModule { - version: WasmVersion::V1, - source: ModuleSource::from(file_contents), - }; - self.module_deploy_aux(sender, wasm_module) - } - - /// Deploy a v1 wasm module as it is output from `cargo concordium build`, - /// i.e. **including** the prefix of 4 version bytes and 4 module length - /// bytes. - /// - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. - pub fn module_deploy_v1>( - &mut self, - sender: AccountAddress, - module_path: P, - ) -> Result { - // Load file - let file_contents = std::fs::read(module_path)?; - let mut cursor = std::io::Cursor::new(file_contents); - let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor)?; - - if wasm_module.version != WasmVersion::V1 { - return Err(DeployModuleError::UnsupportedModuleVersion( - wasm_module.version, - )); - } - self.module_deploy_aux(sender, wasm_module) - } - - /// Initialize a contract. - /// - /// - `sender`: The account paying for the transaction. Will also become the - /// owner of the instance created. - /// - `module_reference`: The reference to the a module that has already - /// been deployed. - /// - `contract_name`: Name of the contract to initialize. - /// - `parameter`: Parameter provided to the init method. - /// - `amount`: The initial balance of the contract. Subtracted from the - /// `sender` account. - /// - `energy_reserved`: Amount of energy reserved for executing the init - /// method. - pub fn contract_init( - &mut self, - sender: AccountAddress, - module_reference: ModuleReference, - contract_name: ContractName, - parameter: OwnedParameter, - amount: Amount, - energy_reserved: Energy, - ) -> Result { - // Lookup artifact - let artifact = self.persistence_contract_module(module_reference)?; - let mut transaction_fee = self.calculate_energy_cost(self.lookup_module_cost(&artifact)); - // Get the account and check that it has sufficient balance to pay for the - // reserved_energy and amount. - let account_info = self.persistence_get_account(sender)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { - return Err(ContractInitError::InsufficientFunds); - } - // Construct the context. - let init_ctx = v0::InitContext { - metadata: ChainMetadata { - slot_time: self.slot_time, - }, - init_origin: sender, - sender_policies: account_info.policies.clone(), - }; - // Initialize contract - let mut loader = v1::trie::Loader::new(&[][..]); - let res = v1::invoke_init( - artifact, - init_ctx, - v1::InitInvocation { - amount, - init_name: contract_name.get_chain_name(), - parameter: ¶meter.0, - energy: Chain::to_interpreter_energy(energy_reserved), - }, - false, - loader, - ); - // Handle the result and update the transaction fee. - // TODO: Extract to helper function. - let res = match res { - Ok(v1::InitResult::Success { - logs, - return_value: _, /* Ignore return value for now, since our tools do not support - * it for inits, currently. */ - remaining_energy, - mut state, - }) => { - let contract_address = self.create_contract_address(); - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - - let mut collector = v1::trie::SizeCollector::default(); - - let contract_instance = Contract { - module_reference, - contract_name: contract_name.to_owned(), - state: state.freeze(&mut loader, &mut collector), // TODO: Charge for storage. - owner: sender, - self_balance: amount, - }; - - // Save the contract instance - self.contracts.insert(contract_address, contract_instance); - // Subtract the from the invoker. - self.persistence_get_account_mut(sender)?.balance -= amount; - - Ok(SuccessfulContractInit { - contract_address, - logs, - energy_used, - transaction_fee, - }) - } - Ok(v1::InitResult::Reject { - reason, - return_value, - remaining_energy, - }) => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - }, - )) - } - Ok(v1::InitResult::Trap { - error: _, // TODO: Should we forward this to the user? - remaining_energy, - }) => { - let energy_used = energy_reserved - - Chain::from_interpreter_energy(InterpreterEnergy { - energy: remaining_energy, - }); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Trap { - energy_used, - transaction_fee, - }, - )) - } - Ok(v1::InitResult::OutOfEnergy) => { - transaction_fee += self.calculate_energy_cost(energy_reserved); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )) - } - Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), - }; - // Charge the account. - // We have to get the account info again because of the borrow checker. - self.persistence_get_account_mut(sender)?.balance -= transaction_fee; - res - } - - /// Used for handling contract invokes internally. - /// - /// *Preconditions:* - /// - `invoker` exists - /// - `invoker` has sufficient balance to pay for `remaining_energy` - /// - `sender` exists - /// - if the contract (`contract_address`) exists, then its `module` must - /// also exist. - fn contract_update_aux( - &mut self, - invoker: AccountAddress, - sender: Address, - contract_address: ContractAddress, - entrypoint: OwnedEntrypointName, - parameter: Parameter, - amount: Amount, - // The CCD amount reserved from the invoker account. While the amount - // is reserved, it is not subtracted in the chain.accounts map. - // Used to handle account balance queries for the invoker account. - invoker_amount_reserved_for_nrg: Amount, - // Uses [`Interpreter`] energy to avoid rounding issues when converting to and fro - // [`Energy`]. - mut remaining_energy: InterpreterEnergy, - chain_events: &mut Vec, - ) -> UpdateAuxResponse { - // Move the amount from the sender to the contract, if any. - // And get the new self_balance. - let instance_self_balance = if amount.micro_ccd() > 0 { - let transfer_result = match sender { - Address::Account(sender_account) => self.changeset_transfer_account_to_contract( - amount, - sender_account, - contract_address, - ), - Address::Contract(sender_contract) => self.changeset_transfer_contract_to_contract( - amount, - sender_contract, - contract_address, - ), - }; - match transfer_result { - Ok(new_balance_from) => new_balance_from, - Err(transfer_error) => { - let kind = match transfer_error { - TransferError::InsufficientBalance => InvokeFailure::InsufficientAmount, - TransferError::ToMissing => InvokeFailure::NonExistentContract, - }; - // Return early. - // TODO: Should we charge any energy for this? - return UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { kind }, - logs: None, - remaining_energy, - }; - } - } - } else { - match self.changeset_contract_balance(contract_address) { - Some(self_balance) => self_balance, - None => { - // Return early. - // TODO: For the top-most update, we should catch this in `contract_update` and - // return `ContractUpdateError::EntrypointMissing`. - return UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { - kind: InvokeFailure::NonExistentContract, - }, - logs: None, - remaining_energy, - }; - } - } - }; - - // Get the instance and artifact. To be used in several places. - let instance = self - .persistence_get_contract(contract_address) - .expect("Contract known to exist at this point"); - let artifact = self.changeset_contract_module(contract_address); - - // Subtract the cost of looking up the module - remaining_energy = remaining_energy - .subtract(Chain::to_interpreter_energy(self.lookup_module_cost(&artifact)).energy); - - // Construct the receive name (or fallback receive name) and ensure its presence - // in the contract. - let receive_name = { - let contract_name = instance.contract_name.as_contract_name().contract_name(); - let receive_name = format!("{}.{}", contract_name, entrypoint); - let fallback_receive_name = format!("{}.", contract_name); - if artifact.has_entrypoint(receive_name.as_str()) { - OwnedReceiveName::new_unchecked(receive_name) - } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { - OwnedReceiveName::new_unchecked(fallback_receive_name) - } else { - // Return early. - return UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { - kind: InvokeFailure::NonExistentEntrypoint, - }, - logs: None, - remaining_energy, - }; - } - }; - - // Construct the receive context - let receive_ctx = v1::ReceiveContext { - entrypoint: entrypoint.to_owned(), - common: v0::ReceiveContext { - metadata: ChainMetadata { - slot_time: self.slot_time, - }, - invoker, - self_address: contract_address, - self_balance: instance_self_balance, - sender, - owner: instance.owner, - sender_policies: self - .persistence_get_account(invoker) - .expect("Precondition violation: invoker must exist.") - .policies - .clone(), - }, - }; - - let contract_name = instance.contract_name.clone(); - - // Construct the instance state - let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = self.changeset_contract_state(contract_address); - let inner = mutable_state.get_inner(&mut loader); - let instance_state = v1::InstanceState::new(loader, inner); - - // Get the initial result from invoking receive - let initial_result = v1::invoke_receive( - artifact, - receive_ctx, - v1::ReceiveInvocation { - amount, - receive_name: receive_name.as_receive_name(), - parameter: parameter.0, - energy: remaining_energy, - }, - instance_state, - v1::ReceiveParams { - max_parameter_size: 65535, - limit_logs_and_return_values: false, - support_queries: true, - }, - ); - - // Set up some data needed for recursively processing the receive until the end, - // i.e. beyond interrupts. - let mut data = ProcessReceiveData { - invoker, - address: contract_address, - contract_name, - amount, - invoker_amount_reserved_for_nrg, - entrypoint, - chain: self, - state: mutable_state, - chain_events: Vec::new(), - loader, - }; - - // Process the receive invocation to the completion. - let result = data.process(initial_result); - let mut new_chain_events = data.chain_events; - - let result = match result { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, - state_changed: _, /* This only reflects changes since last interrupt, we use - * the changeset later to get a more precise result. */ - return_value, - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { - invoke_response: InvokeResponse::Success { - new_balance: self - .changeset_contract_balance_unchecked(contract_address), - data: Some(return_value), - }, - logs: Some(logs), - remaining_energy, - } - } - v1::ReceiveResult::Interrupt { .. } => { - panic!("Internal error: `data.process` returned an interrupt.") - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { - kind: InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, - }, - logs: None, - remaining_energy, - } - } - v1::ReceiveResult::Trap { - error: _, // TODO: Should we return this to the user? - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { - kind: InvokeFailure::RuntimeError, - }, - logs: None, - remaining_energy, - } - } - v1::ReceiveResult::OutOfEnergy => { - let remaining_energy = InterpreterEnergy::from(0); - UpdateAuxResponse { - invoke_response: InvokeResponse::Failure { - kind: InvokeFailure::RuntimeError, - }, - logs: None, - remaining_energy, - } - } - }, - Err(internal_error) => { - panic!("Internal error: Got interpreter error {}", internal_error) - } - }; - - // Append the new chain events if the invocation succeeded. - if result.is_success() { - chain_events.append(&mut new_chain_events); - } - - result - } - - /// Update a contract by calling one of its entrypoints. - /// - /// If successful, any changes will be saved. - pub fn contract_update( - &mut self, - invoker: AccountAddress, - sender: Address, - contract_address: ContractAddress, - entrypoint: EntrypointName, - parameter: OwnedParameter, - amount: Amount, - energy_reserved: Energy, - ) -> Result { - println!( - "Updating contract {}, with parameter: {:?}", - contract_address, parameter.0 - ); - - // Ensure the sender exists. - if !self.persistence_address_exists(sender) { - return Err(ContractUpdateError::SenderDoesNotExist(sender)); - } - - // Ensure account exists and can pay for the reserved energy and amount - // TODO: Could we just remove this amount in the changeset and then put back the - // to_ccd(remaining_energy) afterwards? - let account_info = self.persistence_get_account(invoker)?; - let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); - if account_info.balance < invoker_amount_reserved_for_nrg + amount { - return Err(ContractUpdateError::InsufficientFunds); - } - - // TODO: Should chain events be part of the changeset? - let mut chain_events = Vec::new(); - let result = self.contract_update_aux( - invoker, - sender, - contract_address, - entrypoint.to_owned(), - Parameter(¶meter.0), - amount, - invoker_amount_reserved_for_nrg, - Chain::to_interpreter_energy(energy_reserved), - &mut chain_events, - ); - - // Get the energy to be charged for extra state bytes. Or return an error if out - // of energy. - let (energy_for_state_increase, state_changed) = if result.is_success() { - match self.changeset_persist_and_clear( - Chain::from_interpreter_energy(result.remaining_energy), - contract_address, - ) { - Ok(energy) => energy, - Err(_) => { - return Err(ContractUpdateError::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }); - } - } - } else { - // An error occured, so we don't save the changes. Just clear. - self.changeset_clear(); - (Energy::from(0), false) - }; - - let (res, transaction_fee) = self.convert_update_aux_response( - result, - chain_events, - energy_reserved, - energy_for_state_increase, - state_changed, - ); - - // Charge the transaction fee irrespective of the result. - // TODO: If we charge up front, then we should return to_ccd(remaining_energy) - // here instead. - self.persistence_get_account_mut(invoker)?.balance -= transaction_fee; - res - } - - /// Invoke a contract by calling an entrypoint. - /// - /// Similar to [`contract_update`] except that all changes are discarded - /// afterwards. Typically used for "view" functions. - pub fn contract_invoke( - &mut self, - invoker: AccountAddress, - sender: Address, - contract_address: ContractAddress, - entrypoint: EntrypointName, - parameter: OwnedParameter, - amount: Amount, - energy_reserved: Energy, - ) -> Result { - println!( - "Invoking contract {}, with parameter: {:?}", - contract_address, parameter.0 - ); - - // Ensure the sender exists. - if !self.persistence_address_exists(sender) { - return Err(ContractUpdateError::SenderDoesNotExist(sender)); - } - - // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.persistence_get_account(invoker)?; - let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); - if account_info.balance < invoker_amount_reserved_for_nrg + amount { - return Err(ContractUpdateError::InsufficientFunds); - } - - let mut chain_events = Vec::new(); - let result = self.contract_update_aux( - invoker, - sender, - contract_address, - entrypoint.to_owned(), - Parameter(¶meter.0), - amount, - invoker_amount_reserved_for_nrg, - Chain::to_interpreter_energy(energy_reserved), - &mut chain_events, - ); - - let (energy_for_state_increase, state_changed) = if result.is_success() { - match self.changeset_collect_energy_for_state_and_clear( - Chain::from_interpreter_energy(result.remaining_energy), - contract_address, - ) { - Ok(energy) => energy, - Err(_) => { - return Err(ContractUpdateError::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }); - } - } - } else { - // An error occured, so we just clear the changeset. - self.changeset_clear(); - (Energy::from(0), false) - }; - - let (result, _) = self.convert_update_aux_response( - result, - chain_events, - energy_reserved, - energy_for_state_increase, - state_changed, - ); - - result - } - - /// Create an account. Will override existing account if already present. - pub fn create_account(&mut self, account: AccountAddress, account_info: AccountInfo) { - self.accounts.insert(account, account_info); - } - - /// Creates a contract address with an index one above the highest - /// currently used. Next call to `contract_init` will skip this - /// address. - pub fn create_contract_address(&mut self) -> ContractAddress { - let index = self.next_contract_index; - let subindex = 0; - self.next_contract_index += 1; - ContractAddress::new(index, subindex) - } - - /// Set the chain's slot time. - pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } - - /// Set the chain's Euro per NRG conversion rate. - pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { - self.euro_per_energy = euro_per_energy; - } - - /// Set the chain's microCCD per Euro conversion rate. - pub fn set_micro_ccd_per_euro(&mut self, micro_ccd_per_euro: ExchangeRate) { - self.micro_ccd_per_euro = micro_ccd_per_euro; - } - - /// Returns the balance of an account if it exists. - /// This will always be the persisted account balance. - pub fn persistence_account_balance(&self, address: AccountAddress) -> Option { - self.accounts.get(&address).map(|ai| ai.balance) - } - - /// Returns the balance of an contract if it exists. - /// This will always be the persisted contract balance. - pub fn persistence_contract_balance(&self, address: ContractAddress) -> Option { - self.contracts.get(&address).map(|ci| ci.self_balance) - } - - /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange - /// rates available: - // TODO: Find a way to make this parse the doc tests - // To find the mCCD/NRG exchange rate: - // - // euro mCCD euro * mCCD mCCD - // ---- * ---- = ----------- = ---- - // NRG euro NRG * euro NRG - // - // To convert the `energy` parameter to mCCD: - // - // mCCD NRG * mCCD - // NRG * ---- = ---------- = mCCD - // NRG NRG - pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { - let micro_ccd_per_energy_numerator: BigUint = - BigUint::from(self.euro_per_energy.numerator()) * self.micro_ccd_per_euro.numerator(); - let micro_ccd_per_energy_denominator: BigUint = - BigUint::from(self.euro_per_energy.denominator()) - * self.micro_ccd_per_euro.denominator(); - let cost: BigUint = - (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; - let cost: u64 = u64::try_from(cost).expect("Should never overflow due to use of BigUint"); - Amount::from_micro_ccd(cost) - } - - /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. - fn to_interpreter_energy(energy: Energy) -> InterpreterEnergy { - InterpreterEnergy { - energy: energy.energy * 1000, - } - } - - /// Convert [`InterpreterEnergy`] to [`Energy`] by dividing by `1000`. - fn from_interpreter_energy(interpreter_energy: InterpreterEnergy) -> Energy { - Energy { - energy: interpreter_energy.energy / 1000, - } - } - - /// Calculate the energy energy for looking up a [`ContractModule`]. - fn lookup_module_cost(&self, module: &ContractModule) -> Energy { - // TODO: Is it just the `.code`? - // Comes from Concordium/Cost.hs::lookupModule - Energy::from(module.code.len() as u64 / 50) - } - - /// Returns an Arc clone of the [`ContractModule`] from persistence. - fn persistence_contract_module( - &self, - module_ref: ModuleReference, - ) -> Result, ModuleMissing> { - let module = self - .modules - .get(&module_ref) - .ok_or(ModuleMissing(module_ref))?; - Ok(Arc::clone(module)) - } - - /// Returns an immutable reference to a [`Contract`] from persistence. - fn persistence_get_contract( - &self, - address: ContractAddress, - ) -> Result<&Contract, ContractInstanceMissing> { - self.contracts - .get(&address) - .ok_or(ContractInstanceMissing(address)) - } - - /// Returns an immutable reference to [`AccountInfo`] from persistence. - fn persistence_get_account( - &self, - address: AccountAddress, - ) -> Result<&AccountInfo, AccountMissing> { - self.accounts.get(&address).ok_or(AccountMissing(address)) - } - - /// Returns a mutable reference to [`AccountInfo`] from persistence. - fn persistence_get_account_mut( - &mut self, - address: AccountAddress, - ) -> Result<&mut AccountInfo, AccountMissing> { - self.accounts - .get_mut(&address) - .ok_or(AccountMissing(address)) - } - - /// Convert the wasm_chain_integration result to the one used here and - /// calculate the transaction fee. - /// - /// The `energy_for_state_increase` is only used if the result was a - /// success. - /// - /// The `state_changed` should refer to whether the state of the top-level - /// contract invoked has changed. - /// - /// *Preconditions*: - /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` - fn convert_update_aux_response( - &self, - update_aux_response: UpdateAuxResponse, - chain_events: Vec, - energy_reserved: Energy, - energy_for_state_increase: Energy, - state_changed: bool, - ) -> ( - Result, - Amount, - ) { - let remaining_energy = Chain::from_interpreter_energy(update_aux_response.remaining_energy); - match update_aux_response.invoke_response { - InvokeResponse::Success { new_balance, data } => { - let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; - let transaction_fee = self.calculate_energy_cost(energy_used); - let result = Ok(SuccessfulContractUpdate { - chain_events, - energy_used, - transaction_fee, - return_value: data.unwrap_or_default(), - state_changed, - new_balance, - logs: update_aux_response.logs.unwrap_or_default(), - }); - (result, transaction_fee) - } - InvokeResponse::Failure { kind } => { - let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; - let transaction_fee = self.calculate_energy_cost(energy_used); - let result = Err(ContractUpdateError::ExecutionError { - failure_kind: kind, - energy_used, - transaction_fee, - }); - (result, transaction_fee) - } - } - } -} - -/// Errors related to transfers. -#[derive(PartialEq, Eq, Debug, Error)] -enum TransferError { - /// The receiver does not exist. - #[error("The receiver does not exist.")] - ToMissing, - /// The sender does not have sufficient balance. - #[error("The sender does not have sufficient balance.")] - InsufficientBalance, -} - -/// The contract ran out of energy during execution of an update or invocation. -#[derive(PartialEq, Eq, Debug)] -struct OutOfEnergy; - -/// The entrypoint does not exist. -#[derive(PartialEq, Eq, Debug, Error)] -#[error("The entrypoint '{0}' does not exist.")] -pub struct EntrypointDoesNotExist(OwnedEntrypointName); - -/// Data needed to recursively process a contract update or invocation to -/// completion. -struct ProcessReceiveData<'a, 'b> { - /// The invoker. - invoker: AccountAddress, - /// The contract being called. - address: ContractAddress, - /// The name of the contract. - contract_name: OwnedContractName, - /// The amount sent from the sender to the contract. - amount: Amount, - /// The CCD amount reserved from the invoker account for the energy. While - /// the amount is reserved, it is not subtracted in the chain.accounts - /// map. Used to handle account balance queries for the invoker account. - /// TODO: We could use a changeset for accounts -> balance, and then look up - /// the "chain.accounts" values for chain queries. - invoker_amount_reserved_for_nrg: Amount, - /// The entrypoint to execute. - entrypoint: OwnedEntrypointName, - /// A reference to the chain. - chain: &'a mut Chain, - /// The current state. - state: MutableState, - /// Chain events that have occurred during the execution. - chain_events: Vec, - /// - loader: v1::trie::Loader<&'b [u8]>, -} - -impl<'a, 'b> ProcessReceiveData<'a, 'b> { - /// Process a receive function until completion. - /// - /// *Preconditions*: - /// - Contract instance exists in `chain.contracts`. - /// - Account exists in `chain.accounts`. - fn process( - &mut self, - res: ExecResult>, - ) -> ExecResult> { - match res { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - println!("\tSuccessful contract update {}", self.address); - let update_event = ChainEvent::Updated { - address: self.address, - contract: self.contract_name.clone(), - entrypoint: self.entrypoint.clone(), - amount: self.amount, - }; - // Add update event - self.chain_events.push(update_event); - - // Save changes to changeset. - if state_changed { - self.chain - .changeset_save_state_changes(self.address, &mut self.state); - } - - Ok(v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - }) - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs, - config, - interrupt, - } => { - println!("\tInterrupting contract {}", self.address); - - // Create the interrupt event, which will be included for transfers, calls, and - // upgrades, but not for the remaining interrupts. - let interrupt_event = ChainEvent::Interrupted { - address: self.address, - logs, - }; - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - // Add the interrupt event - self.chain_events.push(interrupt_event); - - println!("\t\tTransferring {} CCD to {}", amount, to); - - let response = match self.chain.changeset_transfer_contract_to_account( - amount, - self.address, - to, - ) { - Ok(new_balance) => InvokeResponse::Success { - new_balance, - data: None, - }, - Err(err) => { - let kind = match err { - TransferError::ToMissing => { - InvokeFailure::NonExistentAccount - } - TransferError::InsufficientBalance => { - InvokeFailure::InsufficientAmount - } - }; - InvokeResponse::Failure { kind } - } - }; - - let success = matches!(response, InvokeResponse::Success { .. }); - if success { - // Add transfer event - self.chain_events.push(ChainEvent::Transferred { - from: self.address, - amount, - to, - }); - } - // Add resume event - self.chain_events.push(ChainEvent::Resumed { - address: self.address, - success, - }); - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(remaining_energy), - &mut self.state, - false, // never changes on transfers - self.loader, - ); - - // Resume - self.process(resume_res) - } - v1::Interrupt::Call { - address, - parameter, - name, - amount, - } => { - // Add the interrupt event - self.chain_events.push(interrupt_event); - - if state_changed { - self.chain - .changeset_save_state_changes(self.address, &mut self.state); - } - - // Save the modification index before the invoke. - let mod_idx_before_invoke = - self.chain.changeset_modification_index(self.address); - - // Make a checkpoint before calling another contract so that we may roll - // back. - self.chain.changeset_checkpoint(); - - if VERBOSE_DEBUG { - println!( - "Before call (after checkpoint): {:#?}", - self.chain.changeset.current() - ); - } - - println!( - "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", - address, parameter - ); - - let res = self.chain.contract_update_aux( - self.invoker, - Address::Contract(self.address), - address, - name, - Parameter(¶meter), - amount, - self.invoker_amount_reserved_for_nrg, - InterpreterEnergy::from(remaining_energy), - &mut self.chain_events, - ); - - let success = res.is_success(); - - // Remove the last state changes if the invocation failed. - let state_changed = if !success { - self.chain.changeset_rollback(); - false // We rolled back, so no changes were made - // to this contract. - } else { - let mod_idx_after_invoke = - self.chain.changeset_modification_index(self.address); - let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; - if state_changed { - // Update the state field with the newest value from the - // changeset. - self.state = self.chain.changeset_contract_state(self.address); - } - state_changed - }; - - if VERBOSE_DEBUG { - println!( - "After call (and potential rollback):\n{:#?}", - self.chain.changeset.current() - ); - } - - println!( - "\tResuming contract {}\n\t\tafter {}", - self.address, - if success { - "succesful invocation" - } else { - "failed invocation" - } - ); - - // Add resume event - let resume_event = ChainEvent::Resumed { - address: self.address, - success, - }; - - self.chain_events.push(resume_event); - - let resume_res = v1::resume_receive( - config, - res.invoke_response, - res.remaining_energy, - &mut self.state, - state_changed, - self.loader, - ); - - self.process(resume_res) - } - v1::Interrupt::Upgrade { module_ref } => { - println!("Upgrading contract to {:?}", module_ref); - - // Add the interrupt event. - self.chain_events.push(interrupt_event); - - // Charge a base cost. - let mut energy_after_invoke = remaining_energy - - Chain::to_interpreter_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST, - ) - .energy; - - let response = match self.chain.modules.get(&module_ref) { - None => InvokeResponse::Failure { - kind: InvokeFailure::UpgradeInvalidModuleRef, - }, - Some(module) => { - // Charge for the module lookup. - energy_after_invoke -= Chain::to_interpreter_energy( - self.chain.lookup_module_cost(module), - ) - .energy; - - if module.export.contains_key( - self.contract_name.as_contract_name().get_chain_name(), - ) { - // Update module reference in the changeset. - let old_module_ref = - self.chain.changeset_save_module_upgrade( - self.address, - module_ref, - ); - - // Charge for the initialization cost. - energy_after_invoke -= Chain::to_interpreter_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - ) - .energy; - - let upgrade_event = ChainEvent::Upgraded { - address: self.address, - from: old_module_ref, - to: module_ref, - }; - - self.chain_events.push(upgrade_event); - - InvokeResponse::Success { - new_balance: self - .chain - .changeset_contract_balance_unchecked(self.address), - data: None, - } - } else { - InvokeResponse::Failure { - kind: InvokeFailure::UpgradeInvalidContractName, - } - } - } - }; - - let success = matches!(response, InvokeResponse::Success { .. }); - self.chain_events.push(ChainEvent::Resumed { - address: self.address, - success, - }); - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - state_changed, - self.loader, - ); - - self.process(resume_res) - } - v1::Interrupt::QueryAccountBalance { address } => { - println!("\t\tQuerying account balance of {}", address); - // When querying an account, the amounts from any `invoke_transfer`s - // should be included. That is handled by - // the `chain` struct already. transaction. - // However, that is hand - let response = match self.chain.changeset_account_balance(address) { - Some(acc_bal) => { - // If you query the invoker account, it should also - // take into account the send-amount and the amount reserved for - // the reserved max energy. The former is handled in - // `contract_update_aux`, but the latter is represented in - // `self.invoker_amount_reserved_for_nrg`. - let acc_bal = if address == self.invoker { - acc_bal - self.invoker_amount_reserved_for_nrg - } else { - acc_bal - }; - - // TODO: Do we need non-zero staked and shielded balances? - let balances = - to_bytes(&(acc_bal, Amount::zero(), Amount::zero())); - InvokeResponse::Success { - new_balance: self - .chain - .changeset_contract_balance_unchecked(self.address), - data: Some(balances), - } - } - None => InvokeResponse::Failure { - kind: InvokeFailure::NonExistentAccount, - }, - }; - - let energy_after_invoke = remaining_energy - - Chain::to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - self.loader, - ); - - self.process(resume_res) - } - v1::Interrupt::QueryContractBalance { address } => { - println!("Querying contract balance of {}", address); - - let response = match self.chain.changeset_contract_balance(address) { - None => InvokeResponse::Failure { - kind: InvokeFailure::NonExistentContract, - }, - Some(bal) => InvokeResponse::Success { - // Balance of contract querying. Won't change. Notice the - // `self.address`. - new_balance: self - .chain - .changeset_contract_balance_unchecked(self.address), - data: Some(to_bytes(&bal)), - }, - }; - - let energy_after_invoke = remaining_energy - - Chain::to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - self.loader, - ); - - self.process(resume_res) - } - v1::Interrupt::QueryExchangeRates => { - println!("Querying exchange rates"); - - let exchange_rates = - (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); - - let response = InvokeResponse::Success { - new_balance: self - .chain - .changeset_contract_balance_unchecked(self.address), - data: Some(to_bytes(&exchange_rates)), - }; - - let energy_after_invoke = remaining_energy - - Chain::to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - self.loader, - ); - - self.process(resume_res) - } - } - } - x => Ok(x), - }, - Err(e) => Err(e), - } - } -} - -/// The contract module does not exist. -#[derive(Debug, Error)] -#[error("Module {:?} does not exist.", 0)] -pub struct ModuleMissing(ModuleReference); - -/// The contract instance does not exist. -#[derive(Debug, Error)] -#[error("Contract instance {0} does not exist.")] -pub struct ContractInstanceMissing(ContractAddress); - -/// The account does not exist. -#[derive(Debug, Error)] -#[error("Account {0} does not exist.")] -pub struct AccountMissing(AccountAddress); - -/// Data about an [`AccountAddress`]. -#[derive(Clone)] -pub struct AccountInfo { - /// The account balance. TODO: Add all three types of balances. - pub balance: Amount, - /// Account policies. - policies: v0::OwnedPolicyBytes, - /// The number of signatures. The number of signatures affect the cost of - /// every transaction for the account. - signature_count: u32, -} - -/// Account policies for testing. -pub struct TestPolicies(v0::OwnedPolicyBytes); - -impl TestPolicies { - // TODO: Make correctly structured policies ~= Vec>. - pub fn empty() -> Self { Self(v0::OwnedPolicyBytes::new()) } - - // TODO: Add helper functions for creating arbitrary valid policies. -} - -impl AccountInfo { - /// Create a new [`Self`] with the provided parameters. - /// The `signature_count` must be >= 1 for transaction costs to be - /// realistic. - pub fn new_with_policy_and_signature_count( - balance: Amount, - policies: TestPolicies, - signature_count: u32, - ) -> Self { - Self { - balance, - policies: policies.0, - signature_count, - } - } - - /// Create new [`Self`] with empty account policies but the provided - /// `signature_count`. The `signature_count` must be >= 1 for transaction - /// costs to be realistic. - pub fn new_with_signature_count(balance: Amount, signature_count: u32) -> Self { - Self { - signature_count, - ..Self::new(balance) - } - } - - /// Create new [`Self`] with empty account policies and a signature - /// count of `1`. - pub fn new_with_policy(balance: Amount, policies: TestPolicies) -> Self { - Self { - balance, - policies: policies.0, - signature_count: 1, - } - } - - /// Create new [`Self`] with empty account policies and a signature - /// count of `1`. - pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } -} - -/// Errors that can occur while initializing a contract. -#[derive(Debug, Error)] -pub enum ContractInitError { - /// Initialization failed for a reason that also exists on the chain. - #[error("failed due to a valid chain error: {:?}", 0)] - ValidChainError(FailedContractInteraction), - /// Module has not been deployed in the test environment. - #[error("module {:?} does not exist", 0.0)] - ModuleDoesNotExist(#[from] ModuleMissing), - /// Account has not been created in test environment. - #[error("account {} does not exist", 0.0)] - AccountDoesNotExist(#[from] AccountMissing), - /// The account does not have enough funds to pay for the energy. - #[error("account does not have enough funds to pay for the energy")] - InsufficientFunds, -} - -/// Errors that can occur during a [`Chain::contract_update]` or -/// [`Chain::contract_invoke`] call. -/// -/// There are two categories of errors here: -/// - `ExecutionError` and `OutOfEnergy` can occur if the preconditions for the -/// function is valid, and a contract is executed. -/// - The rest represent incorrect usage of the function, where some -/// precondition wasn't met. -#[derive(Debug, Error)] -pub enum ContractUpdateError { - /// Update failed for a reason that also exists on the chain. - #[error("failed during execution")] - ExecutionError { - failure_kind: InvokeFailure, - energy_used: Energy, - transaction_fee: Amount, - }, - #[error("ran out of energy")] - OutOfEnergy { - energy_used: Energy, - transaction_fee: Amount, - }, - /// Module has not been deployed in test environment. - #[error("module {:?} does not exist", 0.0)] - ModuleDoesNotExist(#[from] ModuleMissing), - /// Contract instance has not been initialized in the test environment. - #[error("instance {} does not exist", 0.0)] - InstanceDoesNotExist(#[from] ContractInstanceMissing), - /// Entrypoint does not exist and neither does the fallback entrypoint. - #[error("entrypoint does not exist")] - EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), - /// The invoker account has not been created in the test environment. - #[error("invoker account {} does not exist", 0.0)] - InvokerDoesNotExist(#[from] AccountMissing), - /// The sender does not exist in the test environment. - #[error("sender {0} does not exist")] - SenderDoesNotExist(Address), - /// The account does not have enough funds to pay for the energy. - #[error("account does not have enough funds to pay for the energy")] - InsufficientFunds, -} - -/// Represents a failed contract interaction, i.e. an initialization, update, or -/// invocation. -#[derive(Debug)] -pub enum FailedContractInteraction { - /// The contract rejected. - Reject { - /// The error code for why it rejected. - reason: i32, - /// The return value. - return_value: ReturnValue, - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, - /// The contract trapped. - Trap { - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, - /// The contract ran out of energy. - OutOfEnergy { - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, -} - -impl FailedContractInteraction { - /// Get the transaction fee. - pub fn transaction_fee(&self) -> Amount { - match self { - FailedContractInteraction::Reject { - transaction_fee, .. - } => *transaction_fee, - FailedContractInteraction::Trap { - transaction_fee, .. - } => *transaction_fee, - FailedContractInteraction::OutOfEnergy { - transaction_fee, .. - } => *transaction_fee, - } - } -} - -/// An error that can occur while deploying a [`ContractModule`]. -// TODO: Can we get Eq for this when using io::Error? -// TODO: Should this also have the energy used? -#[derive(Debug, Error)] -pub enum DeployModuleError { - /// Failed to read the module file. - #[error("could not read the file due to: {0}")] - ReadFileError(#[from] std::io::Error), - /// The module provided is not valid. - #[error("module is invalid due to: {0}")] - InvalidModule(#[from] anyhow::Error), - /// The account does not have sufficient funds to pay for the deployment. - #[error("account does not have sufficient funds to pay for the energy")] - InsufficientFunds, - /// The account deploying the module does not exist. - #[error("account {} does not exist", 0.0)] - AccountDoesNotExist(#[from] AccountMissing), - /// The module version is not supported. - #[error("wasm version {0} is not supported")] - UnsupportedModuleVersion(WasmVersion), - /// The module has already been deployed. - #[error("module with reference {:?} already exists", 0)] - DuplicateModule(ModuleReference), -} - -/// An event that occurred during a contract update or invocation. -#[derive(Debug)] -pub enum ChainEvent { - /// A contract was interrupted. - Interrupted { - /// The contract interrupted. - address: ContractAddress, - /// Logs produced prior to being interrupted. - logs: v0::Logs, - }, - /// A contract was resumed after being interrupted. - Resumed { - /// The contract resumed. - address: ContractAddress, - /// Whether the action that caused the interrupt succeeded. - success: bool, - }, - /// A contract was upgraded. - Upgraded { - /// The contract upgraded. - address: ContractAddress, - /// The old module reference. - from: ModuleReference, - /// The new module reference. - to: ModuleReference, - }, - /// A contract was updated. - Updated { - /// The contract updated. - address: ContractAddress, - /// The name of the contract. - contract: OwnedContractName, - /// The entrypoint called. - entrypoint: OwnedEntrypointName, - /// The amount added to the contract. - amount: Amount, - }, - /// A contract transferred an [`Amount`] to an account. - Transferred { - /// The sender contract. - from: ContractAddress, - /// The [`Amount`] transferred. - amount: Amount, - /// The receiver account. - to: AccountAddress, - }, -} - -/// Represents a successful contract update (or invocation). -// TODO: Consider adding function to aggregate all logs from the host_events. -#[derive(Debug)] -pub struct SuccessfulContractUpdate { - /// Host events that occured. This includes interrupts, resumes, and - /// upgrades. - pub chain_events: Vec, - /// Energy used. - pub energy_used: Energy, - /// Cost of transaction. - pub transaction_fee: Amount, - /// The returned value. - pub return_value: ReturnValue, - /// Whether the state was changed. - pub state_changed: bool, - /// The new balance of the smart contract. - pub new_balance: Amount, - /// The logs produced since the last interrupt. - pub logs: v0::Logs, -} - -/// A transfer from an contract to an account. -#[derive(Debug, PartialEq, Eq)] -pub struct Transfer { - /// The sender contract. - pub from: ContractAddress, - /// The amount transferred. - pub amount: Amount, - /// The receive account. - pub to: AccountAddress, -} - -impl SuccessfulContractUpdate { - /// Get a list of all transfers that were made from contracts to accounts. - pub fn transfers(&self) -> Vec { - self.chain_events - .iter() - .filter_map(|e| { - if let ChainEvent::Transferred { from, amount, to } = e { - Some(Transfer { - from: *from, - amount: *amount, - to: *to, - }) - } else { - None - } - }) - .collect() - } -} - -/// Represents a successful deployment of a [`ContractModule`]. -#[derive(Debug, PartialEq, Eq)] -pub struct SuccessfulModuleDeployment { - /// The reference of the module deployed. - pub module_reference: ModuleReference, - /// The energy used for deployment. - pub energy: Energy, - /// Cost of transaction. - pub transaction_fee: Amount, -} - -/// Represents a successful initialization of a contract. -#[derive(Debug)] -pub struct SuccessfulContractInit { - /// The address of the new instance. - pub contract_address: ContractAddress, - /// Logs produced during initialization. - pub logs: v0::Logs, - /// Energy used. - pub energy_used: Energy, - /// Cost of transaction. - pub transaction_fee: Amount, -} +pub use types::Chain; #[cfg(test)] mod tests { + use crate::types::*; + use super::*; const ACC_0: AccountAddress = AccountAddress([0; 32]); @@ -3121,18 +626,6 @@ mod tests { assert_eq!(res_view.return_value, expected_res); } - #[test] - fn calculate_cost_will_not_overflow() { - let chain = Chain::new_with_time_and_rates( - SlotTime::from_timestamp_millis(0), - ExchangeRate::new_unchecked(u64::MAX, u64::MAX - 1), - ExchangeRate::new_unchecked(u64::MAX - 2, u64::MAX - 3), - ); - - let energy = Energy::from(u64::MAX - 4); - chain.calculate_energy_cost(energy); - } - mod query_account_balance { use super::*; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs new file mode 100644 index 00000000..42b4f58e --- /dev/null +++ b/contract-testing/src/types.rs @@ -0,0 +1,325 @@ +use std::{collections::BTreeMap, sync::Arc}; +use thiserror::Error; + +use concordium_base::{ + base::Energy, + contracts_common::{ + AccountAddress, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, + OwnedContractName, OwnedEntrypointName, SlotTime, + }, + smart_contracts::WasmVersion, +}; +use wasm_chain_integration::{ + v0, + v1::{self, trie, ReturnValue}, +}; +use wasm_transform::artifact; + +/// A V1 artifact, with concrete types for the generic parameters. +pub type ContractModule = artifact::Artifact; + +/// Represents the block chain and supports a number of operations, including +/// creating accounts, deploying modules, initializing contract, updating +/// contracts and invoking contracts. +pub struct Chain { + /// The slot time viewable inside the smart contracts. + /// Defaults to `0`. + pub slot_time: SlotTime, + /// MicroCCD per Euro ratio. + pub micro_ccd_per_euro: ExchangeRate, + /// Euro per Energy ratio. + pub euro_per_energy: ExchangeRate, + /// Accounts and info about them. + pub accounts: BTreeMap, + /// Smart contract modules. + pub modules: BTreeMap>, + /// Smart contract instances. + pub contracts: BTreeMap, + /// Next contract index to use when creating a new instance. + pub next_contract_index: u64, +} + +/// A smart contract instance along. +#[derive(Clone)] +pub struct Contract { + /// The module which contains this contract. + pub module_reference: ModuleReference, + /// The name of the contract. + pub contract_name: OwnedContractName, + /// The contract state. + pub state: trie::PersistentState, + /// The owner of the contract. + pub owner: AccountAddress, + /// The balance of the contract. + pub self_balance: Amount, +} + +/// Account policies for testing. +pub struct TestPolicies(pub v0::OwnedPolicyBytes); + +/// Data about an [`AccountAddress`]. +#[derive(Clone)] +pub struct AccountInfo { + /// The account balance. TODO: Add all three types of balances. + pub balance: Amount, + /// Account policies. + pub policies: v0::OwnedPolicyBytes, + /// The number of signatures. The number of signatures affect the cost of + /// every transaction for the account. + pub signature_count: u32, +} + +/// An event that occurred during a contract update or invocation. +#[derive(Debug)] +pub enum ChainEvent { + /// A contract was interrupted. + Interrupted { + /// The contract interrupted. + address: ContractAddress, + /// Logs produced prior to being interrupted. + logs: v0::Logs, + }, + /// A contract was resumed after being interrupted. + Resumed { + /// The contract resumed. + address: ContractAddress, + /// Whether the action that caused the interrupt succeeded. + success: bool, + }, + /// A contract was upgraded. + Upgraded { + /// The contract upgraded. + address: ContractAddress, + /// The old module reference. + from: ModuleReference, + /// The new module reference. + to: ModuleReference, + }, + /// A contract was updated. + Updated { + /// The contract updated. + address: ContractAddress, + /// The name of the contract. + contract: OwnedContractName, + /// The entrypoint called. + entrypoint: OwnedEntrypointName, + /// The amount added to the contract. + amount: Amount, + }, + /// A contract transferred an [`Amount`] to an account. + Transferred { + /// The sender contract. + from: ContractAddress, + /// The [`Amount`] transferred. + amount: Amount, + /// The receiver account. + to: AccountAddress, + }, +} + +/// A transfer from an contract to an account. +#[derive(Debug, PartialEq, Eq)] +pub struct Transfer { + /// The sender contract. + pub from: ContractAddress, + /// The amount transferred. + pub amount: Amount, + /// The receive account. + pub to: AccountAddress, +} + +/// Represents a successful deployment of a [`ContractModule`]. +#[derive(Debug, PartialEq, Eq)] +pub struct SuccessfulModuleDeployment { + /// The reference of the module deployed. + pub module_reference: ModuleReference, + /// The energy used for deployment. + pub energy: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, +} + +/// An error that can occur while deploying a [`ContractModule`]. +// TODO: Can we get Eq for this when using io::Error? +// TODO: Should this also have the energy used? +#[derive(Debug, Error)] +pub enum DeployModuleError { + /// Failed to read the module file. + #[error("could not read the file due to: {0}")] + ReadFileError(#[from] std::io::Error), + /// The module provided is not valid. + #[error("module is invalid due to: {0}")] + InvalidModule(#[from] anyhow::Error), + /// The account does not have sufficient funds to pay for the deployment. + #[error("account does not have sufficient funds to pay for the energy")] + InsufficientFunds, + /// The account deploying the module does not exist. + #[error("account {} does not exist", 0.0)] + AccountDoesNotExist(#[from] AccountMissing), + /// The module version is not supported. + #[error("wasm version {0} is not supported")] + UnsupportedModuleVersion(WasmVersion), + /// The module has already been deployed. + #[error("module with reference {:?} already exists", 0)] + DuplicateModule(ModuleReference), +} + +/// Represents a successful initialization of a contract. +#[derive(Debug)] +pub struct SuccessfulContractInit { + /// The address of the new instance. + pub contract_address: ContractAddress, + /// Logs produced during initialization. + pub logs: v0::Logs, + /// Energy used. + pub energy_used: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, +} + +/// Errors that can occur while initializing a contract. +#[derive(Debug, Error)] +pub enum ContractInitError { + /// Initialization failed for a reason that also exists on the chain. + #[error("failed due to a valid chain error: {:?}", 0)] + ValidChainError(FailedContractInteraction), + /// Module has not been deployed in the test environment. + #[error("module {:?} does not exist", 0.0)] + ModuleDoesNotExist(#[from] ModuleMissing), + /// Account has not been created in test environment. + #[error("account {} does not exist", 0.0)] + AccountDoesNotExist(#[from] AccountMissing), + /// The account does not have enough funds to pay for the energy. + #[error("account does not have enough funds to pay for the energy")] + InsufficientFunds, +} + +/// Represents a successful contract update (or invocation). +// TODO: Consider adding function to aggregate all logs from the host_events. +#[derive(Debug)] +pub struct SuccessfulContractUpdate { + /// Host events that occured. This includes interrupts, resumes, and + /// upgrades. + pub chain_events: Vec, + /// Energy used. + pub energy_used: Energy, + /// Cost of transaction. + pub transaction_fee: Amount, + /// The returned value. + pub return_value: ReturnValue, + /// Whether the state was changed. + pub state_changed: bool, + /// The new balance of the smart contract. + pub new_balance: Amount, + /// The logs produced since the last interrupt. + pub logs: v0::Logs, +} + +/// Errors that can occur during a [`Chain::contract_update]` or +/// [`Chain::contract_invoke`] call. +/// +/// There are two categories of errors here: +/// - `ExecutionError` and `OutOfEnergy` can occur if the preconditions for the +/// function is valid, and a contract is executed. +/// - The rest represent incorrect usage of the function, where some +/// precondition wasn't met. +#[derive(Debug, Error)] +pub enum ContractUpdateError { + /// Update failed for a reason that also exists on the chain. + #[error("failed during execution")] + ExecutionError { + failure_kind: v1::InvokeFailure, + energy_used: Energy, + transaction_fee: Amount, + }, + #[error("ran out of energy")] + OutOfEnergy { + energy_used: Energy, + transaction_fee: Amount, + }, + /// Module has not been deployed in test environment. + #[error("module {:?} does not exist", 0.0)] + ModuleDoesNotExist(#[from] ModuleMissing), + /// Contract instance has not been initialized in the test environment. + #[error("instance {} does not exist", 0.0)] + InstanceDoesNotExist(#[from] ContractInstanceMissing), + /// Entrypoint does not exist and neither does the fallback entrypoint. + #[error("entrypoint does not exist")] + EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), + /// The invoker account has not been created in the test environment. + #[error("invoker account {} does not exist", 0.0)] + InvokerDoesNotExist(#[from] AccountMissing), + /// The sender does not exist in the test environment. + #[error("sender {0} does not exist")] + SenderDoesNotExist(Address), + /// The account does not have enough funds to pay for the energy. + #[error("account does not have enough funds to pay for the energy")] + InsufficientFunds, +} + +/// Represents a failed contract interaction, i.e. an initialization, update, or +/// invocation. +#[derive(Debug)] +pub enum FailedContractInteraction { + /// The contract rejected. + Reject { + /// The error code for why it rejected. + reason: i32, + /// The return value. + return_value: ReturnValue, + /// The amount of energy used before rejecting. + energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. + transaction_fee: Amount, + }, + /// The contract trapped. + Trap { + /// The amount of energy used before rejecting. + energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. + transaction_fee: Amount, + }, + /// The contract ran out of energy. + OutOfEnergy { + /// The amount of energy used before rejecting. + energy_used: Energy, + /// The transaction fee to be paid by the invoker for the interaction. + transaction_fee: Amount, + }, +} + +/// A transfer of [`Amount`]s failed because the sender had insufficient +/// balance. +#[derive(Debug)] +pub(crate) struct InsufficientBalanceError; + +/// Errors related to transfers. +#[derive(PartialEq, Eq, Debug, Error)] +pub(crate) enum TransferError { + /// The receiver does not exist. + #[error("The receiver does not exist.")] + ToMissing, + /// The sender does not have sufficient balance. + #[error("The sender does not have sufficient balance.")] + InsufficientBalance, +} + +/// The entrypoint does not exist. +#[derive(PartialEq, Eq, Debug, Error)] +#[error("The entrypoint '{0}' does not exist.")] +pub struct EntrypointDoesNotExist(pub OwnedEntrypointName); + +/// The contract module does not exist. +#[derive(Debug, Error)] +#[error("Module {:?} does not exist.", 0)] +pub struct ModuleMissing(pub ModuleReference); + +/// The contract instance does not exist. +#[derive(Debug, Error)] +#[error("Contract instance {0} does not exist.")] +pub struct ContractInstanceMissing(pub ContractAddress); + +/// The account does not exist. +#[derive(Debug, Error)] +#[error("Account {0} does not exist.")] +pub struct AccountMissing(pub AccountAddress); From 451c4fa665ebc7b2d88425541a43bf48a71868c4 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 17 Feb 2023 10:54:36 +0100 Subject: [PATCH 061/208] Move tests to appropriate files --- contract-testing/src/impls.rs | 33 +- contract-testing/src/invocation/impls.rs | 31 + contract-testing/src/lib.rs | 2098 +--------------------- contract-testing/tests/basics.rs | 597 ++++++ contract-testing/tests/checkpointing.rs | 296 +++ contract-testing/tests/queries.rs | 593 ++++++ contract-testing/tests/upgrades.rs | 561 ++++++ 7 files changed, 2112 insertions(+), 2097 deletions(-) create mode 100644 contract-testing/tests/basics.rs create mode 100644 contract-testing/tests/checkpointing.rs create mode 100644 contract-testing/tests/queries.rs create mode 100644 contract-testing/tests/upgrades.rs diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 63634cf9..2e3c127c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -517,16 +517,6 @@ impl Chain { Ok(Arc::clone(module)) } - /// Returns an immutable reference to a [`Contract`] from persistence. - fn persistence_get_contract( - &self, - address: ContractAddress, - ) -> Result<&Contract, ContractInstanceMissing> { - self.contracts - .get(&address) - .ok_or(ContractInstanceMissing(address)) - } - /// Returns an immutable reference to [`AccountInfo`] from persistence. fn persistence_get_account( &self, @@ -618,7 +608,7 @@ impl Chain { /// Helper function for converting [`Energy`] to [`Amount`] using the two /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. - fn calculate_energy_cost(&self, energy: Energy) -> Amount { + pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { energy_to_amount(energy, self.euro_per_energy, self.micro_ccd_per_euro) } } @@ -758,6 +748,10 @@ pub(crate) fn energy_to_amount( #[cfg(test)] mod tests { use super::*; + + const ACC_0: AccountAddress = AccountAddress([0; 32]); + const ACC_1: AccountAddress = AccountAddress([1; 32]); + #[test] fn calculate_cost_will_not_overflow() { let micro_ccd_per_euro = ExchangeRate::new_unchecked(u64::MAX, u64::MAX - 1); @@ -765,4 +759,21 @@ mod tests { let energy = Energy::from(u64::MAX - 4); energy_to_amount(energy, euro_per_energy, micro_ccd_per_euro); } + + #[test] + fn creating_accounts_work() { + let mut chain = Chain::new(); + chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); + chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); + + assert_eq!(chain.accounts.len(), 2); + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(Amount::from_ccd(1)) + ); + assert_eq!( + chain.persistence_account_balance(ACC_1), + Some(Amount::from_ccd(2)) + ); + } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 2c59de5d..1e13c202 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1380,3 +1380,34 @@ impl From for InsufficientBalanceError { impl From for TransferError { fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } } + +#[cfg(test)] +mod tests { + mod amount_delta { + use crate::{invocation::types::AmountDelta, Amount}; + #[test] + fn test() { + let mut x = AmountDelta::new(); + assert_eq!(x, AmountDelta::Positive(Amount::zero())); + + let one = Amount::from_ccd(1); + let two = Amount::from_ccd(2); + let three = Amount::from_ccd(3); + let five = Amount::from_ccd(5); + + x = x.subtract_amount(one); // -1 CCD + x = x.subtract_amount(one); // -2 CCD + assert_eq!(x, AmountDelta::Negative(two)); + x = x.add_amount(five); // +3 CCD + assert_eq!(x, AmountDelta::Positive(three)); + x = x.subtract_amount(five); // -2 CCD + assert_eq!(x, AmountDelta::Negative(two)); + x = x.add_amount(two); // 0 + + x = x.add_amount(Amount::from_micro_ccd(1)); // 1 mCCD + assert_eq!(x, AmountDelta::Positive(Amount::from_micro_ccd(1))); + x = x.subtract_amount(Amount::from_micro_ccd(2)); // -1 mCCD + assert_eq!(x, AmountDelta::Negative(Amount::from_micro_ccd(1))); + } + } +} diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 65383302..adece1f3 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,2089 +1,15 @@ mod constants; mod impls; mod invocation; -mod types; - -pub use types::Chain; - -#[cfg(test)] -mod tests { - use crate::types::*; - - use super::*; - - const ACC_0: AccountAddress = AccountAddress([0; 32]); - const ACC_1: AccountAddress = AccountAddress([1; 32]); - const WASM_TEST_FOLDER: &str = - "../../concordium-node/concordium-consensus/testdata/contracts/v1"; - - #[test] - fn creating_accounts_work() { - let mut chain = Chain::new(); - chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); - chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); - - assert_eq!(chain.accounts.len(), 2); - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(Amount::from_ccd(1)) - ); - assert_eq!( - chain.persistence_account_balance(ACC_1), - Some(Amount::from_ccd(2)) - ); - } - - #[test] - fn deploying_valid_module_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") - .expect("Deploying valid module should work"); - - assert_eq!(chain.modules.len(), 1); - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(initial_balance - res.transaction_fee) - ); - } - - #[test] - fn initializing_valid_contract_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) - ); - assert_eq!(chain.contracts.len(), 1); - } - - #[test] - fn initializing_with_invalid_parameter_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![99u8]), // Invalid param - Amount::zero(), - Energy::from(10000), - ) - .expect_err("Initializing with invalid params should fail"); - - assert!(matches!(res_init, ContractInitError::ValidChainError(_))); - match res_init { - // Failed in the right way and account is still charged. - ContractInitError::ValidChainError(FailedContractInteraction::Reject { - transaction_fee, - .. - }) => assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(initial_balance - res_deploy.transaction_fee - transaction_fee) - ), - _ => panic!("Expected valid chain error."), - }; - } - - #[test] - fn updating_valid_contract_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("set"), - OwnedParameter::from_bytes(vec![1u8]), // Updated to 1 - Amount::zero(), - Energy::from(10000), - ) - .expect("Updating valid contract should work"); - - let res_invoke_get = chain - .contract_invoke( - ACC_0, - Address::Contract(res_init.contract_address), // Invoke with a contract as sender. - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - // Assert that the updated state is persisted. - assert_eq!(res_invoke_get.return_value, [1u8]); - } - - /// Test that updates and invocations where the sender is missing fail. - #[test] - fn updating_and_invoking_with_missing_sender_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let missing_account = Address::Account(ACC_1); - let missing_contract = Address::Contract(ContractAddress::new(100, 0)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update_acc = chain.contract_update( - ACC_0, - missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); - - let res_invoke_acc = chain.contract_invoke( - ACC_0, - missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); - - let res_update_contr = chain.contract_update( - ACC_0, - missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); - - let res_invoke_contr = chain.contract_invoke( - ACC_0, - missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); - - assert!(matches!( - res_update_acc, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); - assert!(matches!( - res_invoke_acc, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); - assert!(matches!( - res_update_contr, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); - assert!(matches!( - res_invoke_contr, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); - } - - /// Tests using the integrate contract defined in - /// concordium-rust-smart-contract on the 'kb/sc-integration-testing' - /// branch. - mod integrate_contract { - use super::*; - - #[test] - fn update_with_account_transfer_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&ACC_1), - transfer_amount, - Energy::from(10000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - - transfer_amount - ) - ); - assert_eq!( - chain.persistence_account_balance(ACC_1), - Some(initial_balance + transfer_amount) - ); - assert_eq!(res_update.transfers(), [Transfer { - from: res_init.contract_address, - amount: transfer_amount, - to: ACC_1, - }]); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - assert_eq!(res_update.return_value, [2, 0, 0, 0]); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, [2, 0, 0, 0]); - } - - #[test] - fn update_with_account_transfer_to_missing_account_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&ACC_1), // We haven't created ACC_1. - transfer_amount, - Energy::from(100000), - ); - - match res_update { - Err(ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::ContractReject { code, .. }, - transaction_fee, - .. - }) => { - assert_eq!(code, -3); // The custom contract error code for missing account. - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - transaction_fee - ) - ); - } - _ => panic!("Expected contract update to fail"), - } - } - - #[test] - fn update_with_integrate_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("recurse"), - OwnedParameter::new(&10u32), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - let expected_res = 10 + 7 + 11 + 3 + 7 + 11; - assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); - } - - #[test] - fn update_with_rollback_and_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let input_param: u32 = 8; - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("inc-fail-on-zero"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(res_update.state_changed); - let expected_res = 2u32.pow(input_param) - 1; - assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); - } - - #[test] - fn rollback_of_account_balances_after_failed_contract_invoke() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(2); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init_0 = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_1 = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate_other"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let param = (res_init_1.contract_address, initial_balance, ACC_1); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_0.contract_address, - EntrypointName::new_unchecked("mutate_and_forward"), - OwnedParameter::new(¶m), - transfer_amount, - Energy::from(100000), - ) - .expect("Update should succeed"); - } - } - // TODO: Add tests that check: - // - Correct account balances after init / update failures (when Amount > 0) - - #[test] - fn update_with_fib_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_fib"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&6u64), - Amount::zero(), - Energy::from(4000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - let expected_res = u64::to_le_bytes(13); - assert_eq!(res_update.return_value, expected_res); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, expected_res); - } - - mod query_account_balance { - use super::*; - - /// Queries the balance of another account and asserts that it is as - /// expected. - #[test] - fn test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to - // get around it here. - // The contract will query the balance of ACC_1 and assert that the three - // balances match this input. - let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// Queries the balance of the invoker account, which will have have the - /// expected balance of: - /// prior_balance - amount_sent - amount_to_cover_reserved_NRG. - #[test] - fn invoker_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let update_amount = Amount::from_ccd(123); - let energy_limit = Energy::from(100000); - let invoker_reserved_amount = update_amount + chain.calculate_energy_cost(energy_limit); - - // The contract will query the balance of ACC_1, which is also the invoker, and - // assert that the three balances match this input. - let expected_balance = initial_balance - invoker_reserved_amount; - let input_param = (ACC_1, (expected_balance, Amount::zero(), Amount::zero())); - - let res_update = chain - .contract_update( - ACC_1, - Address::Account(ACC_1), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - update_amount, - energy_limit, - ) - .expect("Updating valid contract should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) - ); - assert_eq!( - chain.persistence_account_balance(ACC_1), - // Differs from `expected_balance` as it only includes the actual amount charged - // for the NRG use. Not the reserved amount. - Some(initial_balance - res_update.transaction_fee - update_amount) - ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// Makes a transfer to an account, then queries its balance and asserts - /// that it is as expected. - #[test] - fn transfer_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let amount_to_send = Amount::from_ccd(123); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - amount_to_send, // Make sure the contract has CCD to transfer. - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let amount_to_send = Amount::from_ccd(123); - let expected_balance = initial_balance + amount_to_send; - let input_param = ( - ACC_1, - amount_to_send, - (expected_balance, Amount::zero(), Amount::zero()), - ); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(10000), - ) - .expect("Updating valid contract should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - - amount_to_send - ) - ); - assert_eq!( - chain.persistence_account_balance(ACC_1), - Some(initial_balance + amount_to_send) - ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Transferred { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. } - ])); - } - - #[test] - fn balance_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to - // get around it here. - // The contract will query the balance of ACC_1 and assert that the three - // balances match this input. - let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// Queries the balance of a missing account and asserts that it returns - /// the correct error. - #[test] - fn missing_account_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!( - "{}/queries-account-balance-missing-account.wasm", - WASM_TEST_FOLDER - ), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // The account to query, which doesn't exist in this test case. - let input_param = ACC_1; - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - } - - mod query_contract_balance { - use super::*; - - /// Test querying the balance of another contract, which exists. Asserts - /// that the balance is as expected. - #[test] - fn test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let init_amount = Amount::from_ccd(123); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_other = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, // Set up another contract with `init_amount` balance - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // check that the other contract has `self_balance == init_amount`. - let input_param = (res_init_other.contract_address, init_amount); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// Test querying the balance of the contract instance itself. This - /// should include the amount sent to it in the update transaction. - #[test] - fn query_self_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let init_amount = Amount::from_ccd(123); - let update_amount = Amount::from_ccd(456); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // check that the other contract has `self_balance == init_amount`. - let input_param = (res_init.contract_address, init_amount + update_amount); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - update_amount, - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// Test querying the balance after a transfer of CCD. - #[test] - fn query_self_after_transfer_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let init_amount = Amount::from_ccd(123); - let update_amount = Amount::from_ccd(456); - let transfer_amount = Amount::from_ccd(78); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!( - "{}/queries-contract-balance-transfer.wasm", - WASM_TEST_FOLDER - ), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let input_param = ( - ACC_0, - transfer_amount, - ( - res_init.contract_address, - init_amount + update_amount - transfer_amount, - ), - ); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - update_amount, - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Transferred { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. } - ])); - } - - /// Test querying the balance of a contract that doesn't exist. - #[test] - fn missing_contract_test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!( - "{}/queries-contract-balance-missing-contract.wasm", - WASM_TEST_FOLDER - ), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // Non-existent contract address. - let input_param = ContractAddress::new(123, 456); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - } - - mod query_exchange_rates { - - use super::*; - - /// Test querying the exchange rates. - #[test] - fn test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // Non-existent contract address. - let input_param = (chain.euro_per_energy, chain.micro_ccd_per_euro); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - } - - mod contract_upgrade { - - use super::*; - - /// Test a basic upgrade, ensuring that the new module is in place by - /// checking the available entrypoints. - #[test] - fn test() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - // Deploy the two modules `upgrading_0`, `upgrading_1` - let res_deploy_0 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_deploy_1 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - // Initialize `upgrading_0`. - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - // Upgrade the contract to the `upgrading_1` module by calling the `bump` - // entrypoint. - let res_update_upgrade = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("bump"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - // Call the `newfun` entrypoint which only exists in `upgrading_1`. - let res_update_new = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("newfun"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating the `newfun` from the `upgrading_1` module should work"); - - assert!(matches!(res_update_upgrade.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from, to, .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. }, - ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference)); - assert!(matches!(res_update_new.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - - /// The contract in this test, triggers an upgrade and then in the same - /// invocation, calls a function in the upgraded module. - /// Checking the new module is being used. - #[test] - fn test_self_invoke() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy_0 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - let res_deploy_1 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - // Invoking `contract.name` - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, - // Making the upgrade - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { .. }, - ChainEvent::Resumed { .. }, - // Invoking contract.name again - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, - // The successful update - ChainEvent::Updated { .. }, - ])); - } - - /// Test upgrading to a module that doesn't exist (it uses module - /// `[0u8;32]` inside the contract). The contract checks whether - /// the expected error is returned. - #[test] - fn test_missing_module() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - // No upgrade event, as it is supposed to fail. - ChainEvent::Resumed { success, .. }, - ChainEvent::Updated { .. }, - ] if success == false)); - } - - /// Test upgrading to a module where there isn't a matching contract - /// name. The contract checks whether the expected error is - /// returned. - #[test] - fn test_missing_contract() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy_0 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_deploy_1 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - // No upgrade event, as it is supposed to fail. - ChainEvent::Resumed { success, .. }, - ChainEvent::Updated { .. }, - ] if success == false)); - } - - /// Test upgrading twice in the same transaction. The effect of the - /// second upgrade should be in effect at the end. - #[test] - fn test_twice_in_one_transaction() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy_0 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_deploy_1 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_deploy_2 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(100000), - ) - .expect("Updating valid contract should work"); - - assert!(matches!(res_update.chain_events[..], [ - // Invoke the contract itself to check the name entrypoint return value. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, - // Upgrade from module 0 to 1 - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from: first_from, to: first_to, .. }, - ChainEvent::Resumed { .. }, - // Invoke the contract itself to check the name again. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, - // Upgrade again - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from: second_from, to: second_to, .. }, - ChainEvent::Resumed { .. }, - // Invoke itself again to check name a final time. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, - // Final update event - ChainEvent::Updated { .. }, - ] if first_from == res_deploy_0.module_reference - && first_to == res_deploy_1.module_reference - && second_from == res_deploy_1.module_reference - && second_to == res_deploy_2.module_reference)); - } - - /// Test upgrading to a module where there isn't a matching contract - /// name. The contract checks whether the expected error is - /// returned. - #[test] - fn test_chained_contract() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. - let input_param = (number_of_upgrades, res_deploy.module_reference); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&input_param), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating valid contract should work"); - - // Per upgrade: 3 events for invoking itself + 3 events for the upgrade. - // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful - // update. - assert_eq!( - res_update.chain_events.len() as u32, - 6 * number_of_upgrades + 4 - ) - } - - /// Tests whether a contract which triggers a succesful upgrade, - /// but rejects the transaction from another cause, rollbacks the - /// upgrade as well. - #[test] - fn test_reject() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy_0 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_deploy_1 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update_upgrade = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(1000000), - ); - - let res_update_new_feature = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ); - - // Check the return value manually returned by the contract. - match res_update_upgrade { - Err(ContractUpdateError::ExecutionError { failure_kind, .. }) => match failure_kind - { - InvokeFailure::ContractReject { code, .. } if code == -1 => (), - _ => panic!("Expected ContractReject with code == -1"), - }, - _ => panic!("Expected Err(ContractUpdateError::ExecutionError)"), - } - - // Assert that the new_feature entrypoint doesn't exist since the upgrade - // failed. - assert!(matches!( - res_update_new_feature, - Err(ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, - }) - )); - } - - /// Tests calling an entrypoint introduced by an upgrade of the module - /// can be called and whether an entrypoint removed by an upgrade fail - /// with the appropriate reject reason. - #[test] - fn test_changing_entrypoint() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy_0 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_deploy_1 = chain - .module_deploy_wasm_v1( - ACC_0, - format!("{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update_old_feature_0 = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("old_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating old_feature on old module should work."); - - let res_update_new_feature_0 = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ) - .expect_err("Updating new_feature on old module should _not_ work"); - - let res_update_upgrade = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Upgrading contract should work."); - - let res_update_old_feature_1 = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("old_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ) - .expect_err("Updating old_feature on _new_ module should _not_ work."); - - let res_update_new_feature_1 = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating new_feature on _new_ module should work"); - - assert!(matches!(res_update_old_feature_0.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - assert!(matches!( - res_update_new_feature_0, - ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, - } - )); - assert!(matches!(res_update_upgrade.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. }, - ])); - assert!(matches!( - res_update_old_feature_1, - ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, - } - )); - assert!(matches!(res_update_new_feature_1.chain_events[..], [ - ChainEvent::Updated { .. } - ])); - } - } - - /// Tests related to checkpoints and rollbacks of the contract state. - mod checkpointing { - use super::*; - - /// This test has the following call pattern: - /// A - /// --> B - /// --> A - /// <-- - /// B(trap) - /// A <-- - /// The state at A should be left unchanged by the changes of the - /// 'inner' invocation on contract A. A correctly perceives B's - /// trapping signal. Only V1 contracts are being used. - #[test] - fn test_case_1() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let forward_parameter = ( - res_init_a.contract_address, - 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_modify"), Amount::zero()), - ); - let forward_parameter_len = to_bytes(&forward_parameter).len(); - let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), - EntrypointName::new_unchecked("b_forward_crash"), - Amount::zero(), - ); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), - // We supply one microCCD as we expect a trap - // (see contract for details). - Amount::from_micro_ccd(1), - Energy::from(10000), - ) - .expect("Updating contract should succeed"); - } - - /// This test has the following call pattern: - /// A - /// --> B - /// --> A (no modification, just lookup entry) - /// <-- - /// B - /// A <-- - /// - /// The state at A should be left unchanged. - /// The iterator initialized at the outer A should point to the same - /// entry as before the call. That is, the iterator should not - /// be affected by the inner iterator. Only V1 contracts are - /// being used. - #[test] - fn test_case_2() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let forward_parameter = ( - res_init_a.contract_address, - 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_no_modify"), Amount::zero()), - ); - let forward_parameter_len = to_bytes(&forward_parameter).len(); - let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), - EntrypointName::new_unchecked("b_forward"), - Amount::zero(), - ); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), - // We supply zero microCCD as we're instructing the contract to not expect - // state modifications. Also, the contract does not expect - // errors, i.e., a trap signal from underlying invocations. - // The 'inner' call to contract A does not modify the state. - // See the contract for details. - Amount::zero(), - Energy::from(10000), - ) - .expect("Updating contract should succeed"); - } - - /// This test has the following call pattern: - /// A - /// --> Transfer - /// A <-- - /// - /// The state at A should be left unchanged. - /// The iterator initialized at A should after the call point to the - /// same entry as before the call. Only V1 contracts are being - /// used. - #[test] - fn test_case_3() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(&ACC_1), - // We supply three micro CCDs as we're instructing the contract to carry out a - // transfer instead of a call. See the contract for - // details. - Amount::from_micro_ccd(3), - Energy::from(10000), - ) - .expect("Updating contract should succeed"); - } - - /// This test has the following call pattern: - /// A - /// --> B - /// --> A modify - /// <-- - /// B - /// A <-- - /// - /// The state at A should have changed according to the 'inner' - /// invocation on contract A. Only V1 contracts are being used. - #[test] - fn test_case_4() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - - let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) - .expect("Deploying valid module should work"); - - let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let forward_parameter = ( - res_init_a.contract_address, - 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_modify"), Amount::zero()), - ); - let forward_parameter_len = to_bytes(&forward_parameter).len(); - let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), - EntrypointName::new_unchecked("b_forward"), - Amount::zero(), - ); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), - // We supply four CCDs as we're instructing the contract to expect state - // modifications being made from the 'inner' contract A - // call to be in effect when returned to the caller (a.a_modify_proxy). - // See the contract for details. - Amount::from_micro_ccd(4), - Energy::from(10000), - ) - .expect("Updating contract should succeed"); - } - } - - mod amount_delta { - use super::*; - - #[test] - fn test() { - let mut x = AmountDelta::new(); - assert_eq!(x, AmountDelta::Positive(Amount::zero())); - - let one = Amount::from_ccd(1); - let two = Amount::from_ccd(2); - let three = Amount::from_ccd(3); - let five = Amount::from_ccd(5); - - x = x.subtract_amount(one); // -1 CCD - x = x.subtract_amount(one); // -2 CCD - assert_eq!(x, AmountDelta::Negative(two)); - x = x.add_amount(five); // +3 CCD - assert_eq!(x, AmountDelta::Positive(three)); - x = x.subtract_amount(five); // -2 CCD - assert_eq!(x, AmountDelta::Negative(two)); - x = x.add_amount(two); // 0 - - x = x.add_amount(Amount::from_micro_ccd(1)); // 1 mCCD - assert_eq!(x, AmountDelta::Positive(Amount::from_micro_ccd(1))); - x = x.subtract_amount(Amount::from_micro_ccd(2)); // -1 mCCD - assert_eq!(x, AmountDelta::Negative(Amount::from_micro_ccd(1))); - } - } -} +pub mod types; +pub use types::*; + +pub use concordium_base::{ + base::Energy, + contracts_common::{ + from_bytes, to_bytes, AccountAddress, Address, Amount, ContractAddress, ContractName, + EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, + OwnedParameter, Parameter, SlotTime, + }, +}; +pub use wasm_chain_integration::v1::InvokeFailure; diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs new file mode 100644 index 00000000..950ed59f --- /dev/null +++ b/contract-testing/tests/basics.rs @@ -0,0 +1,597 @@ +//! This module contains tests for the basic functionality of the testing +//! library. +use concordium_smart_contract_testing::*; + +const ACC_0: AccountAddress = AccountAddress([0; 32]); +const ACC_1: AccountAddress = AccountAddress([1; 32]); + +#[test] +fn deploying_valid_module_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work."); + + assert_eq!(chain.modules.len(), 1); + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(initial_balance - res.transaction_fee) + ); +} + +#[test] +fn initializing_valid_contract_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + OwnedParameter::from_bytes(vec![0u8]), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) + ); + assert_eq!(chain.contracts.len(), 1); +} + +#[test] +fn initializing_with_invalid_parameter_fails() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + OwnedParameter::from_bytes(vec![99u8]), // Invalid param + Amount::zero(), + Energy::from(10000), + ) + .expect_err("Initializing with invalid params should fail"); + + assert!(matches!(res_init, ContractInitError::ValidChainError(_))); + match res_init { + // Failed in the right way and account is still charged. + ContractInitError::ValidChainError(FailedContractInteraction::Reject { + transaction_fee, + .. + }) => assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - transaction_fee) + ), + _ => panic!("Expected valid chain error."), + }; +} + +#[test] +fn updating_valid_contract_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("set"), + OwnedParameter::from_bytes(vec![1u8]), // Updated to 1 + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + + let res_invoke_get = chain + .contract_invoke( + ACC_0, + Address::Contract(res_init.contract_address), // Invoke with a contract as sender. + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + // Assert that the updated state is persisted. + assert_eq!(res_invoke_get.return_value, [1u8]); +} + +/// Test that updates and invocations where the sender is missing fail. +#[test] +fn updating_and_invoking_with_missing_sender_fails() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let missing_account = Address::Account(ACC_1); + let missing_contract = Address::Contract(ContractAddress::new(100, 0)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_weather"), + OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update_acc = chain.contract_update( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_invoke_acc = chain.contract_invoke( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_update_contr = chain.contract_update( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + let res_invoke_contr = chain.contract_invoke( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + + assert!(matches!( + res_update_acc, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + assert!(matches!( + res_invoke_acc, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + assert!(matches!( + res_update_contr, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); + assert!(matches!( + res_invoke_contr, + Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); +} + +/// Tests using the integrate contract defined in +/// concordium-rust-smart-contract on the 'kb/sc-integration-testing' +/// branch. +mod integrate_contract { + use super::*; + + #[test] + fn update_with_account_transfer_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(1); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + OwnedParameter::new(&ACC_1), + transfer_amount, + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("view"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + - transfer_amount + ) + ); + assert_eq!( + chain.persistence_account_balance(ACC_1), + Some(initial_balance + transfer_amount) + ); + assert_eq!(res_update.transfers(), [Transfer { + from: res_init.contract_address, + amount: transfer_amount, + to: ACC_1, + }]); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + assert_eq!(res_update.return_value, [2, 0, 0, 0]); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value, [2, 0, 0, 0]); + } + + #[test] + fn update_with_account_transfer_to_missing_account_fails() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(1); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain.contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + OwnedParameter::new(&ACC_1), // We haven't created ACC_1. + transfer_amount, + Energy::from(100000), + ); + + match res_update { + Err(ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::ContractReject { code, .. }, + transaction_fee, + .. + }) => { + assert_eq!(code, -3); // The custom contract error code for missing account. + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - transaction_fee + ) + ); + } + _ => panic!("Expected contract update to fail"), + } + } + + #[test] + fn update_with_integrate_reentry_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("recurse"), + OwnedParameter::new(&10u32), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("view"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + let expected_res = 10 + 7 + 11 + 3 + 7 + 11; + assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); + } + + #[test] + fn update_with_rollback_and_reentry_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let input_param: u32 = 8; + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("inc-fail-on-zero"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("view"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(res_update.state_changed); + let expected_res = 2u32.pow(input_param) - 1; + assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); + } + + #[test] + fn rollback_of_account_balances_after_failed_contract_invoke() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + let transfer_amount = Amount::from_ccd(2); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init_0 = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_1 = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_integrate_other"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let param = (res_init_1.contract_address, initial_balance, ACC_1); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init_0.contract_address, + EntrypointName::new_unchecked("mutate_and_forward"), + OwnedParameter::new(¶m), + transfer_amount, + Energy::from(100000), + ) + .expect("Update should succeed"); + } +} + +#[test] +fn update_with_fib_reentry_works() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_fib"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("receive"), + OwnedParameter::new(&6u64), + Amount::zero(), + Energy::from(4000000), + ) + .expect("Updating valid contract should work"); + + let res_view = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("view"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoking get should work"); + + // This also asserts that the account wasn't charged for the invoke. + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert_eq!(chain.contracts.len(), 1); + assert!(res_update.state_changed); + let expected_res = u64::to_le_bytes(13); + assert_eq!(res_update.return_value, expected_res); + // Assert that the updated state is persisted. + assert_eq!(res_view.return_value, expected_res); +} diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs new file mode 100644 index 00000000..e742cfba --- /dev/null +++ b/contract-testing/tests/checkpointing.rs @@ -0,0 +1,296 @@ +//! This module contains tests for the checkpointing/rollback functionality of +//! the library. +//! +//! When a contract entrypoint execution fails, any changes it has +//! made must be rolled back. That is also the case if a nested contract call +//! fails. +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); +const ACC_1: AccountAddress = AccountAddress([1; 32]); + +/// This test has the following call pattern: +/// A +/// --> B +/// --> A +/// <-- +/// B(trap) +/// A <-- +/// The state at A should be left unchanged by the changes of the +/// 'inner' invocation on contract A. A correctly perceives B's +/// trapping signal. Only V1 contracts are being used. +#[test] +fn test_case_1() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward_crash"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + OwnedParameter::new(¶meter), + // We supply one microCCD as we expect a trap + // (see contract for details). + Amount::from_micro_ccd(1), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} + +/// This test has the following call pattern: +/// A +/// --> B +/// --> A (no modification, just lookup entry) +/// <-- +/// B +/// A <-- +/// +/// The state at A should be left unchanged. +/// The iterator initialized at the outer A should point to the same +/// entry as before the call. That is, the iterator should not +/// be affected by the inner iterator. Only V1 contracts are +/// being used. +#[test] +fn test_case_2() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_no_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + OwnedParameter::new(¶meter), + // We supply zero microCCD as we're instructing the contract to not expect + // state modifications. Also, the contract does not expect + // errors, i.e., a trap signal from underlying invocations. + // The 'inner' call to contract A does not modify the state. + // See the contract for details. + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} + +/// This test has the following call pattern: +/// A +/// --> Transfer +/// A <-- +/// +/// The state at A should be left unchanged. +/// The iterator initialized at A should after the call point to the +/// same entry as before the call. Only V1 contracts are being +/// used. +#[test] +fn test_case_3() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + OwnedParameter::new(&ACC_1), + // We supply three micro CCDs as we're instructing the contract to carry out a + // transfer instead of a call. See the contract for + // details. + Amount::from_micro_ccd(3), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} + +/// This test has the following call pattern: +/// A +/// --> B +/// --> A modify +/// <-- +/// B +/// A <-- +/// +/// The state at A should have changed according to the 'inner' +/// invocation on contract A. Only V1 contracts are being used. +#[test] +fn test_case_4() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_a = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_a"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_b = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_b"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let forward_parameter = ( + res_init_a.contract_address, + 0u16, // length of empty parameter + (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + ); + let forward_parameter_len = to_bytes(&forward_parameter).len(); + let parameter = ( + ( + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, + ), + EntrypointName::new_unchecked("b_forward"), + Amount::zero(), + ); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init_a.contract_address, + EntrypointName::new_unchecked("a_modify_proxy"), + OwnedParameter::new(¶meter), + // We supply four CCDs as we're instructing the contract to expect state + // modifications being made from the 'inner' contract A + // call to be in effect when returned to the caller (a.a_modify_proxy). + // See the contract for details. + Amount::from_micro_ccd(4), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs new file mode 100644 index 00000000..69b1f950 --- /dev/null +++ b/contract-testing/tests/queries.rs @@ -0,0 +1,593 @@ +//! This module contains tests related to the chain queries that are available +//! for smart contracts. +//! +//! Namely queries for: +//! - the balance of a contract, +//! - the balances of an account, +//! - the exhange rates. +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); +const ACC_1: AccountAddress = AccountAddress([1; 32]); + +mod query_account_balance { + use super::*; + + /// Queries the balance of another account and asserts that it is as + /// expected. + #[test] + fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to + // get around it here. + // The contract will query the balance of ACC_1 and assert that the three + // balances match this input. + let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Queries the balance of the invoker account, which will have have the + /// expected balance of: + /// prior_balance - amount_sent - amount_to_cover_reserved_NRG. + #[test] + fn invoker_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let update_amount = Amount::from_ccd(123); + let energy_limit = Energy::from(100000); + let invoker_reserved_amount = update_amount + chain.calculate_energy_cost(energy_limit); + + // The contract will query the balance of ACC_1, which is also the invoker, and + // assert that the three balances match this input. + let expected_balance = initial_balance - invoker_reserved_amount; + let input_param = (ACC_1, (expected_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_1, + Address::Account(ACC_1), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + update_amount, + energy_limit, + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) + ); + assert_eq!( + chain.persistence_account_balance(ACC_1), + // Differs from `expected_balance` as it only includes the actual amount charged + // for the NRG use. Not the reserved amount. + Some(initial_balance - res_update.transaction_fee - update_amount) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Makes a transfer to an account, then queries its balance and asserts + /// that it is as expected. + #[test] + fn transfer_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let amount_to_send = Amount::from_ccd(123); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + amount_to_send, // Make sure the contract has CCD to transfer. + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let amount_to_send = Amount::from_ccd(123); + let expected_balance = initial_balance + amount_to_send; + let input_param = ( + ACC_1, + amount_to_send, + (expected_balance, Amount::zero(), Amount::zero()), + ); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + - amount_to_send + ) + ); + assert_eq!( + chain.persistence_account_balance(ACC_1), + Some(initial_balance + amount_to_send) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Transferred { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. } + ])); + } + + #[test] + fn balance_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to + // get around it here. + // The contract will query the balance of ACC_1 and assert that the three + // balances match this input. + let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Queries the balance of a missing account and asserts that it returns + /// the correct error. + #[test] + fn missing_account_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!( + "{}/queries-account-balance-missing-account.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // The account to query, which doesn't exist in this test case. + let input_param = ACC_1; + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert_eq!( + chain.persistence_account_balance(ACC_0), + Some( + initial_balance + - res_deploy.transaction_fee + - res_init.transaction_fee + - res_update.transaction_fee + ) + ); + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } +} + +mod query_contract_balance { + use super::*; + + /// Test querying the balance of another contract, which exists. Asserts + /// that the balance is as expected. + #[test] + fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_other = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + init_amount, // Set up another contract with `init_amount` balance + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // check that the other contract has `self_balance == init_amount`. + let input_param = (res_init_other.contract_address, init_amount); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance of the contract instance itself. This + /// should include the amount sent to it in the update transaction. + #[test] + fn query_self_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + let update_amount = Amount::from_ccd(456); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + init_amount, + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // check that the other contract has `self_balance == init_amount`. + let input_param = (res_init.contract_address, init_amount + update_amount); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + update_amount, + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance after a transfer of CCD. + #[test] + fn query_self_after_transfer_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let init_amount = Amount::from_ccd(123); + let update_amount = Amount::from_ccd(456); + let transfer_amount = Amount::from_ccd(78); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!( + "{}/queries-contract-balance-transfer.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + init_amount, + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let input_param = ( + ACC_0, + transfer_amount, + ( + res_init.contract_address, + init_amount + update_amount - transfer_amount, + ), + ); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + update_amount, + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Transferred { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. } + ])); + } + + /// Test querying the balance of a contract that doesn't exist. + #[test] + fn missing_contract_test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!( + "{}/queries-contract-balance-missing-contract.wasm", + WASM_TEST_FOLDER + ), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // Non-existent contract address. + let input_param = ContractAddress::new(123, 456); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } +} + +mod query_exchange_rates { + + use super::*; + + /// Test querying the exchange rates. + #[test] + fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // Non-existent contract address. + let input_param = (chain.euro_per_energy, chain.micro_ccd_per_euro); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("query"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + } +} diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs new file mode 100644 index 00000000..4d99e630 --- /dev/null +++ b/contract-testing/tests/upgrades.rs @@ -0,0 +1,561 @@ +//! This module contains tests for the native smart contract upgrade +//! functionality. +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +/// Test a basic upgrade, ensuring that the new module is in place by +/// checking the available entrypoints. +#[test] +fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + // Deploy the two modules `upgrading_0`, `upgrading_1` + let res_deploy_0 = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + // Initialize `upgrading_0`. + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_a"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // Upgrade the contract to the `upgrading_1` module by calling the `bump` + // entrypoint. + let res_update_upgrade = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("bump"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + // Call the `newfun` entrypoint which only exists in `upgrading_1`. + let res_update_new = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("newfun"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating the `newfun` from the `upgrading_1` module should work"); + + assert!(matches!(res_update_upgrade.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { from, to, .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. }, + ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference)); + assert!(matches!(res_update_new.chain_events[..], [ + ChainEvent::Updated { .. } + ])); +} + +/// The contract in this test, triggers an upgrade and then in the same +/// invocation, calls a function in the upgraded module. +/// Checking the new module is being used. +#[test] +fn test_self_invoke() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + let res_deploy_1 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + // Invoking `contract.name` + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Making the upgrade + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { .. }, + ChainEvent::Resumed { .. }, + // Invoking contract.name again + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // The successful update + ChainEvent::Updated { .. }, + ])); +} + +/// Test upgrading to a module that doesn't exist (it uses module +/// `[0u8;32]` inside the contract). The contract checks whether +/// the expected error is returned. +#[test] +fn test_missing_module() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + // No upgrade event, as it is supposed to fail. + ChainEvent::Resumed { success, .. }, + ChainEvent::Updated { .. }, + ] if success == false)); +} + +/// Test upgrading to a module where there isn't a matching contract +/// name. The contract checks whether the expected error is +/// returned. +#[test] +fn test_missing_contract() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + ChainEvent::Interrupted { .. }, + // No upgrade event, as it is supposed to fail. + ChainEvent::Resumed { success, .. }, + ChainEvent::Updated { .. }, + ] if success == false)); +} + +/// Test upgrading twice in the same transaction. The effect of the +/// second upgrade should be in effect at the end. +#[test] +fn test_twice_in_one_transaction() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_deploy_2 = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(100000), + ) + .expect("Updating valid contract should work"); + + assert!(matches!(res_update.chain_events[..], [ + // Invoke the contract itself to check the name entrypoint return value. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Upgrade from module 0 to 1 + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { from: first_from, to: first_to, .. }, + ChainEvent::Resumed { .. }, + // Invoke the contract itself to check the name again. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Upgrade again + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { from: second_from, to: second_to, .. }, + ChainEvent::Resumed { .. }, + // Invoke itself again to check name a final time. + ChainEvent::Interrupted { .. }, + ChainEvent::Updated { .. }, + ChainEvent::Resumed { .. }, + // Final update event + ChainEvent::Updated { .. }, + ] if first_from == res_deploy_0.module_reference + && first_to == res_deploy_1.module_reference + && second_from == res_deploy_1.module_reference + && second_to == res_deploy_2.module_reference)); +} + +/// Test upgrading to a module where there isn't a matching contract +/// name. The contract checks whether the expected error is +/// returned. +#[test] +fn test_chained_contract() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. + let input_param = (number_of_upgrades, res_deploy.module_reference); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&input_param), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Updating valid contract should work"); + + // Per upgrade: 3 events for invoking itself + 3 events for the upgrade. + // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful + // update. + assert_eq!( + res_update.chain_events.len() as u32, + 6 * number_of_upgrades + 4 + ) +} + +/// Tests whether a contract which triggers a succesful upgrade, +/// but rejects the transaction from another cause, rollbacks the +/// upgrade as well. +#[test] +fn test_reject() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update_upgrade = chain.contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(1000000), + ); + + let res_update_new_feature = chain.contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ); + + // Check the return value manually returned by the contract. + match res_update_upgrade { + Err(ContractUpdateError::ExecutionError { failure_kind, .. }) => match failure_kind { + InvokeFailure::ContractReject { code, .. } if code == -1 => (), + _ => panic!("Expected ContractReject with code == -1"), + }, + _ => panic!("Expected Err(ContractUpdateError::ExecutionError)"), + } + + // Assert that the new_feature entrypoint doesn't exist since the upgrade + // failed. + assert!(matches!( + res_update_new_feature, + Err(ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + }) + )); +} + +/// Tests calling an entrypoint introduced by an upgrade of the module +/// can be called and whether an entrypoint removed by an upgrade fail +/// with the appropriate reject reason. +#[test] +fn test_changing_entrypoint() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + + let res_deploy_0 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_deploy_1 = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy_0.module_reference, + ContractName::new_unchecked("init_contract"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_update_old_feature_0 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("old_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Updating old_feature on old module should work."); + + let res_update_new_feature_0 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ) + .expect_err("Updating new_feature on old module should _not_ work"); + + let res_update_upgrade = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::new(&res_deploy_1.module_reference), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Upgrading contract should work."); + + let res_update_old_feature_1 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("old_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ) + .expect_err("Updating old_feature on _new_ module should _not_ work."); + + let res_update_new_feature_1 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ) + .expect("Updating new_feature on _new_ module should work"); + + assert!(matches!(res_update_old_feature_0.chain_events[..], [ + ChainEvent::Updated { .. } + ])); + assert!(matches!( + res_update_new_feature_0, + ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + } + )); + assert!(matches!(res_update_upgrade.chain_events[..], [ + ChainEvent::Interrupted { .. }, + ChainEvent::Upgraded { .. }, + ChainEvent::Resumed { .. }, + ChainEvent::Updated { .. }, + ])); + assert!(matches!( + res_update_old_feature_1, + ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + energy_used: _, + transaction_fee: _, + } + )); + assert!(matches!(res_update_new_feature_1.chain_events[..], [ + ChainEvent::Updated { .. } + ])); +} From 9484fa934d02922327bb75279c86aadff56d4bf9 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 17 Feb 2023 11:36:38 +0100 Subject: [PATCH 062/208] Rename for clarification and adjust comments - Use better names main types in the `invocation` module - Remove `changeset_` and `persistence_` prefixes since the distinction now occurs at a module level - Rename `AccountInfo` to `Account` --- contract-testing/src/impls.rs | 140 +++++++++---------- contract-testing/src/invocation/impls.rs | 164 +++++++++++------------ contract-testing/src/invocation/mod.rs | 6 +- contract-testing/src/invocation/types.rs | 34 ++--- contract-testing/src/types.rs | 8 +- contract-testing/tests/basics.rs | 46 +++---- contract-testing/tests/checkpointing.rs | 10 +- contract-testing/tests/queries.rs | 42 +++--- contract-testing/tests/upgrades.rs | 16 +-- 9 files changed, 221 insertions(+), 245 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 2e3c127c..8c5f64db 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -15,7 +15,7 @@ use std::{collections::BTreeMap, path::Path, sync::Arc}; use wasm_chain_integration::{v0, v1, InterpreterEnergy}; use crate::{ - invocation::{ContractInvocation, UpdateAuxResponse}, + invocation::{EntrypointInvocationHandler, InvokeEntrypointResult}, types::*, }; @@ -91,7 +91,7 @@ impl Chain { + 8 + wasm_module.source.size() + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self.persistence_get_account(sender)?.signature_count; + let number_of_sigs = self.get_account(sender)?.signature_count; let base_cost = cost::base_cost(payload_size, number_of_sigs); let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); base_cost + deploy_module_cost @@ -104,7 +104,7 @@ impl Chain { ); // Try to subtract cost for account - let account = self.persistence_get_account_mut(sender)?; + let account = self.get_account_mut(sender)?; if account.balance < transaction_fee { return Err(DeployModuleError::InsufficientFunds); }; @@ -196,11 +196,11 @@ impl Chain { energy_reserved: Energy, ) -> Result { // Lookup artifact - let artifact = self.persistence_contract_module(module_reference)?; + let artifact = self.contract_module(module_reference)?; let mut transaction_fee = self.calculate_energy_cost(lookup_module_cost(&artifact)); // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. - let account_info = self.persistence_get_account(sender)?; + let account_info = self.get_account(sender)?; if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } @@ -227,7 +227,6 @@ impl Chain { loader, ); // Handle the result and update the transaction fee. - // TODO: Extract to helper function. let res = match res { Ok(v1::InitResult::Success { logs, @@ -254,7 +253,7 @@ impl Chain { // Save the contract instance self.contracts.insert(contract_address, contract_instance); // Subtract the from the invoker. - self.persistence_get_account_mut(sender)?.balance -= amount; + self.get_account_mut(sender)?.balance -= amount; Ok(SuccessfulContractInit { contract_address, @@ -307,7 +306,7 @@ impl Chain { }; // Charge the account. // We have to get the account info again because of the borrow checker. - self.persistence_get_account_mut(sender)?.balance -= transaction_fee; + self.get_account_mut(sender)?.balance -= transaction_fee; res } @@ -330,31 +329,32 @@ impl Chain { ); // Ensure the sender exists. - if !self.persistence_address_exists(sender) { + if !self.address_exists(sender) { return Err(ContractUpdateError::SenderDoesNotExist(sender)); } // Ensure account exists and can pay for the reserved energy and amount // TODO: Could we just remove this amount in the changeset and then put back the // to_ccd(remaining_energy) afterwards? - let account_info = self.persistence_get_account(invoker)?; + let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } // TODO: Should chain events be part of the changeset? - let (changeset, result, chain_events) = ContractInvocation::execute( - &self, - invoker, - sender, - contract_address, - entrypoint.to_owned(), - Parameter(¶meter.0), - amount, - invoker_amount_reserved_for_nrg, - energy_reserved, - ); + let (result, changeset, chain_events) = + EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( + &self, + invoker, + sender, + contract_address, + entrypoint.to_owned(), + Parameter(¶meter.0), + amount, + invoker_amount_reserved_for_nrg, + energy_reserved, + ); // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. @@ -378,7 +378,7 @@ impl Chain { (Energy::from(0), false) }; - let (res, transaction_fee) = self.convert_update_aux_response( + let (res, transaction_fee) = self.convert_invoke_entrypoint_result( result, chain_events, energy_reserved, @@ -389,7 +389,7 @@ impl Chain { // Charge the transaction fee irrespective of the result. // TODO: If we charge up front, then we should return to_ccd(remaining_energy) // here instead. - self.persistence_get_account_mut(invoker)?.balance -= transaction_fee; + self.get_account_mut(invoker)?.balance -= transaction_fee; res } @@ -413,28 +413,29 @@ impl Chain { ); // Ensure the sender exists. - if !self.persistence_address_exists(sender) { + if !self.address_exists(sender) { return Err(ContractUpdateError::SenderDoesNotExist(sender)); } // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.persistence_get_account(invoker)?; + let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); if account_info.balance < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } - let (changeset, result, chain_events) = ContractInvocation::execute( - &self, - invoker, - sender, - contract_address, - entrypoint.to_owned(), - Parameter(¶meter.0), - amount, - invoker_amount_reserved_for_nrg, - energy_reserved, - ); + let (result, changeset, chain_events) = + EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( + &self, + invoker, + sender, + contract_address, + entrypoint.to_owned(), + Parameter(¶meter.0), + amount, + invoker_amount_reserved_for_nrg, + energy_reserved, + ); let (energy_for_state_increase, state_changed) = if result.is_success() { match changeset.collect_energy_for_state( @@ -454,7 +455,7 @@ impl Chain { (Energy::from(0), false) }; - let (result, _) = self.convert_update_aux_response( + let (result, _) = self.convert_invoke_entrypoint_result( result, chain_events, energy_reserved, @@ -466,7 +467,7 @@ impl Chain { } /// Create an account. Will override existing account if already present. - pub fn create_account(&mut self, account: AccountAddress, account_info: AccountInfo) { + pub fn create_account(&mut self, account: AccountAddress, account_info: Account) { self.accounts.insert(account, account_info); } @@ -494,19 +495,17 @@ impl Chain { } /// Returns the balance of an account if it exists. - /// This will always be the persisted account balance. - pub fn persistence_account_balance(&self, address: AccountAddress) -> Option { + pub fn account_balance(&self, address: AccountAddress) -> Option { self.accounts.get(&address).map(|ai| ai.balance) } /// Returns the balance of an contract if it exists. - /// This will always be the persisted contract balance. - pub fn persistence_contract_balance(&self, address: ContractAddress) -> Option { + pub fn contract_balance(&self, address: ContractAddress) -> Option { self.contracts.get(&address).map(|ci| ci.self_balance) } - /// Returns an Arc clone of the [`ContractModule`] from persistence. - fn persistence_contract_module( + /// Returns an [`Arc`] clone of the [`ContractModule`]. + fn contract_module( &self, module_ref: ModuleReference, ) -> Result, ModuleMissing> { @@ -517,46 +516,39 @@ impl Chain { Ok(Arc::clone(module)) } - /// Returns an immutable reference to [`AccountInfo`] from persistence. - fn persistence_get_account( - &self, - address: AccountAddress, - ) -> Result<&AccountInfo, AccountMissing> { + /// Returns an immutable reference to an [`Account`]. + pub fn get_account(&self, address: AccountAddress) -> Result<&Account, AccountMissing> { self.accounts.get(&address).ok_or(AccountMissing(address)) } - /// Returns a mutable reference to [`AccountInfo`] from persistence. - fn persistence_get_account_mut( - &mut self, - address: AccountAddress, - ) -> Result<&mut AccountInfo, AccountMissing> { + /// Returns a mutable reference to [`Account`]. + fn get_account_mut(&mut self, address: AccountAddress) -> Result<&mut Account, AccountMissing> { self.accounts .get_mut(&address) .ok_or(AccountMissing(address)) } - /// Check whether an account exists. - fn persistence_account_exists(&self, address: AccountAddress) -> bool { + /// Check whether an [`Account`] exists. + pub fn account_exists(&self, address: AccountAddress) -> bool { self.accounts.contains_key(&address) } - /// Check whether a contract exists. - fn persistence_contract_exists(&self, address: ContractAddress) -> bool { + /// Check whether a [`Contract`] exists. + pub fn contract_exists(&self, address: ContractAddress) -> bool { self.contracts.contains_key(&address) } - /// Check whether the address exists in persistence. I.e. if it is an + /// Check whether the [`Address`] exists. I.e. if it is an /// account, whether the account exists, and if it is a contract, whether /// the contract exists. - fn persistence_address_exists(&self, address: Address) -> bool { + fn address_exists(&self, address: Address) -> bool { match address { - Address::Account(acc) => self.persistence_account_exists(acc), - Address::Contract(contr) => self.persistence_contract_exists(contr), + Address::Account(acc) => self.account_exists(acc), + Address::Contract(contr) => self.contract_exists(contr), } } - /// Convert the wasm_chain_integration result to the one used here and - /// calculate the transaction fee. + /// Convert the [`InvokeEntrypointResult`] to a contract success or error. /// /// The `energy_for_state_increase` is only used if the result was a /// success. @@ -566,9 +558,9 @@ impl Chain { /// /// *Preconditions*: /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` - fn convert_update_aux_response( + fn convert_invoke_entrypoint_result( &self, - update_aux_response: UpdateAuxResponse, + update_aux_response: InvokeEntrypointResult, chain_events: Vec, energy_reserved: Energy, energy_for_state_increase: Energy, @@ -621,7 +613,7 @@ impl TestPolicies { // TODO: Add helper functions for creating arbitrary valid policies. } -impl AccountInfo { +impl Account { /// Create a new [`Self`] with the provided parameters. /// The `signature_count` must be >= 1 for transaction costs to be /// realistic. @@ -763,17 +755,11 @@ mod tests { #[test] fn creating_accounts_work() { let mut chain = Chain::new(); - chain.create_account(ACC_0, AccountInfo::new(Amount::from_ccd(1))); - chain.create_account(ACC_1, AccountInfo::new(Amount::from_ccd(2))); + chain.create_account(ACC_0, Account::new(Amount::from_ccd(1))); + chain.create_account(ACC_1, Account::new(Amount::from_ccd(2))); assert_eq!(chain.accounts.len(), 2); - assert_eq!( - chain.persistence_account_balance(ACC_0), - Some(Amount::from_ccd(1)) - ); - assert_eq!( - chain.persistence_account_balance(ACC_1), - Some(Amount::from_ccd(2)) - ); + assert_eq!(chain.account_balance(ACC_0), Some(Amount::from_ccd(1))); + assert_eq!(chain.account_balance(ACC_1), Some(Amount::from_ccd(2))); } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 1e13c202..226c3f56 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -3,7 +3,7 @@ use crate::{ constants, impls::{lookup_module_cost, to_interpreter_energy}, types::{ - AccountInfo, Chain, ChainEvent, Contract, ContractModule, InsufficientBalanceError, + Account, Chain, ChainEvent, Contract, ContractModule, InsufficientBalanceError, TransferError, }, }; @@ -25,8 +25,9 @@ use wasm_chain_integration::{ }; use wasm_transform::artifact; -impl ContractInvocation { - /// Execute an entrypoint on a contract. +impl EntrypointInvocationHandler { + /// Invoke an entrypoint and get the result, [`Changeset`], and chain + /// events. /// /// *Preconditions:* /// - `invoker` exists @@ -34,7 +35,7 @@ impl ContractInvocation { /// - `sender` exists /// - if the contract (`contract_address`) exists, then its `module` must /// also exist. - pub(crate) fn execute( + pub(crate) fn invoke_entrypoint_and_get_changes( chain: &Chain, invoker: AccountAddress, sender: Address, @@ -47,7 +48,7 @@ impl ContractInvocation { // Used to handle account balance queries for the invoker account. invoker_amount_reserved_for_nrg: Amount, reserved_energy: Energy, - ) -> (ChangeSet, UpdateAuxResponse, Vec) { + ) -> (InvokeEntrypointResult, ChangeSet, Vec) { let mut contract_invocation = Self { changeset: ChangeSet::new(), accounts: chain.accounts.clone(), /* TODO: These three maps should ideally @@ -61,7 +62,7 @@ impl ContractInvocation { }; let mut chain_events = Vec::new(); - let response = contract_invocation.contract_update_aux( + let result = contract_invocation.invoke_entrypoint( invoker, sender, contract_address, @@ -72,12 +73,10 @@ impl ContractInvocation { to_interpreter_energy(reserved_energy), &mut chain_events, ); - (contract_invocation.changeset, response, chain_events) + (result, contract_invocation.changeset, chain_events) } - /// Used for handling contract invokes internally. - /// - /// TODO: Find a better name for this function. + /// Used for handling contract entrypoint invocations internally. /// /// *Preconditions:* /// - `invoker` exists @@ -85,7 +84,7 @@ impl ContractInvocation { /// - `sender` exists /// - if the contract (`contract_address`) exists, then its `module` must /// also exist. - fn contract_update_aux( + fn invoke_entrypoint( &mut self, invoker: AccountAddress, sender: Address, @@ -101,17 +100,15 @@ impl ContractInvocation { // [`Energy`]. mut remaining_energy: InterpreterEnergy, chain_events: &mut Vec, - ) -> UpdateAuxResponse { + ) -> InvokeEntrypointResult { // Move the amount from the sender to the contract, if any. // And get the new self_balance. let instance_self_balance = if amount.micro_ccd() > 0 { let transfer_result = match sender { - Address::Account(sender_account) => self.changeset_transfer_account_to_contract( - amount, - sender_account, - contract_address, - ), - Address::Contract(sender_contract) => self.changeset_transfer_contract_to_contract( + Address::Account(sender_account) => { + self.transfer_from_account_to_contract(amount, sender_account, contract_address) + } + Address::Contract(sender_contract) => self.transfer_from_contract_to_contract( amount, sender_contract, contract_address, @@ -126,7 +123,7 @@ impl ContractInvocation { }; // Return early. // TODO: Should we charge any energy for this? - return UpdateAuxResponse { + return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind }, logs: None, remaining_energy, @@ -134,13 +131,13 @@ impl ContractInvocation { } } } else { - match self.changeset_contract_balance(contract_address) { + match self.contract_balance(contract_address) { Some(self_balance) => self_balance, None => { // Return early. // TODO: For the top-most update, we should catch this in `contract_update` and // return `ContractUpdateError::EntrypointMissing`. - return UpdateAuxResponse { + return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, @@ -156,7 +153,7 @@ impl ContractInvocation { .contracts .get(&contract_address) .expect("Contract known to exist at this point"); - let artifact = self.changeset_contract_module(contract_address); + let artifact = self.contract_module(contract_address); // Subtract the cost of looking up the module remaining_energy = @@ -174,7 +171,7 @@ impl ContractInvocation { OwnedReceiveName::new_unchecked(fallback_receive_name) } else { // Return early. - return UpdateAuxResponse { + return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, @@ -209,7 +206,7 @@ impl ContractInvocation { // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = self.changeset_contract_state(contract_address); + let mut mutable_state = self.contract_state(contract_address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -233,7 +230,7 @@ impl ContractInvocation { // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. - let mut data = ProcessReceiveData { + let mut data = InvocationData { invoker, address: contract_address, contract_name, @@ -260,10 +257,9 @@ impl ContractInvocation { remaining_energy, } => { let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { + InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Success { - new_balance: self - .changeset_contract_balance_unchecked(contract_address), + new_balance: self.contract_balance_unchecked(contract_address), data: Some(return_value), }, logs: Some(logs), @@ -279,7 +275,7 @@ impl ContractInvocation { remaining_energy, } => { let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { + InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::ContractReject { code: reason, @@ -295,7 +291,7 @@ impl ContractInvocation { remaining_energy, } => { let remaining_energy = InterpreterEnergy::from(remaining_energy); - UpdateAuxResponse { + InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, @@ -305,7 +301,7 @@ impl ContractInvocation { } v1::ReceiveResult::OutOfEnergy => { let remaining_energy = InterpreterEnergy::from(0); - UpdateAuxResponse { + InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, @@ -332,7 +328,7 @@ impl ContractInvocation { /// /// Precondition: /// - Assumes that `from` contract exists. - fn changeset_transfer_contract_to_account( + fn transfer_from_contract_to_account( &mut self, amount: Amount, from: ContractAddress, @@ -344,20 +340,20 @@ impl ContractInvocation { } // Make the transfer. - let new_balance = - self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_account_balance(to, AmountDelta::Positive(amount)) + let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + self.change_account_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(new_balance) } /// Make a transfer between contracts in the changeset. + /// /// Returns the new balance of `from`. /// /// Precondition: /// - Assumes that `from` contract exists. - fn changeset_transfer_contract_to_contract( + fn transfer_from_contract_to_contract( &mut self, amount: Amount, from: ContractAddress, @@ -369,20 +365,20 @@ impl ContractInvocation { } // Make the transfer. - let new_balance = - self.changeset_change_contract_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + self.change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(new_balance) } /// Make a transfer from an account to a contract in the changeset. + /// /// Returns the new balance of `from`. /// /// Precondition: /// - Assumes that `from` account exists. - fn changeset_transfer_account_to_contract( + fn transfer_from_account_to_contract( &mut self, amount: Amount, from: AccountAddress, @@ -394,21 +390,22 @@ impl ContractInvocation { } // Make the transfer. - let new_balance = - self.changeset_change_account_balance(from, AmountDelta::Negative(amount))?; - self.changeset_change_contract_balance(to, AmountDelta::Positive(amount)) + let new_balance = self.change_account_balance(from, AmountDelta::Negative(amount))?; + self.change_contract_balance(to, AmountDelta::Positive(amount)) .expect("Cannot fail when adding"); Ok(new_balance) } // TODO: Should we handle overflows explicitly? /// Changes the contract balance in the topmost checkpoint on the changeset. + /// /// If contract isn't already present in the changeset, it is added. + /// /// Returns the new balance. /// /// Precondition: /// - Contract must exist. - fn changeset_change_contract_balance( + fn change_contract_balance( &mut self, address: ContractAddress, delta: AmountDelta, @@ -444,12 +441,14 @@ impl ContractInvocation { // TODO: Should we handle overflows explicitly? /// Changes the account balance in the topmost checkpoint on the changeset. + /// /// If account isn't already present in the changeset, it is added. + /// /// Returns the new balance. /// /// Precondition: /// - Account must exist. - fn changeset_change_account_balance( + fn change_account_balance( &mut self, address: AccountAddress, delta: AmountDelta, @@ -488,14 +487,14 @@ impl ContractInvocation { /// /// *Preconditions:* /// - Contract must exist. - fn changeset_contract_balance_unchecked(&self, address: ContractAddress) -> Amount { - self.changeset_contract_balance(address) + fn contract_balance_unchecked(&self, address: ContractAddress) -> Amount { + self.contract_balance(address) .expect("Precondition violation: contract must exist") } /// Looks up the contract balance from the topmost checkpoint on the /// changeset. Or, alternatively, from persistence. - fn changeset_contract_balance(&self, address: ContractAddress) -> Option { + fn contract_balance(&self, address: ContractAddress) -> Option { match self.changeset.current().contracts.get(&address) { Some(changes) => Some(changes.current_balance()), None => self.contracts.get(&address).map(|c| c.self_balance), @@ -509,7 +508,7 @@ impl ContractInvocation { /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. - fn changeset_contract_module(&self, address: ContractAddress) -> Arc { + fn contract_module(&self, address: ContractAddress) -> Arc { match self .changeset .current() @@ -544,7 +543,7 @@ impl ContractInvocation { /// /// *Preconditions:* /// - Contract instance must exist. - fn changeset_contract_state(&self, address: ContractAddress) -> trie::MutableState { + fn contract_state(&self, address: ContractAddress) -> trie::MutableState { match self .changeset .current() @@ -566,7 +565,7 @@ impl ContractInvocation { /// Looks up the account balance for an account by first checking the /// changeset, then the persisted values. - fn changeset_account_balance(&self, address: AccountAddress) -> Option { + fn account_balance(&self, address: AccountAddress) -> Option { match self .changeset .current() @@ -592,11 +591,7 @@ impl ContractInvocation { /// /// *Preconditions:* /// - Contract must exist. - fn changeset_save_state_changes( - &mut self, - address: ContractAddress, - state: &mut trie::MutableState, - ) { + fn save_state_changes(&mut self, address: ContractAddress, state: &mut trie::MutableState) { let mut loader = v1::trie::Loader::new(&[][..]); self.changeset .current_mut() @@ -631,7 +626,7 @@ impl ContractInvocation { /// *Preconditions:* /// - Contract must exist. /// - Module must exist. - fn changeset_save_module_upgrade( + fn save_module_upgrade( &mut self, address: ContractAddress, module_reference: ModuleReference, @@ -671,7 +666,7 @@ impl ContractInvocation { /// /// It looks it up in the changeset, and if it isn't there, it will return /// `0`. - fn changeset_modification_index(&self, address: ContractAddress) -> u32 { + fn modification_index(&self, address: ContractAddress) -> u32 { self.changeset .current() .contracts @@ -680,10 +675,10 @@ impl ContractInvocation { } /// Makes a new checkpoint. - fn changeset_checkpoint(&mut self) { self.changeset.checkpoint(); } + fn checkpoint(&mut self) { self.changeset.checkpoint(); } /// Roll back to the previous checkpoint. - fn changeset_rollback(&mut self) { self.changeset.rollback(); } + fn rollback(&mut self) { self.changeset.rollback(); } } impl ChangeSet { @@ -744,7 +739,7 @@ impl ChangeSet { mut self, remaining_energy: Energy, invoked_contract: ContractAddress, - persisted_accounts: &mut BTreeMap, + persisted_accounts: &mut BTreeMap, persisted_contracts: &mut BTreeMap, ) -> Result<(Energy, bool), OutOfEnergy> { let current = self.current_mut(); @@ -967,7 +962,7 @@ impl AccountChanges { } } -impl<'a, 'b> ProcessReceiveData<'a, 'b> { +impl<'a, 'b> InvocationData<'a, 'b> { /// Process a receive function until completion. /// /// *Preconditions*: @@ -997,8 +992,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Save changes to changeset. if state_changed { - self.chain - .changeset_save_state_changes(self.address, &mut self.state); + self.chain.save_state_changes(self.address, &mut self.state); } Ok(v1::ReceiveResult::Success { @@ -1030,7 +1024,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { println!("\t\tTransferring {} CCD to {}", amount, to); - let response = match self.chain.changeset_transfer_contract_to_account( + let response = match self.chain.transfer_from_contract_to_account( amount, self.address, to, @@ -1089,24 +1083,22 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.chain_events.push(interrupt_event); if state_changed { - self.chain - .changeset_save_state_changes(self.address, &mut self.state); + self.chain.save_state_changes(self.address, &mut self.state); } // Save the modification index before the invoke. - let mod_idx_before_invoke = - self.chain.changeset_modification_index(self.address); + let mod_idx_before_invoke = self.chain.modification_index(self.address); // Make a checkpoint before calling another contract so that we may roll // back. - self.chain.changeset_checkpoint(); + self.chain.checkpoint(); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", address, parameter ); - let res = self.chain.contract_update_aux( + let res = self.chain.invoke_entrypoint( self.invoker, Address::Contract(self.address), address, @@ -1122,17 +1114,17 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // Remove the last state changes if the invocation failed. let state_changed = if !success { - self.chain.changeset_rollback(); + self.chain.rollback(); false // We rolled back, so no changes were made // to this contract. } else { let mod_idx_after_invoke = - self.chain.changeset_modification_index(self.address); + self.chain.modification_index(self.address); let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; if state_changed { // Update the state field with the newest value from the // changeset. - self.state = self.chain.changeset_contract_state(self.address); + self.state = self.chain.contract_state(self.address); } state_changed }; @@ -1192,11 +1184,9 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { self.contract_name.as_contract_name().get_chain_name(), ) { // Update module reference in the changeset. - let old_module_ref = - self.chain.changeset_save_module_upgrade( - self.address, - module_ref, - ); + let old_module_ref = self + .chain + .save_module_upgrade(self.address, module_ref); // Charge for the initialization cost. energy_after_invoke -= to_interpreter_energy( @@ -1215,7 +1205,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { v1::InvokeResponse::Success { new_balance: self .chain - .changeset_contract_balance_unchecked(self.address), + .contract_balance_unchecked(self.address), data: None, } } else { @@ -1249,7 +1239,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // should be included. That is handled by // the `chain` struct already. transaction. // However, that is hand - let response = match self.chain.changeset_account_balance(address) { + let response = match self.chain.account_balance(address) { Some(acc_bal) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for @@ -1268,7 +1258,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { v1::InvokeResponse::Success { new_balance: self .chain - .changeset_contract_balance_unchecked(self.address), + .contract_balance_unchecked(self.address), data: Some(balances), } } @@ -1297,7 +1287,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { v1::Interrupt::QueryContractBalance { address } => { println!("Querying contract balance of {}", address); - let response = match self.chain.changeset_contract_balance(address) { + let response = match self.chain.contract_balance(address) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, @@ -1306,7 +1296,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { // `self.address`. new_balance: self .chain - .changeset_contract_balance_unchecked(self.address), + .contract_balance_unchecked(self.address), data: Some(to_bytes(&bal)), }, }; @@ -1335,9 +1325,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); let response = v1::InvokeResponse::Success { - new_balance: self - .chain - .changeset_contract_balance_unchecked(self.address), + new_balance: self.chain.contract_balance_unchecked(self.address), data: Some(to_bytes(&exchange_rates)), }; @@ -1367,7 +1355,7 @@ impl<'a, 'b> ProcessReceiveData<'a, 'b> { } } -impl UpdateAuxResponse { +impl InvokeEntrypointResult { pub(crate) fn is_success(&self) -> bool { matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) } diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index e56b0254..82db5e56 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -1,12 +1,10 @@ -//! Functionality and types for invoking a contract either for a contract update -//! or a contract invoke. +//! Functionality and types for invoking contract entrypoints. //! //! Contract invocation is effectful and transactional. //! We therefore keep track of changes during execution in a [`ChangeSet`]. //! //! Once the execution (transaction) has finished, the changes can then be //! persisted (saved) or discarded, dependent on whether it succeeded or not. -//! For contract invokes the changes will always be discarded. //! //! The changes that may occur are: //! - Mutations to contract state, @@ -15,4 +13,4 @@ mod impls; mod types; -pub(crate) use types::{ContractInvocation, UpdateAuxResponse}; +pub(crate) use types::{EntrypointInvocationHandler, InvokeEntrypointResult}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 388f699f..ae6d0cdd 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,4 +1,4 @@ -use crate::types::{AccountInfo, ChainEvent, Contract, ContractModule}; +use crate::types::{Account, ChainEvent, Contract, ContractModule}; use concordium_base::contracts_common::{ AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, SlotTime, @@ -10,8 +10,8 @@ use wasm_chain_integration::{ InterpreterEnergy, }; -// TODO: Find a better name. -pub(crate) struct UpdateAuxResponse { +/// The result of invoking an entrypoint. +pub(crate) struct InvokeEntrypointResult { /// The result from the invoke. pub(crate) invoke_response: InvokeResponse, /// Will be `Some` if and only if `invoke_response` is `Success`. @@ -20,9 +20,10 @@ pub(crate) struct UpdateAuxResponse { pub(crate) remaining_energy: InterpreterEnergy, } -pub(crate) struct ContractInvocation { +/// A type that supports invoking a contract entrypoint. +pub(crate) struct EntrypointInvocationHandler { pub(super) changeset: ChangeSet, - pub(super) accounts: BTreeMap, + pub(super) accounts: BTreeMap, pub(super) modules: BTreeMap>, pub(super) contracts: BTreeMap, pub(super) slot_time: SlotTime, @@ -38,7 +39,7 @@ pub(crate) struct ChangeSet { } /// Data held for accounts and contracts during the execution of a contract -/// update or invocation. +/// entrypoint. #[derive(Clone, Debug)] pub(super) struct Changes { /// The contracts which have changes. @@ -47,8 +48,7 @@ pub(super) struct Changes { pub(super) accounts: BTreeMap, } -/// Data held for an account during the execution of an update or invoke -/// transaction. +/// Data held for an account during the execution of a contract entrypoint. #[derive(Clone, Debug)] pub(super) struct AccountChanges { /// Should never be modified. @@ -56,8 +56,7 @@ pub(super) struct AccountChanges { pub(super) balance_delta: AmountDelta, } -/// Data held for a contract during the execution of a contract update or -/// invocation. +/// Data held for a contract during the execution of a contract entrypoint. #[derive(Clone, Debug)] pub(super) struct ContractChanges { /// An index that is used to check whether a caller contract has been @@ -74,9 +73,14 @@ pub(super) struct ContractChanges { pub(super) module: Option, } -/// Data needed to recursively process a contract update or invocation to -/// completion. -pub(super) struct ProcessReceiveData<'a, 'b> { +/// Data needed to recursively process a contract entrypoint to completion. +/// +/// In particular, this keeps the data necessary for resuming a contract +/// entrypoint after an interrupt. +/// +/// One `InvocationData` is created for each time +/// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. +pub(super) struct InvocationData<'a, 'b> { /// The invoker. pub(super) invoker: AccountAddress, /// The contract being called. @@ -94,7 +98,7 @@ pub(super) struct ProcessReceiveData<'a, 'b> { /// The entrypoint to execute. pub(super) entrypoint: OwnedEntrypointName, /// A reference to the chain. - pub(super) chain: &'a mut ContractInvocation, + pub(super) chain: &'a mut EntrypointInvocationHandler, /// The current state. pub(super) state: MutableState, /// Chain events that have occurred during the execution. @@ -116,6 +120,6 @@ pub(super) enum AmountDelta { #[derive(Debug)] pub(super) struct UnderflowError; -/// The contract ran out of energy during execution of an update or invocation. +/// The contract ran out of energy during execution of a contract entrypoint. #[derive(PartialEq, Eq, Debug)] pub(crate) struct OutOfEnergy; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 42b4f58e..cc25e9b6 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -30,7 +30,7 @@ pub struct Chain { /// Euro per Energy ratio. pub euro_per_energy: ExchangeRate, /// Accounts and info about them. - pub accounts: BTreeMap, + pub accounts: BTreeMap, /// Smart contract modules. pub modules: BTreeMap>, /// Smart contract instances. @@ -39,7 +39,7 @@ pub struct Chain { pub next_contract_index: u64, } -/// A smart contract instance along. +/// A smart contract instance. #[derive(Clone)] pub struct Contract { /// The module which contains this contract. @@ -57,9 +57,9 @@ pub struct Contract { /// Account policies for testing. pub struct TestPolicies(pub v0::OwnedPolicyBytes); -/// Data about an [`AccountAddress`]. +/// An account. #[derive(Clone)] -pub struct AccountInfo { +pub struct Account { /// The account balance. TODO: Add all three types of balances. pub balance: Amount, /// Account policies. diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 950ed59f..44e3b269 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -9,7 +9,7 @@ const ACC_1: AccountAddress = AccountAddress([1; 32]); fn deploying_valid_module_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res = chain .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") @@ -17,7 +17,7 @@ fn deploying_valid_module_works() { assert_eq!(chain.modules.len(), 1); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some(initial_balance - res.transaction_fee) ); } @@ -26,7 +26,7 @@ fn deploying_valid_module_works() { fn initializing_valid_contract_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") @@ -43,7 +43,7 @@ fn initializing_valid_contract_works() { ) .expect("Initializing valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!(chain.contracts.len(), 1); @@ -53,7 +53,7 @@ fn initializing_valid_contract_works() { fn initializing_with_invalid_parameter_fails() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") @@ -77,7 +77,7 @@ fn initializing_with_invalid_parameter_fails() { transaction_fee, .. }) => assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) ), _ => panic!("Expected valid chain error."), @@ -88,7 +88,7 @@ fn initializing_with_invalid_parameter_fails() { fn updating_valid_contract_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") @@ -131,7 +131,7 @@ fn updating_valid_contract_works() { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -150,7 +150,7 @@ fn updating_valid_contract_works() { fn updating_and_invoking_with_missing_sender_fails() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let missing_account = Address::Account(ACC_1); let missing_contract = Address::Contract(ContractAddress::new(100, 0)); @@ -235,8 +235,8 @@ mod integrate_contract { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") @@ -279,7 +279,7 @@ mod integrate_contract { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -289,7 +289,7 @@ mod integrate_contract { ) ); assert_eq!( - chain.persistence_account_balance(ACC_1), + chain.account_balance(ACC_1), Some(initial_balance + transfer_amount) ); assert_eq!(res_update.transfers(), [Transfer { @@ -309,7 +309,7 @@ mod integrate_contract { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") @@ -344,7 +344,7 @@ mod integrate_contract { }) => { assert_eq!(code, -3); // The custom contract error code for missing account. assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -361,7 +361,7 @@ mod integrate_contract { fn update_with_integrate_reentry_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") @@ -404,7 +404,7 @@ mod integrate_contract { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -424,7 +424,7 @@ mod integrate_contract { fn update_with_rollback_and_reentry_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") @@ -468,7 +468,7 @@ mod integrate_contract { .expect("Invoking get should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -488,8 +488,8 @@ mod integrate_contract { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); let transfer_amount = Amount::from_ccd(2); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") @@ -537,7 +537,7 @@ mod integrate_contract { fn update_with_fib_reentry_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") @@ -580,7 +580,7 @@ fn update_with_fib_reentry_works() { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index e742cfba..de83da3d 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -24,7 +24,7 @@ const ACC_1: AccountAddress = AccountAddress([1; 32]); fn test_case_1() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) @@ -100,7 +100,7 @@ fn test_case_1() { fn test_case_2() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) @@ -175,8 +175,8 @@ fn test_case_2() { fn test_case_3() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) @@ -234,7 +234,7 @@ fn test_case_3() { fn test_case_4() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 69b1f950..c3790f61 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -20,8 +20,8 @@ mod query_account_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -60,7 +60,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -80,8 +80,8 @@ mod query_account_balance { fn invoker_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. @@ -120,11 +120,11 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!( - chain.persistence_account_balance(ACC_1), + chain.account_balance(ACC_1), // Differs from `expected_balance` as it only includes the actual amount charged // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) @@ -140,8 +140,8 @@ mod query_account_balance { fn transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -184,7 +184,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -194,7 +194,7 @@ mod query_account_balance { ) ); assert_eq!( - chain.persistence_account_balance(ACC_1), + chain.account_balance(ACC_1), Some(initial_balance + amount_to_send) ); assert!(matches!(res_update.chain_events[..], [ @@ -209,8 +209,8 @@ mod query_account_balance { fn balance_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); - chain.create_account(ACC_1, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -249,7 +249,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -268,7 +268,7 @@ mod query_account_balance { fn missing_account_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -307,7 +307,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.persistence_account_balance(ACC_0), + chain.account_balance(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -330,7 +330,7 @@ mod query_contract_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let init_amount = Amount::from_ccd(123); @@ -389,7 +389,7 @@ mod query_contract_balance { fn query_self_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -437,7 +437,7 @@ mod query_contract_balance { fn query_self_after_transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -498,7 +498,7 @@ mod query_contract_balance { fn missing_contract_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -551,7 +551,7 @@ mod query_exchange_rates { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 4d99e630..addea2eb 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -11,7 +11,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain @@ -79,7 +79,7 @@ fn test() { fn test_self_invoke() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain .module_deploy_wasm_v1( @@ -142,7 +142,7 @@ fn test_self_invoke() { fn test_missing_module() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -189,7 +189,7 @@ fn test_missing_module() { fn test_missing_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain .module_deploy_wasm_v1( @@ -242,7 +242,7 @@ fn test_missing_contract() { fn test_twice_in_one_transaction() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) @@ -317,7 +317,7 @@ fn test_twice_in_one_transaction() { fn test_chained_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain .module_deploy_wasm_v1( @@ -368,7 +368,7 @@ fn test_chained_contract() { fn test_reject() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain .module_deploy_wasm_v1( @@ -443,7 +443,7 @@ fn test_reject() { fn test_changing_entrypoint() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, AccountInfo::new(initial_balance)); + chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain .module_deploy_wasm_v1( From f4b4b12d5fad8ebc93a72f6cfc5eae82a9dfb8a8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Feb 2023 11:16:03 +0100 Subject: [PATCH 063/208] Add staked and locked balances to accounts --- contract-testing/src/impls.rs | 66 +++++++++++++++++------- contract-testing/src/invocation/impls.rs | 11 ++-- contract-testing/src/lib.rs | 1 + contract-testing/src/types.rs | 8 +-- contract-testing/tests/basics.rs | 20 +++---- contract-testing/tests/queries.rs | 14 ++--- 6 files changed, 75 insertions(+), 45 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 8c5f64db..70e3f26c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -2,9 +2,9 @@ use concordium_base::{ base::Energy, common, contracts_common::{ - AccountAddress, Address, Amount, ChainMetadata, ContractAddress, ContractName, - EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, SlotTime, - Timestamp, + AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, + ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, + SlotTime, Timestamp, }, smart_contracts::{ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost}, @@ -105,10 +105,10 @@ impl Chain { // Try to subtract cost for account let account = self.get_account_mut(sender)?; - if account.balance < transaction_fee { + if account.balance.available() < transaction_fee { return Err(DeployModuleError::InsufficientFunds); }; - account.balance -= transaction_fee; + account.balance.total -= transaction_fee; // Save the module TODO: Use wasm_module.get_module_ref() and find a proper way // to convert ModuleRef to ModuleReference. @@ -201,7 +201,7 @@ impl Chain { // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.get_account(sender)?; - if account_info.balance < self.calculate_energy_cost(energy_reserved) + amount { + if account_info.balance.available() < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } // Construct the context. @@ -253,7 +253,7 @@ impl Chain { // Save the contract instance self.contracts.insert(contract_address, contract_instance); // Subtract the from the invoker. - self.get_account_mut(sender)?.balance -= amount; + self.get_account_mut(sender)?.balance.total -= amount; Ok(SuccessfulContractInit { contract_address, @@ -306,7 +306,7 @@ impl Chain { }; // Charge the account. // We have to get the account info again because of the borrow checker. - self.get_account_mut(sender)?.balance -= transaction_fee; + self.get_account_mut(sender)?.balance.total -= transaction_fee; res } @@ -338,7 +338,7 @@ impl Chain { // to_ccd(remaining_energy) afterwards? let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); - if account_info.balance < invoker_amount_reserved_for_nrg + amount { + if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } @@ -389,7 +389,7 @@ impl Chain { // Charge the transaction fee irrespective of the result. // TODO: If we charge up front, then we should return to_ccd(remaining_energy) // here instead. - self.get_account_mut(invoker)?.balance -= transaction_fee; + self.get_account_mut(invoker)?.balance.total -= transaction_fee; res } @@ -420,7 +420,7 @@ impl Chain { // Ensure account exists and can pay for the reserved energy and amount let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); - if account_info.balance < invoker_amount_reserved_for_nrg + amount { + if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } @@ -495,10 +495,15 @@ impl Chain { } /// Returns the balance of an account if it exists. - pub fn account_balance(&self, address: AccountAddress) -> Option { + pub fn account_balance(&self, address: AccountAddress) -> Option { self.accounts.get(&address).map(|ai| ai.balance) } + /// Returns the available balance of an account if it exists. + pub fn account_balance_available(&self, address: AccountAddress) -> Option { + self.accounts.get(&address).map(|ai| ai.balance.available()) + } + /// Returns the balance of an contract if it exists. pub fn contract_balance(&self, address: ContractAddress) -> Option { self.contracts.get(&address).map(|ci| ci.self_balance) @@ -618,7 +623,7 @@ impl Account { /// The `signature_count` must be >= 1 for transaction costs to be /// realistic. pub fn new_with_policy_and_signature_count( - balance: Amount, + balance: AccountBalance, policies: TestPolicies, signature_count: u32, ) -> Self { @@ -632,16 +637,16 @@ impl Account { /// Create new [`Self`] with empty account policies but the provided /// `signature_count`. The `signature_count` must be >= 1 for transaction /// costs to be realistic. - pub fn new_with_signature_count(balance: Amount, signature_count: u32) -> Self { + pub fn new_with_signature_count(balance: AccountBalance, signature_count: u32) -> Self { Self { signature_count, - ..Self::new(balance) + ..Self::new_with_balance(balance) } } /// Create new [`Self`] with empty account policies and a signature /// count of `1`. - pub fn new_with_policy(balance: Amount, policies: TestPolicies) -> Self { + pub fn new_with_policy(balance: AccountBalance, policies: TestPolicies) -> Self { Self { balance, policies: policies.0, @@ -651,7 +656,24 @@ impl Account { /// Create new [`Self`] with empty account policies and a signature /// count of `1`. - pub fn new(balance: Amount) -> Self { Self::new_with_policy(balance, TestPolicies::empty()) } + pub fn new_with_balance(balance: AccountBalance) -> Self { + Self::new_with_policy(balance, TestPolicies::empty()) + } + + /// Create new [`Self`] with + /// - empty account policies, + /// - a signature count of `1`, + /// - an [`AccountBalance`] from the `total_balance` provided. + pub fn new(total_balance: Amount) -> Self { + Self::new_with_policy( + AccountBalance { + total: total_balance, + staked: Amount::zero(), + locked: Amount::zero(), + }, + TestPolicies::empty(), + ) + } } impl FailedContractInteraction { @@ -759,7 +781,13 @@ mod tests { chain.create_account(ACC_1, Account::new(Amount::from_ccd(2))); assert_eq!(chain.accounts.len(), 2); - assert_eq!(chain.account_balance(ACC_0), Some(Amount::from_ccd(1))); - assert_eq!(chain.account_balance(ACC_1), Some(Amount::from_ccd(2))); + assert_eq!( + chain.account_balance_available(ACC_0), + Some(Amount::from_ccd(1)) + ); + assert_eq!( + chain.account_balance_available(ACC_1), + Some(Amount::from_ccd(2)) + ); } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 226c3f56..75d8eff4 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -460,7 +460,8 @@ impl EntrypointInvocationHandler { .accounts .get(&address) .expect("Precondition violation: account assumed to exist") - .balance; + .balance + .available(); // Try to apply the balance or return an error if insufficient funds. let new_account_balance = delta.apply_to_balance(original_balance)?; // Insert the changes into the changeset. @@ -563,8 +564,8 @@ impl EntrypointInvocationHandler { } } - /// Looks up the account balance for an account by first checking the - /// changeset, then the persisted values. + /// Looks up the available account balance for an account by first checking + /// the changeset, then the persisted values. fn account_balance(&self, address: AccountAddress) -> Option { match self .changeset @@ -576,7 +577,7 @@ impl EntrypointInvocationHandler { // Account exists in changeset. Some(bal) => Some(bal), // Account doesn't exist in changeset. - None => self.accounts.get(&address).map(|a| a.balance), + None => self.accounts.get(&address).map(|a| a.balance.available()), } } @@ -802,7 +803,7 @@ impl ChangeSet { .expect("Precondition violation: account must exist"); // Update balance. if !changes.balance_delta.is_zero() { - account.balance = changes + account.balance.total = changes .balance_delta .apply_to_balance(changes.original_balance) .expect("Precondition violation: amount delta causes underflow"); diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index adece1f3..d7d5d824 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -4,6 +4,7 @@ mod invocation; pub mod types; pub use types::*; +// Re-export types. pub use concordium_base::{ base::Energy, contracts_common::{ diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index cc25e9b6..651f1cde 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -4,8 +4,8 @@ use thiserror::Error; use concordium_base::{ base::Energy, contracts_common::{ - AccountAddress, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, - OwnedContractName, OwnedEntrypointName, SlotTime, + AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, + ModuleReference, OwnedContractName, OwnedEntrypointName, SlotTime, }, smart_contracts::WasmVersion, }; @@ -60,8 +60,8 @@ pub struct TestPolicies(pub v0::OwnedPolicyBytes); /// An account. #[derive(Clone)] pub struct Account { - /// The account balance. TODO: Add all three types of balances. - pub balance: Amount, + /// The account balance. + pub balance: AccountBalance, /// Account policies. pub policies: v0::OwnedPolicyBytes, /// The number of signatures. The number of signatures affect the cost of diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 44e3b269..99b461d3 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -17,7 +17,7 @@ fn deploying_valid_module_works() { assert_eq!(chain.modules.len(), 1); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some(initial_balance - res.transaction_fee) ); } @@ -43,7 +43,7 @@ fn initializing_valid_contract_works() { ) .expect("Initializing valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!(chain.contracts.len(), 1); @@ -77,7 +77,7 @@ fn initializing_with_invalid_parameter_fails() { transaction_fee, .. }) => assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) ), _ => panic!("Expected valid chain error."), @@ -131,7 +131,7 @@ fn updating_valid_contract_works() { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -279,7 +279,7 @@ mod integrate_contract { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -289,7 +289,7 @@ mod integrate_contract { ) ); assert_eq!( - chain.account_balance(ACC_1), + chain.account_balance_available(ACC_1), Some(initial_balance + transfer_amount) ); assert_eq!(res_update.transfers(), [Transfer { @@ -344,7 +344,7 @@ mod integrate_contract { }) => { assert_eq!(code, -3); // The custom contract error code for missing account. assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -404,7 +404,7 @@ mod integrate_contract { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -468,7 +468,7 @@ mod integrate_contract { .expect("Invoking get should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -580,7 +580,7 @@ fn update_with_fib_reentry_works() { // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index c3790f61..6cfce82d 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -60,7 +60,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -120,11 +120,11 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!( - chain.account_balance(ACC_1), + chain.account_balance_available(ACC_1), // Differs from `expected_balance` as it only includes the actual amount charged // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) @@ -184,7 +184,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -194,7 +194,7 @@ mod query_account_balance { ) ); assert_eq!( - chain.account_balance(ACC_1), + chain.account_balance_available(ACC_1), Some(initial_balance + amount_to_send) ); assert!(matches!(res_update.chain_events[..], [ @@ -249,7 +249,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -307,7 +307,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance(ACC_0), + chain.account_balance_available(ACC_0), Some( initial_balance - res_deploy.transaction_fee From 60dfd717b2e5598721f59dd4c490bf3d92abb626 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Feb 2023 11:42:36 +0100 Subject: [PATCH 064/208] Add method for getting chain events grouped by contract --- contract-testing/src/impls.rs | 26 ++++++++++++++++++++++++++ contract-testing/src/types.rs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 70e3f26c..61df5490 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -711,6 +711,32 @@ impl SuccessfulContractUpdate { }) .collect() } + + /// Get the chain events grouped by which contract they originated from. + pub fn chain_events_per_contract(&self) -> BTreeMap> { + let mut map: BTreeMap> = BTreeMap::new(); + for event in self.chain_events.iter() { + map.entry(event.contract_address()) + .and_modify(|v| v.push(event.clone())) + .or_insert(vec![event.clone()]); + } + map + } +} + +impl ChainEvent { + /// Get the contract address that this event relates to. + /// This means the `address` field for all variant except `Transferred`, + /// where it returns the `from`. + pub fn contract_address(&self) -> ContractAddress { + match self { + ChainEvent::Interrupted { address, .. } => *address, + ChainEvent::Resumed { address, .. } => *address, + ChainEvent::Upgraded { address, .. } => *address, + ChainEvent::Updated { address, .. } => *address, + ChainEvent::Transferred { from, .. } => *from, + } + } } /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 651f1cde..17dbe3ea 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -70,7 +70,7 @@ pub struct Account { } /// An event that occurred during a contract update or invocation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ChainEvent { /// A contract was interrupted. Interrupted { From e010cb9a2d6ea56ee38e3040030ce5aa4b2b72fd Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Feb 2023 13:13:42 +0100 Subject: [PATCH 065/208] Use staked and locked account balances in query --- contract-testing/src/invocation/impls.rs | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 75d8eff4..b00a1c02 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -10,8 +10,8 @@ use crate::{ use concordium_base::{ base::Energy, contracts_common::{ - to_bytes, AccountAddress, Address, Amount, ChainMetadata, ContractAddress, ModuleReference, - OwnedEntrypointName, OwnedReceiveName, Parameter, + to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, + ModuleReference, OwnedEntrypointName, OwnedReceiveName, Parameter, }, }; use std::{ @@ -564,9 +564,10 @@ impl EntrypointInvocationHandler { } } - /// Looks up the available account balance for an account by first checking + /// Looks up the account balance for an account by first checking /// the changeset, then the persisted values. - fn account_balance(&self, address: AccountAddress) -> Option { + fn account_balance(&self, address: AccountAddress) -> Option { + let account_balance = self.accounts.get(&address).map(|a| a.balance)?; match self .changeset .current() @@ -575,9 +576,15 @@ impl EntrypointInvocationHandler { .map(|a| a.current_balance()) { // Account exists in changeset. - Some(bal) => Some(bal), + // Return the staked and locked balances from persistence, as they can't change during + // entrypoint invocation. + Some(total) => Some(AccountBalance { + total, + staked: account_balance.staked, + locked: account_balance.locked, + }), // Account doesn't exist in changeset. - None => self.accounts.get(&address).map(|a| a.balance.available()), + None => Some(account_balance), } } @@ -1238,29 +1245,22 @@ impl<'a, 'b> InvocationData<'a, 'b> { println!("\t\tQuerying account balance of {}", address); // When querying an account, the amounts from any `invoke_transfer`s // should be included. That is handled by - // the `chain` struct already. transaction. - // However, that is hand + // the `chain` struct already. let response = match self.chain.account_balance(address) { - Some(acc_bal) => { + Some(mut balance) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for // the reserved max energy. The former is handled in - // `contract_update_aux`, but the latter is represented in + // `invoke_entrypoint`, but the latter is represented in // `self.invoker_amount_reserved_for_nrg`. - let acc_bal = if address == self.invoker { - acc_bal - self.invoker_amount_reserved_for_nrg - } else { - acc_bal - }; - - // TODO: Do we need non-zero staked and shielded balances? - let balances = - to_bytes(&(acc_bal, Amount::zero(), Amount::zero())); + if address == self.invoker { + balance.total -= self.invoker_amount_reserved_for_nrg; + } v1::InvokeResponse::Success { new_balance: self .chain .contract_balance_unchecked(self.address), - data: Some(balances), + data: Some(to_bytes(&balance)), } } None => v1::InvokeResponse::Failure { From 1059e0f63631904efa6db1dc2bcb7ea4becefca5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Feb 2023 13:19:47 +0100 Subject: [PATCH 066/208] Rename field to match type name --- contract-testing/src/invocation/impls.rs | 59 +++++++++++++----------- contract-testing/src/invocation/types.rs | 5 +- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b00a1c02..337e1806 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -237,7 +237,7 @@ impl EntrypointInvocationHandler { amount, invoker_amount_reserved_for_nrg, entrypoint, - chain: self, + invocation_handler: self, state: mutable_state, chain_events: Vec::new(), loader, @@ -974,8 +974,8 @@ impl<'a, 'b> InvocationData<'a, 'b> { /// Process a receive function until completion. /// /// *Preconditions*: - /// - Contract instance exists in `chain.contracts`. - /// - Account exists in `chain.accounts`. + /// - Contract instance exists in `invocation_handler.contracts`. + /// - Account exists in `invocation_handler.accounts`. fn process( &mut self, res: ExecResult>, @@ -1000,7 +1000,8 @@ impl<'a, 'b> InvocationData<'a, 'b> { // Save changes to changeset. if state_changed { - self.chain.save_state_changes(self.address, &mut self.state); + self.invocation_handler + .save_state_changes(self.address, &mut self.state); } Ok(v1::ReceiveResult::Success { @@ -1032,11 +1033,10 @@ impl<'a, 'b> InvocationData<'a, 'b> { println!("\t\tTransferring {} CCD to {}", amount, to); - let response = match self.chain.transfer_from_contract_to_account( - amount, - self.address, - to, - ) { + let response = match self + .invocation_handler + .transfer_from_contract_to_account(amount, self.address, to) + { Ok(new_balance) => v1::InvokeResponse::Success { new_balance, data: None, @@ -1091,22 +1091,24 @@ impl<'a, 'b> InvocationData<'a, 'b> { self.chain_events.push(interrupt_event); if state_changed { - self.chain.save_state_changes(self.address, &mut self.state); + self.invocation_handler + .save_state_changes(self.address, &mut self.state); } // Save the modification index before the invoke. - let mod_idx_before_invoke = self.chain.modification_index(self.address); + let mod_idx_before_invoke = + self.invocation_handler.modification_index(self.address); // Make a checkpoint before calling another contract so that we may roll // back. - self.chain.checkpoint(); + self.invocation_handler.checkpoint(); println!( "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", address, parameter ); - let res = self.chain.invoke_entrypoint( + let res = self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), address, @@ -1122,17 +1124,18 @@ impl<'a, 'b> InvocationData<'a, 'b> { // Remove the last state changes if the invocation failed. let state_changed = if !success { - self.chain.rollback(); + self.invocation_handler.rollback(); false // We rolled back, so no changes were made // to this contract. } else { let mod_idx_after_invoke = - self.chain.modification_index(self.address); + self.invocation_handler.modification_index(self.address); let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; if state_changed { // Update the state field with the newest value from the // changeset. - self.state = self.chain.contract_state(self.address); + self.state = + self.invocation_handler.contract_state(self.address); } state_changed }; @@ -1179,7 +1182,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { ) .energy; - let response = match self.chain.modules.get(&module_ref) { + let response = match self.invocation_handler.modules.get(&module_ref) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::UpgradeInvalidModuleRef, }, @@ -1193,7 +1196,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { ) { // Update module reference in the changeset. let old_module_ref = self - .chain + .invocation_handler .save_module_upgrade(self.address, module_ref); // Charge for the initialization cost. @@ -1212,7 +1215,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { v1::InvokeResponse::Success { new_balance: self - .chain + .invocation_handler .contract_balance_unchecked(self.address), data: None, } @@ -1246,7 +1249,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { // When querying an account, the amounts from any `invoke_transfer`s // should be included. That is handled by // the `chain` struct already. - let response = match self.chain.account_balance(address) { + let response = match self.invocation_handler.account_balance(address) { Some(mut balance) => { // If you query the invoker account, it should also // take into account the send-amount and the amount reserved for @@ -1258,7 +1261,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { } v1::InvokeResponse::Success { new_balance: self - .chain + .invocation_handler .contract_balance_unchecked(self.address), data: Some(to_bytes(&balance)), } @@ -1288,7 +1291,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { v1::Interrupt::QueryContractBalance { address } => { println!("Querying contract balance of {}", address); - let response = match self.chain.contract_balance(address) { + let response = match self.invocation_handler.contract_balance(address) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, @@ -1296,7 +1299,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { // Balance of contract querying. Won't change. Notice the // `self.address`. new_balance: self - .chain + .invocation_handler .contract_balance_unchecked(self.address), data: Some(to_bytes(&bal)), }, @@ -1322,11 +1325,15 @@ impl<'a, 'b> InvocationData<'a, 'b> { v1::Interrupt::QueryExchangeRates => { println!("Querying exchange rates"); - let exchange_rates = - (self.chain.euro_per_energy, self.chain.micro_ccd_per_euro); + let exchange_rates = ( + self.invocation_handler.euro_per_energy, + self.invocation_handler.micro_ccd_per_euro, + ); let response = v1::InvokeResponse::Success { - new_balance: self.chain.contract_balance_unchecked(self.address), + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), data: Some(to_bytes(&exchange_rates)), }; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index ae6d0cdd..225ed899 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -97,8 +97,9 @@ pub(super) struct InvocationData<'a, 'b> { pub(super) invoker_amount_reserved_for_nrg: Amount, /// The entrypoint to execute. pub(super) entrypoint: OwnedEntrypointName, - /// A reference to the chain. - pub(super) chain: &'a mut EntrypointInvocationHandler, + /// A reference to the [`EntrypointInvocationHandler`], which is used to for + /// handling interrupts and for querying chain data. + pub(super) invocation_handler: &'a mut EntrypointInvocationHandler, /// The current state. pub(super) state: MutableState, /// Chain events that have occurred during the execution. From 0a7422da6ee6e4bde4203d729a231577bfcb7905 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Feb 2023 13:21:06 +0100 Subject: [PATCH 067/208] Remove unneeded field --- contract-testing/src/invocation/impls.rs | 15 +++++++-------- contract-testing/src/invocation/types.rs | 6 ++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 337e1806..d37b250b 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -240,7 +240,6 @@ impl EntrypointInvocationHandler { invocation_handler: self, state: mutable_state, chain_events: Vec::new(), - loader, }; // Process the receive invocation to the completion. @@ -970,7 +969,7 @@ impl AccountChanges { } } -impl<'a, 'b> InvocationData<'a, 'b> { +impl<'a> InvocationData<'a> { /// Process a receive function until completion. /// /// *Preconditions*: @@ -1075,7 +1074,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { InterpreterEnergy::from(remaining_energy), &mut self.state, false, // never changes on transfers - self.loader, + v1::trie::Loader::new(&[][..]), ); // Resume @@ -1164,7 +1163,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { res.remaining_energy, &mut self.state, state_changed, - self.loader, + v1::trie::Loader::new(&[][..]), ); self.process(resume_res) @@ -1239,7 +1238,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { InterpreterEnergy::from(energy_after_invoke), &mut self.state, state_changed, - self.loader, + v1::trie::Loader::new(&[][..]), ); self.process(resume_res) @@ -1283,7 +1282,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { InterpreterEnergy::from(energy_after_invoke), &mut self.state, false, // State never changes on queries. - self.loader, + v1::trie::Loader::new(&[][..]), ); self.process(resume_res) @@ -1317,7 +1316,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { InterpreterEnergy::from(energy_after_invoke), &mut self.state, false, // State never changes on queries. - self.loader, + v1::trie::Loader::new(&[][..]), ); self.process(resume_res) @@ -1349,7 +1348,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { InterpreterEnergy::from(energy_after_invoke), &mut self.state, false, // State never changes on queries. - self.loader, + v1::trie::Loader::new(&[][..]), ); self.process(resume_res) diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 225ed899..9f2c6fd6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -6,7 +6,7 @@ use concordium_base::contracts_common::{ use std::{collections::BTreeMap, sync::Arc}; use wasm_chain_integration::{ v0, - v1::{self, trie::MutableState, InvokeResponse}, + v1::{trie::MutableState, InvokeResponse}, InterpreterEnergy, }; @@ -80,7 +80,7 @@ pub(super) struct ContractChanges { /// /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. -pub(super) struct InvocationData<'a, 'b> { +pub(super) struct InvocationData<'a> { /// The invoker. pub(super) invoker: AccountAddress, /// The contract being called. @@ -104,8 +104,6 @@ pub(super) struct InvocationData<'a, 'b> { pub(super) state: MutableState, /// Chain events that have occurred during the execution. pub(super) chain_events: Vec, - /// - pub(super) loader: v1::trie::Loader<&'b [u8]>, } /// A positive or negative delta in for an [`Amount`]. From 540ab9c63450966949a5a0ab9576fab7ccb2d757 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 21 Feb 2023 16:02:10 +0100 Subject: [PATCH 068/208] Correct cost accounting for contract initialization --- contract-testing/src/impls.rs | 226 ++++++++++++++++------- contract-testing/src/invocation/impls.rs | 41 ++-- contract-testing/src/invocation/types.rs | 4 +- contract-testing/src/types.rs | 12 +- contract-testing/tests/basics.rs | 33 ++++ 5 files changed, 222 insertions(+), 94 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 61df5490..b1deeef9 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,13 +1,13 @@ use concordium_base::{ - base::Energy, - common, + base::{self, Energy}, + common::{self, to_bytes}, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, SlotTime, Timestamp, }, smart_contracts::{ModuleSource, WasmModule, WasmVersion}, - transactions::{self, cost}, + transactions::{self, cost, InitContractPayload}, }; use num_bigint::BigUint; use sha2::{Digest, Sha256}; @@ -15,6 +15,7 @@ use std::{collections::BTreeMap, path::Path, sync::Arc}; use wasm_chain_integration::{v0, v1, InterpreterEnergy}; use crate::{ + constants, invocation::{EntrypointInvocationHandler, InvokeEntrypointResult}, types::*, }; @@ -122,10 +123,13 @@ impl Chain { if self.modules.contains_key(&module_reference) { return Err(DeployModuleError::DuplicateModule(module_reference)); } - self.modules.insert(module_reference, Arc::new(artifact)); + self.modules.insert(module_reference, ContractModule { + size: wasm_module.source.size(), + artifact: Arc::new(artifact), + }); Ok(SuccessfulModuleDeployment { module_reference, - energy, + energy_used: energy, transaction_fee, }) } @@ -195,15 +199,49 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - // Lookup artifact - let artifact = self.contract_module(module_reference)?; - let mut transaction_fee = self.calculate_energy_cost(lookup_module_cost(&artifact)); // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.get_account(sender)?; if account_info.balance.available() < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } + + let mut remaining_energy = energy_reserved; + + // Compute the base cost for checking the transaction header. + let check_header_cost = { + let payload = InitContractPayload { + amount, + mod_ref: module_reference.into(), + init_name: contract_name.to_owned(), + param: parameter.clone().into(), + }; + let pre_account_trx = transactions::construct::init_contract( + account_info.signature_count, + sender, + base::Nonce::from(0), // Value not matter, only used for serialized size. + common::types::TransactionTime::from_seconds(0), /* Value does not matter, only + * used for serialized size. */ + payload, + energy_reserved, + ); + let transaction_size = to_bytes(&pre_account_trx).len() as u64; + transactions::cost::base_cost(transaction_size, account_info.signature_count) + }; + + // Charge the header cost. + remaining_energy = remaining_energy.saturating_sub(check_header_cost); + + // Charge the base cost for initializing a contract. + remaining_energy = + remaining_energy.saturating_sub(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST); + + // Lookup module. + let module = self.contract_module(module_reference)?; + let lookup_cost = lookup_module_cost(&module); + // Charge the cost for looking up the module. + remaining_energy = remaining_energy.saturating_sub(lookup_cost); + // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { @@ -215,19 +253,19 @@ impl Chain { // Initialize contract let mut loader = v1::trie::Loader::new(&[][..]); let res = v1::invoke_init( - artifact, + module.artifact, init_ctx, v1::InitInvocation { amount, init_name: contract_name.get_chain_name(), parameter: ¶meter.0, - energy: to_interpreter_energy(energy_reserved), + energy: to_interpreter_energy(remaining_energy), }, false, loader, ); - // Handle the result and update the transaction fee. - let res = match res { + // Handle the result and compute the transaction fee. + let (res, transaction_fee) = match res { Ok(v1::InitResult::Success { logs, return_value: _, /* Ignore return value for now, since our tools do not support @@ -236,71 +274,123 @@ impl Chain { mut state, }) => { let contract_address = self.create_contract_address(); - let energy_used = energy_reserved - - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - transaction_fee += self.calculate_energy_cost(energy_used); - let mut collector = v1::trie::SizeCollector::default(); - let contract_instance = Contract { - module_reference, - contract_name: contract_name.to_owned(), - state: state.freeze(&mut loader, &mut collector), // TODO: Charge for storage. - owner: sender, - self_balance: amount, - }; - - // Save the contract instance - self.contracts.insert(contract_address, contract_instance); - // Subtract the from the invoker. - self.get_account_mut(sender)?.balance.total -= amount; - - Ok(SuccessfulContractInit { - contract_address, - logs, - energy_used, - transaction_fee, - }) + let persisted_state = state.freeze(&mut loader, &mut collector); + + let mut remaining_energy = InterpreterEnergy::from(remaining_energy); + + // Charge one energy per stored state byte. + let energy_for_state_storage = + to_interpreter_energy(Energy::from(collector.collect())); + remaining_energy = remaining_energy.saturating_sub(energy_for_state_storage); + + // Charge the constant cost for initializing a contract. + remaining_energy = remaining_energy.saturating_sub(to_interpreter_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + )); + + if remaining_energy.energy == 0 { + // Ran out of energy. Charge the `energy_reserved`. + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }, + )), + transaction_fee, + ) + } else { + // Perform the subtraction in the more finegrained (*1000) `InterpreterEnergy`, + // and *then* convert to `Energy`. This is how it is done in the node, and if we + // swap the operations, it can result in a small discrepancy due to rounding. + let energy_reserved = to_interpreter_energy(energy_reserved); + let energy_used = + from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); + let transaction_fee = self.calculate_energy_cost(energy_used); + + let contract_instance = Contract { + module_reference, + contract_name: contract_name.to_owned(), + state: persisted_state, + owner: sender, + self_balance: amount, + }; + + // Save the contract instance + self.contracts.insert(contract_address, contract_instance); + + // Subtract the amount from the invoker. + self.get_account_mut(sender) + .expect("Account known to exist") + .balance + .total -= amount; + + ( + Ok(SuccessfulContractInit { + contract_address, + logs, + energy_used, + transaction_fee, + }), + transaction_fee, + ) + } } Ok(v1::InitResult::Reject { reason, return_value, remaining_energy, }) => { - let energy_used = energy_reserved - - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Reject { - reason, - return_value, - energy_used, - transaction_fee, - }, - )) + let energy_reserved = to_interpreter_energy(energy_reserved); + let energy_used = from_interpreter_energy( + energy_reserved.saturating_sub(InterpreterEnergy::from(remaining_energy)), + ); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Reject { + reason, + return_value, + energy_used, + transaction_fee, + }, + )), + transaction_fee, + ) } Ok(v1::InitResult::Trap { error: _, // TODO: Should we forward this to the user? remaining_energy, }) => { - let energy_used = energy_reserved - - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - transaction_fee += self.calculate_energy_cost(energy_used); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Trap { - energy_used, - transaction_fee, - }, - )) + let energy_reserved = to_interpreter_energy(energy_reserved); + let energy_used = from_interpreter_energy( + energy_reserved.saturating_sub(InterpreterEnergy::from(remaining_energy)), + ); + let transaction_fee = self.calculate_energy_cost(energy_used); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::Trap { + energy_used, + transaction_fee, + }, + )), + transaction_fee, + ) } Ok(v1::InitResult::OutOfEnergy) => { - transaction_fee += self.calculate_energy_cost(energy_reserved); - Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )) + let transaction_fee = self.calculate_energy_cost(energy_reserved); + ( + Err(ContractInitError::ValidChainError( + FailedContractInteraction::OutOfEnergy { + energy_used: energy_reserved, + transaction_fee, + }, + )), + transaction_fee, + ) } Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), }; @@ -509,16 +599,17 @@ impl Chain { self.contracts.get(&address).map(|ci| ci.self_balance) } - /// Returns an [`Arc`] clone of the [`ContractModule`]. + /// Return a clone of the [`ContractModule`] (which has an `Arc` around the + /// artifact). fn contract_module( &self, module_ref: ModuleReference, - ) -> Result, ModuleMissing> { + ) -> Result { let module = self .modules .get(&module_ref) .ok_or(ModuleMissing(module_ref))?; - Ok(Arc::clone(module)) + Ok(module.clone()) } /// Returns an immutable reference to an [`Account`]. @@ -751,9 +842,8 @@ pub(crate) fn from_interpreter_energy(interpreter_energy: InterpreterEnergy) -> /// Calculate the energy energy for looking up a [`ContractModule`]. pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { - // TODO: Is it just the `.code`? - // Comes from Concordium/Cost.hs::lookupModule - Energy::from(module.code.len() as u64 / 50) + // The ratio is from Concordium/Cost.hs::lookupModule + Energy::from(module.size / 50) } /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index d37b250b..7ad93414 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -14,10 +14,7 @@ use concordium_base::{ ModuleReference, OwnedEntrypointName, OwnedReceiveName, Parameter, }, }; -use std::{ - collections::{btree_map, BTreeMap}, - sync::Arc, -}; +use std::collections::{btree_map, BTreeMap}; use wasm_chain_integration::{ v0, v1::{self, trie}, @@ -153,11 +150,11 @@ impl EntrypointInvocationHandler { .contracts .get(&contract_address) .expect("Contract known to exist at this point"); - let artifact = self.contract_module(contract_address); + let module = self.contract_module(contract_address); // Subtract the cost of looking up the module remaining_energy = - remaining_energy.subtract(to_interpreter_energy(lookup_module_cost(&artifact)).energy); + remaining_energy.subtract(to_interpreter_energy(lookup_module_cost(&module)).energy); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -165,9 +162,12 @@ impl EntrypointInvocationHandler { let contract_name = instance.contract_name.as_contract_name().contract_name(); let receive_name = format!("{}.{}", contract_name, entrypoint); let fallback_receive_name = format!("{}.", contract_name); - if artifact.has_entrypoint(receive_name.as_str()) { + if module.artifact.has_entrypoint(receive_name.as_str()) { OwnedReceiveName::new_unchecked(receive_name) - } else if artifact.has_entrypoint(fallback_receive_name.as_str()) { + } else if module + .artifact + .has_entrypoint(fallback_receive_name.as_str()) + { OwnedReceiveName::new_unchecked(fallback_receive_name) } else { // Return early. @@ -212,7 +212,7 @@ impl EntrypointInvocationHandler { // Get the initial result from invoking receive let initial_result = v1::invoke_receive( - artifact, + module.artifact, receive_ctx, v1::ReceiveInvocation { amount, @@ -508,7 +508,7 @@ impl EntrypointInvocationHandler { /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. - fn contract_module(&self, address: ContractAddress) -> Arc { + fn contract_module(&self, address: ContractAddress) -> ContractModule { match self .changeset .current() @@ -517,11 +517,11 @@ impl EntrypointInvocationHandler { .and_then(|c| c.module) { // Contract has been upgrade, new module exists. - Some(new_module) => Arc::clone( - self.modules - .get(&new_module) - .expect("Precondition violation: module must exist."), - ), + Some(new_module) => self + .modules + .get(&new_module) + .expect("Precondition violation: module must exist.") + .clone(), // Contract hasn't been upgraded. Use persisted module. None => { let module_ref = self @@ -529,11 +529,10 @@ impl EntrypointInvocationHandler { .get(&address) .expect("Precondition violation: contract must exist.") .module_reference; - Arc::clone( - self.modules - .get(&module_ref) - .expect("Precondition violation: module must exist."), - ) + self.modules + .get(&module_ref) + .expect("Precondition violation: module must exist.") + .clone() } } } @@ -1190,7 +1189,7 @@ impl<'a> InvocationData<'a> { energy_after_invoke -= to_interpreter_energy(lookup_module_cost(module)).energy; - if module.export.contains_key( + if module.artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), ) { // Update module reference in the changeset. diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 9f2c6fd6..c2e6be4e 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -3,7 +3,7 @@ use concordium_base::contracts_common::{ AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, SlotTime, }; -use std::{collections::BTreeMap, sync::Arc}; +use std::collections::BTreeMap; use wasm_chain_integration::{ v0, v1::{trie::MutableState, InvokeResponse}, @@ -24,7 +24,7 @@ pub(crate) struct InvokeEntrypointResult { pub(crate) struct EntrypointInvocationHandler { pub(super) changeset: ChangeSet, pub(super) accounts: BTreeMap, - pub(super) modules: BTreeMap>, + pub(super) modules: BTreeMap, pub(super) contracts: BTreeMap, pub(super) slot_time: SlotTime, pub(super) euro_per_energy: ExchangeRate, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 17dbe3ea..8e2ed936 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -16,7 +16,13 @@ use wasm_chain_integration::{ use wasm_transform::artifact; /// A V1 artifact, with concrete types for the generic parameters. -pub type ContractModule = artifact::Artifact; +#[derive(Debug, Clone)] +pub struct ContractModule { + /// Size of the module in bytes. Used for cost accounting. + pub(crate) size: u64, + /// The runnable module (artifact). + pub(crate) artifact: Arc>, +} /// Represents the block chain and supports a number of operations, including /// creating accounts, deploying modules, initializing contract, updating @@ -32,7 +38,7 @@ pub struct Chain { /// Accounts and info about them. pub accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap>, + pub modules: BTreeMap, /// Smart contract instances. pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. @@ -134,7 +140,7 @@ pub struct SuccessfulModuleDeployment { /// The reference of the module deployed. pub module_reference: ModuleReference, /// The energy used for deployment. - pub energy: Energy, + pub energy_used: Energy, /// Cost of transaction. pub transaction_fee: Amount, } diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 99b461d3..1dbb2f01 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -533,6 +533,35 @@ mod integrate_contract { } } +#[test] +fn init_with_less_energy_than_module_lookup() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") + .expect("Deploying valid module should work"); + + let reserved_energy = Energy::from(10); + + let res_init = chain.contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_fib"), + OwnedParameter::empty(), + Amount::zero(), + reserved_energy, + ); + match res_init { + Err(ContractInitError::ValidChainError(FailedContractInteraction::OutOfEnergy { + energy_used, + .. + })) if energy_used == reserved_energy => (), + _ => panic!("Expected to fail with out of energy."), + } +} + #[test] fn update_with_fib_reentry_works() { let mut chain = Chain::new(); @@ -588,6 +617,10 @@ fn update_with_fib_reentry_works() { - res_update.transaction_fee ) ); + // TODO: These values come from executing the same transactions on the node. + // Perhaps we should remove them once the cost accounting is in place. + assert_eq!(res_deploy.energy_used, Energy::from(17547)); + assert_eq!(res_init.energy_used, Energy::from(1072)); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); let expected_res = u64::to_le_bytes(13); From 8d7b59809070762d03bf24784bd999de4895236c Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 21 Feb 2023 16:04:21 +0100 Subject: [PATCH 069/208] Get module reference in a simpler way --- contract-testing/src/impls.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index b1deeef9..1e3214b6 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -10,7 +10,6 @@ use concordium_base::{ transactions::{self, cost, InitContractPayload}, }; use num_bigint::BigUint; -use sha2::{Digest, Sha256}; use std::{collections::BTreeMap, path::Path, sync::Arc}; use wasm_chain_integration::{v0, v1, InterpreterEnergy}; @@ -111,14 +110,8 @@ impl Chain { }; account.balance.total -= transaction_fee; - // Save the module TODO: Use wasm_module.get_module_ref() and find a proper way - // to convert ModuleRef to ModuleReference. - let module_reference = { - let mut hasher = Sha256::new(); - hasher.update(wasm_module.source.as_ref()); - let hash: [u8; 32] = hasher.finalize().into(); - ModuleReference::from(hash) - }; + // Save the module + let module_reference: ModuleReference = wasm_module.get_module_ref().into(); // Ensure module hasn't been deployed before. if self.modules.contains_key(&module_reference) { return Err(DeployModuleError::DuplicateModule(module_reference)); From 0e733bf039664777c57b1c539a9f46c41259483b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 21 Feb 2023 16:24:14 +0100 Subject: [PATCH 070/208] Revise the init contract error type --- contract-testing/src/impls.rs | 63 +++++++++---------------- contract-testing/src/types.rs | 81 +++++++++++++++----------------- contract-testing/tests/basics.rs | 11 +++-- 3 files changed, 66 insertions(+), 89 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 1e3214b6..b91a4f46 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -235,6 +235,9 @@ impl Chain { // Charge the cost for looking up the module. remaining_energy = remaining_energy.saturating_sub(lookup_cost); + // TODO: Should we return another kind of error than `ExecutionError` if we run + // out of energy before invoking? + // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { @@ -287,12 +290,11 @@ impl Chain { // Ran out of energy. Charge the `energy_reserved`. let transaction_fee = self.calculate_energy_cost(energy_reserved); ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }, - )), + Err(ContractInitError::ExecutionError { + failure_kind: InitFailure::OutOfEnergy, + energy_used: energy_reserved, + transaction_fee: self.calculate_energy_cost(energy_reserved), + }), transaction_fee, ) } else { @@ -343,14 +345,14 @@ impl Chain { ); let transaction_fee = self.calculate_energy_cost(energy_used); ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Reject { + Err(ContractInitError::ExecutionError { + failure_kind: InitFailure::Reject { reason, return_value, - energy_used, - transaction_fee, }, - )), + energy_used, + transaction_fee, + }), transaction_fee, ) } @@ -364,24 +366,22 @@ impl Chain { ); let transaction_fee = self.calculate_energy_cost(energy_used); ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::Trap { - energy_used, - transaction_fee, - }, - )), + Err(ContractInitError::ExecutionError { + failure_kind: InitFailure::Trap, + energy_used, + transaction_fee, + }), transaction_fee, ) } Ok(v1::InitResult::OutOfEnergy) => { let transaction_fee = self.calculate_energy_cost(energy_reserved); ( - Err(ContractInitError::ValidChainError( - FailedContractInteraction::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee, - }, - )), + Err(ContractInitError::ExecutionError { + failure_kind: InitFailure::OutOfEnergy, + energy_used: energy_reserved, + transaction_fee, + }), transaction_fee, ) } @@ -760,23 +760,6 @@ impl Account { } } -impl FailedContractInteraction { - /// Get the transaction fee. - pub fn transaction_fee(&self) -> Amount { - match self { - FailedContractInteraction::Reject { - transaction_fee, .. - } => *transaction_fee, - FailedContractInteraction::Trap { - transaction_fee, .. - } => *transaction_fee, - FailedContractInteraction::OutOfEnergy { - transaction_fee, .. - } => *transaction_fee, - } - } -} - impl SuccessfulContractUpdate { /// Get a list of all transfers that were made from contracts to accounts. pub fn transfers(&self) -> Vec { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 8e2ed936..7743412e 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -156,12 +156,13 @@ pub enum DeployModuleError { /// The module provided is not valid. #[error("module is invalid due to: {0}")] InvalidModule(#[from] anyhow::Error), - /// The account does not have sufficient funds to pay for the deployment. - #[error("account does not have sufficient funds to pay for the energy")] + /// The invoker account does not have sufficient funds to pay for the + /// deployment. + #[error("invoker does not have sufficient funds to pay for the energy")] InsufficientFunds, - /// The account deploying the module does not exist. - #[error("account {} does not exist", 0.0)] - AccountDoesNotExist(#[from] AccountMissing), + /// The sender account deploying the module does not exist. + #[error("sender account {} does not exist", 0.0)] + SenderDoesNotExist(#[from] AccountMissing), /// The module version is not supported. #[error("wasm version {0} is not supported")] UnsupportedModuleVersion(WasmVersion), @@ -188,18 +189,41 @@ pub struct SuccessfulContractInit { pub enum ContractInitError { /// Initialization failed for a reason that also exists on the chain. #[error("failed due to a valid chain error: {:?}", 0)] - ValidChainError(FailedContractInteraction), + ExecutionError { + /// The reason for why the contract initialization failed. + failure_kind: InitFailure, + /// The energy used. + energy_used: Energy, + /// The transaction fee charged. + transaction_fee: Amount, + }, /// Module has not been deployed in the test environment. #[error("module {:?} does not exist", 0.0)] ModuleDoesNotExist(#[from] ModuleMissing), - /// Account has not been created in test environment. - #[error("account {} does not exist", 0.0)] - AccountDoesNotExist(#[from] AccountMissing), - /// The account does not have enough funds to pay for the energy. - #[error("account does not have enough funds to pay for the energy")] + /// The sender account has not been created in test environment. + #[error("sender account {} does not exist", 0.0)] + SenderDoesNotExist(#[from] AccountMissing), + /// The invoker account does not have enough funds to pay for the energy. + #[error("invoker does not have enough funds to pay for the energy")] InsufficientFunds, } +/// The reason for why a contract initialization failed during execution. +#[derive(Debug)] +pub enum InitFailure { + /// The contract rejected. + Reject { + /// The error code for why it rejected. + reason: i32, + /// The return value. + return_value: ReturnValue, + }, + /// The contract trapped. + Trap, + /// The contract ran out of energy. + OutOfEnergy, +} + /// Represents a successful contract update (or invocation). // TODO: Consider adding function to aggregate all logs from the host_events. #[derive(Debug)] @@ -258,42 +282,11 @@ pub enum ContractUpdateError { /// The sender does not exist in the test environment. #[error("sender {0} does not exist")] SenderDoesNotExist(Address), - /// The account does not have enough funds to pay for the energy. - #[error("account does not have enough funds to pay for the energy")] + /// The invoker account does not have enough funds to pay for the energy. + #[error("invoker does not have enough funds to pay for the energy")] InsufficientFunds, } -/// Represents a failed contract interaction, i.e. an initialization, update, or -/// invocation. -#[derive(Debug)] -pub enum FailedContractInteraction { - /// The contract rejected. - Reject { - /// The error code for why it rejected. - reason: i32, - /// The return value. - return_value: ReturnValue, - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, - /// The contract trapped. - Trap { - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, - /// The contract ran out of energy. - OutOfEnergy { - /// The amount of energy used before rejecting. - energy_used: Energy, - /// The transaction fee to be paid by the invoker for the interaction. - transaction_fee: Amount, - }, -} - /// A transfer of [`Amount`]s failed because the sender had insufficient /// balance. #[derive(Debug)] diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 1dbb2f01..ac7a2d98 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -70,13 +70,13 @@ fn initializing_with_invalid_parameter_fails() { ) .expect_err("Initializing with invalid params should fail"); - assert!(matches!(res_init, ContractInitError::ValidChainError(_))); match res_init { // Failed in the right way and account is still charged. - ContractInitError::ValidChainError(FailedContractInteraction::Reject { + ContractInitError::ExecutionError { + failure_kind: InitFailure::Reject { .. }, transaction_fee, .. - }) => assert_eq!( + } => assert_eq!( chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) ), @@ -554,10 +554,11 @@ fn init_with_less_energy_than_module_lookup() { reserved_energy, ); match res_init { - Err(ContractInitError::ValidChainError(FailedContractInteraction::OutOfEnergy { + Err(ContractInitError::ExecutionError { + failure_kind: InitFailure::OutOfEnergy, energy_used, .. - })) if energy_used == reserved_energy => (), + }) if energy_used == reserved_energy => (), _ => panic!("Expected to fail with out of energy."), } } From 4bb8eb043883cf1a86ac7836debd42b5969bd28c Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Feb 2023 16:06:55 +0100 Subject: [PATCH 071/208] Correct the costs for contract updates --- contract-testing/src/constants.rs | 4 + contract-testing/src/impls.rs | 52 +++- contract-testing/src/invocation/impls.rs | 315 +++++++++++++++-------- contract-testing/src/invocation/types.rs | 9 +- contract-testing/tests/basics.rs | 13 +- 5 files changed, 277 insertions(+), 116 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 65e86adc..cc10b04d 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -20,3 +20,7 @@ pub(crate) const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energ /// Cost of creating an empty smart contract instance. pub(crate) const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: Energy = Energy { energy: 200 }; + +/// The base cost of updating a contract instance to cover administrative +/// costs. Even if no code is run. +pub(crate) const UPDATE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index b91a4f46..42f99ca8 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -6,8 +6,8 @@ use concordium_base::{ ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, SlotTime, Timestamp, }, - smart_contracts::{ModuleSource, WasmModule, WasmVersion}, - transactions::{self, cost, InitContractPayload}, + smart_contracts::{ModuleSource, OwnedReceiveName, WasmModule, WasmVersion}, + transactions::{self, cost}, }; use num_bigint::BigUint; use std::{collections::BTreeMap, path::Path, sync::Arc}; @@ -203,7 +203,7 @@ impl Chain { // Compute the base cost for checking the transaction header. let check_header_cost = { - let payload = InitContractPayload { + let payload = transactions::InitContractPayload { amount, mod_ref: module_reference.into(), init_name: contract_name.to_owned(), @@ -425,6 +425,45 @@ impl Chain { return Err(ContractUpdateError::InsufficientFunds); } + let mut remaining_energy = energy_reserved; + + // Compute the base cost for checking the transaction header. + let check_header_cost = { + let receive_name = { + let contract_name = self + .contracts + .get(&contract_address) + .ok_or(ContractUpdateError::InstanceDoesNotExist( + ContractInstanceMissing(contract_address), + ))? + .contract_name + .as_contract_name(); + OwnedReceiveName::construct_unchecked(contract_name, entrypoint) + }; + + let payload = transactions::UpdateContractPayload { + amount, + address: contract_address, + receive_name, + message: parameter.clone().into(), + }; + + let pre_account_trx = transactions::construct::update_contract( + account_info.signature_count, + invoker, + base::Nonce::from(0), // Value not matter, only used for serialized size. + common::types::TransactionTime::from_seconds(0), /* Value does not matter, only + * used for serialized size. */ + payload, + energy_reserved, + ); + let transaction_size = to_bytes(&pre_account_trx).len() as u64; + transactions::cost::base_cost(transaction_size, account_info.signature_count) + }; + + // Charge the header cost. + remaining_energy = remaining_energy.saturating_sub(check_header_cost); + // TODO: Should chain events be part of the changeset? let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( @@ -436,12 +475,17 @@ impl Chain { Parameter(¶meter.0), amount, invoker_amount_reserved_for_nrg, - energy_reserved, + remaining_energy, ); // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. let (energy_for_state_increase, state_changed) = if result.is_success() { + let remaining_energy = from_interpreter_energy(result.remaining_energy); + println!( + ">>> contract_update before state cost: {}", + remaining_energy + ); match changeset.persist( from_interpreter_energy(result.remaining_energy), contract_address, diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 7ad93414..cf20f7f1 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1,7 +1,7 @@ use super::types::*; use crate::{ constants, - impls::{lookup_module_cost, to_interpreter_energy}, + impls::{from_interpreter_energy, lookup_module_cost, to_interpreter_energy}, types::{ Account, Chain, ChainEvent, Contract, ContractModule, InsufficientBalanceError, TransferError, @@ -67,7 +67,7 @@ impl EntrypointInvocationHandler { parameter, amount, invoker_amount_reserved_for_nrg, - to_interpreter_energy(reserved_energy), + reserved_energy, &mut chain_events, ); (result, contract_invocation.changeset, chain_events) @@ -93,11 +93,14 @@ impl EntrypointInvocationHandler { // is reserved, it is not subtracted in the chain.accounts map. // Used to handle account balance queries for the invoker account. invoker_amount_reserved_for_nrg: Amount, - // Uses [`Interpreter`] energy to avoid rounding issues when converting to and fro - // [`Energy`]. - mut remaining_energy: InterpreterEnergy, + mut remaining_energy: Energy, chain_events: &mut Vec, ) -> InvokeEntrypointResult { + println!( + ">>> invoke_entrypoint before base cost: {}", + remaining_energy + ); + // Move the amount from the sender to the contract, if any. // And get the new self_balance. let instance_self_balance = if amount.micro_ccd() > 0 { @@ -121,9 +124,9 @@ impl EntrypointInvocationHandler { // Return early. // TODO: Should we charge any energy for this? return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { kind }, - logs: None, - remaining_energy, + invoke_response: v1::InvokeResponse::Failure { kind }, + logs: None, + remaining_energy: to_interpreter_energy(remaining_energy), }; } } @@ -135,11 +138,11 @@ impl EntrypointInvocationHandler { // TODO: For the top-most update, we should catch this in `contract_update` and // return `ContractUpdateError::EntrypointMissing`. return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { + invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, - logs: None, - remaining_energy, + logs: None, + remaining_energy: to_interpreter_energy(remaining_energy), }; } } @@ -152,9 +155,16 @@ impl EntrypointInvocationHandler { .expect("Contract known to exist at this point"); let module = self.contract_module(contract_address); - // Subtract the cost of looking up the module + // Charge the base cost for updating a contract. remaining_energy = - remaining_energy.subtract(to_interpreter_energy(lookup_module_cost(&module)).energy); + remaining_energy.saturating_sub(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST); + + // Subtract the cost of looking up the module + remaining_energy = remaining_energy.saturating_sub(lookup_module_cost(&module)); + println!( + ">>> invoke_entrypoint after module lookup: {}", + remaining_energy + ); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -172,11 +182,11 @@ impl EntrypointInvocationHandler { } else { // Return early. return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { + invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, - logs: None, - remaining_energy, + logs: None, + remaining_energy: to_interpreter_energy(remaining_energy), }; } }; @@ -211,22 +221,24 @@ impl EntrypointInvocationHandler { let instance_state = v1::InstanceState::new(loader, inner); // Get the initial result from invoking receive - let initial_result = v1::invoke_receive( - module.artifact, - receive_ctx, - v1::ReceiveInvocation { - amount, - receive_name: receive_name.as_receive_name(), - parameter: parameter.0, - energy: remaining_energy, - }, - instance_state, - v1::ReceiveParams { - max_parameter_size: 65535, - limit_logs_and_return_values: false, - support_queries: true, - }, - ); + let initial_result = run_interpreter(remaining_energy, |energy| { + v1::invoke_receive( + module.artifact, + receive_ctx, + v1::ReceiveInvocation { + amount, + receive_name: receive_name.as_receive_name(), + parameter: parameter.0, + energy, + }, + instance_state, + v1::ReceiveParams { + max_parameter_size: 65535, + limit_logs_and_return_values: false, + support_queries: true, + }, + ) + }); // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. @@ -687,6 +699,75 @@ impl EntrypointInvocationHandler { fn rollback(&mut self) { self.changeset.rollback(); } } +fn run_interpreter( + available_energy: Energy, + f: F, +) -> ExecResult> +where + F: FnOnce(InterpreterEnergy) -> ExecResult>, { + let available_interpreter_energy = to_interpreter_energy(available_energy); + let res = f(available_interpreter_energy); + if let Ok(res) = res { + let subtract_then_convert = |remaining_energy| -> u64 { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + println!( + ">>> Inside run_interpreter:\n\tavailable: {}\n\tremaining: {}", + available_interpreter_energy, remaining_energy + ); + let used_energy = from_interpreter_energy( + available_interpreter_energy.saturating_sub(remaining_energy), + ); + to_interpreter_energy(available_energy.saturating_sub(used_energy)).energy + }; + match res { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy: subtract_then_convert(remaining_energy), + }), + + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => Ok(v1::ReceiveResult::Interrupt { + remaining_energy: subtract_then_convert(remaining_energy), + state_changed, + logs, + config, + interrupt, + }), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => Ok(v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy: subtract_then_convert(remaining_energy), + }), + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => Ok(v1::ReceiveResult::Trap { + error, + remaining_energy: subtract_then_convert(remaining_energy), + }), + v1::ReceiveResult::OutOfEnergy => Ok(v1::ReceiveResult::OutOfEnergy), + } + } else { + res + } +} + impl ChangeSet { /// Creates a new changeset with one empty [`Changes`] element on the /// stack.. @@ -1067,13 +1148,18 @@ impl<'a> InvocationData<'a> { success, }); - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(remaining_energy), - &mut self.state, - false, // never changes on transfers - v1::trie::Loader::new(&[][..]), + let resume_res = run_interpreter( + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), + |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // never changes on transfers + v1::trie::Loader::new(&[][..]), + ) + }, ); // Resume @@ -1106,6 +1192,8 @@ impl<'a> InvocationData<'a> { address, parameter ); + println!(">>> Before internal call: {}", remaining_energy); + let res = self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), @@ -1114,7 +1202,7 @@ impl<'a> InvocationData<'a> { Parameter(¶meter), amount, self.invoker_amount_reserved_for_nrg, - InterpreterEnergy::from(remaining_energy), + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), &mut self.chain_events, ); @@ -1156,13 +1244,25 @@ impl<'a> InvocationData<'a> { self.chain_events.push(resume_event); - let resume_res = v1::resume_receive( - config, - res.invoke_response, - res.remaining_energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), + println!( + ">>> After internal call: {} (state changed: {})", + res.remaining_energy, state_changed + ); + + let resume_res = run_interpreter( + from_interpreter_energy(InterpreterEnergy::from( + res.remaining_energy, + )), + |energy| { + v1::resume_receive( + config, + res.invoke_response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + }, ); self.process(resume_res) @@ -1173,12 +1273,12 @@ impl<'a> InvocationData<'a> { // Add the interrupt event. self.chain_events.push(interrupt_event); + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + // Charge a base cost. - let mut energy_after_invoke = remaining_energy - - to_interpreter_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST, - ) - .energy; + remaining_energy = remaining_energy + .saturating_sub(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST); let response = match self.invocation_handler.modules.get(&module_ref) { None => v1::InvokeResponse::Failure { @@ -1186,8 +1286,8 @@ impl<'a> InvocationData<'a> { }, Some(module) => { // Charge for the module lookup. - energy_after_invoke -= - to_interpreter_energy(lookup_module_cost(module)).energy; + remaining_energy = + remaining_energy.saturating_sub(lookup_module_cost(module)); if module.artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), @@ -1198,10 +1298,9 @@ impl<'a> InvocationData<'a> { .save_module_upgrade(self.address, module_ref); // Charge for the initialization cost. - energy_after_invoke -= to_interpreter_energy( + remaining_energy = remaining_energy.saturating_sub( constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - ) - .energy; + ); let upgrade_event = ChainEvent::Upgraded { address: self.address, @@ -1231,14 +1330,16 @@ impl<'a> InvocationData<'a> { success, }); - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ); + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + }); self.process(resume_res) } @@ -1269,21 +1370,23 @@ impl<'a> InvocationData<'a> { }, }; - let energy_after_invoke = remaining_energy - - to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy = remaining_energy.saturating_sub( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, ); + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + self.process(resume_res) } v1::Interrupt::QueryContractBalance { address } => { @@ -1303,21 +1406,23 @@ impl<'a> InvocationData<'a> { }, }; - let energy_after_invoke = remaining_energy - - to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy = remaining_energy.saturating_sub( + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, ); + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + self.process(resume_res) } v1::Interrupt::QueryExchangeRates => { @@ -1335,21 +1440,23 @@ impl<'a> InvocationData<'a> { data: Some(to_bytes(&exchange_rates)), }; - let energy_after_invoke = remaining_energy - - to_interpreter_energy( - constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, - ) - .energy; - - let resume_res = v1::resume_receive( - config, - response, - InterpreterEnergy::from(energy_after_invoke), - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy = remaining_energy.saturating_sub( + constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, ); + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + self.process(resume_res) } } diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index c2e6be4e..a5cd4118 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,7 +1,10 @@ use crate::types::{Account, ChainEvent, Contract, ContractModule}; -use concordium_base::contracts_common::{ - AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, - OwnedEntrypointName, SlotTime, +use concordium_base::{ + base::Energy, + contracts_common::{ + AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, + OwnedEntrypointName, SlotTime, + }, }; use std::collections::BTreeMap; use wasm_chain_integration::{ diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index ac7a2d98..87c38289 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -590,9 +590,9 @@ fn update_with_fib_reentry_works() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&6u64), + OwnedParameter::new(&3u64), Amount::zero(), - Energy::from(4000000), + Energy::from(10000), ) .expect("Updating valid contract should work"); @@ -619,9 +619,12 @@ fn update_with_fib_reentry_works() { ) ); // TODO: These values come from executing the same transactions on the node. - // Perhaps we should remove them once the cost accounting is in place. - assert_eq!(res_deploy.energy_used, Energy::from(17547)); - assert_eq!(res_init.energy_used, Energy::from(1072)); + // Since they differ slightly dependent on which machine that built it, we + // should remove them once cost accounting is in place. + assert_eq!(res_deploy.energy_used, Energy::from(18225)); + assert_eq!(res_init.energy_used, Energy::from(1085)); + assert_eq!(res_update.energy_used, Energy::from(3496)); + assert_eq!(res_view.energy_used, Energy::from(631)); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); let expected_res = u64::to_le_bytes(13); From f6ff71aaeb6d5b41ff29dc0f1d3787d45a031cc8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Feb 2023 16:35:20 +0100 Subject: [PATCH 072/208] Add more scheduler tests --- contract-testing/src/impls.rs | 7 + contract-testing/src/invocation/types.rs | 9 +- .../tests/all_new_host_functions.rs | 23 ++ contract-testing/tests/counter.rs | 85 +++++++ contract-testing/tests/error_codes.rs | 222 ++++++++++++++++++ contract-testing/tests/fallback.rs | 76 ++++++ 6 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 contract-testing/tests/all_new_host_functions.rs create mode 100644 contract-testing/tests/counter.rs create mode 100644 contract-testing/tests/error_codes.rs create mode 100644 contract-testing/tests/fallback.rs diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 42f99ca8..cfe51ef2 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -636,6 +636,13 @@ impl Chain { self.contracts.get(&address).map(|ci| ci.self_balance) } + /// Helper method for looking up part of the state of a smart contract, + /// which is a trie. + pub fn contract_state_lookup(&self, address: ContractAddress, key: &[u8]) -> Option> { + let mut loader = v1::trie::Loader::new(&[][..]); + self.contracts.get(&address)?.state.lookup(&mut loader, key) + } + /// Return a clone of the [`ContractModule`] (which has an `Arc` around the /// artifact). fn contract_module( diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index a5cd4118..c2e6be4e 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,10 +1,7 @@ use crate::types::{Account, ChainEvent, Contract, ContractModule}; -use concordium_base::{ - base::Energy, - contracts_common::{ - AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, - OwnedEntrypointName, SlotTime, - }, +use concordium_base::contracts_common::{ + AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, + OwnedEntrypointName, SlotTime, }; use std::collections::BTreeMap; use wasm_chain_integration::{ diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs new file mode 100644 index 00000000..bf4fee73 --- /dev/null +++ b/contract-testing/tests/all_new_host_functions.rs @@ -0,0 +1,23 @@ +//! This module tests that a module containing all the new V1 host functions is +//! accepted. It serves as a basic integration test. Individual functions either +//! have tests in wasm-chain-integration, or as part of other scheduler tests if +//! they require more complex interactions with the chain. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_all_new_host_functions() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); +} diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs new file mode 100644 index 00000000..3025852b --- /dev/null +++ b/contract-testing/tests/counter.rs @@ -0,0 +1,85 @@ +//! This module tests calling a contract from a contract and inspecting the +//! return message. Concretely it invokes a counter contract that maintains a +//! 64-bit counter in its state. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_counter() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_counter"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("inc"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_counter_state(&mut chain, res_init.contract_address, 1); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("inc"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_counter_state(&mut chain, res_init.contract_address, 2); + + let parameter = ( + res_init.contract_address, + OwnedParameter::empty(), + (EntrypointName::new_unchecked("inc"), Amount::zero()), + ); + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("inc10"), + OwnedParameter::new(¶meter), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_counter_state(&mut chain, res_init.contract_address, 12); +} + +/// Looks up in the root of the state trie and compares the value with the +/// `expected`. +fn assert_counter_state(chain: &mut Chain, contract_address: ContractAddress, expected: u64) { + assert_eq!( + chain + .contract_state_lookup(contract_address, &[0, 0, 0, 0, 0, 0, 0, 0]) + .unwrap(), + u64::to_le_bytes(expected) + ); +} diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs new file mode 100644 index 00000000..344bb9ff --- /dev/null +++ b/contract-testing/tests/error_codes.rs @@ -0,0 +1,222 @@ +//! This module tests invoking a V1 contract which invokes an operation which +//! fails. The test is to make sure error codes are correctly returned to the +//! contract. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_error_codes() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/caller.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_caller"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // Invoke an entrypoint that calls the "fail" entrypoint. + // The expected return code is + // 0x0100_ffff_ffef + // because + // - the return value is pushed (hence 01) + // - the call to "fail" fails with a "logic error" (hence the 00) + // - the return value is -17 (which when converted with two's complement i32 is + // ffff_ffef) + let parameter_0 = ( + 1u32, // instruction + res_init.contract_address, + ( + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::zero(), + ), + ); + let res_update_0 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_0), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_0.return_value, + u64::to_le_bytes(0x0100_ffff_ffef) + ); + + // Invoke an entrypoint that tries to transfer an amount that it does not have + // via contract invoke. The expected return code is + // 0x0001_0000_0000 + // because + // - there is no return value (hence 00) + // - the call fails with "insufficient funds" (hence 01) + // - the remaining is set to 0 since there is no logic error + let parameter_1 = ( + 1u32, // instruction + res_init.contract_address, + ( + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::from_micro_ccd(10_000), + ), + ); + let res_update_1 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_1), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_1.return_value, + u64::to_le_bytes(0x0001_0000_0000) + ); + + // Invoke an entrypoint that traps + // The expected return code is + // 0x0002_0000_0000 + // because + // - there is no return value (hence 00) + // - the call fails with "missing account" (hence 02) + // - the remaining is set to 0 since there is no logic error + let parameter_2 = ( + 0u32, // instruction + AccountAddress([9; 32]), // Account which doesn't exist + Amount::zero(), + ); + let res_update_2 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_2), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_2.return_value, + u64::to_le_bytes(0x0002_0000_0000) + ); + + // Invoke an entrypoint that tries to invoke a non-existing contract. + // The expected return code is + // 0x0003_0000_0000 + // because + // - there is no return value (hence 00) + // - the call fails with "missing contract" (hence 03) + // - the remaining is set to 0 since there is no logic error + let parameter_3 = ( + 1u32, // instruction + ContractAddress::new(1234, 5678), // Address which does not exist. + ( + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::zero(), + ), + ); + let res_update_3 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_3), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_3.return_value, + u64::to_le_bytes(0x0003_0000_0000) + ); + + // Invoke an entrypoint that tries to invoke a non-existing entrypoint. + // The expected return code is + // 0x0004_0000_0000 + // because + // - there is no return value (hence 00) + // - the call fails with "invalid entrypoint" (hence 04) + // - the remaining is set to 0 since there is no logic error + let parameter_4 = ( + 1u32, // instruction + res_init.contract_address, + ( + OwnedParameter::empty(), + EntrypointName::new_unchecked("nonexisting"), + Amount::zero(), + ), + ); + let res_update_4 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_4), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_4.return_value, + u64::to_le_bytes(0x0004_0000_0000) + ); + + // Test 5 is omitted as it uses a v0 contract which is not supported in this + // library. + + // |Invoke an entrypoint that traps + // The expected return code is + // 0x0006_0000_0000 + // because + // - there is no return value (hence 00) + // - the call fails with "trap" (hence 06) + // - the remaining is set to 0 since there is no logic error + let parameter_6 = ( + 1u32, // instruction + res_init.contract_address, + ( + OwnedParameter::empty(), + EntrypointName::new_unchecked("trap"), + Amount::zero(), + ), + ); + let res_update_6 = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("call"), + OwnedParameter::new(¶meter_6), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating valid contract should work"); + assert_eq!( + res_update_6.return_value, + u64::to_le_bytes(0x0006_0000_0000) + ); +} diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs new file mode 100644 index 00000000..334a4426 --- /dev/null +++ b/contract-testing/tests/fallback.rs @@ -0,0 +1,76 @@ +//! Tests for the contract default method/fallback functionality. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_fallback() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/fallback.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init_two = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_two"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let res_init_one = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_one"), + OwnedParameter::new(&res_init_two.contract_address), /* Pass in address of contract + * "two". */ + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + // Invoke the fallback directly. This should fail with execution failure/trap + // because it will redirect to "two." which does not exist. Hence this will fail + // and the fallback will try to look up a non-existing return value. + let res_invoke_1 = chain.contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init_one.contract_address, + EntrypointName::new_unchecked(""), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ); + match res_invoke_1 { + Err(ContractUpdateError::ExecutionError { + failure_kind: InvokeFailure::RuntimeError, + .. + }) => (), + _ => panic!("Test failed, expected a runtime error."), + } + + // Invoke "two.do" via "one.do" and the fallback. + let parameter = OwnedParameter::new(&"ASDF"); + let res_invoke_2 = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init_one.contract_address, + EntrypointName::new_unchecked("do"), + parameter.clone(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Invoke should succeed."); + assert_eq!(res_invoke_2.return_value, parameter.0); // Parameter is returned + // via the fallback. +} From ecab142171645578ca502eb041881b716914b0c2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 24 Feb 2023 13:57:46 +0100 Subject: [PATCH 073/208] Fix minor energy discrepancy bug and charge for simple transfers --- contract-testing/src/constants.rs | 4 + contract-testing/src/invocation/impls.rs | 109 ++++++++++++----------- contract-testing/tests/basics.rs | 11 +-- 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index cc10b04d..8b663150 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -24,3 +24,7 @@ pub(crate) const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: Energy = Energy { ene /// The base cost of updating a contract instance to cover administrative /// costs. Even if no code is run. pub(crate) const UPDATE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; + +/// The cost for a simple transfer (simple because it is not an encrypted or +/// scheduled transfer). +pub(crate) const SIMPLE_TRANSFER_COST: Energy = Energy { energy: 300 }; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index cf20f7f1..e22bc373 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -96,11 +96,6 @@ impl EntrypointInvocationHandler { mut remaining_energy: Energy, chain_events: &mut Vec, ) -> InvokeEntrypointResult { - println!( - ">>> invoke_entrypoint before base cost: {}", - remaining_energy - ); - // Move the amount from the sender to the contract, if any. // And get the new self_balance. let instance_self_balance = if amount.micro_ccd() > 0 { @@ -161,10 +156,6 @@ impl EntrypointInvocationHandler { // Subtract the cost of looking up the module remaining_energy = remaining_energy.saturating_sub(lookup_module_cost(&module)); - println!( - ">>> invoke_entrypoint after module lookup: {}", - remaining_energy - ); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -600,6 +591,10 @@ impl EntrypointInvocationHandler { /// Saves a mutable state for a contract in the changeset. /// + /// If `with_fresh_generation`, then it will use the + /// [`MutableState::make_fresh_generation`] function, otherwise it will + /// make a clone. + /// /// If the contract already has an entry in the changeset, the old state /// will be replaced. Otherwise, the entry is created and the state is /// added. @@ -609,28 +604,37 @@ impl EntrypointInvocationHandler { /// /// *Preconditions:* /// - Contract must exist. - fn save_state_changes(&mut self, address: ContractAddress, state: &mut trie::MutableState) { - let mut loader = v1::trie::Loader::new(&[][..]); - self.changeset - .current_mut() - .contracts - .entry(address) - .and_modify(|changes| { - changes.state = Some(state.make_fresh_generation(&mut loader)); - changes.modification_index += 1; - }) - .or_insert({ + fn save_state_changes( + &mut self, + address: ContractAddress, + state: &mut trie::MutableState, + with_fresh_generation: bool, + ) { + let state = if with_fresh_generation { + let mut loader = v1::trie::Loader::new(&[][..]); + state.make_fresh_generation(&mut loader) + } else { + state.clone() + }; + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { let original_balance = self .contracts .get(&address) .expect("Precondition violation: contract must exist.") .self_balance; - ContractChanges { - state: Some(state.make_fresh_generation(&mut loader)), + vac.insert(ContractChanges { + state: Some(state), modification_index: 1, // Increment from default, 0, to 1. ..ContractChanges::new(original_balance) - } - }); + }); + } + btree_map::Entry::Occupied(mut occ) => { + let changes = occ.get_mut(); + changes.state = Some(state); + changes.modification_index += 1; + } + } } /// Saves a new module reference for the contract in the changeset. @@ -699,6 +703,10 @@ impl EntrypointInvocationHandler { fn rollback(&mut self) { self.changeset.rollback(); } } +/// Run the interpreter with the provided function and energy. +/// +/// This function ensures that the energy calculations is handled as in the +/// node. fn run_interpreter( available_energy: Energy, f: F, @@ -710,10 +718,6 @@ where if let Ok(res) = res { let subtract_then_convert = |remaining_energy| -> u64 { let remaining_energy = InterpreterEnergy::from(remaining_energy); - println!( - ">>> Inside run_interpreter:\n\tavailable: {}\n\tremaining: {}", - available_interpreter_energy, remaining_energy - ); let used_energy = from_interpreter_energy( available_interpreter_energy.saturating_sub(remaining_energy), ); @@ -1079,8 +1083,11 @@ impl<'a> InvocationData<'a> { // Save changes to changeset. if state_changed { - self.invocation_handler - .save_state_changes(self.address, &mut self.state); + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + false, + ); } Ok(v1::ReceiveResult::Success { @@ -1148,19 +1155,21 @@ impl<'a> InvocationData<'a> { success, }); - let resume_res = run_interpreter( - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), - |energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // never changes on transfers - v1::trie::Loader::new(&[][..]), - ) - }, - ); + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy = + remaining_energy.saturating_sub(constants::SIMPLE_TRANSFER_COST); + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // never changes on transfers + v1::trie::Loader::new(&[][..]), + ) + }); // Resume self.process(resume_res) @@ -1175,8 +1184,11 @@ impl<'a> InvocationData<'a> { self.chain_events.push(interrupt_event); if state_changed { - self.invocation_handler - .save_state_changes(self.address, &mut self.state); + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + true, + ); } // Save the modification index before the invoke. @@ -1192,8 +1204,6 @@ impl<'a> InvocationData<'a> { address, parameter ); - println!(">>> Before internal call: {}", remaining_energy); - let res = self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), @@ -1244,11 +1254,6 @@ impl<'a> InvocationData<'a> { self.chain_events.push(resume_event); - println!( - ">>> After internal call: {} (state changed: {})", - res.remaining_energy, state_changed - ); - let resume_res = run_interpreter( from_interpreter_energy(InterpreterEnergy::from( res.remaining_energy, diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 87c38289..73fd27a5 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -590,9 +590,9 @@ fn update_with_fib_reentry_works() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&3u64), + OwnedParameter::new(&6u64), Amount::zero(), - Energy::from(10000), + Energy::from(100000), ) .expect("Updating valid contract should work"); @@ -618,13 +618,6 @@ fn update_with_fib_reentry_works() { - res_update.transaction_fee ) ); - // TODO: These values come from executing the same transactions on the node. - // Since they differ slightly dependent on which machine that built it, we - // should remove them once cost accounting is in place. - assert_eq!(res_deploy.energy_used, Energy::from(18225)); - assert_eq!(res_init.energy_used, Energy::from(1085)); - assert_eq!(res_update.energy_used, Energy::from(3496)); - assert_eq!(res_view.energy_used, Energy::from(631)); assert_eq!(chain.contracts.len(), 1); assert!(res_update.state_changed); let expected_res = u64::to_le_bytes(13); From 261bea8655ebc54138e40ac8a91a1c975760b733 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 24 Feb 2023 15:44:24 +0100 Subject: [PATCH 074/208] Add iterator and recorder integration tests --- contract-testing/tests/iterator.rs | 55 +++++++++++++++++++++++++++ contract-testing/tests/recorder.rs | 60 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 contract-testing/tests/iterator.rs create mode 100644 contract-testing/tests/recorder.rs diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs new file mode 100644 index 00000000..ccc94a12 --- /dev/null +++ b/contract-testing/tests/iterator.rs @@ -0,0 +1,55 @@ +//! This module tests calling a contract which makes use of an iterator. +//! The checks are being performed in the contract itself so if invoking the +//! contract completes successfully then this implies that the tests have done +//! so as well. Note. as per above no checks are being performed in this file +//! wrt. the state etc. after execution etc. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_iterator() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/iterator.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_iterator"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("iteratetest"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Should succeed"); + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("lockingtest"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Should succeed."); +} diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs new file mode 100644 index 00000000..c0a11f12 --- /dev/null +++ b/contract-testing/tests/recorder.rs @@ -0,0 +1,60 @@ +//! This module tests basic V1 state operations with the recorder contract. + +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_recorder() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/record-parameters.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_recorder"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("record_u64"), + OwnedParameter::new(&20u64), + Amount::zero(), + Energy::from(100000), + ) + .expect("Update failed"); + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("record_u64"), + OwnedParameter::new(&40u64), + Amount::zero(), + Energy::from(100000), + ) + .expect("Update failed"); + // Assert that all 60 values were inserted in the state. + for key in 0..60u64 { + assert!(chain + .contract_state_lookup(res_init.contract_address, &u64::to_le_bytes(key)) + .is_some()); + } +} From 39052f35ebe79b9169f42cd599d05287b9ed8a10 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 27 Feb 2023 10:56:39 +0100 Subject: [PATCH 075/208] Simplify balance queries for invoker account --- contract-testing/src/impls.rs | 48 ++++++++++++++---------- contract-testing/src/invocation/impls.rs | 36 +++--------------- contract-testing/src/invocation/types.rs | 20 ++++------ 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index cfe51ef2..7c82fba5 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -416,14 +416,16 @@ impl Chain { return Err(ContractUpdateError::SenderDoesNotExist(sender)); } - // Ensure account exists and can pay for the reserved energy and amount - // TODO: Could we just remove this amount in the changeset and then put back the - // to_ccd(remaining_energy) afterwards? - let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + // Ensure account exists and can pay for the reserved energy and amount + let account_info = self.get_account_mut(invoker)?; + let invoker_signature_count = account_info.signature_count; if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } + // Charge account for the reserved energy up front. This is to ensure that + // contract queries for the invoker balance are correct. + account_info.balance.total -= invoker_amount_reserved_for_nrg; let mut remaining_energy = energy_reserved; @@ -449,16 +451,16 @@ impl Chain { }; let pre_account_trx = transactions::construct::update_contract( - account_info.signature_count, + invoker_signature_count, invoker, - base::Nonce::from(0), // Value not matter, only used for serialized size. + base::Nonce::from(0), // Value does not matter, only used for serialized size. common::types::TransactionTime::from_seconds(0), /* Value does not matter, only * used for serialized size. */ payload, energy_reserved, ); let transaction_size = to_bytes(&pre_account_trx).len() as u64; - transactions::cost::base_cost(transaction_size, account_info.signature_count) + transactions::cost::base_cost(transaction_size, invoker_signature_count) }; // Charge the header cost. @@ -474,18 +476,12 @@ impl Chain { entrypoint.to_owned(), Parameter(¶meter.0), amount, - invoker_amount_reserved_for_nrg, remaining_energy, ); // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. let (energy_for_state_increase, state_changed) = if result.is_success() { - let remaining_energy = from_interpreter_energy(result.remaining_energy); - println!( - ">>> contract_update before state cost: {}", - remaining_energy - ); match changeset.persist( from_interpreter_energy(result.remaining_energy), contract_address, @@ -513,10 +509,13 @@ impl Chain { state_changed, ); - // Charge the transaction fee irrespective of the result. - // TODO: If we charge up front, then we should return to_ccd(remaining_energy) - // here instead. - self.get_account_mut(invoker)?.balance.total -= transaction_fee; + // The `invoker` was charged for all the `reserved_energy` up front. + // Here, we return the amount for any remaining energy. + let return_amount = invoker_amount_reserved_for_nrg - transaction_fee; + self.get_account_mut(invoker) + .expect("Known to exist") + .balance + .total += return_amount; res } @@ -544,12 +543,15 @@ impl Chain { return Err(ContractUpdateError::SenderDoesNotExist(sender)); } - // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.get_account(invoker)?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + // Ensure account exists and can pay for the reserved energy and amount + let account_info = self.get_account_mut(invoker)?; if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } + // Charge account for the reserved energy up front. This is to ensure that + // contract queries for the invoker balance are correct. + account_info.balance.total -= invoker_amount_reserved_for_nrg; let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( @@ -560,7 +562,6 @@ impl Chain { entrypoint.to_owned(), Parameter(¶meter.0), amount, - invoker_amount_reserved_for_nrg, energy_reserved, ); @@ -590,6 +591,13 @@ impl Chain { state_changed, ); + // Return the amount charged for the reserved energy, as this is not a + // transaction. + self.get_account_mut(invoker) + .expect("Known to exist") + .balance + .total += invoker_amount_reserved_for_nrg; + result } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index e22bc373..d3cf6b71 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -40,10 +40,6 @@ impl EntrypointInvocationHandler { entrypoint: OwnedEntrypointName, parameter: Parameter, amount: Amount, - // The CCD amount reserved from the invoker account. While the amount - // is reserved, it is not subtracted in the chain.accounts map. - // Used to handle account balance queries for the invoker account. - invoker_amount_reserved_for_nrg: Amount, reserved_energy: Energy, ) -> (InvokeEntrypointResult, ChangeSet, Vec) { let mut contract_invocation = Self { @@ -66,7 +62,6 @@ impl EntrypointInvocationHandler { entrypoint, parameter, amount, - invoker_amount_reserved_for_nrg, reserved_energy, &mut chain_events, ); @@ -89,10 +84,6 @@ impl EntrypointInvocationHandler { entrypoint: OwnedEntrypointName, parameter: Parameter, amount: Amount, - // The CCD amount reserved from the invoker account. While the amount - // is reserved, it is not subtracted in the chain.accounts map. - // Used to handle account balance queries for the invoker account. - invoker_amount_reserved_for_nrg: Amount, mut remaining_energy: Energy, chain_events: &mut Vec, ) -> InvokeEntrypointResult { @@ -238,7 +229,6 @@ impl EntrypointInvocationHandler { address: contract_address, contract_name, amount, - invoker_amount_reserved_for_nrg, entrypoint, invocation_handler: self, state: mutable_state, @@ -1211,7 +1201,6 @@ impl<'a> InvocationData<'a> { name, Parameter(¶meter), amount, - self.invoker_amount_reserved_for_nrg, from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), &mut self.chain_events, ); @@ -1350,26 +1339,13 @@ impl<'a> InvocationData<'a> { } v1::Interrupt::QueryAccountBalance { address } => { println!("\t\tQuerying account balance of {}", address); - // When querying an account, the amounts from any `invoke_transfer`s - // should be included. That is handled by - // the `chain` struct already. let response = match self.invocation_handler.account_balance(address) { - Some(mut balance) => { - // If you query the invoker account, it should also - // take into account the send-amount and the amount reserved for - // the reserved max energy. The former is handled in - // `invoke_entrypoint`, but the latter is represented in - // `self.invoker_amount_reserved_for_nrg`. - if address == self.invoker { - balance.total -= self.invoker_amount_reserved_for_nrg; - } - v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&balance)), - } - } + Some(balance) => v1::InvokeResponse::Success { + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: Some(to_bytes(&balance)), + }, None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentAccount, }, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index c2e6be4e..caf99edd 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -82,28 +82,22 @@ pub(super) struct ContractChanges { /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. pub(super) struct InvocationData<'a> { /// The invoker. - pub(super) invoker: AccountAddress, + pub(super) invoker: AccountAddress, /// The contract being called. - pub(super) address: ContractAddress, + pub(super) address: ContractAddress, /// The name of the contract. - pub(super) contract_name: OwnedContractName, + pub(super) contract_name: OwnedContractName, /// The amount sent from the sender to the contract. - pub(super) amount: Amount, - /// The CCD amount reserved from the invoker account for the energy. While - /// the amount is reserved, it is not subtracted in the chain.accounts - /// map. Used to handle account balance queries for the invoker account. - /// TODO: We could use a changeset for accounts -> balance, and then look up - /// the "chain.accounts" values for chain queries. - pub(super) invoker_amount_reserved_for_nrg: Amount, + pub(super) amount: Amount, /// The entrypoint to execute. - pub(super) entrypoint: OwnedEntrypointName, + pub(super) entrypoint: OwnedEntrypointName, /// A reference to the [`EntrypointInvocationHandler`], which is used to for /// handling interrupts and for querying chain data. pub(super) invocation_handler: &'a mut EntrypointInvocationHandler, /// The current state. - pub(super) state: MutableState, + pub(super) state: MutableState, /// Chain events that have occurred during the execution. - pub(super) chain_events: Vec, + pub(super) chain_events: Vec, } /// A positive or negative delta in for an [`Amount`]. From afe4b3ef7dbe72a4ee72f50a863deca9f12ff728 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 27 Feb 2023 11:14:27 +0100 Subject: [PATCH 076/208] Simplify parameters with new serialize instances --- contract-testing/tests/checkpointing.rs | 33 ++++++++++---------- contract-testing/tests/counter.rs | 3 +- contract-testing/tests/error_codes.rs | 40 ++++++++++--------------- contract-testing/tests/queries.rs | 18 +++++------ 4 files changed, 40 insertions(+), 54 deletions(-) diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index de83da3d..0254871a 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -55,15 +55,14 @@ fn test_case_1() { let forward_parameter = ( res_init_a.contract_address, 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + EntrypointName::new_unchecked("a_modify"), + Amount::zero(), ); let forward_parameter_len = to_bytes(&forward_parameter).len(); let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, EntrypointName::new_unchecked("b_forward_crash"), Amount::zero(), ); @@ -131,15 +130,14 @@ fn test_case_2() { let forward_parameter = ( res_init_a.contract_address, 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_no_modify"), Amount::zero()), + EntrypointName::new_unchecked("a_no_modify"), + Amount::zero(), ); let forward_parameter_len = to_bytes(&forward_parameter).len(); let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, EntrypointName::new_unchecked("b_forward"), Amount::zero(), ); @@ -265,15 +263,14 @@ fn test_case_4() { let forward_parameter = ( res_init_a.contract_address, 0u16, // length of empty parameter - (EntrypointName::new_unchecked("a_modify"), Amount::zero()), + EntrypointName::new_unchecked("a_modify"), + Amount::zero(), ); let forward_parameter_len = to_bytes(&forward_parameter).len(); let parameter = ( - ( - res_init_b.contract_address, - forward_parameter_len as u16, - forward_parameter, - ), + res_init_b.contract_address, + forward_parameter_len as u16, + forward_parameter, EntrypointName::new_unchecked("b_forward"), Amount::zero(), ); diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 3025852b..7f9f18d9 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -57,7 +57,8 @@ fn test_counter() { let parameter = ( res_init.contract_address, OwnedParameter::empty(), - (EntrypointName::new_unchecked("inc"), Amount::zero()), + EntrypointName::new_unchecked("inc"), + Amount::zero(), ); chain .contract_update( diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 344bb9ff..e8871da4 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -39,11 +39,9 @@ fn test_error_codes() { let parameter_0 = ( 1u32, // instruction res_init.contract_address, - ( - OwnedParameter::empty(), - EntrypointName::new_unchecked("fail"), - Amount::zero(), - ), + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::zero(), ); let res_update_0 = chain .contract_update( @@ -71,11 +69,9 @@ fn test_error_codes() { let parameter_1 = ( 1u32, // instruction res_init.contract_address, - ( - OwnedParameter::empty(), - EntrypointName::new_unchecked("fail"), - Amount::from_micro_ccd(10_000), - ), + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::from_micro_ccd(10_000), ); let res_update_1 = chain .contract_update( @@ -131,11 +127,9 @@ fn test_error_codes() { let parameter_3 = ( 1u32, // instruction ContractAddress::new(1234, 5678), // Address which does not exist. - ( - OwnedParameter::empty(), - EntrypointName::new_unchecked("fail"), - Amount::zero(), - ), + OwnedParameter::empty(), + EntrypointName::new_unchecked("fail"), + Amount::zero(), ); let res_update_3 = chain .contract_update( @@ -163,11 +157,9 @@ fn test_error_codes() { let parameter_4 = ( 1u32, // instruction res_init.contract_address, - ( - OwnedParameter::empty(), - EntrypointName::new_unchecked("nonexisting"), - Amount::zero(), - ), + OwnedParameter::empty(), + EntrypointName::new_unchecked("nonexisting"), + Amount::zero(), ); let res_update_4 = chain .contract_update( @@ -198,11 +190,9 @@ fn test_error_codes() { let parameter_6 = ( 1u32, // instruction res_init.contract_address, - ( - OwnedParameter::empty(), - EntrypointName::new_unchecked("trap"), - Amount::zero(), - ), + OwnedParameter::empty(), + EntrypointName::new_unchecked("trap"), + Amount::zero(), ); let res_update_6 = chain .contract_update( diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 6cfce82d..531c0469 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -41,11 +41,9 @@ mod query_account_balance { ) .expect("Initializing valid contract should work"); - // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to - // get around it here. // The contract will query the balance of ACC_1 and assert that the three // balances match this input. - let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain .contract_update( @@ -105,7 +103,7 @@ mod query_account_balance { // The contract will query the balance of ACC_1, which is also the invoker, and // assert that the three balances match this input. let expected_balance = initial_balance - invoker_reserved_amount; - let input_param = (ACC_1, (expected_balance, Amount::zero(), Amount::zero())); + let input_param = (ACC_1, expected_balance, Amount::zero(), Amount::zero()); let res_update = chain .contract_update( @@ -168,7 +166,9 @@ mod query_account_balance { let input_param = ( ACC_1, amount_to_send, - (expected_balance, Amount::zero(), Amount::zero()), + expected_balance, + Amount::zero(), + Amount::zero(), ); let res_update = chain @@ -234,7 +234,7 @@ mod query_account_balance { // get around it here. // The contract will query the balance of ACC_1 and assert that the three // balances match this input. - let input_param = (ACC_1, (initial_balance, Amount::zero(), Amount::zero())); + let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain .contract_update( @@ -467,10 +467,8 @@ mod query_contract_balance { let input_param = ( ACC_0, transfer_amount, - ( - res_init.contract_address, - init_amount + update_amount - transfer_amount, - ), + res_init.contract_address, + init_amount + update_amount - transfer_amount, ); let res_update = chain From 8eff874d28213ccee0d707606f510e431d4d9967 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 27 Feb 2023 16:34:34 +0100 Subject: [PATCH 077/208] Add transfer test from scheduler --- contract-testing/src/types.rs | 2 +- contract-testing/tests/transfer.rs | 100 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 contract-testing/tests/transfer.rs diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 7743412e..11171615 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -76,7 +76,7 @@ pub struct Account { } /// An event that occurred during a contract update or invocation. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ChainEvent { /// A contract was interrupted. Interrupted { diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs new file mode 100644 index 00000000..db1e1712 --- /dev/null +++ b/contract-testing/tests/transfer.rs @@ -0,0 +1,100 @@ +//! This module contains tests for transfers fr&om a contract to an account. +//! See more details about the specific test inside the `transfer.wat` file. +use concordium_smart_contract_testing::*; +use wasm_chain_integration::v0::Logs; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +#[test] +fn test_transfer() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1(ACC_0, format!("{}/transfer.wasm", WASM_TEST_FOLDER)) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_transfer"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + + let contract_address = res_init.contract_address; + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("forward"), + OwnedParameter::new(&ACC_0), + Amount::from_micro_ccd(123), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + // Contract should have forwarded the amount and thus have balance == 0. + assert_eq!( + Amount::zero(), + chain.contracts.get(&contract_address).unwrap().self_balance + ); + + // Deposit 1000 micro CCD. + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("deposit"), + OwnedParameter::empty(), + Amount::from_micro_ccd(1000), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + + let res_update = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("send"), + OwnedParameter::new(&(ACC_0, Amount::from_micro_ccd(17))), /* Tell it to send 17 + * mCCD to ACC_0. */ + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); + // Contract should have 1000 - 17 microCCD in balance. + assert_eq!( + Amount::from_micro_ccd(1000 - 17), + chain.contracts.get(&contract_address).unwrap().self_balance + ); + assert_eq!(res_update.chain_events[..], [ + ChainEvent::Interrupted { + address: contract_address, + logs: Logs::new(), + }, + ChainEvent::Transferred { + from: contract_address, + amount: Amount::from_micro_ccd(17), + to: ACC_0, + }, + ChainEvent::Resumed { + address: contract_address, + success: true, + }, + ChainEvent::Updated { + address: contract_address, + contract: OwnedContractName::new_unchecked("init_transfer".into()), + entrypoint: OwnedEntrypointName::new_unchecked("send".into()), + amount: Amount::zero(), + } + ]) +} From 340b7cb28ac8c9ddd26a00d04c94d986374d112e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 27 Feb 2023 17:50:59 +0100 Subject: [PATCH 078/208] Add relaxed restrictions test from scheduler --- contract-testing/src/impls.rs | 2 +- .../tests/relaxed_restrictions.rs | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 contract-testing/tests/relaxed_restrictions.rs diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7c82fba5..7f53cfd4 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -402,7 +402,7 @@ impl Chain { sender: Address, contract_address: ContractAddress, entrypoint: EntrypointName, - parameter: OwnedParameter, + parameter: OwnedParameter, // TODO: Should we check size <= 65535? amount: Amount, energy_reserved: Energy, ) -> Result { diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs new file mode 100644 index 00000000..75bc4559 --- /dev/null +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -0,0 +1,123 @@ +//! This module tests the relaxed smart contract restrictions introduced in P5 +//! for V1 contracts. +//! +//! This will only check that the P5 limits are in effect, as the testing +//! library only supports the most current protocol version (for now, at least). +//! +//! The limit changes in P5 are: +//! - Parameter size limit: 1kb -> 65kb +//! - Return value size limit: 16kb -> no limit (apart from energy) +//! - Number of logs: 64 -> no limit (apart from energy) +//! - Cost of parameters: +//! - Of size <= 1kb: base cost + 1NRG / 1 *kilobyte* (same as before P5) +//! - Of size > 1 kb: base cost + 1NRG / 1 *byte* +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +/// Test the new parameter size limit. +#[test] +fn test_new_parameter_limit() { + let (mut chain, contract_address) = deploy_and_init(); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("param"), + mk_parameter(65535, 65535), + Amount::zero(), + Energy::from(700000), + ) + .expect("Updating contract should succeed"); +} + +/// Test the new return value limit. +#[test] +fn test_new_return_value_limit() { + let (mut chain, contract_address) = deploy_and_init(); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("return-value"), + OwnedParameter::new(&100_000u32), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} + +/// Test the new number of logs limit. +#[test] +fn test_new_log_limit() { + let (mut chain, contract_address) = deploy_and_init(); + + chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + contract_address, + EntrypointName::new_unchecked("logs"), + OwnedParameter::new(&64u32), + Amount::zero(), + Energy::from(10000), + ) + .expect("Updating contract should succeed"); +} + +/// Helper for deploying and initializing the `relaxed-restrictions.wasm` +/// contract. +fn deploy_and_init() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_wasm_v1( + ACC_0, + format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + ACC_0, + res_deploy.module_reference, + ContractName::new_unchecked("init_relax"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect("Initializing valid contract should work"); + (chain, res_init.contract_address) +} + +/// Helper for creating a parameter of a specific size. +/// +/// The `internal_param_size` is the size of the parameter passed to the +/// `param-aux` entrypoint. This is used to check the parameter size limit +/// inside the wasm interpreter. +/// +/// The `desired_size` is the desired total length of the parameter produced by +/// this function. It is used to check the parameter size limit checked in the +/// testing library. +/// +/// The parameter returned will contain +/// - `internal_param_size` (2 bytes) +/// - the entrypoint `"param-aux"` (2 + 9 bytes) +/// - filler `1u8` bytes (remaining until `desired_size` is reached) +fn mk_parameter(internal_param_size: u16, desired_size: u32) -> OwnedParameter { + let entrypoint = OwnedEntrypointName::new_unchecked("param-aux".into()); + let filler_size = desired_size + - 2 // internal_param_size + - 2 // entrypoint name len + - 9 // entrypoint name + - 4; // length of filler vector + let filler = vec![1u8; filler_size as usize]; + OwnedParameter::new(&(internal_param_size, entrypoint, filler)) +} From 9e7167d65ac5e3100b5bab599b70b7f6e7c0be5e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 28 Feb 2023 10:11:03 +0100 Subject: [PATCH 079/208] Check parameter lengths --- contract-testing/src/constants.rs | 3 ++ contract-testing/src/impls.rs | 34 ++++++++++--------- contract-testing/src/invocation/impls.rs | 27 --------------- contract-testing/src/types.rs | 6 ++++ .../tests/relaxed_restrictions.rs | 1 + 5 files changed, 28 insertions(+), 43 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 8b663150..748bb193 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -2,6 +2,9 @@ use concordium_base::base::Energy; +/// The maximum size of parameters in PV 5+; +pub(crate) const MAX_PARAMETER_SIZE: usize = 65_535; + // Energy constants from Cost.hs in concordium-base. /// Cost of querying the account balance from a within smart contract instance. diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7f53cfd4..f0f63982 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -97,11 +97,6 @@ impl Chain { base_cost + deploy_module_cost }; let transaction_fee = self.calculate_energy_cost(energy); - println!( - "Deploying module with size {}, resulting in {} NRG.", - wasm_module.source.size(), - energy - ); // Try to subtract cost for account let account = self.get_account_mut(sender)?; @@ -199,6 +194,11 @@ impl Chain { return Err(ContractInitError::InsufficientFunds); } + // Ensure that the parameter has a valid size (+2 for the length of parameter). + if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + return Err(ContractInitError::ParameterTooLarge); + } + let mut remaining_energy = energy_reserved; // Compute the base cost for checking the transaction header. @@ -402,15 +402,10 @@ impl Chain { sender: Address, contract_address: ContractAddress, entrypoint: EntrypointName, - parameter: OwnedParameter, // TODO: Should we check size <= 65535? + parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, ) -> Result { - println!( - "Updating contract {}, with parameter: {:?}", - contract_address, parameter.0 - ); - // Ensure the sender exists. if !self.address_exists(sender) { return Err(ContractUpdateError::SenderDoesNotExist(sender)); @@ -423,6 +418,12 @@ impl Chain { if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } + + // Ensure that the parameter has a valid size (+2 for the length of parameter). + if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + return Err(ContractUpdateError::ParameterTooLarge); + } + // Charge account for the reserved energy up front. This is to ensure that // contract queries for the invoker balance are correct. account_info.balance.total -= invoker_amount_reserved_for_nrg; @@ -533,11 +534,6 @@ impl Chain { amount: Amount, energy_reserved: Energy, ) -> Result { - println!( - "Invoking contract {}, with parameter: {:?}", - contract_address, parameter.0 - ); - // Ensure the sender exists. if !self.address_exists(sender) { return Err(ContractUpdateError::SenderDoesNotExist(sender)); @@ -549,6 +545,12 @@ impl Chain { if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } + + // Ensure that the parameter has a valid size (+2 for the length of parameter). + if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + return Err(ContractUpdateError::ParameterTooLarge); + } + // Charge account for the reserved energy up front. This is to ensure that // contract queries for the invoker balance are correct. account_info.balance.total -= invoker_amount_reserved_for_nrg; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index d3cf6b71..80e5ed4b 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1061,7 +1061,6 @@ impl<'a> InvocationData<'a> { return_value, remaining_energy, } => { - println!("\tSuccessful contract update {}", self.address); let update_event = ChainEvent::Updated { address: self.address, contract: self.contract_name.clone(), @@ -1094,8 +1093,6 @@ impl<'a> InvocationData<'a> { config, interrupt, } => { - println!("\tInterrupting contract {}", self.address); - // Create the interrupt event, which will be included for transfers, calls, and // upgrades, but not for the remaining interrupts. let interrupt_event = ChainEvent::Interrupted { @@ -1107,8 +1104,6 @@ impl<'a> InvocationData<'a> { // Add the interrupt event self.chain_events.push(interrupt_event); - println!("\t\tTransferring {} CCD to {}", amount, to); - let response = match self .invocation_handler .transfer_from_contract_to_account(amount, self.address, to) @@ -1189,11 +1184,6 @@ impl<'a> InvocationData<'a> { // back. self.invocation_handler.checkpoint(); - println!( - "\t\tCalling contract {}\n\t\t\twith parameter: {:?}", - address, parameter - ); - let res = self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), @@ -1225,16 +1215,6 @@ impl<'a> InvocationData<'a> { state_changed }; - println!( - "\tResuming contract {}\n\t\tafter {}", - self.address, - if success { - "succesful invocation" - } else { - "failed invocation" - } - ); - // Add resume event let resume_event = ChainEvent::Resumed { address: self.address, @@ -1262,8 +1242,6 @@ impl<'a> InvocationData<'a> { self.process(resume_res) } v1::Interrupt::Upgrade { module_ref } => { - println!("Upgrading contract to {:?}", module_ref); - // Add the interrupt event. self.chain_events.push(interrupt_event); @@ -1338,7 +1316,6 @@ impl<'a> InvocationData<'a> { self.process(resume_res) } v1::Interrupt::QueryAccountBalance { address } => { - println!("\t\tQuerying account balance of {}", address); let response = match self.invocation_handler.account_balance(address) { Some(balance) => v1::InvokeResponse::Success { new_balance: self @@ -1371,8 +1348,6 @@ impl<'a> InvocationData<'a> { self.process(resume_res) } v1::Interrupt::QueryContractBalance { address } => { - println!("Querying contract balance of {}", address); - let response = match self.invocation_handler.contract_balance(address) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, @@ -1407,8 +1382,6 @@ impl<'a> InvocationData<'a> { self.process(resume_res) } v1::Interrupt::QueryExchangeRates => { - println!("Querying exchange rates"); - let exchange_rates = ( self.invocation_handler.euro_per_energy, self.invocation_handler.micro_ccd_per_euro, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 11171615..c105a2fc 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -206,6 +206,9 @@ pub enum ContractInitError { /// The invoker account does not have enough funds to pay for the energy. #[error("invoker does not have enough funds to pay for the energy")] InsufficientFunds, + /// The parameter is too large. + #[error("the provided parameter exceeds the max size allowed")] + ParameterTooLarge, } /// The reason for why a contract initialization failed during execution. @@ -285,6 +288,9 @@ pub enum ContractUpdateError { /// The invoker account does not have enough funds to pay for the energy. #[error("invoker does not have enough funds to pay for the energy")] InsufficientFunds, + /// The parameter is too large. + #[error("the provided parameter exceeds the max size allowed")] + ParameterTooLarge, } /// A transfer of [`Amount`]s failed because the sender had insufficient diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 75bc4559..bf26ad4b 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -114,6 +114,7 @@ fn deploy_and_init() -> (Chain, ContractAddress) { fn mk_parameter(internal_param_size: u16, desired_size: u32) -> OwnedParameter { let entrypoint = OwnedEntrypointName::new_unchecked("param-aux".into()); let filler_size = desired_size + - 2 // length of the parameter itself - 2 // internal_param_size - 2 // entrypoint name len - 9 // entrypoint name From 298ddcaafeea43d75cda96442e22b73cb3c2dce6 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 28 Feb 2023 11:30:11 +0100 Subject: [PATCH 080/208] Add library documentation - And fix a number of doc-related things --- contract-testing/src/impls.rs | 114 +++++++++++++---------- contract-testing/src/invocation/impls.rs | 42 ++++----- contract-testing/src/invocation/mod.rs | 3 +- contract-testing/src/lib.rs | 65 +++++++++++++ contract-testing/src/types.rs | 4 +- 5 files changed, 155 insertions(+), 73 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index f0f63982..7b70a422 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -91,7 +91,7 @@ impl Chain { + 8 + wasm_module.source.size() + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self.get_account(sender)?.signature_count; + let number_of_sigs = self.account(sender)?.signature_count; let base_cost = cost::base_cost(payload_size, number_of_sigs); let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); base_cost + deploy_module_cost @@ -99,7 +99,7 @@ impl Chain { let transaction_fee = self.calculate_energy_cost(energy); // Try to subtract cost for account - let account = self.get_account_mut(sender)?; + let account = self.account_mut(sender)?; if account.balance.available() < transaction_fee { return Err(DeployModuleError::InsufficientFunds); }; @@ -126,8 +126,9 @@ impl Chain { /// bytes and 4 module length bytes. /// The module still has to a valid V1 smart contract module. /// - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. + /// **Parameters:** + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. pub fn module_deploy_wasm_v1>( &mut self, sender: AccountAddress, @@ -146,8 +147,9 @@ impl Chain { /// i.e. **including** the prefix of 4 version bytes and 4 module length /// bytes. /// - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. + /// **Parameters:** + /// - `sender`: The account paying for the transaction. + /// - `module_path`: Path to a module file. pub fn module_deploy_v1>( &mut self, sender: AccountAddress, @@ -168,15 +170,16 @@ impl Chain { /// Initialize a contract. /// - /// - `sender`: The account paying for the transaction. Will also become the - /// owner of the instance created. - /// - `module_reference`: The reference to the a module that has already - /// been deployed. - /// - `contract_name`: Name of the contract to initialize. - /// - `parameter`: Parameter provided to the init method. - /// - `amount`: The initial balance of the contract. Subtracted from the + /// **Parameters:** + /// - `sender`: The account paying for the transaction. Will also become + /// the owner of the instance created. + /// - `module_reference`: The reference to the a module that has already + /// been deployed. + /// - `contract_name`: Name of the contract to initialize. + /// - `parameter`: Parameter provided to the init method. + /// - `amount`: The initial balance of the contract. Subtracted from the /// `sender` account. - /// - `energy_reserved`: Amount of energy reserved for executing the init + /// - `energy_reserved`: Amount of energy reserved for executing the init /// method. pub fn contract_init( &mut self, @@ -189,7 +192,7 @@ impl Chain { ) -> Result { // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. - let account_info = self.get_account(sender)?; + let account_info = self.account(sender)?; if account_info.balance.available() < self.calculate_energy_cost(energy_reserved) + amount { return Err(ContractInitError::InsufficientFunds); } @@ -318,7 +321,7 @@ impl Chain { self.contracts.insert(contract_address, contract_instance); // Subtract the amount from the invoker. - self.get_account_mut(sender) + self.account_mut(sender) .expect("Account known to exist") .balance .total -= amount; @@ -389,13 +392,22 @@ impl Chain { }; // Charge the account. // We have to get the account info again because of the borrow checker. - self.get_account_mut(sender)?.balance.total -= transaction_fee; + self.account_mut(sender)?.balance.total -= transaction_fee; res } /// Update a contract by calling one of its entrypoints. /// - /// If successful, any changes will be saved. + /// If successful, all changes will be saved. + /// + /// **Parameters:** + /// - `invoker`: the account paying for the transaction. + /// - `sender`: the sender of the transaction, can also be a contract. + /// - `contract_address`: the contract to update. + /// - `entrypoint`: the entrypoint to call. + /// - `parameter`: the contract parameter. + /// - `amount`: the amount sent to the contract. + /// - `energy_reserved`: the maximum energy that can be used in the update. pub fn contract_update( &mut self, invoker: AccountAddress, @@ -413,7 +425,7 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.get_account_mut(invoker)?; + let account_info = self.account_mut(invoker)?; let invoker_signature_count = account_info.signature_count; if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); @@ -513,7 +525,7 @@ impl Chain { // The `invoker` was charged for all the `reserved_energy` up front. // Here, we return the amount for any remaining energy. let return_amount = invoker_amount_reserved_for_nrg - transaction_fee; - self.get_account_mut(invoker) + self.account_mut(invoker) .expect("Known to exist") .balance .total += return_amount; @@ -522,8 +534,17 @@ impl Chain { /// Invoke a contract by calling an entrypoint. /// - /// Similar to [`contract_update`] except that all changes are discarded - /// afterwards. Typically used for "view" functions. + /// Similar to [`Self::contract_update`] except that all changes are + /// discarded afterwards. Typically used for "view" functions. + /// + /// **Parameters:** + /// - `invoker`: the account paying for the transaction. + /// - `sender`: the sender of the transaction, can also be a contract. + /// - `contract_address`: the contract to update. + /// - `entrypoint`: the entrypoint to call. + /// - `parameter`: the contract parameter. + /// - `amount`: the amount sent to the contract. + /// - `energy_reserved`: the maximum energy that can be used in the update. pub fn contract_invoke( &mut self, invoker: AccountAddress, @@ -541,7 +562,7 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.get_account_mut(invoker)?; + let account_info = self.account_mut(invoker)?; if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { return Err(ContractUpdateError::InsufficientFunds); } @@ -595,7 +616,7 @@ impl Chain { // Return the amount charged for the reserved energy, as this is not a // transaction. - self.get_account_mut(invoker) + self.account_mut(invoker) .expect("Known to exist") .balance .total += invoker_amount_reserved_for_nrg; @@ -603,14 +624,18 @@ impl Chain { result } - /// Create an account. Will override existing account if already present. + /// Create an account. + /// + /// Will override existing account if already present. pub fn create_account(&mut self, account: AccountAddress, account_info: Account) { self.accounts.insert(account, account_info); } - /// Creates a contract address with an index one above the highest - /// currently used. Next call to `contract_init` will skip this - /// address. + /// Create a contract address. + /// + /// It will have an index one above the previously highest contract index. + /// + /// Next call to `contract_init` will skip this address. pub fn create_contract_address(&mut self) -> ContractAddress { let index = self.next_contract_index; let subindex = 0; @@ -618,19 +643,6 @@ impl Chain { ContractAddress::new(index, subindex) } - /// Set the chain's slot time. - pub fn set_slot_time(&mut self, slot_time: SlotTime) { self.slot_time = slot_time; } - - /// Set the chain's Euro per NRG conversion rate. - pub fn set_euro_per_energy(&mut self, euro_per_energy: ExchangeRate) { - self.euro_per_energy = euro_per_energy; - } - - /// Set the chain's microCCD per Euro conversion rate. - pub fn set_micro_ccd_per_euro(&mut self, micro_ccd_per_euro: ExchangeRate) { - self.micro_ccd_per_euro = micro_ccd_per_euro; - } - /// Returns the balance of an account if it exists. pub fn account_balance(&self, address: AccountAddress) -> Option { self.accounts.get(&address).map(|ai| ai.balance) @@ -667,12 +679,12 @@ impl Chain { } /// Returns an immutable reference to an [`Account`]. - pub fn get_account(&self, address: AccountAddress) -> Result<&Account, AccountMissing> { + pub fn account(&self, address: AccountAddress) -> Result<&Account, AccountMissing> { self.accounts.get(&address).ok_or(AccountMissing(address)) } /// Returns a mutable reference to [`Account`]. - fn get_account_mut(&mut self, address: AccountAddress) -> Result<&mut Account, AccountMissing> { + fn account_mut(&mut self, address: AccountAddress) -> Result<&mut Account, AccountMissing> { self.accounts .get_mut(&address) .ok_or(AccountMissing(address)) @@ -688,9 +700,10 @@ impl Chain { self.contracts.contains_key(&address) } - /// Check whether the [`Address`] exists. I.e. if it is an - /// account, whether the account exists, and if it is a contract, whether - /// the contract exists. + /// Check whether the [`Address`] exists. + /// + /// I.e. if it is an account, whether the account exists, + /// and if it is a contract, whether the contract exists. fn address_exists(&self, address: Address) -> bool { match address { Address::Account(acc) => self.account_exists(acc), @@ -707,7 +720,7 @@ impl Chain { /// contract invoked has changed. /// /// *Preconditions*: - /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` + /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` fn convert_invoke_entrypoint_result( &self, update_aux_response: InvokeEntrypointResult, @@ -765,6 +778,7 @@ impl TestPolicies { impl Account { /// Create a new [`Self`] with the provided parameters. + /// /// The `signature_count` must be >= 1 for transaction costs to be /// realistic. pub fn new_with_policy_and_signature_count( @@ -780,7 +794,9 @@ impl Account { } /// Create new [`Self`] with empty account policies but the provided - /// `signature_count`. The `signature_count` must be >= 1 for transaction + /// `signature_count`. + /// + /// The `signature_count` must be >= 1 for transaction /// costs to be realistic. pub fn new_with_signature_count(balance: AccountBalance, signature_count: u32) -> Self { Self { @@ -789,7 +805,7 @@ impl Account { } } - /// Create new [`Self`] with empty account policies and a signature + /// Create new [`Self`] with the provided account policies and a signature /// count of `1`. pub fn new_with_policy(balance: AccountBalance, policies: TestPolicies) -> Self { Self { diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 80e5ed4b..afffb4c2 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -26,7 +26,7 @@ impl EntrypointInvocationHandler { /// Invoke an entrypoint and get the result, [`Changeset`], and chain /// events. /// - /// *Preconditions:* + /// **Preconditions:** /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// - `sender` exists @@ -70,7 +70,7 @@ impl EntrypointInvocationHandler { /// Used for handling contract entrypoint invocations internally. /// - /// *Preconditions:* + /// **Preconditions:** /// - `invoker` exists /// - `invoker` has sufficient balance to pay for `remaining_energy` /// - `sender` exists @@ -318,8 +318,8 @@ impl EntrypointInvocationHandler { /// Make a transfer from a contract to an account in the changeset. /// Returns the new balance of `from`. /// - /// Precondition: - /// - Assumes that `from` contract exists. + /// **Preconditions:** + /// - Assumes that `from` contract exists. fn transfer_from_contract_to_account( &mut self, amount: Amount, @@ -343,8 +343,8 @@ impl EntrypointInvocationHandler { /// /// Returns the new balance of `from`. /// - /// Precondition: - /// - Assumes that `from` contract exists. + /// **Preconditions:** + /// - Assumes that `from` contract exists. fn transfer_from_contract_to_contract( &mut self, amount: Amount, @@ -368,8 +368,8 @@ impl EntrypointInvocationHandler { /// /// Returns the new balance of `from`. /// - /// Precondition: - /// - Assumes that `from` account exists. + /// **Preconditions:** + /// - Assumes that `from` account exists. fn transfer_from_account_to_contract( &mut self, amount: Amount, @@ -395,7 +395,7 @@ impl EntrypointInvocationHandler { /// /// Returns the new balance. /// - /// Precondition: + /// **Preconditions:** /// - Contract must exist. fn change_contract_balance( &mut self, @@ -438,7 +438,7 @@ impl EntrypointInvocationHandler { /// /// Returns the new balance. /// - /// Precondition: + /// **Preconditions:** /// - Account must exist. fn change_account_balance( &mut self, @@ -478,7 +478,7 @@ impl EntrypointInvocationHandler { /// Returns the contract balance from the topmost checkpoint on the /// changeset. Or, alternatively, from persistence. /// - /// *Preconditions:* + /// **Preconditions:** /// - Contract must exist. fn contract_balance_unchecked(&self, address: ContractAddress) -> Amount { self.contract_balance(address) @@ -497,7 +497,7 @@ impl EntrypointInvocationHandler { /// Returns the contract module from the topmost checkpoint on /// the changeset. Or, alternatively, from persistence. /// - /// *Preconditions:* + /// **Preconditions:** /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. @@ -533,7 +533,7 @@ impl EntrypointInvocationHandler { /// Get the contract state, either from the changeset or by thawing it from /// persistence. /// - /// *Preconditions:* + /// **Preconditions:** /// - Contract instance must exist. fn contract_state(&self, address: ContractAddress) -> trie::MutableState { match self @@ -592,7 +592,7 @@ impl EntrypointInvocationHandler { /// This also increments the modification index. It will be set to 1 if the /// contract has no entry in the changeset. /// - /// *Preconditions:* + /// **Preconditions:** /// - Contract must exist. fn save_state_changes( &mut self, @@ -635,7 +635,7 @@ impl EntrypointInvocationHandler { /// Returns the previous module, which is either the one from persistence, /// or the most recent one from the changeset. /// - /// *Preconditions:* + /// **Preconditions:** /// - Contract must exist. /// - Module must exist. fn save_module_upgrade( @@ -812,7 +812,7 @@ impl ChangeSet { /// bytes added to contract states. It also returns whether the state of the /// provided `invoked_contract` was changed. /// - /// *Preconditions:* + /// **Preconditions:** /// - All contracts, modules, accounts referred must exist in persistence. /// - All amount deltas must be valid (i.e. not cause underflows when added /// to balance). @@ -1021,7 +1021,7 @@ impl ContractChanges { /// Get the current balance by adding the original balance and the balance /// delta. /// - /// *Preconditions:* + /// **Preconditions:** /// - `balance_delta + original_balance` must be larger than `0`. fn current_balance(&self) -> Amount { self.self_balance_delta @@ -1034,7 +1034,7 @@ impl AccountChanges { /// Get the current balance by adding the original balance and the balance /// delta. /// - /// *Preconditions:* + /// **Preconditions:** /// - `balance_delta + original_balance` must be larger than `0`. fn current_balance(&self) -> Amount { self.balance_delta @@ -1046,9 +1046,9 @@ impl AccountChanges { impl<'a> InvocationData<'a> { /// Process a receive function until completion. /// - /// *Preconditions*: - /// - Contract instance exists in `invocation_handler.contracts`. - /// - Account exists in `invocation_handler.accounts`. + /// **Preconditions**: + /// - Contract instance exists in `invocation_handler.contracts`. + /// - Account exists in `invocation_handler.accounts`. fn process( &mut self, res: ExecResult>, diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index 82db5e56..1b9aa7d3 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -1,7 +1,8 @@ //! Functionality and types for invoking contract entrypoints. //! //! Contract invocation is effectful and transactional. -//! We therefore keep track of changes during execution in a [`ChangeSet`]. +//! We therefore keep track of changes during execution in a +//! [`ChangeSet`][types::ChangeSet]. //! //! Once the execution (transaction) has finished, the changes can then be //! persisted (saved) or discarded, dependent on whether it succeeded or not. diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index d7d5d824..311ea38b 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,3 +1,68 @@ +//! # Concordium Smart Contract Testing +//! +//! This library supports integration testing of Concordium smart contracts +//! written in Rust. +//! +//! ## Basic usage +//! +//! ```no_run +//! use concordium_smart_contract_testing::*; +//! +//! // Create a "chain" with default parameters. +//! let mut chain = Chain::new(); +//! +//! // Define an account address to be used. +//! const ACC: AccountAddress = AccountAddress([0;32]); +//! +//! // Create an account with 10000 CCD in balance. +//! chain.create_account(ACC, Account::new(Amount::from_ccd(1000))); +//! +//! // Deploy a smart contract module (built with [Cargo Concordium](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#cargo-concordium)). +//! let deployment = chain +//! .module_deploy_v1(ACC, "path/to/contract.wasm.v1") +//! .unwrap(); +//! +//! // Initialize a smart contract from the deployed module. +//! let initialization = chain +//! .contract_init( +//! ACC, // Invoker account. +//! deployment.module_reference, // Module to initialize from. +//! ContractName::new_unchecked("init_my_contract"), // Contract to init. +//! OwnedParameter::new(&"my_param"), // Any type implementing [`Serial`] can be used. +//! Amount::zero(), // CCD to send the contract. +//! Energy::from(10000), // Maximum energy allowed for initializing. +//! ) +//! .unwrap(); +//! +//! // Update the initialized contract. +//! let update = chain +//! .contract_update( +//! ACC, // Invoker account. +//! Address::Account(ACC), // Sender (can also be a contract). +//! initialization.contract_address, // The contract to update. +//! EntrypointName::new_unchecked("my_entrypoint"), // The entrypoint to call. +//! OwnedParameter::new(&42u8), // Another type of parameter. +//! Amount::from_ccd(100), // Sending the contract 100 CCD. +//! Energy::from(10000), // Maximum energy allowed for the update. +//! ) +//! .unwrap(); +//! +//! // Check the chain events produced (updates, interrupts, resumes, transfers, etc.). +//! assert!(matches!(update.chain_events[..], [ChainEvent::Updated{..}])); +//! +//! // Check the return value. +//! assert_eq!(update.return_value, to_bytes(&84u8)); +//! +//! // Check the balances of both contracts and accounts. +//! assert_eq!(chain.contract_balance(initialization.contract_address), Some(Amount::from_ccd(100))); +//! assert_eq!(chain.account_balance_available(ACC), Some( +//! Amount::from_ccd(1000) +//! - Amount::from_ccd(100) // Amount sent to contract. +//! - deployment.transaction_fee +//! - initialization.transaction_fee +//! - update.transaction_fee)); +//! +//! ``` mod constants; mod impls; mod invocation; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index c105a2fc..544557b5 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -15,12 +15,12 @@ use wasm_chain_integration::{ }; use wasm_transform::artifact; -/// A V1 artifact, with concrete types for the generic parameters. +/// A smart contract module. #[derive(Debug, Clone)] pub struct ContractModule { /// Size of the module in bytes. Used for cost accounting. pub(crate) size: u64, - /// The runnable module (artifact). + /// The runnable module. pub(crate) artifact: Arc>, } From e14af1a8c2d487b3f677e84b7373aa7cedc5a150 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 28 Feb 2023 11:32:56 +0100 Subject: [PATCH 081/208] Add changelog --- contract-testing/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 contract-testing/CHANGELOG.md diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md new file mode 100644 index 00000000..90aa1ab3 --- /dev/null +++ b/contract-testing/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased changes + +- First version of the library. From 2d22e0b530bf9dde1531bc124150b9fe23bf88ee Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 13:25:41 +0100 Subject: [PATCH 082/208] Update base and handle changes --- contract-testing/Cargo.lock | 88 +++++++++--------- contract-testing/Cargo.toml | 4 +- contract-testing/src/impls.rs | 26 +++--- contract-testing/src/invocation/impls.rs | 10 +-- contract-testing/src/invocation/types.rs | 4 +- contract-testing/src/lib.rs | 6 +- contract-testing/src/types.rs | 9 +- .../tests/all_new_host_functions.rs | 2 +- contract-testing/tests/basics.rs | 90 ++++++++++++++----- contract-testing/tests/checkpointing.rs | 10 +-- contract-testing/tests/counter.rs | 4 +- contract-testing/tests/error_codes.rs | 14 +-- contract-testing/tests/fallback.rs | 14 +-- contract-testing/tests/iterator.rs | 2 +- contract-testing/tests/queries.rs | 22 ++--- contract-testing/tests/recorder.rs | 6 +- .../tests/relaxed_restrictions.rs | 9 +- contract-testing/tests/transfer.rs | 8 +- contract-testing/tests/upgrades.rs | 18 ++-- 19 files changed, 195 insertions(+), 151 deletions(-) diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock index d84c0455..3910d664 100644 --- a/contract-testing/Cargo.lock +++ b/contract-testing/Cargo.lock @@ -275,7 +275,7 @@ dependencies = [ [[package]] name = "concordium-contracts-common" -version = "5.0.0" +version = "5.2.0" dependencies = [ "bs58", "chrono", @@ -301,17 +301,50 @@ dependencies = [ "syn", ] +[[package]] +name = "concordium-smart-contract-engine" +version = "1.0.0" +dependencies = [ + "anyhow", + "byteorder", + "concordium-contracts-common", + "concordium-wasm", + "derive_more", + "ed25519-zebra", + "libc", + "num_enum", + "rand 0.8.5", + "secp256k1", + "serde", + "sha2 0.10.6", + "sha3", + "slab", + "thiserror", + "tinyvec", +] + [[package]] name = "concordium-smart-contract-testing" version = "0.1.0" dependencies = [ "anyhow", + "concordium-smart-contract-engine", + "concordium-wasm", "concordium_base", "num-bigint 0.4.3", "sha2 0.10.6", "thiserror", - "wasm-chain-integration", - "wasm-transform", +] + +[[package]] +name = "concordium-wasm" +version = "1.0.0" +dependencies = [ + "anyhow", + "concordium-contracts-common", + "derive_more", + "leb128", + "num_enum", ] [[package]] @@ -320,6 +353,7 @@ version = "0.1.0" dependencies = [ "aggregate_sig", "anyhow", + "bs58", "byteorder", "chrono", "concordium-contracts-common", @@ -334,6 +368,7 @@ dependencies = [ "hex", "id", "itertools", + "leb128", "libc", "num", "num-bigint 0.4.3", @@ -692,7 +727,6 @@ dependencies = [ "rand 0.7.3", "random_oracle", "serde", - "wasm-bindgen", ] [[package]] @@ -720,13 +754,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ffi_helpers" -version = "0.1.0" -dependencies = [ - "libc", -] - [[package]] name = "fnv" version = "1.0.7" @@ -874,7 +901,6 @@ dependencies = [ "serde_json", "sha2 0.10.6", "thiserror", - "wasm-bindgen", ] [[package]] @@ -1424,9 +1450,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "secp256k1" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" dependencies = [ "secp256k1-sys", ] @@ -1721,40 +1747,6 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" -[[package]] -name = "wasm-chain-integration" -version = "0.2.0" -dependencies = [ - "anyhow", - "byteorder", - "concordium-contracts-common", - "derive_more", - "ed25519-zebra", - "ffi_helpers", - "libc", - "num_enum", - "rand 0.8.5", - "secp256k1", - "serde", - "sha2 0.10.6", - "sha3", - "slab", - "thiserror", - "tinyvec", - "wasm-transform", -] - -[[package]] -name = "wasm-transform" -version = "0.1.1" -dependencies = [ - "anyhow", - "concordium-contracts-common", - "derive_more", - "leb128", - "num_enum", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 99e9e988..0810c481 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" [dependencies] concordium_base = {path = "../concordium-base/rust-src/concordium_base"} -wasm-chain-integration = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} -wasm-transform = {path = "../concordium-base/smart-contracts/wasm-transform"} +concordium-smart-contract-engine = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7b70a422..dc57d3aa 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -3,15 +3,15 @@ use concordium_base::{ common::{self, to_bytes}, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, Parameter, - SlotTime, Timestamp, + ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, SlotTime, + Timestamp, }, smart_contracts::{ModuleSource, OwnedReceiveName, WasmModule, WasmVersion}, transactions::{self, cost}, }; +use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; use num_bigint::BigUint; use std::{collections::BTreeMap, path::Path, sync::Arc}; -use wasm_chain_integration::{v0, v1, InterpreterEnergy}; use crate::{ constants, @@ -77,7 +77,7 @@ impl Chain { wasm_module: WasmModule, ) -> Result { // Deserialize as wasm module (artifact) - let artifact = wasm_transform::utils::instantiate_with_metering::( + let artifact = concordium_wasm::utils::instantiate_with_metering::( &v1::ConcordiumAllowedImports { support_upgrade: true, }, @@ -198,7 +198,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { return Err(ContractInitError::ParameterTooLarge); } @@ -257,7 +257,7 @@ impl Chain { v1::InitInvocation { amount, init_name: contract_name.get_chain_name(), - parameter: ¶meter.0, + parameter: parameter.as_ref(), energy: to_interpreter_energy(remaining_energy), }, false, @@ -432,7 +432,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { return Err(ContractUpdateError::ParameterTooLarge); } @@ -487,7 +487,7 @@ impl Chain { sender, contract_address, entrypoint.to_owned(), - Parameter(¶meter.0), + parameter.as_parameter(), amount, remaining_energy, ); @@ -568,7 +568,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.0.len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { return Err(ContractUpdateError::ParameterTooLarge); } @@ -583,7 +583,7 @@ impl Chain { sender, contract_address, entrypoint.to_owned(), - Parameter(¶meter.0), + parameter.as_parameter(), amount, energy_reserved, ); @@ -771,11 +771,15 @@ impl Chain { impl TestPolicies { // TODO: Make correctly structured policies ~= Vec>. - pub fn empty() -> Self { Self(v0::OwnedPolicyBytes::new()) } + pub fn empty() -> Self { Self(Vec::new()) } // TODO: Add helper functions for creating arbitrary valid policies. } +impl AsRef<[u8]> for TestPolicies { + fn as_ref(&self) -> &[u8] { &self.0 } +} + impl Account { /// Create a new [`Self`] with the provided parameters. /// diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index afffb4c2..90b9aefd 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -14,13 +14,13 @@ use concordium_base::{ ModuleReference, OwnedEntrypointName, OwnedReceiveName, Parameter, }, }; -use std::collections::{btree_map, BTreeMap}; -use wasm_chain_integration::{ +use concordium_smart_contract_engine::{ v0, v1::{self, trie}, ExecResult, InterpreterEnergy, }; -use wasm_transform::artifact; +use concordium_wasm::artifact; +use std::collections::{btree_map, BTreeMap}; impl EntrypointInvocationHandler { /// Invoke an entrypoint and get the result, [`Changeset`], and chain @@ -210,7 +210,7 @@ impl EntrypointInvocationHandler { v1::ReceiveInvocation { amount, receive_name: receive_name.as_receive_name(), - parameter: parameter.0, + parameter: parameter.as_ref(), energy, }, instance_state, @@ -1189,7 +1189,7 @@ impl<'a> InvocationData<'a> { Address::Contract(self.address), address, name, - Parameter(¶meter), + Parameter::new_unchecked(¶meter), amount, from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), &mut self.chain_events, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index caf99edd..64c8d9a4 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -3,12 +3,12 @@ use concordium_base::contracts_common::{ AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, SlotTime, }; -use std::collections::BTreeMap; -use wasm_chain_integration::{ +use concordium_smart_contract_engine::{ v0, v1::{trie::MutableState, InvokeResponse}, InterpreterEnergy, }; +use std::collections::BTreeMap; /// The result of invoking an entrypoint. pub(crate) struct InvokeEntrypointResult { diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 311ea38b..ac2871c7 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -28,7 +28,7 @@ //! ACC, // Invoker account. //! deployment.module_reference, // Module to initialize from. //! ContractName::new_unchecked("init_my_contract"), // Contract to init. -//! OwnedParameter::new(&"my_param"), // Any type implementing [`Serial`] can be used. +//! OwnedParameter::from_serial(&"my_param").unwrap(), // Any type implementing [`Serial`] can be used. //! Amount::zero(), // CCD to send the contract. //! Energy::from(10000), // Maximum energy allowed for initializing. //! ) @@ -41,7 +41,7 @@ //! Address::Account(ACC), // Sender (can also be a contract). //! initialization.contract_address, // The contract to update. //! EntrypointName::new_unchecked("my_entrypoint"), // The entrypoint to call. -//! OwnedParameter::new(&42u8), // Another type of parameter. +//! OwnedParameter::from_serial(&42u8).unwrap(), // Another type of parameter. //! Amount::from_ccd(100), // Sending the contract 100 CCD. //! Energy::from(10000), // Maximum energy allowed for the update. //! ) @@ -78,4 +78,4 @@ pub use concordium_base::{ OwnedParameter, Parameter, SlotTime, }, }; -pub use wasm_chain_integration::v1::InvokeFailure; +pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 544557b5..ae5a1ca6 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -9,11 +9,11 @@ use concordium_base::{ }, smart_contracts::WasmVersion, }; -use wasm_chain_integration::{ +use concordium_smart_contract_engine::{ v0, v1::{self, trie, ReturnValue}, }; -use wasm_transform::artifact; +use concordium_wasm::artifact; /// A smart contract module. #[derive(Debug, Clone)] @@ -61,7 +61,8 @@ pub struct Contract { } /// Account policies for testing. -pub struct TestPolicies(pub v0::OwnedPolicyBytes); +#[derive(Clone, Debug)] +pub struct TestPolicies(pub Vec); /// An account. #[derive(Clone)] @@ -69,7 +70,7 @@ pub struct Account { /// The account balance. pub balance: AccountBalance, /// Account policies. - pub policies: v0::OwnedPolicyBytes, + pub policies: Vec, // TODO: Decide how policies should be represented. /// The number of signatures. The number of signatures affect the cost of /// every transaction for the account. pub signature_count: u32, diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index bf4fee73..578c230a 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -5,7 +5,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 73fd27a5..887ce196 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -12,7 +12,10 @@ fn deploying_valid_module_works() { chain.create_account(ACC_0, Account::new(initial_balance)); let res = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) .expect("Deploying valid module should work."); assert_eq!(chain.modules.len(), 1); @@ -29,7 +32,10 @@ fn initializing_valid_contract_works() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -37,7 +43,7 @@ fn initializing_valid_contract_works() { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), + OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), Amount::zero(), Energy::from(10000), ) @@ -56,7 +62,10 @@ fn initializing_with_invalid_parameter_fails() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -64,7 +73,7 @@ fn initializing_with_invalid_parameter_fails() { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![99u8]), // Invalid param + OwnedParameter::try_from(vec![99u8]).expect("Parameter has valid size."), // Invalid param Amount::zero(), Energy::from(10000), ) @@ -91,7 +100,10 @@ fn updating_valid_contract_works() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -99,7 +111,7 @@ fn updating_valid_contract_works() { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 + OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 Amount::zero(), Energy::from(10000), ) @@ -111,7 +123,7 @@ fn updating_valid_contract_works() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("set"), - OwnedParameter::from_bytes(vec![1u8]), // Updated to 1 + OwnedParameter::try_from(vec![1u8]).expect("Parameter has valid size."), // Updated to 1 Amount::zero(), Energy::from(10000), ) @@ -156,7 +168,10 @@ fn updating_and_invoking_with_missing_sender_fails() { let missing_contract = Address::Contract(ContractAddress::new(100, 0)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/icecream/icecream.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -164,7 +179,7 @@ fn updating_and_invoking_with_missing_sender_fails() { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_weather"), - OwnedParameter::from_bytes(vec![0u8]), // Starts as 0 + OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 Amount::zero(), Energy::from(10000), ) @@ -239,7 +254,10 @@ mod integrate_contract { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -259,7 +277,7 @@ mod integrate_contract { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&ACC_1), + OwnedParameter::from_serial(&ACC_1).expect("Parameter has valid size"), transfer_amount, Energy::from(10000), ) @@ -312,7 +330,10 @@ mod integrate_contract { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -331,7 +352,9 @@ mod integrate_contract { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&ACC_1), // We haven't created ACC_1. + OwnedParameter::from_serial(&ACC_1).expect( + "Parameter has valid size", + ), // We haven't created ACC_1. transfer_amount, Energy::from(100000), ); @@ -364,7 +387,10 @@ mod integrate_contract { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -384,7 +410,10 @@ mod integrate_contract { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("recurse"), - OwnedParameter::new(&10u32), + OwnedParameter::from_serial(&10u32).expect( + "Parameter has +valid size", + ), Amount::zero(), Energy::from(1000000), ) @@ -427,7 +456,10 @@ mod integrate_contract { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", + ) .expect("Deploying valid module should work"); let input_param: u32 = 8; @@ -449,7 +481,10 @@ mod integrate_contract { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("inc-fail-on-zero"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect( + "Parameter +has valid size", + ), Amount::zero(), Energy::from(100000000), ) @@ -492,7 +527,10 @@ mod integrate_contract { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/integrate/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init_0 = chain @@ -525,7 +563,7 @@ mod integrate_contract { Address::Account(ACC_0), res_init_0.contract_address, EntrypointName::new_unchecked("mutate_and_forward"), - OwnedParameter::new(¶m), + OwnedParameter::from_serial(¶m).expect("Parameter has valid size."), transfer_amount, Energy::from(100000), ) @@ -540,7 +578,10 @@ fn init_with_less_energy_than_module_lookup() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1", + ) .expect("Deploying valid module should work"); let reserved_energy = Energy::from(10); @@ -570,7 +611,10 @@ fn update_with_fib_reentry_works() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, "examples/fib/a.wasm.v1") + .module_deploy_v1( + ACC_0, + "../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1", + ) .expect("Deploying valid module should work"); let res_init = chain @@ -590,7 +634,7 @@ fn update_with_fib_reentry_works() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("receive"), - OwnedParameter::new(&6u64), + OwnedParameter::from_serial(&6u64).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 0254871a..605210de 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -6,7 +6,7 @@ //! fails. use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); const ACC_1: AccountAddress = AccountAddress([1; 32]); @@ -73,7 +73,7 @@ fn test_case_1() { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), + OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), // We supply one microCCD as we expect a trap // (see contract for details). Amount::from_micro_ccd(1), @@ -148,7 +148,7 @@ fn test_case_2() { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), + OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), // We supply zero microCCD as we're instructing the contract to not expect // state modifications. Also, the contract does not expect // errors, i.e., a trap signal from underlying invocations. @@ -208,7 +208,7 @@ fn test_case_3() { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(&ACC_1), + OwnedParameter::from_serial(&ACC_1).expect("Parameter has valid size"), // We supply three micro CCDs as we're instructing the contract to carry out a // transfer instead of a call. See the contract for // details. @@ -281,7 +281,7 @@ fn test_case_4() { Address::Account(ACC_0), res_init_a.contract_address, EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::new(¶meter), + OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), // We supply four CCDs as we're instructing the contract to expect state // modifications being made from the 'inner' contract A // call to be in effect when returned to the caller (a.a_modify_proxy). diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 7f9f18d9..88d37e9c 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -4,7 +4,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] @@ -66,7 +66,7 @@ fn test_counter() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("inc10"), - OwnedParameter::new(¶meter), + OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index e8871da4..105b6fb7 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -4,7 +4,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] @@ -49,7 +49,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_0), + OwnedParameter::from_serial(¶meter_0).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -79,7 +79,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_1), + OwnedParameter::from_serial(¶meter_1).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -107,7 +107,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_2), + OwnedParameter::from_serial(¶meter_2).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -137,7 +137,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_3), + OwnedParameter::from_serial(¶meter_3).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -167,7 +167,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_4), + OwnedParameter::from_serial(¶meter_4).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -200,7 +200,7 @@ fn test_error_codes() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("call"), - OwnedParameter::new(¶meter_6), + OwnedParameter::from_serial(¶meter_6).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index 334a4426..eb0a7ac7 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -2,7 +2,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] @@ -31,8 +31,9 @@ fn test_fallback() { ACC_0, res_deploy.module_reference, ContractName::new_unchecked("init_one"), - OwnedParameter::new(&res_init_two.contract_address), /* Pass in address of contract - * "two". */ + OwnedParameter::from_serial(&res_init_two.contract_address) + .expect("Parameter has valid size"), /* Pass in address of contract + * "two". */ Amount::zero(), Energy::from(10000), ) @@ -59,7 +60,7 @@ fn test_fallback() { } // Invoke "two.do" via "one.do" and the fallback. - let parameter = OwnedParameter::new(&"ASDF"); + let parameter = OwnedParameter::from_serial(&"ASDF").expect("Parameter has valid size."); let res_invoke_2 = chain .contract_invoke( ACC_0, @@ -71,6 +72,7 @@ fn test_fallback() { Energy::from(10000), ) .expect("Invoke should succeed."); - assert_eq!(res_invoke_2.return_value, parameter.0); // Parameter is returned - // via the fallback. + assert_eq!(res_invoke_2.return_value, parameter.as_ref()); // Parameter is + // returned + // via the fallback. } diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index ccc94a12..4c29ddf8 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -6,7 +6,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 531c0469..3f58eb40 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -7,7 +7,7 @@ //! - the exhange rates. use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); const ACC_1: AccountAddress = AccountAddress([1; 32]); @@ -51,7 +51,7 @@ mod query_account_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -111,7 +111,7 @@ mod query_account_balance { Address::Account(ACC_1), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), update_amount, energy_limit, ) @@ -177,7 +177,7 @@ mod query_account_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -242,7 +242,7 @@ mod query_account_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -300,7 +300,7 @@ mod query_account_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -372,7 +372,7 @@ mod query_contract_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -421,7 +421,7 @@ mod query_contract_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), update_amount, Energy::from(100000), ) @@ -477,7 +477,7 @@ mod query_contract_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), update_amount, Energy::from(100000), ) @@ -528,7 +528,7 @@ mod query_contract_balance { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -578,7 +578,7 @@ mod query_exchange_rates { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("query"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index c0a11f12..a677e0aa 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -2,7 +2,7 @@ use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] @@ -35,7 +35,7 @@ fn test_recorder() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("record_u64"), - OwnedParameter::new(&20u64), + OwnedParameter::from_serial(&20u64).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -46,7 +46,7 @@ fn test_recorder() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("record_u64"), - OwnedParameter::new(&40u64), + OwnedParameter::from_serial(&40u64).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index bf26ad4b..aef8a39a 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -13,7 +13,7 @@ //! - Of size > 1 kb: base cost + 1NRG / 1 *byte* use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); /// Test the new parameter size limit. @@ -45,7 +45,7 @@ fn test_new_return_value_limit() { Address::Account(ACC_0), contract_address, EntrypointName::new_unchecked("return-value"), - OwnedParameter::new(&100_000u32), + OwnedParameter::from_serial(&100_000u32).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -63,7 +63,7 @@ fn test_new_log_limit() { Address::Account(ACC_0), contract_address, EntrypointName::new_unchecked("logs"), - OwnedParameter::new(&64u32), + OwnedParameter::from_serial(&64u32).expect("Parameter has valid size"), Amount::zero(), Energy::from(10000), ) @@ -120,5 +120,6 @@ fn mk_parameter(internal_param_size: u16, desired_size: u32) -> OwnedParameter { - 9 // entrypoint name - 4; // length of filler vector let filler = vec![1u8; filler_size as usize]; - OwnedParameter::new(&(internal_param_size, entrypoint, filler)) + OwnedParameter::from_serial(&(internal_param_size, entrypoint, filler)) + .expect("Parameter has valid size.") } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index db1e1712..2b89f934 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -1,9 +1,9 @@ //! This module contains tests for transfers fr&om a contract to an account. //! See more details about the specific test inside the `transfer.wat` file. use concordium_smart_contract_testing::*; -use wasm_chain_integration::v0::Logs; +use concordium_smart_contract_engine::v0::Logs; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); #[test] @@ -35,7 +35,7 @@ fn test_transfer() { Address::Account(ACC_0), contract_address, EntrypointName::new_unchecked("forward"), - OwnedParameter::new(&ACC_0), + OwnedParameter::from_serial(&ACC_0).expect("Parameter has valid size"), Amount::from_micro_ccd(123), Energy::from(10000), ) @@ -65,7 +65,7 @@ fn test_transfer() { Address::Account(ACC_0), contract_address, EntrypointName::new_unchecked("send"), - OwnedParameter::new(&(ACC_0, Amount::from_micro_ccd(17))), /* Tell it to send 17 + OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))).expect("Parameter has valid size"), /* Tell it to send 17 * mCCD to ACC_0. */ Amount::zero(), Energy::from(10000), diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index addea2eb..f05310e0 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -2,7 +2,7 @@ //! functionality. use concordium_smart_contract_testing::*; -const WASM_TEST_FOLDER: &str = "../../concordium-node/concordium-consensus/testdata/contracts/v1"; +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); /// Test a basic upgrade, ensuring that the new module is in place by @@ -42,7 +42,7 @@ fn test() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("bump"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -55,7 +55,7 @@ fn test() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("newfun"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -111,7 +111,7 @@ fn test_self_invoke() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -222,7 +222,7 @@ fn test_missing_contract() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -275,7 +275,7 @@ fn test_twice_in_one_transaction() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -346,7 +346,7 @@ fn test_chained_contract() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&input_param), + OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), Amount::zero(), Energy::from(1000000), ) @@ -400,7 +400,7 @@ fn test_reject() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(1000000), ); @@ -500,7 +500,7 @@ fn test_changing_entrypoint() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::new(&res_deploy_1.module_reference), + OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), Amount::zero(), Energy::from(1000000), ) From e21a6b7c626d79e61c953c4ca444b723f9e4fa8a Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 13:32:58 +0100 Subject: [PATCH 083/208] Remove redundant tests --- contract-testing/tests/basics.rs | 332 ------------------------------- 1 file changed, 332 deletions(-) diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 887ce196..4b320fd7 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -239,338 +239,6 @@ fn updating_and_invoking_with_missing_sender_fails() { Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); } -/// Tests using the integrate contract defined in -/// concordium-rust-smart-contract on the 'kb/sc-integration-testing' -/// branch. -mod integrate_contract { - use super::*; - - #[test] - fn update_with_account_transfer_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - ACC_0, - "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::from_serial(&ACC_1).expect("Parameter has valid size"), - transfer_amount, - Energy::from(10000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.account_balance_available(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - - transfer_amount - ) - ); - assert_eq!( - chain.account_balance_available(ACC_1), - Some(initial_balance + transfer_amount) - ); - assert_eq!(res_update.transfers(), [Transfer { - from: res_init.contract_address, - amount: transfer_amount, - to: ACC_1, - }]); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - assert_eq!(res_update.return_value, [2, 0, 0, 0]); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, [2, 0, 0, 0]); - } - - #[test] - fn update_with_account_transfer_to_missing_account_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(1); - chain.create_account(ACC_0, Account::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - ACC_0, - "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::from_serial(&ACC_1).expect( - "Parameter has valid size", - ), // We haven't created ACC_1. - transfer_amount, - Energy::from(100000), - ); - - match res_update { - Err(ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::ContractReject { code, .. }, - transaction_fee, - .. - }) => { - assert_eq!(code, -3); // The custom contract error code for missing account. - assert_eq!( - chain.account_balance_available(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - transaction_fee - ) - ); - } - _ => panic!("Expected contract update to fail"), - } - } - - #[test] - fn update_with_integrate_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - ACC_0, - "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("recurse"), - OwnedParameter::from_serial(&10u32).expect( - "Parameter has -valid size", - ), - Amount::zero(), - Energy::from(1000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.account_balance_available(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert_eq!(chain.contracts.len(), 1); - assert!(res_update.state_changed); - let expected_res = 10 + 7 + 11 + 3 + 7 + 11; - assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); - } - - #[test] - fn update_with_rollback_and_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - ACC_0, - "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", - ) - .expect("Deploying valid module should work"); - - let input_param: u32 = 8; - - let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("inc-fail-on-zero"), - OwnedParameter::from_serial(&input_param).expect( - "Parameter -has valid size", - ), - Amount::zero(), - Energy::from(100000000), - ) - .expect("Updating valid contract should work"); - - let res_view = chain - .contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Invoking get should work"); - - assert_eq!( - chain.account_balance_available(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(res_update.state_changed); - let expected_res = 2u32.pow(input_param) - 1; - assert_eq!(res_update.return_value, u32::to_le_bytes(expected_res)); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, u32::to_le_bytes(expected_res)); - } - - #[test] - fn rollback_of_account_balances_after_failed_contract_invoke() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - let transfer_amount = Amount::from_ccd(2); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - ACC_0, - "../../concordium-rust-smart-contracts/examples/integrate/a.wasm.v1", - ) - .expect("Deploying valid module should work"); - - let res_init_0 = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let res_init_1 = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_integrate_other"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) - .expect("Initializing valid contract should work"); - - let param = (res_init_1.contract_address, initial_balance, ACC_1); - - chain - .contract_update( - ACC_0, - Address::Account(ACC_0), - res_init_0.contract_address, - EntrypointName::new_unchecked("mutate_and_forward"), - OwnedParameter::from_serial(¶m).expect("Parameter has valid size."), - transfer_amount, - Energy::from(100000), - ) - .expect("Update should succeed"); - } -} - #[test] fn init_with_less_energy_than_module_lookup() { let mut chain = Chain::new(); From 22b669f951d8de98363419abb3a2e65757fd8de8 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 13:36:13 +0100 Subject: [PATCH 084/208] Remove duplicate cost constants --- contract-testing/src/constants.rs | 7 ------- contract-testing/src/impls.rs | 8 ++++---- contract-testing/src/invocation/impls.rs | 5 +++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 748bb193..cc10b04d 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -2,9 +2,6 @@ use concordium_base::base::Energy; -/// The maximum size of parameters in PV 5+; -pub(crate) const MAX_PARAMETER_SIZE: usize = 65_535; - // Energy constants from Cost.hs in concordium-base. /// Cost of querying the account balance from a within smart contract instance. @@ -27,7 +24,3 @@ pub(crate) const INITIALIZE_CONTRACT_INSTANCE_CREATE_COST: Energy = Energy { ene /// The base cost of updating a contract instance to cover administrative /// costs. Even if no code is run. pub(crate) const UPDATE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; - -/// The cost for a simple transfer (simple because it is not an encrypted or -/// scheduled transfer). -pub(crate) const SIMPLE_TRANSFER_COST: Energy = Energy { energy: 300 }; diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index dc57d3aa..33bbd405 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -2,7 +2,7 @@ use concordium_base::{ base::{self, Energy}, common::{self, to_bytes}, contracts_common::{ - AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, + self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, SlotTime, Timestamp, }, @@ -198,7 +198,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { return Err(ContractInitError::ParameterTooLarge); } @@ -432,7 +432,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { return Err(ContractUpdateError::ParameterTooLarge); } @@ -568,7 +568,7 @@ impl Chain { } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > constants::MAX_PARAMETER_SIZE { + if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { return Err(ContractUpdateError::ParameterTooLarge); } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 90b9aefd..05f9eb42 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1142,8 +1142,9 @@ impl<'a> InvocationData<'a> { let mut remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy = - remaining_energy.saturating_sub(constants::SIMPLE_TRANSFER_COST); + remaining_energy = remaining_energy.saturating_sub( + concordium_base::transactions::cost::SIMPLE_TRANSFER, + ); let resume_res = run_interpreter(remaining_energy, |energy| { v1::resume_receive( From 3a70a0a0dba286b55eae68588365ad046c1566c5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 13:48:44 +0100 Subject: [PATCH 085/208] Remove Cargo.lock and add it to gitignore --- contract-testing/.gitignore | 1 + contract-testing/Cargo.lock | 1800 ----------------------------------- 2 files changed, 1 insertion(+), 1800 deletions(-) create mode 100644 contract-testing/.gitignore delete mode 100644 contract-testing/Cargo.lock diff --git a/contract-testing/.gitignore b/contract-testing/.gitignore new file mode 100644 index 00000000..03314f77 --- /dev/null +++ b/contract-testing/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/contract-testing/Cargo.lock b/contract-testing/Cargo.lock deleted file mode 100644 index 3910d664..00000000 --- a/contract-testing/Cargo.lock +++ /dev/null @@ -1,1800 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aes" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aggregate_sig" -version = "0.1.0" -dependencies = [ - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "generic-array", - "id", - "pairing", - "rand 0.7.3", - "random_oracle", - "rayon", - "serde", - "sha2 0.10.6", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.8", - "once_cell", - "version_check", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64ct" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" -dependencies = [ - "generic-array", -] - -[[package]] -name = "borsh" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" -dependencies = [ - "borsh-derive", - "hashbrown 0.11.2", -] - -[[package]] -name = "borsh-derive" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bs58" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" -dependencies = [ - "sha2 0.9.9", -] - -[[package]] -name = "bulletproofs" -version = "0.1.0" -dependencies = [ - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "group", - "pairing", - "pedersen_scheme", - "rand 0.7.3", - "random_oracle", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" - -[[package]] -name = "bytecheck" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" -dependencies = [ - "bytecheck_derive", - "ptr_meta", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "concordium-contracts-common" -version = "5.2.0" -dependencies = [ - "bs58", - "chrono", - "concordium-contracts-common-derive", - "fnv", - "hashbrown 0.11.2", - "hex", - "num-bigint 0.4.3", - "num-integer", - "num-traits", - "rust_decimal", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "concordium-contracts-common-derive" -version = "1.0.1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "concordium-smart-contract-engine" -version = "1.0.0" -dependencies = [ - "anyhow", - "byteorder", - "concordium-contracts-common", - "concordium-wasm", - "derive_more", - "ed25519-zebra", - "libc", - "num_enum", - "rand 0.8.5", - "secp256k1", - "serde", - "sha2 0.10.6", - "sha3", - "slab", - "thiserror", - "tinyvec", -] - -[[package]] -name = "concordium-smart-contract-testing" -version = "0.1.0" -dependencies = [ - "anyhow", - "concordium-smart-contract-engine", - "concordium-wasm", - "concordium_base", - "num-bigint 0.4.3", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "concordium-wasm" -version = "1.0.0" -dependencies = [ - "anyhow", - "concordium-contracts-common", - "derive_more", - "leb128", - "num_enum", -] - -[[package]] -name = "concordium_base" -version = "0.1.0" -dependencies = [ - "aggregate_sig", - "anyhow", - "bs58", - "byteorder", - "chrono", - "concordium-contracts-common", - "crypto_common", - "derive_more", - "ecvrf", - "ed25519-dalek", - "eddsa_ed25519", - "either", - "encrypted_transfers", - "ff", - "hex", - "id", - "itertools", - "leb128", - "libc", - "num", - "num-bigint 0.4.3", - "num-traits", - "pairing", - "rand 0.7.3", - "rand_core 0.5.1", - "random_oracle", - "rust_decimal", - "serde", - "serde_json", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto_common" -version = "0.1.0" -dependencies = [ - "aes", - "anyhow", - "base64", - "byteorder", - "cbc", - "concordium-contracts-common", - "crypto_common_derive", - "derive_more", - "ed25519-dalek", - "either", - "ff", - "group", - "hex", - "hmac", - "libc", - "pairing", - "pbkdf2", - "rand 0.7.3", - "serde", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "crypto_common_derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "curve25519-dalek" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve_arithmetic" -version = "0.1.0" -dependencies = [ - "anyhow", - "byteorder", - "crypto_common", - "crypto_common_derive", - "ff", - "group", - "pairing", - "rand 0.7.3", - "serde", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "cxx" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer 0.10.3", - "crypto-common", - "subtle", -] - -[[package]] -name = "dodis_yampolskiy_prf" -version = "0.1.0" -dependencies = [ - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "pairing", - "rand 0.7.3", - "rand_core 0.5.1", - "serde", - "thiserror", -] - -[[package]] -name = "ecvrf" -version = "0.0.1" -dependencies = [ - "crypto_common", - "crypto_common_derive", - "curve25519-dalek", - "libc", - "rand 0.7.3", - "sha2 0.10.6", - "subtle", - "thiserror", - "zeroize", -] - -[[package]] -name = "ed25519" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" -dependencies = [ - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand 0.7.3", - "serde", - "sha2 0.9.9", - "zeroize", -] - -[[package]] -name = "ed25519-zebra" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" -dependencies = [ - "curve25519-dalek", - "hashbrown 0.12.3", - "hex", - "rand_core 0.6.4", - "serde", - "sha2 0.9.9", - "zeroize", -] - -[[package]] -name = "eddsa_ed25519" -version = "0.1.0" -dependencies = [ - "anyhow", - "crypto_common", - "crypto_common_derive", - "curve25519-dalek", - "ed25519-dalek", - "libc", - "rand 0.7.3", - "random_oracle", - "serde", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "either" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" - -[[package]] -name = "elgamal" -version = "0.1.0" -dependencies = [ - "anyhow", - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "libc", - "pairing", - "rand 0.7.3", - "rand_core 0.5.1", - "serde", - "thiserror", -] - -[[package]] -name = "encrypted_transfers" -version = "0.1.0" -dependencies = [ - "bulletproofs", - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "elgamal", - "ff", - "id", - "itertools", - "libc", - "pairing", - "pedersen_scheme", - "rand 0.7.3", - "random_oracle", - "serde", -] - -[[package]] -name = "ff" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4530da57967e140ee0b44e0143aa66b5cb42bd9c503dbe316a15d5b0be65713e" -dependencies = [ - "byteorder", - "ff_derive", - "rand_core 0.5.1", -] - -[[package]] -name = "ff_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5796e7d62ca01a00ed3a649b0da1ffa1ac8f06bcad40339df09dbdd69a05ba9" -dependencies = [ - "num-bigint 0.2.6", - "num-integer", - "num-traits", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "group" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbdfc48f95bef47e3daf3b9d552a1dde6311e3a5fefa43e16c59f651d56fe5b" -dependencies = [ - "ff", - "rand 0.7.3", - "rand_xorshift", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.6", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "id" -version = "0.1.0" -dependencies = [ - "anyhow", - "bulletproofs", - "byteorder", - "chrono", - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "derive_more", - "dodis_yampolskiy_prf", - "ed25519-dalek", - "either", - "elgamal", - "ff", - "hex", - "itertools", - "libc", - "pairing", - "pedersen_scheme", - "ps_sig", - "rand 0.7.3", - "rand_core 0.5.1", - "random_oracle", - "serde", - "serde_json", - "sha2 0.10.6", - "thiserror", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" - -[[package]] -name = "js-sys" -version = "0.3.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "keccak" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" - -[[package]] -name = "link-cplusplus" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" -dependencies = [ - "cc", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint 0.4.3", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-bigint 0.4.3", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" -dependencies = [ - "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "once_cell" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "pairing" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c40534479a28199cd5109da27fe2fc4a4728e4fc701d9e9c1bded78f3271e4" -dependencies = [ - "byteorder", - "ff", - "group", - "rand_core 0.5.1", -] - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest 0.10.6", - "hmac", - "password-hash", - "sha2 0.10.6", -] - -[[package]] -name = "pedersen_scheme" -version = "0.1.0" -dependencies = [ - "byteorder", - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "pairing", - "rand 0.7.3", - "rand_core 0.5.1", - "serde", - "thiserror", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" -dependencies = [ - "once_cell", - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro2" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ps_sig" -version = "0.1.0" -dependencies = [ - "anyhow", - "byteorder", - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "ff", - "libc", - "pairing", - "pedersen_scheme", - "rand 0.7.3", - "rand_core 0.5.1", - "serde", - "thiserror", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.8", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_xorshift" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "random_oracle" -version = "0.1.0" -dependencies = [ - "crypto_common", - "crypto_common_derive", - "curve_arithmetic", - "rand 0.7.3", - "sha3", -] - -[[package]] -name = "rayon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" -dependencies = [ - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "rend" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "rkyv" -version = "0.7.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" -dependencies = [ - "bytecheck", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rust_decimal" -version = "1.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9" -dependencies = [ - "arrayvec", - "borsh", - "bytecheck", - "byteorder", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "secp256k1" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" -dependencies = [ - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" -dependencies = [ - "cc", -] - -[[package]] -name = "semver" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" - -[[package]] -name = "serde" -version = "1.0.148" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.148" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.6", -] - -[[package]] -name = "sha3" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" -dependencies = [ - "digest 0.10.6", - "keccak", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - -[[package]] -name = "slab" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" -dependencies = [ - "autocfg", -] - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "toml" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" -dependencies = [ - "serde", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "unicode-ident" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "zeroize" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] From 8159597ad58127255a44814795013103968c4039 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 15:43:20 +0100 Subject: [PATCH 086/208] Remove redundant into and from calls, and other minor changes --- contract-testing/Cargo.toml | 4 +-- contract-testing/src/impls.rs | 42 +++++++++++------------- contract-testing/src/invocation/impls.rs | 4 +-- contract-testing/src/lib.rs | 4 +-- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 0810c481..984e99f7 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-smart-contract-testing" -version = "0.1.0" +version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,4 +12,4 @@ concordium-wasm = {path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" -num-bigint = "0.4.3" +num-bigint = "0.4" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 33bbd405..9dc63ae4 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -106,7 +106,7 @@ impl Chain { account.balance.total -= transaction_fee; // Save the module - let module_reference: ModuleReference = wasm_module.get_module_ref().into(); + let module_reference: ModuleReference = wasm_module.get_module_ref(); // Ensure module hasn't been deployed before. if self.modules.contains_key(&module_reference) { return Err(DeployModuleError::DuplicateModule(module_reference)); @@ -158,7 +158,7 @@ impl Chain { // Load file let file_contents = std::fs::read(module_path)?; let mut cursor = std::io::Cursor::new(file_contents); - let wasm_module: WasmModule = common::Deserial::deserial(&mut cursor)?; + let wasm_module: WasmModule = common::from_bytes(&mut cursor)?; if wasm_module.version != WasmVersion::V1 { return Err(DeployModuleError::UnsupportedModuleVersion( @@ -208,9 +208,9 @@ impl Chain { let check_header_cost = { let payload = transactions::InitContractPayload { amount, - mod_ref: module_reference.into(), + mod_ref: module_reference, init_name: contract_name.to_owned(), - param: parameter.clone().into(), + param: parameter.clone(), }; let pre_account_trx = transactions::construct::init_contract( account_info.signature_count, @@ -238,9 +238,6 @@ impl Chain { // Charge the cost for looking up the module. remaining_energy = remaining_energy.saturating_sub(lookup_cost); - // TODO: Should we return another kind of error than `ExecutionError` if we run - // out of energy before invoking? - // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { @@ -269,7 +266,7 @@ impl Chain { logs, return_value: _, /* Ignore return value for now, since our tools do not support * it for inits, currently. */ - remaining_energy, + mut remaining_energy, mut state, }) => { let contract_address = self.create_contract_address(); @@ -277,8 +274,6 @@ impl Chain { let persisted_state = state.freeze(&mut loader, &mut collector); - let mut remaining_energy = InterpreterEnergy::from(remaining_energy); - // Charge one energy per stored state byte. let energy_for_state_storage = to_interpreter_energy(Energy::from(collector.collect())); @@ -343,9 +338,8 @@ impl Chain { remaining_energy, }) => { let energy_reserved = to_interpreter_energy(energy_reserved); - let energy_used = from_interpreter_energy( - energy_reserved.saturating_sub(InterpreterEnergy::from(remaining_energy)), - ); + let energy_used = + from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractInitError::ExecutionError { @@ -364,9 +358,8 @@ impl Chain { remaining_energy, }) => { let energy_reserved = to_interpreter_energy(energy_reserved); - let energy_used = from_interpreter_energy( - energy_reserved.saturating_sub(InterpreterEnergy::from(remaining_energy)), - ); + let energy_used = + from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); let transaction_fee = self.calculate_energy_cost(energy_used); ( Err(ContractInitError::ExecutionError { @@ -460,7 +453,7 @@ impl Chain { amount, address: contract_address, receive_name, - message: parameter.clone().into(), + message: parameter.clone(), }; let pre_account_trx = transactions::construct::update_contract( @@ -479,10 +472,9 @@ impl Chain { // Charge the header cost. remaining_energy = remaining_energy.saturating_sub(check_header_cost); - // TODO: Should chain events be part of the changeset? let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( - &self, + self, invoker, sender, contract_address, @@ -578,7 +570,7 @@ impl Chain { let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( - &self, + self, invoker, sender, contract_address, @@ -626,9 +618,13 @@ impl Chain { /// Create an account. /// - /// Will override existing account if already present. - pub fn create_account(&mut self, account: AccountAddress, account_info: Account) { - self.accounts.insert(account, account_info); + /// Will override an existing account and return it. + pub fn create_account( + &mut self, + account: AccountAddress, + account_info: Account, + ) -> Option { + self.accounts.insert(account, account_info) } /// Create a contract address. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 05f9eb42..10f9ad8a 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1225,9 +1225,7 @@ impl<'a> InvocationData<'a> { self.chain_events.push(resume_event); let resume_res = run_interpreter( - from_interpreter_energy(InterpreterEnergy::from( - res.remaining_energy, - )), + from_interpreter_energy(res.remaining_energy), |energy| { v1::resume_receive( config, diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index ac2871c7..5492c09b 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -1,7 +1,7 @@ //! # Concordium Smart Contract Testing //! -//! This library supports integration testing of Concordium smart contracts -//! written in Rust. +//! This library supports writing integration tests in Rust for Concordium smart +//! contracts. //! //! ## Basic usage //! From 7bfedf198ce17934f5a16399361dbf69b270fac2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 10 Mar 2023 15:49:47 +0100 Subject: [PATCH 087/208] Remove redundant Option --- contract-testing/src/impls.rs | 2 +- contract-testing/src/invocation/impls.rs | 14 +++++++------- contract-testing/src/invocation/types.rs | 5 +++-- contract-testing/tests/queries.rs | 5 ++++- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 9dc63ae4..91a41b70 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -740,7 +740,7 @@ impl Chain { return_value: data.unwrap_or_default(), state_changed, new_balance, - logs: update_aux_response.logs.unwrap_or_default(), + logs: update_aux_response.logs, }); (result, transaction_fee) } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 10f9ad8a..b4654f01 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -111,7 +111,7 @@ impl EntrypointInvocationHandler { // TODO: Should we charge any energy for this? return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind }, - logs: None, + logs: v0::Logs::new(), remaining_energy: to_interpreter_energy(remaining_energy), }; } @@ -127,7 +127,7 @@ impl EntrypointInvocationHandler { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, - logs: None, + logs: v0::Logs::new(), remaining_energy: to_interpreter_energy(remaining_energy), }; } @@ -167,7 +167,7 @@ impl EntrypointInvocationHandler { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, - logs: None, + logs: v0::Logs::new(), remaining_energy: to_interpreter_energy(remaining_energy), }; } @@ -254,7 +254,7 @@ impl EntrypointInvocationHandler { new_balance: self.contract_balance_unchecked(contract_address), data: Some(return_value), }, - logs: Some(logs), + logs, remaining_energy, } } @@ -274,7 +274,7 @@ impl EntrypointInvocationHandler { data: return_value, }, }, - logs: None, + logs: v0::Logs::new(), remaining_energy, } } @@ -287,7 +287,7 @@ impl EntrypointInvocationHandler { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, - logs: None, + logs: v0::Logs::new(), remaining_energy, } } @@ -297,7 +297,7 @@ impl EntrypointInvocationHandler { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, - logs: None, + logs: v0::Logs::new(), remaining_energy, } } diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 64c8d9a4..f348da78 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -14,8 +14,9 @@ use std::collections::BTreeMap; pub(crate) struct InvokeEntrypointResult { /// The result from the invoke. pub(crate) invoke_response: InvokeResponse, - /// Will be `Some` if and only if `invoke_response` is `Success`. - pub(crate) logs: Option, + /// Logs created during the invocation. + /// Has entries if and only if `invoke_response` is `Success`. + pub(crate) logs: v0::Logs, /// The remaining energy after the invocation. pub(crate) remaining_energy: InterpreterEnergy, } diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 3f58eb40..6f04d060 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -82,7 +82,10 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) // TODO: Add wasm files to the repo for tests. + .module_deploy_wasm_v1( + ACC_0, + format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + ) .expect("Deploying valid module should work"); let res_init = chain From 7ab1f63d7b1b0a5dddc4278faa1d3633bfbcc090 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 14 Mar 2023 14:04:51 +0100 Subject: [PATCH 088/208] Charge invoker for all types of invocation errors --- contract-testing/src/impls.rs | 392 +++++++++++++---------- contract-testing/src/invocation/impls.rs | 2 +- contract-testing/src/invocation/mod.rs | 2 +- contract-testing/src/invocation/types.rs | 4 - contract-testing/src/types.rs | 25 +- contract-testing/tests/basics.rs | 96 +++--- contract-testing/tests/fallback.rs | 26 +- contract-testing/tests/upgrades.rs | 88 ++--- 8 files changed, 355 insertions(+), 280 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 91a41b70..a95c8110 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,5 +1,6 @@ +use crate::{constants, invocation::EntrypointInvocationHandler, types::*}; use concordium_base::{ - base::{self, Energy}, + base::{self, Energy, OutOfEnergy}, common::{self, to_bytes}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, @@ -13,12 +14,6 @@ use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; use num_bigint::BigUint; use std::{collections::BTreeMap, path::Path, sync::Arc}; -use crate::{ - constants, - invocation::{EntrypointInvocationHandler, InvokeEntrypointResult}, - types::*, -}; - impl Default for Chain { fn default() -> Self { Self::new() } } @@ -389,6 +384,104 @@ impl Chain { res } + /// Helper method that handles contract invocation. + /// + /// *Preconditions:* + /// - `invoker` exists. + /// - `sender` exists. + /// - `invoker`s balance is >= `amount`. + fn contract_invocation_worker( + &mut self, + invoker: AccountAddress, + sender: Address, + contract_address: ContractAddress, + entrypoint: EntrypointName, + parameter: OwnedParameter, + amount: Amount, + energy_reserved: Energy, + remaining_energy: &mut Energy, + should_persist: bool, + ) -> Result { + // Ensure that the parameter has a valid size (+2 for the length of parameter). + if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { + return Err(self.from_invocation_error_kind( + ContractInvocationErrorKind::ParameterTooLarge, + energy_reserved, + *remaining_energy, + )); + } + + let (result, changeset, chain_events) = + EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( + self, + invoker, + sender, + contract_address, + entrypoint.to_owned(), + parameter.as_parameter(), + amount, + *remaining_energy, + ); + + // Update the remaining energy. TODO: Should we tick here instead? + *remaining_energy = from_interpreter_energy(result.remaining_energy); + + // Get the energy to be charged for extra state bytes. Or return an error if out + // of energy. + let state_changed = if result.is_success() { + let res = if should_persist { + changeset.persist( + *remaining_energy, + contract_address, + &mut self.accounts, + &mut self.contracts, + ) + } else { + changeset.collect_energy_for_state(*remaining_energy, contract_address) + }; + + let (energy_for_state_increase, state_changed) = res.map_err(|error| { + self.from_invocation_error_kind(error.into(), energy_reserved, *remaining_energy) + })?; + // Charge for the potential state size increase. + remaining_energy + .tick_energy(energy_for_state_increase) + .map_err(|error| { + self.from_invocation_error_kind( + error.into(), + energy_reserved, + *remaining_energy, + ) + })?; + + state_changed + } else { + // An error occured, so state hasn't changed. + false + }; + + match result.invoke_response { + v1::InvokeResponse::Success { new_balance, data } => { + let energy_used = energy_reserved - *remaining_energy; + let transaction_fee = self.calculate_energy_cost(energy_used); + Ok(ContractInvocationSuccess { + chain_events, + energy_used, + transaction_fee, + return_value: data.unwrap_or_default(), + state_changed, + new_balance, + logs: result.logs, + }) + } + v1::InvokeResponse::Failure { kind } => Err(self.from_invocation_error_kind( + ContractInvocationErrorKind::ExecutionError { failure_kind: kind }, + energy_reserved, + *remaining_energy, + )), + } + } + /// Update a contract by calling one of its entrypoints. /// /// If successful, all changes will be saved. @@ -410,45 +503,48 @@ impl Chain { parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, - ) -> Result { + ) -> Result { + // Ensure the invoker exists. + if !self.account_exists(invoker) { + return Err(ContractInvocationError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvocationErrorKind::InvokerDoesNotExist(AccountMissing( + invoker, + )), + }); + } // Ensure the sender exists. if !self.address_exists(sender) { - return Err(ContractUpdateError::SenderDoesNotExist(sender)); + // TODO: Should we charge the header cost if the invoker exists but the sender + // doesn't? + return Err(ContractInvocationError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvocationErrorKind::SenderDoesNotExist(sender), + }); } - let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); - // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.account_mut(invoker)?; - let invoker_signature_count = account_info.signature_count; - if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { - return Err(ContractUpdateError::InsufficientFunds); - } - - // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { - return Err(ContractUpdateError::ParameterTooLarge); - } - - // Charge account for the reserved energy up front. This is to ensure that - // contract queries for the invoker balance are correct. - account_info.balance.total -= invoker_amount_reserved_for_nrg; - - let mut remaining_energy = energy_reserved; + // Get the signature count. TODO: Add as parameter instead? + let invoker_signature_count = self + .account_mut(invoker) + .expect("existence already checked") + .signature_count; // Compute the base cost for checking the transaction header. let check_header_cost = { let receive_name = { - let contract_name = self - .contracts - .get(&contract_address) - .ok_or(ContractUpdateError::InstanceDoesNotExist( - ContractInstanceMissing(contract_address), - ))? - .contract_name - .as_contract_name(); + let contract_name = if let Some(contract) = self.contracts.get(&contract_address) { + contract.contract_name.as_contract_name() + } else { + // Contract does not exist, but we should not just throw an error. + // We still need to charge the invoker account. + // Use the empty name for calculating the size of the transaction. + // TODO: Check existence of contract in worker. + ContractName::new_unchecked("") + }; OwnedReceiveName::construct_unchecked(contract_name, entrypoint) }; - let payload = transactions::UpdateContractPayload { amount, address: contract_address, @@ -470,57 +566,63 @@ impl Chain { }; // Charge the header cost. - remaining_energy = remaining_energy.saturating_sub(check_header_cost); + let mut remaining_energy = + energy_reserved + .checked_sub(check_header_cost) + .ok_or(ContractInvocationError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + // TODO: Consider adding a string or different kind for this type of OOE. + kind: ContractInvocationErrorKind::OutOfEnergy, + })?; - let (result, changeset, chain_events) = - EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( - self, - invoker, - sender, - contract_address, - entrypoint.to_owned(), - parameter.as_parameter(), - amount, - remaining_energy, - ); + let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + let account_info = self + .account_mut(invoker) + .expect("existence already checked"); - // Get the energy to be charged for extra state bytes. Or return an error if out - // of energy. - let (energy_for_state_increase, state_changed) = if result.is_success() { - match changeset.persist( - from_interpreter_energy(result.remaining_energy), - contract_address, - &mut self.accounts, - &mut self.contracts, - ) { - Ok(energy) => energy, - Err(_) => { - return Err(ContractUpdateError::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }); - } - } - } else { - // An error occured, so no energy should be charged for storing the state. - (Energy::from(0), false) - }; + // Ensure the account has sufficient funds to pay for the energy and amount. + if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { + let energy_used = energy_reserved - remaining_energy; + return Err(ContractInvocationError { + energy_used, + transaction_fee: self.calculate_energy_cost(energy_used), + kind: ContractInvocationErrorKind::InsufficientFunds, + }); + } - let (res, transaction_fee) = self.convert_invoke_entrypoint_result( - result, - chain_events, + // Charge account for the reserved energy up front. This is to ensure that + // contract queries for the invoker balance are correct. + // The `amount` is handled in contract_invocation_worker. + // + // *It is vital that we do not return early before returning the amount for + // remaining energy.* + account_info.balance.total -= invoker_amount_reserved_for_nrg; + + let res = self.contract_invocation_worker( + invoker, + sender, + contract_address, + entrypoint, + parameter, + amount, energy_reserved, - energy_for_state_increase, - state_changed, + &mut remaining_energy, + true, ); + let transaction_fee = match &res { + Ok(s) => s.transaction_fee, + Err(e) => e.transaction_fee, + }; // The `invoker` was charged for all the `reserved_energy` up front. // Here, we return the amount for any remaining energy. let return_amount = invoker_amount_reserved_for_nrg - transaction_fee; self.account_mut(invoker) - .expect("Known to exist") + .expect("existence already checked") .balance .total += return_amount; + res } @@ -546,64 +648,57 @@ impl Chain { parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, - ) -> Result { + ) -> Result { + // Ensure the invoker exists. + if !self.account_exists(invoker) { + return Err(ContractInvocationError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvocationErrorKind::InvokerDoesNotExist(AccountMissing( + invoker, + )), + }); + } // Ensure the sender exists. if !self.address_exists(sender) { - return Err(ContractUpdateError::SenderDoesNotExist(sender)); + return Err(ContractInvocationError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvocationErrorKind::SenderDoesNotExist(sender), + }); } let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); // Ensure account exists and can pay for the reserved energy and amount - let account_info = self.account_mut(invoker)?; - if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { - return Err(ContractUpdateError::InsufficientFunds); - } + let account_info = self + .account_mut(invoker) + .expect("existence already checked"); - // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { - return Err(ContractUpdateError::ParameterTooLarge); + if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { + let energy_used = Energy::from(0); + return Err(ContractInvocationError { + energy_used, + transaction_fee: self.calculate_energy_cost(energy_used), + kind: ContractInvocationErrorKind::InsufficientFunds, + }); } // Charge account for the reserved energy up front. This is to ensure that // contract queries for the invoker balance are correct. account_info.balance.total -= invoker_amount_reserved_for_nrg; - let (result, changeset, chain_events) = - EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( - self, - invoker, - sender, - contract_address, - entrypoint.to_owned(), - parameter.as_parameter(), - amount, - energy_reserved, - ); - - let (energy_for_state_increase, state_changed) = if result.is_success() { - match changeset.collect_energy_for_state( - from_interpreter_energy(result.remaining_energy), - contract_address, - ) { - Ok(energy) => energy, - Err(_) => { - return Err(ContractUpdateError::OutOfEnergy { - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }); - } - } - } else { - // An error occured, so no energy should be charged for storing state. - (Energy::from(0), false) - }; + let mut remaining_energy = energy_reserved; - let (result, _) = self.convert_invoke_entrypoint_result( - result, - chain_events, + let result = self.contract_invocation_worker( + invoker, + sender, + contract_address, + entrypoint, + parameter, + amount, energy_reserved, - energy_for_state_increase, - state_changed, + &mut remaining_energy, + false, ); // Return the amount charged for the reserved energy, as this is not a @@ -707,53 +802,18 @@ impl Chain { } } - /// Convert the [`InvokeEntrypointResult`] to a contract success or error. - /// - /// The `energy_for_state_increase` is only used if the result was a - /// success. - /// - /// The `state_changed` should refer to whether the state of the top-level - /// contract invoked has changed. - /// - /// *Preconditions*: - /// - `energy_reserved - remaining_energy + energy_for_state_increase >= 0` - fn convert_invoke_entrypoint_result( + fn from_invocation_error_kind( &self, - update_aux_response: InvokeEntrypointResult, - chain_events: Vec, + kind: ContractInvocationErrorKind, energy_reserved: Energy, - energy_for_state_increase: Energy, - state_changed: bool, - ) -> ( - Result, - Amount, - ) { - let remaining_energy = from_interpreter_energy(update_aux_response.remaining_energy); - match update_aux_response.invoke_response { - v1::InvokeResponse::Success { new_balance, data } => { - let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; - let transaction_fee = self.calculate_energy_cost(energy_used); - let result = Ok(SuccessfulContractUpdate { - chain_events, - energy_used, - transaction_fee, - return_value: data.unwrap_or_default(), - state_changed, - new_balance, - logs: update_aux_response.logs, - }); - (result, transaction_fee) - } - v1::InvokeResponse::Failure { kind } => { - let energy_used = energy_reserved - remaining_energy + energy_for_state_increase; - let transaction_fee = self.calculate_energy_cost(energy_used); - let result = Err(ContractUpdateError::ExecutionError { - failure_kind: kind, - energy_used, - transaction_fee, - }); - (result, transaction_fee) - } + remaining_energy: Energy, + ) -> ContractInvocationError { + let energy_used = energy_reserved - remaining_energy; + let transaction_fee = self.calculate_energy_cost(energy_used); + ContractInvocationError { + energy_used, + transaction_fee, + kind, } } @@ -837,7 +897,7 @@ impl Account { } } -impl SuccessfulContractUpdate { +impl ContractInvocationSuccess { /// Get a list of all transfers that were made from contracts to accounts. pub fn transfers(&self) -> Vec { self.chain_events @@ -883,6 +943,10 @@ impl ChainEvent { } } +impl From for ContractInvocationErrorKind { + fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } +} + /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. pub(crate) fn to_interpreter_energy(energy: Energy) -> InterpreterEnergy { InterpreterEnergy::from(energy.energy * 1000) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b4654f01..94772739 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -8,7 +8,7 @@ use crate::{ }, }; use concordium_base::{ - base::Energy, + base::{Energy, OutOfEnergy}, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ModuleReference, OwnedEntrypointName, OwnedReceiveName, Parameter, diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index 1b9aa7d3..c4b466fa 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -14,4 +14,4 @@ mod impls; mod types; -pub(crate) use types::{EntrypointInvocationHandler, InvokeEntrypointResult}; +pub(crate) use types::EntrypointInvocationHandler; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index f348da78..1e4c1010 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -113,7 +113,3 @@ pub(super) enum AmountDelta { /// An underflow occurred. #[derive(Debug)] pub(super) struct UnderflowError; - -/// The contract ran out of energy during execution of a contract entrypoint. -#[derive(PartialEq, Eq, Debug)] -pub(crate) struct OutOfEnergy; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index ae5a1ca6..c519a038 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -231,7 +231,7 @@ pub enum InitFailure { /// Represents a successful contract update (or invocation). // TODO: Consider adding function to aggregate all logs from the host_events. #[derive(Debug)] -pub struct SuccessfulContractUpdate { +pub struct ContractInvocationSuccess { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. pub chain_events: Vec, @@ -249,6 +249,13 @@ pub struct SuccessfulContractUpdate { pub logs: v0::Logs, } +#[derive(Debug)] +pub struct ContractInvocationError { + pub energy_used: Energy, + pub transaction_fee: Amount, + pub kind: ContractInvocationErrorKind, +} + /// Errors that can occur during a [`Chain::contract_update]` or /// [`Chain::contract_invoke`] call. /// @@ -258,19 +265,12 @@ pub struct SuccessfulContractUpdate { /// - The rest represent incorrect usage of the function, where some /// precondition wasn't met. #[derive(Debug, Error)] -pub enum ContractUpdateError { +pub enum ContractInvocationErrorKind { /// Update failed for a reason that also exists on the chain. #[error("failed during execution")] - ExecutionError { - failure_kind: v1::InvokeFailure, - energy_used: Energy, - transaction_fee: Amount, - }, + ExecutionError { failure_kind: v1::InvokeFailure }, #[error("ran out of energy")] - OutOfEnergy { - energy_used: Energy, - transaction_fee: Amount, - }, + OutOfEnergy, /// Module has not been deployed in test environment. #[error("module {:?} does not exist", 0.0)] ModuleDoesNotExist(#[from] ModuleMissing), @@ -286,7 +286,8 @@ pub enum ContractUpdateError { /// The sender does not exist in the test environment. #[error("sender {0} does not exist")] SenderDoesNotExist(Address), - /// The invoker account does not have enough funds to pay for the energy. + /// The invoker account does not have enough funds to pay for the energy and + /// amount sent. #[error("invoker does not have enough funds to pay for the energy")] InsufficientFunds, /// The parameter is too large. diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 4b320fd7..bbd67f65 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -185,58 +185,66 @@ fn updating_and_invoking_with_missing_sender_fails() { ) .expect("Initializing valid contract should work"); - let res_update_acc = chain.contract_update( - ACC_0, - missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); + let res_update_acc = chain + .contract_update( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect_err("should fail"); - let res_invoke_acc = chain.contract_invoke( - ACC_0, - missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); + let res_invoke_acc = chain + .contract_invoke( + ACC_0, + missing_account, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect_err("should fail"); - let res_update_contr = chain.contract_update( - ACC_0, - missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); + let res_update_contr = chain + .contract_update( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect_err("should fail"); - let res_invoke_contr = chain.contract_invoke( - ACC_0, - missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); + let res_invoke_contr = chain + .contract_invoke( + ACC_0, + missing_contract, + res_init.contract_address, + EntrypointName::new_unchecked("get"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect_err("should fail"); assert!(matches!( - res_update_acc, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + res_update_acc.kind, + ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); assert!(matches!( - res_invoke_acc, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_account)); + res_invoke_acc.kind, + ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); assert!(matches!( - res_update_contr, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); + res_update_contr.kind, + ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); assert!(matches!( - res_invoke_contr, - Err(ContractUpdateError::SenderDoesNotExist(addr)) if addr == missing_contract)); + res_invoke_contr.kind, + ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); } #[test] diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index eb0a7ac7..fc926182 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -42,20 +42,22 @@ fn test_fallback() { // Invoke the fallback directly. This should fail with execution failure/trap // because it will redirect to "two." which does not exist. Hence this will fail // and the fallback will try to look up a non-existing return value. - let res_invoke_1 = chain.contract_invoke( - ACC_0, - Address::Account(ACC_0), - res_init_one.contract_address, - EntrypointName::new_unchecked(""), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ); - match res_invoke_1 { - Err(ContractUpdateError::ExecutionError { + let res_invoke_1 = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + res_init_one.contract_address, + EntrypointName::new_unchecked(""), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(10000), + ) + .expect_err("should fail"); + match res_invoke_1.kind { + ContractInvocationErrorKind::ExecutionError { failure_kind: InvokeFailure::RuntimeError, .. - }) => (), + } => (), _ => panic!("Test failed, expected a runtime error."), } diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index f05310e0..9622b295 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -42,7 +42,8 @@ fn test() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("bump"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -55,7 +56,8 @@ fn test() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("newfun"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -111,7 +113,8 @@ fn test_self_invoke() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -222,7 +225,8 @@ fn test_missing_contract() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), Amount::zero(), Energy::from(100000), ) @@ -395,29 +399,34 @@ fn test_reject() { ) .expect("Initializing valid contract should work"); - let res_update_upgrade = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), - Amount::zero(), - Energy::from(1000000), - ); - - let res_update_new_feature = chain.contract_update( - ACC_0, - Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(1000000), - ); + let res_update_upgrade = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("upgrade"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + Amount::zero(), + Energy::from(1000000), + ) + .expect_err("should fail"); + + let res_update_new_feature = chain + .contract_update( + ACC_0, + Address::Account(ACC_0), + res_init.contract_address, + EntrypointName::new_unchecked("new_feature"), + OwnedParameter::empty(), + Amount::zero(), + Energy::from(1000000), + ) + .expect_err("should fail"); // Check the return value manually returned by the contract. - match res_update_upgrade { - Err(ContractUpdateError::ExecutionError { failure_kind, .. }) => match failure_kind { + match res_update_upgrade.kind { + ContractInvocationErrorKind::ExecutionError { failure_kind, .. } => match failure_kind { InvokeFailure::ContractReject { code, .. } if code == -1 => (), _ => panic!("Expected ContractReject with code == -1"), }, @@ -427,12 +436,10 @@ fn test_reject() { // Assert that the new_feature entrypoint doesn't exist since the upgrade // failed. assert!(matches!( - res_update_new_feature, - Err(ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, - }) + res_update_new_feature.kind, + ContractInvocationErrorKind::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, + } )); } @@ -500,7 +507,8 @@ fn test_changing_entrypoint() { Address::Account(ACC_0), res_init.contract_address, EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference).expect("Parameter has valid size"), + OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), Amount::zero(), Energy::from(1000000), ) @@ -534,11 +542,9 @@ fn test_changing_entrypoint() { ChainEvent::Updated { .. } ])); assert!(matches!( - res_update_new_feature_0, - ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, + res_update_new_feature_0.kind, + ContractInvocationErrorKind::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, } )); assert!(matches!(res_update_upgrade.chain_events[..], [ @@ -548,11 +554,9 @@ fn test_changing_entrypoint() { ChainEvent::Updated { .. }, ])); assert!(matches!( - res_update_old_feature_1, - ContractUpdateError::ExecutionError { - failure_kind: InvokeFailure::NonExistentEntrypoint, - energy_used: _, - transaction_fee: _, + res_update_old_feature_1.kind, + ContractInvocationErrorKind::ExecutionError { + failure_kind: InvokeFailure::NonExistentEntrypoint, } )); assert!(matches!(res_update_new_feature_1.chain_events[..], [ From 5f049c700ac743e2619674d26734c15d42bf2481 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Mar 2023 10:14:59 +0100 Subject: [PATCH 089/208] Always charge account for energy used in contract init --- contract-testing/src/impls.rs | 280 ++++++++++++++++++------------- contract-testing/src/types.rs | 44 +++-- contract-testing/tests/basics.rs | 14 +- 3 files changed, 201 insertions(+), 137 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index a95c8110..7caf239d 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -184,21 +184,74 @@ impl Chain { parameter: OwnedParameter, amount: Amount, energy_reserved: Energy, - ) -> Result { + ) -> Result { + let mut remaining_energy = energy_reserved; + if !self.account_exists(sender) { + return Err(self.from_init_error_kind( + ContractInitErrorKind::SenderDoesNotExist(AccountMissing(sender)), + energy_reserved, + remaining_energy, + )); + } + + let res = self.contract_init_worker( + sender, + module_reference, + contract_name, + parameter, + amount, + energy_reserved, + &mut remaining_energy, + ); + + let (res, transaction_fee) = match res { + Ok(s) => { + let transaction_fee = s.transaction_fee; + (Ok(s), transaction_fee) + } + Err(e) => { + let err = self.from_init_error_kind(e, energy_reserved, remaining_energy); + let transaction_fee = err.transaction_fee; + (Err(err), transaction_fee) + } + }; + + // Charge the account. + self.account_mut(sender) + .expect("existence already checked") + .balance + .total -= transaction_fee; + res + } + + /// Helper method for initializing contracts, which does most of the actual + /// work. + /// + /// The main reason for splitting init in two is to have this method return + /// early if it runs out of energy. `contract_init` will then always + /// ensure to charge the account for the energy used. + fn contract_init_worker( + &mut self, + sender: AccountAddress, + module_reference: ModuleReference, + contract_name: ContractName, + parameter: OwnedParameter, + amount: Amount, + energy_reserved: Energy, + remaining_energy: &mut Energy, + ) -> Result { // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.account(sender)?; if account_info.balance.available() < self.calculate_energy_cost(energy_reserved) + amount { - return Err(ContractInitError::InsufficientFunds); + return Err(ContractInitErrorKind::InsufficientFunds); } // Ensure that the parameter has a valid size (+2 for the length of parameter). if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { - return Err(ContractInitError::ParameterTooLarge); + return Err(ContractInitErrorKind::ParameterTooLarge); } - let mut remaining_energy = energy_reserved; - // Compute the base cost for checking the transaction header. let check_header_cost = { let payload = transactions::InitContractPayload { @@ -221,17 +274,16 @@ impl Chain { }; // Charge the header cost. - remaining_energy = remaining_energy.saturating_sub(check_header_cost); + remaining_energy.tick_energy(check_header_cost)?; // Charge the base cost for initializing a contract. - remaining_energy = - remaining_energy.saturating_sub(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST); + remaining_energy.tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; // Lookup module. let module = self.contract_module(module_reference)?; let lookup_cost = lookup_module_cost(&module); // Charge the cost for looking up the module. - remaining_energy = remaining_energy.saturating_sub(lookup_cost); + remaining_energy.tick_energy(lookup_cost)?; // Construct the context. let init_ctx = v0::InitContext { @@ -243,6 +295,8 @@ impl Chain { }; // Initialize contract let mut loader = v1::trie::Loader::new(&[][..]); + + let energy_given_to_interpreter = to_interpreter_energy(*remaining_energy); let res = v1::invoke_init( module.artifact, init_ctx, @@ -250,18 +304,18 @@ impl Chain { amount, init_name: contract_name.get_chain_name(), parameter: parameter.as_ref(), - energy: to_interpreter_energy(remaining_energy), + energy: energy_given_to_interpreter, }, false, loader, ); - // Handle the result and compute the transaction fee. - let (res, transaction_fee) = match res { + // Handle the result + match res { Ok(v1::InitResult::Success { logs, return_value: _, /* Ignore return value for now, since our tools do not support * it for inits, currently. */ - mut remaining_energy, + remaining_energy: remaining_interpreter_energy, mut state, }) => { let contract_address = self.create_contract_address(); @@ -269,119 +323,85 @@ impl Chain { let persisted_state = state.freeze(&mut loader, &mut collector); + // Perform the subtraction in the more finegrained (*1000) `InterpreterEnergy`, + // and *then* convert to `Energy`. This is how it is done in the node, and if we + // swap the operations, it can result in a small discrepancy due to rounding. + let energy_used_in_interpreter = from_interpreter_energy( + energy_given_to_interpreter.saturating_sub(remaining_interpreter_energy), + ); + remaining_energy.tick_energy(energy_used_in_interpreter)?; + // Charge one energy per stored state byte. - let energy_for_state_storage = - to_interpreter_energy(Energy::from(collector.collect())); - remaining_energy = remaining_energy.saturating_sub(energy_for_state_storage); + let energy_for_state_storage = Energy::from(collector.collect()); + remaining_energy.tick_energy(energy_for_state_storage)?; // Charge the constant cost for initializing a contract. - remaining_energy = remaining_energy.saturating_sub(to_interpreter_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - )); - - if remaining_energy.energy == 0 { - // Ran out of energy. Charge the `energy_reserved`. - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractInitError::ExecutionError { - failure_kind: InitFailure::OutOfEnergy, - energy_used: energy_reserved, - transaction_fee: self.calculate_energy_cost(energy_reserved), - }), - transaction_fee, - ) - } else { - // Perform the subtraction in the more finegrained (*1000) `InterpreterEnergy`, - // and *then* convert to `Energy`. This is how it is done in the node, and if we - // swap the operations, it can result in a small discrepancy due to rounding. - let energy_reserved = to_interpreter_energy(energy_reserved); - let energy_used = - from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); - let transaction_fee = self.calculate_energy_cost(energy_used); - - let contract_instance = Contract { - module_reference, - contract_name: contract_name.to_owned(), - state: persisted_state, - owner: sender, - self_balance: amount, - }; - - // Save the contract instance - self.contracts.insert(contract_address, contract_instance); - - // Subtract the amount from the invoker. - self.account_mut(sender) - .expect("Account known to exist") - .balance - .total -= amount; - - ( - Ok(SuccessfulContractInit { - contract_address, - logs, - energy_used, - transaction_fee, - }), - transaction_fee, - ) - } + remaining_energy + .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST)?; + + let contract_instance = Contract { + module_reference, + contract_name: contract_name.to_owned(), + state: persisted_state, + owner: sender, + self_balance: amount, + }; + + // Save the contract instance + self.contracts.insert(contract_address, contract_instance); + + // Subtract the amount from the invoker. + self.account_mut(sender) + .expect("Account known to exist") + .balance + .total -= amount; + + let energy_used = energy_reserved - *remaining_energy; + let transaction_fee = self.calculate_energy_cost(energy_used); + + Ok(ContractInitSuccess { + contract_address, + logs, + energy_used, + transaction_fee, + }) } Ok(v1::InitResult::Reject { reason, return_value, - remaining_energy, + remaining_energy: remaining_interpreter_energy, }) => { - let energy_reserved = to_interpreter_energy(energy_reserved); - let energy_used = - from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractInitError::ExecutionError { - failure_kind: InitFailure::Reject { - reason, - return_value, - }, - energy_used, - transaction_fee, - }), - transaction_fee, - ) + let energy_used_in_interpreter = from_interpreter_energy( + energy_given_to_interpreter.saturating_sub(remaining_interpreter_energy), + ); + remaining_energy.tick_energy(energy_used_in_interpreter)?; + Err(ContractInitErrorKind::ExecutionError { + failure_kind: InitFailure::Reject { + reason, + return_value, + }, + }) } Ok(v1::InitResult::Trap { error: _, // TODO: Should we forward this to the user? - remaining_energy, + remaining_energy: remaining_interpreter_energy, }) => { - let energy_reserved = to_interpreter_energy(energy_reserved); - let energy_used = - from_interpreter_energy(energy_reserved.saturating_sub(remaining_energy)); - let transaction_fee = self.calculate_energy_cost(energy_used); - ( - Err(ContractInitError::ExecutionError { - failure_kind: InitFailure::Trap, - energy_used, - transaction_fee, - }), - transaction_fee, - ) + let energy_used_in_interpreter = from_interpreter_energy( + energy_given_to_interpreter.saturating_sub(remaining_interpreter_energy), + ); + remaining_energy.tick_energy(energy_used_in_interpreter)?; + Err(ContractInitErrorKind::ExecutionError { + failure_kind: InitFailure::Trap, + }) } Ok(v1::InitResult::OutOfEnergy) => { - let transaction_fee = self.calculate_energy_cost(energy_reserved); - ( - Err(ContractInitError::ExecutionError { - failure_kind: InitFailure::OutOfEnergy, - energy_used: energy_reserved, - transaction_fee, - }), - transaction_fee, - ) + *remaining_energy = Energy::from(0); + Err(ContractInitErrorKind::ExecutionError { + failure_kind: InitFailure::OutOfEnergy, + }) } Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), - }; - // Charge the account. - // We have to get the account info again because of the borrow checker. - self.account_mut(sender)?.balance.total -= transaction_fee; - res + } } /// Helper method that handles contract invocation. @@ -411,6 +431,7 @@ impl Chain { )); } + let energy_given_to_interpreter = *remaining_energy; let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( self, @@ -420,11 +441,19 @@ impl Chain { entrypoint.to_owned(), parameter.as_parameter(), amount, - *remaining_energy, + energy_given_to_interpreter, ); - // Update the remaining energy. TODO: Should we tick here instead? - *remaining_energy = from_interpreter_energy(result.remaining_energy); + // Update the remaining energy. Subtract in interpreter energy, then convert to + // energy. Otherwise this might be slightly different than the node due to + // rounding. + let energy_used = from_interpreter_energy( + to_interpreter_energy(energy_given_to_interpreter) + .saturating_sub(result.remaining_energy), + ); + remaining_energy.tick_energy(energy_used).map_err(|error| { + self.from_invocation_error_kind(error.into(), energy_reserved, *remaining_energy) + })?; // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. @@ -802,6 +831,9 @@ impl Chain { } } + /// Convert a [`ContractInovcationErrorKind`] to a + /// [`ContractInvocationError`] by calculating the `energy_used` and + /// `transaction_fee`. fn from_invocation_error_kind( &self, kind: ContractInvocationErrorKind, @@ -817,6 +849,24 @@ impl Chain { } } + /// Convert a [`ContractInitErrorKind`] to a + /// [`ContractInitError`] by calculating the `energy_used` and + /// `transaction_fee`. + fn from_init_error_kind( + &self, + kind: ContractInitErrorKind, + energy_reserved: Energy, + remaining_energy: Energy, + ) -> ContractInitError { + let energy_used = energy_reserved - remaining_energy; + let transaction_fee = self.calculate_energy_cost(energy_used); + ContractInitError { + energy_used, + transaction_fee, + kind, + } + } + /// Helper function for converting [`Energy`] to [`Amount`] using the two /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { @@ -947,6 +997,10 @@ impl From for ContractInvocationErrorKind { fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } } +impl From for ContractInitErrorKind { + fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } +} + /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. pub(crate) fn to_interpreter_energy(energy: Energy) -> InterpreterEnergy { InterpreterEnergy::from(energy.energy * 1000) diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index c519a038..8e3400e3 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -174,7 +174,7 @@ pub enum DeployModuleError { /// Represents a successful initialization of a contract. #[derive(Debug)] -pub struct SuccessfulContractInit { +pub struct ContractInitSuccess { /// The address of the new instance. pub contract_address: ContractAddress, /// Logs produced during initialization. @@ -185,19 +185,30 @@ pub struct SuccessfulContractInit { pub transaction_fee: Amount, } -/// Errors that can occur while initializing a contract. +/// An error that occured in [`Chain::contract_init`]. +#[derive(Debug)] +pub struct ContractInitError { + /// Energy used. + pub energy_used: Energy, + /// The transaction fee. This is the amount charged to the `sender` + /// account. + pub transaction_fee: Amount, + /// The specific reason for why the initialization failed. + pub kind: ContractInitErrorKind, +} + +/// Types of errors that can occur in [`Chain::contract_init`]. #[derive(Debug, Error)] -pub enum ContractInitError { +pub enum ContractInitErrorKind { /// Initialization failed for a reason that also exists on the chain. #[error("failed due to a valid chain error: {:?}", 0)] ExecutionError { /// The reason for why the contract initialization failed. - failure_kind: InitFailure, - /// The energy used. - energy_used: Energy, - /// The transaction fee charged. - transaction_fee: Amount, + failure_kind: InitFailure, }, + /// Ran out of energy. + #[error("ran out of energy")] + OutOfEnergy, /// Module has not been deployed in the test environment. #[error("module {:?} does not exist", 0.0)] ModuleDoesNotExist(#[from] ModuleMissing), @@ -249,26 +260,27 @@ pub struct ContractInvocationSuccess { pub logs: v0::Logs, } +/// An error that occured during a [`Chain::contract_update`] or +/// [`Chain::contract_invoke`]. #[derive(Debug)] pub struct ContractInvocationError { + /// The energy used. pub energy_used: Energy, + /// The transaction fee. For [`Chain::contract_update`], this is the amount + /// charged to the `invoker` account. pub transaction_fee: Amount, + /// The specific reason for why the invocation failed. pub kind: ContractInvocationErrorKind, } -/// Errors that can occur during a [`Chain::contract_update]` or -/// [`Chain::contract_invoke`] call. -/// -/// There are two categories of errors here: -/// - `ExecutionError` and `OutOfEnergy` can occur if the preconditions for the -/// function is valid, and a contract is executed. -/// - The rest represent incorrect usage of the function, where some -/// precondition wasn't met. +/// The error kinds that can occur during [`Chain::contract_update`] or +/// [`Chain::contract_invoke`]. #[derive(Debug, Error)] pub enum ContractInvocationErrorKind { /// Update failed for a reason that also exists on the chain. #[error("failed during execution")] ExecutionError { failure_kind: v1::InvokeFailure }, + /// Ran out of energy. #[error("ran out of energy")] OutOfEnergy, /// Module has not been deployed in test environment. diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index bbd67f65..0d5c3680 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -79,12 +79,11 @@ fn initializing_with_invalid_parameter_fails() { ) .expect_err("Initializing with invalid params should fail"); - match res_init { + let transaction_fee = res_init.transaction_fee; + match res_init.kind { // Failed in the right way and account is still charged. - ContractInitError::ExecutionError { + ContractInitErrorKind::ExecutionError { failure_kind: InitFailure::Reject { .. }, - transaction_fee, - .. } => assert_eq!( chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) @@ -271,11 +270,10 @@ fn init_with_less_energy_than_module_lookup() { reserved_energy, ); match res_init { - Err(ContractInitError::ExecutionError { - failure_kind: InitFailure::OutOfEnergy, - energy_used, + Err(ContractInitError { + kind: ContractInitErrorKind::OutOfEnergy, .. - }) if energy_used == reserved_energy => (), + }) => (), _ => panic!("Expected to fail with out of energy."), } } From cdf48e2328eecd33f9944874905c75661d8269bb Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Mar 2023 13:46:43 +0100 Subject: [PATCH 090/208] Improve handling of energy inside invocation --- contract-testing/src/impls.rs | 1 - contract-testing/src/invocation/impls.rs | 118 +++++++++++++---------- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7caf239d..1a6a55ae 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -601,7 +601,6 @@ impl Chain { .ok_or(ContractInvocationError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - // TODO: Consider adding a string or different kind for this type of OOE. kind: ContractInvocationErrorKind::OutOfEnergy, })?; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 94772739..917b7b99 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -87,6 +87,15 @@ impl EntrypointInvocationHandler { mut remaining_energy: Energy, chain_events: &mut Vec, ) -> InvokeEntrypointResult { + // Charge the base cost for updating a contract. + remaining_energy = if let Some(remaining_energy) = + remaining_energy.checked_sub(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST) + { + remaining_energy + } else { + return InvokeEntrypointResult::new_out_of_energy_failure(); + }; + // Move the amount from the sender to the contract, if any. // And get the new self_balance. let instance_self_balance = if amount.micro_ccd() > 0 { @@ -108,7 +117,6 @@ impl EntrypointInvocationHandler { TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. - // TODO: Should we charge any energy for this? return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind }, logs: v0::Logs::new(), @@ -141,12 +149,14 @@ impl EntrypointInvocationHandler { .expect("Contract known to exist at this point"); let module = self.contract_module(contract_address); - // Charge the base cost for updating a contract. - remaining_energy = - remaining_energy.saturating_sub(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST); - // Subtract the cost of looking up the module - remaining_energy = remaining_energy.saturating_sub(lookup_module_cost(&module)); + remaining_energy = if let Some(remaining_energy) = + remaining_energy.checked_sub(lookup_module_cost(&module)) + { + remaining_energy + } else { + return InvokeEntrypointResult::new_out_of_energy_failure(); + }; // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. @@ -292,14 +302,7 @@ impl EntrypointInvocationHandler { } } v1::ReceiveResult::OutOfEnergy => { - let remaining_energy = InterpreterEnergy::from(0); - InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - logs: v0::Logs::new(), - remaining_energy, - } + InvokeEntrypointResult::new_out_of_energy_failure() } }, Err(internal_error) => { @@ -698,7 +701,7 @@ impl EntrypointInvocationHandler { /// This function ensures that the energy calculations is handled as in the /// node. fn run_interpreter( - available_energy: Energy, + mut available_energy: Energy, f: F, ) -> ExecResult> where @@ -706,12 +709,15 @@ where let available_interpreter_energy = to_interpreter_energy(available_energy); let res = f(available_interpreter_energy); if let Ok(res) = res { - let subtract_then_convert = |remaining_energy| -> u64 { + let mut subtract_then_convert = |remaining_energy| -> Result { let remaining_energy = InterpreterEnergy::from(remaining_energy); + // Using `saturating_sub` here should be ok since we should never be able to use + // more energy than what is available. let used_energy = from_interpreter_energy( available_interpreter_energy.saturating_sub(remaining_energy), ); - to_interpreter_energy(available_energy.saturating_sub(used_energy)).energy + available_energy.tick_energy(used_energy)?; + Ok(to_interpreter_energy(available_energy).energy) }; match res { v1::ReceiveResult::Success { @@ -723,7 +729,7 @@ where logs, state_changed, return_value, - remaining_energy: subtract_then_convert(remaining_energy), + remaining_energy: subtract_then_convert(remaining_energy)?, }), v1::ReceiveResult::Interrupt { @@ -733,7 +739,7 @@ where config, interrupt, } => Ok(v1::ReceiveResult::Interrupt { - remaining_energy: subtract_then_convert(remaining_energy), + remaining_energy: subtract_then_convert(remaining_energy)?, state_changed, logs, config, @@ -746,14 +752,14 @@ where } => Ok(v1::ReceiveResult::Reject { reason, return_value, - remaining_energy: subtract_then_convert(remaining_energy), + remaining_energy: subtract_then_convert(remaining_energy)?, }), v1::ReceiveResult::Trap { error, remaining_energy, } => Ok(v1::ReceiveResult::Trap { error, - remaining_energy: subtract_then_convert(remaining_energy), + remaining_energy: subtract_then_convert(remaining_energy)?, }), v1::ReceiveResult::OutOfEnergy => Ok(v1::ReceiveResult::OutOfEnergy), } @@ -818,7 +824,7 @@ impl ChangeSet { /// to balance). pub(crate) fn persist( mut self, - remaining_energy: Energy, + mut remaining_energy: Energy, invoked_contract: ContractAddress, persisted_accounts: &mut BTreeMap, persisted_contracts: &mut BTreeMap, @@ -841,13 +847,8 @@ impl ChangeSet { // One energy per extra byte of state. let energy_for_state_increase = Energy::from(collector.collect()); - // Return an error if out of energy, and clear the changeset. - if remaining_energy - .checked_sub(energy_for_state_increase) - .is_none() - { - return Err(OutOfEnergy); - } + // Return an error if out of energy. + remaining_energy.tick_energy(energy_for_state_increase)?; // Then persist all the changes. for (addr, changes) in current.contracts.iter_mut() { @@ -907,7 +908,7 @@ impl ChangeSet { /// whether the state of the provided `invoked_contract` has changed. pub(crate) fn collect_energy_for_state( mut self, - remaining_energy: Energy, + mut remaining_energy: Energy, invoked_contract: ContractAddress, ) -> Result<(Energy, bool), OutOfEnergy> { let mut invoked_contract_has_state_changes = false; @@ -925,12 +926,9 @@ impl ChangeSet { // One energy per extra byte in the state. let energy_for_state_increase = Energy::from(collector.collect()); - if remaining_energy - .checked_sub(energy_for_state_increase) - .is_none() - { - return Err(OutOfEnergy); - } + // Return an error if we run out of energy. + remaining_energy.tick_energy(energy_for_state_increase)?; + Ok(( energy_for_state_increase, invoked_contract_has_state_changes, @@ -1142,9 +1140,9 @@ impl<'a> InvocationData<'a> { let mut remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy = remaining_energy.saturating_sub( + remaining_energy.tick_energy( concordium_base::transactions::cost::SIMPLE_TRANSFER, - ); + )?; let resume_res = run_interpreter(remaining_energy, |energy| { v1::resume_receive( @@ -1248,8 +1246,8 @@ impl<'a> InvocationData<'a> { from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); // Charge a base cost. - remaining_energy = remaining_energy - .saturating_sub(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST); + remaining_energy + .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; let response = match self.invocation_handler.modules.get(&module_ref) { None => v1::InvokeResponse::Failure { @@ -1257,8 +1255,7 @@ impl<'a> InvocationData<'a> { }, Some(module) => { // Charge for the module lookup. - remaining_energy = - remaining_energy.saturating_sub(lookup_module_cost(module)); + remaining_energy.tick_energy(lookup_module_cost(module))?; if module.artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), @@ -1269,9 +1266,9 @@ impl<'a> InvocationData<'a> { .save_module_upgrade(self.address, module_ref); // Charge for the initialization cost. - remaining_energy = remaining_energy.saturating_sub( + remaining_energy.tick_energy( constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - ); + )?; let upgrade_event = ChainEvent::Upgraded { address: self.address, @@ -1329,9 +1326,9 @@ impl<'a> InvocationData<'a> { let mut remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy = remaining_energy.saturating_sub( + remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, - ); + )?; let resume_res = run_interpreter(remaining_energy, |energy| { v1::resume_receive( @@ -1363,9 +1360,9 @@ impl<'a> InvocationData<'a> { let mut remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy = remaining_energy.saturating_sub( + remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - ); + )?; let resume_res = run_interpreter(remaining_energy, |energy| { v1::resume_receive( @@ -1395,9 +1392,9 @@ impl<'a> InvocationData<'a> { let mut remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy = remaining_energy.saturating_sub( + remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, - ); + )?; let resume_res = run_interpreter(remaining_energy, |energy| { v1::resume_receive( @@ -1416,15 +1413,34 @@ impl<'a> InvocationData<'a> { } x => Ok(x), }, - Err(e) => Err(e), + Err(e) => { + if e.downcast_ref::().is_some() { + Ok(v1::ReceiveResult::OutOfEnergy) + } else { + Err(e) + } + } } } } impl InvokeEntrypointResult { + /// Whether the invoke was successful. pub(crate) fn is_success(&self) -> bool { matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) } + + /// Construct a new [`Self`] for an out of energy error. Which uses + /// `v1::InvokeFailure::RuntimeError` as the failure kind. + fn new_out_of_energy_failure() -> Self { + Self { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: v0::Logs::new(), + remaining_energy: 0.into(), + } + } } impl From for InsufficientBalanceError { From 0c3a2270c5ce26bf121ff4027690aed14fb321c4 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 15 Mar 2023 15:37:12 +0100 Subject: [PATCH 091/208] Charge for energy in partial successes of module deployment --- contract-testing/src/impls.rs | 143 +++++++++++++++++++++++++--------- contract-testing/src/types.rs | 19 ++++- 2 files changed, 123 insertions(+), 39 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 1a6a55ae..14a4c927 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -70,41 +70,105 @@ impl Chain { &mut self, sender: AccountAddress, wasm_module: WasmModule, - ) -> Result { - // Deserialize as wasm module (artifact) - let artifact = concordium_wasm::utils::instantiate_with_metering::( - &v1::ConcordiumAllowedImports { - support_upgrade: true, - }, - wasm_module.source.as_ref(), - )?; + ) -> Result { + // Ensure sender account exists. + if !self.account_exists(sender) { + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::SenderDoesNotExist(AccountMissing(sender)), + energy_used: 0.into(), + transaction_fee: Amount::zero(), + }); + } - // Calculate transaction fee of deployment - let energy = { + let check_header_energy = { // +1 for the tag, +8 for size and version let payload_size = 1 + 8 + wasm_module.source.size() + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self.account(sender)?.signature_count; - let base_cost = cost::base_cost(payload_size, number_of_sigs); - let deploy_module_cost = cost::deploy_module(wasm_module.source.size()); - base_cost + deploy_module_cost + let number_of_sigs = self + .account(sender) + .expect("existence already checked") + .signature_count; + cost::base_cost(payload_size, number_of_sigs) }; - let transaction_fee = self.calculate_energy_cost(energy); + let check_header_cost = self.calculate_energy_cost(check_header_energy); + + if self + .account(sender) + .expect("existence already checked") + .balance + .available() + < check_header_cost + { + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::InsufficientFunds, + energy_used: 0.into(), + transaction_fee: Amount::zero(), + }); + } + + // Only v1 modules are supported in this testing library. + if wasm_module.version != WasmVersion::V1 { + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::UnsupportedModuleVersion( + wasm_module.version, + ), + energy_used: 0.into(), + transaction_fee: Amount::zero(), + }); + } - // Try to subtract cost for account - let account = self.account_mut(sender)?; - if account.balance.available() < transaction_fee { - return Err(DeployModuleError::InsufficientFunds); + let account = self.account_mut(sender).expect("existence already checked"); + + // TODO: Ensure that this matches the node for both invalid and valid modules. + // to_ccd(header_cost) + to_ccd(deploy_cost) != to_ccd(header_cost + + // deploy_cost). + account.balance.total -= check_header_cost; + + // Construct the artifact. + let artifact = + match concordium_wasm::utils::instantiate_with_metering::( + &v1::ConcordiumAllowedImports { + support_upgrade: true, + }, + wasm_module.source.as_ref(), + ) { + Ok(artifact) => artifact, + Err(err) => { + return Err(ModuleDeployError { + kind: err.into(), + energy_used: check_header_energy, + transaction_fee: check_header_cost, + }) + } + }; + + // Calculate the deploy module cost. + let deploy_module_energy = cost::deploy_module(wasm_module.source.size()); + let deploy_module_cost = self.calculate_energy_cost(deploy_module_energy); + + // Subtract the cost from the account if it has sufficient funds. + let account = self.account_mut(sender).expect("existence already checked"); + if account.balance.available() < deploy_module_cost { + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::InsufficientFunds, + energy_used: check_header_energy, + transaction_fee: check_header_cost, + }); }; - account.balance.total -= transaction_fee; + account.balance.total -= deploy_module_cost; - // Save the module + // Save the module. let module_reference: ModuleReference = wasm_module.get_module_ref(); + // Ensure module hasn't been deployed before. if self.modules.contains_key(&module_reference) { - return Err(DeployModuleError::DuplicateModule(module_reference)); + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::DuplicateModule(module_reference), + energy_used: check_header_energy + deploy_module_energy, + transaction_fee: check_header_cost + deploy_module_cost, + }); } self.modules.insert(module_reference, ContractModule { size: wasm_module.source.size(), @@ -112,11 +176,12 @@ impl Chain { }); Ok(SuccessfulModuleDeployment { module_reference, - energy_used: energy, - transaction_fee, + energy_used: check_header_energy + deploy_module_energy, + transaction_fee: check_header_cost + deploy_module_cost, }) } + // TODO: Make this function return a wasm module to be used with aux instead. /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version /// bytes and 4 module length bytes. /// The module still has to a valid V1 smart contract module. @@ -128,9 +193,13 @@ impl Chain { &mut self, sender: AccountAddress, module_path: P, - ) -> Result { + ) -> Result { // Load file - let file_contents = std::fs::read(module_path)?; + let file_contents = std::fs::read(module_path).map_err(|e| ModuleDeployError { + kind: e.into(), + energy_used: 0.into(), + transaction_fee: Amount::zero(), + })?; let wasm_module = WasmModule { version: WasmVersion::V1, source: ModuleSource::from(file_contents), @@ -138,6 +207,7 @@ impl Chain { self.module_deploy_aux(sender, wasm_module) } + // TODO: Make this function return a wasm module to be used with aux instead. /// Deploy a v1 wasm module as it is output from `cargo concordium build`, /// i.e. **including** the prefix of 4 version bytes and 4 module length /// bytes. @@ -149,17 +219,20 @@ impl Chain { &mut self, sender: AccountAddress, module_path: P, - ) -> Result { + ) -> Result { // Load file - let file_contents = std::fs::read(module_path)?; + let file_contents = std::fs::read(module_path).map_err(|e| ModuleDeployError { + kind: e.into(), + energy_used: 0.into(), + transaction_fee: Amount::zero(), + })?; let mut cursor = std::io::Cursor::new(file_contents); - let wasm_module: WasmModule = common::from_bytes(&mut cursor)?; - - if wasm_module.version != WasmVersion::V1 { - return Err(DeployModuleError::UnsupportedModuleVersion( - wasm_module.version, - )); - } + let wasm_module: WasmModule = + common::from_bytes(&mut cursor).map_err(|e| ModuleDeployError { + kind: e.into(), + energy_used: 0.into(), + transaction_fee: Amount::zero(), + })?; self.module_deploy_aux(sender, wasm_module) } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 8e3400e3..4788bbec 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -146,11 +146,22 @@ pub struct SuccessfulModuleDeployment { pub transaction_fee: Amount, } -/// An error that can occur while deploying a [`ContractModule`]. -// TODO: Can we get Eq for this when using io::Error? -// TODO: Should this also have the energy used? +/// An error that occured while deploying a [`ContractModule`]. +#[derive(Debug)] +pub struct ModuleDeployError { + /// The energy used for deployment. + pub energy_used: Energy, + /// The transaction fee. This is the amount charged to the `sender` + /// account. + pub transaction_fee: Amount, + /// The specific reason for why the deployment failed. + pub kind: ModuleDeployErrorKind, +} + +/// The specific kind of error that occured while deploying a +/// [`ContractModule`]. #[derive(Debug, Error)] -pub enum DeployModuleError { +pub enum ModuleDeployErrorKind { /// Failed to read the module file. #[error("could not read the file due to: {0}")] ReadFileError(#[from] std::io::Error), From 0d00f304e810035eac629aa793ec7bd4ffd98205 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Mar 2023 10:09:48 +0100 Subject: [PATCH 092/208] Rename slot_time to block_time --- contract-testing/src/impls.rs | 14 +++++++------- contract-testing/src/invocation/impls.rs | 4 ++-- contract-testing/src/invocation/types.rs | 2 +- contract-testing/src/types.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 14a4c927..3c6299e2 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -22,12 +22,12 @@ impl Chain { /// Create a new [`Self`] where all the configurable parameters are /// provided. pub fn new_with_time_and_rates( - slot_time: SlotTime, + block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, euro_per_energy: ExchangeRate, ) -> Self { Self { - slot_time, + block_time, micro_ccd_per_euro, euro_per_energy, accounts: BTreeMap::new(), @@ -37,18 +37,18 @@ impl Chain { } } - /// Create a new [`Self`] with a specified `slot_time` where + /// Create a new [`Self`] with a specified `block_time` where /// - `micro_ccd_per_euro` defaults to `147235241 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. - pub fn new_with_time(slot_time: SlotTime) -> Self { + pub fn new_with_time(block_time: SlotTime) -> Self { Self { - slot_time, + block_time, ..Self::new() } } /// Create a new [`Self`] where - /// - `slot_time` defaults to `0`, + /// - `block_time` defaults to `0`, /// - `micro_ccd_per_euro` defaults to `147235241 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new() -> Self { @@ -361,7 +361,7 @@ impl Chain { // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { - slot_time: self.slot_time, + slot_time: self.block_time, }, init_origin: sender, sender_policies: account_info.policies.clone(), diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 917b7b99..730f74f8 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -49,7 +49,7 @@ impl EntrypointInvocationHandler { * immutable references. */ modules: chain.modules.clone(), contracts: chain.contracts.clone(), - slot_time: chain.slot_time, + block_time: chain.block_time, euro_per_energy: chain.euro_per_energy, micro_ccd_per_euro: chain.micro_ccd_per_euro, }; @@ -188,7 +188,7 @@ impl EntrypointInvocationHandler { entrypoint: entrypoint.to_owned(), common: v0::ReceiveContext { metadata: ChainMetadata { - slot_time: self.slot_time, + slot_time: self.block_time, }, invoker, self_address: contract_address, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 1e4c1010..621c4c89 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -27,7 +27,7 @@ pub(crate) struct EntrypointInvocationHandler { pub(super) accounts: BTreeMap, pub(super) modules: BTreeMap, pub(super) contracts: BTreeMap, - pub(super) slot_time: SlotTime, + pub(super) block_time: SlotTime, pub(super) euro_per_energy: ExchangeRate, pub(super) micro_ccd_per_euro: ExchangeRate, } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 4788bbec..1d1fc049 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -28,9 +28,9 @@ pub struct ContractModule { /// creating accounts, deploying modules, initializing contract, updating /// contracts and invoking contracts. pub struct Chain { - /// The slot time viewable inside the smart contracts. + /// The block time viewable inside the smart contracts. /// Defaults to `0`. - pub slot_time: SlotTime, + pub block_time: SlotTime, /// MicroCCD per Euro ratio. pub micro_ccd_per_euro: ExchangeRate, /// Euro per Energy ratio. From 6d3970d3c1fc0e7ac7e2457b7053f8673c07ea5f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Mar 2023 11:13:09 +0100 Subject: [PATCH 093/208] Split module load from module deployment This allows the user to have a setup step in their tests which loads the module only once. --- contract-testing/src/impls.rs | 75 +++++---------- contract-testing/src/invocation/types.rs | 2 +- contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 35 +++++-- .../tests/all_new_host_functions.rs | 4 +- contract-testing/tests/basics.rs | 31 ++++-- contract-testing/tests/checkpointing.rs | 8 +- contract-testing/tests/counter.rs | 2 +- contract-testing/tests/error_codes.rs | 2 +- contract-testing/tests/fallback.rs | 2 +- contract-testing/tests/iterator.rs | 2 +- contract-testing/tests/queries.rs | 77 ++++++++++----- contract-testing/tests/recorder.rs | 4 +- .../tests/relaxed_restrictions.rs | 4 +- contract-testing/tests/transfer.rs | 2 +- contract-testing/tests/upgrades.rs | 95 ++++++++++++++----- 16 files changed, 215 insertions(+), 132 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 3c6299e2..9a2cae27 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -59,14 +59,15 @@ impl Chain { ) } - /// Helper function that handles the actual logic of deploying the module - /// bytes. + /// Deploy a smart contract module. + /// + /// The `WasmModule` can be loaded from disk with either + /// [`Chain::load_module_v1`] or [`Chain::load_module_v1_raw`]. /// /// Parameters: /// - `sender`: the sender account. - /// - `module`: the raw wasm module (i.e. **without** the contract version - /// and module length bytes (8 bytes total)). - fn module_deploy_aux( + /// - `module`: the v1 wasm module. + pub fn module_deploy_v1( &mut self, sender: AccountAddress, wasm_module: WasmModule, @@ -137,7 +138,7 @@ impl Chain { Ok(artifact) => artifact, Err(err) => { return Err(ModuleDeployError { - kind: err.into(), + kind: InvalidModuleError(err).into(), energy_used: check_header_energy, transaction_fee: check_header_cost, }) @@ -181,59 +182,31 @@ impl Chain { }) } - // TODO: Make this function return a wasm module to be used with aux instead. - /// Deploy a raw wasm module, i.e. one **without** the prefix of 4 version + /// Load a raw wasm module, i.e. one **without** the prefix of 4 version /// bytes and 4 module length bytes. - /// The module still has to a valid V1 smart contract module. - /// - /// **Parameters:** - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. - pub fn module_deploy_wasm_v1>( - &mut self, - sender: AccountAddress, - module_path: P, - ) -> Result { - // Load file - let file_contents = std::fs::read(module_path).map_err(|e| ModuleDeployError { - kind: e.into(), - energy_used: 0.into(), - transaction_fee: Amount::zero(), - })?; - let wasm_module = WasmModule { + /// The module still has to be a valid V1 smart contract module. + pub fn module_load_v1_raw( + module_path: impl AsRef, + ) -> Result { + let file_contents = std::fs::read(module_path)?; + Ok(WasmModule { version: WasmVersion::V1, source: ModuleSource::from(file_contents), - }; - self.module_deploy_aux(sender, wasm_module) + }) } - // TODO: Make this function return a wasm module to be used with aux instead. - /// Deploy a v1 wasm module as it is output from `cargo concordium build`, + /// Load a v1 wasm module as it is output from `cargo concordium build`, /// i.e. **including** the prefix of 4 version bytes and 4 module length /// bytes. - /// - /// **Parameters:** - /// - `sender`: The account paying for the transaction. - /// - `module_path`: Path to a module file. - pub fn module_deploy_v1>( - &mut self, - sender: AccountAddress, - module_path: P, - ) -> Result { - // Load file - let file_contents = std::fs::read(module_path).map_err(|e| ModuleDeployError { - kind: e.into(), - energy_used: 0.into(), - transaction_fee: Amount::zero(), - })?; + pub fn module_load_v1(module_path: impl AsRef) -> Result { + let file_contents = std::fs::read(module_path)?; let mut cursor = std::io::Cursor::new(file_contents); - let wasm_module: WasmModule = - common::from_bytes(&mut cursor).map_err(|e| ModuleDeployError { - kind: e.into(), - energy_used: 0.into(), - transaction_fee: Amount::zero(), - })?; - self.module_deploy_aux(sender, wasm_module) + let module: WasmModule = + common::from_bytes(&mut cursor).map_err(|e| InvalidModuleError(e))?; + if module.version != WasmVersion::V1 { + return Err(ModuleLoadError::UnsupportedModuleVersion(module.version)); + } + Ok(module) } /// Initialize a contract. diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 621c4c89..12f617b6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -27,7 +27,7 @@ pub(crate) struct EntrypointInvocationHandler { pub(super) accounts: BTreeMap, pub(super) modules: BTreeMap, pub(super) contracts: BTreeMap, - pub(super) block_time: SlotTime, + pub(super) block_time: SlotTime, pub(super) euro_per_energy: ExchangeRate, pub(super) micro_ccd_per_euro: ExchangeRate, } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 5492c09b..a5ec3a5f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -19,7 +19,7 @@ //! //! // Deploy a smart contract module (built with [Cargo Concordium](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#cargo-concordium)). //! let deployment = chain -//! .module_deploy_v1(ACC, "path/to/contract.wasm.v1") +//! .module_deploy_v1(ACC, Chain::module_load_v1("path/to/contract.wasm.v1").unwrap()) //! .unwrap(); //! //! // Initialize a smart contract from the deployed module. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 1d1fc049..3011300e 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -162,27 +162,44 @@ pub struct ModuleDeployError { /// [`ContractModule`]. #[derive(Debug, Error)] pub enum ModuleDeployErrorKind { - /// Failed to read the module file. - #[error("could not read the file due to: {0}")] - ReadFileError(#[from] std::io::Error), /// The module provided is not valid. #[error("module is invalid due to: {0}")] - InvalidModule(#[from] anyhow::Error), - /// The invoker account does not have sufficient funds to pay for the + InvalidModule(#[from] InvalidModuleError), + /// The sender account does not have sufficient funds to pay for the /// deployment. - #[error("invoker does not have sufficient funds to pay for the energy")] + #[error("sender does not have sufficient funds to pay for the energy")] InsufficientFunds, /// The sender account deploying the module does not exist. #[error("sender account {} does not exist", 0.0)] SenderDoesNotExist(#[from] AccountMissing), - /// The module version is not supported. - #[error("wasm version {0} is not supported")] - UnsupportedModuleVersion(WasmVersion), /// The module has already been deployed. #[error("module with reference {:?} already exists", 0)] DuplicateModule(ModuleReference), + /// The module version is not supported. + #[error("wasm version {0} is not supported")] + UnsupportedModuleVersion(WasmVersion), +} + +/// An error that can occur while loading a smart contract module. +#[derive(Debug, Error)] +pub enum ModuleLoadError { + /// Failed to read the module file. + #[error("could not read the file due to: {0}")] + ReadFileError(#[from] std::io::Error), + /// The module version is not supported. + #[error("wasm version {0} is not supported")] + UnsupportedModuleVersion(WasmVersion), + /// The module provided is not valid. + #[error("module is invalid due to: {0}")] + InvalidModule(#[from] InvalidModuleError), } +/// The error produced when trying to load or deploy an invalid smart contract +/// module. +#[derive(Debug, Error)] +#[error("The module is invalid due to: {0}")] +pub struct InvalidModuleError(pub(crate) anyhow::Error); + /// Represents a successful initialization of a contract. #[derive(Debug)] pub struct ContractInitSuccess { diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 578c230a..161bc4bf 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -15,9 +15,9 @@ fn test_all_new_host_functions() { chain.create_account(ACC_0, Account::new(initial_balance)); chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), ) .expect("Deploying valid module should work"); } diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 0d5c3680..77a5bb8c 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -14,7 +14,10 @@ fn deploying_valid_module_works() { let res = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + Chain::module_load_v1( + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) + .expect("module should exist"), ) .expect("Deploying valid module should work."); @@ -34,7 +37,10 @@ fn initializing_valid_contract_works() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + Chain::module_load_v1( + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -64,7 +70,10 @@ fn initializing_with_invalid_parameter_fails() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + Chain::module_load_v1( + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -101,7 +110,10 @@ fn updating_valid_contract_works() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + Chain::module_load_v1( + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -169,7 +181,10 @@ fn updating_and_invoking_with_missing_sender_fails() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + Chain::module_load_v1( + "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", + ) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -255,7 +270,8 @@ fn init_with_less_energy_than_module_lookup() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1", + Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -287,7 +303,8 @@ fn update_with_fib_reentry_works() { let res_deploy = chain .module_deploy_v1( ACC_0, - "../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1", + Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") + .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 605210de..3d7a6391 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -27,7 +27,7 @@ fn test_case_1() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init_a = chain @@ -102,7 +102,7 @@ fn test_case_2() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init_a = chain @@ -177,7 +177,7 @@ fn test_case_3() { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init_a = chain @@ -235,7 +235,7 @@ fn test_case_4() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init_a = chain diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 88d37e9c..176c0482 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -14,7 +14,7 @@ fn test_counter() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init = chain diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 105b6fb7..8118282a 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -14,7 +14,7 @@ fn test_error_codes() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/caller.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init = chain diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index fc926182..4d333488 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -12,7 +12,7 @@ fn test_fallback() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/fallback.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init_two = chain diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 4c29ddf8..07397547 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -16,7 +16,7 @@ fn test_iterator() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/iterator.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init = chain diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 6f04d060..b0900c18 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -24,9 +24,13 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-account-balance.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -82,9 +86,13 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-account-balance.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -145,9 +153,13 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-account-balance-transfer.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -216,9 +228,13 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-account-balance.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -274,12 +290,13 @@ mod query_account_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!( + Chain::module_load_v1_raw(format!( "{}/queries-account-balance-missing-account.wasm", WASM_TEST_FOLDER - ), + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -338,9 +355,13 @@ mod query_contract_balance { let init_amount = Amount::from_ccd(123); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-contract-balance.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -398,9 +419,13 @@ mod query_contract_balance { let update_amount = Amount::from_ccd(456); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-contract-balance.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-contract-balance.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -447,12 +472,13 @@ mod query_contract_balance { let transfer_amount = Amount::from_ccd(78); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!( + Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-transfer.wasm", WASM_TEST_FOLDER - ), + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -502,12 +528,13 @@ mod query_contract_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!( + Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-missing-contract.wasm", WASM_TEST_FOLDER - ), + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -555,9 +582,13 @@ mod query_exchange_rates { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/queries-exchange-rates.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index a677e0aa..47e45cc1 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -12,9 +12,9 @@ fn test_recorder() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/record-parameters.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)).expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index aef8a39a..7873d99b 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -78,9 +78,9 @@ fn deploy_and_init() -> (Chain, ContractAddress) { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 2b89f934..c018d43d 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -13,7 +13,7 @@ fn test_transfer() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/transfer.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)).expect("module should exist")) .expect("Deploying valid module should work"); let res_init = chain diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 9622b295..4dfb1075 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -15,11 +15,19 @@ fn test() { // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); // Initialize `upgrading_0`. @@ -84,15 +92,17 @@ fn test_self_invoke() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -148,9 +158,13 @@ fn test_missing_module() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/upgrading-missing-module.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -195,16 +209,24 @@ fn test_missing_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/upgrading-missing-contract0.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/upgrading-missing-contract1.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -249,15 +271,27 @@ fn test_twice_in_one_transaction() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_deploy_2 = chain - .module_deploy_wasm_v1(ACC_0, format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init = chain @@ -324,9 +358,10 @@ fn test_chained_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -375,16 +410,18 @@ fn test_reject() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -453,16 +490,24 @@ fn test_changing_entrypoint() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/upgrading-changing-entrypoints0.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_wasm_v1( + .module_deploy_v1( ACC_0, - format!("{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER), + Chain::module_load_v1_raw(format!( + "{}/upgrading-changing-entrypoints1.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), ) .expect("Deploying valid module should work"); From aee27e6ff4ad0206a3f34fa456930497bcfb541e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Mar 2023 11:24:21 +0100 Subject: [PATCH 094/208] Improve read file error --- contract-testing/src/impls.rs | 14 ++++++++++++-- contract-testing/src/types.rs | 9 ++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 9a2cae27..e49150b1 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -188,7 +188,12 @@ impl Chain { pub fn module_load_v1_raw( module_path: impl AsRef, ) -> Result { - let file_contents = std::fs::read(module_path)?; + let module_path = module_path.as_ref(); + let file_contents = + std::fs::read(module_path).map_err(|e| ModuleLoadError::ReadFileError { + path: module_path.to_path_buf(), + error: e, + })?; Ok(WasmModule { version: WasmVersion::V1, source: ModuleSource::from(file_contents), @@ -199,7 +204,12 @@ impl Chain { /// i.e. **including** the prefix of 4 version bytes and 4 module length /// bytes. pub fn module_load_v1(module_path: impl AsRef) -> Result { - let file_contents = std::fs::read(module_path)?; + let module_path = module_path.as_ref(); + let file_contents = + std::fs::read(module_path).map_err(|e| ModuleLoadError::ReadFileError { + path: module_path.to_path_buf(), + error: e, + })?; let mut cursor = std::io::Cursor::new(file_contents); let module: WasmModule = common::from_bytes(&mut cursor).map_err(|e| InvalidModuleError(e))?; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 3011300e..9f5228f6 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; use thiserror::Error; use concordium_base::{ @@ -184,8 +184,11 @@ pub enum ModuleDeployErrorKind { #[derive(Debug, Error)] pub enum ModuleLoadError { /// Failed to read the module file. - #[error("could not read the file due to: {0}")] - ReadFileError(#[from] std::io::Error), + #[error("could not read the file '{path}' due to: {error}")] + ReadFileError { + path: PathBuf, + error: std::io::Error, + }, /// The module version is not supported. #[error("wasm version {0} is not supported")] UnsupportedModuleVersion(WasmVersion), From 2d1d61fa2f9290f327d20702d26de31c84f507c5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 16 Mar 2023 17:50:51 +0100 Subject: [PATCH 095/208] Use `InitContractPayload` and `UpdateContractPayload` --- contract-testing/src/impls.rs | 136 +++------ contract-testing/src/invocation/impls.rs | 136 +++++---- contract-testing/src/lib.rs | 23 +- contract-testing/tests/basics.rs | 154 +++++----- contract-testing/tests/checkpointing.rs | 208 +++++++------- contract-testing/tests/counter.rs | 51 ++-- contract-testing/tests/error_codes.rs | 86 +++--- contract-testing/tests/fallback.rs | 54 ++-- contract-testing/tests/iterator.rs | 40 +-- contract-testing/tests/queries.rs | 264 ++++++++--------- contract-testing/tests/recorder.rs | 39 +-- .../tests/relaxed_restrictions.rs | 49 ++-- contract-testing/tests/transfer.rs | 56 ++-- contract-testing/tests/upgrades.rs | 270 ++++++++++-------- 14 files changed, 835 insertions(+), 731 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index e49150b1..b0868e94 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -4,11 +4,10 @@ use concordium_base::{ common::{self, to_bytes}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedParameter, SlotTime, - Timestamp, + ExchangeRate, ModuleReference, SlotTime, Timestamp, }, - smart_contracts::{ModuleSource, OwnedReceiveName, WasmModule, WasmVersion}, - transactions::{self, cost}, + smart_contracts::{ModuleSource, WasmModule, WasmVersion}, + transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; use num_bigint::BigUint; @@ -224,22 +223,20 @@ impl Chain { /// **Parameters:** /// - `sender`: The account paying for the transaction. Will also become /// the owner of the instance created. - /// - `module_reference`: The reference to the a module that has already - /// been deployed. - /// - `contract_name`: Name of the contract to initialize. - /// - `parameter`: Parameter provided to the init method. - /// - `amount`: The initial balance of the contract. Subtracted from the - /// `sender` account. /// - `energy_reserved`: Amount of energy reserved for executing the init /// method. + /// - `payload`: + /// - `amount`: The initial balance of the contract. Subtracted from the + /// `sender` account. + /// - `mod_ref`: The reference to the a module that has already been + /// deployed. + /// - `init_name`: Name of the contract to initialize. + /// - `param`: Parameter provided to the init method. pub fn contract_init( &mut self, sender: AccountAddress, - module_reference: ModuleReference, - contract_name: ContractName, - parameter: OwnedParameter, - amount: Amount, energy_reserved: Energy, + payload: InitContractPayload, ) -> Result { let mut remaining_energy = energy_reserved; if !self.account_exists(sender) { @@ -250,15 +247,8 @@ impl Chain { )); } - let res = self.contract_init_worker( - sender, - module_reference, - contract_name, - parameter, - amount, - energy_reserved, - &mut remaining_energy, - ); + let res = + self.contract_init_worker(sender, energy_reserved, payload, &mut remaining_energy); let (res, transaction_fee) = match res { Ok(s) => { @@ -289,40 +279,33 @@ impl Chain { fn contract_init_worker( &mut self, sender: AccountAddress, - module_reference: ModuleReference, - contract_name: ContractName, - parameter: OwnedParameter, - amount: Amount, energy_reserved: Energy, + payload: InitContractPayload, remaining_energy: &mut Energy, ) -> Result { // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.account(sender)?; - if account_info.balance.available() < self.calculate_energy_cost(energy_reserved) + amount { + if account_info.balance.available() + < self.calculate_energy_cost(energy_reserved) + payload.amount + { return Err(ContractInitErrorKind::InsufficientFunds); } // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { + if payload.param.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { return Err(ContractInitErrorKind::ParameterTooLarge); } // Compute the base cost for checking the transaction header. let check_header_cost = { - let payload = transactions::InitContractPayload { - amount, - mod_ref: module_reference, - init_name: contract_name.to_owned(), - param: parameter.clone(), - }; let pre_account_trx = transactions::construct::init_contract( account_info.signature_count, sender, base::Nonce::from(0), // Value not matter, only used for serialized size. common::types::TransactionTime::from_seconds(0), /* Value does not matter, only * used for serialized size. */ - payload, + payload.clone(), energy_reserved, ); let transaction_size = to_bytes(&pre_account_trx).len() as u64; @@ -336,7 +319,7 @@ impl Chain { remaining_energy.tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; // Lookup module. - let module = self.contract_module(module_reference)?; + let module = self.contract_module(payload.mod_ref)?; let lookup_cost = lookup_module_cost(&module); // Charge the cost for looking up the module. remaining_energy.tick_energy(lookup_cost)?; @@ -357,10 +340,10 @@ impl Chain { module.artifact, init_ctx, v1::InitInvocation { - amount, - init_name: contract_name.get_chain_name(), - parameter: parameter.as_ref(), - energy: energy_given_to_interpreter, + amount: payload.amount, + init_name: payload.init_name.as_contract_name().get_chain_name(), + parameter: payload.param.as_ref(), + energy: energy_given_to_interpreter, }, false, loader, @@ -396,11 +379,11 @@ impl Chain { .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST)?; let contract_instance = Contract { - module_reference, - contract_name: contract_name.to_owned(), - state: persisted_state, - owner: sender, - self_balance: amount, + module_reference: payload.mod_ref, + contract_name: payload.init_name, + state: persisted_state, + owner: sender, + self_balance: payload.amount, }; // Save the contract instance @@ -410,7 +393,7 @@ impl Chain { self.account_mut(sender) .expect("Account known to exist") .balance - .total -= amount; + .total -= payload.amount; let energy_used = energy_reserved - *remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); @@ -470,34 +453,28 @@ impl Chain { &mut self, invoker: AccountAddress, sender: Address, - contract_address: ContractAddress, - entrypoint: EntrypointName, - parameter: OwnedParameter, - amount: Amount, energy_reserved: Energy, + payload: UpdateContractPayload, remaining_energy: &mut Energy, should_persist: bool, ) -> Result { // Ensure that the parameter has a valid size (+2 for the length of parameter). - if parameter.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { + if payload.message.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { return Err(self.from_invocation_error_kind( ContractInvocationErrorKind::ParameterTooLarge, energy_reserved, *remaining_energy, )); } - + let contract_address = payload.address; let energy_given_to_interpreter = *remaining_energy; let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( self, invoker, sender, - contract_address, - entrypoint.to_owned(), - parameter.as_parameter(), - amount, energy_given_to_interpreter, + payload, ); // Update the remaining energy. Subtract in interpreter energy, then convert to @@ -583,11 +560,8 @@ impl Chain { &mut self, invoker: AccountAddress, sender: Address, - contract_address: ContractAddress, - entrypoint: EntrypointName, - parameter: OwnedParameter, - amount: Amount, energy_reserved: Energy, + payload: UpdateContractPayload, ) -> Result { // Ensure the invoker exists. if !self.account_exists(invoker) { @@ -618,32 +592,13 @@ impl Chain { // Compute the base cost for checking the transaction header. let check_header_cost = { - let receive_name = { - let contract_name = if let Some(contract) = self.contracts.get(&contract_address) { - contract.contract_name.as_contract_name() - } else { - // Contract does not exist, but we should not just throw an error. - // We still need to charge the invoker account. - // Use the empty name for calculating the size of the transaction. - // TODO: Check existence of contract in worker. - ContractName::new_unchecked("") - }; - OwnedReceiveName::construct_unchecked(contract_name, entrypoint) - }; - let payload = transactions::UpdateContractPayload { - amount, - address: contract_address, - receive_name, - message: parameter.clone(), - }; - let pre_account_trx = transactions::construct::update_contract( invoker_signature_count, invoker, base::Nonce::from(0), // Value does not matter, only used for serialized size. common::types::TransactionTime::from_seconds(0), /* Value does not matter, only * used for serialized size. */ - payload, + payload.clone(), energy_reserved, ); let transaction_size = to_bytes(&pre_account_trx).len() as u64; @@ -666,7 +621,7 @@ impl Chain { .expect("existence already checked"); // Ensure the account has sufficient funds to pay for the energy and amount. - if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { + if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { let energy_used = energy_reserved - remaining_energy; return Err(ContractInvocationError { energy_used, @@ -686,11 +641,8 @@ impl Chain { let res = self.contract_invocation_worker( invoker, sender, - contract_address, - entrypoint, - parameter, - amount, energy_reserved, + payload, &mut remaining_energy, true, ); @@ -727,11 +679,8 @@ impl Chain { &mut self, invoker: AccountAddress, sender: Address, - contract_address: ContractAddress, - entrypoint: EntrypointName, - parameter: OwnedParameter, - amount: Amount, energy_reserved: Energy, + payload: UpdateContractPayload, ) -> Result { // Ensure the invoker exists. if !self.account_exists(invoker) { @@ -758,7 +707,7 @@ impl Chain { .account_mut(invoker) .expect("existence already checked"); - if account_info.balance.available() < invoker_amount_reserved_for_nrg + amount { + if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { let energy_used = Energy::from(0); return Err(ContractInvocationError { energy_used, @@ -776,11 +725,8 @@ impl Chain { let result = self.contract_invocation_worker( invoker, sender, - contract_address, - entrypoint, - parameter, - amount, energy_reserved, + payload, &mut remaining_energy, false, ); diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 730f74f8..b58765df 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -11,8 +11,10 @@ use concordium_base::{ base::{Energy, OutOfEnergy}, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ModuleReference, OwnedEntrypointName, OwnedReceiveName, Parameter, + ModuleReference, OwnedReceiveName, }, + smart_contracts::{OwnedContractName, OwnedParameter}, + transactions::UpdateContractPayload, }; use concordium_smart_contract_engine::{ v0, @@ -36,11 +38,8 @@ impl EntrypointInvocationHandler { chain: &Chain, invoker: AccountAddress, sender: Address, - contract_address: ContractAddress, - entrypoint: OwnedEntrypointName, - parameter: Parameter, - amount: Amount, reserved_energy: Energy, + payload: UpdateContractPayload, ) -> (InvokeEntrypointResult, ChangeSet, Vec) { let mut contract_invocation = Self { changeset: ChangeSet::new(), @@ -58,11 +57,8 @@ impl EntrypointInvocationHandler { let result = contract_invocation.invoke_entrypoint( invoker, sender, - contract_address, - entrypoint, - parameter, - amount, reserved_energy, + payload, &mut chain_events, ); (result, contract_invocation.changeset, chain_events) @@ -80,11 +76,8 @@ impl EntrypointInvocationHandler { &mut self, invoker: AccountAddress, sender: Address, - contract_address: ContractAddress, - entrypoint: OwnedEntrypointName, - parameter: Parameter, - amount: Amount, mut remaining_energy: Energy, + payload: UpdateContractPayload, chain_events: &mut Vec, ) -> InvokeEntrypointResult { // Charge the base cost for updating a contract. @@ -98,15 +91,17 @@ impl EntrypointInvocationHandler { // Move the amount from the sender to the contract, if any. // And get the new self_balance. - let instance_self_balance = if amount.micro_ccd() > 0 { + let instance_self_balance = if payload.amount.micro_ccd() > 0 { let transfer_result = match sender { - Address::Account(sender_account) => { - self.transfer_from_account_to_contract(amount, sender_account, contract_address) - } + Address::Account(sender_account) => self.transfer_from_account_to_contract( + payload.amount, + sender_account, + payload.address, + ), Address::Contract(sender_contract) => self.transfer_from_contract_to_contract( - amount, + payload.amount, sender_contract, - contract_address, + payload.address, ), }; match transfer_result { @@ -125,7 +120,7 @@ impl EntrypointInvocationHandler { } } } else { - match self.contract_balance(contract_address) { + match self.contract_balance(payload.address) { Some(self_balance) => self_balance, None => { // Return early. @@ -145,9 +140,9 @@ impl EntrypointInvocationHandler { // Get the instance and artifact. To be used in several places. let instance = self .contracts - .get(&contract_address) + .get(&payload.address) .expect("Contract known to exist at this point"); - let module = self.contract_module(contract_address); + let module = self.contract_module(payload.address); // Subtract the cost of looking up the module remaining_energy = if let Some(remaining_energy) = @@ -159,18 +154,31 @@ impl EntrypointInvocationHandler { }; // Construct the receive name (or fallback receive name) and ensure its presence - // in the contract. - let receive_name = { - let contract_name = instance.contract_name.as_contract_name().contract_name(); - let receive_name = format!("{}.{}", contract_name, entrypoint); + // in the contract. Also returns the contract name and entrypoint name as they + // are needed further down. + let (contract_name, receive_name, entrypoint_name) = { + let borrowed_receive_name = payload.receive_name.as_receive_name(); + let contract_name = borrowed_receive_name.contract_name(); + let owned_contract_name = + OwnedContractName::new_unchecked(format!("init_{}", contract_name)); + let owned_entrypoint_name = borrowed_receive_name.entrypoint_name().to_owned(); + let receive_name = borrowed_receive_name.get_chain_name(); let fallback_receive_name = format!("{}.", contract_name); - if module.artifact.has_entrypoint(receive_name.as_str()) { - OwnedReceiveName::new_unchecked(receive_name) + if module.artifact.has_entrypoint(receive_name) { + ( + owned_contract_name, + payload.receive_name, + owned_entrypoint_name, + ) } else if module .artifact .has_entrypoint(fallback_receive_name.as_str()) { - OwnedReceiveName::new_unchecked(fallback_receive_name) + ( + owned_contract_name, + OwnedReceiveName::new_unchecked(fallback_receive_name), + owned_entrypoint_name, + ) } else { // Return early. return InvokeEntrypointResult { @@ -185,13 +193,16 @@ impl EntrypointInvocationHandler { // Construct the receive context let receive_ctx = v1::ReceiveContext { - entrypoint: entrypoint.to_owned(), + // This should be the entrypoint specified, even if we end up + // calling the fallback entrypoint, as this will be visible to the + // contract via a host function. + entrypoint: entrypoint_name.clone(), common: v0::ReceiveContext { metadata: ChainMetadata { slot_time: self.block_time, }, invoker, - self_address: contract_address, + self_address: payload.address, self_balance: instance_self_balance, sender, owner: instance.owner, @@ -204,11 +215,9 @@ impl EntrypointInvocationHandler { }, }; - let contract_name = instance.contract_name.clone(); - // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); - let mut mutable_state = self.contract_state(contract_address); + let mut mutable_state = self.contract_state(payload.address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -218,9 +227,10 @@ impl EntrypointInvocationHandler { module.artifact, receive_ctx, v1::ReceiveInvocation { - amount, + amount: payload.amount, + // This will either be the one provided on the fallback receive name. receive_name: receive_name.as_receive_name(), - parameter: parameter.as_ref(), + parameter: payload.message.as_ref(), energy, }, instance_state, @@ -236,10 +246,10 @@ impl EntrypointInvocationHandler { // i.e. beyond interrupts. let mut data = InvocationData { invoker, - address: contract_address, + address: payload.address, contract_name, - amount, - entrypoint, + amount: payload.amount, + entrypoint: entrypoint_name, invocation_handler: self, state: mutable_state, chain_events: Vec::new(), @@ -261,7 +271,7 @@ impl EntrypointInvocationHandler { let remaining_energy = InterpreterEnergy::from(remaining_energy); InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Success { - new_balance: self.contract_balance_unchecked(contract_address), + new_balance: self.contract_balance_unchecked(payload.address), data: Some(return_value), }, logs, @@ -1183,16 +1193,42 @@ impl<'a> InvocationData<'a> { // back. self.invocation_handler.checkpoint(); - let res = self.invocation_handler.invoke_entrypoint( - self.invoker, - Address::Contract(self.address), - address, - name, - Parameter::new_unchecked(¶meter), - amount, - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)), - &mut self.chain_events, - ); + let res = match self + .invocation_handler + .contracts + .get(&address) + .map(|c| c.contract_name.as_contract_name()) + { + // The contract to call does not exist. + None => InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + logs: v0::Logs::new(), + remaining_energy: InterpreterEnergy::from(remaining_energy), + }, + Some(contract_name) => { + let receive_name = OwnedReceiveName::construct_unchecked( + contract_name, + name.as_entrypoint_name(), + ); + let message = OwnedParameter::new_unchecked(parameter); + self.invocation_handler.invoke_entrypoint( + self.invoker, + Address::Contract(self.address), + from_interpreter_energy(InterpreterEnergy::from( + remaining_energy, + )), + UpdateContractPayload { + amount, + address, + receive_name, + message, + }, + &mut self.chain_events, + ) + } + }; let success = res.is_success(); diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index a5ec3a5f..b1579b9b 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -26,11 +26,13 @@ //! let initialization = chain //! .contract_init( //! ACC, // Invoker account. -//! deployment.module_reference, // Module to initialize from. -//! ContractName::new_unchecked("init_my_contract"), // Contract to init. -//! OwnedParameter::from_serial(&"my_param").unwrap(), // Any type implementing [`Serial`] can be used. -//! Amount::zero(), // CCD to send the contract. //! Energy::from(10000), // Maximum energy allowed for initializing. +//! InitContractPayload { +//! mod_ref: deployment.module_reference, // Module to initialize from. +//! init_name: OwnedContractName::new_unchecked("init_my_contract".into()), // Contract to init. +//! param: OwnedParameter::from_serial(&"my_param").unwrap(), // Any type implementing [`Serial`] can be used. +//! amount: Amount::zero(), // CCD to send the contract. +//! } //! ) //! .unwrap(); //! @@ -39,11 +41,13 @@ //! .contract_update( //! ACC, // Invoker account. //! Address::Account(ACC), // Sender (can also be a contract). -//! initialization.contract_address, // The contract to update. -//! EntrypointName::new_unchecked("my_entrypoint"), // The entrypoint to call. -//! OwnedParameter::from_serial(&42u8).unwrap(), // Another type of parameter. -//! Amount::from_ccd(100), // Sending the contract 100 CCD. //! Energy::from(10000), // Maximum energy allowed for the update. +//! UpdateContractPayload { +//! address: initialization.contract_address, // The contract to update. +//! receive_name: OwnedReceiveName::new_unchecked("my_contract.my_entrypoint".into()), // The receive function to call. +//! message: OwnedParameter::from_serial(&42u8).unwrap(), // The parameter sent to the contract. +//! amount: Amount::from_ccd(100), // Sending the contract 100 CCD. +//! } //! ) //! .unwrap(); //! @@ -75,7 +79,8 @@ pub use concordium_base::{ contracts_common::{ from_bytes, to_bytes, AccountAddress, Address, Amount, ContractAddress, ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, - OwnedParameter, Parameter, SlotTime, + OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, }, + transactions::{InitContractPayload, UpdateContractPayload}, }; pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 77a5bb8c..12f34a15 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -45,14 +45,12 @@ fn initializing_valid_contract_works() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), + }) .expect("Initializing valid contract should work"); assert_eq!( chain.account_balance_available(ACC_0), @@ -80,11 +78,13 @@ fn initializing_with_invalid_parameter_fails() { let res_init = chain .contract_init( ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::try_from(vec![99u8]).expect("Parameter has valid size."), // Invalid param - Amount::zero(), Energy::from(10000), + InitContractPayload{ + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![99u8]).expect("Parameter has valid size."), // Invalid param + } ) .expect_err("Initializing with invalid params should fail"); @@ -120,11 +120,13 @@ fn updating_valid_contract_works() { let res_init = chain .contract_init( ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 - Amount::zero(), Energy::from(10000), + InitContractPayload{ + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 + } ) .expect("Initializing valid contract should work"); @@ -132,11 +134,14 @@ fn updating_valid_contract_works() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("set"), - OwnedParameter::try_from(vec![1u8]).expect("Parameter has valid size."), // Updated to 1 - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.set".into()), + message: OwnedParameter::try_from(vec![1u8]) + .expect("Parameter has valid size."), // Updated to 1 + }, ) .expect("Updating valid contract should work"); @@ -144,11 +149,13 @@ fn updating_valid_contract_works() { .contract_invoke( ACC_0, Address::Contract(res_init.contract_address), // Invoke with a contract as sender. - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), + message: OwnedParameter::empty(), + }, ) .expect("Invoking get should work"); @@ -191,11 +198,13 @@ fn updating_and_invoking_with_missing_sender_fails() { let res_init = chain .contract_init( ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_weather"), - OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 - Amount::zero(), Energy::from(10000), + InitContractPayload{ + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 + } ) .expect("Initializing valid contract should work"); @@ -203,11 +212,13 @@ fn updating_and_invoking_with_missing_sender_fails() { .contract_update( ACC_0, missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), + message: OwnedParameter::empty(), + }, ) .expect_err("should fail"); @@ -215,11 +226,13 @@ fn updating_and_invoking_with_missing_sender_fails() { .contract_invoke( ACC_0, missing_account, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), + message: OwnedParameter::empty(), + }, ) .expect_err("should fail"); @@ -227,11 +240,13 @@ fn updating_and_invoking_with_missing_sender_fails() { .contract_update( ACC_0, missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), + message: OwnedParameter::empty(), + }, ) .expect_err("should fail"); @@ -239,11 +254,13 @@ fn updating_and_invoking_with_missing_sender_fails() { .contract_invoke( ACC_0, missing_contract, - res_init.contract_address, - EntrypointName::new_unchecked("get"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), + message: OwnedParameter::empty(), + }, ) .expect_err("should fail"); @@ -277,14 +294,13 @@ fn init_with_less_energy_than_module_lookup() { let reserved_energy = Energy::from(10); - let res_init = chain.contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_fib"), - OwnedParameter::empty(), - Amount::zero(), - reserved_energy, - ); + let res_init = chain.contract_init(ACC_0, reserved_energy, InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + + init_name: OwnedContractName::new_unchecked("init_fib".into()), + param: OwnedParameter::empty(), + }); match res_init { Err(ContractInitError { kind: ContractInitErrorKind::OutOfEnergy, @@ -309,25 +325,25 @@ fn update_with_fib_reentry_works() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_fib"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_fib".into()), + param: OwnedParameter::empty(), + }) .expect("Initializing valid contract should work"); let res_update = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("receive"), - OwnedParameter::from_serial(&6u64).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("fib.receive".into()), + message: OwnedParameter::from_serial(&6u64).expect("Parameter has valid size"), + }, ) .expect("Updating valid contract should work"); @@ -335,11 +351,13 @@ fn update_with_fib_reentry_works() { .contract_invoke( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("view"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("fib.view".into()), + message: OwnedParameter::empty(), + }, ) .expect("Invoking get should work"); diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 3d7a6391..6e92786f 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -27,29 +27,29 @@ fn test_case_1() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -71,13 +71,16 @@ fn test_case_1() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), - // We supply one microCCD as we expect a trap - // (see contract for details). - Amount::from_micro_ccd(1), Energy::from(10000), + UpdateContractPayload { + address: res_init_a.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.a_modify_proxy".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size"), + // We supply one microCCD as we expect a trap + // (see contract for details). + amount: Amount::from_micro_ccd(1), + }, ) .expect("Updating contract should succeed"); } @@ -102,29 +105,29 @@ fn test_case_2() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -146,16 +149,19 @@ fn test_case_2() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), - // We supply zero microCCD as we're instructing the contract to not expect - // state modifications. Also, the contract does not expect - // errors, i.e., a trap signal from underlying invocations. - // The 'inner' call to contract A does not modify the state. - // See the contract for details. - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init_a.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.a_modify_proxy".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size"), + // We supply zero microCCD as we're instructing the contract to not expect + // state modifications. Also, the contract does not expect + // errors, i.e., a trap signal from underlying invocations. + // The 'inner' call to contract A does not modify the state. + // See the contract for details. + amount: Amount::zero(), + }, ) .expect("Updating contract should succeed"); } @@ -177,43 +183,46 @@ fn test_case_3() { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::from_serial(&ACC_1).expect("Parameter has valid size"), - // We supply three micro CCDs as we're instructing the contract to carry out a - // transfer instead of a call. See the contract for - // details. - Amount::from_micro_ccd(3), Energy::from(10000), + UpdateContractPayload { + address: res_init_a.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.a_modify_proxy".into()), + message: OwnedParameter::from_serial(&ACC_1) + .expect("Parameter has valid size"), + // We supply three micro CCDs as we're instructing the contract to carry out a + // transfer instead of a call. See the contract for + // details. + amount: Amount::from_micro_ccd(3), + }, ) .expect("Updating contract should succeed"); } @@ -235,29 +244,29 @@ fn test_case_4() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_b"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -279,15 +288,18 @@ fn test_case_4() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init_a.contract_address, - EntrypointName::new_unchecked("a_modify_proxy"), - OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), - // We supply four CCDs as we're instructing the contract to expect state - // modifications being made from the 'inner' contract A - // call to be in effect when returned to the caller (a.a_modify_proxy). - // See the contract for details. - Amount::from_micro_ccd(4), Energy::from(10000), + UpdateContractPayload { + address: res_init_a.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.a_modify_proxy".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size"), + // We supply four CCDs as we're instructing the contract to expect state + // modifications being made from the 'inner' contract A + // call to be in effect when returned to the caller (a.a_modify_proxy). + // See the contract for details. + amount: Amount::from_micro_ccd(4), + }, ) .expect("Updating contract should succeed"); } diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 176c0482..98f6268b 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -14,29 +14,33 @@ fn test_counter() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_counter"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_counter".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("inc"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("counter.inc".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_counter_state(&mut chain, res_init.contract_address, 1); @@ -45,11 +49,13 @@ fn test_counter() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("inc"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("counter.inc".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_counter_state(&mut chain, res_init.contract_address, 2); @@ -64,11 +70,14 @@ fn test_counter() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("inc10"), - OwnedParameter::from_serial(¶meter).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("counter.inc10".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_counter_state(&mut chain, res_init.contract_address, 12); diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 8118282a..24d2960b 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -14,18 +14,20 @@ fn test_error_codes() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_caller"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_caller".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // Invoke an entrypoint that calls the "fail" entrypoint. @@ -47,11 +49,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_0).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_0) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( @@ -77,11 +82,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_1).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_1) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( @@ -105,11 +113,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_2).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_2) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( @@ -135,11 +146,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_3).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_3) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( @@ -165,11 +179,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_4).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_4) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( @@ -198,11 +215,14 @@ fn test_error_codes() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("call"), - OwnedParameter::from_serial(¶meter_6).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("caller.call".into()), + message: OwnedParameter::from_serial(¶meter_6) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); assert_eq!( diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index 4d333488..83e4d924 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -12,31 +12,31 @@ fn test_fallback() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init_two = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_two"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_two".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_init_one = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_one"), - OwnedParameter::from_serial(&res_init_two.contract_address) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_one".into()), + param: OwnedParameter::from_serial(&res_init_two.contract_address) .expect("Parameter has valid size"), /* Pass in address of contract * "two". */ - Amount::zero(), - Energy::from(10000), - ) + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // Invoke the fallback directly. This should fail with execution failure/trap @@ -46,11 +46,13 @@ fn test_fallback() { .contract_invoke( ACC_0, Address::Account(ACC_0), - res_init_one.contract_address, - EntrypointName::new_unchecked(""), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init_one.contract_address, + receive_name: OwnedReceiveName::new_unchecked("one.".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect_err("should fail"); match res_invoke_1.kind { @@ -67,11 +69,13 @@ fn test_fallback() { .contract_invoke( ACC_0, Address::Account(ACC_0), - res_init_one.contract_address, - EntrypointName::new_unchecked("do"), - parameter.clone(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init_one.contract_address, + receive_name: OwnedReceiveName::new_unchecked("one.do".into()), + message: parameter.clone(), + amount: Amount::zero(), + }, ) .expect("Invoke should succeed."); assert_eq!(res_invoke_2.return_value, parameter.as_ref()); // Parameter is diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 07397547..76d9e1cb 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -16,40 +16,46 @@ fn test_iterator() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_iterator"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_iterator".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("iteratetest"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("iterator.iteratetest".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Should succeed"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("lockingtest"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("iterator.lockingtest".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Should succeed."); } diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index b0900c18..7feb9a79 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -35,14 +35,12 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // The contract will query the balance of ACC_1 and assert that the three @@ -53,11 +51,14 @@ mod query_account_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -97,14 +98,12 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let update_amount = Amount::from_ccd(123); @@ -120,11 +119,14 @@ mod query_account_balance { .contract_update( ACC_1, Address::Account(ACC_1), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - update_amount, energy_limit, + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: update_amount, + }, ) .expect("Updating valid contract should work"); @@ -166,14 +168,12 @@ mod query_account_balance { let amount_to_send = Amount::from_ccd(123); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - amount_to_send, // Make sure the contract has CCD to transfer. - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: amount_to_send, // Make sure the contract has CCD to transfer. + }) .expect("Initializing valid contract should work"); let amount_to_send = Amount::from_ccd(123); @@ -190,11 +190,14 @@ mod query_account_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -239,14 +242,12 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to @@ -259,11 +260,14 @@ mod query_account_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -301,14 +305,12 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // The account to query, which doesn't exist in this test case. @@ -318,11 +320,14 @@ mod query_account_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -366,25 +371,21 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_init_other = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, // Set up another contract with `init_amount` balance - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, // Set up another contract with `init_amount` balance + }) .expect("Initializing valid contract should work"); // check that the other contract has `self_balance == init_amount`. @@ -394,11 +395,14 @@ mod query_contract_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -430,14 +434,12 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, + }) .expect("Initializing valid contract should work"); // check that the other contract has `self_balance == init_amount`. @@ -447,11 +449,14 @@ mod query_contract_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - update_amount, Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: update_amount, + }, ) .expect("Updating valid contract should work"); @@ -483,14 +488,12 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - init_amount, - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, + }) .expect("Initializing valid contract should work"); let input_param = ( @@ -504,11 +507,14 @@ mod query_contract_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - update_amount, Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: update_amount, + }, ) .expect("Updating valid contract should work"); @@ -539,14 +545,12 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // Non-existent contract address. @@ -556,11 +560,14 @@ mod query_contract_balance { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -593,14 +600,12 @@ mod query_exchange_rates { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // Non-existent contract address. @@ -610,11 +615,14 @@ mod query_exchange_rates { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("query"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.query".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index 47e45cc1..d89f2d57 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -14,41 +14,46 @@ fn test_recorder() { let res_deploy = chain .module_deploy_v1( ACC_0, - Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)).expect("module should exist"), + Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_recorder"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_recorder".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("record_u64"), - OwnedParameter::from_serial(&20u64).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.record_u64".into()), + message: OwnedParameter::from_serial(&20u64) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Update failed"); chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("record_u64"), - OwnedParameter::from_serial(&40u64).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.record_u64".into()), + message: OwnedParameter::from_serial(&40u64) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Update failed"); // Assert that all 60 values were inserted in the state. diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 7873d99b..8991e4cc 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -25,11 +25,13 @@ fn test_new_parameter_limit() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("param"), - mk_parameter(65535, 65535), - Amount::zero(), Energy::from(700000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("relax.param".into()), + message: mk_parameter(65535, 65535), + amount: Amount::zero(), + }, ) .expect("Updating contract should succeed"); } @@ -43,11 +45,14 @@ fn test_new_return_value_limit() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("return-value"), - OwnedParameter::from_serial(&100_000u32).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("relax.return-value".into()), + message: OwnedParameter::from_serial(&100_000u32) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating contract should succeed"); } @@ -61,11 +66,14 @@ fn test_new_log_limit() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("logs"), - OwnedParameter::from_serial(&64u32).expect("Parameter has valid size"), - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("relax.logs".into()), + message: OwnedParameter::from_serial(&64u32) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating contract should succeed"); } @@ -80,19 +88,18 @@ fn deploy_and_init() -> (Chain, ContractAddress) { let res_deploy = chain .module_deploy_v1( ACC_0, - Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), + Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_relax"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); (chain, res_init.contract_address) } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index c018d43d..d07ea49c 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -1,7 +1,7 @@ //! This module contains tests for transfers fr&om a contract to an account. //! See more details about the specific test inside the `transfer.wat` file. -use concordium_smart_contract_testing::*; use concordium_smart_contract_engine::v0::Logs; +use concordium_smart_contract_testing::*; const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); @@ -13,18 +13,20 @@ fn test_transfer() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(ACC_0, Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)).expect("module should exist")) + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_transfer"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_transfer".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let contract_address = res_init.contract_address; @@ -33,11 +35,14 @@ fn test_transfer() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("forward"), - OwnedParameter::from_serial(&ACC_0).expect("Parameter has valid size"), - Amount::from_micro_ccd(123), Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("transfer.forward".into()), + message: OwnedParameter::from_serial(&ACC_0) + .expect("Parameter has valid size"), + amount: Amount::from_micro_ccd(123), + }, ) .expect("Updating contract should succeed"); // Contract should have forwarded the amount and thus have balance == 0. @@ -51,11 +56,13 @@ fn test_transfer() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("deposit"), - OwnedParameter::empty(), - Amount::from_micro_ccd(1000), Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("transfer.deposit".into()), + message: OwnedParameter::empty(), + amount: Amount::from_micro_ccd(1000), + }, ) .expect("Updating contract should succeed"); @@ -63,12 +70,15 @@ fn test_transfer() { .contract_update( ACC_0, Address::Account(ACC_0), - contract_address, - EntrypointName::new_unchecked("send"), - OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))).expect("Parameter has valid size"), /* Tell it to send 17 - * mCCD to ACC_0. */ - Amount::zero(), Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), + message: OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))) + .expect("Parameter has valid size"), /* Tell it to send 17 + * mCCD to ACC_0. */ + amount: Amount::zero(), + }, ) .expect("Updating contract should succeed"); // Contract should have 1000 - 17 microCCD in balance. diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 4dfb1075..deda31fc 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -32,14 +32,13 @@ fn test() { // Initialize `upgrading_0`. let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_a"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_a".into()), + mod_ref: res_deploy_0.module_reference, + + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); // Upgrade the contract to the `upgrading_1` module by calling the `bump` @@ -48,12 +47,14 @@ fn test() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("bump"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.bump".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -62,12 +63,14 @@ fn test() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("newfun"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("a.newfun".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating the `newfun` from the `upgrading_1` module should work"); @@ -107,26 +110,26 @@ fn test_self_invoke() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_update = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -169,25 +172,25 @@ fn test_missing_module() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_update = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -231,26 +234,27 @@ fn test_missing_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, + + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_update = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -295,14 +299,13 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, + + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); @@ -311,11 +314,14 @@ fn test_twice_in_one_transaction() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -366,14 +372,12 @@ fn test_chained_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. @@ -383,11 +387,14 @@ fn test_chained_contract() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&input_param).expect("Parameter has valid size"), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&input_param) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Updating valid contract should work"); @@ -426,26 +433,26 @@ fn test_reject() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + mod_ref: res_deploy_0.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_update_upgrade = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect_err("should fail"); @@ -453,11 +460,13 @@ fn test_reject() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.new_feature".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect_err("should fail"); @@ -512,25 +521,26 @@ fn test_changing_entrypoint() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init( - ACC_0, - res_deploy_0.module_reference, - ContractName::new_unchecked("init_contract"), - OwnedParameter::empty(), - Amount::zero(), - Energy::from(10000), - ) + .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, + + amount: Amount::zero(), + }) .expect("Initializing valid contract should work"); let res_update_old_feature_0 = chain .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("old_feature"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.old_feature".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Updating old_feature on old module should work."); @@ -538,11 +548,13 @@ fn test_changing_entrypoint() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.new_feature".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect_err("Updating new_feature on old module should _not_ work"); @@ -550,12 +562,14 @@ fn test_changing_entrypoint() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("upgrade"), - OwnedParameter::from_serial(&res_deploy_1.module_reference) - .expect("Parameter has valid size"), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), + message: OwnedParameter::from_serial(&res_deploy_1.module_reference) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, ) .expect("Upgrading contract should work."); @@ -563,11 +577,13 @@ fn test_changing_entrypoint() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("old_feature"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.old_feature".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect_err("Updating old_feature on _new_ module should _not_ work."); @@ -575,11 +591,13 @@ fn test_changing_entrypoint() { .contract_update( ACC_0, Address::Account(ACC_0), - res_init.contract_address, - EntrypointName::new_unchecked("new_feature"), - OwnedParameter::empty(), - Amount::zero(), Energy::from(1000000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.new_feature".into()), + message: OwnedParameter::empty(), + amount: Amount::zero(), + }, ) .expect("Updating new_feature on _new_ module should work"); From bc2ae661cb65e2ec5518523b6ab8cfca83fb0cf3 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 17 Mar 2023 10:06:50 +0100 Subject: [PATCH 096/208] Forward trap errors on contract initialization --- contract-testing/src/impls.rs | 6 ++++-- contract-testing/src/types.rs | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index b0868e94..2b55ffac 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -422,7 +422,7 @@ impl Chain { }) } Ok(v1::InitResult::Trap { - error: _, // TODO: Should we forward this to the user? + error, remaining_energy: remaining_interpreter_energy, }) => { let energy_used_in_interpreter = from_interpreter_energy( @@ -430,7 +430,9 @@ impl Chain { ); remaining_energy.tick_energy(energy_used_in_interpreter)?; Err(ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::Trap, + failure_kind: InitFailure::Trap { + error: TrapError(error), + }, }) } Ok(v1::InitResult::OutOfEnergy) => { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 9f5228f6..c0df7024 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -265,11 +265,16 @@ pub enum InitFailure { return_value: ReturnValue, }, /// The contract trapped. - Trap, + Trap { error: TrapError }, /// The contract ran out of energy. OutOfEnergy, } +/// The error produced when a contract traps. +#[derive(Debug, Error)] +#[error("The contract trapped due to: {0}")] +pub struct TrapError(pub(crate) anyhow::Error); + /// Represents a successful contract update (or invocation). // TODO: Consider adding function to aggregate all logs from the host_events. #[derive(Debug)] From 9010a3c2647ca775eca7665e2e284369c0ab43f2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 17 Mar 2023 11:45:06 +0100 Subject: [PATCH 097/208] Ad Rename and fix typos and other small tasks --- contract-testing/CHANGELOG.md | 4 +- contract-testing/src/constants.rs | 6 +- contract-testing/src/impls.rs | 99 +-- contract-testing/src/invocation/impls.rs | 735 +++++++++++------------ contract-testing/src/invocation/types.rs | 13 +- contract-testing/src/types.rs | 94 +-- 6 files changed, 488 insertions(+), 463 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 90aa1ab3..6bbd5c82 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## Unreleased changes +## 1.0.0 -- First version of the library. +- The initial release of the library. diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index cc10b04d..a4d0fe36 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -4,13 +4,13 @@ use concordium_base::base::Energy; // Energy constants from Cost.hs in concordium-base. -/// Cost of querying the account balance from a within smart contract instance. +/// Cost of querying the account balance from within a smart contract instance. pub(crate) const CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST: Energy = Energy { energy: 200 }; -/// Cost of querying the contract balance from a within smart contract instance. +/// Cost of querying the contract balance from within a smart contract instance. pub(crate) const CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST: Energy = Energy { energy: 200 }; -/// Cost of querying the current exchange rates from a within smart contract +/// Cost of querying the current exchange rates from within a smart contract /// instance. pub(crate) const CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST: Energy = Energy { energy: 100 }; diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 2b55ffac..713131c2 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -74,7 +74,9 @@ impl Chain { // Ensure sender account exists. if !self.account_exists(sender) { return Err(ModuleDeployError { - kind: ModuleDeployErrorKind::SenderDoesNotExist(AccountMissing(sender)), + kind: ModuleDeployErrorKind::SenderDoesNotExist(AccountDoesNotExist { + address: sender, + }), energy_used: 0.into(), transaction_fee: Amount::zero(), }); @@ -241,7 +243,7 @@ impl Chain { let mut remaining_energy = energy_reserved; if !self.account_exists(sender) { return Err(self.from_init_error_kind( - ContractInitErrorKind::SenderDoesNotExist(AccountMissing(sender)), + ContractInitErrorKind::SenderDoesNotExist(AccountDoesNotExist { address: sender }), energy_reserved, remaining_energy, )); @@ -333,6 +335,8 @@ impl Chain { sender_policies: account_info.policies.clone(), }; // Initialize contract + // We create an empty loader as no caching is used in this testing library + // presently, so the loader is not used. let mut loader = v1::trie::Loader::new(&[][..]); let energy_given_to_interpreter = to_interpreter_energy(*remaining_energy); @@ -431,7 +435,7 @@ impl Chain { remaining_energy.tick_energy(energy_used_in_interpreter)?; Err(ContractInitErrorKind::ExecutionError { failure_kind: InitFailure::Trap { - error: TrapError(error), + error: ExecutionError(error), }, }) } @@ -441,7 +445,11 @@ impl Chain { failure_kind: InitFailure::OutOfEnergy, }) } - Err(e) => panic!("Internal error: init failed with interpreter error: {}", e), + Err(err) => Err(ContractInitErrorKind::ExecutionError { + failure_kind: InitFailure::Trap { + error: ExecutionError(err), + }, + }), } } @@ -570,9 +578,9 @@ impl Chain { return Err(ContractInvocationError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::InvokerDoesNotExist(AccountMissing( - invoker, - )), + kind: ContractInvocationErrorKind::InvokerDoesNotExist( + AccountDoesNotExist { address: invoker }, + ), }); } // Ensure the sender exists. @@ -689,9 +697,9 @@ impl Chain { return Err(ContractInvocationError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::InvokerDoesNotExist(AccountMissing( - invoker, - )), + kind: ContractInvocationErrorKind::InvokerDoesNotExist( + AccountDoesNotExist { address: invoker }, + ), }); } // Ensure the sender exists. @@ -754,12 +762,8 @@ impl Chain { self.accounts.insert(account, account_info) } - /// Create a contract address. - /// - /// It will have an index one above the previously highest contract index. - /// - /// Next call to `contract_init` will skip this address. - pub fn create_contract_address(&mut self) -> ContractAddress { + /// Create a contract address by giving it the next available index. + fn create_contract_address(&mut self) -> ContractAddress { let index = self.next_contract_index; let subindex = 0; self.next_contract_index += 1; @@ -782,7 +786,7 @@ impl Chain { } /// Helper method for looking up part of the state of a smart contract, - /// which is a trie. + /// which is a key-value store. pub fn contract_state_lookup(&self, address: ContractAddress, key: &[u8]) -> Option> { let mut loader = v1::trie::Loader::new(&[][..]); self.contracts.get(&address)?.state.lookup(&mut loader, key) @@ -793,24 +797,28 @@ impl Chain { fn contract_module( &self, module_ref: ModuleReference, - ) -> Result { - let module = self - .modules - .get(&module_ref) - .ok_or(ModuleMissing(module_ref))?; + ) -> Result { + let module = self.modules.get(&module_ref).ok_or(ModuleDoesNotExist { + module_reference: module_ref, + })?; Ok(module.clone()) } /// Returns an immutable reference to an [`Account`]. - pub fn account(&self, address: AccountAddress) -> Result<&Account, AccountMissing> { - self.accounts.get(&address).ok_or(AccountMissing(address)) + pub fn account(&self, address: AccountAddress) -> Result<&Account, AccountDoesNotExist> { + self.accounts + .get(&address) + .ok_or(AccountDoesNotExist { address }) } /// Returns a mutable reference to [`Account`]. - fn account_mut(&mut self, address: AccountAddress) -> Result<&mut Account, AccountMissing> { + fn account_mut( + &mut self, + address: AccountAddress, + ) -> Result<&mut Account, AccountDoesNotExist> { self.accounts .get_mut(&address) - .ok_or(AccountMissing(address)) + .ok_or(AccountDoesNotExist { address }) } /// Check whether an [`Account`] exists. @@ -823,10 +831,10 @@ impl Chain { self.contracts.contains_key(&address) } - /// Check whether the [`Address`] exists. + /// Check whether an object with the [`Address`] exists. /// - /// I.e. if it is an account, whether the account exists, - /// and if it is a contract, whether the contract exists. + /// That is, if it is an account address, whether the account exists, + /// and if it is a contract address, whether the contract exists. fn address_exists(&self, address: Address) -> bool { match address { Address::Account(acc) => self.account_exists(acc), @@ -951,22 +959,20 @@ impl Account { } impl ContractInvocationSuccess { - /// Get a list of all transfers that were made from contracts to accounts. - pub fn transfers(&self) -> Vec { - self.chain_events - .iter() - .filter_map(|e| { - if let ChainEvent::Transferred { from, amount, to } = e { - Some(Transfer { - from: *from, - amount: *amount, - to: *to, - }) - } else { - None - } - }) - .collect() + /// Get an iterator of all transfers that were made from contracts to + /// accounts. + pub fn transfers(&self) -> impl Iterator + '_ { + self.chain_events.iter().filter_map(|e| { + if let ChainEvent::Transferred { from, amount, to } = e { + Some(Transfer { + from: *from, + amount: *amount, + to: *to, + }) + } else { + None + } + }) } /// Get the chain events grouped by which contract they originated from. @@ -1014,7 +1020,7 @@ pub(crate) fn from_interpreter_energy(interpreter_energy: InterpreterEnergy) -> Energy::from(interpreter_energy.energy / 1000) } -/// Calculate the energy energy for looking up a [`ContractModule`]. +/// Calculate the energy for looking up a [`ContractModule`]. pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { // The ratio is from Concordium/Cost.hs::lookupModule Energy::from(module.size / 50) @@ -1022,7 +1028,6 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange /// rates provided. -// TODO: Find a way to make this parse the doc tests // To find the mCCD/NRG exchange rate: // // euro mCCD euro * mCCD mCCD diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b58765df..48ea3ecb 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -125,7 +125,7 @@ impl EntrypointInvocationHandler { None => { // Return early. // TODO: For the top-most update, we should catch this in `contract_update` and - // return `ContractUpdateError::EntrypointMissing`. + // return `ContractUpdateError::EntrypointDoesNotExist`. return InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, @@ -144,15 +144,6 @@ impl EntrypointInvocationHandler { .expect("Contract known to exist at this point"); let module = self.contract_module(payload.address); - // Subtract the cost of looking up the module - remaining_energy = if let Some(remaining_energy) = - remaining_energy.checked_sub(lookup_module_cost(&module)) - { - remaining_energy - } else { - return InvokeEntrypointResult::new_out_of_energy_failure(); - }; - // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. Also returns the contract name and entrypoint name as they // are needed further down. @@ -191,6 +182,15 @@ impl EntrypointInvocationHandler { } }; + // Subtract the cost of looking up the module + remaining_energy = if let Some(remaining_energy) = + remaining_energy.checked_sub(lookup_module_cost(&module)) + { + remaining_energy + } else { + return InvokeEntrypointResult::new_out_of_energy_failure(); + }; + // Construct the receive context let receive_ctx = v1::ReceiveContext { // This should be the entrypoint specified, even if we end up @@ -216,7 +216,7 @@ impl EntrypointInvocationHandler { }; // Construct the instance state - let mut loader = v1::trie::Loader::new(&[][..]); + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut mutable_state = self.contract_state(payload.address); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -614,7 +614,7 @@ impl EntrypointInvocationHandler { with_fresh_generation: bool, ) { let state = if with_fresh_generation { - let mut loader = v1::trie::Loader::new(&[][..]); + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. state.make_fresh_generation(&mut loader) } else { state.clone() @@ -843,7 +843,7 @@ impl ChangeSet { let mut invoked_contract_has_state_changes = false; // Persist contract changes and collect the total increase in states sizes. let mut collector = v1::trie::SizeCollector::default(); - let mut loader = v1::trie::Loader::new(&[][..]); + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut frozen_states: BTreeMap = BTreeMap::new(); @@ -922,7 +922,7 @@ impl ChangeSet { invoked_contract: ContractAddress, ) -> Result<(Energy, bool), OutOfEnergy> { let mut invoked_contract_has_state_changes = false; - let mut loader = v1::trie::Loader::new(&[][..]); + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut collector = v1::trie::SizeCollector::default(); for (addr, changes) in self.current_mut().contracts.iter_mut() { if let Some(modified_state) = &mut changes.state { @@ -1061,401 +1061,398 @@ impl<'a> InvocationData<'a> { &mut self, res: ExecResult>, ) -> ExecResult> { - match res { - Ok(r) => match r { - v1::ReceiveResult::Success { + let receive_result = match res { + Ok(receive_result) => receive_result, + Err(e) => { + if e.downcast_ref::().is_some() { + return Ok(v1::ReceiveResult::OutOfEnergy); + } else { + return Err(e); + } + } + }; + match receive_result { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + let update_event = ChainEvent::Updated { + address: self.address, + contract: self.contract_name.clone(), + entrypoint: self.entrypoint.clone(), + amount: self.amount, + }; + // Add update event + self.chain_events.push(update_event); + + // Save changes to changeset. + if state_changed { + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + false, + ); + } + + Ok(v1::ReceiveResult::Success { logs, state_changed, return_value, remaining_energy, - } => { - let update_event = ChainEvent::Updated { - address: self.address, - contract: self.contract_name.clone(), - entrypoint: self.entrypoint.clone(), - amount: self.amount, - }; - // Add update event - self.chain_events.push(update_event); - - // Save changes to changeset. - if state_changed { - self.invocation_handler.save_state_changes( - self.address, - &mut self.state, - false, - ); - } - - Ok(v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - }) - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, + }) + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => { + // Create the interrupt event, which will be included for transfers, calls, and + // upgrades, but not for the remaining interrupts. + let interrupt_event = ChainEvent::Interrupted { + address: self.address, logs, - config, - interrupt, - } => { - // Create the interrupt event, which will be included for transfers, calls, and - // upgrades, but not for the remaining interrupts. - let interrupt_event = ChainEvent::Interrupted { - address: self.address, - logs, - }; - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - // Add the interrupt event - self.chain_events.push(interrupt_event); - - let response = match self - .invocation_handler - .transfer_from_contract_to_account(amount, self.address, to) - { - Ok(new_balance) => v1::InvokeResponse::Success { - new_balance, - data: None, - }, - Err(err) => { - let kind = match err { - TransferError::ToMissing => { - v1::InvokeFailure::NonExistentAccount - } - TransferError::InsufficientBalance => { - v1::InvokeFailure::InsufficientAmount - } - }; - v1::InvokeResponse::Failure { kind } - } - }; - - let success = matches!(response, v1::InvokeResponse::Success { .. }); - if success { - // Add transfer event - self.chain_events.push(ChainEvent::Transferred { - from: self.address, - amount, - to, - }); + }; + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + + let response = match self + .invocation_handler + .transfer_from_contract_to_account(amount, self.address, to) + { + Ok(new_balance) => v1::InvokeResponse::Success { + new_balance, + data: None, + }, + Err(err) => { + let kind = match err { + TransferError::ToMissing => { + v1::InvokeFailure::NonExistentAccount + } + TransferError::InsufficientBalance => { + v1::InvokeFailure::InsufficientAmount + } + }; + v1::InvokeResponse::Failure { kind } } - // Add resume event - self.chain_events.push(ChainEvent::Resumed { - address: self.address, - success, + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + if success { + // Add transfer event + self.chain_events.push(ChainEvent::Transferred { + from: self.address, + amount, + to, }); - - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy.tick_energy( - concordium_base::transactions::cost::SIMPLE_TRANSFER, - )?; - - let resume_res = run_interpreter(remaining_energy, |energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // never changes on transfers - v1::trie::Loader::new(&[][..]), - ) - }); - - // Resume - self.process(resume_res) } - v1::Interrupt::Call { - address, - parameter, - name, - amount, - } => { - // Add the interrupt event - self.chain_events.push(interrupt_event); - - if state_changed { - self.invocation_handler.save_state_changes( - self.address, - &mut self.state, - true, - ); - } + // Add resume event + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy + .tick_energy(concordium_base::transactions::cost::SIMPLE_TRANSFER)?; + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // never changes on transfers + v1::trie::Loader::new(&[][..]), + ) + }); + + // Resume + self.process(resume_res) + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => { + // Add the interrupt event + self.chain_events.push(interrupt_event); + + if state_changed { + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + true, + ); + } - // Save the modification index before the invoke. - let mod_idx_before_invoke = - self.invocation_handler.modification_index(self.address); + // Save the modification index before the invoke. + let mod_idx_before_invoke = + self.invocation_handler.modification_index(self.address); - // Make a checkpoint before calling another contract so that we may roll - // back. - self.invocation_handler.checkpoint(); + // Make a checkpoint before calling another contract so that we may roll + // back. + self.invocation_handler.checkpoint(); - let res = match self - .invocation_handler - .contracts - .get(&address) - .map(|c| c.contract_name.as_contract_name()) - { - // The contract to call does not exist. - None => InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, - logs: v0::Logs::new(), - remaining_energy: InterpreterEnergy::from(remaining_energy), - }, - Some(contract_name) => { - let receive_name = OwnedReceiveName::construct_unchecked( - contract_name, - name.as_entrypoint_name(), - ); - let message = OwnedParameter::new_unchecked(parameter); - self.invocation_handler.invoke_entrypoint( - self.invoker, - Address::Contract(self.address), - from_interpreter_energy(InterpreterEnergy::from( - remaining_energy, - )), - UpdateContractPayload { - amount, - address, - receive_name, - message, - }, - &mut self.chain_events, - ) - } - }; - - let success = res.is_success(); - - // Remove the last state changes if the invocation failed. - let state_changed = if !success { - self.invocation_handler.rollback(); - false // We rolled back, so no changes were made - // to this contract. - } else { - let mod_idx_after_invoke = - self.invocation_handler.modification_index(self.address); - let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; - if state_changed { - // Update the state field with the newest value from the - // changeset. - self.state = - self.invocation_handler.contract_state(self.address); - } - state_changed - }; - - // Add resume event - let resume_event = ChainEvent::Resumed { - address: self.address, - success, - }; - - self.chain_events.push(resume_event); - - let resume_res = run_interpreter( - from_interpreter_energy(res.remaining_energy), - |energy| { - v1::resume_receive( - config, - res.invoke_response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) + let res = match self + .invocation_handler + .contracts + .get(&address) + .map(|c| c.contract_name.as_contract_name()) + { + // The contract to call does not exist. + None => InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, }, - ); - - self.process(resume_res) - } - v1::Interrupt::Upgrade { module_ref } => { - // Add the interrupt event. - self.chain_events.push(interrupt_event); + logs: v0::Logs::new(), + remaining_energy: InterpreterEnergy::from(remaining_energy), + }, + Some(contract_name) => { + let receive_name = OwnedReceiveName::construct_unchecked( + contract_name, + name.as_entrypoint_name(), + ); + let message = OwnedParameter::new_unchecked(parameter); + self.invocation_handler.invoke_entrypoint( + self.invoker, + Address::Contract(self.address), + from_interpreter_energy(InterpreterEnergy::from( + remaining_energy, + )), + UpdateContractPayload { + amount, + address, + receive_name, + message, + }, + &mut self.chain_events, + ) + } + }; - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + let success = res.is_success(); - // Charge a base cost. - remaining_energy - .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; + // Remove the last state changes if the invocation failed. + let state_changed = if !success { + self.invocation_handler.rollback(); + false // We rolled back, so no changes were made + // to this contract. + } else { + let mod_idx_after_invoke = + self.invocation_handler.modification_index(self.address); + let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + self.state = self.invocation_handler.contract_state(self.address); + } + state_changed + }; - let response = match self.invocation_handler.modules.get(&module_ref) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidModuleRef, - }, - Some(module) => { - // Charge for the module lookup. - remaining_energy.tick_energy(lookup_module_cost(module))?; - - if module.artifact.export.contains_key( - self.contract_name.as_contract_name().get_chain_name(), - ) { - // Update module reference in the changeset. - let old_module_ref = self - .invocation_handler - .save_module_upgrade(self.address, module_ref); - - // Charge for the initialization cost. - remaining_energy.tick_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - )?; - - let upgrade_event = ChainEvent::Upgraded { - address: self.address, - from: old_module_ref, - to: module_ref, - }; - - self.chain_events.push(upgrade_event); - - v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: None, - } - } else { - v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidContractName, - } - } - } - }; + // Add resume event + let resume_event = ChainEvent::Resumed { + address: self.address, + success, + }; - let success = matches!(response, v1::InvokeResponse::Success { .. }); - self.chain_events.push(ChainEvent::Resumed { - address: self.address, - success, - }); + self.chain_events.push(resume_event); - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = run_interpreter( + from_interpreter_energy(res.remaining_energy), + |energy| { v1::resume_receive( config, - response, + res.invoke_response, energy, &mut self.state, state_changed, v1::trie::Loader::new(&[][..]), ) - }); + }, + ); - self.process(resume_res) - } - v1::Interrupt::QueryAccountBalance { address } => { - let response = match self.invocation_handler.account_balance(address) { - Some(balance) => v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&balance)), - }, - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - }, - }; + self.process(resume_res) + } + v1::Interrupt::Upgrade { module_ref } => { + // Add the interrupt event. + self.chain_events.push(interrupt_event); - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, - )?; + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - let resume_res = run_interpreter(remaining_energy, |energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - }); + // Charge a base cost. + remaining_energy + .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; - self.process(resume_res) - } - v1::Interrupt::QueryContractBalance { address } => { - let response = match self.invocation_handler.contract_balance(address) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, - Some(bal) => v1::InvokeResponse::Success { - // Balance of contract querying. Won't change. Notice the - // `self.address`. - new_balance: self + let response = match self.invocation_handler.modules.get(&module_ref) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(module) => { + // Charge for the module lookup. + remaining_energy.tick_energy(lookup_module_cost(module))?; + + if module.artifact.export.contains_key( + self.contract_name.as_contract_name().get_chain_name(), + ) { + // Update module reference in the changeset. + let old_module_ref = self .invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&bal)), - }, - }; + .save_module_upgrade(self.address, module_ref); - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - )?; + // Charge for the initialization cost. + remaining_energy.tick_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + )?; - let resume_res = run_interpreter(remaining_energy, |energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - }); + let upgrade_event = ChainEvent::Upgraded { + address: self.address, + from: old_module_ref, + to: module_ref, + }; - self.process(resume_res) - } - v1::Interrupt::QueryExchangeRates => { - let exchange_rates = ( - self.invocation_handler.euro_per_energy, - self.invocation_handler.micro_ccd_per_euro, - ); + self.chain_events.push(upgrade_event); - let response = v1::InvokeResponse::Success { + v1::InvokeResponse::Success { + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, + } + } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + self.chain_events.push(ChainEvent::Resumed { + address: self.address, + success, + }); + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + }); + + self.process(resume_res) + } + v1::Interrupt::QueryAccountBalance { address } => { + let response = match self.invocation_handler.account_balance(address) { + Some(balance) => v1::InvokeResponse::Success { new_balance: self .invocation_handler .contract_balance_unchecked(self.address), - data: Some(to_bytes(&exchange_rates)), - }; - - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, - )?; - - let resume_res = run_interpreter(remaining_energy, |energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - }); + data: Some(to_bytes(&balance)), + }, + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy + .tick_energy(constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST)?; + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + + self.process(resume_res) + } + v1::Interrupt::QueryContractBalance { address } => { + let response = match self.invocation_handler.contract_balance(address) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + Some(bal) => v1::InvokeResponse::Success { + // Balance of contract querying. Won't change. Notice the + // `self.address`. + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: Some(to_bytes(&bal)), + }, + }; + + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + )?; + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + + self.process(resume_res) + } + v1::Interrupt::QueryExchangeRates => { + let exchange_rates = ( + self.invocation_handler.euro_per_energy, + self.invocation_handler.micro_ccd_per_euro, + ); - self.process(resume_res) - } + let response = v1::InvokeResponse::Success { + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: Some(to_bytes(&exchange_rates)), + }; + + let mut remaining_energy = + from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + remaining_energy + .tick_energy(constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST)?; + + let resume_res = run_interpreter(remaining_energy, |energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + }); + + self.process(resume_res) } } - x => Ok(x), - }, - Err(e) => { - if e.downcast_ref::().is_some() { - Ok(v1::ReceiveResult::OutOfEnergy) - } else { - Err(e) - } } + x => Ok(x), } } } diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 12f617b6..2c015c0a 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -23,12 +23,23 @@ pub(crate) struct InvokeEntrypointResult { /// A type that supports invoking a contract entrypoint. pub(crate) struct EntrypointInvocationHandler { + /// The changeset which keeps track of changes to accounts, modules, and + /// contracts that occur during an invocation. pub(super) changeset: ChangeSet, + /// The accounts of the chain. These are currently clones and only used as a + /// reference. Any changes are saved to the changeset. pub(super) accounts: BTreeMap, + /// The modules of the chain. These are currently clones and only used as a + /// reference. Any changes are saved to the changeset. pub(super) modules: BTreeMap, + /// The contracts of the chain. These are currently clones and only used as + /// a reference. Any changes are saved to the changeset. pub(super) contracts: BTreeMap, + /// The current block time. pub(super) block_time: SlotTime, + /// The euro per energy exchange rate. pub(super) euro_per_energy: ExchangeRate, + /// The mCCD per euro exchange rate. pub(super) micro_ccd_per_euro: ExchangeRate, } @@ -104,7 +115,7 @@ pub(super) struct InvocationData<'a> { /// A positive or negative delta in for an [`Amount`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum AmountDelta { - /// A posittive delta. + /// A positive delta. Positive(Amount), /// A negative delta. Negative(Amount), diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index c0df7024..81fa2bf0 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -24,9 +24,10 @@ pub struct ContractModule { pub(crate) artifact: Arc>, } -/// Represents the block chain and supports a number of operations, including +/// Represents the blockchain and supports a number of operations, including /// creating accounts, deploying modules, initializing contract, updating /// contracts and invoking contracts. +#[derive(Debug)] pub struct Chain { /// The block time viewable inside the smart contracts. /// Defaults to `0`. @@ -46,7 +47,7 @@ pub struct Chain { } /// A smart contract instance. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Contract { /// The module which contains this contract. pub module_reference: ModuleReference, @@ -65,7 +66,7 @@ pub struct Contract { pub struct TestPolicies(pub Vec); /// An account. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Account { /// The account balance. pub balance: AccountBalance, @@ -124,7 +125,7 @@ pub enum ChainEvent { }, } -/// A transfer from an contract to an account. +/// A transfer from a contract to an account. #[derive(Debug, PartialEq, Eq)] pub struct Transfer { /// The sender contract. @@ -171,9 +172,9 @@ pub enum ModuleDeployErrorKind { InsufficientFunds, /// The sender account deploying the module does not exist. #[error("sender account {} does not exist", 0.0)] - SenderDoesNotExist(#[from] AccountMissing), + SenderDoesNotExist(#[from] AccountDoesNotExist), /// The module has already been deployed. - #[error("module with reference {:?} already exists", 0)] + #[error("module with reference {0} already exists")] DuplicateModule(ModuleReference), /// The module version is not supported. #[error("wasm version {0} is not supported")] @@ -231,26 +232,26 @@ pub struct ContractInitError { /// Types of errors that can occur in [`Chain::contract_init`]. #[derive(Debug, Error)] pub enum ContractInitErrorKind { - /// Initialization failed for a reason that also exists on the chain. - #[error("failed due to a valid chain error: {:?}", 0)] + /// Initialization during execution. + #[error("Failed with an execution error: {failure_kind:?}")] ExecutionError { /// The reason for why the contract initialization failed. failure_kind: InitFailure, }, /// Ran out of energy. - #[error("ran out of energy")] + #[error("Ran out of energy")] OutOfEnergy, /// Module has not been deployed in the test environment. - #[error("module {:?} does not exist", 0.0)] - ModuleDoesNotExist(#[from] ModuleMissing), + #[error("{0}")] + ModuleDoesNotExist(#[from] ModuleDoesNotExist), /// The sender account has not been created in test environment. - #[error("sender account {} does not exist", 0.0)] - SenderDoesNotExist(#[from] AccountMissing), + #[error("Sender missing: {0}")] + SenderDoesNotExist(#[from] AccountDoesNotExist), /// The invoker account does not have enough funds to pay for the energy. - #[error("invoker does not have enough funds to pay for the energy")] + #[error("Invoker does not have enough funds to pay for the energy")] InsufficientFunds, /// The parameter is too large. - #[error("the provided parameter exceeds the max size allowed")] + #[error("The provided parameter exceeds the maximum size allowed")] ParameterTooLarge, } @@ -265,18 +266,17 @@ pub enum InitFailure { return_value: ReturnValue, }, /// The contract trapped. - Trap { error: TrapError }, + Trap { error: ExecutionError }, /// The contract ran out of energy. OutOfEnergy, } -/// The error produced when a contract traps. +/// An error that occured while executing a contract init or receive function. #[derive(Debug, Error)] -#[error("The contract trapped due to: {0}")] -pub struct TrapError(pub(crate) anyhow::Error); +#[error("The contract execution halted due to: {0}")] +pub struct ExecutionError(pub(crate) anyhow::Error); /// Represents a successful contract update (or invocation). -// TODO: Consider adding function to aggregate all logs from the host_events. #[derive(Debug)] pub struct ContractInvocationSuccess { /// Host events that occured. This includes interrupts, resumes, and @@ -313,33 +313,33 @@ pub struct ContractInvocationError { /// [`Chain::contract_invoke`]. #[derive(Debug, Error)] pub enum ContractInvocationErrorKind { - /// Update failed for a reason that also exists on the chain. - #[error("failed during execution")] + /// Invocation failed during execution. + #[error("Failed during execution: {failure_kind:?}")] ExecutionError { failure_kind: v1::InvokeFailure }, /// Ran out of energy. - #[error("ran out of energy")] + #[error("Ran out of energy")] OutOfEnergy, /// Module has not been deployed in test environment. - #[error("module {:?} does not exist", 0.0)] - ModuleDoesNotExist(#[from] ModuleMissing), + #[error("{0}")] + ModuleDoesNotExist(#[from] ModuleDoesNotExist), /// Contract instance has not been initialized in the test environment. - #[error("instance {} does not exist", 0.0)] - InstanceDoesNotExist(#[from] ContractInstanceMissing), + #[error("{0}")] + ContractDoesNotExist(#[from] ContractDoesNotExist), /// Entrypoint does not exist and neither does the fallback entrypoint. - #[error("entrypoint does not exist")] + #[error("{0}")] EntrypointDoesNotExist(#[from] EntrypointDoesNotExist), /// The invoker account has not been created in the test environment. - #[error("invoker account {} does not exist", 0.0)] - InvokerDoesNotExist(#[from] AccountMissing), + #[error("Invoker missing: {0}")] + InvokerDoesNotExist(#[from] AccountDoesNotExist), /// The sender does not exist in the test environment. - #[error("sender {0} does not exist")] + #[error("Sender missing: the object with address '{0}' does not exist")] SenderDoesNotExist(Address), /// The invoker account does not have enough funds to pay for the energy and /// amount sent. - #[error("invoker does not have enough funds to pay for the energy")] + #[error("Invoker does not have enough funds to pay for the energy")] InsufficientFunds, /// The parameter is too large. - #[error("the provided parameter exceeds the max size allowed")] + #[error("The provided parameter exceeds the maximum size allowed")] ParameterTooLarge, } @@ -361,20 +361,32 @@ pub(crate) enum TransferError { /// The entrypoint does not exist. #[derive(PartialEq, Eq, Debug, Error)] -#[error("The entrypoint '{0}' does not exist.")] -pub struct EntrypointDoesNotExist(pub OwnedEntrypointName); +#[error("Entrypoint '{entrypoint}' does not exist.")] +pub struct EntrypointDoesNotExist { + /// The missing entrypoint. + pub entrypoint: OwnedEntrypointName, +} /// The contract module does not exist. #[derive(Debug, Error)] -#[error("Module {:?} does not exist.", 0)] -pub struct ModuleMissing(pub ModuleReference); +#[error("Module '{module_reference}' does not exist.")] +pub struct ModuleDoesNotExist { + /// The reference of the missing module. + pub module_reference: ModuleReference, +} /// The contract instance does not exist. #[derive(Debug, Error)] -#[error("Contract instance {0} does not exist.")] -pub struct ContractInstanceMissing(pub ContractAddress); +#[error("Contract instance '{address}' does not exist.")] +pub struct ContractDoesNotExist { + /// The address of the missing contract. + pub address: ContractAddress, +} /// The account does not exist. #[derive(Debug, Error)] -#[error("Account {0} does not exist.")] -pub struct AccountMissing(pub AccountAddress); +#[error("Account '{address}' does not exist.")] +pub struct AccountDoesNotExist { + /// The address of the missing account. + pub address: AccountAddress, +} From 3973e155db959fd39c5898430d5aaf85b4a1a6c9 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Mar 2023 08:08:02 +0100 Subject: [PATCH 098/208] Fix parameter size check --- contract-testing/src/impls.rs | 8 ++--- .../tests/relaxed_restrictions.rs | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 713131c2..cdf05995 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -294,8 +294,8 @@ impl Chain { return Err(ContractInitErrorKind::InsufficientFunds); } - // Ensure that the parameter has a valid size (+2 for the length of parameter). - if payload.param.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { + // Ensure that the parameter has a valid size. + if payload.param.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { return Err(ContractInitErrorKind::ParameterTooLarge); } @@ -468,8 +468,8 @@ impl Chain { remaining_energy: &mut Energy, should_persist: bool, ) -> Result { - // Ensure that the parameter has a valid size (+2 for the length of parameter). - if payload.message.as_ref().len() + 2 > contracts_common::constants::MAX_PARAMETER_LEN { + // Ensure that the parameter has a valid size. + if payload.message.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { return Err(self.from_invocation_error_kind( ContractInvocationErrorKind::ParameterTooLarge, energy_reserved, diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 8991e4cc..4885f2a4 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -16,10 +16,31 @@ use concordium_smart_contract_testing::*; const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; const ACC_0: AccountAddress = AccountAddress([0; 32]); -/// Test the new parameter size limit. +/// Test the new parameter size limit on both init and update. #[test] fn test_new_parameter_limit() { - let (mut chain, contract_address) = deploy_and_init(); + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let parameter = mk_parameter(65535, 65535); + + let res_deploy = chain + .module_deploy_v1( + ACC_0, + Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init(ACC_0, Energy::from(80000), InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: parameter.clone(), // Check parameter size limit on init. + amount: Amount::zero(), + }) + .expect("Initializing valid contract should work"); chain .contract_update( @@ -27,9 +48,9 @@ fn test_new_parameter_limit() { Address::Account(ACC_0), Energy::from(700000), UpdateContractPayload { - address: contract_address, + address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("relax.param".into()), - message: mk_parameter(65535, 65535), + message: parameter, // Check parameter size limit on updates. amount: Amount::zero(), }, ) @@ -121,7 +142,6 @@ fn deploy_and_init() -> (Chain, ContractAddress) { fn mk_parameter(internal_param_size: u16, desired_size: u32) -> OwnedParameter { let entrypoint = OwnedEntrypointName::new_unchecked("param-aux".into()); let filler_size = desired_size - - 2 // length of the parameter itself - 2 // internal_param_size - 2 // entrypoint name len - 9 // entrypoint name From a5827a8ae4880cdb2795eb090dcb1bbb58e7d5b4 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 20 Mar 2023 15:07:44 +0100 Subject: [PATCH 099/208] Check validity of exchange rates --- contract-testing/src/impls.rs | 145 +++++++++++++++++++++--------- contract-testing/src/types.rs | 24 +++-- contract-testing/tests/queries.rs | 2 +- 3 files changed, 122 insertions(+), 49 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index cdf05995..ddcdd843 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -2,6 +2,7 @@ use crate::{constants, invocation::EntrypointInvocationHandler, types::*}; use concordium_base::{ base::{self, Energy, OutOfEnergy}, common::{self, to_bytes}, + constants::MAX_ALLOWED_INVOKE_ENERGY, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, SlotTime, Timestamp, @@ -20,12 +21,18 @@ impl Default for Chain { impl Chain { /// Create a new [`Self`] where all the configurable parameters are /// provided. + /// + /// Returns an error if the exchange rates provided makes one energy cost + /// more than `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, euro_per_energy: ExchangeRate, - ) -> Self { - Self { + ) -> Result { + // Ensure the exchange rates are within a valid range. + check_exchange_rates(euro_per_energy, micro_ccd_per_euro)?; + + Ok(Self { block_time, micro_ccd_per_euro, euro_per_energy, @@ -33,7 +40,7 @@ impl Chain { modules: BTreeMap::new(), contracts: BTreeMap::new(), next_contract_index: 0, - } + }) } /// Create a new [`Self`] with a specified `block_time` where @@ -48,14 +55,17 @@ impl Chain { /// Create a new [`Self`] where /// - `block_time` defaults to `0`, - /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. + /// + /// With these exchange rates, one energy costs one microCCD. pub fn new() -> Self { Self::new_with_time_and_rates( Timestamp::from_timestamp_millis(0), - ExchangeRate::new_unchecked(147235241, 1), + ExchangeRate::new_unchecked(50000, 1), ExchangeRate::new_unchecked(1, 50000), ) + .expect("Rates known to be within range.") } /// Deploy a smart contract module. @@ -883,6 +893,28 @@ impl Chain { pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { energy_to_amount(energy, self.euro_per_energy, self.micro_ccd_per_euro) } + + /// Try to set the exchange rates on the chain. + /// + /// Will fail if they result in the cost of one energy being larger than + /// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + pub fn set_exchange_rate( + &mut self, + micro_ccd_per_euro: ExchangeRate, + euro_per_energy: ExchangeRate, + ) -> Result<(), ExchangeRateError> { + // Ensure the exchange rates are within a valid range. + check_exchange_rates(euro_per_energy, micro_ccd_per_euro)?; + self.micro_ccd_per_euro = micro_ccd_per_euro; + self.euro_per_energy = euro_per_energy; + Ok(()) + } + + /// Return the current microCCD per euro exchange rate. + pub fn micro_ccd_per_euro(&self) -> ExchangeRate { self.micro_ccd_per_euro } + + /// Return the current euro per energy exchange rate. + pub fn euro_per_energy(&self) -> ExchangeRate { self.euro_per_energy } } impl TestPolicies { @@ -1028,17 +1060,21 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange /// rates provided. -// To find the mCCD/NRG exchange rate: -// -// euro mCCD euro * mCCD mCCD -// ---- * ---- = ----------- = ---- -// NRG euro NRG * euro NRG -// -// To convert the `energy` parameter to mCCD: -// -// mCCD NRG * mCCD -// NRG * ---- = ---------- = mCCD -// NRG NRG +/// +/// To find the mCCD/NRG exchange rate: +/// ```ignore +/// euro mCCD euro * mCCD mCCD +/// ---- * ---- = ----------- = ---- +/// NRG euro NRG * euro NRG +/// ``` +/// +/// To convert the `energy` parameter to mCCD: +/// ```ignore +/// +/// mCCD NRG * mCCD +/// NRG * ---- = ---------- = mCCD +/// NRG NRG +/// ``` pub(crate) fn energy_to_amount( energy: Energy, euro_per_energy: ExchangeRate, @@ -1050,39 +1086,66 @@ pub(crate) fn energy_to_amount( BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); let cost: BigUint = (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; - let cost: u64 = u64::try_from(cost).expect("Should never overflow due to use of BigUint"); + let cost: u64 = u64::try_from(cost).expect( + "Should never overflow since reasonable exchange rates are ensured when constructing the \ + [`Chain`].", + ); Amount::from_micro_ccd(cost) } +/// Helper function that checks the validity of the exchange rates. +/// +/// More specifically, it checks that the cost of one energy is <= `u64::MAX / +/// MAX_ALLOWED_INVOKE_ENERGY`, which ensures that overflows won't occur. +fn check_exchange_rates( + euro_per_energy: ExchangeRate, + micro_ccd_per_euro: ExchangeRate, +) -> Result<(), ExchangeRateError> { + let micro_ccd_per_energy_numerator: BigUint = + BigUint::from(euro_per_energy.numerator()) * micro_ccd_per_euro.numerator(); + let micro_ccd_per_energy_denominator: BigUint = + BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); + let max_allowed_micro_ccd_to_energy = u64::MAX / MAX_ALLOWED_INVOKE_ENERGY.energy; + let micro_ccd_per_energy = + u64::try_from(micro_ccd_per_energy_numerator / micro_ccd_per_energy_denominator) + .map_err(|_| ExchangeRateError)?; + if micro_ccd_per_energy > max_allowed_micro_ccd_to_energy { + return Err(ExchangeRateError); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; - const ACC_0: AccountAddress = AccountAddress([0; 32]); - const ACC_1: AccountAddress = AccountAddress([1; 32]); - + /// A few checks that test whether the function behavior matches its doc + /// comments. #[test] - fn calculate_cost_will_not_overflow() { - let micro_ccd_per_euro = ExchangeRate::new_unchecked(u64::MAX, u64::MAX - 1); - let euro_per_energy = ExchangeRate::new_unchecked(u64::MAX - 2, u64::MAX - 3); - let energy = Energy::from(u64::MAX - 4); - energy_to_amount(energy, euro_per_energy, micro_ccd_per_euro); - } + fn check_exchange_rates_works() { + let max_allowed_micro_ccd_per_energy = u64::MAX / MAX_ALLOWED_INVOKE_ENERGY.energy; + check_exchange_rates( + ExchangeRate::new_unchecked(max_allowed_micro_ccd_per_energy + 1, 1), + ExchangeRate::new_unchecked(1, 1), + ) + .expect_err("should fail"); - #[test] - fn creating_accounts_work() { - let mut chain = Chain::new(); - chain.create_account(ACC_0, Account::new(Amount::from_ccd(1))); - chain.create_account(ACC_1, Account::new(Amount::from_ccd(2))); - - assert_eq!(chain.accounts.len(), 2); - assert_eq!( - chain.account_balance_available(ACC_0), - Some(Amount::from_ccd(1)) - ); - assert_eq!( - chain.account_balance_available(ACC_1), - Some(Amount::from_ccd(2)) - ); + check_exchange_rates( + ExchangeRate::new_unchecked(max_allowed_micro_ccd_per_energy / 2 + 1, 1), + ExchangeRate::new_unchecked(2, 1), + ) + .expect_err("should fail"); + + check_exchange_rates( + ExchangeRate::new_unchecked(max_allowed_micro_ccd_per_energy, 1), + ExchangeRate::new_unchecked(1, 1), + ) + .expect("should succeed"); + + check_exchange_rates( + ExchangeRate::new_unchecked(50000, 1), + ExchangeRate::new_unchecked(1, 50000), + ) + .expect("should succeed"); } } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 81fa2bf0..34248f9e 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -31,19 +31,23 @@ pub struct ContractModule { pub struct Chain { /// The block time viewable inside the smart contracts. /// Defaults to `0`. - pub block_time: SlotTime, + pub block_time: SlotTime, /// MicroCCD per Euro ratio. - pub micro_ccd_per_euro: ExchangeRate, + // This is not public because we ensure a reasonable value during the construction of the + // [`Chain`]. + pub(crate) micro_ccd_per_euro: ExchangeRate, /// Euro per Energy ratio. - pub euro_per_energy: ExchangeRate, + // This is not public because we ensure a reasonable value during the construction of the + // [`Chain`]. + pub(crate) euro_per_energy: ExchangeRate, /// Accounts and info about them. - pub accounts: BTreeMap, + pub accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap, + pub modules: BTreeMap, /// Smart contract instances. - pub contracts: BTreeMap, + pub contracts: BTreeMap, /// Next contract index to use when creating a new instance. - pub next_contract_index: u64, + pub next_contract_index: u64, } /// A smart contract instance. @@ -390,3 +394,9 @@ pub struct AccountDoesNotExist { /// The address of the missing account. pub address: AccountAddress, } + +/// The provided exchange rates are not valid. +/// Meaning that they do not correspond to one energy costing less than +/// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. +#[derive(Debug)] +pub struct ExchangeRateError; diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 7feb9a79..3e114d20 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -609,7 +609,7 @@ mod query_exchange_rates { .expect("Initializing valid contract should work"); // Non-existent contract address. - let input_param = (chain.euro_per_energy, chain.micro_ccd_per_euro); + let input_param = (chain.euro_per_energy(), chain.micro_ccd_per_euro()); let res_update = chain .contract_update( From 24e67cc96a4d709d515b7f491605ac4159c7f63e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 21 Mar 2023 11:03:39 +0100 Subject: [PATCH 100/208] Restrict module loading to valid sizes and revise errors --- contract-testing/src/impls.rs | 57 +++++++++++++++++++++++++---------- contract-testing/src/types.rs | 54 ++++++++++++++++++++------------- 2 files changed, 74 insertions(+), 37 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index ddcdd843..66920833 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,8 +1,9 @@ use crate::{constants, invocation::EntrypointInvocationHandler, types::*}; +use anyhow::anyhow; use concordium_base::{ base::{self, Energy, OutOfEnergy}, common::{self, to_bytes}, - constants::MAX_ALLOWED_INVOKE_ENERGY, + constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, SlotTime, Timestamp, @@ -149,7 +150,7 @@ impl Chain { Ok(artifact) => artifact, Err(err) => { return Err(ModuleDeployError { - kind: InvalidModuleError(err).into(), + kind: ModuleInvalidError(err).into(), energy_used: check_header_energy, transaction_fee: check_header_cost, }) @@ -200,14 +201,33 @@ impl Chain { module_path: impl AsRef, ) -> Result { let module_path = module_path.as_ref(); - let file_contents = - std::fs::read(module_path).map_err(|e| ModuleLoadError::ReadFileError { - path: module_path.to_path_buf(), - error: e, + // To avoid reading a giant file, we open the file for reading, check its size + // and then load the contents. + let (mut reader, metadata) = std::fs::File::open(module_path) + .and_then(|reader| reader.metadata().map(|metadata| (reader, metadata))) + .map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: e.into(), })?; + if metadata.len() > MAX_WASM_MODULE_SIZE.into() { + return Err(ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::ReadModule( + anyhow!("Maximum size of a Wasm module is {}", MAX_WASM_MODULE_SIZE).into(), + ), + }); + } + // We cannot deserialize directly to [`ModuleSource`] as it expects the first + // four bytes to be the length, which is isn't for this raw file. + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut reader, &mut buffer).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::OpenFile(e.into()), /* This is unlikely to happen, since + * we already opened it. */ + })?; Ok(WasmModule { version: WasmVersion::V1, - source: ModuleSource::from(file_contents), + source: ModuleSource::from(buffer), }) } @@ -216,16 +236,21 @@ impl Chain { /// bytes. pub fn module_load_v1(module_path: impl AsRef) -> Result { let module_path = module_path.as_ref(); - let file_contents = - std::fs::read(module_path).map_err(|e| ModuleLoadError::ReadFileError { - path: module_path.to_path_buf(), - error: e, - })?; - let mut cursor = std::io::Cursor::new(file_contents); - let module: WasmModule = - common::from_bytes(&mut cursor).map_err(|e| InvalidModuleError(e))?; + // To avoid reading a giant file, we just open the file for reading and then + // parse it as a wasm module, which checks the length up front. + let mut reader = std::fs::File::open(module_path).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: e.into(), + })?; + let module: WasmModule = common::from_bytes(&mut reader).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::ReadModule(e.into()), + })?; if module.version != WasmVersion::V1 { - return Err(ModuleLoadError::UnsupportedModuleVersion(module.version)); + return Err(ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::UnsupportedModuleVersion(module.version), + }); } Ok(module) } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 34248f9e..a3c56ada 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -168,45 +168,57 @@ pub struct ModuleDeployError { #[derive(Debug, Error)] pub enum ModuleDeployErrorKind { /// The module provided is not valid. - #[error("module is invalid due to: {0}")] - InvalidModule(#[from] InvalidModuleError), + #[error("Module is invalid due to: {0}")] + InvalidModule(#[from] ModuleInvalidError), /// The sender account does not have sufficient funds to pay for the /// deployment. - #[error("sender does not have sufficient funds to pay for the energy")] + #[error("Sender does not have sufficient funds to pay for the energy")] InsufficientFunds, /// The sender account deploying the module does not exist. - #[error("sender account {} does not exist", 0.0)] + #[error("Sender account {} does not exist", 0.0)] SenderDoesNotExist(#[from] AccountDoesNotExist), /// The module has already been deployed. - #[error("module with reference {0} already exists")] + #[error("Module with reference {0} already exists")] DuplicateModule(ModuleReference), /// The module version is not supported. - #[error("wasm version {0} is not supported")] + #[error("Wasm version {0} is not supported")] UnsupportedModuleVersion(WasmVersion), } /// An error that can occur while loading a smart contract module. #[derive(Debug, Error)] -pub enum ModuleLoadError { - /// Failed to read the module file. - #[error("could not read the file '{path}' due to: {error}")] - ReadFileError { - path: PathBuf, - error: std::io::Error, - }, +#[error("Could not load the module file '{path}' due to: {kind}")] +pub struct ModuleLoadError { + /// The module file. + pub path: PathBuf, + /// The reason why loading the module failed. + pub kind: ModuleLoadErrorKind, +} + +/// The specific reason why loading a module failed. +#[derive(Debug, Error)] +pub enum ModuleLoadErrorKind { + /// Failed to open the module file for reading. + #[error("Could not open the file for reading to: {0}")] + OpenFile(#[from] std::io::Error), + /// Failed to read the module from the file. + #[error("Could not read the module due to: {0}")] + ReadModule(#[from] ModuleReadError), /// The module version is not supported. - #[error("wasm version {0} is not supported")] + #[error("The module has wasm version {0}, which is not supported")] UnsupportedModuleVersion(WasmVersion), - /// The module provided is not valid. - #[error("module is invalid due to: {0}")] - InvalidModule(#[from] InvalidModuleError), } -/// The error produced when trying to load or deploy an invalid smart contract -/// module. +/// The error produced when trying to read a smart contract +/// module from a file. +#[derive(Debug, Error)] +#[error("The module could not be read due to: {0}")] +pub struct ModuleReadError(#[from] pub(crate) anyhow::Error); + +/// The error produced when trying to parse a smart contract module. #[derive(Debug, Error)] -#[error("The module is invalid due to: {0}")] -pub struct InvalidModuleError(pub(crate) anyhow::Error); +#[error("The module is invalid to: {0}")] +pub struct ModuleInvalidError(#[from] pub(crate) anyhow::Error); /// Represents a successful initialization of a contract. #[derive(Debug)] From b715c18211e071d883dbd78b67118bf72f537ab5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 21 Mar 2023 16:10:51 +0100 Subject: [PATCH 101/208] Rework OutOfEnergy handling and convert interpreter errors to traps --- contract-testing/src/impls.rs | 44 ++- contract-testing/src/invocation/impls.rs | 384 +++++++++++------------ contract-testing/src/invocation/types.rs | 24 +- 3 files changed, 213 insertions(+), 239 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 66920833..e1345f55 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -512,26 +512,15 @@ impl Chain { )); } let contract_address = payload.address; - let energy_given_to_interpreter = *remaining_energy; let (result, changeset, chain_events) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( self, invoker, sender, - energy_given_to_interpreter, + remaining_energy, payload, - ); - - // Update the remaining energy. Subtract in interpreter energy, then convert to - // energy. Otherwise this might be slightly different than the node due to - // rounding. - let energy_used = from_interpreter_energy( - to_interpreter_energy(energy_given_to_interpreter) - .saturating_sub(result.remaining_energy), - ); - remaining_energy.tick_energy(energy_used).map_err(|error| { - self.from_invocation_error_kind(error.into(), energy_reserved, *remaining_energy) - })?; + ) + .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. @@ -547,19 +536,13 @@ impl Chain { changeset.collect_energy_for_state(*remaining_energy, contract_address) }; - let (energy_for_state_increase, state_changed) = res.map_err(|error| { - self.from_invocation_error_kind(error.into(), energy_reserved, *remaining_energy) - })?; + let (energy_for_state_increase, state_changed) = + res.map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; + // Charge for the potential state size increase. remaining_energy .tick_energy(energy_for_state_increase) - .map_err(|error| { - self.from_invocation_error_kind( - error.into(), - energy_reserved, - *remaining_energy, - ) - })?; + .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; state_changed } else { @@ -877,7 +860,7 @@ impl Chain { } } - /// Convert a [`ContractInovcationErrorKind`] to a + /// Convert a [`ContractInvocationErrorKind`] to a /// [`ContractInvocationError`] by calculating the `energy_used` and /// `transaction_fee`. fn from_invocation_error_kind( @@ -895,6 +878,17 @@ impl Chain { } } + /// Construct a [`ContractInvocationError`] of the `OutOfEnergy` kind with + /// the energy and transaction fee fields based on the `energy_reserved` + /// parameter. + fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvocationError { + self.from_invocation_error_kind( + ContractInvocationErrorKind::OutOfEnergy, + energy_reserved, + Energy::from(0), + ) + } + /// Convert a [`ContractInitErrorKind`] to a /// [`ContractInitError`] by calculating the `energy_used` and /// `transaction_fee`. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 48ea3ecb..b406c545 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -24,7 +24,7 @@ use concordium_smart_contract_engine::{ use concordium_wasm::artifact; use std::collections::{btree_map, BTreeMap}; -impl EntrypointInvocationHandler { +impl<'a> EntrypointInvocationHandler<'a> { /// Invoke an entrypoint and get the result, [`Changeset`], and chain /// events. /// @@ -38,30 +38,26 @@ impl EntrypointInvocationHandler { chain: &Chain, invoker: AccountAddress, sender: Address, - reserved_energy: Energy, + remaining_energy: &'a mut Energy, payload: UpdateContractPayload, - ) -> (InvokeEntrypointResult, ChangeSet, Vec) { + ) -> Result<(InvokeEntrypointResult, ChangeSet, Vec), OutOfEnergy> { let mut contract_invocation = Self { - changeset: ChangeSet::new(), - accounts: chain.accounts.clone(), /* TODO: These three maps should ideally - * be - * immutable references. */ - modules: chain.modules.clone(), - contracts: chain.contracts.clone(), - block_time: chain.block_time, - euro_per_energy: chain.euro_per_energy, + changeset: ChangeSet::new(), + remaining_energy, + accounts: chain.accounts.clone(), /* TODO: These three maps should ideally + * be + * immutable references. */ + modules: chain.modules.clone(), + contracts: chain.contracts.clone(), + block_time: chain.block_time, + euro_per_energy: chain.euro_per_energy, micro_ccd_per_euro: chain.micro_ccd_per_euro, }; let mut chain_events = Vec::new(); - let result = contract_invocation.invoke_entrypoint( - invoker, - sender, - reserved_energy, - payload, - &mut chain_events, - ); - (result, contract_invocation.changeset, chain_events) + let result = + contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut chain_events)?; + Ok((result, contract_invocation.changeset, chain_events)) } /// Used for handling contract entrypoint invocations internally. @@ -76,18 +72,12 @@ impl EntrypointInvocationHandler { &mut self, invoker: AccountAddress, sender: Address, - mut remaining_energy: Energy, payload: UpdateContractPayload, chain_events: &mut Vec, - ) -> InvokeEntrypointResult { + ) -> Result { // Charge the base cost for updating a contract. - remaining_energy = if let Some(remaining_energy) = - remaining_energy.checked_sub(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST) - { - remaining_energy - } else { - return InvokeEntrypointResult::new_out_of_energy_failure(); - }; + self.remaining_energy + .tick_energy(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST)?; // Move the amount from the sender to the contract, if any. // And get the new self_balance. @@ -112,11 +102,10 @@ impl EntrypointInvocationHandler { TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. - return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { kind }, - logs: v0::Logs::new(), - remaining_energy: to_interpreter_energy(remaining_energy), - }; + return Ok(InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { kind }, + logs: v0::Logs::new(), + }); } } } else { @@ -126,13 +115,12 @@ impl EntrypointInvocationHandler { // Return early. // TODO: For the top-most update, we should catch this in `contract_update` and // return `ContractUpdateError::EntrypointDoesNotExist`. - return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { + return Ok(InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, - logs: v0::Logs::new(), - remaining_energy: to_interpreter_energy(remaining_energy), - }; + logs: v0::Logs::new(), + }); } } }; @@ -172,24 +160,18 @@ impl EntrypointInvocationHandler { ) } else { // Return early. - return InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { + return Ok(InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, - logs: v0::Logs::new(), - remaining_energy: to_interpreter_energy(remaining_energy), - }; + logs: v0::Logs::new(), + }); } }; // Subtract the cost of looking up the module - remaining_energy = if let Some(remaining_energy) = - remaining_energy.checked_sub(lookup_module_cost(&module)) - { - remaining_energy - } else { - return InvokeEntrypointResult::new_out_of_energy_failure(); - }; + self.remaining_energy + .tick_energy(lookup_module_cost(&module))?; // Construct the receive context let receive_ctx = v1::ReceiveContext { @@ -222,7 +204,7 @@ impl EntrypointInvocationHandler { let instance_state = v1::InstanceState::new(loader, inner); // Get the initial result from invoking receive - let initial_result = run_interpreter(remaining_energy, |energy| { + let initial_result = self.run_interpreter(|energy| { v1::invoke_receive( module.artifact, receive_ctx, @@ -240,7 +222,7 @@ impl EntrypointInvocationHandler { support_queries: true, }, ) - }); + })?; // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. @@ -256,67 +238,51 @@ impl EntrypointInvocationHandler { }; // Process the receive invocation to the completion. - let result = data.process(initial_result); + let result = data.process(initial_result)?; let mut new_chain_events = data.chain_events; let result = match result { - Ok(r) => match r { - v1::ReceiveResult::Success { - logs, - state_changed: _, /* This only reflects changes since last interrupt, we use - * the changeset later to get a more precise result. */ - return_value, - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Success { - new_balance: self.contract_balance_unchecked(payload.address), - data: Some(return_value), - }, - logs, - remaining_energy, - } - } - v1::ReceiveResult::Interrupt { .. } => { - panic!("Internal error: `data.process` returned an interrupt.") - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, - }, - logs: v0::Logs::new(), - remaining_energy, - } - } - v1::ReceiveResult::Trap { - error: _, // TODO: Should we return this to the user? - remaining_energy, - } => { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - logs: v0::Logs::new(), - remaining_energy, - } - } - v1::ReceiveResult::OutOfEnergy => { - InvokeEntrypointResult::new_out_of_energy_failure() - } + v1::ReceiveResult::Success { + logs, + state_changed: _, /* This only reflects changes since last interrupt, we use + * the changeset later to get a more precise result. */ + return_value, + remaining_energy: _, + } => InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Success { + new_balance: self.contract_balance_unchecked(payload.address), + data: Some(return_value), + }, + logs, + }, + v1::ReceiveResult::Interrupt { .. } => { + panic!("Internal error: `data.process` returned an interrupt.") + } + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy: _, + } => InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + logs: v0::Logs::new(), + }, + v1::ReceiveResult::Trap { + error: _, // TODO: Should we return this to the user? + remaining_energy: _, + } => InvokeEntrypointResult { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: v0::Logs::new(), }, - Err(internal_error) => { - panic!("Internal error: Got interpreter error {}", internal_error) + v1::ReceiveResult::OutOfEnergy => { + // Convert to an error so that we will short-circuit the processing. + return Err(OutOfEnergy); } }; @@ -325,7 +291,7 @@ impl EntrypointInvocationHandler { chain_events.append(&mut new_chain_events); } - result + Ok(result) } /// Make a transfer from a contract to an account in the changeset. @@ -704,21 +670,38 @@ impl EntrypointInvocationHandler { /// Roll back to the previous checkpoint. fn rollback(&mut self) { self.changeset.rollback(); } -} -/// Run the interpreter with the provided function and energy. -/// -/// This function ensures that the energy calculations is handled as in the -/// node. -fn run_interpreter( - mut available_energy: Energy, - f: F, -) -> ExecResult> -where - F: FnOnce(InterpreterEnergy) -> ExecResult>, { - let available_interpreter_energy = to_interpreter_energy(available_energy); - let res = f(available_interpreter_energy); - if let Ok(res) = res { + /// Update the `remaining_energy` field by converting the input to + /// [`InterpreterEnergy`] and then [`Energy`]. + fn update_energy(&mut self, remaining_energy: u64) { + *self.remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + } + + /// Run the interpreter with the provided function and the + /// `self.remaining_energy`. + /// + /// This function ensures that the energy calculations is handled as in the + /// node. + fn run_interpreter( + &mut self, + f: F, + ) -> Result, OutOfEnergy> + where + F: FnOnce(InterpreterEnergy) -> ExecResult>, + { + let available_interpreter_energy = to_interpreter_energy(*self.remaining_energy); + let res = match f(available_interpreter_energy) { + Ok(res) => res, + Err(err) => { + // An error occured in the interpreter and it doesn't return the remanining + // energy. We convert this to a trap and set the energy to the + // last known amount. + return Ok(v1::ReceiveResult::Trap { + error: err, + remaining_energy: available_interpreter_energy.energy, + }); + } + }; let mut subtract_then_convert = |remaining_energy| -> Result { let remaining_energy = InterpreterEnergy::from(remaining_energy); // Using `saturating_sub` here should be ok since we should never be able to use @@ -726,8 +709,8 @@ where let used_energy = from_interpreter_energy( available_interpreter_energy.saturating_sub(remaining_energy), ); - available_energy.tick_energy(used_energy)?; - Ok(to_interpreter_energy(available_energy).energy) + self.remaining_energy.tick_energy(used_energy)?; + Ok(to_interpreter_energy(*self.remaining_energy).energy) }; match res { v1::ReceiveResult::Success { @@ -771,10 +754,9 @@ where error, remaining_energy: subtract_then_convert(remaining_energy)?, }), - v1::ReceiveResult::OutOfEnergy => Ok(v1::ReceiveResult::OutOfEnergy), + // Convert this to an error so that we will short-circuit the processing. + v1::ReceiveResult::OutOfEnergy => Err(OutOfEnergy), } - } else { - res } } @@ -1051,7 +1033,7 @@ impl AccountChanges { } } -impl<'a> InvocationData<'a> { +impl<'a, 'b> InvocationData<'a, 'b> { /// Process a receive function until completion. /// /// **Preconditions**: @@ -1059,18 +1041,8 @@ impl<'a> InvocationData<'a> { /// - Account exists in `invocation_handler.accounts`. fn process( &mut self, - res: ExecResult>, - ) -> ExecResult> { - let receive_result = match res { - Ok(receive_result) => receive_result, - Err(e) => { - if e.downcast_ref::().is_some() { - return Ok(v1::ReceiveResult::OutOfEnergy); - } else { - return Err(e); - } - } - }; + receive_result: v1::ReceiveResult, + ) -> Result, OutOfEnergy> { match receive_result { v1::ReceiveResult::Success { logs, @@ -1078,6 +1050,9 @@ impl<'a> InvocationData<'a> { return_value, remaining_energy, } => { + // Update the remaining_energy field. + self.invocation_handler.update_energy(remaining_energy); + let update_event = ChainEvent::Updated { address: self.address, contract: self.contract_name.clone(), @@ -1110,6 +1085,8 @@ impl<'a> InvocationData<'a> { config, interrupt, } => { + // Update the remaining_energy field. + self.invocation_handler.update_energy(remaining_energy); // Create the interrupt event, which will be included for transfers, calls, and // upgrades, but not for the remaining interrupts. let interrupt_event = ChainEvent::Interrupted { @@ -1157,21 +1134,22 @@ impl<'a> InvocationData<'a> { success, }); - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy + self.invocation_handler + .remaining_energy .tick_energy(concordium_base::transactions::cost::SIMPLE_TRANSFER)?; - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, energy, &mut self.state, false, // never changes on transfers + // An empty loader is fine currently, as we do not use caching + // in this lib. v1::trie::Loader::new(&[][..]), ) - }); + })?; // Resume self.process(resume_res) @@ -1209,11 +1187,10 @@ impl<'a> InvocationData<'a> { { // The contract to call does not exist. None => InvokeEntrypointResult { - invoke_response: v1::InvokeResponse::Failure { + invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, - logs: v0::Logs::new(), - remaining_energy: InterpreterEnergy::from(remaining_energy), + logs: v0::Logs::new(), }, Some(contract_name) => { let receive_name = OwnedReceiveName::construct_unchecked( @@ -1224,9 +1201,6 @@ impl<'a> InvocationData<'a> { self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), - from_interpreter_energy(InterpreterEnergy::from( - remaining_energy, - )), UpdateContractPayload { amount, address, @@ -1234,7 +1208,7 @@ impl<'a> InvocationData<'a> { message, }, &mut self.chain_events, - ) + )? } }; @@ -1265,19 +1239,16 @@ impl<'a> InvocationData<'a> { self.chain_events.push(resume_event); - let resume_res = run_interpreter( - from_interpreter_energy(res.remaining_energy), - |energy| { - v1::resume_receive( - config, - res.invoke_response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - }, - ); + let resume_res = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + res.invoke_response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + })?; self.process(resume_res) } @@ -1285,11 +1256,9 @@ impl<'a> InvocationData<'a> { // Add the interrupt event. self.chain_events.push(interrupt_event); - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - // Charge a base cost. - remaining_energy + self.invocation_handler + .remaining_energy .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; let response = match self.invocation_handler.modules.get(&module_ref) { @@ -1298,7 +1267,9 @@ impl<'a> InvocationData<'a> { }, Some(module) => { // Charge for the module lookup. - remaining_energy.tick_energy(lookup_module_cost(module))?; + self.invocation_handler + .remaining_energy + .tick_energy(lookup_module_cost(module))?; if module.artifact.export.contains_key( self.contract_name.as_contract_name().get_chain_name(), @@ -1309,7 +1280,7 @@ impl<'a> InvocationData<'a> { .save_module_upgrade(self.address, module_ref); // Charge for the initialization cost. - remaining_energy.tick_energy( + self.invocation_handler.remaining_energy.tick_energy( constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, )?; @@ -1341,7 +1312,7 @@ impl<'a> InvocationData<'a> { success, }); - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1350,7 +1321,7 @@ impl<'a> InvocationData<'a> { state_changed, v1::trie::Loader::new(&[][..]), ) - }); + })?; self.process(resume_res) } @@ -1367,12 +1338,11 @@ impl<'a> InvocationData<'a> { }, }; - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy + self.invocation_handler + .remaining_energy .tick_energy(constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST)?; - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1381,7 +1351,7 @@ impl<'a> InvocationData<'a> { false, // State never changes on queries. v1::trie::Loader::new(&[][..]), ) - }); + })?; self.process(resume_res) } @@ -1400,13 +1370,11 @@ impl<'a> InvocationData<'a> { }, }; - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy.tick_energy( + self.invocation_handler.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, )?; - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1415,7 +1383,7 @@ impl<'a> InvocationData<'a> { false, // State never changes on queries. v1::trie::Loader::new(&[][..]), ) - }); + })?; self.process(resume_res) } @@ -1432,12 +1400,11 @@ impl<'a> InvocationData<'a> { data: Some(to_bytes(&exchange_rates)), }; - let mut remaining_energy = - from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - remaining_energy + self.invocation_handler + .remaining_energy .tick_energy(constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST)?; - let resume_res = run_interpreter(remaining_energy, |energy| { + let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1446,13 +1413,36 @@ impl<'a> InvocationData<'a> { false, // State never changes on queries. v1::trie::Loader::new(&[][..]), ) - }); + })?; self.process(resume_res) } } } - x => Ok(x), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + self.invocation_handler.update_energy(remaining_energy); + Ok(v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + }) + } + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => { + self.invocation_handler.update_energy(remaining_energy); + Ok(v1::ReceiveResult::Trap { + error, + remaining_energy, + }) + } + // Convert this to an error here, so that we will short circuit processing. + v1::ReceiveResult::OutOfEnergy => Err(OutOfEnergy), } } } @@ -1462,18 +1452,6 @@ impl InvokeEntrypointResult { pub(crate) fn is_success(&self) -> bool { matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) } - - /// Construct a new [`Self`] for an out of energy error. Which uses - /// `v1::InvokeFailure::RuntimeError` as the failure kind. - fn new_out_of_energy_failure() -> Self { - Self { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - logs: v0::Logs::new(), - remaining_energy: 0.into(), - } - } } impl From for InsufficientBalanceError { diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 2c015c0a..b2cc6b8b 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,31 +1,33 @@ use crate::types::{Account, ChainEvent, Contract, ContractModule}; -use concordium_base::contracts_common::{ - AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, - OwnedEntrypointName, SlotTime, +use concordium_base::{ + base::Energy, + contracts_common::{ + AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, + OwnedEntrypointName, SlotTime, + }, }; use concordium_smart_contract_engine::{ v0, v1::{trie::MutableState, InvokeResponse}, - InterpreterEnergy, }; use std::collections::BTreeMap; /// The result of invoking an entrypoint. pub(crate) struct InvokeEntrypointResult { /// The result from the invoke. - pub(crate) invoke_response: InvokeResponse, + pub(crate) invoke_response: InvokeResponse, /// Logs created during the invocation. /// Has entries if and only if `invoke_response` is `Success`. - pub(crate) logs: v0::Logs, - /// The remaining energy after the invocation. - pub(crate) remaining_energy: InterpreterEnergy, + pub(crate) logs: v0::Logs, } /// A type that supports invoking a contract entrypoint. -pub(crate) struct EntrypointInvocationHandler { +pub(crate) struct EntrypointInvocationHandler<'a> { /// The changeset which keeps track of changes to accounts, modules, and /// contracts that occur during an invocation. pub(super) changeset: ChangeSet, + /// The energy remaining for execution. + pub(super) remaining_energy: &'a mut Energy, /// The accounts of the chain. These are currently clones and only used as a /// reference. Any changes are saved to the changeset. pub(super) accounts: BTreeMap, @@ -92,7 +94,7 @@ pub(super) struct ContractChanges { /// /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. -pub(super) struct InvocationData<'a> { +pub(super) struct InvocationData<'a, 'b> { /// The invoker. pub(super) invoker: AccountAddress, /// The contract being called. @@ -105,7 +107,7 @@ pub(super) struct InvocationData<'a> { pub(super) entrypoint: OwnedEntrypointName, /// A reference to the [`EntrypointInvocationHandler`], which is used to for /// handling interrupts and for querying chain data. - pub(super) invocation_handler: &'a mut EntrypointInvocationHandler, + pub(super) invocation_handler: &'a mut EntrypointInvocationHandler<'b>, /// The current state. pub(super) state: MutableState, /// Chain events that have occurred during the execution. From 4627d8f865c50bdac34e4e9501f070713de7e021 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Mar 2023 13:39:13 +0100 Subject: [PATCH 102/208] Derive From --- contract-testing/src/impls.rs | 6 +++--- contract-testing/src/invocation/impls.rs | 2 +- contract-testing/src/types.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index e1345f55..61d56cfe 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -470,7 +470,7 @@ impl Chain { remaining_energy.tick_energy(energy_used_in_interpreter)?; Err(ContractInitErrorKind::ExecutionError { failure_kind: InitFailure::Trap { - error: ExecutionError(error), + error: error.into(), }, }) } @@ -480,9 +480,9 @@ impl Chain { failure_kind: InitFailure::OutOfEnergy, }) } - Err(err) => Err(ContractInitErrorKind::ExecutionError { + Err(error) => Err(ContractInitErrorKind::ExecutionError { failure_kind: InitFailure::Trap { - error: ExecutionError(err), + error: error.into(), }, }), } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b406c545..6f061765 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -693,7 +693,7 @@ impl<'a> EntrypointInvocationHandler<'a> { let res = match f(available_interpreter_energy) { Ok(res) => res, Err(err) => { - // An error occured in the interpreter and it doesn't return the remanining + // An error occured in the interpreter and it doesn't return the remaining // energy. We convert this to a trap and set the energy to the // last known amount. return Ok(v1::ReceiveResult::Trap { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index a3c56ada..00252978 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -290,7 +290,7 @@ pub enum InitFailure { /// An error that occured while executing a contract init or receive function. #[derive(Debug, Error)] #[error("The contract execution halted due to: {0}")] -pub struct ExecutionError(pub(crate) anyhow::Error); +pub struct ExecutionError(#[from] pub(crate) anyhow::Error); /// Represents a successful contract update (or invocation). #[derive(Debug)] From 55b75996bfff0d945c2833572411c1a57eb17318 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Mar 2023 14:02:27 +0100 Subject: [PATCH 103/208] Use `ExchangeRates` directly to ensure correct serialization - I moved it to from concordium-std to contracts-common to allow this. --- contract-testing/src/invocation/impls.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 6f061765..127cc6a1 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -11,7 +11,7 @@ use concordium_base::{ base::{Energy, OutOfEnergy}, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ModuleReference, OwnedReceiveName, + ExchangeRates, ModuleReference, OwnedReceiveName, }, smart_contracts::{OwnedContractName, OwnedParameter}, transactions::UpdateContractPayload, @@ -1388,10 +1388,10 @@ impl<'a, 'b> InvocationData<'a, 'b> { self.process(resume_res) } v1::Interrupt::QueryExchangeRates => { - let exchange_rates = ( - self.invocation_handler.euro_per_energy, - self.invocation_handler.micro_ccd_per_euro, - ); + let exchange_rates = ExchangeRates { + euro_per_energy: self.invocation_handler.euro_per_energy, + micro_ccd_per_euro: self.invocation_handler.micro_ccd_per_euro, + }; let response = v1::InvokeResponse::Success { new_balance: self From 8c736c898eb4e4aae9c65f0f9f6ce149c44efecd Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Mar 2023 15:19:35 +0100 Subject: [PATCH 104/208] Use `OwnedPolicy` instead of `TestPolicy` - Also removes some constructors using signature count for accounts as they won't be needed soon. --- contract-testing/src/impls.rs | 88 +++++++++--------------- contract-testing/src/invocation/impls.rs | 13 ++-- contract-testing/src/types.rs | 10 +-- 3 files changed, 44 insertions(+), 67 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 61d56cfe..0a6a1327 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -6,7 +6,7 @@ use concordium_base::{ constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ExchangeRate, ModuleReference, SlotTime, Timestamp, + ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, smart_contracts::{ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, @@ -367,7 +367,7 @@ impl Chain { slot_time: self.block_time, }, init_origin: sender, - sender_policies: account_info.policies.clone(), + sender_policies: contracts_common::to_bytes(&account_info.policy), }; // Initialize contract // We create an empty loader as no caching is used in this testing library @@ -936,67 +936,34 @@ impl Chain { pub fn euro_per_energy(&self) -> ExchangeRate { self.euro_per_energy } } -impl TestPolicies { - // TODO: Make correctly structured policies ~= Vec>. - pub fn empty() -> Self { Self(Vec::new()) } - - // TODO: Add helper functions for creating arbitrary valid policies. -} - -impl AsRef<[u8]> for TestPolicies { - fn as_ref(&self) -> &[u8] { &self.0 } -} - impl Account { - /// Create a new [`Self`] with the provided parameters. - /// - /// The `signature_count` must be >= 1 for transaction costs to be - /// realistic. - pub fn new_with_policy_and_signature_count( - balance: AccountBalance, - policies: TestPolicies, - signature_count: u32, - ) -> Self { + /// Create new [`Self`] with the provided account policy. + pub fn new_with_policy(balance: AccountBalance, policy: OwnedPolicy) -> Self { Self { balance, - policies: policies.0, - signature_count, - } - } - - /// Create new [`Self`] with empty account policies but the provided - /// `signature_count`. - /// - /// The `signature_count` must be >= 1 for transaction - /// costs to be realistic. - pub fn new_with_signature_count(balance: AccountBalance, signature_count: u32) -> Self { - Self { - signature_count, - ..Self::new_with_balance(balance) - } - } - - /// Create new [`Self`] with the provided account policies and a signature - /// count of `1`. - pub fn new_with_policy(balance: AccountBalance, policies: TestPolicies) -> Self { - Self { - balance, - policies: policies.0, + policy, signature_count: 1, } } - /// Create new [`Self`] with empty account policies and a signature - /// count of `1`. + /// Create new [`Self`] with the provided balance and a default account + /// policy. + /// + /// See [`new`][Self::new] for what the default policy is. pub fn new_with_balance(balance: AccountBalance) -> Self { - Self::new_with_policy(balance, TestPolicies::empty()) + Self::new_with_policy(balance, Self::empty_policy()) } - /// Create new [`Self`] with - /// - empty account policies, - /// - a signature count of `1`, - /// - an [`AccountBalance`] from the `total_balance` provided. + /// Create new [`Self`] with the provided total balance. + /// + /// The `policy` will have: + /// - `identity_provider`: 0, + /// - `created_at`: unix epoch, + /// - `valid_to`: unix epoch + `u64::MAX` milliseconds, + /// - `items`: none, + /// + /// The [`AccountBalance`] will be created with the provided + /// `total_balance`. pub fn new(total_balance: Amount) -> Self { Self::new_with_policy( AccountBalance { @@ -1004,9 +971,22 @@ impl Account { staked: Amount::zero(), locked: Amount::zero(), }, - TestPolicies::empty(), + Self::empty_policy(), ) } + + /// Helper for creating an empty policy. + /// + /// It has identity provider `0`, no items, and is valid from unix epoch + /// until unix epoch + u64::MAX milliseconds. + fn empty_policy() -> OwnedPolicy { + OwnedPolicy { + identity_provider: 0, + created_at: Timestamp::from_timestamp_millis(0), + valid_to: Timestamp::from_timestamp_millis(u64::MAX), + items: Vec::new(), + } + } } impl ContractInvocationSuccess { diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 127cc6a1..4e66b244 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -188,12 +188,13 @@ impl<'a> EntrypointInvocationHandler<'a> { self_balance: instance_self_balance, sender, owner: instance.owner, - sender_policies: self - .accounts - .get(&invoker) - .expect("Precondition violation: invoker must exist.") - .policies - .clone(), + sender_policies: to_bytes( + &self + .accounts + .get(&invoker) + .expect("Precondition violation: invoker must exist.") + .policy, + ), }, }; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 00252978..954e92f0 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -5,7 +5,7 @@ use concordium_base::{ base::Energy, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, - ModuleReference, OwnedContractName, OwnedEntrypointName, SlotTime, + ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, smart_contracts::WasmVersion, }; @@ -65,17 +65,13 @@ pub struct Contract { pub self_balance: Amount, } -/// Account policies for testing. -#[derive(Clone, Debug)] -pub struct TestPolicies(pub Vec); - /// An account. #[derive(Clone, Debug)] pub struct Account { /// The account balance. pub balance: AccountBalance, - /// Account policies. - pub policies: Vec, // TODO: Decide how policies should be represented. + /// Account policy. + pub policy: OwnedPolicy, /// The number of signatures. The number of signatures affect the cost of /// every transaction for the account. pub signature_count: u32, From 0acb93b21f405b3aee09e0f3fd779c2a153c7d60 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Mar 2023 16:12:41 +0100 Subject: [PATCH 105/208] Add a `Signer` for transactions --- contract-testing/src/impls.rs | 54 ++++--- contract-testing/src/invocation/impls.rs | 3 +- contract-testing/src/lib.rs | 7 +- contract-testing/src/types.rs | 19 ++- .../tests/all_new_host_functions.rs | 2 +- contract-testing/tests/basics.rs | 143 +++++++++++------- contract-testing/tests/checkpointing.rs | 32 ++-- contract-testing/tests/counter.rs | 10 +- contract-testing/tests/error_codes.rs | 16 +- contract-testing/tests/fallback.rs | 6 +- contract-testing/tests/iterator.rs | 8 +- contract-testing/tests/queries.rs | 62 ++++---- contract-testing/tests/recorder.rs | 8 +- .../tests/relaxed_restrictions.rs | 14 +- contract-testing/tests/transfer.rs | 10 +- contract-testing/tests/upgrades.rs | 74 ++++----- 16 files changed, 264 insertions(+), 204 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 0a6a1327..c89de025 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -75,10 +75,12 @@ impl Chain { /// [`Chain::load_module_v1`] or [`Chain::load_module_v1_raw`]. /// /// Parameters: + /// - `signer`: the signer with a number of keys, which affects the cost. /// - `sender`: the sender account. /// - `module`: the v1 wasm module. pub fn module_deploy_v1( &mut self, + signer: Signer, sender: AccountAddress, wasm_module: WasmModule, ) -> Result { @@ -99,11 +101,7 @@ impl Chain { + 8 + wasm_module.source.size() + transactions::construct::TRANSACTION_HEADER_SIZE; - let number_of_sigs = self - .account(sender) - .expect("existence already checked") - .signature_count; - cost::base_cost(payload_size, number_of_sigs) + cost::base_cost(payload_size, signer.num_keys) }; let check_header_cost = self.calculate_energy_cost(check_header_energy); @@ -258,6 +256,7 @@ impl Chain { /// Initialize a contract. /// /// **Parameters:** + /// - `signer`: the signer with a number of keys, which affects the cost. /// - `sender`: The account paying for the transaction. Will also become /// the owner of the instance created. /// - `energy_reserved`: Amount of energy reserved for executing the init @@ -271,6 +270,7 @@ impl Chain { /// - `param`: Parameter provided to the init method. pub fn contract_init( &mut self, + signer: Signer, sender: AccountAddress, energy_reserved: Energy, payload: InitContractPayload, @@ -284,8 +284,13 @@ impl Chain { )); } - let res = - self.contract_init_worker(sender, energy_reserved, payload, &mut remaining_energy); + let res = self.contract_init_worker( + signer, + sender, + energy_reserved, + payload, + &mut remaining_energy, + ); let (res, transaction_fee) = match res { Ok(s) => { @@ -315,6 +320,7 @@ impl Chain { /// ensure to charge the account for the energy used. fn contract_init_worker( &mut self, + signer: Signer, sender: AccountAddress, energy_reserved: Energy, payload: InitContractPayload, @@ -337,7 +343,7 @@ impl Chain { // Compute the base cost for checking the transaction header. let check_header_cost = { let pre_account_trx = transactions::construct::init_contract( - account_info.signature_count, + signer.num_keys, sender, base::Nonce::from(0), // Value not matter, only used for serialized size. common::types::TransactionTime::from_seconds(0), /* Value does not matter, only @@ -346,7 +352,7 @@ impl Chain { energy_reserved, ); let transaction_size = to_bytes(&pre_account_trx).len() as u64; - transactions::cost::base_cost(transaction_size, account_info.signature_count) + transactions::cost::base_cost(transaction_size, signer.num_keys) }; // Charge the header cost. @@ -586,6 +592,7 @@ impl Chain { /// - `energy_reserved`: the maximum energy that can be used in the update. pub fn contract_update( &mut self, + signer: Signer, invoker: AccountAddress, sender: Address, energy_reserved: Energy, @@ -612,16 +619,10 @@ impl Chain { }); } - // Get the signature count. TODO: Add as parameter instead? - let invoker_signature_count = self - .account_mut(invoker) - .expect("existence already checked") - .signature_count; - // Compute the base cost for checking the transaction header. let check_header_cost = { let pre_account_trx = transactions::construct::update_contract( - invoker_signature_count, + signer.num_keys, invoker, base::Nonce::from(0), // Value does not matter, only used for serialized size. common::types::TransactionTime::from_seconds(0), /* Value does not matter, only @@ -630,7 +631,7 @@ impl Chain { energy_reserved, ); let transaction_size = to_bytes(&pre_account_trx).len() as u64; - transactions::cost::base_cost(transaction_size, invoker_signature_count) + transactions::cost::base_cost(transaction_size, signer.num_keys) }; // Charge the header cost. @@ -939,11 +940,7 @@ impl Chain { impl Account { /// Create new [`Self`] with the provided account policy. pub fn new_with_policy(balance: AccountBalance, policy: OwnedPolicy) -> Self { - Self { - balance, - policy, - signature_count: 1, - } + Self { balance, policy } } /// Create new [`Self`] with the provided balance and a default account @@ -989,6 +986,19 @@ impl Account { } } +impl Signer { + /// Create a signer which always signs with one key. + pub fn with_one_key() -> Self { Self { num_keys: 1 } } + + /// Create a signer with a non-zero number of keys. + pub fn with_keys(num_keys: u32) -> Result { + if num_keys == 0 { + return Err(ZeroKeysError); + } + Ok(Self { num_keys }) + } +} + impl ContractInvocationSuccess { /// Get an iterator of all transfers that were made from contracts to /// accounts. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 4e66b244..d673ef16 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -273,7 +273,8 @@ impl<'a> EntrypointInvocationHandler<'a> { logs: v0::Logs::new(), }, v1::ReceiveResult::Trap { - error: _, // TODO: Should we return this to the user? + error: _, /* TODO: Forward to the user inside the `InvokeFailure::RuntimeError` + * once the field has been added. */ remaining_energy: _, } => InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index b1579b9b..030972e2 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -19,12 +19,16 @@ //! //! // Deploy a smart contract module (built with [Cargo Concordium](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#cargo-concordium)). //! let deployment = chain -//! .module_deploy_v1(ACC, Chain::module_load_v1("path/to/contract.wasm.v1").unwrap()) +//! .module_deploy_v1( +//! Signer::with_one_key(), +//! ACC, +//! Chain::module_load_v1("path/to/contract.wasm.v1").unwrap()) //! .unwrap(); //! //! // Initialize a smart contract from the deployed module. //! let initialization = chain //! .contract_init( +//! Signer::with_one_key(), // Used for specifying the number of signatures. //! ACC, // Invoker account. //! Energy::from(10000), // Maximum energy allowed for initializing. //! InitContractPayload { @@ -39,6 +43,7 @@ //! // Update the initialized contract. //! let update = chain //! .contract_update( +//! Signer::with_one_key(), // Used for specifying the number of signatures. //! ACC, // Invoker account. //! Address::Account(ACC), // Sender (can also be a contract). //! Energy::from(10000), // Maximum energy allowed for the update. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 954e92f0..ced4d23a 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -69,12 +69,17 @@ pub struct Contract { #[derive(Clone, Debug)] pub struct Account { /// The account balance. - pub balance: AccountBalance, + pub balance: AccountBalance, /// Account policy. - pub policy: OwnedPolicy, - /// The number of signatures. The number of signatures affect the cost of - /// every transaction for the account. - pub signature_count: u32, + pub policy: OwnedPolicy, +} + +/// A signer with a number of keys, the amount of which affects the cost of +/// transactions. +#[derive(Copy, Clone, Debug)] +pub struct Signer { + /// The number of keys used for signing. + pub(crate) num_keys: u32, } /// An event that occurred during a contract update or invocation. @@ -408,3 +413,7 @@ pub struct AccountDoesNotExist { /// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. #[derive(Debug)] pub struct ExchangeRateError; + +/// A [`Signer`] cannot be created with `0` keys. +#[derive(Debug)] +pub struct ZeroKeysError; diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 161bc4bf..102459b1 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -15,7 +15,7 @@ fn test_all_new_host_functions() { chain.create_account(ACC_0, Account::new(initial_balance)); chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), ) diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 12f34a15..ed2cc2e9 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -13,6 +13,7 @@ fn deploying_valid_module_works() { let res = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1( "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", @@ -36,6 +37,7 @@ fn initializing_valid_contract_works() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1( "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", @@ -45,12 +47,17 @@ fn initializing_valid_contract_works() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), + }, + ) .expect("Initializing valid contract should work"); assert_eq!( chain.account_balance_available(ACC_0), @@ -67,6 +74,7 @@ fn initializing_with_invalid_parameter_fails() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1( "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", @@ -75,18 +83,21 @@ fn initializing_with_invalid_parameter_fails() { ) .expect("Deploying valid module should work"); - let res_init = chain - .contract_init( - ACC_0, - Energy::from(10000), - InitContractPayload{ - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![99u8]).expect("Parameter has valid size."), // Invalid param - } - ) - .expect_err("Initializing with invalid params should fail"); + let res_init = + chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![99u8]) + .expect("Parameter has valid size."), // Invalid param + }, + ) + .expect_err("Initializing with invalid params should fail"); let transaction_fee = res_init.transaction_fee; match res_init.kind { @@ -109,6 +120,7 @@ fn updating_valid_contract_works() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1( "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", @@ -117,21 +129,25 @@ fn updating_valid_contract_works() { ) .expect("Deploying valid module should work"); - let res_init = chain - .contract_init( - ACC_0, - Energy::from(10000), - InitContractPayload{ - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 - } - ) - .expect("Initializing valid contract should work"); + let res_init = + chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]) + .expect("Parameter has valid size."), // Starts as 0 + }, + ) + .expect("Initializing valid contract should work"); let res_update = chain .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -187,6 +203,7 @@ fn updating_and_invoking_with_missing_sender_fails() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1( "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", @@ -195,21 +212,25 @@ fn updating_and_invoking_with_missing_sender_fails() { ) .expect("Deploying valid module should work"); - let res_init = chain - .contract_init( - ACC_0, - Energy::from(10000), - InitContractPayload{ - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), // Starts as 0 - } - ) - .expect("Initializing valid contract should work"); + let res_init = + chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".into()), + param: OwnedParameter::try_from(vec![0u8]) + .expect("Parameter has valid size."), // Starts as 0 + }, + ) + .expect("Initializing valid contract should work"); let res_update_acc = chain .contract_update( + Signer::with_one_key(), ACC_0, missing_account, Energy::from(10000), @@ -238,6 +259,7 @@ fn updating_and_invoking_with_missing_sender_fails() { let res_update_contr = chain .contract_update( + Signer::with_one_key(), ACC_0, missing_contract, Energy::from(10000), @@ -286,6 +308,7 @@ fn init_with_less_energy_than_module_lookup() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") .expect("module should exist"), @@ -294,13 +317,18 @@ fn init_with_less_energy_than_module_lookup() { let reserved_energy = Energy::from(10); - let res_init = chain.contract_init(ACC_0, reserved_energy, InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, + let res_init = chain.contract_init( + Signer::with_one_key(), + ACC_0, + reserved_energy, + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_fib".into()), - param: OwnedParameter::empty(), - }); + init_name: OwnedContractName::new_unchecked("init_fib".into()), + param: OwnedParameter::empty(), + }, + ); match res_init { Err(ContractInitError { kind: ContractInitErrorKind::OutOfEnergy, @@ -318,6 +346,7 @@ fn update_with_fib_reentry_works() { let res_deploy = chain .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") .expect("module should exist"), @@ -325,16 +354,22 @@ fn update_with_fib_reentry_works() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_fib".into()), - param: OwnedParameter::empty(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + amount: Amount::zero(), + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_fib".into()), + param: OwnedParameter::empty(), + }, + ) .expect("Initializing valid contract should work"); let res_update = chain .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 6e92786f..76e3a5c1 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -27,7 +27,7 @@ fn test_case_1() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -35,7 +35,7 @@ fn test_case_1() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_a".into()), param: OwnedParameter::empty(), @@ -44,7 +44,7 @@ fn test_case_1() { .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_b".into()), param: OwnedParameter::empty(), @@ -68,7 +68,7 @@ fn test_case_1() { ); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -105,7 +105,7 @@ fn test_case_2() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -113,7 +113,7 @@ fn test_case_2() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_a".into()), param: OwnedParameter::empty(), @@ -122,7 +122,7 @@ fn test_case_2() { .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_b".into()), param: OwnedParameter::empty(), @@ -146,7 +146,7 @@ fn test_case_2() { ); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -183,7 +183,7 @@ fn test_case_3() { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -191,7 +191,7 @@ fn test_case_3() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_a".into()), param: OwnedParameter::empty(), @@ -200,7 +200,7 @@ fn test_case_3() { .expect("Initializing valid contract should work"); chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_b".into()), param: OwnedParameter::empty(), @@ -209,7 +209,7 @@ fn test_case_3() { .expect("Initializing valid contract should work"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -244,7 +244,7 @@ fn test_case_4() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -252,7 +252,7 @@ fn test_case_4() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_a".into()), param: OwnedParameter::empty(), @@ -261,7 +261,7 @@ fn test_case_4() { .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_b".into()), param: OwnedParameter::empty(), @@ -285,7 +285,7 @@ fn test_case_4() { ); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 98f6268b..454aacda 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -14,7 +14,7 @@ fn test_counter() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,7 +22,7 @@ fn test_counter() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_counter".into()), param: OwnedParameter::empty(), @@ -31,7 +31,7 @@ fn test_counter() { .expect("Initializing valid contract should work"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +46,7 @@ fn test_counter() { assert_counter_state(&mut chain, res_init.contract_address, 1); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -67,7 +67,7 @@ fn test_counter() { Amount::zero(), ); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 24d2960b..14aec417 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -14,7 +14,7 @@ fn test_error_codes() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,7 +22,7 @@ fn test_error_codes() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_caller".into()), param: OwnedParameter::empty(), @@ -46,7 +46,7 @@ fn test_error_codes() { Amount::zero(), ); let res_update_0 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -79,7 +79,7 @@ fn test_error_codes() { Amount::from_micro_ccd(10_000), ); let res_update_1 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -110,7 +110,7 @@ fn test_error_codes() { Amount::zero(), ); let res_update_2 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -143,7 +143,7 @@ fn test_error_codes() { Amount::zero(), ); let res_update_3 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -176,7 +176,7 @@ fn test_error_codes() { Amount::zero(), ); let res_update_4 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -212,7 +212,7 @@ fn test_error_codes() { Amount::zero(), ); let res_update_6 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index 83e4d924..dda64ea8 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -12,7 +12,7 @@ fn test_fallback() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -20,7 +20,7 @@ fn test_fallback() { .expect("Deploying valid module should work"); let res_init_two = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_two".into()), param: OwnedParameter::empty(), @@ -29,7 +29,7 @@ fn test_fallback() { .expect("Initializing valid contract should work"); let res_init_one = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_one".into()), param: OwnedParameter::from_serial(&res_init_two.contract_address) diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 76d9e1cb..49ba741d 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -16,7 +16,7 @@ fn test_iterator() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -24,7 +24,7 @@ fn test_iterator() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_iterator".into()), param: OwnedParameter::empty(), @@ -33,7 +33,7 @@ fn test_iterator() { .expect("Initializing valid contract should work"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +46,7 @@ fn test_iterator() { ) .expect("Should succeed"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 3e114d20..dd307520 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -24,7 +24,7 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -35,7 +35,7 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -48,7 +48,7 @@ mod query_account_balance { let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -87,7 +87,7 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -98,7 +98,7 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -116,7 +116,7 @@ mod query_account_balance { let input_param = (ACC_1, expected_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_1, Address::Account(ACC_1), energy_limit, @@ -155,7 +155,7 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance-transfer.wasm", @@ -168,7 +168,7 @@ mod query_account_balance { let amount_to_send = Amount::from_ccd(123); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -187,7 +187,7 @@ mod query_account_balance { ); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -231,7 +231,7 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -242,7 +242,7 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -257,7 +257,7 @@ mod query_account_balance { let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -294,7 +294,7 @@ mod query_account_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance-missing-account.wasm", @@ -305,7 +305,7 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -317,7 +317,7 @@ mod query_account_balance { let input_param = ACC_1; let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -360,7 +360,7 @@ mod query_contract_balance { let init_amount = Amount::from_ccd(123); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", @@ -371,7 +371,7 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -380,7 +380,7 @@ mod query_contract_balance { .expect("Initializing valid contract should work"); let res_init_other = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -392,7 +392,7 @@ mod query_contract_balance { let input_param = (res_init_other.contract_address, init_amount); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -423,7 +423,7 @@ mod query_contract_balance { let update_amount = Amount::from_ccd(456); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", @@ -434,7 +434,7 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -446,7 +446,7 @@ mod query_contract_balance { let input_param = (res_init.contract_address, init_amount + update_amount); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -477,7 +477,7 @@ mod query_contract_balance { let transfer_amount = Amount::from_ccd(78); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-transfer.wasm", @@ -488,7 +488,7 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -504,7 +504,7 @@ mod query_contract_balance { ); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -534,7 +534,7 @@ mod query_contract_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-missing-contract.wasm", @@ -545,7 +545,7 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -557,7 +557,7 @@ mod query_contract_balance { let input_param = ContractAddress::new(123, 456); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -589,7 +589,7 @@ mod query_exchange_rates { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-exchange-rates.wasm", @@ -600,7 +600,7 @@ mod query_exchange_rates { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -612,7 +612,7 @@ mod query_exchange_rates { let input_param = (chain.euro_per_energy(), chain.micro_ccd_per_euro()); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index d89f2d57..b76e6fae 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -12,7 +12,7 @@ fn test_recorder() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -20,7 +20,7 @@ fn test_recorder() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_recorder".into()), param: OwnedParameter::empty(), @@ -29,7 +29,7 @@ fn test_recorder() { .expect("Initializing valid contract should work"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -43,7 +43,7 @@ fn test_recorder() { ) .expect("Update failed"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 4885f2a4..59d65972 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -26,7 +26,7 @@ fn test_new_parameter_limit() { let parameter = mk_parameter(65535, 65535); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -34,7 +34,7 @@ fn test_new_parameter_limit() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(80000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(80000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_relax".into()), param: parameter.clone(), // Check parameter size limit on init. @@ -43,7 +43,7 @@ fn test_new_parameter_limit() { .expect("Initializing valid contract should work"); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(700000), @@ -63,7 +63,7 @@ fn test_new_return_value_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -84,7 +84,7 @@ fn test_new_log_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -107,7 +107,7 @@ fn deploy_and_init() -> (Chain, ContractAddress) { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -115,7 +115,7 @@ fn deploy_and_init() -> (Chain, ContractAddress) { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_relax".into()), param: OwnedParameter::empty(), diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index d07ea49c..22aa4b79 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -13,7 +13,7 @@ fn test_transfer() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -21,7 +21,7 @@ fn test_transfer() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_transfer".into()), param: OwnedParameter::empty(), @@ -32,7 +32,7 @@ fn test_transfer() { let contract_address = res_init.contract_address; chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -53,7 +53,7 @@ fn test_transfer() { // Deposit 1000 micro CCD. chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -67,7 +67,7 @@ fn test_transfer() { .expect("Updating contract should succeed"); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index deda31fc..362fe60d 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -15,7 +15,7 @@ fn test() { // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -23,7 +23,7 @@ fn test() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -32,7 +32,7 @@ fn test() { // Initialize `upgrading_0`. let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_a".into()), mod_ref: res_deploy_0.module_reference, @@ -44,7 +44,7 @@ fn test() { // Upgrade the contract to the `upgrading_1` module by calling the `bump` // entrypoint. let res_update_upgrade = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -60,7 +60,7 @@ fn test() { // Call the `newfun` entrypoint which only exists in `upgrading_1`. let res_update_new = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -95,14 +95,14 @@ fn test_self_invoke() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -110,7 +110,7 @@ fn test_self_invoke() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), mod_ref: res_deploy_0.module_reference, @@ -119,7 +119,7 @@ fn test_self_invoke() { .expect("Initializing valid contract should work"); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -161,7 +161,7 @@ fn test_missing_module() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-module.wasm", @@ -172,7 +172,7 @@ fn test_missing_module() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -181,7 +181,7 @@ fn test_missing_module() { .expect("Initializing valid contract should work"); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -212,7 +212,7 @@ fn test_missing_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-contract0.wasm", @@ -223,7 +223,7 @@ fn test_missing_contract() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-contract1.wasm", @@ -234,7 +234,7 @@ fn test_missing_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), mod_ref: res_deploy_0.module_reference, @@ -244,7 +244,7 @@ fn test_missing_contract() { .expect("Initializing valid contract should work"); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -275,7 +275,7 @@ fn test_twice_in_one_transaction() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -283,7 +283,7 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -291,7 +291,7 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_deploy_2 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -299,7 +299,7 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), mod_ref: res_deploy_0.module_reference, @@ -311,7 +311,7 @@ fn test_twice_in_one_transaction() { let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -364,7 +364,7 @@ fn test_chained_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -372,7 +372,7 @@ fn test_chained_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -384,7 +384,7 @@ fn test_chained_contract() { let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -417,7 +417,7 @@ fn test_reject() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -425,7 +425,7 @@ fn test_reject() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -433,7 +433,7 @@ fn test_reject() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy_0.module_reference, init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), @@ -442,7 +442,7 @@ fn test_reject() { .expect("Initializing valid contract should work"); let res_update_upgrade = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -457,7 +457,7 @@ fn test_reject() { .expect_err("should fail"); let res_update_new_feature = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -499,7 +499,7 @@ fn test_changing_entrypoint() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints0.wasm", @@ -510,7 +510,7 @@ fn test_changing_entrypoint() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1( + .module_deploy_v1(Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints1.wasm", @@ -521,7 +521,7 @@ fn test_changing_entrypoint() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(ACC_0, Energy::from(10000), InitContractPayload { + .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), param: OwnedParameter::empty(), mod_ref: res_deploy_0.module_reference, @@ -531,7 +531,7 @@ fn test_changing_entrypoint() { .expect("Initializing valid contract should work"); let res_update_old_feature_0 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -545,7 +545,7 @@ fn test_changing_entrypoint() { .expect("Updating old_feature on old module should work."); let res_update_new_feature_0 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -559,7 +559,7 @@ fn test_changing_entrypoint() { .expect_err("Updating new_feature on old module should _not_ work"); let res_update_upgrade = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -574,7 +574,7 @@ fn test_changing_entrypoint() { .expect("Upgrading contract should work."); let res_update_old_feature_1 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -588,7 +588,7 @@ fn test_changing_entrypoint() { .expect_err("Updating old_feature on _new_ module should _not_ work."); let res_update_new_feature_1 = chain - .contract_update( + .contract_update(Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), From cd3f014e30cf763c71af4a172a7384e75034fc8f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 22 Mar 2023 16:43:07 +0100 Subject: [PATCH 106/208] Simplify calculation of check header cost --- contract-testing/src/impls.rs | 38 +++++++++++------------------------ 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index c89de025..5203b318 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,8 +1,7 @@ use crate::{constants, invocation::EntrypointInvocationHandler, types::*}; use anyhow::anyhow; use concordium_base::{ - base::{self, Energy, OutOfEnergy}, - common::{self, to_bytes}, + base::{Energy, OutOfEnergy}, constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, @@ -240,10 +239,11 @@ impl Chain { path: module_path.to_path_buf(), kind: e.into(), })?; - let module: WasmModule = common::from_bytes(&mut reader).map_err(|e| ModuleLoadError { - path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::ReadModule(e.into()), - })?; + let module: WasmModule = + concordium_base::common::from_bytes(&mut reader).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::ReadModule(e.into()), + })?; if module.version != WasmVersion::V1 { return Err(ModuleLoadError { path: module_path.to_path_buf(), @@ -342,16 +342,9 @@ impl Chain { // Compute the base cost for checking the transaction header. let check_header_cost = { - let pre_account_trx = transactions::construct::init_contract( - signer.num_keys, - sender, - base::Nonce::from(0), // Value not matter, only used for serialized size. - common::types::TransactionTime::from_seconds(0), /* Value does not matter, only - * used for serialized size. */ - payload.clone(), - energy_reserved, - ); - let transaction_size = to_bytes(&pre_account_trx).len() as u64; + // 1 byte for the tag. + let transaction_size = + transactions::construct::TRANSACTION_HEADER_SIZE + 1 + payload.size() as u64; transactions::cost::base_cost(transaction_size, signer.num_keys) }; @@ -621,16 +614,9 @@ impl Chain { // Compute the base cost for checking the transaction header. let check_header_cost = { - let pre_account_trx = transactions::construct::update_contract( - signer.num_keys, - invoker, - base::Nonce::from(0), // Value does not matter, only used for serialized size. - common::types::TransactionTime::from_seconds(0), /* Value does not matter, only - * used for serialized size. */ - payload.clone(), - energy_reserved, - ); - let transaction_size = to_bytes(&pre_account_trx).len() as u64; + // 1 byte for the tag. + let transaction_size = + transactions::construct::TRANSACTION_HEADER_SIZE + 1 + payload.size() as u64; transactions::cost::base_cost(transaction_size, signer.num_keys) }; From 43fe15c9156bf760d86f636ddfebfc391aff0e27 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Mar 2023 08:48:16 +0100 Subject: [PATCH 107/208] Check early if the contract does not exist --- contract-testing/src/impls.rs | 12 ++++++++++++ contract-testing/src/invocation/impls.rs | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 5203b318..838a9252 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -502,6 +502,18 @@ impl Chain { remaining_energy: &mut Energy, should_persist: bool, ) -> Result { + // Check if the contract to invoke exists. + if !self.contract_exists(payload.address) { + return Err(self.from_invocation_error_kind( + ContractDoesNotExist { + address: payload.address, + } + .into(), + energy_reserved, + *remaining_energy, + )); + } + // Ensure that the parameter has a valid size. if payload.message.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { return Err(self.from_invocation_error_kind( diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index d673ef16..8d4127c7 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -113,8 +113,6 @@ impl<'a> EntrypointInvocationHandler<'a> { Some(self_balance) => self_balance, None => { // Return early. - // TODO: For the top-most update, we should catch this in `contract_update` and - // return `ContractUpdateError::EntrypointDoesNotExist`. return Ok(InvokeEntrypointResult { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, From 01381e8f56e1a6418da74abc9ab5568148dfe52b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Mar 2023 09:13:25 +0100 Subject: [PATCH 108/208] Renaming for consistency --- contract-testing/src/impls.rs | 74 +++---- contract-testing/src/invocation/impls.rs | 27 ++- contract-testing/src/invocation/types.rs | 4 +- contract-testing/src/types.rs | 18 +- contract-testing/tests/basics.rs | 10 +- contract-testing/tests/fallback.rs | 43 ++-- contract-testing/tests/queries.rs | 249 +++++++++++++++-------- contract-testing/tests/upgrades.rs | 231 +++++++++++++-------- 8 files changed, 407 insertions(+), 249 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 838a9252..5dd715aa 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -82,7 +82,7 @@ impl Chain { signer: Signer, sender: AccountAddress, wasm_module: WasmModule, - ) -> Result { + ) -> Result { // Ensure sender account exists. if !self.account_exists(sender) { return Err(ModuleDeployError { @@ -184,7 +184,7 @@ impl Chain { size: wasm_module.source.size(), artifact: Arc::new(artifact), }); - Ok(SuccessfulModuleDeployment { + Ok(ModuleDeploySuccess { module_reference, energy_used: check_header_energy + deploy_module_energy, transaction_fee: check_header_cost + deploy_module_cost, @@ -258,7 +258,7 @@ impl Chain { /// **Parameters:** /// - `signer`: the signer with a number of keys, which affects the cost. /// - `sender`: The account paying for the transaction. Will also become - /// the owner of the instance created. + /// the owner of the contract created. /// - `energy_reserved`: Amount of energy reserved for executing the init /// method. /// - `payload`: @@ -416,7 +416,7 @@ impl Chain { remaining_energy .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST)?; - let contract_instance = Contract { + let contract = Contract { module_reference: payload.mod_ref, contract_name: payload.init_name, state: persisted_state, @@ -424,8 +424,8 @@ impl Chain { self_balance: payload.amount, }; - // Save the contract instance - self.contracts.insert(contract_address, contract_instance); + // Save the contract. + self.contracts.insert(contract_address, contract); // Subtract the amount from the invoker. self.account_mut(sender) @@ -453,7 +453,7 @@ impl Chain { ); remaining_energy.tick_energy(energy_used_in_interpreter)?; Err(ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::Reject { + error: InitExecutionError::Reject { reason, return_value, }, @@ -468,7 +468,7 @@ impl Chain { ); remaining_energy.tick_energy(energy_used_in_interpreter)?; Err(ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::Trap { + error: InitExecutionError::Trap { error: error.into(), }, }) @@ -476,11 +476,11 @@ impl Chain { Ok(v1::InitResult::OutOfEnergy) => { *remaining_energy = Energy::from(0); Err(ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::OutOfEnergy, + error: InitExecutionError::OutOfEnergy, }) } Err(error) => Err(ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::Trap { + error: InitExecutionError::Trap { error: error.into(), }, }), @@ -501,7 +501,7 @@ impl Chain { payload: UpdateContractPayload, remaining_energy: &mut Energy, should_persist: bool, - ) -> Result { + ) -> Result { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { return Err(self.from_invocation_error_kind( @@ -517,7 +517,7 @@ impl Chain { // Ensure that the parameter has a valid size. if payload.message.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { return Err(self.from_invocation_error_kind( - ContractInvocationErrorKind::ParameterTooLarge, + ContractInvokeErrorKind::ParameterTooLarge, energy_reserved, *remaining_energy, )); @@ -565,7 +565,7 @@ impl Chain { v1::InvokeResponse::Success { new_balance, data } => { let energy_used = energy_reserved - *remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); - Ok(ContractInvocationSuccess { + Ok(ContractInvokeSuccess { chain_events, energy_used, transaction_fee, @@ -576,7 +576,7 @@ impl Chain { }) } v1::InvokeResponse::Failure { kind } => Err(self.from_invocation_error_kind( - ContractInvocationErrorKind::ExecutionError { failure_kind: kind }, + ContractInvokeErrorKind::ExecutionError { failure_kind: kind }, energy_reserved, *remaining_energy, )), @@ -602,13 +602,13 @@ impl Chain { sender: Address, energy_reserved: Energy, payload: UpdateContractPayload, - ) -> Result { + ) -> Result { // Ensure the invoker exists. if !self.account_exists(invoker) { - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::InvokerDoesNotExist( + kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), }); @@ -617,10 +617,10 @@ impl Chain { if !self.address_exists(sender) { // TODO: Should we charge the header cost if the invoker exists but the sender // doesn't? - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::SenderDoesNotExist(sender), + kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), }); } @@ -636,10 +636,10 @@ impl Chain { let mut remaining_energy = energy_reserved .checked_sub(check_header_cost) - .ok_or(ContractInvocationError { + .ok_or(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::OutOfEnergy, + kind: ContractInvokeErrorKind::OutOfEnergy, })?; let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); @@ -650,10 +650,10 @@ impl Chain { // Ensure the account has sufficient funds to pay for the energy and amount. if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { let energy_used = energy_reserved - remaining_energy; - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used, transaction_fee: self.calculate_energy_cost(energy_used), - kind: ContractInvocationErrorKind::InsufficientFunds, + kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -708,23 +708,23 @@ impl Chain { sender: Address, energy_reserved: Energy, payload: UpdateContractPayload, - ) -> Result { + ) -> Result { // Ensure the invoker exists. if !self.account_exists(invoker) { - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::InvokerDoesNotExist( + kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), }); } // Ensure the sender exists. if !self.address_exists(sender) { - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvocationErrorKind::SenderDoesNotExist(sender), + kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), }); } @@ -736,10 +736,10 @@ impl Chain { if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { let energy_used = Energy::from(0); - return Err(ContractInvocationError { + return Err(ContractInvokeError { energy_used, transaction_fee: self.calculate_energy_cost(energy_used), - kind: ContractInvocationErrorKind::InsufficientFunds, + kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -864,13 +864,13 @@ impl Chain { /// `transaction_fee`. fn from_invocation_error_kind( &self, - kind: ContractInvocationErrorKind, + kind: ContractInvokeErrorKind, energy_reserved: Energy, remaining_energy: Energy, - ) -> ContractInvocationError { + ) -> ContractInvokeError { let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); - ContractInvocationError { + ContractInvokeError { energy_used, transaction_fee, kind, @@ -880,9 +880,9 @@ impl Chain { /// Construct a [`ContractInvocationError`] of the `OutOfEnergy` kind with /// the energy and transaction fee fields based on the `energy_reserved` /// parameter. - fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvocationError { + fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvokeError { self.from_invocation_error_kind( - ContractInvocationErrorKind::OutOfEnergy, + ContractInvokeErrorKind::OutOfEnergy, energy_reserved, Energy::from(0), ) @@ -997,7 +997,7 @@ impl Signer { } } -impl ContractInvocationSuccess { +impl ContractInvokeSuccess { /// Get an iterator of all transfers that were made from contracts to /// accounts. pub fn transfers(&self) -> impl Iterator + '_ { @@ -1041,7 +1041,7 @@ impl ChainEvent { } } -impl From for ContractInvocationErrorKind { +impl From for ContractInvokeErrorKind { fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 8d4127c7..8c5e0dc1 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -40,7 +40,7 @@ impl<'a> EntrypointInvocationHandler<'a> { sender: Address, remaining_energy: &'a mut Energy, payload: UpdateContractPayload, - ) -> Result<(InvokeEntrypointResult, ChangeSet, Vec), OutOfEnergy> { + ) -> Result<(InvokeEntrypointResponse, ChangeSet, Vec), OutOfEnergy> { let mut contract_invocation = Self { changeset: ChangeSet::new(), remaining_energy, @@ -74,7 +74,7 @@ impl<'a> EntrypointInvocationHandler<'a> { sender: Address, payload: UpdateContractPayload, chain_events: &mut Vec, - ) -> Result { + ) -> Result { // Charge the base cost for updating a contract. self.remaining_energy .tick_energy(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST)?; @@ -102,7 +102,7 @@ impl<'a> EntrypointInvocationHandler<'a> { TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. - return Ok(InvokeEntrypointResult { + return Ok(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind }, logs: v0::Logs::new(), }); @@ -113,7 +113,7 @@ impl<'a> EntrypointInvocationHandler<'a> { Some(self_balance) => self_balance, None => { // Return early. - return Ok(InvokeEntrypointResult { + return Ok(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, @@ -158,7 +158,7 @@ impl<'a> EntrypointInvocationHandler<'a> { ) } else { // Return early. - return Ok(InvokeEntrypointResult { + return Ok(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, @@ -247,7 +247,7 @@ impl<'a> EntrypointInvocationHandler<'a> { * the changeset later to get a more precise result. */ return_value, remaining_energy: _, - } => InvokeEntrypointResult { + } => InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Success { new_balance: self.contract_balance_unchecked(payload.address), data: Some(return_value), @@ -261,7 +261,7 @@ impl<'a> EntrypointInvocationHandler<'a> { reason, return_value, remaining_energy: _, - } => InvokeEntrypointResult { + } => InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::ContractReject { code: reason, @@ -274,7 +274,7 @@ impl<'a> EntrypointInvocationHandler<'a> { error: _, /* TODO: Forward to the user inside the `InvokeFailure::RuntimeError` * once the field has been added. */ remaining_energy: _, - } => InvokeEntrypointResult { + } => InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, @@ -415,7 +415,12 @@ impl<'a> EntrypointInvocationHandler<'a> { /// /// If account isn't already present in the changeset, it is added. /// - /// Returns the new balance. + /// Returns an error if a negative delta is provided which exceeds the + /// available balance of the account. Otherwise, it returns the new + /// available balance of the account. + /// + /// The precondition is not part of the error type, as this is an internal + /// helper function that is only called when the precondition is met. /// /// **Preconditions:** /// - Account must exist. @@ -1186,7 +1191,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { .map(|c| c.contract_name.as_contract_name()) { // The contract to call does not exist. - None => InvokeEntrypointResult { + None => InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, @@ -1447,7 +1452,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { } } -impl InvokeEntrypointResult { +impl InvokeEntrypointResponse { /// Whether the invoke was successful. pub(crate) fn is_success(&self) -> bool { matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index b2cc6b8b..b9190ca6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -12,8 +12,8 @@ use concordium_smart_contract_engine::{ }; use std::collections::BTreeMap; -/// The result of invoking an entrypoint. -pub(crate) struct InvokeEntrypointResult { +/// The response from invoking an entrypoint. +pub(crate) struct InvokeEntrypointResponse { /// The result from the invoke. pub(crate) invoke_response: InvokeResponse, /// Logs created during the invocation. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index ced4d23a..f33a6b27 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -143,7 +143,7 @@ pub struct Transfer { /// Represents a successful deployment of a [`ContractModule`]. #[derive(Debug, PartialEq, Eq)] -pub struct SuccessfulModuleDeployment { +pub struct ModuleDeploySuccess { /// The reference of the module deployed. pub module_reference: ModuleReference, /// The energy used for deployment. @@ -250,10 +250,10 @@ pub struct ContractInitError { #[derive(Debug, Error)] pub enum ContractInitErrorKind { /// Initialization during execution. - #[error("Failed with an execution error: {failure_kind:?}")] + #[error("Failed with an execution error: {error:?}")] ExecutionError { /// The reason for why the contract initialization failed. - failure_kind: InitFailure, + error: InitExecutionError, }, /// Ran out of energy. #[error("Ran out of energy")] @@ -274,7 +274,7 @@ pub enum ContractInitErrorKind { /// The reason for why a contract initialization failed during execution. #[derive(Debug)] -pub enum InitFailure { +pub enum InitExecutionError { /// The contract rejected. Reject { /// The error code for why it rejected. @@ -295,7 +295,7 @@ pub struct ExecutionError(#[from] pub(crate) anyhow::Error); /// Represents a successful contract update (or invocation). #[derive(Debug)] -pub struct ContractInvocationSuccess { +pub struct ContractInvokeSuccess { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. pub chain_events: Vec, @@ -305,7 +305,7 @@ pub struct ContractInvocationSuccess { pub transaction_fee: Amount, /// The returned value. pub return_value: ReturnValue, - /// Whether the state was changed. + /// Whether the state of the invoked contract was changed. pub state_changed: bool, /// The new balance of the smart contract. pub new_balance: Amount, @@ -316,20 +316,20 @@ pub struct ContractInvocationSuccess { /// An error that occured during a [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. #[derive(Debug)] -pub struct ContractInvocationError { +pub struct ContractInvokeError { /// The energy used. pub energy_used: Energy, /// The transaction fee. For [`Chain::contract_update`], this is the amount /// charged to the `invoker` account. pub transaction_fee: Amount, /// The specific reason for why the invocation failed. - pub kind: ContractInvocationErrorKind, + pub kind: ContractInvokeErrorKind, } /// The error kinds that can occur during [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. #[derive(Debug, Error)] -pub enum ContractInvocationErrorKind { +pub enum ContractInvokeErrorKind { /// Invocation failed during execution. #[error("Failed during execution: {failure_kind:?}")] ExecutionError { failure_kind: v1::InvokeFailure }, diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index ed2cc2e9..7e6c45d0 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -103,7 +103,7 @@ fn initializing_with_invalid_parameter_fails() { match res_init.kind { // Failed in the right way and account is still charged. ContractInitErrorKind::ExecutionError { - failure_kind: InitFailure::Reject { .. }, + error: InitExecutionError::Reject { .. }, } => assert_eq!( chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - transaction_fee) @@ -288,16 +288,16 @@ fn updating_and_invoking_with_missing_sender_fails() { assert!(matches!( res_update_acc.kind, - ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); + ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); assert!(matches!( res_invoke_acc.kind, - ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); + ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); assert!(matches!( res_update_contr.kind, - ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); + ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); assert!(matches!( res_invoke_contr.kind, - ContractInvocationErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); + ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); } #[test] diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index dda64ea8..dd64889a 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -12,7 +12,8 @@ fn test_fallback() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -20,23 +21,33 @@ fn test_fallback() { .expect("Deploying valid module should work"); let res_init_two = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_two".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_two".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_one = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_one".into()), - param: OwnedParameter::from_serial(&res_init_two.contract_address) - .expect("Parameter has valid size"), /* Pass in address of contract - * "two". */ - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_one".into()), + param: OwnedParameter::from_serial(&res_init_two.contract_address) + .expect("Parameter has valid size"), /* Pass in address of contract + * "two". */ + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Invoke the fallback directly. This should fail with execution failure/trap @@ -56,7 +67,7 @@ fn test_fallback() { ) .expect_err("should fail"); match res_invoke_1.kind { - ContractInvocationErrorKind::ExecutionError { + ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::RuntimeError, .. } => (), diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index dd307520..25cd4520 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -24,7 +24,8 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -35,12 +36,17 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // The contract will query the balance of ACC_1 and assert that the three @@ -48,7 +54,8 @@ mod query_account_balance { let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -87,7 +94,8 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -98,12 +106,17 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let update_amount = Amount::from_ccd(123); @@ -116,7 +129,8 @@ mod query_account_balance { let input_param = (ACC_1, expected_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_1, Address::Account(ACC_1), energy_limit, @@ -155,7 +169,8 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance-transfer.wasm", @@ -168,12 +183,17 @@ mod query_account_balance { let amount_to_send = Amount::from_ccd(123); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: amount_to_send, // Make sure the contract has CCD to transfer. - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: amount_to_send, // Make sure the contract has CCD to transfer. + }, + ) .expect("Initializing valid contract should work"); let amount_to_send = Amount::from_ccd(123); @@ -187,7 +207,8 @@ mod query_account_balance { ); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -231,7 +252,8 @@ mod query_account_balance { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance.wasm", @@ -242,22 +264,26 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); - // TODO: Implement serial for four-tuples in contracts-common. Nesting tuples to - // get around it here. // The contract will query the balance of ACC_1 and assert that the three // balances match this input. let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -294,7 +320,8 @@ mod query_account_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-account-balance-missing-account.wasm", @@ -305,19 +332,25 @@ mod query_account_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // The account to query, which doesn't exist in this test case. let input_param = ACC_1; let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -360,7 +393,8 @@ mod query_contract_balance { let init_amount = Amount::from_ccd(123); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", @@ -371,28 +405,39 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_other = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: init_amount, // Set up another contract with `init_amount` balance - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, // Set up another contract with `init_amount` balance + }, + ) .expect("Initializing valid contract should work"); // check that the other contract has `self_balance == init_amount`. let input_param = (res_init_other.contract_address, init_amount); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -423,7 +468,8 @@ mod query_contract_balance { let update_amount = Amount::from_ccd(456); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", @@ -434,19 +480,25 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: init_amount, - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, + }, + ) .expect("Initializing valid contract should work"); // check that the other contract has `self_balance == init_amount`. let input_param = (res_init.contract_address, init_amount + update_amount); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -477,7 +529,8 @@ mod query_contract_balance { let transfer_amount = Amount::from_ccd(78); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-transfer.wasm", @@ -488,12 +541,17 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: init_amount, - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: init_amount, + }, + ) .expect("Initializing valid contract should work"); let input_param = ( @@ -504,7 +562,8 @@ mod query_contract_balance { ); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -534,7 +593,8 @@ mod query_contract_balance { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-contract-balance-missing-contract.wasm", @@ -545,19 +605,25 @@ mod query_contract_balance { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Non-existent contract address. let input_param = ContractAddress::new(123, 456); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -589,7 +655,8 @@ mod query_exchange_rates { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/queries-exchange-rates.wasm", @@ -600,19 +667,25 @@ mod query_exchange_rates { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Non-existent contract address. let input_param = (chain.euro_per_energy(), chain.micro_ccd_per_euro()); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 362fe60d..5e48a272 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -15,7 +15,8 @@ fn test() { // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -23,7 +24,8 @@ fn test() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -32,19 +34,25 @@ fn test() { // Initialize `upgrading_0`. let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - init_name: OwnedContractName::new_unchecked("init_a".into()), - mod_ref: res_deploy_0.module_reference, + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_a".into()), + mod_ref: res_deploy_0.module_reference, - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Upgrade the contract to the `upgrading_1` module by calling the `bump` // entrypoint. let res_update_upgrade = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -60,7 +68,8 @@ fn test() { // Call the `newfun` entrypoint which only exists in `upgrading_1`. let res_update_new = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -95,14 +104,16 @@ fn test_self_invoke() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -110,16 +121,22 @@ fn test_self_invoke() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - mod_ref: res_deploy_0.module_reference, - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -161,7 +178,8 @@ fn test_missing_module() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-module.wasm", @@ -172,16 +190,22 @@ fn test_missing_module() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -212,7 +236,8 @@ fn test_missing_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-contract0.wasm", @@ -223,7 +248,8 @@ fn test_missing_contract() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-missing-contract1.wasm", @@ -234,17 +260,23 @@ fn test_missing_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - mod_ref: res_deploy_0.module_reference, + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, - amount: Amount::zero(), - }) + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -275,7 +307,8 @@ fn test_twice_in_one_transaction() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -283,7 +316,8 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -291,7 +325,8 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_deploy_2 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -299,19 +334,25 @@ fn test_twice_in_one_transaction() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - mod_ref: res_deploy_0.module_reference, + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, - amount: Amount::zero(), - }) + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let input_param = (res_deploy_1.module_reference, res_deploy_2.module_reference); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -364,7 +405,8 @@ fn test_chained_contract() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -372,19 +414,25 @@ fn test_chained_contract() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -417,7 +465,8 @@ fn test_reject() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -425,7 +474,8 @@ fn test_reject() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -433,16 +483,22 @@ fn test_reject() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy_0.module_reference, - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy_0.module_reference, + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_update_upgrade = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -457,7 +513,8 @@ fn test_reject() { .expect_err("should fail"); let res_update_new_feature = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -472,7 +529,7 @@ fn test_reject() { // Check the return value manually returned by the contract. match res_update_upgrade.kind { - ContractInvocationErrorKind::ExecutionError { failure_kind, .. } => match failure_kind { + ContractInvokeErrorKind::ExecutionError { failure_kind, .. } => match failure_kind { InvokeFailure::ContractReject { code, .. } if code == -1 => (), _ => panic!("Expected ContractReject with code == -1"), }, @@ -483,7 +540,7 @@ fn test_reject() { // failed. assert!(matches!( res_update_new_feature.kind, - ContractInvocationErrorKind::ExecutionError { + ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); @@ -499,7 +556,8 @@ fn test_changing_entrypoint() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy_0 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints0.wasm", @@ -510,7 +568,8 @@ fn test_changing_entrypoint() { .expect("Deploying valid module should work"); let res_deploy_1 = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints1.wasm", @@ -521,17 +580,23 @@ fn test_changing_entrypoint() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - init_name: OwnedContractName::new_unchecked("init_contract".into()), - param: OwnedParameter::empty(), - mod_ref: res_deploy_0.module_reference, + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + param: OwnedParameter::empty(), + mod_ref: res_deploy_0.module_reference, - amount: Amount::zero(), - }) + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_update_old_feature_0 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -545,7 +610,8 @@ fn test_changing_entrypoint() { .expect("Updating old_feature on old module should work."); let res_update_new_feature_0 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -559,7 +625,8 @@ fn test_changing_entrypoint() { .expect_err("Updating new_feature on old module should _not_ work"); let res_update_upgrade = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -574,7 +641,8 @@ fn test_changing_entrypoint() { .expect("Upgrading contract should work."); let res_update_old_feature_1 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -588,7 +656,8 @@ fn test_changing_entrypoint() { .expect_err("Updating old_feature on _new_ module should _not_ work."); let res_update_new_feature_1 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(1000000), @@ -606,7 +675,7 @@ fn test_changing_entrypoint() { ])); assert!(matches!( res_update_new_feature_0.kind, - ContractInvocationErrorKind::ExecutionError { + ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); @@ -618,7 +687,7 @@ fn test_changing_entrypoint() { ])); assert!(matches!( res_update_old_feature_1.kind, - ContractInvocationErrorKind::ExecutionError { + ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); From 2251fd355b86eaf8383a0537dc9307650ca3cf5a Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Mar 2023 11:37:59 +0100 Subject: [PATCH 109/208] Explicitly handle balance overflows --- contract-testing/src/impls.rs | 30 +++++++-- contract-testing/src/invocation/impls.rs | 83 ++++++++++++++---------- contract-testing/src/invocation/mod.rs | 2 +- contract-testing/src/invocation/types.rs | 11 +++- contract-testing/src/types.rs | 35 +++++++--- 5 files changed, 109 insertions(+), 52 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 5dd715aa..43a797d6 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,4 +1,8 @@ -use crate::{constants, invocation::EntrypointInvocationHandler, types::*}; +use crate::{ + constants, + invocation::{EntrypointInvocationHandler, TestConfigurationError}, + types::*, +}; use anyhow::anyhow; use concordium_base::{ base::{Energy, OutOfEnergy}, @@ -531,7 +535,9 @@ impl Chain { remaining_energy, payload, ) - .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; + .map_err(|err| { + self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) + })?; // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. @@ -862,12 +868,21 @@ impl Chain { /// Convert a [`ContractInvocationErrorKind`] to a /// [`ContractInvocationError`] by calculating the `energy_used` and /// `transaction_fee`. + /// + /// If the `kind` is an out of energy, then `0` is used instead of the + /// `remaining_energy` parameter, as it will likely not be `0` due to short + /// circuiting during execution. fn from_invocation_error_kind( &self, kind: ContractInvokeErrorKind, energy_reserved: Energy, remaining_energy: Energy, ) -> ContractInvokeError { + let remaining_energy = if matches!(kind, ContractInvokeErrorKind::OutOfEnergy) { + 0.into() + } else { + remaining_energy + }; let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); ContractInvokeError { @@ -1041,12 +1056,17 @@ impl ChainEvent { } } -impl From for ContractInvokeErrorKind { +impl From for ContractInitErrorKind { fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } } -impl From for ContractInitErrorKind { - fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } +impl From for ContractInvokeErrorKind { + fn from(err: TestConfigurationError) -> Self { + match err { + TestConfigurationError::OutOfEnergy => Self::OutOfEnergy, + TestConfigurationError::BalanceOverflow => Self::BalanceOverflow, + } + } } /// Convert [`Energy`] to [`InterpreterEnergy`] by multiplying by `1000`. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 8c5e0dc1..c7e700f9 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -2,10 +2,7 @@ use super::types::*; use crate::{ constants, impls::{from_interpreter_energy, lookup_module_cost, to_interpreter_energy}, - types::{ - Account, Chain, ChainEvent, Contract, ContractModule, InsufficientBalanceError, - TransferError, - }, + types::{Account, BalanceError, Chain, ChainEvent, Contract, ContractModule, TransferError}, }; use concordium_base::{ base::{Energy, OutOfEnergy}, @@ -40,7 +37,8 @@ impl<'a> EntrypointInvocationHandler<'a> { sender: Address, remaining_energy: &'a mut Energy, payload: UpdateContractPayload, - ) -> Result<(InvokeEntrypointResponse, ChangeSet, Vec), OutOfEnergy> { + ) -> Result<(InvokeEntrypointResponse, ChangeSet, Vec), TestConfigurationError> + { let mut contract_invocation = Self { changeset: ChangeSet::new(), remaining_energy, @@ -74,7 +72,7 @@ impl<'a> EntrypointInvocationHandler<'a> { sender: Address, payload: UpdateContractPayload, chain_events: &mut Vec, - ) -> Result { + ) -> Result { // Charge the base cost for updating a contract. self.remaining_energy .tick_energy(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST)?; @@ -98,7 +96,15 @@ impl<'a> EntrypointInvocationHandler<'a> { Ok(new_balance_from) => new_balance_from, Err(transfer_error) => { let kind = match transfer_error { - TransferError::InsufficientBalance => v1::InvokeFailure::InsufficientAmount, + TransferError::BalanceError { + error: BalanceError::Overflow, + } => { + // Balance overflows are unrecoverable and short circuit. + return Err(TestConfigurationError::BalanceOverflow); + } + TransferError::BalanceError { + error: BalanceError::Insufficient, + } => v1::InvokeFailure::InsufficientAmount, TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. @@ -282,7 +288,7 @@ impl<'a> EntrypointInvocationHandler<'a> { }, v1::ReceiveResult::OutOfEnergy => { // Convert to an error so that we will short-circuit the processing. - return Err(OutOfEnergy); + return Err(TestConfigurationError::OutOfEnergy); } }; @@ -337,8 +343,7 @@ impl<'a> EntrypointInvocationHandler<'a> { // Make the transfer. let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; - self.change_contract_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); + self.change_contract_balance(to, AmountDelta::Positive(amount))?; Ok(new_balance) } @@ -362,16 +367,22 @@ impl<'a> EntrypointInvocationHandler<'a> { // Make the transfer. let new_balance = self.change_account_balance(from, AmountDelta::Negative(amount))?; - self.change_contract_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); + self.change_contract_balance(to, AmountDelta::Positive(amount))?; Ok(new_balance) } - // TODO: Should we handle overflows explicitly? /// Changes the contract balance in the topmost checkpoint on the changeset. /// /// If contract isn't already present in the changeset, it is added. /// + /// Returns an error if the change in balance would go below `0`, which is a + /// valid error, or if the amounts would overflow, which is an unrecoverable + /// configuration error in the tests. + /// Otherwise, it returns the new balance of the contract. + /// + /// The precondition is not part of the error type, as this is an internal + /// helper function that is only called when the precondition is met. + /// /// Returns the new balance. /// /// **Preconditions:** @@ -380,7 +391,7 @@ impl<'a> EntrypointInvocationHandler<'a> { &mut self, address: ContractAddress, delta: AmountDelta, - ) -> Result { + ) -> Result { match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { // get original balance @@ -410,7 +421,6 @@ impl<'a> EntrypointInvocationHandler<'a> { } } - // TODO: Should we handle overflows explicitly? /// Changes the account balance in the topmost checkpoint on the changeset. /// /// If account isn't already present in the changeset, it is added. @@ -418,6 +428,7 @@ impl<'a> EntrypointInvocationHandler<'a> { /// Returns an error if a negative delta is provided which exceeds the /// available balance of the account. Otherwise, it returns the new /// available balance of the account. + /// Otherwise, it returns the new balance of the account. /// /// The precondition is not part of the error type, as this is an internal /// helper function that is only called when the precondition is met. @@ -428,7 +439,7 @@ impl<'a> EntrypointInvocationHandler<'a> { &mut self, address: AccountAddress, delta: AmountDelta, - ) -> Result { + ) -> Result { match self.changeset.current_mut().accounts.entry(address) { btree_map::Entry::Vacant(vac) => { // get original balance @@ -985,17 +996,13 @@ impl AmountDelta { } } - /// Returns a new balance with the `AmountDelta` applied, or, an - /// error if `balance + self < 0`. - /// - /// Panics if an overflow occurs. - fn apply_to_balance(&self, balance: Amount) -> Result { + /// Returns a new balance with the `AmountDelta` applied, or a + /// [`BalanceError`] error if the change would result in a negative + /// balance or an overflow. + fn apply_to_balance(&self, balance: Amount) -> Result { match self { - AmountDelta::Positive(d) => Ok(balance - .checked_add(*d) - .expect("Overflow occured when adding Amounts.")), // TODO: Should we return an - // error for this? - AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(UnderflowError), + AmountDelta::Positive(d) => balance.checked_add(*d).ok_or(BalanceError::Overflow), + AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(BalanceError::Insufficient), } } } @@ -1047,7 +1054,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { fn process( &mut self, receive_result: v1::ReceiveResult, - ) -> Result, OutOfEnergy> { + ) -> Result, TestConfigurationError> { match receive_result { v1::ReceiveResult::Success { logs, @@ -1116,8 +1123,16 @@ impl<'a, 'b> InvocationData<'a, 'b> { TransferError::ToMissing => { v1::InvokeFailure::NonExistentAccount } - TransferError::InsufficientBalance => { - v1::InvokeFailure::InsufficientAmount + + TransferError::BalanceError { + error: BalanceError::Insufficient, + } => v1::InvokeFailure::InsufficientAmount, + + TransferError::BalanceError { + error: BalanceError::Overflow, + } => { + // Balance overflows are unrecoverable and short circuit. + return Err(TestConfigurationError::BalanceOverflow); } }; v1::InvokeResponse::Failure { kind } @@ -1447,7 +1462,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { }) } // Convert this to an error here, so that we will short circuit processing. - v1::ReceiveResult::OutOfEnergy => Err(OutOfEnergy), + v1::ReceiveResult::OutOfEnergy => Err(TestConfigurationError::OutOfEnergy), } } } @@ -1459,12 +1474,8 @@ impl InvokeEntrypointResponse { } } -impl From for InsufficientBalanceError { - fn from(_: UnderflowError) -> Self { InsufficientBalanceError } -} - -impl From for TransferError { - fn from(_: InsufficientBalanceError) -> Self { Self::InsufficientBalance } +impl From for TestConfigurationError { + fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } } #[cfg(test)] diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index c4b466fa..951071c0 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -14,4 +14,4 @@ mod impls; mod types; -pub(crate) use types::EntrypointInvocationHandler; +pub(crate) use types::{EntrypointInvocationHandler, TestConfigurationError}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index b9190ca6..50d16a96 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -123,6 +123,13 @@ pub(super) enum AmountDelta { Negative(Amount), } -/// An underflow occurred. +/// Errors that occur due to the configuration of the test. #[derive(Debug)] -pub(super) struct UnderflowError; +pub(crate) enum TestConfigurationError { + /// The method ran out of energy. + OutOfEnergy, + /// The balance of an account or contract oveflowed while adding a new + /// [`Amount`]. On the chain there is roughly 10 billion CCD, which + /// means that overflows of amounts cannot occur. + BalanceOverflow, +} diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index f33a6b27..55d4a249 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -336,6 +336,10 @@ pub enum ContractInvokeErrorKind { /// Ran out of energy. #[error("Ran out of energy")] OutOfEnergy, + /// The balance of an account or contract overflowed. + /// If you are seeing this error, lower the [`Amount`]s used in your tests. + #[error("The balance of an account or contract overflowed")] + BalanceOverflow, /// Module has not been deployed in test environment. #[error("{0}")] ModuleDoesNotExist(#[from] ModuleDoesNotExist), @@ -360,20 +364,35 @@ pub enum ContractInvokeErrorKind { ParameterTooLarge, } -/// A transfer of [`Amount`]s failed because the sender had insufficient -/// balance. -#[derive(Debug)] -pub(crate) struct InsufficientBalanceError; +/// A balance error which can occur when transferring [`Amount`]s. +#[derive(Debug, PartialEq, Eq, Error)] +pub(crate) enum BalanceError { + /// The sender had insufficient balance. + #[error("The sender had insufficient balance.")] + Insufficient, + /// The balance change resulted in an overflow. + /// + /// This is a configuration error in the tests, where unrealistic balances + /// have been set, and should thus be *unrecoverable*. + /// + /// On the chain there is roughly 10 billion CCD, so an overflow wil never + /// occur when adding CCDs. + #[error("An overflow on CCD amounts occurred.")] + Overflow, +} /// Errors related to transfers. -#[derive(PartialEq, Eq, Debug, Error)] +#[derive(Debug, PartialEq, Eq, Error)] pub(crate) enum TransferError { /// The receiver does not exist. #[error("The receiver does not exist.")] ToMissing, - /// The sender does not have sufficient balance. - #[error("The sender does not have sufficient balance.")] - InsufficientBalance, + /// A balance error occurred. + #[error("A balance error occurred: {error:?}")] + BalanceError { + #[from] + error: BalanceError, + }, } /// The entrypoint does not exist. From c2f825568efca0edf69a96d20722016a165a2b31 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Mar 2023 15:28:03 +0100 Subject: [PATCH 110/208] Support account aliases --- contract-testing/src/impls.rs | 125 +++++++++++++++++++++-- contract-testing/src/invocation/impls.rs | 15 +-- contract-testing/src/invocation/types.rs | 9 +- contract-testing/src/types.rs | 13 ++- 4 files changed, 140 insertions(+), 22 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 43a797d6..142e4010 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -777,12 +777,31 @@ impl Chain { /// Create an account. /// /// Will override an existing account and return it. - pub fn create_account( - &mut self, - account: AccountAddress, - account_info: Account, - ) -> Option { - self.accounts.insert(account, account_info) + /// + /// Note that if the first 29-bytes of an account are identical, then + /// they are *considered aliases* on each other in all methods. + /// See the example below: + /// + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let mut chain = Chain::new(); + /// let acc = AccountAddress([ + /// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /// 0, 0, + /// ]); + /// let acc_alias = AccountAddress([ + /// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + /// 2, 3, // Only last three bytes differ. + /// ]); + /// + /// chain.create_account(acc, Account::new(Amount::from_ccd(123))); + /// assert_eq!( + /// chain.account_balance_available(acc_alias), // Using the alias for lookup. + /// Some(Amount::from_ccd(123)) + /// ); + /// ``` + pub fn create_account(&mut self, address: AccountAddress, account: Account) -> Option { + self.accounts.insert(address.into(), account) } /// Create a contract address by giving it the next available index. @@ -795,12 +814,14 @@ impl Chain { /// Returns the balance of an account if it exists. pub fn account_balance(&self, address: AccountAddress) -> Option { - self.accounts.get(&address).map(|ai| ai.balance) + self.accounts.get(&address.into()).map(|ai| ai.balance) } /// Returns the available balance of an account if it exists. pub fn account_balance_available(&self, address: AccountAddress) -> Option { - self.accounts.get(&address).map(|ai| ai.balance.available()) + self.accounts + .get(&address.into()) + .map(|ai| ai.balance.available()) } /// Returns the balance of an contract if it exists. @@ -830,7 +851,7 @@ impl Chain { /// Returns an immutable reference to an [`Account`]. pub fn account(&self, address: AccountAddress) -> Result<&Account, AccountDoesNotExist> { self.accounts - .get(&address) + .get(&address.into()) .ok_or(AccountDoesNotExist { address }) } @@ -840,13 +861,13 @@ impl Chain { address: AccountAddress, ) -> Result<&mut Account, AccountDoesNotExist> { self.accounts - .get_mut(&address) + .get_mut(&address.into()) .ok_or(AccountDoesNotExist { address }) } /// Check whether an [`Account`] exists. pub fn account_exists(&self, address: AccountAddress) -> bool { - self.accounts.contains_key(&address) + self.accounts.contains_key(&address.into()) } /// Check whether a [`Contract`] exists. @@ -999,6 +1020,46 @@ impl Account { } } +impl From for AccountAddress { + fn from(aae: AccountAddressEq) -> Self { aae.0 } +} + +impl From for AccountAddressEq { + fn from(address: AccountAddress) -> Self { Self(address) } +} + +impl PartialEq for AccountAddressEq { + fn eq(&self, other: &Self) -> bool { + let bytes_1: &[u8; 32] = self.0.as_ref(); + let bytes_2: &[u8; 32] = other.0.as_ref(); + bytes_1[0..29] == bytes_2[0..29] + } +} + +impl PartialOrd for AccountAddressEq { + fn partial_cmp(&self, other: &Self) -> Option { + let bytes_1: &[u8; 32] = self.0.as_ref(); + let bytes_2: &[u8; 32] = other.0.as_ref(); + if bytes_1[0..29] < bytes_2[0..29] { + Some(std::cmp::Ordering::Less) + } else if bytes_1[0..29] > bytes_2[0..29] { + Some(std::cmp::Ordering::Greater) + } else { + Some(std::cmp::Ordering::Equal) + } + } +} + +impl Ord for AccountAddressEq { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).expect("Is always some.") + } +} + +impl AsRef for AccountAddress { + fn as_ref(&self) -> &AccountAddressEq { unsafe { std::mem::transmute(self) } } +} + impl Signer { /// Create a signer which always signs with one key. pub fn with_one_key() -> Self { Self { num_keys: 1 } } @@ -1175,4 +1236,46 @@ mod tests { ) .expect("should succeed"); } + + /// Test that account aliases are seen as one account. + #[test] + fn test_account_aliases() { + let mut chain = Chain::new(); + let acc = AccountAddress([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + let acc_alias = AccountAddress([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 3, // Last three bytes can differ for aliases. + ]); + let acc_other = AccountAddress([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 2, 3, 4, // This differs on last four bytes, so it is a different account. + ]); + let acc_eq = AccountAddressEq(acc); + let acc_alias_eq = AccountAddressEq(acc_alias); + let acc_other_eq = AccountAddressEq(acc_other); + + let expected_amount = Amount::from_ccd(10); + let expected_amount_other = Amount::from_ccd(123); + + chain.create_account(acc, Account::new(expected_amount)); + chain.create_account(acc_other, Account::new(expected_amount_other)); + + assert_eq!(acc_eq, acc_alias_eq); + assert_ne!(acc_eq, acc_other_eq); + + assert_eq!(acc_eq.cmp(&acc_alias_eq), std::cmp::Ordering::Equal); + assert_eq!(acc_eq.cmp(&acc_other_eq), std::cmp::Ordering::Less); + + assert_eq!( + chain.account_balance_available(acc_alias), + Some(expected_amount) + ); + assert_eq!( + chain.account_balance_available(acc_other), + Some(expected_amount_other) + ); + } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index c7e700f9..9da12846 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -3,6 +3,7 @@ use crate::{ constants, impls::{from_interpreter_energy, lookup_module_cost, to_interpreter_energy}, types::{Account, BalanceError, Chain, ChainEvent, Contract, ContractModule, TransferError}, + AccountAddressEq, }; use concordium_base::{ base::{Energy, OutOfEnergy}, @@ -195,7 +196,7 @@ impl<'a> EntrypointInvocationHandler<'a> { sender_policies: to_bytes( &self .accounts - .get(&invoker) + .get(&invoker.into()) .expect("Precondition violation: invoker must exist.") .policy, ), @@ -312,7 +313,7 @@ impl<'a> EntrypointInvocationHandler<'a> { to: AccountAddress, ) -> Result { // Ensure the `to` account exists. - if !self.accounts.contains_key(&to) { + if !self.accounts.contains_key(&to.into()) { return Err(TransferError::ToMissing); } @@ -440,12 +441,12 @@ impl<'a> EntrypointInvocationHandler<'a> { address: AccountAddress, delta: AmountDelta, ) -> Result { - match self.changeset.current_mut().accounts.entry(address) { + match self.changeset.current_mut().accounts.entry(address.into()) { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self .accounts - .get(&address) + .get(&address.into()) .expect("Precondition violation: account assumed to exist") .balance .available(); @@ -553,12 +554,12 @@ impl<'a> EntrypointInvocationHandler<'a> { /// Looks up the account balance for an account by first checking /// the changeset, then the persisted values. fn account_balance(&self, address: AccountAddress) -> Option { - let account_balance = self.accounts.get(&address).map(|a| a.balance)?; + let account_balance = self.accounts.get(&address.into()).map(|a| a.balance)?; match self .changeset .current() .accounts - .get(&address) + .get(&address.into()) .map(|a| a.current_balance()) { // Account exists in changeset. @@ -834,7 +835,7 @@ impl ChangeSet { mut self, mut remaining_energy: Energy, invoked_contract: ContractAddress, - persisted_accounts: &mut BTreeMap, + persisted_accounts: &mut BTreeMap, persisted_contracts: &mut BTreeMap, ) -> Result<(Energy, bool), OutOfEnergy> { let current = self.current_mut(); diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 50d16a96..2d1d3ed6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,4 +1,7 @@ -use crate::types::{Account, ChainEvent, Contract, ContractModule}; +use crate::{ + types::{Account, ChainEvent, Contract, ContractModule}, + AccountAddressEq, +}; use concordium_base::{ base::Energy, contracts_common::{ @@ -30,7 +33,7 @@ pub(crate) struct EntrypointInvocationHandler<'a> { pub(super) remaining_energy: &'a mut Energy, /// The accounts of the chain. These are currently clones and only used as a /// reference. Any changes are saved to the changeset. - pub(super) accounts: BTreeMap, + pub(super) accounts: BTreeMap, /// The modules of the chain. These are currently clones and only used as a /// reference. Any changes are saved to the changeset. pub(super) modules: BTreeMap, @@ -59,7 +62,7 @@ pub(super) struct Changes { /// The contracts which have changes. pub(super) contracts: BTreeMap, /// The accounts which have changes. - pub(super) accounts: BTreeMap, + pub(super) accounts: BTreeMap, } /// Data held for an account during the execution of a contract entrypoint. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 55d4a249..0e0bd219 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -41,7 +41,9 @@ pub struct Chain { // [`Chain`]. pub(crate) euro_per_energy: ExchangeRate, /// Accounts and info about them. - pub accounts: BTreeMap, + /// This uses [`AccountAddressEq`] to ensure that account aliases are seen + /// as one account. + pub accounts: BTreeMap, /// Smart contract modules. pub modules: BTreeMap, /// Smart contract instances. @@ -74,6 +76,15 @@ pub struct Account { pub policy: OwnedPolicy, } +/// A helper struct that is used to ensure that aliases of an account are seen +/// as being the same account. +/// +/// Account aliases share the first 29 bytes of the address, so the +/// [`PartialEq`]/[`PartialOrd`] for this type adheres to that. +#[repr(transparent)] +#[derive(Eq, Debug, Clone, Copy)] +pub struct AccountAddressEq(pub(crate) AccountAddress); + /// A signer with a number of keys, the amount of which affects the cost of /// transactions. #[derive(Copy, Clone, Debug)] From 656512c4a2a129d596979eb01a3e97f111c965c2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 23 Mar 2023 16:57:02 +0100 Subject: [PATCH 111/208] Replace `ChainEvent` with `ContractTraceElement` - Also replaces `v0::Logs` with `Vec` for consistency --- contract-testing/src/impls.rs | 52 ++++++----- contract-testing/src/invocation/impls.rs | 87 +++++++++++------- contract-testing/src/invocation/types.rs | 19 ++-- contract-testing/src/lib.rs | 5 +- contract-testing/src/types.rs | 70 ++------------- contract-testing/tests/queries.rs | 52 +++++------ contract-testing/tests/transfer.rs | 63 ++++++++----- contract-testing/tests/upgrades.rs | 108 +++++++++++------------ 8 files changed, 228 insertions(+), 228 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 142e4010..1d4fd93f 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -11,7 +11,10 @@ use concordium_base::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, - smart_contracts::{ModuleSource, WasmModule, WasmVersion}, + smart_contracts::{ + ContractEvent, ContractTraceElement, InstanceUpdatedEvent, ModuleSource, WasmModule, + WasmVersion, + }, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; @@ -442,7 +445,7 @@ impl Chain { Ok(ContractInitSuccess { contract_address, - logs, + events: contract_events_from_logs(logs), energy_used, transaction_fee, }) @@ -527,7 +530,7 @@ impl Chain { )); } let contract_address = payload.address; - let (result, changeset, chain_events) = + let (result, changeset, trace_elements) = EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( self, invoker, @@ -572,13 +575,13 @@ impl Chain { let energy_used = energy_reserved - *remaining_energy; let transaction_fee = self.calculate_energy_cost(energy_used); Ok(ContractInvokeSuccess { - chain_events, + trace_elements, energy_used, transaction_fee, return_value: data.unwrap_or_default(), state_changed, new_balance, - logs: result.logs, + events: contract_events_from_logs(result.logs), }) } v1::InvokeResponse::Failure { kind } => Err(self.from_invocation_error_kind( @@ -1077,8 +1080,8 @@ impl ContractInvokeSuccess { /// Get an iterator of all transfers that were made from contracts to /// accounts. pub fn transfers(&self) -> impl Iterator + '_ { - self.chain_events.iter().filter_map(|e| { - if let ChainEvent::Transferred { from, amount, to } = e { + self.trace_elements.iter().filter_map(|e| { + if let ContractTraceElement::Transferred { from, amount, to } = e { Some(Transfer { from: *from, amount: *amount, @@ -1090,29 +1093,31 @@ impl ContractInvokeSuccess { }) } - /// Get the chain events grouped by which contract they originated from. - pub fn chain_events_per_contract(&self) -> BTreeMap> { - let mut map: BTreeMap> = BTreeMap::new(); - for event in self.chain_events.iter() { - map.entry(event.contract_address()) + /// Get the trace elements grouped by which contract they originated from. + pub fn trace_elements_per_contract( + &self, + ) -> BTreeMap> { + let mut map: BTreeMap> = BTreeMap::new(); + for event in self.trace_elements.iter() { + map.entry(Self::extract_contract_address(&event)) .and_modify(|v| v.push(event.clone())) .or_insert(vec![event.clone()]); } map } -} -impl ChainEvent { /// Get the contract address that this event relates to. /// This means the `address` field for all variant except `Transferred`, /// where it returns the `from`. - pub fn contract_address(&self) -> ContractAddress { - match self { - ChainEvent::Interrupted { address, .. } => *address, - ChainEvent::Resumed { address, .. } => *address, - ChainEvent::Upgraded { address, .. } => *address, - ChainEvent::Updated { address, .. } => *address, - ChainEvent::Transferred { from, .. } => *from, + fn extract_contract_address(element: &ContractTraceElement) -> ContractAddress { + match element { + ContractTraceElement::Interrupted { address, .. } => *address, + ContractTraceElement::Resumed { address, .. } => *address, + ContractTraceElement::Upgraded { address, .. } => *address, + ContractTraceElement::Updated { + data: InstanceUpdatedEvent { address, .. }, + } => *address, + ContractTraceElement::Transferred { from, .. } => *from, } } } @@ -1203,6 +1208,11 @@ fn check_exchange_rates( Ok(()) } +/// A helper function for converting `[v0::Logs]` into [`Vec`]. +pub(crate) fn contract_events_from_logs(logs: v0::Logs) -> Vec { + logs.logs.into_iter().map(ContractEvent::from).collect() +} + #[cfg(test)] mod tests { use super::*; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 9da12846..171f6c84 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1,8 +1,11 @@ use super::types::*; use crate::{ constants, - impls::{from_interpreter_energy, lookup_module_cost, to_interpreter_energy}, - types::{Account, BalanceError, Chain, ChainEvent, Contract, ContractModule, TransferError}, + impls::{ + contract_events_from_logs, from_interpreter_energy, lookup_module_cost, + to_interpreter_energy, + }, + types::{Account, BalanceError, Chain, Contract, ContractModule, TransferError}, AccountAddressEq, }; use concordium_base::{ @@ -11,7 +14,9 @@ use concordium_base::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedReceiveName, }, - smart_contracts::{OwnedContractName, OwnedParameter}, + smart_contracts::{ + ContractTraceElement, InstanceUpdatedEvent, OwnedContractName, OwnedParameter, WasmVersion, + }, transactions::UpdateContractPayload, }; use concordium_smart_contract_engine::{ @@ -38,8 +43,14 @@ impl<'a> EntrypointInvocationHandler<'a> { sender: Address, remaining_energy: &'a mut Energy, payload: UpdateContractPayload, - ) -> Result<(InvokeEntrypointResponse, ChangeSet, Vec), TestConfigurationError> - { + ) -> Result< + ( + InvokeEntrypointResponse, + ChangeSet, + Vec, + ), + TestConfigurationError, + > { let mut contract_invocation = Self { changeset: ChangeSet::new(), remaining_energy, @@ -53,10 +64,10 @@ impl<'a> EntrypointInvocationHandler<'a> { micro_ccd_per_euro: chain.micro_ccd_per_euro, }; - let mut chain_events = Vec::new(); + let mut trace_elements = Vec::new(); let result = - contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut chain_events)?; - Ok((result, contract_invocation.changeset, chain_events)) + contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements)?; + Ok((result, contract_invocation.changeset, trace_elements)) } /// Used for handling contract entrypoint invocations internally. @@ -72,7 +83,7 @@ impl<'a> EntrypointInvocationHandler<'a> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - chain_events: &mut Vec, + trace_elements: &mut Vec, ) -> Result { // Charge the base cost for updating a contract. self.remaining_energy @@ -234,18 +245,20 @@ impl<'a> EntrypointInvocationHandler<'a> { // i.e. beyond interrupts. let mut data = InvocationData { invoker, + sender, address: payload.address, contract_name, - amount: payload.amount, entrypoint: entrypoint_name, + parameter: payload.message, + amount: payload.amount, invocation_handler: self, state: mutable_state, - chain_events: Vec::new(), + trace_elements: Vec::new(), }; // Process the receive invocation to the completion. let result = data.process(initial_result)?; - let mut new_chain_events = data.chain_events; + let mut new_trace_elements = data.trace_elements; let result = match result { v1::ReceiveResult::Success { @@ -293,9 +306,9 @@ impl<'a> EntrypointInvocationHandler<'a> { } }; - // Append the new chain events if the invocation succeeded. + // Append the new trace elements if the invocation succeeded. if result.is_success() { - chain_events.append(&mut new_chain_events); + trace_elements.append(&mut new_trace_elements); } Ok(result) @@ -1066,14 +1079,22 @@ impl<'a, 'b> InvocationData<'a, 'b> { // Update the remaining_energy field. self.invocation_handler.update_energy(remaining_energy); - let update_event = ChainEvent::Updated { - address: self.address, - contract: self.contract_name.clone(), - entrypoint: self.entrypoint.clone(), - amount: self.amount, + let update_event = ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + contract_version: WasmVersion::V1, + address: self.address, + instigator: self.sender, + amount: self.amount, + message: self.parameter.clone(), + receive_name: OwnedReceiveName::construct_unchecked( + self.contract_name.as_contract_name(), + self.entrypoint.as_entrypoint_name(), + ), + events: contract_events_from_logs(logs.clone()), + }, }; // Add update event - self.chain_events.push(update_event); + self.trace_elements.push(update_event); // Save changes to changeset. if state_changed { @@ -1102,14 +1123,14 @@ impl<'a, 'b> InvocationData<'a, 'b> { self.invocation_handler.update_energy(remaining_energy); // Create the interrupt event, which will be included for transfers, calls, and // upgrades, but not for the remaining interrupts. - let interrupt_event = ChainEvent::Interrupted { + let interrupt_event = ContractTraceElement::Interrupted { address: self.address, - logs, + events: contract_events_from_logs(logs.clone()), }; match interrupt { v1::Interrupt::Transfer { to, amount } => { // Add the interrupt event - self.chain_events.push(interrupt_event); + self.trace_elements.push(interrupt_event); let response = match self .invocation_handler @@ -1143,14 +1164,14 @@ impl<'a, 'b> InvocationData<'a, 'b> { let success = matches!(response, v1::InvokeResponse::Success { .. }); if success { // Add transfer event - self.chain_events.push(ChainEvent::Transferred { + self.trace_elements.push(ContractTraceElement::Transferred { from: self.address, amount, to, }); } // Add resume event - self.chain_events.push(ChainEvent::Resumed { + self.trace_elements.push(ContractTraceElement::Resumed { address: self.address, success, }); @@ -1182,7 +1203,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { amount, } => { // Add the interrupt event - self.chain_events.push(interrupt_event); + self.trace_elements.push(interrupt_event); if state_changed { self.invocation_handler.save_state_changes( @@ -1228,7 +1249,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { receive_name, message, }, - &mut self.chain_events, + &mut self.trace_elements, )? } }; @@ -1253,12 +1274,12 @@ impl<'a, 'b> InvocationData<'a, 'b> { }; // Add resume event - let resume_event = ChainEvent::Resumed { + let resume_event = ContractTraceElement::Resumed { address: self.address, success, }; - self.chain_events.push(resume_event); + self.trace_elements.push(resume_event); let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( @@ -1275,7 +1296,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { } v1::Interrupt::Upgrade { module_ref } => { // Add the interrupt event. - self.chain_events.push(interrupt_event); + self.trace_elements.push(interrupt_event); // Charge a base cost. self.invocation_handler @@ -1305,13 +1326,13 @@ impl<'a, 'b> InvocationData<'a, 'b> { constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, )?; - let upgrade_event = ChainEvent::Upgraded { + let upgrade_event = ContractTraceElement::Upgraded { address: self.address, from: old_module_ref, to: module_ref, }; - self.chain_events.push(upgrade_event); + self.trace_elements.push(upgrade_event); v1::InvokeResponse::Success { new_balance: self @@ -1328,7 +1349,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { }; let success = matches!(response, v1::InvokeResponse::Success { .. }); - self.chain_events.push(ChainEvent::Resumed { + self.trace_elements.push(ContractTraceElement::Resumed { address: self.address, success, }); diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 2d1d3ed6..9b88754c 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,13 +1,14 @@ use crate::{ - types::{Account, ChainEvent, Contract, ContractModule}, + types::{Account, Contract, ContractModule}, AccountAddressEq, }; use concordium_base::{ base::Energy, contracts_common::{ - AccountAddress, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, - OwnedEntrypointName, SlotTime, + AccountAddress, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, + OwnedContractName, OwnedEntrypointName, SlotTime, }, + smart_contracts::{ContractTraceElement, OwnedParameter}, }; use concordium_smart_contract_engine::{ v0, @@ -100,21 +101,25 @@ pub(super) struct ContractChanges { pub(super) struct InvocationData<'a, 'b> { /// The invoker. pub(super) invoker: AccountAddress, + /// The sender. + pub(super) sender: Address, /// The contract being called. pub(super) address: ContractAddress, /// The name of the contract. pub(super) contract_name: OwnedContractName, - /// The amount sent from the sender to the contract. - pub(super) amount: Amount, /// The entrypoint to execute. pub(super) entrypoint: OwnedEntrypointName, + /// The amount sent from the sender to the contract. + pub(super) amount: Amount, + /// The parameter given to the entrypoint. + pub(super) parameter: OwnedParameter, /// A reference to the [`EntrypointInvocationHandler`], which is used to for /// handling interrupts and for querying chain data. pub(super) invocation_handler: &'a mut EntrypointInvocationHandler<'b>, /// The current state. pub(super) state: MutableState, - /// Chain events that have occurred during the execution. - pub(super) chain_events: Vec, + /// Trace elements that have occurred during the execution. + pub(super) trace_elements: Vec, } /// A positive or negative delta in for an [`Amount`]. diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 030972e2..13dfff94 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -56,8 +56,8 @@ //! ) //! .unwrap(); //! -//! // Check the chain events produced (updates, interrupts, resumes, transfers, etc.). -//! assert!(matches!(update.chain_events[..], [ChainEvent::Updated{..}])); +//! // Check the trace elements produced (updates, interrupts, resumes, transfers, etc.). +//! assert!(matches!(update.trace_elements[..], [ContractTraceElement::Updated{..}])); //! //! // Check the return value. //! assert_eq!(update.return_value, to_bytes(&84u8)); @@ -86,6 +86,7 @@ pub use concordium_base::{ EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, }, + smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, transactions::{InitContractPayload, UpdateContractPayload}, }; pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 0e0bd219..864a46b3 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -1,19 +1,15 @@ -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; -use thiserror::Error; - use concordium_base::{ base::Energy, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, - smart_contracts::WasmVersion, -}; -use concordium_smart_contract_engine::{ - v0, - v1::{self, trie, ReturnValue}, + smart_contracts::{ContractEvent, ContractTraceElement, WasmVersion}, }; +use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; use concordium_wasm::artifact; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; +use thiserror::Error; /// A smart contract module. #[derive(Debug, Clone)] @@ -93,54 +89,6 @@ pub struct Signer { pub(crate) num_keys: u32, } -/// An event that occurred during a contract update or invocation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ChainEvent { - /// A contract was interrupted. - Interrupted { - /// The contract interrupted. - address: ContractAddress, - /// Logs produced prior to being interrupted. - logs: v0::Logs, - }, - /// A contract was resumed after being interrupted. - Resumed { - /// The contract resumed. - address: ContractAddress, - /// Whether the action that caused the interrupt succeeded. - success: bool, - }, - /// A contract was upgraded. - Upgraded { - /// The contract upgraded. - address: ContractAddress, - /// The old module reference. - from: ModuleReference, - /// The new module reference. - to: ModuleReference, - }, - /// A contract was updated. - Updated { - /// The contract updated. - address: ContractAddress, - /// The name of the contract. - contract: OwnedContractName, - /// The entrypoint called. - entrypoint: OwnedEntrypointName, - /// The amount added to the contract. - amount: Amount, - }, - /// A contract transferred an [`Amount`] to an account. - Transferred { - /// The sender contract. - from: ContractAddress, - /// The [`Amount`] transferred. - amount: Amount, - /// The receiver account. - to: AccountAddress, - }, -} - /// A transfer from a contract to an account. #[derive(Debug, PartialEq, Eq)] pub struct Transfer { @@ -237,8 +185,8 @@ pub struct ModuleInvalidError(#[from] pub(crate) anyhow::Error); pub struct ContractInitSuccess { /// The address of the new instance. pub contract_address: ContractAddress, - /// Logs produced during initialization. - pub logs: v0::Logs, + /// Contract events (logs) produced during initialization. + pub events: Vec, /// Energy used. pub energy_used: Energy, /// Cost of transaction. @@ -309,7 +257,7 @@ pub struct ExecutionError(#[from] pub(crate) anyhow::Error); pub struct ContractInvokeSuccess { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. - pub chain_events: Vec, + pub trace_elements: Vec, /// Energy used. pub energy_used: Energy, /// Cost of transaction. @@ -320,8 +268,8 @@ pub struct ContractInvokeSuccess { pub state_changed: bool, /// The new balance of the smart contract. pub new_balance: Amount, - /// The logs produced since the last interrupt. - pub logs: v0::Logs, + /// The contract events (logs) produced since the last interrupt. + pub events: Vec, } /// An error that occured during a [`Chain::contract_update`] or diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 25cd4520..cf92b217 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -78,8 +78,8 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -154,8 +154,8 @@ mod query_account_balance { // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -236,11 +236,11 @@ mod query_account_balance { chain.account_balance_available(ACC_1), Some(initial_balance + amount_to_send) ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Transferred { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Transferred { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. } ])); } @@ -306,8 +306,8 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -373,8 +373,8 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } } @@ -451,8 +451,8 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -512,8 +512,8 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -577,11 +577,11 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Transferred { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Transferred { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. } ])); } @@ -637,8 +637,8 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } } @@ -699,8 +699,8 @@ mod query_exchange_rates { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 22aa4b79..b5bf72c2 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -1,6 +1,5 @@ //! This module contains tests for transfers fr&om a contract to an account. //! See more details about the specific test inside the `transfer.wat` file. -use concordium_smart_contract_engine::v0::Logs; use concordium_smart_contract_testing::*; const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; @@ -13,7 +12,8 @@ fn test_transfer() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -21,18 +21,24 @@ fn test_transfer() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_transfer".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_transfer".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let contract_address = res_init.contract_address; chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -53,7 +59,8 @@ fn test_transfer() { // Deposit 1000 micro CCD. chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -66,17 +73,20 @@ fn test_transfer() { ) .expect("Updating contract should succeed"); + // Tell it to send 17 mCCD to ACC_0. + let parameter = OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))) + .expect("Parameter has valid size"); + let res_update = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), - message: OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))) - .expect("Parameter has valid size"), /* Tell it to send 17 - * mCCD to ACC_0. */ + message: parameter.clone(), amount: Amount::zero(), }, ) @@ -86,25 +96,30 @@ fn test_transfer() { Amount::from_micro_ccd(1000 - 17), chain.contracts.get(&contract_address).unwrap().self_balance ); - assert_eq!(res_update.chain_events[..], [ - ChainEvent::Interrupted { + assert_eq!(res_update.trace_elements[..], [ + ContractTraceElement::Interrupted { address: contract_address, - logs: Logs::new(), + events: Vec::new(), }, - ChainEvent::Transferred { + ContractTraceElement::Transferred { from: contract_address, amount: Amount::from_micro_ccd(17), to: ACC_0, }, - ChainEvent::Resumed { + ContractTraceElement::Resumed { address: contract_address, success: true, }, - ChainEvent::Updated { - address: contract_address, - contract: OwnedContractName::new_unchecked("init_transfer".into()), - entrypoint: OwnedEntrypointName::new_unchecked("send".into()), - amount: Amount::zero(), + ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + address: contract_address, + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), + contract_version: concordium_base::smart_contracts::WasmVersion::V1, + instigator: Address::Account(ACC_0), + message: parameter, + events: Vec::new(), + }, } ]) } diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 5e48a272..8f48e49b 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -83,14 +83,14 @@ fn test() { ) .expect("Updating the `newfun` from the `upgrading_1` module should work"); - assert!(matches!(res_update_upgrade.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from, to, .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. }, + assert!(matches!(res_update_upgrade.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { from, to, .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. }, ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference)); - assert!(matches!(res_update_new.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update_new.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } @@ -150,21 +150,21 @@ fn test_self_invoke() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ + assert!(matches!(res_update.trace_elements[..], [ // Invoking `contract.name` - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, // Making the upgrade - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { .. }, + ContractTraceElement::Resumed { .. }, // Invoking contract.name again - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, // The successful update - ChainEvent::Updated { .. }, + ContractTraceElement::Updated { .. }, ])); } @@ -218,11 +218,11 @@ fn test_missing_module() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. - ChainEvent::Resumed { success, .. }, - ChainEvent::Updated { .. }, + ContractTraceElement::Resumed { success, .. }, + ContractTraceElement::Updated { .. }, ] if success == false)); } @@ -290,11 +290,11 @@ fn test_missing_contract() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ - ChainEvent::Interrupted { .. }, + assert!(matches!(res_update.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. - ChainEvent::Resumed { success, .. }, - ChainEvent::Updated { .. }, + ContractTraceElement::Resumed { success, .. }, + ContractTraceElement::Updated { .. }, ] if success == false)); } @@ -366,29 +366,29 @@ fn test_twice_in_one_transaction() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.chain_events[..], [ + assert!(matches!(res_update.trace_elements[..], [ // Invoke the contract itself to check the name entrypoint return value. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, // Upgrade from module 0 to 1 - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from: first_from, to: first_to, .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { from: first_from, to: first_to, .. }, + ContractTraceElement::Resumed { .. }, // Invoke the contract itself to check the name again. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, // Upgrade again - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { from: second_from, to: second_to, .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { from: second_from, to: second_to, .. }, + ContractTraceElement::Resumed { .. }, // Invoke itself again to check name a final time. - ChainEvent::Interrupted { .. }, - ChainEvent::Updated { .. }, - ChainEvent::Resumed { .. }, + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, // Final update event - ChainEvent::Updated { .. }, + ContractTraceElement::Updated { .. }, ] if first_from == res_deploy_0.module_reference && first_to == res_deploy_1.module_reference && second_from == res_deploy_1.module_reference @@ -427,7 +427,7 @@ fn test_chained_contract() { ) .expect("Initializing valid contract should work"); - let number_of_upgrades: u32 = 82; // TODO: Stack will overflow if larger than 82. + let number_of_upgrades: u32 = 76; // TODO: Stack will overflow if set larger. let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain @@ -450,7 +450,7 @@ fn test_chained_contract() { // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful // update. assert_eq!( - res_update.chain_events.len() as u32, + res_update.trace_elements.len() as u32, 6 * number_of_upgrades + 4 ) } @@ -670,8 +670,8 @@ fn test_changing_entrypoint() { ) .expect("Updating new_feature on _new_ module should work"); - assert!(matches!(res_update_old_feature_0.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update_old_feature_0.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); assert!(matches!( res_update_new_feature_0.kind, @@ -679,11 +679,11 @@ fn test_changing_entrypoint() { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); - assert!(matches!(res_update_upgrade.chain_events[..], [ - ChainEvent::Interrupted { .. }, - ChainEvent::Upgraded { .. }, - ChainEvent::Resumed { .. }, - ChainEvent::Updated { .. }, + assert!(matches!(res_update_upgrade.trace_elements[..], [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. }, ])); assert!(matches!( res_update_old_feature_1.kind, @@ -691,7 +691,7 @@ fn test_changing_entrypoint() { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); - assert!(matches!(res_update_new_feature_1.chain_events[..], [ - ChainEvent::Updated { .. } + assert!(matches!(res_update_new_feature_1.trace_elements[..], [ + ContractTraceElement::Updated { .. } ])); } From cb9d32273dd56e206043e10c9febaf367f068678 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 24 Mar 2023 13:49:03 +0100 Subject: [PATCH 112/208] Add self-balance test and fix issues found --- contract-testing/src/invocation/impls.rs | 49 +++++--- contract-testing/tests/self_balance.rs | 148 +++++++++++++++++++++++ contract-testing/tests/upgrades.rs | 2 +- 3 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 contract-testing/tests/self_balance.rs diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 171f6c84..931f104b 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -315,6 +315,7 @@ impl<'a> EntrypointInvocationHandler<'a> { } /// Make a transfer from a contract to an account in the changeset. + /// /// Returns the new balance of `from`. /// /// **Preconditions:** @@ -340,7 +341,7 @@ impl<'a> EntrypointInvocationHandler<'a> { /// Make a transfer between contracts in the changeset. /// - /// Returns the new balance of `from`. + /// Returns the new balance of `to`. /// /// **Preconditions:** /// - Assumes that `from` contract exists. @@ -356,15 +357,14 @@ impl<'a> EntrypointInvocationHandler<'a> { } // Make the transfer. - let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; - self.change_contract_balance(to, AmountDelta::Positive(amount))?; - + self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; Ok(new_balance) } /// Make a transfer from an account to a contract in the changeset. /// - /// Returns the new balance of `from`. + /// Returns the new balance of `to`. /// /// **Preconditions:** /// - Assumes that `from` account exists. @@ -380,8 +380,8 @@ impl<'a> EntrypointInvocationHandler<'a> { } // Make the transfer. - let new_balance = self.change_account_balance(from, AmountDelta::Negative(amount))?; - self.change_contract_balance(to, AmountDelta::Positive(amount))?; + self.change_account_balance(from, AmountDelta::Negative(amount))?; + let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; Ok(new_balance) } @@ -1221,26 +1221,26 @@ impl<'a, 'b> InvocationData<'a, 'b> { // back. self.invocation_handler.checkpoint(); - let res = match self + let (success, invoke_response) = match self .invocation_handler .contracts .get(&address) .map(|c| c.contract_name.as_contract_name()) { // The contract to call does not exist. - None => InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { + None => { + let invoke_response = v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, - }, - logs: v0::Logs::new(), - }, + }; + (false, invoke_response) + } Some(contract_name) => { let receive_name = OwnedReceiveName::construct_unchecked( contract_name, name.as_entrypoint_name(), ); let message = OwnedParameter::new_unchecked(parameter); - self.invocation_handler.invoke_entrypoint( + let res = self.invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), UpdateContractPayload { @@ -1250,12 +1250,25 @@ impl<'a, 'b> InvocationData<'a, 'b> { message, }, &mut self.trace_elements, - )? + )?; + match res.invoke_response { + v1::InvokeResponse::Success { data, .. } => { + let invoke_response = v1::InvokeResponse::Success { + // The balance returned by `invoke_entrypoint` is the + // balance of the contract called. But we are interested + // in the new balance of the caller. + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data, + }; + (true, invoke_response) + } + failure => (false, failure), + } } }; - let success = res.is_success(); - // Remove the last state changes if the invocation failed. let state_changed = if !success { self.invocation_handler.rollback(); @@ -1284,7 +1297,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { let resume_res = self.invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, - res.invoke_response, + invoke_response, energy, &mut self.state, state_changed, diff --git a/contract-testing/tests/self_balance.rs b/contract-testing/tests/self_balance.rs new file mode 100644 index 00000000..2c7e2906 --- /dev/null +++ b/contract-testing/tests/self_balance.rs @@ -0,0 +1,148 @@ +//! This module tests that the correct self-balance is exposed to V1 contracts. +//! In essense that the self-balance is updated by the invoke. +//! +//! See more details about the specific test inside the `self-balance.wat` and +//! `self-blaance-nested.wat` files. +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +/// Invoke an entrypoint and transfer to ourselves. +/// The before and after self-balances are the same. +#[test] +fn test_invoke_1() { + let (mut chain, contract_address, _) = deploy_and_init("self-balance.wasm", "init_transfer"); + + let parameter = ( + 1u32, // instruction + contract_address, + OwnedParameter::empty(), + EntrypointName::new_unchecked("accept"), + Amount::from_micro_ccd(0), + ); + let result = chain.contract_update( + Signer::with_one_key(), + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + address: contract_address, + amount: Amount::from_micro_ccd(123), + receive_name: OwnedReceiveName::new_unchecked("transfer.forward".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + }, + ); + assert_success( + result, + Amount::from_micro_ccd(123), + Amount::from_micro_ccd(123), + "Self selfBalance", + ); +} + +/// Invoke an entrypoint and transfer to another instance. +/// The before and after balances are different. +/// The key difference from `test_invoke_1` is that the contract address in the +/// parameter is different. +#[test] +fn test_invoke_2() { + let (mut chain, self_address, mod_ref) = deploy_and_init("self-balance.wasm", "init_transfer"); + + let res_init_another = chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref, + init_name: OwnedContractName::new_unchecked("init_transfer".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) + .expect("Initializing valid contract should work"); + + let parameter = ( + 1u32, // instruction + res_init_another.contract_address, // Transfer to another contract instance. + OwnedParameter::empty(), + EntrypointName::new_unchecked("accept"), + Amount::from_micro_ccd(100), + ); + let result = chain.contract_update( + Signer::with_one_key(), + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + address: self_address, + receive_name: OwnedReceiveName::new_unchecked("transfer.forward".into()), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + amount: Amount::from_micro_ccd(123), + }, + ); + assert_success( + result, + Amount::from_micro_ccd(123), + Amount::from_micro_ccd(23), + "Self selfBalance", + ); +} + +/// Helper for deploying and initializing the provided contract. +fn deploy_and_init( + file_name: &str, + contract_name: &str, +) -> (Chain, ContractAddress, ModuleReference) { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(10000); + chain.create_account(ACC_0, Account::new(initial_balance)); + + let res_deploy = chain + .module_deploy_v1( + Signer::with_one_key(), + ACC_0, + Chain::module_load_v1_raw(format!("{}/{}", WASM_TEST_FOLDER, file_name)) + .expect("module should exist"), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked(contract_name.into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) + .expect("Initializing valid contract should work"); + ( + chain, + res_init.contract_address, + res_deploy.module_reference, + ) +} + +/// Helper for asserting the success. +fn assert_success( + result: Result, + expected_before: Amount, + expected_after: Amount, + error_message: &str, +) { + if let Ok(success) = result { + assert_eq!( + success.return_value, + to_bytes(&(expected_before, expected_after)) + ) + } else { + panic!("Test failed ( {} )", error_message) + } +} diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 8f48e49b..2c6f3034 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -427,7 +427,7 @@ fn test_chained_contract() { ) .expect("Initializing valid contract should work"); - let number_of_upgrades: u32 = 76; // TODO: Stack will overflow if set larger. + let number_of_upgrades: u32 = 74; // TODO: Stack will overflow if set larger. let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain From c4b3683770732127d59b2a7bea47a4c46db1d3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 27 Mar 2023 20:38:49 +0200 Subject: [PATCH 113/208] Reduce cloning and usage of expect. --- contract-testing/Cargo.toml | 1 + contract-testing/src/impls.rs | 144 +++++++++++++++-------- contract-testing/src/invocation/impls.rs | 22 ++-- contract-testing/src/invocation/types.rs | 24 ++-- contract-testing/src/types.rs | 13 +- 5 files changed, 125 insertions(+), 79 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 984e99f7..d27fdef0 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -2,6 +2,7 @@ name = "concordium-smart-contract-testing" version = "1.0.0" edition = "2021" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 1d4fd93f..78e1e2d9 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -25,12 +25,37 @@ impl Default for Chain { fn default() -> Self { Self::new() } } -impl Chain { +impl ChainParameters { /// Create a new [`Self`] where all the configurable parameters are /// provided. /// /// Returns an error if the exchange rates provided makes one energy cost /// more than `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + pub fn new() -> Self { + Self::new_with_time_and_rates( + Timestamp::from_timestamp_millis(0), + ExchangeRate::new_unchecked(50000, 1), + ExchangeRate::new_unchecked(1, 50000), + ) + .expect("Parameters are in range.") + } + + /// Create a new [`Self`] with a specified `block_time` where + /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. + pub fn new_with_time(block_time: SlotTime) -> Self { + Self { + block_time, + ..Self::new() + } + } + + /// Create a new [`Self`] where + /// - `block_time` defaults to `0`, + /// - `micro_ccd_per_euro` defaults to `50000 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. + /// + /// With these exchange rates, one energy costs one microCCD. pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -43,9 +68,36 @@ impl Chain { block_time, micro_ccd_per_euro, euro_per_energy, - accounts: BTreeMap::new(), - modules: BTreeMap::new(), - contracts: BTreeMap::new(), + }) + } + + /// Helper function for converting [`Energy`] to [`Amount`] using the two + /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. + pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { + energy_to_amount(energy, self.euro_per_energy, self.micro_ccd_per_euro) + } +} + +impl Chain { + /// Create a new [`Self`] where all the configurable parameters are + /// provided. + /// + /// Returns an error if the exchange rates provided makes one energy cost + /// more than `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + pub fn new_with_time_and_rates( + block_time: SlotTime, + micro_ccd_per_euro: ExchangeRate, + euro_per_energy: ExchangeRate, + ) -> Result { + Ok(Self { + parameters: ChainParameters::new_with_time_and_rates( + block_time, + micro_ccd_per_euro, + euro_per_energy, + )?, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + contracts: BTreeMap::new(), next_contract_index: 0, }) } @@ -55,7 +107,7 @@ impl Chain { /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(block_time: SlotTime) -> Self { Self { - block_time, + parameters: ChainParameters::new_with_time(block_time), ..Self::new() } } @@ -75,6 +127,12 @@ impl Chain { .expect("Rates known to be within range.") } + /// Helper function for converting [`Energy`] to [`Amount`] using the two + /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. + pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { + self.parameters.calculate_energy_cost(energy) + } + /// Deploy a smart contract module. /// /// The `WasmModule` can be loaded from disk with either @@ -90,8 +148,10 @@ impl Chain { sender: AccountAddress, wasm_module: WasmModule, ) -> Result { - // Ensure sender account exists. - if !self.account_exists(sender) { + let Ok(sender_account) = self.accounts + .get_mut(&sender.into()) + .ok_or(AccountDoesNotExist { address: sender }) else { + // Ensure sender account exists. return Err(ModuleDeployError { kind: ModuleDeployErrorKind::SenderDoesNotExist(AccountDoesNotExist { address: sender, @@ -99,8 +159,9 @@ impl Chain { energy_used: 0.into(), transaction_fee: Amount::zero(), }); - } + }; + let parameters = &self.parameters; let check_header_energy = { // +1 for the tag, +8 for size and version let payload_size = 1 @@ -109,15 +170,9 @@ impl Chain { + transactions::construct::TRANSACTION_HEADER_SIZE; cost::base_cost(payload_size, signer.num_keys) }; - let check_header_cost = self.calculate_energy_cost(check_header_energy); + let check_header_cost = parameters.calculate_energy_cost(check_header_energy); - if self - .account(sender) - .expect("existence already checked") - .balance - .available() - < check_header_cost - { + if sender_account.balance.available() < check_header_cost { return Err(ModuleDeployError { kind: ModuleDeployErrorKind::InsufficientFunds, energy_used: 0.into(), @@ -136,7 +191,7 @@ impl Chain { }); } - let account = self.account_mut(sender).expect("existence already checked"); + let account = sender_account; // TODO: Ensure that this matches the node for both invalid and valid modules. // to_ccd(header_cost) + to_ccd(deploy_cost) != to_ccd(header_cost + @@ -163,10 +218,9 @@ impl Chain { // Calculate the deploy module cost. let deploy_module_energy = cost::deploy_module(wasm_module.source.size()); - let deploy_module_cost = self.calculate_energy_cost(deploy_module_energy); + let deploy_module_cost = parameters.calculate_energy_cost(deploy_module_energy); // Subtract the cost from the account if it has sufficient funds. - let account = self.account_mut(sender).expect("existence already checked"); if account.balance.available() < deploy_module_cost { return Err(ModuleDeployError { kind: ModuleDeployErrorKind::InsufficientFunds, @@ -337,7 +391,7 @@ impl Chain { // reserved_energy and amount. let account_info = self.account(sender)?; if account_info.balance.available() - < self.calculate_energy_cost(energy_reserved) + payload.amount + < self.parameters.calculate_energy_cost(energy_reserved) + payload.amount { return Err(ContractInitErrorKind::InsufficientFunds); } @@ -370,7 +424,7 @@ impl Chain { // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { - slot_time: self.block_time, + slot_time: self.parameters.block_time, }, init_origin: sender, sender_policies: contracts_common::to_bytes(&account_info.policy), @@ -441,7 +495,7 @@ impl Chain { .total -= payload.amount; let energy_used = energy_reserved - *remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); + let transaction_fee = self.parameters.calculate_energy_cost(energy_used); Ok(ContractInitSuccess { contract_address, @@ -573,7 +627,7 @@ impl Chain { match result.invoke_response { v1::InvokeResponse::Success { new_balance, data } => { let energy_used = energy_reserved - *remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); + let transaction_fee = self.parameters.calculate_energy_cost(energy_used); Ok(ContractInvokeSuccess { trace_elements, energy_used, @@ -651,7 +705,7 @@ impl Chain { kind: ContractInvokeErrorKind::OutOfEnergy, })?; - let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); let account_info = self .account_mut(invoker) .expect("existence already checked"); @@ -661,7 +715,7 @@ impl Chain { let energy_used = energy_reserved - remaining_energy; return Err(ContractInvokeError { energy_used, - transaction_fee: self.calculate_energy_cost(energy_used), + transaction_fee: self.parameters.calculate_energy_cost(energy_used), kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -737,7 +791,7 @@ impl Chain { }); } - let invoker_amount_reserved_for_nrg = self.calculate_energy_cost(energy_reserved); + let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); // Ensure account exists and can pay for the reserved energy and amount let account_info = self .account_mut(invoker) @@ -747,7 +801,7 @@ impl Chain { let energy_used = Energy::from(0); return Err(ContractInvokeError { energy_used, - transaction_fee: self.calculate_energy_cost(energy_used), + transaction_fee: self.parameters.calculate_energy_cost(energy_used), kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -908,7 +962,7 @@ impl Chain { remaining_energy }; let energy_used = energy_reserved - remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); + let transaction_fee = self.parameters.calculate_energy_cost(energy_used); ContractInvokeError { energy_used, transaction_fee, @@ -937,7 +991,7 @@ impl Chain { remaining_energy: Energy, ) -> ContractInitError { let energy_used = energy_reserved - remaining_energy; - let transaction_fee = self.calculate_energy_cost(energy_used); + let transaction_fee = self.parameters.calculate_energy_cost(energy_used); ContractInitError { energy_used, transaction_fee, @@ -945,12 +999,6 @@ impl Chain { } } - /// Helper function for converting [`Energy`] to [`Amount`] using the two - /// [`ExchangeRate`]s `euro_per_energy` and `micro_ccd_per_euro`. - pub fn calculate_energy_cost(&self, energy: Energy) -> Amount { - energy_to_amount(energy, self.euro_per_energy, self.micro_ccd_per_euro) - } - /// Try to set the exchange rates on the chain. /// /// Will fail if they result in the cost of one energy being larger than @@ -962,16 +1010,16 @@ impl Chain { ) -> Result<(), ExchangeRateError> { // Ensure the exchange rates are within a valid range. check_exchange_rates(euro_per_energy, micro_ccd_per_euro)?; - self.micro_ccd_per_euro = micro_ccd_per_euro; - self.euro_per_energy = euro_per_energy; + self.parameters.micro_ccd_per_euro = micro_ccd_per_euro; + self.parameters.euro_per_energy = euro_per_energy; Ok(()) } /// Return the current microCCD per euro exchange rate. - pub fn micro_ccd_per_euro(&self) -> ExchangeRate { self.micro_ccd_per_euro } + pub fn micro_ccd_per_euro(&self) -> ExchangeRate { self.parameters.micro_ccd_per_euro } /// Return the current euro per energy exchange rate. - pub fn euro_per_energy(&self) -> ExchangeRate { self.euro_per_energy } + pub fn euro_per_energy(&self) -> ExchangeRate { self.parameters.euro_per_energy } } impl Account { @@ -1023,6 +1071,7 @@ impl Account { } } +// TODO: This should go to concordium-base. impl From for AccountAddress { fn from(aae: AccountAddressEq) -> Self { aae.0 } } @@ -1040,22 +1089,15 @@ impl PartialEq for AccountAddressEq { } impl PartialOrd for AccountAddressEq { - fn partial_cmp(&self, other: &Self) -> Option { - let bytes_1: &[u8; 32] = self.0.as_ref(); - let bytes_2: &[u8; 32] = other.0.as_ref(); - if bytes_1[0..29] < bytes_2[0..29] { - Some(std::cmp::Ordering::Less) - } else if bytes_1[0..29] > bytes_2[0..29] { - Some(std::cmp::Ordering::Greater) - } else { - Some(std::cmp::Ordering::Equal) - } - } + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for AccountAddressEq { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).expect("Is always some.") + let bytes_1: &[u8; 32] = self.0.as_ref(); + let bytes_2: &[u8; 32] = other.0.as_ref(); + bytes_1[0..29].cmp(&bytes_2[0..29]) } } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 931f104b..a890c357 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -27,7 +27,7 @@ use concordium_smart_contract_engine::{ use concordium_wasm::artifact; use std::collections::{btree_map, BTreeMap}; -impl<'a> EntrypointInvocationHandler<'a> { +impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Invoke an entrypoint and get the result, [`Changeset`], and chain /// events. /// @@ -38,7 +38,7 @@ impl<'a> EntrypointInvocationHandler<'a> { /// - if the contract (`contract_address`) exists, then its `module` must /// also exist. pub(crate) fn invoke_entrypoint_and_get_changes( - chain: &Chain, + chain: &'b Chain, invoker: AccountAddress, sender: Address, remaining_energy: &'a mut Energy, @@ -54,14 +54,12 @@ impl<'a> EntrypointInvocationHandler<'a> { let mut contract_invocation = Self { changeset: ChangeSet::new(), remaining_energy, - accounts: chain.accounts.clone(), /* TODO: These three maps should ideally - * be - * immutable references. */ - modules: chain.modules.clone(), - contracts: chain.contracts.clone(), - block_time: chain.block_time, - euro_per_energy: chain.euro_per_energy, - micro_ccd_per_euro: chain.micro_ccd_per_euro, + accounts: &chain.accounts, + modules: &chain.modules, + contracts: &chain.contracts, + block_time: chain.parameters.block_time, + euro_per_energy: chain.parameters.euro_per_energy, + micro_ccd_per_euro: chain.parameters.micro_ccd_per_euro, }; let mut trace_elements = Vec::new(); @@ -1059,7 +1057,7 @@ impl AccountChanges { } } -impl<'a, 'b> InvocationData<'a, 'b> { +impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { /// Process a receive function until completion. /// /// **Preconditions**: @@ -1125,7 +1123,7 @@ impl<'a, 'b> InvocationData<'a, 'b> { // upgrades, but not for the remaining interrupts. let interrupt_event = ContractTraceElement::Interrupted { address: self.address, - events: contract_events_from_logs(logs.clone()), + events: contract_events_from_logs(logs), }; match interrupt { v1::Interrupt::Transfer { to, amount } => { diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 9b88754c..ed4721d0 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -26,21 +26,21 @@ pub(crate) struct InvokeEntrypointResponse { } /// A type that supports invoking a contract entrypoint. -pub(crate) struct EntrypointInvocationHandler<'a> { +pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// The changeset which keeps track of changes to accounts, modules, and /// contracts that occur during an invocation. pub(super) changeset: ChangeSet, /// The energy remaining for execution. pub(super) remaining_energy: &'a mut Energy, - /// The accounts of the chain. These are currently clones and only used as a - /// reference. Any changes are saved to the changeset. - pub(super) accounts: BTreeMap, - /// The modules of the chain. These are currently clones and only used as a - /// reference. Any changes are saved to the changeset. - pub(super) modules: BTreeMap, - /// The contracts of the chain. These are currently clones and only used as - /// a reference. Any changes are saved to the changeset. - pub(super) contracts: BTreeMap, + /// The accounts of the chain. These are only used to look up the state + /// before a transaction. All changes are stored in the changeset. + pub(super) accounts: &'b BTreeMap, + /// The modules of the chain. These are only used to look up the state + /// before a transaction. All changes are stored in the changeset. + pub(super) modules: &'b BTreeMap, + /// The contracts of the chain. These are only used to look up the state + /// before a transaction. All changes are stored in the changeset. + pub(super) contracts: &'b BTreeMap, /// The current block time. pub(super) block_time: SlotTime, /// The euro per energy exchange rate. @@ -98,7 +98,7 @@ pub(super) struct ContractChanges { /// /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. -pub(super) struct InvocationData<'a, 'b> { +pub(super) struct InvocationData<'a, 'b, 'c> { /// The invoker. pub(super) invoker: AccountAddress, /// The sender. @@ -115,7 +115,7 @@ pub(super) struct InvocationData<'a, 'b> { pub(super) parameter: OwnedParameter, /// A reference to the [`EntrypointInvocationHandler`], which is used to for /// handling interrupts and for querying chain data. - pub(super) invocation_handler: &'a mut EntrypointInvocationHandler<'b>, + pub(super) invocation_handler: &'c mut EntrypointInvocationHandler<'a, 'b>, /// The current state. pub(super) state: MutableState, /// Trace elements that have occurred during the execution. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 864a46b3..bee259c1 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -20,11 +20,8 @@ pub struct ContractModule { pub(crate) artifact: Arc>, } -/// Represents the blockchain and supports a number of operations, including -/// creating accounts, deploying modules, initializing contract, updating -/// contracts and invoking contracts. #[derive(Debug)] -pub struct Chain { +pub(crate) struct ChainParameters { /// The block time viewable inside the smart contracts. /// Defaults to `0`. pub block_time: SlotTime, @@ -36,6 +33,14 @@ pub struct Chain { // This is not public because we ensure a reasonable value during the construction of the // [`Chain`]. pub(crate) euro_per_energy: ExchangeRate, +} + +/// Represents the blockchain and supports a number of operations, including +/// creating accounts, deploying modules, initializing contract, updating +/// contracts and invoking contracts. +#[derive(Debug)] +pub struct Chain { + pub(crate) parameters: ChainParameters, /// Accounts and info about them. /// This uses [`AccountAddressEq`] to ensure that account aliases are seen /// as one account. From ab5fa57f9e081dee14ed39d86c6ab9b116a381fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 27 Mar 2023 20:56:41 +0200 Subject: [PATCH 114/208] Pass in immutable reference to the chain directly. --- contract-testing/src/invocation/impls.rs | 50 ++++++++++++------------ contract-testing/src/invocation/types.rs | 29 +++----------- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index a890c357..5a1df034 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -37,6 +37,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// - `sender` exists /// - if the contract (`contract_address`) exists, then its `module` must /// also exist. + // TODO: This should not be inside an impl block. pub(crate) fn invoke_entrypoint_and_get_changes( chain: &'b Chain, invoker: AccountAddress, @@ -54,12 +55,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let mut contract_invocation = Self { changeset: ChangeSet::new(), remaining_energy, - accounts: &chain.accounts, - modules: &chain.modules, - contracts: &chain.contracts, - block_time: chain.parameters.block_time, - euro_per_energy: chain.parameters.euro_per_energy, - micro_ccd_per_euro: chain.parameters.micro_ccd_per_euro, + chain, }; let mut trace_elements = Vec::new(); @@ -141,6 +137,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Get the instance and artifact. To be used in several places. let instance = self + .chain .contracts .get(&payload.address) .expect("Contract known to exist at this point"); @@ -195,7 +192,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { entrypoint: entrypoint_name.clone(), common: v0::ReceiveContext { metadata: ChainMetadata { - slot_time: self.block_time, + slot_time: self.chain.parameters.block_time, }, invoker, self_address: payload.address, @@ -204,8 +201,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { owner: instance.owner, sender_policies: to_bytes( &self - .accounts - .get(&invoker.into()) + .chain + .account(invoker.into()) .expect("Precondition violation: invoker must exist.") .policy, ), @@ -325,7 +322,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { to: AccountAddress, ) -> Result { // Ensure the `to` account exists. - if !self.accounts.contains_key(&to.into()) { + if !self.chain.accounts.contains_key(&to.into()) { return Err(TransferError::ToMissing); } @@ -350,7 +347,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { to: ContractAddress, ) -> Result { // Ensure the `to` contract exists. - if !self.contracts.contains_key(&to) { + if !self.chain.contracts.contains_key(&to) { return Err(TransferError::ToMissing); } @@ -373,7 +370,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { to: ContractAddress, ) -> Result { // Ensure the `to` account exists. - if !self.contracts.contains_key(&to) { + if !self.chain.contracts.contains_key(&to) { return Err(TransferError::ToMissing); } @@ -408,6 +405,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self + .chain .contracts .get(&address) .expect("Precondition violation: contract assumed to exist") @@ -456,7 +454,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self - .accounts + .chain.accounts .get(&address.into()) .expect("Precondition violation: account assumed to exist") .balance @@ -497,7 +495,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { fn contract_balance(&self, address: ContractAddress) -> Option { match self.changeset.current().contracts.get(&address) { Some(changes) => Some(changes.current_balance()), - None => self.contracts.get(&address).map(|c| c.self_balance), + None => self.chain.contracts.get(&address).map(|c| c.self_balance), } } @@ -518,18 +516,18 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { { // Contract has been upgrade, new module exists. Some(new_module) => self - .modules + .chain.modules .get(&new_module) .expect("Precondition violation: module must exist.") .clone(), // Contract hasn't been upgraded. Use persisted module. None => { let module_ref = self - .contracts + .chain.contracts .get(&address) .expect("Precondition violation: contract must exist.") .module_reference; - self.modules + self.chain.modules .get(&module_ref) .expect("Precondition violation: module must exist.") .clone() @@ -554,7 +552,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { Some(modified_state) => modified_state, // Contract state hasn't been modified. Thaw from persistence. None => self - .contracts + .chain.contracts .get(&address) .expect("Precondition violation: contract must exist") .state @@ -565,7 +563,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Looks up the account balance for an account by first checking /// the changeset, then the persisted values. fn account_balance(&self, address: AccountAddress) -> Option { - let account_balance = self.accounts.get(&address.into()).map(|a| a.balance)?; + let account_balance = self.chain.accounts.get(&address.into()).map(|a| a.balance)?; match self .changeset .current() @@ -616,7 +614,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { let original_balance = self - .contracts + .chain.contracts .get(&address) .expect("Precondition violation: contract must exist.") .self_balance; @@ -653,7 +651,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { let contract = self - .contracts + .chain.contracts .get(&address) .expect("Precondition violation: contract must exist."); let old_module_ref = contract.module_reference; @@ -669,7 +667,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let old_module_ref = match changes.module { Some(old_module) => old_module, None => { - self.contracts + self.chain.contracts .get(&address) .expect("Precondition violation: contract must exist.") .module_reference @@ -1221,7 +1219,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { let (success, invoke_response) = match self .invocation_handler - .contracts + .chain.contracts .get(&address) .map(|c| c.contract_name.as_contract_name()) { @@ -1314,7 +1312,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { .remaining_energy .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; - let response = match self.invocation_handler.modules.get(&module_ref) { + let response = match self.invocation_handler.chain.modules.get(&module_ref) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::UpgradeInvalidModuleRef, }, @@ -1442,8 +1440,8 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { } v1::Interrupt::QueryExchangeRates => { let exchange_rates = ExchangeRates { - euro_per_energy: self.invocation_handler.euro_per_energy, - micro_ccd_per_euro: self.invocation_handler.micro_ccd_per_euro, + euro_per_energy: self.invocation_handler.chain.parameters.euro_per_energy, + micro_ccd_per_euro: self.invocation_handler.chain.parameters.micro_ccd_per_euro, }; let response = v1::InvokeResponse::Success { diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index ed4721d0..70656563 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,12 +1,9 @@ -use crate::{ - types::{Account, Contract, ContractModule}, - AccountAddressEq, -}; +use crate::{AccountAddressEq, Chain}; use concordium_base::{ base::Energy, contracts_common::{ - AccountAddress, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, - OwnedContractName, OwnedEntrypointName, SlotTime, + AccountAddress, Address, Amount, ContractAddress, ModuleReference, OwnedContractName, + OwnedEntrypointName, }, smart_contracts::{ContractTraceElement, OwnedParameter}, }; @@ -29,24 +26,10 @@ pub(crate) struct InvokeEntrypointResponse { pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// The changeset which keeps track of changes to accounts, modules, and /// contracts that occur during an invocation. - pub(super) changeset: ChangeSet, + pub(super) changeset: ChangeSet, /// The energy remaining for execution. - pub(super) remaining_energy: &'a mut Energy, - /// The accounts of the chain. These are only used to look up the state - /// before a transaction. All changes are stored in the changeset. - pub(super) accounts: &'b BTreeMap, - /// The modules of the chain. These are only used to look up the state - /// before a transaction. All changes are stored in the changeset. - pub(super) modules: &'b BTreeMap, - /// The contracts of the chain. These are only used to look up the state - /// before a transaction. All changes are stored in the changeset. - pub(super) contracts: &'b BTreeMap, - /// The current block time. - pub(super) block_time: SlotTime, - /// The euro per energy exchange rate. - pub(super) euro_per_energy: ExchangeRate, - /// The mCCD per euro exchange rate. - pub(super) micro_ccd_per_euro: ExchangeRate, + pub(super) remaining_energy: &'a mut Energy, + pub(super) chain: &'b Chain, } /// The set of [`Changes`] represented as a stack. From 2857e4ec668daab23583195103bddfd0ac6e1bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 27 Mar 2023 21:01:50 +0200 Subject: [PATCH 115/208] Make method a function since it does not use or return Self. --- contract-testing/src/impls.rs | 22 ++--- contract-testing/src/invocation/impls.rs | 119 +++++++++++++---------- contract-testing/src/invocation/mod.rs | 3 +- 3 files changed, 81 insertions(+), 63 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 78e1e2d9..17e67ee6 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,6 +1,6 @@ use crate::{ constants, - invocation::{EntrypointInvocationHandler, TestConfigurationError}, + invocation::{invoke_entrypoint_and_get_changes, TestConfigurationError}, types::*, }; use anyhow::anyhow; @@ -585,16 +585,10 @@ impl Chain { } let contract_address = payload.address; let (result, changeset, trace_elements) = - EntrypointInvocationHandler::invoke_entrypoint_and_get_changes( - self, - invoker, - sender, - remaining_energy, - payload, - ) - .map_err(|err| { - self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) - })?; + invoke_entrypoint_and_get_changes(self, invoker, sender, remaining_energy, payload) + .map_err(|err| { + self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) + })?; // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. @@ -705,7 +699,8 @@ impl Chain { kind: ContractInvokeErrorKind::OutOfEnergy, })?; - let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); + let invoker_amount_reserved_for_nrg = + self.parameters.calculate_energy_cost(energy_reserved); let account_info = self .account_mut(invoker) .expect("existence already checked"); @@ -791,7 +786,8 @@ impl Chain { }); } - let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); + let invoker_amount_reserved_for_nrg = + self.parameters.calculate_energy_cost(energy_reserved); // Ensure account exists and can pay for the reserved energy and amount let account_info = self .account_mut(invoker) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 5a1df034..041e3bc3 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -27,43 +27,42 @@ use concordium_smart_contract_engine::{ use concordium_wasm::artifact; use std::collections::{btree_map, BTreeMap}; -impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { - /// Invoke an entrypoint and get the result, [`Changeset`], and chain - /// events. - /// - /// **Preconditions:** - /// - `invoker` exists - /// - `invoker` has sufficient balance to pay for `remaining_energy` - /// - `sender` exists - /// - if the contract (`contract_address`) exists, then its `module` must - /// also exist. - // TODO: This should not be inside an impl block. - pub(crate) fn invoke_entrypoint_and_get_changes( - chain: &'b Chain, - invoker: AccountAddress, - sender: Address, - remaining_energy: &'a mut Energy, - payload: UpdateContractPayload, - ) -> Result< - ( - InvokeEntrypointResponse, - ChangeSet, - Vec, - ), - TestConfigurationError, - > { - let mut contract_invocation = Self { - changeset: ChangeSet::new(), - remaining_energy, - chain, - }; - - let mut trace_elements = Vec::new(); - let result = - contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements)?; - Ok((result, contract_invocation.changeset, trace_elements)) - } +/// Invoke an entrypoint and get the result, [`Changeset`], and chain +/// events. +/// +/// **Preconditions:** +/// - `invoker` exists +/// - `invoker` has sufficient balance to pay for `remaining_energy` +/// - `sender` exists +/// - if the contract (`contract_address`) exists, then its `module` must also +/// exist. +pub(crate) fn invoke_entrypoint_and_get_changes<'a, 'b>( + chain: &'b Chain, + invoker: AccountAddress, + sender: Address, + remaining_energy: &'a mut Energy, + payload: UpdateContractPayload, +) -> Result< + ( + InvokeEntrypointResponse, + ChangeSet, + Vec, + ), + TestConfigurationError, +> { + let mut contract_invocation = EntrypointInvocationHandler { + changeset: ChangeSet::new(), + remaining_energy, + chain, + }; + + let mut trace_elements = Vec::new(); + let result = + contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements)?; + Ok((result, contract_invocation.changeset, trace_elements)) +} +impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Used for handling contract entrypoint invocations internally. /// /// **Preconditions:** @@ -454,7 +453,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { btree_map::Entry::Vacant(vac) => { // get original balance let original_balance = self - .chain.accounts + .chain + .accounts .get(&address.into()) .expect("Precondition violation: account assumed to exist") .balance @@ -516,18 +516,21 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { { // Contract has been upgrade, new module exists. Some(new_module) => self - .chain.modules + .chain + .modules .get(&new_module) .expect("Precondition violation: module must exist.") .clone(), // Contract hasn't been upgraded. Use persisted module. None => { let module_ref = self - .chain.contracts + .chain + .contracts .get(&address) .expect("Precondition violation: contract must exist.") .module_reference; - self.chain.modules + self.chain + .modules .get(&module_ref) .expect("Precondition violation: module must exist.") .clone() @@ -552,7 +555,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { Some(modified_state) => modified_state, // Contract state hasn't been modified. Thaw from persistence. None => self - .chain.contracts + .chain + .contracts .get(&address) .expect("Precondition violation: contract must exist") .state @@ -563,7 +567,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Looks up the account balance for an account by first checking /// the changeset, then the persisted values. fn account_balance(&self, address: AccountAddress) -> Option { - let account_balance = self.chain.accounts.get(&address.into()).map(|a| a.balance)?; + let account_balance = self + .chain + .accounts + .get(&address.into()) + .map(|a| a.balance)?; match self .changeset .current() @@ -614,7 +622,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { let original_balance = self - .chain.contracts + .chain + .contracts .get(&address) .expect("Precondition violation: contract must exist.") .self_balance; @@ -651,7 +660,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { let contract = self - .chain.contracts + .chain + .contracts .get(&address) .expect("Precondition violation: contract must exist."); let old_module_ref = contract.module_reference; @@ -667,7 +677,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let old_module_ref = match changes.module { Some(old_module) => old_module, None => { - self.chain.contracts + self.chain + .contracts .get(&address) .expect("Precondition violation: contract must exist.") .module_reference @@ -1219,7 +1230,8 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { let (success, invoke_response) = match self .invocation_handler - .chain.contracts + .chain + .contracts .get(&address) .map(|c| c.contract_name.as_contract_name()) { @@ -1312,7 +1324,8 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { .remaining_energy .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; - let response = match self.invocation_handler.chain.modules.get(&module_ref) { + let response = match self.invocation_handler.chain.modules.get(&module_ref) + { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::UpgradeInvalidModuleRef, }, @@ -1440,8 +1453,16 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { } v1::Interrupt::QueryExchangeRates => { let exchange_rates = ExchangeRates { - euro_per_energy: self.invocation_handler.chain.parameters.euro_per_energy, - micro_ccd_per_euro: self.invocation_handler.chain.parameters.micro_ccd_per_euro, + euro_per_energy: self + .invocation_handler + .chain + .parameters + .euro_per_energy, + micro_ccd_per_euro: self + .invocation_handler + .chain + .parameters + .micro_ccd_per_euro, }; let response = v1::InvokeResponse::Success { diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index 951071c0..df50ae11 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -14,4 +14,5 @@ mod impls; mod types; -pub(crate) use types::{EntrypointInvocationHandler, TestConfigurationError}; +pub(crate) use impls::invoke_entrypoint_and_get_changes; +pub(crate) use types::TestConfigurationError; From d14faee4ce8e980c46563f6bb6a483f5099122af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 27 Mar 2023 21:45:17 +0200 Subject: [PATCH 116/208] Account has its own address. --- contract-testing/src/impls.rs | 27 ++++++++++++------ contract-testing/src/types.rs | 1 + .../tests/all_new_host_functions.rs | 2 +- contract-testing/tests/basics.rs | 14 +++++----- contract-testing/tests/checkpointing.rs | 10 +++---- contract-testing/tests/counter.rs | 2 +- contract-testing/tests/error_codes.rs | 2 +- contract-testing/tests/fallback.rs | 2 +- contract-testing/tests/iterator.rs | 2 +- contract-testing/tests/queries.rs | 28 +++++++++---------- contract-testing/tests/recorder.rs | 2 +- .../tests/relaxed_restrictions.rs | 4 +-- contract-testing/tests/self_balance.rs | 2 +- contract-testing/tests/transfer.rs | 2 +- contract-testing/tests/upgrades.rs | 16 +++++------ 15 files changed, 63 insertions(+), 53 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 17e67ee6..90a659c7 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -853,8 +853,8 @@ impl Chain { /// Some(Amount::from_ccd(123)) /// ); /// ``` - pub fn create_account(&mut self, address: AccountAddress, account: Account) -> Option { - self.accounts.insert(address.into(), account) + pub fn create_account(&mut self, account: Account) -> Option { + self.accounts.insert(account.address.into(), account) } /// Create a contract address by giving it the next available index. @@ -1020,16 +1020,24 @@ impl Chain { impl Account { /// Create new [`Self`] with the provided account policy. - pub fn new_with_policy(balance: AccountBalance, policy: OwnedPolicy) -> Self { - Self { balance, policy } + pub fn new_with_policy( + address: AccountAddress, + balance: AccountBalance, + policy: OwnedPolicy, + ) -> Self { + Self { + balance, + policy, + address, + } } /// Create new [`Self`] with the provided balance and a default account /// policy. /// /// See [`new`][Self::new] for what the default policy is. - pub fn new_with_balance(balance: AccountBalance) -> Self { - Self::new_with_policy(balance, Self::empty_policy()) + pub fn new_with_balance(address: AccountAddress, balance: AccountBalance) -> Self { + Self::new_with_policy(address, balance, Self::empty_policy()) } /// Create new [`Self`] with the provided total balance. @@ -1042,8 +1050,9 @@ impl Account { /// /// The [`AccountBalance`] will be created with the provided /// `total_balance`. - pub fn new(total_balance: Amount) -> Self { + pub fn new(address: AccountAddress, total_balance: Amount) -> Self { Self::new_with_policy( + address, AccountBalance { total: total_balance, staked: Amount::zero(), @@ -1308,8 +1317,8 @@ mod tests { let expected_amount = Amount::from_ccd(10); let expected_amount_other = Amount::from_ccd(123); - chain.create_account(acc, Account::new(expected_amount)); - chain.create_account(acc_other, Account::new(expected_amount_other)); + chain.create_account(Account::new(acc, expected_amount)); + chain.create_account(Account::new(acc_other, expected_amount_other)); assert_eq!(acc_eq, acc_alias_eq); assert_ne!(acc_eq, acc_other_eq); diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index bee259c1..b988211f 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -71,6 +71,7 @@ pub struct Contract { /// An account. #[derive(Clone, Debug)] pub struct Account { + pub address: AccountAddress, /// The account balance. pub balance: AccountBalance, /// Account policy. diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 102459b1..2399c39b 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -12,7 +12,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_all_new_host_functions() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 7e6c45d0..0c782950 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -9,7 +9,7 @@ const ACC_1: AccountAddress = AccountAddress([1; 32]); fn deploying_valid_module_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res = chain .module_deploy_v1( @@ -33,7 +33,7 @@ fn deploying_valid_module_works() { fn initializing_valid_contract_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -70,7 +70,7 @@ fn initializing_valid_contract_works() { fn initializing_with_invalid_parameter_fails() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -116,7 +116,7 @@ fn initializing_with_invalid_parameter_fails() { fn updating_valid_contract_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -196,7 +196,7 @@ fn updating_valid_contract_works() { fn updating_and_invoking_with_missing_sender_fails() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let missing_account = Address::Account(ACC_1); let missing_contract = Address::Contract(ContractAddress::new(100, 0)); @@ -304,7 +304,7 @@ fn updating_and_invoking_with_missing_sender_fails() { fn init_with_less_energy_than_module_lookup() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -342,7 +342,7 @@ fn init_with_less_energy_than_module_lookup() { fn update_with_fib_reentry_works() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 76e3a5c1..66122b32 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -24,7 +24,7 @@ const ACC_1: AccountAddress = AccountAddress([1; 32]); fn test_case_1() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), @@ -102,7 +102,7 @@ fn test_case_1() { fn test_case_2() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), @@ -179,8 +179,8 @@ fn test_case_2() { fn test_case_3() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), @@ -241,7 +241,7 @@ fn test_case_3() { fn test_case_4() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 454aacda..dfb38235 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -11,7 +11,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_counter() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 14aec417..d7a87393 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -11,7 +11,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_error_codes() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index dd64889a..0d78fa81 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -9,7 +9,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_fallback() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 49ba741d..6bf8b8e4 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -13,7 +13,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_iterator() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index cf92b217..292060a2 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -20,8 +20,8 @@ mod query_account_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -90,8 +90,8 @@ mod query_account_balance { fn invoker_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -165,8 +165,8 @@ mod query_account_balance { fn transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -248,8 +248,8 @@ mod query_account_balance { fn balance_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); - chain.create_account(ACC_1, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -317,7 +317,7 @@ mod query_account_balance { fn missing_account_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -388,7 +388,7 @@ mod query_contract_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); @@ -462,7 +462,7 @@ mod query_contract_balance { fn query_self_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -522,7 +522,7 @@ mod query_contract_balance { fn query_self_after_transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -590,7 +590,7 @@ mod query_contract_balance { fn missing_contract_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -652,7 +652,7 @@ mod query_exchange_rates { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index b76e6fae..46e4768c 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -9,7 +9,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_recorder() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 59d65972..66db8ece 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -21,7 +21,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_new_parameter_limit() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let parameter = mk_parameter(65535, 65535); @@ -104,7 +104,7 @@ fn test_new_log_limit() { fn deploy_and_init() -> (Chain, ContractAddress) { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1(Signer::with_one_key(), diff --git a/contract-testing/tests/self_balance.rs b/contract-testing/tests/self_balance.rs index 2c7e2906..814ad91f 100644 --- a/contract-testing/tests/self_balance.rs +++ b/contract-testing/tests/self_balance.rs @@ -99,7 +99,7 @@ fn deploy_and_init( ) -> (Chain, ContractAddress, ModuleReference) { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index b5bf72c2..98798cb4 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -9,7 +9,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test_transfer() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 2c6f3034..06e13653 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -11,7 +11,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain @@ -101,7 +101,7 @@ fn test() { fn test_self_invoke() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( @@ -175,7 +175,7 @@ fn test_self_invoke() { fn test_missing_module() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -233,7 +233,7 @@ fn test_missing_module() { fn test_missing_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( @@ -304,7 +304,7 @@ fn test_missing_contract() { fn test_twice_in_one_transaction() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( @@ -402,7 +402,7 @@ fn test_twice_in_one_transaction() { fn test_chained_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( @@ -462,7 +462,7 @@ fn test_chained_contract() { fn test_reject() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( @@ -553,7 +553,7 @@ fn test_reject() { fn test_changing_entrypoint() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(ACC_0, Account::new(initial_balance)); + chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( From 9a3198b111897893040434539d6df1075461cad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Tue, 28 Mar 2023 15:15:38 +0200 Subject: [PATCH 117/208] Documentation. --- contract-testing/src/impls.rs | 94 ++++++++++++------------ contract-testing/src/invocation/impls.rs | 6 +- contract-testing/src/invocation/mod.rs | 2 +- contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 19 +++-- 5 files changed, 64 insertions(+), 59 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 90a659c7..de2cb93f 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,6 +1,6 @@ use crate::{ constants, - invocation::{invoke_entrypoint_and_get_changes, TestConfigurationError}, + invocation::{invoke_entrypoint_and_get_changes, ChangeSet, TestConfigurationError}, types::*, }; use anyhow::anyhow; @@ -26,11 +26,12 @@ impl Default for Chain { } impl ChainParameters { - /// Create a new [`Self`] where all the configurable parameters are - /// provided. + /// Create a new [`Self`] where + /// - `block_time` defaults to `0`, + /// - `micro_ccd_per_euro` defaults to `50000 / 1` + /// - `euro_per_energy` defaults to `1 / 50000`. /// - /// Returns an error if the exchange rates provided makes one energy cost - /// more than `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + /// With these exchange rates, one energy costs one microCCD. pub fn new() -> Self { Self::new_with_time_and_rates( Timestamp::from_timestamp_millis(0), @@ -50,12 +51,11 @@ impl ChainParameters { } } - /// Create a new [`Self`] where - /// - `block_time` defaults to `0`, - /// - `micro_ccd_per_euro` defaults to `50000 / 1` - /// - `euro_per_energy` defaults to `1 / 50000`. + /// Create a new [`Self`] where all the configurable parameters are + /// provided. /// - /// With these exchange rates, one energy costs one microCCD. + /// Returns an error if the exchange rates provided makes one energy cost + /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -83,7 +83,7 @@ impl Chain { /// provided. /// /// Returns an error if the exchange rates provided makes one energy cost - /// more than `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -136,7 +136,7 @@ impl Chain { /// Deploy a smart contract module. /// /// The `WasmModule` can be loaded from disk with either - /// [`Chain::load_module_v1`] or [`Chain::load_module_v1_raw`]. + /// [`Chain::module_load_v1`] or [`Chain::module_load_v1_raw`]. /// /// Parameters: /// - `signer`: the signer with a number of keys, which affects the cost. @@ -555,13 +555,17 @@ impl Chain { /// - `sender` exists. /// - `invoker`s balance is >= `amount`. fn contract_invocation_worker( - &mut self, + &self, invoker: AccountAddress, sender: Address, energy_reserved: Energy, payload: UpdateContractPayload, remaining_energy: &mut Energy, - should_persist: bool, + process_changeset: impl FnOnce( + ChangeSet, + Energy, + ContractAddress, + ) -> Result<(Energy, bool), OutOfEnergy>, ) -> Result { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { @@ -593,17 +597,7 @@ impl Chain { // Get the energy to be charged for extra state bytes. Or return an error if out // of energy. let state_changed = if result.is_success() { - let res = if should_persist { - changeset.persist( - *remaining_energy, - contract_address, - &mut self.accounts, - &mut self.contracts, - ) - } else { - changeset.collect_energy_for_state(*remaining_energy, contract_address) - }; - + let res = process_changeset(changeset, *remaining_energy, contract_address); let (energy_for_state_increase, state_changed) = res.map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; @@ -647,6 +641,7 @@ impl Chain { /// **Parameters:** /// - `invoker`: the account paying for the transaction. /// - `sender`: the sender of the transaction, can also be a contract. + /// TODO: This does not make sense. Senders cannot be contracts. /// - `contract_address`: the contract to update. /// - `entrypoint`: the entrypoint to call. /// - `parameter`: the contract parameter. @@ -656,7 +651,7 @@ impl Chain { &mut self, signer: Signer, invoker: AccountAddress, - sender: Address, + sender: Address, // TODO: Why does this exist? contra energy_reserved: Energy, payload: UpdateContractPayload, ) -> Result { @@ -674,6 +669,7 @@ impl Chain { if !self.address_exists(sender) { // TODO: Should we charge the header cost if the invoker exists but the sender // doesn't? + // No, this situation should not happen. return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), @@ -729,7 +725,14 @@ impl Chain { energy_reserved, payload, &mut remaining_energy, - true, + |changeset: ChangeSet, remaining_energy, contract_address| { + changeset.persist( + remaining_energy, + contract_address, + &mut self.accounts, + &mut self.contracts, + ) + }, ); let transaction_fee = match &res { @@ -761,37 +764,33 @@ impl Chain { /// - `amount`: the amount sent to the contract. /// - `energy_reserved`: the maximum energy that can be used in the update. pub fn contract_invoke( - &mut self, + &self, invoker: AccountAddress, sender: Address, energy_reserved: Energy, payload: UpdateContractPayload, ) -> Result { - // Ensure the invoker exists. - if !self.account_exists(invoker) { + // Ensure the sender exists. + if !self.address_exists(sender) { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvokeErrorKind::InvokerDoesNotExist( - AccountDoesNotExist { address: invoker }, - ), + kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), }); } - // Ensure the sender exists. - if !self.address_exists(sender) { + + let Some(account_info) = self.accounts.get_mut(&invoker.into()) else { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), - kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), + kind: ContractInvokeErrorKind::InvokerDoesNotExist( + AccountDoesNotExist { address: invoker }, + ), }); - } + }; let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); - // Ensure account exists and can pay for the reserved energy and amount - let account_info = self - .account_mut(invoker) - .expect("existence already checked"); if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { let energy_used = Energy::from(0); @@ -814,7 +813,9 @@ impl Chain { energy_reserved, payload, &mut remaining_energy, - false, + |changeset: ChangeSet, remaining_energy, contract_address| { + changeset.collect_energy_for_state(remaining_energy, contract_address) + }, ); // Return the amount charged for the reserved energy, as this is not a @@ -829,7 +830,8 @@ impl Chain { /// Create an account. /// - /// Will override an existing account and return it. + /// If an account with a matching address already exists this method will + /// replace it and return it. /// /// Note that if the first 29-bytes of an account are identical, then /// they are *considered aliases* on each other in all methods. @@ -847,7 +849,7 @@ impl Chain { /// 2, 3, // Only last three bytes differ. /// ]); /// - /// chain.create_account(acc, Account::new(Amount::from_ccd(123))); + /// chain.create_account(Account::new(acc, Amount::from_ccd(123))); /// assert_eq!( /// chain.account_balance_available(acc_alias), // Using the alias for lookup. /// Some(Amount::from_ccd(123)) @@ -890,7 +892,7 @@ impl Chain { } /// Return a clone of the [`ContractModule`] (which has an `Arc` around the - /// artifact). + /// artifact so cloning is cheap). fn contract_module( &self, module_ref: ModuleReference, @@ -1236,7 +1238,7 @@ pub(crate) fn energy_to_amount( /// Helper function that checks the validity of the exchange rates. /// /// More specifically, it checks that the cost of one energy is <= `u64::MAX / -/// MAX_ALLOWED_INVOKE_ENERGY`, which ensures that overflows won't occur. +/// [`MAX_ALLOWED_INVOKE_ENERGY`]`, which ensures that overflows won't occur. fn check_exchange_rates( euro_per_energy: ExchangeRate, micro_ccd_per_euro: ExchangeRate, diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 041e3bc3..b23560ff 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -932,9 +932,9 @@ impl ChangeSet { /// Returns an [`OutOfEnergy`] error if the energy needed for storing the /// extra state is larger than `remaining_energy`. /// - /// Otherwise, it returns - /// the [`Energy`] needed for storing the extra state. It also returns - /// whether the state of the provided `invoked_contract` has changed. + /// Otherwise, it returns the [`Energy`] needed for storing the extra state. + /// It also returns whether the state of the provided `invoked_contract` + /// has changed. pub(crate) fn collect_energy_for_state( mut self, mut remaining_energy: Energy, diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index df50ae11..a7f92284 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -15,4 +15,4 @@ mod impls; mod types; pub(crate) use impls::invoke_entrypoint_and_get_changes; -pub(crate) use types::TestConfigurationError; +pub(crate) use types::{TestConfigurationError, ChangeSet}; diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 13dfff94..ecc14e5f 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -75,7 +75,7 @@ mod constants; mod impls; mod invocation; -pub mod types; +mod types; pub use types::*; // Re-export types. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index b988211f..ba770b9f 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -40,17 +40,17 @@ pub(crate) struct ChainParameters { /// contracts and invoking contracts. #[derive(Debug)] pub struct Chain { - pub(crate) parameters: ChainParameters, + pub(crate) parameters: ChainParameters, /// Accounts and info about them. /// This uses [`AccountAddressEq`] to ensure that account aliases are seen /// as one account. - pub accounts: BTreeMap, + pub(crate) accounts: BTreeMap, /// Smart contract modules. - pub modules: BTreeMap, + pub(crate) modules: BTreeMap, /// Smart contract instances. - pub contracts: BTreeMap, + pub(crate) contracts: BTreeMap, /// Next contract index to use when creating a new instance. - pub next_contract_index: u64, + pub(crate) next_contract_index: u64, } /// A smart contract instance. @@ -83,6 +83,7 @@ pub struct Account { /// /// Account aliases share the first 29 bytes of the address, so the /// [`PartialEq`]/[`PartialOrd`] for this type adheres to that. +// TODO: This should be moved to concordium-base. #[repr(transparent)] #[derive(Eq, Debug, Clone, Copy)] pub struct AccountAddressEq(pub(crate) AccountAddress); @@ -394,10 +395,12 @@ pub struct AccountDoesNotExist { /// The provided exchange rates are not valid. /// Meaning that they do not correspond to one energy costing less than -/// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. -#[derive(Debug)] +/// `u64::MAX / ` [`concordium_base::constants::MAX_ALLOWED_INVOKE_ENERGY`]`. +#[derive(Debug, Error)] +#[error("An exchange rate was too high.")] pub struct ExchangeRateError; /// A [`Signer`] cannot be created with `0` keys. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Any signer must have at least one key.")] pub struct ZeroKeysError; From 1c104e842582e1a714570db4eaf80f6615969262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 13:47:13 +0200 Subject: [PATCH 118/208] Do not ask for mutable reference in invoke. --- contract-testing/src/impls.rs | 219 ++++++++++++++--------- contract-testing/src/invocation/impls.rs | 47 +++-- contract-testing/src/invocation/mod.rs | 2 +- contract-testing/src/invocation/types.rs | 11 +- contract-testing/src/lib.rs | 2 +- contract-testing/tests/basics.rs | 8 +- contract-testing/tests/transfer.rs | 4 +- 7 files changed, 176 insertions(+), 117 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index de2cb93f..a00f9155 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,6 +1,9 @@ use crate::{ constants, - invocation::{invoke_entrypoint_and_get_changes, ChangeSet, TestConfigurationError}, + invocation::{ + invoke_entrypoint_and_get_changes, ChangeSet, InvokeEntrypointResponse, + TestConfigurationError, + }, types::*, }; use anyhow::anyhow; @@ -26,7 +29,7 @@ impl Default for Chain { } impl ChainParameters { - /// Create a new [`Self`] where + /// Create a new [`ChainParameters`](Self) where /// - `block_time` defaults to `0`, /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. @@ -41,7 +44,8 @@ impl ChainParameters { .expect("Parameters are in range.") } - /// Create a new [`Self`] with a specified `block_time` where + /// Create a new [`ChainParameters`](Self) with a specified `block_time` + /// where /// - `micro_ccd_per_euro` defaults to `147235241 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(block_time: SlotTime) -> Self { @@ -51,8 +55,8 @@ impl ChainParameters { } } - /// Create a new [`Self`] where all the configurable parameters are - /// provided. + /// Create a new [`ChainParameters`](Self) where all the configurable + /// parameters are provided. /// /// Returns an error if the exchange rates provided makes one energy cost /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. @@ -79,7 +83,7 @@ impl ChainParameters { } impl Chain { - /// Create a new [`Self`] where all the configurable parameters are + /// Create a new [`Chain`](Self) where all the configurable parameters are /// provided. /// /// Returns an error if the exchange rates provided makes one energy cost @@ -102,7 +106,7 @@ impl Chain { }) } - /// Create a new [`Self`] with a specified `block_time` where + /// Create a new [`Chain`](Self) with a specified `block_time` where /// - `micro_ccd_per_euro` defaults to `147235241 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(block_time: SlotTime) -> Self { @@ -112,7 +116,7 @@ impl Chain { } } - /// Create a new [`Self`] where + /// Create a new [`Chain`](Self) where /// - `block_time` defaults to `0`, /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. @@ -133,6 +137,22 @@ impl Chain { self.parameters.calculate_energy_cost(energy) } + /// Get the state of the contract if it exists in the [`Chain`](Self). + pub fn get_contract(&self, address: ContractAddress) -> Option<&Contract> { + self.contracts.get(&address) + } + + /// Get the the module if it exists in the [`Chain`](Self). + pub fn get_module(&self, module: ModuleReference) -> Option<&ContractModule> { + self.modules.get(&module) + } + + /// Get the state of the account if it exists in the [`Chain`](Self). + /// Account addresses that are aliases will return the same account. + pub fn get_account(&self, address: AccountAddress) -> Option<&Account> { + self.accounts.get(&address.into()) + } + /// Deploy a smart contract module. /// /// The `WasmModule` can be loaded from disk with either @@ -559,14 +579,17 @@ impl Chain { invoker: AccountAddress, sender: Address, energy_reserved: Energy, + amount_reserved_for_energy: Amount, payload: UpdateContractPayload, remaining_energy: &mut Energy, - process_changeset: impl FnOnce( + ) -> Result< + ( + InvokeEntrypointResponse, ChangeSet, - Energy, - ContractAddress, - ) -> Result<(Energy, bool), OutOfEnergy>, - ) -> Result { + Vec, + ), + ContractInvokeError, + > { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { return Err(self.from_invocation_error_kind( @@ -587,34 +610,31 @@ impl Chain { *remaining_energy, )); } - let contract_address = payload.address; - let (result, changeset, trace_elements) = - invoke_entrypoint_and_get_changes(self, invoker, sender, remaining_energy, payload) - .map_err(|err| { - self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) - })?; - - // Get the energy to be charged for extra state bytes. Or return an error if out - // of energy. - let state_changed = if result.is_success() { - let res = process_changeset(changeset, *remaining_energy, contract_address); - let (energy_for_state_increase, state_changed) = - res.map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; - - // Charge for the potential state size increase. - remaining_energy - .tick_energy(energy_for_state_increase) - .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))?; - - state_changed - } else { - // An error occured, so state hasn't changed. - false - }; + let (result, changeset, trace_elements) = invoke_entrypoint_and_get_changes( + self, + invoker, + amount_reserved_for_energy, + sender, + remaining_energy, + payload, + ) + .map_err(|err| { + self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) + })?; + Ok((result, changeset, trace_elements)) + } + fn contract_invocation_process_response( + &self, + result: InvokeEntrypointResponse, + trace_elements: Vec, + energy_reserved: Energy, + remaining_energy: Energy, + state_changed: bool, + ) -> Result { match result.invoke_response { v1::InvokeResponse::Success { new_balance, data } => { - let energy_used = energy_reserved - *remaining_energy; + let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.parameters.calculate_energy_cost(energy_used); Ok(ContractInvokeSuccess { trace_elements, @@ -629,7 +649,7 @@ impl Chain { v1::InvokeResponse::Failure { kind } => Err(self.from_invocation_error_kind( ContractInvokeErrorKind::ExecutionError { failure_kind: kind }, energy_reserved, - *remaining_energy, + remaining_energy, )), } } @@ -697,9 +717,7 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); - let account_info = self - .account_mut(invoker) - .expect("existence already checked"); + let account_info = self.account(invoker).expect("existence already checked"); // Ensure the account has sufficient funds to pay for the energy and amount. if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { @@ -714,46 +732,60 @@ impl Chain { // Charge account for the reserved energy up front. This is to ensure that // contract queries for the invoker balance are correct. // The `amount` is handled in contract_invocation_worker. - // - // *It is vital that we do not return early before returning the amount for - // remaining energy.* - account_info.balance.total -= invoker_amount_reserved_for_nrg; - + let contract_address = payload.address; let res = self.contract_invocation_worker( invoker, sender, energy_reserved, + invoker_amount_reserved_for_nrg, payload, &mut remaining_energy, - |changeset: ChangeSet, remaining_energy, contract_address| { - changeset.persist( + ); + let res = match res { + Ok((result, changeset, trace_elements)) => { + // Charge energy for contract storage. Or return an error if out + // of energy. + let state_changed = if result.is_success() { + let res = changeset.persist( + &mut remaining_energy, + contract_address, + &mut self.accounts, + &mut self.contracts, + ); + res.map_err(|_| self.invocation_out_of_energy_error(energy_reserved))? + } else { + // An error occured, so state hasn't changed. + false + }; + self.contract_invocation_process_response( + result, + trace_elements, + energy_reserved, remaining_energy, - contract_address, - &mut self.accounts, - &mut self.contracts, + state_changed, ) - }, - ); + } + Err(e) => Err(e), + }; let transaction_fee = match &res { Ok(s) => s.transaction_fee, Err(e) => e.transaction_fee, }; - // The `invoker` was charged for all the `reserved_energy` up front. - // Here, we return the amount for any remaining energy. - let return_amount = invoker_amount_reserved_for_nrg - transaction_fee; + // Change for execution and deposit. self.account_mut(invoker) .expect("existence already checked") .balance - .total += return_amount; - + .total -= transaction_fee; + // TODO: Need to charge for deposit somewhere as well. res } /// Invoke a contract by calling an entrypoint. /// - /// Similar to [`Self::contract_update`] except that all changes are - /// discarded afterwards. Typically used for "view" functions. + /// Similar to [`Chain::contract_update`](Self::contract_update) except that + /// all changes are discarded afterwards. Typically used for "view" + /// functions. /// /// **Parameters:** /// - `invoker`: the account paying for the transaction. @@ -779,7 +811,7 @@ impl Chain { }); } - let Some(account_info) = self.accounts.get_mut(&invoker.into()) else { + let Some(account_info) = self.accounts.get(&invoker.into()) else { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), @@ -801,31 +833,42 @@ impl Chain { }); } - // Charge account for the reserved energy up front. This is to ensure that - // contract queries for the invoker balance are correct. - account_info.balance.total -= invoker_amount_reserved_for_nrg; - let mut remaining_energy = energy_reserved; - let result = self.contract_invocation_worker( + let contract_address = payload.address; + + let res = self.contract_invocation_worker( invoker, sender, energy_reserved, + invoker_amount_reserved_for_nrg, payload, &mut remaining_energy, - |changeset: ChangeSet, remaining_energy, contract_address| { - changeset.collect_energy_for_state(remaining_energy, contract_address) - }, ); + let res = match res { + Ok((result, changeset, trace_elements)) => { + // Charge energy for contract storage. Or return an error if out + // of energy. + let state_changed = if result.is_success() { + changeset + .collect_energy_for_state(&mut remaining_energy, contract_address) + .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))? + } else { + // An error occured, so state hasn't changed. + false + }; + self.contract_invocation_process_response( + result, + trace_elements, + energy_reserved, + remaining_energy, + state_changed, + ) + } + Err(e) => Err(e), + }; - // Return the amount charged for the reserved energy, as this is not a - // transaction. - self.account_mut(invoker) - .expect("Known to exist") - .balance - .total += invoker_amount_reserved_for_nrg; - - result + res } /// Create an account. @@ -941,8 +984,8 @@ impl Chain { } } - /// Convert a [`ContractInvocationErrorKind`] to a - /// [`ContractInvocationError`] by calculating the `energy_used` and + /// Convert a [`ContractInvokeErrorKind`] to a + /// [`ContractInvokeError`] by calculating the `energy_used` and /// `transaction_fee`. /// /// If the `kind` is an out of energy, then `0` is used instead of the @@ -968,7 +1011,7 @@ impl Chain { } } - /// Construct a [`ContractInvocationError`] of the `OutOfEnergy` kind with + /// Construct a [`ContractInvokeErrorKind`] of the `OutOfEnergy` kind with /// the energy and transaction fee fields based on the `energy_reserved` /// parameter. fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvokeError { @@ -1021,7 +1064,7 @@ impl Chain { } impl Account { - /// Create new [`Self`] with the provided account policy. + /// Create new [`Account`](Self) with the provided account policy. pub fn new_with_policy( address: AccountAddress, balance: AccountBalance, @@ -1034,15 +1077,15 @@ impl Account { } } - /// Create new [`Self`] with the provided balance and a default account - /// policy. + /// Create a new [`Account`](Self) with the provided balance and a default + /// account policy. /// /// See [`new`][Self::new] for what the default policy is. pub fn new_with_balance(address: AccountAddress, balance: AccountBalance) -> Self { Self::new_with_policy(address, balance, Self::empty_policy()) } - /// Create new [`Self`] with the provided total balance. + /// Create new [`Account`](Self) with the provided total balance. /// /// The `policy` will have: /// - `identity_provider`: 0, @@ -1204,20 +1247,20 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// rates provided. /// /// To find the mCCD/NRG exchange rate: -/// ```ignore +/// ```markdown /// euro mCCD euro * mCCD mCCD /// ---- * ---- = ----------- = ---- /// NRG euro NRG * euro NRG /// ``` /// /// To convert the `energy` parameter to mCCD: -/// ```ignore +/// ```markdown /// /// mCCD NRG * mCCD /// NRG * ---- = ---------- = mCCD /// NRG NRG /// ``` -pub(crate) fn energy_to_amount( +pub fn energy_to_amount( energy: Energy, euro_per_energy: ExchangeRate, micro_ccd_per_euro: ExchangeRate, diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b23560ff..2a987744 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -27,7 +27,7 @@ use concordium_smart_contract_engine::{ use concordium_wasm::artifact; use std::collections::{btree_map, BTreeMap}; -/// Invoke an entrypoint and get the result, [`Changeset`], and chain +/// Invoke an entrypoint and get the result, [`ChangeSet`], and chain /// events. /// /// **Preconditions:** @@ -39,6 +39,7 @@ use std::collections::{btree_map, BTreeMap}; pub(crate) fn invoke_entrypoint_and_get_changes<'a, 'b>( chain: &'b Chain, invoker: AccountAddress, + reserved_amount: Amount, sender: Address, remaining_energy: &'a mut Energy, payload: UpdateContractPayload, @@ -54,6 +55,8 @@ pub(crate) fn invoke_entrypoint_and_get_changes<'a, 'b>( changeset: ChangeSet::new(), remaining_energy, chain, + reserved_amount, + invoker, }; let mut trace_elements = Vec::new(); @@ -452,13 +455,18 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match self.changeset.current_mut().accounts.entry(address.into()) { btree_map::Entry::Vacant(vac) => { // get original balance - let original_balance = self + let mut original_balance = self .chain .accounts .get(&address.into()) .expect("Precondition violation: account assumed to exist") .balance .available(); + if self.invoker == address { + // It has been checked that the invoker account has sufficient balance for + // paying. + original_balance -= self.reserved_amount; + } // Try to apply the balance or return an error if insufficient funds. let new_account_balance = delta.apply_to_balance(original_balance)?; // Insert the changes into the changeset. @@ -567,7 +575,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Looks up the account balance for an account by first checking /// the changeset, then the persisted values. fn account_balance(&self, address: AccountAddress) -> Option { - let account_balance = self + let mut account_balance = self .chain .accounts .get(&address.into()) @@ -588,15 +596,20 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { locked: account_balance.locked, }), // Account doesn't exist in changeset. - None => Some(account_balance), + None => { + if self.invoker == address { + account_balance.total -= self.reserved_amount; + } + Some(account_balance) + } } } /// Saves a mutable state for a contract in the changeset. /// /// If `with_fresh_generation`, then it will use the - /// [`MutableState::make_fresh_generation`] function, otherwise it will - /// make a clone. + /// [`MutableState::make_fresh_generation`][make_fresh_generation] + /// function, otherwise it will make a clone. /// /// If the contract already has an entry in the changeset, the old state /// will be replaced. Otherwise, the entry is created and the state is @@ -607,6 +620,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// /// **Preconditions:** /// - Contract must exist. + /// + /// [make_fresh_generation]: trie::MutableState::make_fresh_generation fn save_state_changes( &mut self, address: ContractAddress, @@ -853,11 +868,11 @@ impl ChangeSet { /// to balance). pub(crate) fn persist( mut self, - mut remaining_energy: Energy, + remaining_energy: &mut Energy, invoked_contract: ContractAddress, persisted_accounts: &mut BTreeMap, persisted_contracts: &mut BTreeMap, - ) -> Result<(Energy, bool), OutOfEnergy> { + ) -> Result { let current = self.current_mut(); let mut invoked_contract_has_state_changes = false; // Persist contract changes and collect the total increase in states sizes. @@ -915,15 +930,12 @@ impl ChangeSet { if !changes.balance_delta.is_zero() { account.balance.total = changes .balance_delta - .apply_to_balance(changes.original_balance) + .apply_to_balance(account.balance.total) .expect("Precondition violation: amount delta causes underflow"); } } - Ok(( - energy_for_state_increase, - invoked_contract_has_state_changes, - )) + Ok(invoked_contract_has_state_changes) } /// Traverses the last checkpoint in the changeset and collects the energy @@ -937,9 +949,9 @@ impl ChangeSet { /// has changed. pub(crate) fn collect_energy_for_state( mut self, - mut remaining_energy: Energy, + remaining_energy: &mut Energy, invoked_contract: ContractAddress, - ) -> Result<(Energy, bool), OutOfEnergy> { + ) -> Result { let mut invoked_contract_has_state_changes = false; let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut collector = v1::trie::SizeCollector::default(); @@ -958,10 +970,7 @@ impl ChangeSet { // Return an error if we run out of energy. remaining_energy.tick_energy(energy_for_state_increase)?; - Ok(( - energy_for_state_increase, - invoked_contract_has_state_changes, - )) + Ok(invoked_contract_has_state_changes) } } diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index a7f92284..169562d3 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -15,4 +15,4 @@ mod impls; mod types; pub(crate) use impls::invoke_entrypoint_and_get_changes; -pub(crate) use types::{TestConfigurationError, ChangeSet}; +pub(crate) use types::{TestConfigurationError, ChangeSet, InvokeEntrypointResponse}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 70656563..9e516371 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -24,8 +24,15 @@ pub(crate) struct InvokeEntrypointResponse { /// A type that supports invoking a contract entrypoint. pub(crate) struct EntrypointInvocationHandler<'a, 'b> { - /// The changeset which keeps track of changes to accounts, modules, and - /// contracts that occur during an invocation. + /// Amount reserved for execution. This is used to return the correct + /// balance of the invoker account. + pub(super) reserved_amount: Amount, + /// Address of the invoker of the transaction. This is used to return the + /// correct balance of the invoker account. + pub(super) invoker: AccountAddress, + /// The changeset which keeps track of + /// changes to accounts, modules, and contracts that occur during an + /// invocation. pub(super) changeset: ChangeSet, /// The energy remaining for execution. pub(super) remaining_energy: &'a mut Energy, diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index ecc14e5f..b4f474ea 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -15,7 +15,7 @@ //! const ACC: AccountAddress = AccountAddress([0;32]); //! //! // Create an account with 10000 CCD in balance. -//! chain.create_account(ACC, Account::new(Amount::from_ccd(1000))); +//! chain.create_account(Account::new(ACC, Amount::from_ccd(1000))); //! //! // Deploy a smart contract module (built with [Cargo Concordium](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#cargo-concordium)). //! let deployment = chain diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 0c782950..47ce2efb 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -22,7 +22,7 @@ fn deploying_valid_module_works() { ) .expect("Deploying valid module should work."); - assert_eq!(chain.modules.len(), 1); + assert!(chain.get_module(res.module_reference).is_some()); assert_eq!( chain.account_balance_available(ACC_0), Some(initial_balance - res.transaction_fee) @@ -63,7 +63,7 @@ fn initializing_valid_contract_works() { chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); - assert_eq!(chain.contracts.len(), 1); + assert!(chain.get_contract(ContractAddress::new(0,0)).is_some()); } #[test] @@ -185,7 +185,7 @@ fn updating_valid_contract_works() { - res_update.transaction_fee ) ); - assert_eq!(chain.contracts.len(), 1); + assert!(chain.get_contract(res_init.contract_address).is_some()); assert!(res_update.state_changed); // Assert that the updated state is persisted. assert_eq!(res_invoke_get.return_value, [1u8]); @@ -406,7 +406,7 @@ fn update_with_fib_reentry_works() { - res_update.transaction_fee ) ); - assert_eq!(chain.contracts.len(), 1); + assert!(chain.get_contract(res_init.contract_address).is_some()); assert!(res_update.state_changed); let expected_res = u64::to_le_bytes(13); assert_eq!(res_update.return_value, expected_res); diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 98798cb4..1168bd52 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -54,7 +54,7 @@ fn test_transfer() { // Contract should have forwarded the amount and thus have balance == 0. assert_eq!( Amount::zero(), - chain.contracts.get(&contract_address).unwrap().self_balance + chain.get_contract(contract_address).unwrap().self_balance ); // Deposit 1000 micro CCD. @@ -94,7 +94,7 @@ fn test_transfer() { // Contract should have 1000 - 17 microCCD in balance. assert_eq!( Amount::from_micro_ccd(1000 - 17), - chain.contracts.get(&contract_address).unwrap().self_balance + chain.get_contract(contract_address).unwrap().self_balance ); assert_eq!(res_update.trace_elements[..], [ ContractTraceElement::Interrupted { From 8ad2e4a85a8f1a9ba7e4d76619b4a0c17cf6fb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 15:38:11 +0200 Subject: [PATCH 119/208] Avoid duplicate lookups. --- contract-testing/src/impls.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index a00f9155..cee974af 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -675,16 +675,6 @@ impl Chain { energy_reserved: Energy, payload: UpdateContractPayload, ) -> Result { - // Ensure the invoker exists. - if !self.account_exists(invoker) { - return Err(ContractInvokeError { - energy_used: Energy::from(0), - transaction_fee: Amount::zero(), - kind: ContractInvokeErrorKind::InvokerDoesNotExist( - AccountDoesNotExist { address: invoker }, - ), - }); - } // Ensure the sender exists. if !self.address_exists(sender) { // TODO: Should we charge the header cost if the invoker exists but the sender @@ -717,7 +707,17 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); - let account_info = self.account(invoker).expect("existence already checked"); + + // Ensure the invoker exists. + let Ok(account_info) = self.account(invoker) else { + return Err(ContractInvokeError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvokeErrorKind::InvokerDoesNotExist( + AccountDoesNotExist { address: invoker }, + ), + }); + }; // Ensure the account has sufficient funds to pay for the energy and amount. if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { @@ -772,12 +772,11 @@ impl Chain { Ok(s) => s.transaction_fee, Err(e) => e.transaction_fee, }; - // Change for execution and deposit. + // Change for execution. self.account_mut(invoker) .expect("existence already checked") .balance .total -= transaction_fee; - // TODO: Need to charge for deposit somewhere as well. res } From ff382f41d3fedd9fd7ce8acca6a7e20db42c046a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 15:45:28 +0200 Subject: [PATCH 120/208] Format. --- .../tests/all_new_host_functions.rs | 6 +- contract-testing/tests/checkpointing.rs | 160 ++++++++++++------ contract-testing/tests/counter.rs | 29 ++-- contract-testing/tests/error_codes.rs | 38 +++-- contract-testing/tests/iterator.rs | 26 ++- contract-testing/tests/recorder.rs | 26 ++- .../tests/relaxed_restrictions.rs | 49 ++++-- 7 files changed, 218 insertions(+), 116 deletions(-) diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 102459b1..46ca558a 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -15,9 +15,11 @@ fn test_all_new_host_functions() { chain.create_account(ACC_0, Account::new(initial_balance)); chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), + Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); } diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 76e3a5c1..eac45941 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -27,7 +27,8 @@ fn test_case_1() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -35,21 +36,31 @@ fn test_case_1() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -68,7 +79,8 @@ fn test_case_1() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -105,7 +117,8 @@ fn test_case_2() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -113,21 +126,31 @@ fn test_case_2() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -146,7 +169,8 @@ fn test_case_2() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -183,7 +207,8 @@ fn test_case_3() { chain.create_account(ACC_1, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -191,25 +216,36 @@ fn test_case_3() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -244,7 +280,8 @@ fn test_case_4() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -252,21 +289,31 @@ fn test_case_4() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -285,7 +332,8 @@ fn test_case_4() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 454aacda..2bf6074d 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -14,7 +14,8 @@ fn test_counter() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,16 +23,22 @@ fn test_counter() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_counter".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_counter".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +53,8 @@ fn test_counter() { assert_counter_state(&mut chain, res_init.contract_address, 1); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -67,7 +75,8 @@ fn test_counter() { Amount::zero(), ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 14aec417..fd84548e 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -14,7 +14,8 @@ fn test_error_codes() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,12 +23,17 @@ fn test_error_codes() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_caller".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_caller".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Invoke an entrypoint that calls the "fail" entrypoint. @@ -46,7 +52,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_0 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -79,7 +86,8 @@ fn test_error_codes() { Amount::from_micro_ccd(10_000), ); let res_update_1 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -110,7 +118,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_2 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -143,7 +152,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_3 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -176,7 +186,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_4 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -212,7 +223,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_6 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 49ba741d..23386e9d 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -16,7 +16,8 @@ fn test_iterator() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -24,16 +25,22 @@ fn test_iterator() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_iterator".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_iterator".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +53,8 @@ fn test_iterator() { ) .expect("Should succeed"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index b76e6fae..b32b385c 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -12,7 +12,8 @@ fn test_recorder() { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -20,16 +21,22 @@ fn test_recorder() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_recorder".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_recorder".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -43,7 +50,8 @@ fn test_recorder() { ) .expect("Update failed"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 59d65972..e37621be 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -26,7 +26,8 @@ fn test_new_parameter_limit() { let parameter = mk_parameter(65535, 65535); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -34,16 +35,22 @@ fn test_new_parameter_limit() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(80000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_relax".into()), - param: parameter.clone(), // Check parameter size limit on init. - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(80000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: parameter.clone(), // Check parameter size limit on init. + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(700000), @@ -63,7 +70,8 @@ fn test_new_return_value_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -84,7 +92,8 @@ fn test_new_log_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -107,7 +116,8 @@ fn deploy_and_init() -> (Chain, ContractAddress) { chain.create_account(ACC_0, Account::new(initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -115,12 +125,17 @@ fn deploy_and_init() -> (Chain, ContractAddress) { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_relax".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); (chain, res_init.contract_address) } From feffacb86aa224adad414910b2b9e22039520476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 15:45:45 +0200 Subject: [PATCH 121/208] Format. --- contract-testing/src/invocation/mod.rs | 2 +- contract-testing/src/invocation/types.rs | 4 +- .../tests/all_new_host_functions.rs | 6 +- contract-testing/tests/basics.rs | 2 +- contract-testing/tests/checkpointing.rs | 160 ++++++++++++------ contract-testing/tests/counter.rs | 29 ++-- contract-testing/tests/error_codes.rs | 38 +++-- contract-testing/tests/iterator.rs | 26 ++- contract-testing/tests/recorder.rs | 26 ++- .../tests/relaxed_restrictions.rs | 49 ++++-- 10 files changed, 222 insertions(+), 120 deletions(-) diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index 169562d3..edab10fd 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -15,4 +15,4 @@ mod impls; mod types; pub(crate) use impls::invoke_entrypoint_and_get_changes; -pub(crate) use types::{TestConfigurationError, ChangeSet, InvokeEntrypointResponse}; +pub(crate) use types::{ChangeSet, InvokeEntrypointResponse, TestConfigurationError}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 9e516371..0bbcd24b 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -26,10 +26,10 @@ pub(crate) struct InvokeEntrypointResponse { pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// Amount reserved for execution. This is used to return the correct /// balance of the invoker account. - pub(super) reserved_amount: Amount, + pub(super) reserved_amount: Amount, /// Address of the invoker of the transaction. This is used to return the /// correct balance of the invoker account. - pub(super) invoker: AccountAddress, + pub(super) invoker: AccountAddress, /// The changeset which keeps track of /// changes to accounts, modules, and contracts that occur during an /// invocation. diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 2399c39b..2065fed3 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -15,9 +15,11 @@ fn test_all_new_host_functions() { chain.create_account(Account::new(ACC_0, initial_balance)); chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)).expect("module should exist"), + Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); } diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 47ce2efb..bdd51f27 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -63,7 +63,7 @@ fn initializing_valid_contract_works() { chain.account_balance_available(ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); - assert!(chain.get_contract(ContractAddress::new(0,0)).is_some()); + assert!(chain.get_contract(ContractAddress::new(0, 0)).is_some()); } #[test] diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 66122b32..208be2ca 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -27,7 +27,8 @@ fn test_case_1() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -35,21 +36,31 @@ fn test_case_1() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -68,7 +79,8 @@ fn test_case_1() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -105,7 +117,8 @@ fn test_case_2() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -113,21 +126,31 @@ fn test_case_2() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -146,7 +169,8 @@ fn test_case_2() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -183,7 +207,8 @@ fn test_case_3() { chain.create_account(Account::new(ACC_1, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -191,25 +216,36 @@ fn test_case_3() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -244,7 +280,8 @@ fn test_case_4() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -252,21 +289,31 @@ fn test_case_4() { .expect("Deploying valid module should work"); let res_init_a = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_a".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_a".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let res_init_b = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_b".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_b".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); let forward_parameter = ( @@ -285,7 +332,8 @@ fn test_case_4() { ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index dfb38235..e276d2bf 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -14,7 +14,8 @@ fn test_counter() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,16 +23,22 @@ fn test_counter() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_counter".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_counter".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +53,8 @@ fn test_counter() { assert_counter_state(&mut chain, res_init.contract_address, 1); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -67,7 +75,8 @@ fn test_counter() { Amount::zero(), ); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index d7a87393..8d7416d8 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -14,7 +14,8 @@ fn test_error_codes() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -22,12 +23,17 @@ fn test_error_codes() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_caller".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_caller".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); // Invoke an entrypoint that calls the "fail" entrypoint. @@ -46,7 +52,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_0 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -79,7 +86,8 @@ fn test_error_codes() { Amount::from_micro_ccd(10_000), ); let res_update_1 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -110,7 +118,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_2 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -143,7 +152,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_3 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -176,7 +186,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_4 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -212,7 +223,8 @@ fn test_error_codes() { Amount::zero(), ); let res_update_6 = chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 6bf8b8e4..de07cef7 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -16,7 +16,8 @@ fn test_iterator() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -24,16 +25,22 @@ fn test_iterator() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_iterator".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_iterator".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -46,7 +53,8 @@ fn test_iterator() { ) .expect("Should succeed"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index 46e4768c..fa416b7f 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -12,7 +12,8 @@ fn test_recorder() { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -20,16 +21,22 @@ fn test_recorder() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_recorder".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_recorder".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), @@ -43,7 +50,8 @@ fn test_recorder() { ) .expect("Update failed"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(100000), diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 66db8ece..876486db 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -26,7 +26,8 @@ fn test_new_parameter_limit() { let parameter = mk_parameter(65535, 65535); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -34,16 +35,22 @@ fn test_new_parameter_limit() { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(80000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_relax".into()), - param: parameter.clone(), // Check parameter size limit on init. - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(80000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: parameter.clone(), // Check parameter size limit on init. + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(700000), @@ -63,7 +70,8 @@ fn test_new_return_value_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -84,7 +92,8 @@ fn test_new_log_limit() { let (mut chain, contract_address) = deploy_and_init(); chain - .contract_update(Signer::with_one_key(), + .contract_update( + Signer::with_one_key(), ACC_0, Address::Account(ACC_0), Energy::from(10000), @@ -107,7 +116,8 @@ fn deploy_and_init() -> (Chain, ContractAddress) { chain.create_account(Account::new(ACC_0, initial_balance)); let res_deploy = chain - .module_deploy_v1(Signer::with_one_key(), + .module_deploy_v1( + Signer::with_one_key(), ACC_0, Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), @@ -115,12 +125,17 @@ fn deploy_and_init() -> (Chain, ContractAddress) { .expect("Deploying valid module should work"); let res_init = chain - .contract_init(Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_relax".into()), - param: OwnedParameter::empty(), - amount: Amount::zero(), - }) + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + mod_ref: res_deploy.module_reference, + init_name: OwnedContractName::new_unchecked("init_relax".into()), + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) .expect("Initializing valid contract should work"); (chain, res_init.contract_address) } From 717caa3758f7ca651d60380ba5a0f0aa200a2e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 21:01:18 +0200 Subject: [PATCH 122/208] Inline function that is only used once. --- contract-testing/src/impls.rs | 30 +++++++++-------- contract-testing/src/invocation/impls.rs | 42 ++---------------------- contract-testing/src/invocation/mod.rs | 5 +-- contract-testing/src/invocation/types.rs | 10 +++--- 4 files changed, 26 insertions(+), 61 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index cee974af..95c5a0e5 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,8 +1,7 @@ use crate::{ constants, invocation::{ - invoke_entrypoint_and_get_changes, ChangeSet, InvokeEntrypointResponse, - TestConfigurationError, + ChangeSet, EntrypointInvocationHandler, InvokeEntrypointResponse, TestConfigurationError, }, types::*, }; @@ -464,7 +463,7 @@ impl Chain { parameter: payload.param.as_ref(), energy: energy_given_to_interpreter, }, - false, + false, // We only support protocol P5 and up, so no limiting. loader, ); // Handle the result @@ -610,18 +609,21 @@ impl Chain { *remaining_energy, )); } - let (result, changeset, trace_elements) = invoke_entrypoint_and_get_changes( - self, - invoker, - amount_reserved_for_energy, - sender, + let mut contract_invocation = EntrypointInvocationHandler { + changeset: ChangeSet::new(), remaining_energy, - payload, - ) - .map_err(|err| { - self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy) - })?; - Ok((result, changeset, trace_elements)) + chain: self, + reserved_amount: amount_reserved_for_energy, + invoker, + }; + + let mut trace_elements = Vec::new(); + match contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements) { + Ok(result) => Ok((result, contract_invocation.changeset, trace_elements)), + Err(err) => { + Err(self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy)) + } + } } fn contract_invocation_process_response( diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 2a987744..85f1a433 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -27,44 +27,6 @@ use concordium_smart_contract_engine::{ use concordium_wasm::artifact; use std::collections::{btree_map, BTreeMap}; -/// Invoke an entrypoint and get the result, [`ChangeSet`], and chain -/// events. -/// -/// **Preconditions:** -/// - `invoker` exists -/// - `invoker` has sufficient balance to pay for `remaining_energy` -/// - `sender` exists -/// - if the contract (`contract_address`) exists, then its `module` must also -/// exist. -pub(crate) fn invoke_entrypoint_and_get_changes<'a, 'b>( - chain: &'b Chain, - invoker: AccountAddress, - reserved_amount: Amount, - sender: Address, - remaining_energy: &'a mut Energy, - payload: UpdateContractPayload, -) -> Result< - ( - InvokeEntrypointResponse, - ChangeSet, - Vec, - ), - TestConfigurationError, -> { - let mut contract_invocation = EntrypointInvocationHandler { - changeset: ChangeSet::new(), - remaining_energy, - chain, - reserved_amount, - invoker, - }; - - let mut trace_elements = Vec::new(); - let result = - contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements)?; - Ok((result, contract_invocation.changeset, trace_elements)) -} - impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Used for handling contract entrypoint invocations internally. /// @@ -74,7 +36,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// - `sender` exists /// - if the contract (`contract_address`) exists, then its `module` must /// also exist. - fn invoke_entrypoint( + pub(crate) fn invoke_entrypoint( &mut self, invoker: AccountAddress, sender: Address, @@ -815,7 +777,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { impl ChangeSet { /// Creates a new changeset with one empty [`Changes`] element on the /// stack.. - fn new() -> Self { + pub(crate) fn new() -> Self { Self { stack: vec![Changes { contracts: BTreeMap::new(), diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index edab10fd..4d4b61e1 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -14,5 +14,6 @@ mod impls; mod types; -pub(crate) use impls::invoke_entrypoint_and_get_changes; -pub(crate) use types::{ChangeSet, InvokeEntrypointResponse, TestConfigurationError}; +pub(crate) use types::{ + ChangeSet, EntrypointInvocationHandler, InvokeEntrypointResponse, TestConfigurationError, +}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 0bbcd24b..f51fd916 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -26,17 +26,17 @@ pub(crate) struct InvokeEntrypointResponse { pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// Amount reserved for execution. This is used to return the correct /// balance of the invoker account. - pub(super) reserved_amount: Amount, + pub(crate) reserved_amount: Amount, /// Address of the invoker of the transaction. This is used to return the /// correct balance of the invoker account. - pub(super) invoker: AccountAddress, + pub(crate) invoker: AccountAddress, /// The changeset which keeps track of /// changes to accounts, modules, and contracts that occur during an /// invocation. - pub(super) changeset: ChangeSet, + pub(crate) changeset: ChangeSet, /// The energy remaining for execution. - pub(super) remaining_energy: &'a mut Energy, - pub(super) chain: &'b Chain, + pub(crate) remaining_energy: &'a mut Energy, + pub(crate) chain: &'b Chain, } /// The set of [`Changes`] represented as a stack. From 636a2befb2f3ed7d4abc6a5eb41380041debb788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 21:05:45 +0200 Subject: [PATCH 123/208] Specify versions of concordium dependencies. --- contract-testing/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index d27fdef0..acc9732c 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -7,9 +7,9 @@ rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {path = "../concordium-base/rust-src/concordium_base"} -concordium-smart-contract-engine = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} -concordium-wasm = {path = "../concordium-base/smart-contracts/wasm-transform"} +concordium_base = {version = "1.0", path = "../concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {version = "1.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {version = "1.0", path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" From 37948f47dbfa03a3b3c37759dbfed89c8ccb88d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 29 Mar 2023 21:16:44 +0200 Subject: [PATCH 124/208] Remove unused import. --- contract-testing/src/invocation/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 85f1a433..3f48b961 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -5,7 +5,7 @@ use crate::{ contract_events_from_logs, from_interpreter_energy, lookup_module_cost, to_interpreter_energy, }, - types::{Account, BalanceError, Chain, Contract, ContractModule, TransferError}, + types::{Account, BalanceError, Contract, ContractModule, TransferError}, AccountAddressEq, }; use concordium_base::{ From cb9afb8e7303143a41c99e64bf716ff463cf0afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 08:41:43 +0200 Subject: [PATCH 125/208] Documentation. --- contract-testing/src/impls.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 95c5a0e5..ae231fc8 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -662,8 +662,11 @@ impl Chain { /// /// **Parameters:** /// - `invoker`: the account paying for the transaction. - /// - `sender`: the sender of the transaction, can also be a contract. - /// TODO: This does not make sense. Senders cannot be contracts. + /// - `sender`: the sender of the message, can be an account or contract. + /// For top-level invocations, such as those caused by sending a contract + /// update transaction on the chain, the `sender` is always the + /// `invoker`. Here we provide extra freedom for testing invocations + /// where the sender differs. /// - `contract_address`: the contract to update. /// - `entrypoint`: the entrypoint to call. /// - `parameter`: the contract parameter. @@ -673,15 +676,15 @@ impl Chain { &mut self, signer: Signer, invoker: AccountAddress, - sender: Address, // TODO: Why does this exist? contra + sender: Address, energy_reserved: Energy, payload: UpdateContractPayload, ) -> Result { // Ensure the sender exists. if !self.address_exists(sender) { - // TODO: Should we charge the header cost if the invoker exists but the sender - // doesn't? - // No, this situation should not happen. + // This situation never happens on the chain since to send a message the sender + // is verifier upfront. So what we do here is custom behaviour, and we reject + // without consuming any eneryg. return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), From 4c1d4b4ff22c06a180b3de06c3b467b8edd4808b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 14:06:32 +0200 Subject: [PATCH 126/208] Move AccountAddressEq to concordium-base. --- contract-testing/src/impls.rs | 49 +++++------------------- contract-testing/src/invocation/impls.rs | 17 ++++---- contract-testing/src/invocation/types.rs | 8 ++-- contract-testing/src/types.rs | 12 +----- 4 files changed, 23 insertions(+), 63 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index ae231fc8..c997319e 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::anyhow; use concordium_base::{ - base::{Energy, OutOfEnergy}, + base::{Energy, InsufficientEnergy}, constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, @@ -1125,40 +1125,6 @@ impl Account { } } -// TODO: This should go to concordium-base. -impl From for AccountAddress { - fn from(aae: AccountAddressEq) -> Self { aae.0 } -} - -impl From for AccountAddressEq { - fn from(address: AccountAddress) -> Self { Self(address) } -} - -impl PartialEq for AccountAddressEq { - fn eq(&self, other: &Self) -> bool { - let bytes_1: &[u8; 32] = self.0.as_ref(); - let bytes_2: &[u8; 32] = other.0.as_ref(); - bytes_1[0..29] == bytes_2[0..29] - } -} - -impl PartialOrd for AccountAddressEq { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } -} - -impl Ord for AccountAddressEq { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let bytes_1: &[u8; 32] = self.0.as_ref(); - let bytes_2: &[u8; 32] = other.0.as_ref(); - bytes_1[0..29].cmp(&bytes_2[0..29]) - } -} - -impl AsRef for AccountAddress { - fn as_ref(&self) -> &AccountAddressEq { unsafe { std::mem::transmute(self) } } -} - impl Signer { /// Create a signer which always signs with one key. pub fn with_one_key() -> Self { Self { num_keys: 1 } } @@ -1218,8 +1184,9 @@ impl ContractInvokeSuccess { } } -impl From for ContractInitErrorKind { - fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } +impl From for ContractInitErrorKind { + #[inline(always)] + fn from(_: InsufficientEnergy) -> Self { Self::OutOfEnergy } } impl From for ContractInvokeErrorKind { @@ -1311,6 +1278,8 @@ pub(crate) fn contract_events_from_logs(logs: v0::Logs) -> Vec { #[cfg(test)] mod tests { + use concordium_base::base::AccountAddressEq; + use super::*; /// A few checks that test whether the function behavior matches its doc @@ -1359,9 +1328,9 @@ mod tests { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, // This differs on last four bytes, so it is a different account. ]); - let acc_eq = AccountAddressEq(acc); - let acc_alias_eq = AccountAddressEq(acc_alias); - let acc_other_eq = AccountAddressEq(acc_other); + let acc_eq: AccountAddressEq = acc.into(); + let acc_alias_eq: AccountAddressEq = acc_alias.into(); + let acc_other_eq: AccountAddressEq = acc_other.into(); let expected_amount = Amount::from_ccd(10); let expected_amount_other = Amount::from_ccd(123); diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 3f48b961..7c003a9a 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -6,10 +6,9 @@ use crate::{ to_interpreter_energy, }, types::{Account, BalanceError, Contract, ContractModule, TransferError}, - AccountAddressEq, }; use concordium_base::{ - base::{Energy, OutOfEnergy}, + base::{AccountAddressEq, Energy, InsufficientEnergy}, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedReceiveName, @@ -699,7 +698,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { fn run_interpreter( &mut self, f: F, - ) -> Result, OutOfEnergy> + ) -> Result, InsufficientEnergy> where F: FnOnce(InterpreterEnergy) -> ExecResult>, { @@ -716,7 +715,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }); } }; - let mut subtract_then_convert = |remaining_energy| -> Result { + let mut subtract_then_convert = |remaining_energy| -> Result { let remaining_energy = InterpreterEnergy::from(remaining_energy); // Using `saturating_sub` here should be ok since we should never be able to use // more energy than what is available. @@ -769,7 +768,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { remaining_energy: subtract_then_convert(remaining_energy)?, }), // Convert this to an error so that we will short-circuit the processing. - v1::ReceiveResult::OutOfEnergy => Err(OutOfEnergy), + v1::ReceiveResult::OutOfEnergy => Err(InsufficientEnergy), } } } @@ -834,7 +833,7 @@ impl ChangeSet { invoked_contract: ContractAddress, persisted_accounts: &mut BTreeMap, persisted_contracts: &mut BTreeMap, - ) -> Result { + ) -> Result { let current = self.current_mut(); let mut invoked_contract_has_state_changes = false; // Persist contract changes and collect the total increase in states sizes. @@ -913,7 +912,7 @@ impl ChangeSet { mut self, remaining_energy: &mut Energy, invoked_contract: ContractAddress, - ) -> Result { + ) -> Result { let mut invoked_contract_has_state_changes = false; let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut collector = v1::trie::SizeCollector::default(); @@ -1497,8 +1496,8 @@ impl InvokeEntrypointResponse { } } -impl From for TestConfigurationError { - fn from(_: OutOfEnergy) -> Self { Self::OutOfEnergy } +impl From for TestConfigurationError { + fn from(_: InsufficientEnergy) -> Self { Self::OutOfEnergy } } #[cfg(test)] diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index f51fd916..4904738a 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -1,6 +1,6 @@ -use crate::{AccountAddressEq, Chain}; +use crate::Chain; use concordium_base::{ - base::Energy, + base::{AccountAddressEq, Energy}, contracts_common::{ AccountAddress, Address, Amount, ContractAddress, ModuleReference, OwnedContractName, OwnedEntrypointName, @@ -52,7 +52,9 @@ pub(crate) struct ChangeSet { pub(super) struct Changes { /// The contracts which have changes. pub(super) contracts: BTreeMap, - /// The accounts which have changes. + /// The accounts which have changes. These are indexed by account address + /// equivalence classes so that account aliases are resolved to the same + /// account. pub(super) accounts: BTreeMap, } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index ba770b9f..43bd2205 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -1,5 +1,5 @@ use concordium_base::{ - base::Energy, + base::{AccountAddressEq, Energy}, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, @@ -78,16 +78,6 @@ pub struct Account { pub policy: OwnedPolicy, } -/// A helper struct that is used to ensure that aliases of an account are seen -/// as being the same account. -/// -/// Account aliases share the first 29 bytes of the address, so the -/// [`PartialEq`]/[`PartialOrd`] for this type adheres to that. -// TODO: This should be moved to concordium-base. -#[repr(transparent)] -#[derive(Eq, Debug, Clone, Copy)] -pub struct AccountAddressEq(pub(crate) AccountAddress); - /// A signer with a number of keys, the amount of which affects the cost of /// transactions. #[derive(Copy, Clone, Debug)] From 5c1bbc78570ae7067d33912f73bec132bae1ff65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 14:42:20 +0200 Subject: [PATCH 127/208] Add documentation about the discrepancy with the node. --- contract-testing/src/impls.rs | 42 +++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index c997319e..bb8bccf7 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -167,6 +167,17 @@ impl Chain { sender: AccountAddress, wasm_module: WasmModule, ) -> Result { + // For maintainers: + // + // This function does not correspond exactly to what happens in the node. + // There a user is also expected to give a max energy bound and the failures are + // slightly different. There it is possible to fail with "out of energy" + // error whereas here we only fail with "insufficient funds" if the user does + // not have enough CCD to pay. + // + // If users use our tools to deploy modules the costs are calculated for them so + // that deployment should never fail with out of energy. Not requiring energy + // provides a more ergonomic experience. let Ok(sender_account) = self.accounts .get_mut(&sender.into()) .ok_or(AccountDoesNotExist { address: sender }) else { @@ -210,12 +221,19 @@ impl Chain { }); } - let account = sender_account; + // Calculate the deploy module cost. + let deploy_module_energy = cost::deploy_module(wasm_module.source.size()); + let deploy_module_cost = parameters.calculate_energy_cost(deploy_module_energy); - // TODO: Ensure that this matches the node for both invalid and valid modules. - // to_ccd(header_cost) + to_ccd(deploy_cost) != to_ccd(header_cost + - // deploy_cost). - account.balance.total -= check_header_cost; + // Subtract the cost from the account if it has sufficient funds. + if sender_account.balance.available() < deploy_module_cost { + return Err(ModuleDeployError { + kind: ModuleDeployErrorKind::InsufficientFunds, + energy_used: check_header_energy, + transaction_fee: check_header_cost, + }); + }; + sender_account.balance.total -= check_header_cost + deploy_module_cost; // Construct the artifact. let artifact = @@ -235,20 +253,6 @@ impl Chain { } }; - // Calculate the deploy module cost. - let deploy_module_energy = cost::deploy_module(wasm_module.source.size()); - let deploy_module_cost = parameters.calculate_energy_cost(deploy_module_energy); - - // Subtract the cost from the account if it has sufficient funds. - if account.balance.available() < deploy_module_cost { - return Err(ModuleDeployError { - kind: ModuleDeployErrorKind::InsufficientFunds, - energy_used: check_header_energy, - transaction_fee: check_header_cost, - }); - }; - account.balance.total -= deploy_module_cost; - // Save the module. let module_reference: ModuleReference = wasm_module.get_module_ref(); From 50911dba0b9dc712688d578814728640847a47cf Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 30 Mar 2023 15:47:38 +0200 Subject: [PATCH 128/208] Fix review comments --- contract-testing/src/impls.rs | 13 +++++-------- contract-testing/src/invocation/impls.rs | 10 ++++------ contract-testing/src/invocation/types.rs | 6 ++++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index bb8bccf7..394494a4 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -45,7 +45,7 @@ impl ChainParameters { /// Create a new [`ChainParameters`](Self) with a specified `block_time` /// where - /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(block_time: SlotTime) -> Self { Self { @@ -106,7 +106,7 @@ impl Chain { } /// Create a new [`Chain`](Self) with a specified `block_time` where - /// - `micro_ccd_per_euro` defaults to `147235241 / 1` + /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. pub fn new_with_time(block_time: SlotTime) -> Self { Self { @@ -687,8 +687,8 @@ impl Chain { // Ensure the sender exists. if !self.address_exists(sender) { // This situation never happens on the chain since to send a message the sender - // is verifier upfront. So what we do here is custom behaviour, and we reject - // without consuming any eneryg. + // is verified upfront. So what we do here is custom behaviour, and we reject + // without consuming any energy. return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), @@ -738,9 +738,6 @@ impl Chain { }); } - // Charge account for the reserved energy up front. This is to ensure that - // contract queries for the invoker balance are correct. - // The `amount` is handled in contract_invocation_worker. let contract_address = payload.address; let res = self.contract_invocation_worker( invoker, @@ -781,7 +778,7 @@ impl Chain { Ok(s) => s.transaction_fee, Err(e) => e.transaction_fee, }; - // Change for execution. + // Charge for execution. self.account_mut(invoker) .expect("existence already checked") .balance diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 7c003a9a..538192a4 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -819,9 +819,8 @@ impl ChangeSet { /// - no changes will be persisted, /// - an [`OutOfEnergy`] error is returned. /// - /// Otherwise, it returns the [`Energy`] to be charged for the additional - /// bytes added to contract states. It also returns whether the state of the - /// provided `invoked_contract` was changed. + /// Otherwise, it returns whether the state of the provided + /// `invoked_contract` was changed. /// /// **Preconditions:** /// - All contracts, modules, accounts referred must exist in persistence. @@ -905,9 +904,8 @@ impl ChangeSet { /// Returns an [`OutOfEnergy`] error if the energy needed for storing the /// extra state is larger than `remaining_energy`. /// - /// Otherwise, it returns the [`Energy`] needed for storing the extra state. - /// It also returns whether the state of the provided `invoked_contract` - /// has changed. + /// Otherwise, it returns whether the state of the provided + /// `invoked_contract` has changed. pub(crate) fn collect_energy_for_state( mut self, remaining_energy: &mut Energy, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 4904738a..7e242ba0 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -61,8 +61,14 @@ pub(super) struct Changes { /// Data held for an account during the execution of a contract entrypoint. #[derive(Clone, Debug)] pub(super) struct AccountChanges { + /// The original balance. + /// + /// For the `invoker`, this will be the `original_balance - reserved_amount` + /// (from `EntrypointInvocationHandler`). + /// /// Should never be modified. pub(super) original_balance: Amount, + /// The change in the account balance. pub(super) balance_delta: AmountDelta, } From 8763875496eec624036ffd2371ab7a0ed46db6ff Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 30 Mar 2023 16:25:04 +0200 Subject: [PATCH 129/208] Fix costs of deploy module --- contract-testing/src/impls.rs | 68 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 394494a4..ab3847d2 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -191,26 +191,9 @@ impl Chain { }); }; - let parameters = &self.parameters; - let check_header_energy = { - // +1 for the tag, +8 for size and version - let payload_size = 1 - + 8 - + wasm_module.source.size() - + transactions::construct::TRANSACTION_HEADER_SIZE; - cost::base_cost(payload_size, signer.num_keys) - }; - let check_header_cost = parameters.calculate_energy_cost(check_header_energy); - - if sender_account.balance.available() < check_header_cost { - return Err(ModuleDeployError { - kind: ModuleDeployErrorKind::InsufficientFunds, - energy_used: 0.into(), - transaction_fee: Amount::zero(), - }); - } - // Only v1 modules are supported in this testing library. + // This error case does not exist in the node, so we don't need to match a + // specific cost. We charge 0 for it. if wasm_module.version != WasmVersion::V1 { return Err(ModuleDeployError { kind: ModuleDeployErrorKind::UnsupportedModuleVersion( @@ -221,19 +204,36 @@ impl Chain { }); } + let parameters = &self.parameters; + let check_header_energy = { + // +1 for the tag, +8 for size and version + let payload_size = 1 + + 8 + + wasm_module.source.size() + + transactions::construct::TRANSACTION_HEADER_SIZE; + cost::base_cost(payload_size, signer.num_keys) + }; + // Calculate the deploy module cost. let deploy_module_energy = cost::deploy_module(wasm_module.source.size()); - let deploy_module_cost = parameters.calculate_energy_cost(deploy_module_energy); - - // Subtract the cost from the account if it has sufficient funds. - if sender_account.balance.available() < deploy_module_cost { + let energy_used = check_header_energy + deploy_module_energy; + let transaction_fee = parameters.calculate_energy_cost(energy_used); + + // Check if the account has sufficient balance to cover the transaction fee. + // This fee corresponds to the energy_reserved that our tools calculate when + // sending the transaction to the node. The account is not charged in the node + // unless it has sufficient balance to pay for the full deployment (and thus all + // the energy). + if sender_account.balance.available() < transaction_fee { return Err(ModuleDeployError { kind: ModuleDeployErrorKind::InsufficientFunds, - energy_used: check_header_energy, - transaction_fee: check_header_cost, + energy_used: 0.into(), + transaction_fee: Amount::zero(), }); }; - sender_account.balance.total -= check_header_cost + deploy_module_cost; + + // Charge the account. + sender_account.balance.total -= transaction_fee; // Construct the artifact. let artifact = @@ -246,9 +246,9 @@ impl Chain { Ok(artifact) => artifact, Err(err) => { return Err(ModuleDeployError { - kind: ModuleInvalidError(err).into(), - energy_used: check_header_energy, - transaction_fee: check_header_cost, + kind: ModuleInvalidError(err).into(), + energy_used, + transaction_fee, }) } }; @@ -259,9 +259,9 @@ impl Chain { // Ensure module hasn't been deployed before. if self.modules.contains_key(&module_reference) { return Err(ModuleDeployError { - kind: ModuleDeployErrorKind::DuplicateModule(module_reference), - energy_used: check_header_energy + deploy_module_energy, - transaction_fee: check_header_cost + deploy_module_cost, + kind: ModuleDeployErrorKind::DuplicateModule(module_reference), + energy_used, + transaction_fee, }); } self.modules.insert(module_reference, ContractModule { @@ -270,8 +270,8 @@ impl Chain { }); Ok(ModuleDeploySuccess { module_reference, - energy_used: check_header_energy + deploy_module_energy, - transaction_fee: check_header_cost + deploy_module_cost, + energy_used, + transaction_fee, }) } From 12381db366d367dce3d0274fcdb2a2ffb1814b04 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 30 Mar 2023 17:05:18 +0200 Subject: [PATCH 130/208] Handle amount too large and missing init in contract_init --- contract-testing/src/impls.rs | 42 ++++++++++++++++++++++++----------- contract-testing/src/types.rs | 13 ++++++++++- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index ab3847d2..7c387d16 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -253,7 +253,6 @@ impl Chain { } }; - // Save the module. let module_reference: ModuleReference = wasm_module.get_module_ref(); // Ensure module hasn't been deployed before. @@ -299,7 +298,7 @@ impl Chain { }); } // We cannot deserialize directly to [`ModuleSource`] as it expects the first - // four bytes to be the length, which is isn't for this raw file. + // four bytes to be the length, which it isn't for this raw file. let mut buffer = Vec::new(); std::io::Read::read_to_end(&mut reader, &mut buffer).map_err(|e| ModuleLoadError { path: module_path.to_path_buf(), @@ -413,15 +412,12 @@ impl Chain { // Get the account and check that it has sufficient balance to pay for the // reserved_energy and amount. let account_info = self.account(sender)?; - if account_info.balance.available() - < self.parameters.calculate_energy_cost(energy_reserved) + payload.amount - { - return Err(ContractInitErrorKind::InsufficientFunds); - } - // Ensure that the parameter has a valid size. - if payload.param.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { - return Err(ContractInitErrorKind::ParameterTooLarge); + let energy_reserved_cost = self.parameters.calculate_energy_cost(energy_reserved); + + // Check that the account can pay for the reserved energy. + if account_info.balance.available() < energy_reserved_cost { + return Err(ContractInitErrorKind::InsufficientFunds); } // Compute the base cost for checking the transaction header. @@ -435,15 +431,35 @@ impl Chain { // Charge the header cost. remaining_energy.tick_energy(check_header_cost)?; + // Ensure that the parameter has a valid size. + if payload.param.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { + return Err(ContractInitErrorKind::ParameterTooLarge); + } + // Charge the base cost for initializing a contract. remaining_energy.tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; + // Check that the account also has enough funds to pay for the amount (in + // addition to the reserved energy). + if account_info.balance.available() < energy_reserved_cost + payload.amount { + return Err(ContractInitErrorKind::AmountTooLarge); + } + // Lookup module. let module = self.contract_module(payload.mod_ref)?; let lookup_cost = lookup_module_cost(&module); + // Charge the cost for looking up the module. remaining_energy.tick_energy(lookup_cost)?; + // Ensure the module contains the provided init name. + let init_name = payload.init_name.as_contract_name().get_chain_name(); + if !module.artifact.export.get(init_name).is_some() { + return Err(ContractInitErrorKind::ContractNotPresentInModule { + name: payload.init_name, + }); + } + // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { @@ -462,10 +478,10 @@ impl Chain { module.artifact, init_ctx, v1::InitInvocation { - amount: payload.amount, - init_name: payload.init_name.as_contract_name().get_chain_name(), + amount: payload.amount, + init_name, parameter: payload.param.as_ref(), - energy: energy_given_to_interpreter, + energy: energy_given_to_interpreter, }, false, // We only support protocol P5 and up, so no limiting. loader, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 43bd2205..5e37f13d 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -217,12 +217,23 @@ pub enum ContractInitErrorKind { /// Module has not been deployed in the test environment. #[error("{0}")] ModuleDoesNotExist(#[from] ModuleDoesNotExist), + /// The specified contract does not exist in the module. + #[error("{0}")] + ContractNotPresentInModule { + /// The name of the contract (init method) which is not present. + name: OwnedContractName, + }, /// The sender account has not been created in test environment. #[error("Sender missing: {0}")] SenderDoesNotExist(#[from] AccountDoesNotExist), - /// The invoker account does not have enough funds to pay for the energy. + /// The invoker account does not have enough funds to pay for the energy + /// reserved. #[error("Invoker does not have enough funds to pay for the energy")] InsufficientFunds, + /// The invoker account does not have enough funds to pay for the amount. + /// However it does it have enough funds for the energy reserved. + #[error("Invoker does not have enough funds to pay for the amount")] + AmountTooLarge, /// The parameter is too large. #[error("The provided parameter exceeds the maximum size allowed")] ParameterTooLarge, From 1b984429726c0b345f642110b4d87f035a09e7f9 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 30 Mar 2023 17:28:15 +0200 Subject: [PATCH 131/208] Handle amount too large separately for contract invocations --- contract-testing/src/impls.rs | 47 ++++++++++++++++++++++++----------- contract-testing/src/types.rs | 10 +++++--- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7c387d16..33186e80 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -592,7 +592,7 @@ impl Chain { /// *Preconditions:* /// - `invoker` exists. /// - `sender` exists. - /// - `invoker`s balance is >= `amount`. + /// - `invoker` has sufficient balance to pay for `energy_reserved`. fn contract_invocation_worker( &self, invoker: AccountAddress, @@ -629,6 +629,23 @@ impl Chain { *remaining_energy, )); } + + // Check that the invoker has sufficient funds to pay for amount (in addition to + // the energy reserved, which is already checked). + if self + .account(invoker) + .expect("Precondition violation: must already exist") + .balance + .available() + < amount_reserved_for_energy + payload.amount + { + return Err(self.from_invocation_error_kind( + ContractInvokeErrorKind::AmountTooLarge, + energy_reserved, + *remaining_energy, + )); + } + let mut contract_invocation = EntrypointInvocationHandler { changeset: ChangeSet::new(), remaining_energy, @@ -712,6 +729,17 @@ impl Chain { }); } + // Ensure the invoker exists. + let Ok(account_info) = self.account(invoker) else { + return Err(ContractInvokeError { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + kind: ContractInvokeErrorKind::InvokerDoesNotExist( + AccountDoesNotExist { address: invoker }, + ), + }); + }; + // Compute the base cost for checking the transaction header. let check_header_cost = { // 1 byte for the tag. @@ -733,19 +761,8 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); - // Ensure the invoker exists. - let Ok(account_info) = self.account(invoker) else { - return Err(ContractInvokeError { - energy_used: Energy::from(0), - transaction_fee: Amount::zero(), - kind: ContractInvokeErrorKind::InvokerDoesNotExist( - AccountDoesNotExist { address: invoker }, - ), - }); - }; - - // Ensure the account has sufficient funds to pay for the energy and amount. - if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { + // Ensure the account has sufficient funds to pay for the energy. + if account_info.balance.available() < invoker_amount_reserved_for_nrg { let energy_used = energy_reserved - remaining_energy; return Err(ContractInvokeError { energy_used, @@ -845,7 +862,7 @@ impl Chain { let invoker_amount_reserved_for_nrg = self.parameters.calculate_energy_cost(energy_reserved); - if account_info.balance.available() < invoker_amount_reserved_for_nrg + payload.amount { + if account_info.balance.available() < invoker_amount_reserved_for_nrg { let energy_used = Energy::from(0); return Err(ContractInvokeError { energy_used, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 5e37f13d..5c292e18 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -218,7 +218,7 @@ pub enum ContractInitErrorKind { #[error("{0}")] ModuleDoesNotExist(#[from] ModuleDoesNotExist), /// The specified contract does not exist in the module. - #[error("{0}")] + #[error("The contract (init name) '{name}' does not exist in the module")] ContractNotPresentInModule { /// The name of the contract (init method) which is not present. name: OwnedContractName, @@ -322,10 +322,14 @@ pub enum ContractInvokeErrorKind { /// The sender does not exist in the test environment. #[error("Sender missing: the object with address '{0}' does not exist")] SenderDoesNotExist(Address), - /// The invoker account does not have enough funds to pay for the energy and - /// amount sent. + /// The invoker account does not have enough funds to pay for the energy + /// reserved. #[error("Invoker does not have enough funds to pay for the energy")] InsufficientFunds, + /// The invoker account does not have enough funds to pay for the amount. + /// However it does it have enough funds for the energy reserved. + #[error("Invoker does not have enough funds to pay for the amount")] + AmountTooLarge, /// The parameter is too large. #[error("The provided parameter exceeds the maximum size allowed")] ParameterTooLarge, From 376c38c0e831191b4d8e9ce9e52c842026b61f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 20:26:40 +0200 Subject: [PATCH 132/208] Don't do redundant lookups. --- contract-testing/src/impls.rs | 1 + contract-testing/src/invocation/impls.rs | 17 ++++++----------- contract-testing/src/types.rs | 1 + 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 33186e80..f50e3834 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -522,6 +522,7 @@ impl Chain { state: persisted_state, owner: sender, self_balance: payload.amount, + address: contract_address, }; // Save the contract. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 538192a4..b58de190 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -9,6 +9,7 @@ use crate::{ }; use concordium_base::{ base::{AccountAddressEq, Energy, InsufficientEnergy}, + constants::MAX_PARAMETER_LEN, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedReceiveName, @@ -104,7 +105,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { .contracts .get(&payload.address) .expect("Contract known to exist at this point"); - let module = self.contract_module(payload.address); + let module = self.contract_module(instance); // Construct the receive name (or fallback receive name) and ensure its presence // in the contract. Also returns the contract name and entrypoint name as they @@ -192,7 +193,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, instance_state, v1::ReceiveParams { - max_parameter_size: 65535, + max_parameter_size: MAX_PARAMETER_LEN, limit_logs_and_return_values: false, support_queries: true, }, @@ -475,12 +476,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// - Contract instance must exist (and therefore also the artifact). /// - If the changeset contains a module reference, then it must refer a /// deployed module. - fn contract_module(&self, address: ContractAddress) -> ContractModule { + fn contract_module(&self, contract: &Contract) -> ContractModule { match self .changeset .current() .contracts - .get(&address) + .get(&contract.address) .and_then(|c| c.module) { // Contract has been upgrade, new module exists. @@ -492,15 +493,9 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { .clone(), // Contract hasn't been upgraded. Use persisted module. None => { - let module_ref = self - .chain - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .module_reference; self.chain .modules - .get(&module_ref) + .get(&contract.module_reference) .expect("Precondition violation: module must exist.") .clone() } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 5c292e18..92d9ea05 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -56,6 +56,7 @@ pub struct Chain { /// A smart contract instance. #[derive(Clone, Debug)] pub struct Contract { + pub address: ContractAddress, /// The module which contains this contract. pub module_reference: ModuleReference, /// The name of the contract. From 815593eecb62b07331e11bf7adfa9404bb7d64d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 21:33:26 +0200 Subject: [PATCH 133/208] Make process not directly recursive. The only remaining recursion is invoke_entrypoint -> process -> invoke_entrypoint. --- contract-testing/src/invocation/impls.rs | 832 +++++++++++------------ 1 file changed, 415 insertions(+), 417 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b58de190..2ab7c5ff 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -492,13 +492,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { .expect("Precondition violation: module must exist.") .clone(), // Contract hasn't been upgraded. Use persisted module. - None => { - self.chain - .modules - .get(&contract.module_reference) - .expect("Precondition violation: module must exist.") - .clone() - } + None => self + .chain + .modules + .get(&contract.module_reference) + .expect("Precondition violation: module must exist.") + .clone(), } } @@ -1037,447 +1036,446 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { /// - Account exists in `invocation_handler.accounts`. fn process( &mut self, - receive_result: v1::ReceiveResult, + mut receive_result: v1::ReceiveResult, ) -> Result, TestConfigurationError> { - match receive_result { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - // Update the remaining_energy field. - self.invocation_handler.update_energy(remaining_energy); - - let update_event = ContractTraceElement::Updated { - data: InstanceUpdatedEvent { - contract_version: WasmVersion::V1, - address: self.address, - instigator: self.sender, - amount: self.amount, - message: self.parameter.clone(), - receive_name: OwnedReceiveName::construct_unchecked( - self.contract_name.as_contract_name(), - self.entrypoint.as_entrypoint_name(), - ), - events: contract_events_from_logs(logs.clone()), - }, - }; - // Add update event - self.trace_elements.push(update_event); - - // Save changes to changeset. - if state_changed { - self.invocation_handler.save_state_changes( - self.address, - &mut self.state, - false, - ); - } - - Ok(v1::ReceiveResult::Success { + loop { + match receive_result { + v1::ReceiveResult::Success { logs, state_changed, return_value, remaining_energy, - }) - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs, - config, - interrupt, - } => { - // Update the remaining_energy field. - self.invocation_handler.update_energy(remaining_energy); - // Create the interrupt event, which will be included for transfers, calls, and - // upgrades, but not for the remaining interrupts. - let interrupt_event = ContractTraceElement::Interrupted { - address: self.address, - events: contract_events_from_logs(logs), - }; - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - // Add the interrupt event - self.trace_elements.push(interrupt_event); - - let response = match self - .invocation_handler - .transfer_from_contract_to_account(amount, self.address, to) - { - Ok(new_balance) => v1::InvokeResponse::Success { - new_balance, - data: None, - }, - Err(err) => { - let kind = match err { - TransferError::ToMissing => { - v1::InvokeFailure::NonExistentAccount - } + } => { + // Update the remaining_energy field. + self.invocation_handler.update_energy(remaining_energy); + + let update_event = ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + contract_version: WasmVersion::V1, + address: self.address, + instigator: self.sender, + amount: self.amount, + message: self.parameter.clone(), + receive_name: OwnedReceiveName::construct_unchecked( + self.contract_name.as_contract_name(), + self.entrypoint.as_entrypoint_name(), + ), + events: contract_events_from_logs(logs.clone()), + }, + }; + // Add update event + self.trace_elements.push(update_event); + + // Save changes to changeset. + if state_changed { + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + false, + ); + } - TransferError::BalanceError { - error: BalanceError::Insufficient, - } => v1::InvokeFailure::InsufficientAmount, + return Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + }); + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => { + // Update the remaining_energy field. + self.invocation_handler.update_energy(remaining_energy); + // Create the interrupt event, which will be included for transfers, calls, and + // upgrades, but not for the remaining interrupts. + let interrupt_event = ContractTraceElement::Interrupted { + address: self.address, + events: contract_events_from_logs(logs), + }; + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + // Add the interrupt event + self.trace_elements.push(interrupt_event); - TransferError::BalanceError { - error: BalanceError::Overflow, - } => { - // Balance overflows are unrecoverable and short circuit. - return Err(TestConfigurationError::BalanceOverflow); - } - }; - v1::InvokeResponse::Failure { kind } + let response = match self + .invocation_handler + .transfer_from_contract_to_account(amount, self.address, to) + { + Ok(new_balance) => v1::InvokeResponse::Success { + new_balance, + data: None, + }, + Err(err) => { + let kind = match err { + TransferError::ToMissing => { + v1::InvokeFailure::NonExistentAccount + } + + TransferError::BalanceError { + error: BalanceError::Insufficient, + } => v1::InvokeFailure::InsufficientAmount, + + TransferError::BalanceError { + error: BalanceError::Overflow, + } => { + // Balance overflows are unrecoverable and short + // circuit. + return Err(TestConfigurationError::BalanceOverflow); + } + }; + v1::InvokeResponse::Failure { kind } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + if success { + // Add transfer event + self.trace_elements.push(ContractTraceElement::Transferred { + from: self.address, + amount, + to, + }); } - }; - - let success = matches!(response, v1::InvokeResponse::Success { .. }); - if success { - // Add transfer event - self.trace_elements.push(ContractTraceElement::Transferred { - from: self.address, - amount, - to, + // Add resume event + self.trace_elements.push(ContractTraceElement::Resumed { + address: self.address, + success, }); - } - // Add resume event - self.trace_elements.push(ContractTraceElement::Resumed { - address: self.address, - success, - }); - - self.invocation_handler - .remaining_energy - .tick_energy(concordium_base::transactions::cost::SIMPLE_TRANSFER)?; - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // never changes on transfers - // An empty loader is fine currently, as we do not use caching - // in this lib. - v1::trie::Loader::new(&[][..]), - ) - })?; - - // Resume - self.process(resume_res) - } - v1::Interrupt::Call { - address, - parameter, - name, - amount, - } => { - // Add the interrupt event - self.trace_elements.push(interrupt_event); - - if state_changed { - self.invocation_handler.save_state_changes( - self.address, - &mut self.state, - true, - ); - } - // Save the modification index before the invoke. - let mod_idx_before_invoke = - self.invocation_handler.modification_index(self.address); - - // Make a checkpoint before calling another contract so that we may roll - // back. - self.invocation_handler.checkpoint(); + self.invocation_handler.remaining_energy.tick_energy( + concordium_base::transactions::cost::SIMPLE_TRANSFER, + )?; + + let resume_res = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // never changes on transfers + // An empty loader is fine currently, as we do not use caching + // in this lib. + v1::trie::Loader::new(&[][..]), + ) + })?; + + // Resume + receive_result = resume_res; + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => { + // Add the interrupt event + self.trace_elements.push(interrupt_event); - let (success, invoke_response) = match self - .invocation_handler - .chain - .contracts - .get(&address) - .map(|c| c.contract_name.as_contract_name()) - { - // The contract to call does not exist. - None => { - let invoke_response = v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }; - (false, invoke_response) - } - Some(contract_name) => { - let receive_name = OwnedReceiveName::construct_unchecked( - contract_name, - name.as_entrypoint_name(), + if state_changed { + self.invocation_handler.save_state_changes( + self.address, + &mut self.state, + true, ); - let message = OwnedParameter::new_unchecked(parameter); - let res = self.invocation_handler.invoke_entrypoint( - self.invoker, - Address::Contract(self.address), - UpdateContractPayload { - amount, - address, - receive_name, - message, - }, - &mut self.trace_elements, - )?; - match res.invoke_response { - v1::InvokeResponse::Success { data, .. } => { - let invoke_response = v1::InvokeResponse::Success { - // The balance returned by `invoke_entrypoint` is the - // balance of the contract called. But we are interested - // in the new balance of the caller. - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data, - }; - (true, invoke_response) - } - failure => (false, failure), - } } - }; - - // Remove the last state changes if the invocation failed. - let state_changed = if !success { - self.invocation_handler.rollback(); - false // We rolled back, so no changes were made - // to this contract. - } else { - let mod_idx_after_invoke = + + // Save the modification index before the invoke. + let mod_idx_before_invoke = self.invocation_handler.modification_index(self.address); - let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; - if state_changed { - // Update the state field with the newest value from the - // changeset. - self.state = self.invocation_handler.contract_state(self.address); - } - state_changed - }; - - // Add resume event - let resume_event = ContractTraceElement::Resumed { - address: self.address, - success, - }; - - self.trace_elements.push(resume_event); - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - invoke_response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; - - self.process(resume_res) - } - v1::Interrupt::Upgrade { module_ref } => { - // Add the interrupt event. - self.trace_elements.push(interrupt_event); - - // Charge a base cost. - self.invocation_handler - .remaining_energy - .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; - - let response = match self.invocation_handler.chain.modules.get(&module_ref) - { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidModuleRef, - }, - Some(module) => { - // Charge for the module lookup. - self.invocation_handler - .remaining_energy - .tick_energy(lookup_module_cost(module))?; - - if module.artifact.export.contains_key( - self.contract_name.as_contract_name().get_chain_name(), - ) { - // Update module reference in the changeset. - let old_module_ref = self - .invocation_handler - .save_module_upgrade(self.address, module_ref); - // Charge for the initialization cost. - self.invocation_handler.remaining_energy.tick_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - )?; + // Make a checkpoint before calling another contract so that we may roll + // back. + self.invocation_handler.checkpoint(); - let upgrade_event = ContractTraceElement::Upgraded { - address: self.address, - from: old_module_ref, - to: module_ref, + let (success, invoke_response) = match self + .invocation_handler + .chain + .contracts + .get(&address) + .map(|c| c.contract_name.as_contract_name()) + { + // The contract to call does not exist. + None => { + let invoke_response = v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, }; - - self.trace_elements.push(upgrade_event); - - v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: None, - } - } else { - v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidContractName, + (false, invoke_response) + } + Some(contract_name) => { + let receive_name = OwnedReceiveName::construct_unchecked( + contract_name, + name.as_entrypoint_name(), + ); + let message = OwnedParameter::new_unchecked(parameter); + let res = self.invocation_handler.invoke_entrypoint( + self.invoker, + Address::Contract(self.address), + UpdateContractPayload { + amount, + address, + receive_name, + message, + }, + &mut self.trace_elements, + )?; + match res.invoke_response { + v1::InvokeResponse::Success { data, .. } => { + let invoke_response = v1::InvokeResponse::Success { + // The balance returned by `invoke_entrypoint` is + // the + // balance of the contract called. But we are + // interested + // in the new balance of the caller. + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data, + }; + (true, invoke_response) + } + failure => (false, failure), } } - } - }; - - let success = matches!(response, v1::InvokeResponse::Success { .. }); - self.trace_elements.push(ContractTraceElement::Resumed { - address: self.address, - success, - }); - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; - - self.process(resume_res) - } - v1::Interrupt::QueryAccountBalance { address } => { - let response = match self.invocation_handler.account_balance(address) { - Some(balance) => v1::InvokeResponse::Success { - new_balance: self + }; + + // Remove the last state changes if the invocation failed. + let state_changed = if !success { + self.invocation_handler.rollback(); + false // We rolled back, so no changes were made + // to this contract. + } else { + let mod_idx_after_invoke = + self.invocation_handler.modification_index(self.address); + let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + self.state = + self.invocation_handler.contract_state(self.address); + } + state_changed + }; + + // Add resume event + let resume_event = ContractTraceElement::Resumed { + address: self.address, + success, + }; + + self.trace_elements.push(resume_event); + + let resume_res = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + invoke_response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + })?; + + receive_result = resume_res; + } + v1::Interrupt::Upgrade { module_ref } => { + // Add the interrupt event. + self.trace_elements.push(interrupt_event); + + // Charge a base cost. + self.invocation_handler + .remaining_energy + .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; + + let response = + match self.invocation_handler.chain.modules.get(&module_ref) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(module) => { + // Charge for the module lookup. + self.invocation_handler + .remaining_energy + .tick_energy(lookup_module_cost(module))?; + + if module.artifact.export.contains_key( + self.contract_name.as_contract_name().get_chain_name(), + ) { + // Update module reference in the changeset. + let old_module_ref = self + .invocation_handler + .save_module_upgrade(self.address, module_ref); + + // Charge for the initialization cost. + self.invocation_handler.remaining_energy.tick_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + )?; + + let upgrade_event = ContractTraceElement::Upgraded { + address: self.address, + from: old_module_ref, + to: module_ref, + }; + + self.trace_elements.push(upgrade_event); + + v1::InvokeResponse::Success { + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, + } + } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + self.trace_elements.push(ContractTraceElement::Resumed { + address: self.address, + success, + }); + + let resume_res = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + })?; + receive_result = resume_res; + } + v1::Interrupt::QueryAccountBalance { address } => { + let response = match self.invocation_handler.account_balance(address) { + Some(balance) => v1::InvokeResponse::Success { + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: Some(to_bytes(&balance)), + }, + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + + self.invocation_handler.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, + )?; + + receive_result = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } + v1::Interrupt::QueryContractBalance { address } => { + let response = match self.invocation_handler.contract_balance(address) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + Some(bal) => v1::InvokeResponse::Success { + // Balance of contract querying. Won't change. Notice the + // `self.address`. + new_balance: self + .invocation_handler + .contract_balance_unchecked(self.address), + data: Some(to_bytes(&bal)), + }, + }; + + self.invocation_handler.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + )?; + + receive_result = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } + v1::Interrupt::QueryExchangeRates => { + let exchange_rates = ExchangeRates { + euro_per_energy: self .invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&balance)), - }, - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - }, - }; - - self.invocation_handler - .remaining_energy - .tick_energy(constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST)?; - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - - self.process(resume_res) - } - v1::Interrupt::QueryContractBalance { address } => { - let response = match self.invocation_handler.contract_balance(address) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, - Some(bal) => v1::InvokeResponse::Success { - // Balance of contract querying. Won't change. Notice the - // `self.address`. + .chain + .parameters + .euro_per_energy, + micro_ccd_per_euro: self + .invocation_handler + .chain + .parameters + .micro_ccd_per_euro, + }; + + let response = v1::InvokeResponse::Success { new_balance: self .invocation_handler .contract_balance_unchecked(self.address), - data: Some(to_bytes(&bal)), - }, - }; - - self.invocation_handler.remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - )?; - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - - self.process(resume_res) - } - v1::Interrupt::QueryExchangeRates => { - let exchange_rates = ExchangeRates { - euro_per_energy: self - .invocation_handler - .chain - .parameters - .euro_per_energy, - micro_ccd_per_euro: self - .invocation_handler - .chain - .parameters - .micro_ccd_per_euro, - }; - - let response = v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&exchange_rates)), - }; - - self.invocation_handler - .remaining_energy - .tick_energy(constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST)?; - - let resume_res = self.invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - - self.process(resume_res) + data: Some(to_bytes(&exchange_rates)), + }; + + self.invocation_handler.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, + )?; + + receive_result = self.invocation_handler.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut self.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } } } - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - self.invocation_handler.update_energy(remaining_energy); - Ok(v1::ReceiveResult::Reject { + v1::ReceiveResult::Reject { reason, return_value, remaining_energy, - }) - } - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => { - self.invocation_handler.update_energy(remaining_energy); - Ok(v1::ReceiveResult::Trap { + } => { + self.invocation_handler.update_energy(remaining_energy); + return Ok(v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + }); + } + v1::ReceiveResult::Trap { error, remaining_energy, - }) + } => { + self.invocation_handler.update_energy(remaining_energy); + return Ok(v1::ReceiveResult::Trap { + error, + remaining_energy, + }); + } + // Convert this to an error here, so that we will short circuit processing. + v1::ReceiveResult::OutOfEnergy => return Err(TestConfigurationError::OutOfEnergy), } - // Convert this to an error here, so that we will short circuit processing. - v1::ReceiveResult::OutOfEnergy => Err(TestConfigurationError::OutOfEnergy), } } } From 4c4b753b4640d64cb6da3ce2e9815c062811e7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 21:51:17 +0200 Subject: [PATCH 134/208] Remove EntrypointInvocationHandler from InvocationData. --- contract-testing/src/invocation/impls.rs | 174 ++++++++++------------- contract-testing/src/invocation/types.rs | 23 ++- contract-testing/src/types.rs | 2 +- 3 files changed, 89 insertions(+), 110 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 2ab7c5ff..211bee3c 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -210,13 +210,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { entrypoint: entrypoint_name, parameter: payload.message, amount: payload.amount, - invocation_handler: self, state: mutable_state, trace_elements: Vec::new(), }; // Process the receive invocation to the completion. - let result = data.process(initial_result)?; + let result = data.process(self, initial_result)?; let mut new_trace_elements = data.trace_elements; let result = match result { @@ -1028,7 +1027,7 @@ impl AccountChanges { } } -impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { +impl InvocationData { /// Process a receive function until completion. /// /// **Preconditions**: @@ -1036,6 +1035,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { /// - Account exists in `invocation_handler.accounts`. fn process( &mut self, + invocation_handler: &mut EntrypointInvocationHandler, mut receive_result: v1::ReceiveResult, ) -> Result, TestConfigurationError> { loop { @@ -1047,7 +1047,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { remaining_energy, } => { // Update the remaining_energy field. - self.invocation_handler.update_energy(remaining_energy); + invocation_handler.update_energy(remaining_energy); let update_event = ContractTraceElement::Updated { data: InstanceUpdatedEvent { @@ -1068,11 +1068,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { // Save changes to changeset. if state_changed { - self.invocation_handler.save_state_changes( - self.address, - &mut self.state, - false, - ); + invocation_handler.save_state_changes(self.address, &mut self.state, false); } return Ok(v1::ReceiveResult::Success { @@ -1090,7 +1086,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { interrupt, } => { // Update the remaining_energy field. - self.invocation_handler.update_energy(remaining_energy); + invocation_handler.update_energy(remaining_energy); // Create the interrupt event, which will be included for transfers, calls, and // upgrades, but not for the remaining interrupts. let interrupt_event = ContractTraceElement::Interrupted { @@ -1102,8 +1098,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { // Add the interrupt event self.trace_elements.push(interrupt_event); - let response = match self - .invocation_handler + let response = match invocation_handler .transfer_from_contract_to_account(amount, self.address, to) { Ok(new_balance) => v1::InvokeResponse::Success { @@ -1147,11 +1142,11 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { success, }); - self.invocation_handler.remaining_energy.tick_energy( + invocation_handler.remaining_energy.tick_energy( concordium_base::transactions::cost::SIMPLE_TRANSFER, )?; - let resume_res = self.invocation_handler.run_interpreter(|energy| { + let resume_res = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1177,7 +1172,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { self.trace_elements.push(interrupt_event); if state_changed { - self.invocation_handler.save_state_changes( + invocation_handler.save_state_changes( self.address, &mut self.state, true, @@ -1186,14 +1181,13 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { // Save the modification index before the invoke. let mod_idx_before_invoke = - self.invocation_handler.modification_index(self.address); + invocation_handler.modification_index(self.address); // Make a checkpoint before calling another contract so that we may roll // back. - self.invocation_handler.checkpoint(); + invocation_handler.checkpoint(); - let (success, invoke_response) = match self - .invocation_handler + let (success, invoke_response) = match invocation_handler .chain .contracts .get(&address) @@ -1212,7 +1206,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { name.as_entrypoint_name(), ); let message = OwnedParameter::new_unchecked(parameter); - let res = self.invocation_handler.invoke_entrypoint( + let res = invocation_handler.invoke_entrypoint( self.invoker, Address::Contract(self.address), UpdateContractPayload { @@ -1227,12 +1221,9 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { v1::InvokeResponse::Success { data, .. } => { let invoke_response = v1::InvokeResponse::Success { // The balance returned by `invoke_entrypoint` is - // the - // balance of the contract called. But we are - // interested - // in the new balance of the caller. - new_balance: self - .invocation_handler + // the balance of the contract called. But we are + // interested in the new balance of the caller. + new_balance: invocation_handler .contract_balance_unchecked(self.address), data, }; @@ -1245,18 +1236,17 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { // Remove the last state changes if the invocation failed. let state_changed = if !success { - self.invocation_handler.rollback(); + invocation_handler.rollback(); false // We rolled back, so no changes were made // to this contract. } else { let mod_idx_after_invoke = - self.invocation_handler.modification_index(self.address); + invocation_handler.modification_index(self.address); let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; if state_changed { // Update the state field with the newest value from the // changeset. - self.state = - self.invocation_handler.contract_state(self.address); + self.state = invocation_handler.contract_state(self.address); } state_changed }; @@ -1269,7 +1259,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { self.trace_elements.push(resume_event); - let resume_res = self.invocation_handler.run_interpreter(|energy| { + let resume_res = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, invoke_response, @@ -1287,55 +1277,52 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { self.trace_elements.push(interrupt_event); // Charge a base cost. - self.invocation_handler + invocation_handler .remaining_energy .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; - let response = - match self.invocation_handler.chain.modules.get(&module_ref) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidModuleRef, - }, - Some(module) => { - // Charge for the module lookup. - self.invocation_handler - .remaining_energy - .tick_energy(lookup_module_cost(module))?; - - if module.artifact.export.contains_key( - self.contract_name.as_contract_name().get_chain_name(), - ) { - // Update module reference in the changeset. - let old_module_ref = self - .invocation_handler - .save_module_upgrade(self.address, module_ref); - - // Charge for the initialization cost. - self.invocation_handler.remaining_energy.tick_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - )?; - - let upgrade_event = ContractTraceElement::Upgraded { - address: self.address, - from: old_module_ref, - to: module_ref, - }; - - self.trace_elements.push(upgrade_event); - - v1::InvokeResponse::Success { - new_balance: self - .invocation_handler - .contract_balance_unchecked(self.address), - data: None, - } - } else { - v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidContractName, - } + let response = match invocation_handler.chain.modules.get(&module_ref) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(module) => { + // Charge for the module lookup. + invocation_handler + .remaining_energy + .tick_energy(lookup_module_cost(module))?; + + if module.artifact.export.contains_key( + self.contract_name.as_contract_name().get_chain_name(), + ) { + // Update module reference in the changeset. + let old_module_ref = invocation_handler + .save_module_upgrade(self.address, module_ref); + + // Charge for the initialization cost. + invocation_handler.remaining_energy.tick_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + )?; + + let upgrade_event = ContractTraceElement::Upgraded { + address: self.address, + from: old_module_ref, + to: module_ref, + }; + + self.trace_elements.push(upgrade_event); + + v1::InvokeResponse::Success { + new_balance: invocation_handler + .contract_balance_unchecked(self.address), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, } } - }; + } + }; let success = matches!(response, v1::InvokeResponse::Success { .. }); self.trace_elements.push(ContractTraceElement::Resumed { @@ -1343,7 +1330,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { success, }); - let resume_res = self.invocation_handler.run_interpreter(|energy| { + let resume_res = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1356,10 +1343,9 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { receive_result = resume_res; } v1::Interrupt::QueryAccountBalance { address } => { - let response = match self.invocation_handler.account_balance(address) { + let response = match invocation_handler.account_balance(address) { Some(balance) => v1::InvokeResponse::Success { - new_balance: self - .invocation_handler + new_balance: invocation_handler .contract_balance_unchecked(self.address), data: Some(to_bytes(&balance)), }, @@ -1368,11 +1354,11 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { }, }; - self.invocation_handler.remaining_energy.tick_energy( + invocation_handler.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, )?; - receive_result = self.invocation_handler.run_interpreter(|energy| { + receive_result = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1384,25 +1370,24 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { })?; } v1::Interrupt::QueryContractBalance { address } => { - let response = match self.invocation_handler.contract_balance(address) { + let response = match invocation_handler.contract_balance(address) { None => v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, Some(bal) => v1::InvokeResponse::Success { // Balance of contract querying. Won't change. Notice the // `self.address`. - new_balance: self - .invocation_handler + new_balance: invocation_handler .contract_balance_unchecked(self.address), data: Some(to_bytes(&bal)), }, }; - self.invocation_handler.remaining_energy.tick_energy( + invocation_handler.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, )?; - receive_result = self.invocation_handler.run_interpreter(|energy| { + receive_result = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1415,30 +1400,27 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { } v1::Interrupt::QueryExchangeRates => { let exchange_rates = ExchangeRates { - euro_per_energy: self - .invocation_handler + euro_per_energy: invocation_handler .chain .parameters .euro_per_energy, - micro_ccd_per_euro: self - .invocation_handler + micro_ccd_per_euro: invocation_handler .chain .parameters .micro_ccd_per_euro, }; let response = v1::InvokeResponse::Success { - new_balance: self - .invocation_handler + new_balance: invocation_handler .contract_balance_unchecked(self.address), data: Some(to_bytes(&exchange_rates)), }; - self.invocation_handler.remaining_energy.tick_energy( + invocation_handler.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, )?; - receive_result = self.invocation_handler.run_interpreter(|energy| { + receive_result = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, response, @@ -1456,7 +1438,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { return_value, remaining_energy, } => { - self.invocation_handler.update_energy(remaining_energy); + invocation_handler.update_energy(remaining_energy); return Ok(v1::ReceiveResult::Reject { reason, return_value, @@ -1467,7 +1449,7 @@ impl<'a, 'b, 'c> InvocationData<'a, 'b, 'c> { error, remaining_energy, } => { - self.invocation_handler.update_energy(remaining_energy); + invocation_handler.update_energy(remaining_energy); return Ok(v1::ReceiveResult::Trap { error, remaining_energy, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 7e242ba0..cdbee425 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -96,28 +96,25 @@ pub(super) struct ContractChanges { /// /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. -pub(super) struct InvocationData<'a, 'b, 'c> { +pub(super) struct InvocationData { /// The invoker. - pub(super) invoker: AccountAddress, + pub(super) invoker: AccountAddress, /// The sender. - pub(super) sender: Address, + pub(super) sender: Address, /// The contract being called. - pub(super) address: ContractAddress, + pub(super) address: ContractAddress, /// The name of the contract. - pub(super) contract_name: OwnedContractName, + pub(super) contract_name: OwnedContractName, /// The entrypoint to execute. - pub(super) entrypoint: OwnedEntrypointName, + pub(super) entrypoint: OwnedEntrypointName, /// The amount sent from the sender to the contract. - pub(super) amount: Amount, + pub(super) amount: Amount, /// The parameter given to the entrypoint. - pub(super) parameter: OwnedParameter, - /// A reference to the [`EntrypointInvocationHandler`], which is used to for - /// handling interrupts and for querying chain data. - pub(super) invocation_handler: &'c mut EntrypointInvocationHandler<'a, 'b>, + pub(super) parameter: OwnedParameter, /// The current state. - pub(super) state: MutableState, + pub(super) state: MutableState, /// Trace elements that have occurred during the execution. - pub(super) trace_elements: Vec, + pub(super) trace_elements: Vec, } /// A positive or negative delta in for an [`Amount`]. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 92d9ea05..b6e3f73a 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -56,7 +56,7 @@ pub struct Chain { /// A smart contract instance. #[derive(Clone, Debug)] pub struct Contract { - pub address: ContractAddress, + pub address: ContractAddress, /// The module which contains this contract. pub module_reference: ModuleReference, /// The name of the contract. From 18b7766e14fe9d0fcf0bf5c583dd0d44e3d9222b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 30 Mar 2023 22:14:25 +0200 Subject: [PATCH 135/208] Remove result post-processing in invoke_entrypoint. --- contract-testing/src/invocation/impls.rs | 88 +++++++----------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 211bee3c..0d0c92c7 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -41,7 +41,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - trace_elements: &mut Vec, + trace_elements: &mut Vec, /* TODO: Are trace elements + * appropriately rolled back? */ ) -> Result { // Charge the base cost for updating a contract. self.remaining_energy @@ -218,52 +219,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let result = data.process(self, initial_result)?; let mut new_trace_elements = data.trace_elements; - let result = match result { - v1::ReceiveResult::Success { - logs, - state_changed: _, /* This only reflects changes since last interrupt, we use - * the changeset later to get a more precise result. */ - return_value, - remaining_energy: _, - } => InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Success { - new_balance: self.contract_balance_unchecked(payload.address), - data: Some(return_value), - }, - logs, - }, - v1::ReceiveResult::Interrupt { .. } => { - panic!("Internal error: `data.process` returned an interrupt.") - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy: _, - } => InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, - }, - logs: v0::Logs::new(), - }, - v1::ReceiveResult::Trap { - error: _, /* TODO: Forward to the user inside the `InvokeFailure::RuntimeError` - * once the field has been added. */ - remaining_energy: _, - } => InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - logs: v0::Logs::new(), - }, - v1::ReceiveResult::OutOfEnergy => { - // Convert to an error so that we will short-circuit the processing. - return Err(TestConfigurationError::OutOfEnergy); - } - }; - // Append the new trace elements if the invocation succeeded. if result.is_success() { trace_elements.append(&mut new_trace_elements); @@ -1037,7 +992,7 @@ impl InvocationData { &mut self, invocation_handler: &mut EntrypointInvocationHandler, mut receive_result: v1::ReceiveResult, - ) -> Result, TestConfigurationError> { + ) -> Result { loop { match receive_result { v1::ReceiveResult::Success { @@ -1071,11 +1026,13 @@ impl InvocationData { invocation_handler.save_state_changes(self.address, &mut self.state, false); } - return Ok(v1::ReceiveResult::Success { + return Ok(InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Success { + new_balance: invocation_handler + .contract_balance_unchecked(self.address), + data: Some(return_value), + }, logs, - state_changed, - return_value, - remaining_energy, }); } v1::ReceiveResult::Interrupt { @@ -1236,6 +1193,7 @@ impl InvocationData { // Remove the last state changes if the invocation failed. let state_changed = if !success { + // TODO: we should likely need to roll back traces as well. invocation_handler.rollback(); false // We rolled back, so no changes were made // to this contract. @@ -1259,7 +1217,7 @@ impl InvocationData { self.trace_elements.push(resume_event); - let resume_res = invocation_handler.run_interpreter(|energy| { + receive_result = invocation_handler.run_interpreter(|energy| { v1::resume_receive( config, invoke_response, @@ -1269,8 +1227,6 @@ impl InvocationData { v1::trie::Loader::new(&[][..]), ) })?; - - receive_result = resume_res; } v1::Interrupt::Upgrade { module_ref } => { // Add the interrupt event. @@ -1439,20 +1395,26 @@ impl InvocationData { remaining_energy, } => { invocation_handler.update_energy(remaining_energy); - return Ok(v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, + return Ok(InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + logs: v0::Logs::new(), }); } v1::ReceiveResult::Trap { - error, + error: _, // FIXME: This would ideally be propagated to the caller. remaining_energy, } => { invocation_handler.update_energy(remaining_energy); - return Ok(v1::ReceiveResult::Trap { - error, - remaining_energy, + return Ok(InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: v0::Logs::new(), }); } // Convert this to an error here, so that we will short circuit processing. From 7be87e85eb4eabcc2b2727dd983a58f060fcfc47 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 31 Mar 2023 15:39:26 +0200 Subject: [PATCH 136/208] Apply ceiling to energy to amount calculation to match node --- contract-testing/src/impls.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 33186e80..72cf356c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1258,12 +1258,11 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// NRG euro NRG * euro NRG /// ``` /// -/// To convert the `energy` parameter to mCCD: -/// ```markdown -/// -/// mCCD NRG * mCCD -/// NRG * ---- = ---------- = mCCD -/// NRG NRG +/// To convert the `energy` parameter to mCCD (the vertical lines represent +/// ceiling): ```markdown +/// ⌈ mCCD ⌉ ⌈ NRG * mCCD ⌉ +/// | NRG * ---- | = | ---------- | = mCCD +/// | NRG | | NRG | /// ``` pub fn energy_to_amount( energy: Energy, @@ -1274,12 +1273,18 @@ pub fn energy_to_amount( BigUint::from(euro_per_energy.numerator()) * micro_ccd_per_euro.numerator(); let micro_ccd_per_energy_denominator: BigUint = BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); + let remainder: BigUint = (micro_ccd_per_energy_numerator.clone() * energy.energy) + % micro_ccd_per_energy_denominator.clone(); let cost: BigUint = (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; - let cost: u64 = u64::try_from(cost).expect( + let mut cost: u64 = u64::try_from(cost).expect( "Should never overflow since reasonable exchange rates are ensured when constructing the \ [`Chain`].", ); + // Apply ceiling. + if remainder != 0u8.into() { + cost += 1 + } Amount::from_micro_ccd(cost) } From cccfaced20b867b4711097979f88b43923bb22a9 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 31 Mar 2023 15:39:56 +0200 Subject: [PATCH 137/208] Remove redundant tests, use wat fib and check energy and amounts --- contract-testing/tests/basics.rs | 405 ++++--------------------------- 1 file changed, 48 insertions(+), 357 deletions(-) diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index bdd51f27..39c4ece1 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -1,373 +1,47 @@ -//! This module contains tests for the basic functionality of the testing -//! library. +//! This module contains tests that test various basic things, such as state +//! reentry and energy usage and amounts charged. use concordium_smart_contract_testing::*; const ACC_0: AccountAddress = AccountAddress([0; 32]); -const ACC_1: AccountAddress = AccountAddress([1; 32]); +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; #[test] -fn deploying_valid_module_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); +fn fib_reentry_and_cost_test() { + let mut chain = Chain::new_with_time_and_rates( + SlotTime::from_timestamp_millis(0), + // Set a specific value, taken from testnet, to compare the exact amounts charged. + ExchangeRate::new_unchecked(3127635127520773120, 24857286553), + ExchangeRate::new_unchecked(1, 50000), + ) + .expect("Values known to be in range."); + + let initial_balance = Amount::from_ccd(100_000); chain.create_account(Account::new(ACC_0, initial_balance)); - let res = chain + let deployment = chain .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1( - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", - ) - .expect("module should exist"), - ) - .expect("Deploying valid module should work."); - - assert!(chain.get_module(res.module_reference).is_some()); - assert_eq!( - chain.account_balance_available(ACC_0), - Some(initial_balance - res.transaction_fee) - ); -} - -#[test] -fn initializing_valid_contract_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1( - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", - ) - .expect("module should exist"), + Chain::module_load_v1_raw(format!("{}/fib.wasm", WASM_TEST_FOLDER)) + .expect("Module should exist."), ) .expect("Deploying valid module should work"); - let res_init = chain + let init = chain .contract_init( Signer::with_one_key(), ACC_0, Energy::from(10000), InitContractPayload { amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]).expect("Parameter has valid size."), - }, - ) - .expect("Initializing valid contract should work"); - assert_eq!( - chain.account_balance_available(ACC_0), - Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) - ); - assert!(chain.get_contract(ContractAddress::new(0, 0)).is_some()); -} - -#[test] -fn initializing_with_invalid_parameter_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1( - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", - ) - .expect("module should exist"), - ) - .expect("Deploying valid module should work"); - - let res_init = - chain - .contract_init( - Signer::with_one_key(), - ACC_0, - Energy::from(10000), - InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![99u8]) - .expect("Parameter has valid size."), // Invalid param - }, - ) - .expect_err("Initializing with invalid params should fail"); - - let transaction_fee = res_init.transaction_fee; - match res_init.kind { - // Failed in the right way and account is still charged. - ContractInitErrorKind::ExecutionError { - error: InitExecutionError::Reject { .. }, - } => assert_eq!( - chain.account_balance_available(ACC_0), - Some(initial_balance - res_deploy.transaction_fee - transaction_fee) - ), - _ => panic!("Expected valid chain error."), - }; -} - -#[test] -fn updating_valid_contract_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1( - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", - ) - .expect("module should exist"), - ) - .expect("Deploying valid module should work"); - - let res_init = - chain - .contract_init( - Signer::with_one_key(), - ACC_0, - Energy::from(10000), - InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]) - .expect("Parameter has valid size."), // Starts as 0 - }, - ) - .expect("Initializing valid contract should work"); - - let res_update = chain - .contract_update( - Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.set".into()), - message: OwnedParameter::try_from(vec![1u8]) - .expect("Parameter has valid size."), // Updated to 1 - }, - ) - .expect("Updating valid contract should work"); - - let res_invoke_get = chain - .contract_invoke( - ACC_0, - Address::Contract(res_init.contract_address), // Invoke with a contract as sender. - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), - message: OwnedParameter::empty(), - }, - ) - .expect("Invoking get should work"); - - // This also asserts that the account wasn't charged for the invoke. - assert_eq!( - chain.account_balance_available(ACC_0), - Some( - initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee - ) - ); - assert!(chain.get_contract(res_init.contract_address).is_some()); - assert!(res_update.state_changed); - // Assert that the updated state is persisted. - assert_eq!(res_invoke_get.return_value, [1u8]); -} - -/// Test that updates and invocations where the sender is missing fail. -#[test] -fn updating_and_invoking_with_missing_sender_fails() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let missing_account = Address::Account(ACC_1); - let missing_contract = Address::Contract(ContractAddress::new(100, 0)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1( - "../../concordium-rust-smart-contracts/examples/icecream/a.wasm.v1", - ) - .expect("module should exist"), - ) - .expect("Deploying valid module should work"); - - let res_init = - chain - .contract_init( - Signer::with_one_key(), - ACC_0, - Energy::from(10000), - InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - init_name: OwnedContractName::new_unchecked("init_weather".into()), - param: OwnedParameter::try_from(vec![0u8]) - .expect("Parameter has valid size."), // Starts as 0 - }, - ) - .expect("Initializing valid contract should work"); - - let res_update_acc = chain - .contract_update( - Signer::with_one_key(), - ACC_0, - missing_account, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("should fail"); - - let res_invoke_acc = chain - .contract_invoke( - ACC_0, - missing_account, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("should fail"); - - let res_update_contr = chain - .contract_update( - Signer::with_one_key(), - ACC_0, - missing_contract, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("should fail"); - - let res_invoke_contr = chain - .contract_invoke( - ACC_0, - missing_contract, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: res_init.contract_address, - receive_name: OwnedReceiveName::new_unchecked("weather.get".into()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("should fail"); - - assert!(matches!( - res_update_acc.kind, - ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); - assert!(matches!( - res_invoke_acc.kind, - ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_account)); - assert!(matches!( - res_update_contr.kind, - ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); - assert!(matches!( - res_invoke_contr.kind, - ContractInvokeErrorKind::SenderDoesNotExist(addr) if addr == missing_contract)); -} - -#[test] -fn init_with_less_energy_than_module_lookup() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") - .expect("module should exist"), - ) - .expect("Deploying valid module should work"); - - let reserved_energy = Energy::from(10); - - let res_init = chain.contract_init( - Signer::with_one_key(), - ACC_0, - reserved_energy, - InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, - - init_name: OwnedContractName::new_unchecked("init_fib".into()), - param: OwnedParameter::empty(), - }, - ); - match res_init { - Err(ContractInitError { - kind: ContractInitErrorKind::OutOfEnergy, - .. - }) => (), - _ => panic!("Expected to fail with out of energy."), - } -} - -#[test] -fn update_with_fib_reentry_works() { - let mut chain = Chain::new(); - let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - - let res_deploy = chain - .module_deploy_v1( - Signer::with_one_key(), - ACC_0, - Chain::module_load_v1("../../concordium-rust-smart-contracts/examples/fib/a.wasm.v1") - .expect("module should exist"), - ) - .expect("Deploying valid module should work"); - - let res_init = chain - .contract_init( - Signer::with_one_key(), - ACC_0, - Energy::from(10000), - InitContractPayload { - amount: Amount::zero(), - mod_ref: res_deploy.module_reference, + mod_ref: deployment.module_reference, init_name: OwnedContractName::new_unchecked("init_fib".into()), param: OwnedParameter::empty(), }, ) .expect("Initializing valid contract should work"); - let res_update = chain + let update = chain .contract_update( Signer::with_one_key(), ACC_0, @@ -375,41 +49,58 @@ fn update_with_fib_reentry_works() { Energy::from(100000), UpdateContractPayload { amount: Amount::zero(), - address: res_init.contract_address, + address: init.contract_address, receive_name: OwnedReceiveName::new_unchecked("fib.receive".into()), message: OwnedParameter::from_serial(&6u64).expect("Parameter has valid size"), }, ) .expect("Updating valid contract should work"); - let res_view = chain + let view = chain .contract_invoke( ACC_0, Address::Account(ACC_0), Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), - address: res_init.contract_address, + address: init.contract_address, receive_name: OwnedReceiveName::new_unchecked("fib.view".into()), message: OwnedParameter::empty(), }, ) .expect("Invoking get should work"); + // Check the state and return values. + assert!(chain.get_contract(init.contract_address).is_some()); + assert!(update.state_changed); + let expected_res = u64::to_le_bytes(13); + assert_eq!(update.return_value, expected_res); + // Assert that the updated state is persisted. + assert_eq!(view.return_value, expected_res); + + // Check that the account was correctly charged for all transactions. // This also asserts that the account wasn't charged for the invoke. assert_eq!( chain.account_balance_available(ACC_0), Some( initial_balance - - res_deploy.transaction_fee - - res_init.transaction_fee - - res_update.transaction_fee + - deployment.transaction_fee + - init.transaction_fee + - update.transaction_fee ) ); - assert!(chain.get_contract(res_init.contract_address).is_some()); - assert!(res_update.state_changed); - let expected_res = u64::to_le_bytes(13); - assert_eq!(res_update.return_value, expected_res); - // Assert that the updated state is persisted. - assert_eq!(res_view.return_value, expected_res); + + // Check that the energy usage matches the node. + assert_eq!(deployment.energy_used, 1067.into()); + assert_eq!(init.energy_used, 771.into()); + assert_eq!(update.energy_used, 8198.into()); + assert_eq!(view.energy_used, 316.into()); + + // Check that the amounts charged matches the node. + assert_eq!( + deployment.transaction_fee, + Amount::from_micro_ccd(2_685_078) + ); + assert_eq!(init.transaction_fee, Amount::from_micro_ccd(1_940_202)); + assert_eq!(update.transaction_fee, Amount::from_micro_ccd(20_630_050)); } From 564957878835a3c50b5f3bbb10ca3932651ea399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sat, 8 Apr 2023 21:10:47 +0200 Subject: [PATCH 138/208] Remove mutual recursion by inlining process. This is in anticipation of the next step of removing recursion. --- contract-testing/src/invocation/impls.rs | 2247 +++++++++++----------- contract-testing/src/invocation/types.rs | 2 - 2 files changed, 1129 insertions(+), 1120 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 0d0c92c7..32bb1659 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -24,26 +24,19 @@ use concordium_smart_contract_engine::{ v1::{self, trie}, ExecResult, InterpreterEnergy, }; -use concordium_wasm::artifact; +use concordium_wasm::artifact::{self, CompiledFunction}; use std::collections::{btree_map, BTreeMap}; impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { - /// Used for handling contract entrypoint invocations internally. - /// - /// **Preconditions:** - /// - `invoker` exists - /// - `invoker` has sufficient balance to pay for `remaining_energy` - /// - `sender` exists - /// - if the contract (`contract_address`) exists, then its `module` must - /// also exist. - pub(crate) fn invoke_entrypoint( + fn invoke_entrypoint_initial( &mut self, invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - trace_elements: &mut Vec, /* TODO: Are trace elements - * appropriately rolled back? */ - ) -> Result { + ) -> Result< + Result<(v1::ReceiveResult, InvocationData), InvokeEntrypointResponse>, + TestConfigurationError, + > { // Charge the base cost for updating a contract. self.remaining_energy .tick_energy(constants::UPDATE_CONTRACT_INSTANCE_BASE_COST)?; @@ -79,10 +72,10 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. - return Ok(InvokeEntrypointResponse { + return Ok(Err(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind }, logs: v0::Logs::new(), - }); + })); } } } else { @@ -90,12 +83,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { Some(self_balance) => self_balance, None => { // Return early. - return Ok(InvokeEntrypointResponse { + return Ok(Err(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, logs: v0::Logs::new(), - }); + })); } } }; @@ -136,12 +129,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { ) } else { // Return early. - return Ok(InvokeEntrypointResponse { + return Ok(Err(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, logs: v0::Logs::new(), - }); + })); } }; @@ -203,8 +196,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. - let mut data = InvocationData { - invoker, + Ok(Ok((initial_result, InvocationData { sender, address: payload.address, contract_name, @@ -213,1214 +205,1233 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { amount: payload.amount, state: mutable_state, trace_elements: Vec::new(), - }; - - // Process the receive invocation to the completion. - let result = data.process(self, initial_result)?; - let mut new_trace_elements = data.trace_elements; - - // Append the new trace elements if the invocation succeeded. - if result.is_success() { - trace_elements.append(&mut new_trace_elements); - } - - Ok(result) + }))) } - /// Make a transfer from a contract to an account in the changeset. - /// - /// Returns the new balance of `from`. + /// Used for handling contract entrypoint invocations internally. /// /// **Preconditions:** - /// - Assumes that `from` contract exists. - fn transfer_from_contract_to_account( + /// - `invoker` exists + /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// - `sender` exists + /// - if the contract (`contract_address`) exists, then its `module` must + /// also exist. + pub(crate) fn invoke_entrypoint( &mut self, - amount: Amount, - from: ContractAddress, - to: AccountAddress, - ) -> Result { - // Ensure the `to` account exists. - if !self.chain.accounts.contains_key(&to.into()) { - return Err(TransferError::ToMissing); - } + invoker: AccountAddress, + sender: Address, + payload: UpdateContractPayload, + trace_elements: &mut Vec, /* TODO: Are trace elements + * appropriately rolled back? */ + ) -> Result { + let (mut receive_result, mut invocation_data) = + match self.invoke_entrypoint_initial(invoker, sender, payload)? { + Ok(x) => x, + Err(ier) => return Ok(ier), + }; - // Make the transfer. - let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; - self.change_account_balance(to, AmountDelta::Positive(amount)) - .expect("Cannot fail when adding"); + // Process the receive invocation to the completion. + let result = 'result: loop { + match receive_result { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => { + // Update the remaining_energy field. + self.update_energy(remaining_energy); - Ok(new_balance) - } + let update_event = ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + contract_version: WasmVersion::V1, + address: invocation_data.address, + instigator: invocation_data.sender, + amount: invocation_data.amount, + message: invocation_data.parameter.clone(), + receive_name: OwnedReceiveName::construct_unchecked( + invocation_data.contract_name.as_contract_name(), + invocation_data.entrypoint.as_entrypoint_name(), + ), + events: contract_events_from_logs(logs.clone()), + }, + }; + // Add update event + invocation_data.trace_elements.push(update_event); - /// Make a transfer between contracts in the changeset. - /// - /// Returns the new balance of `to`. - /// - /// **Preconditions:** - /// - Assumes that `from` contract exists. - fn transfer_from_contract_to_contract( - &mut self, - amount: Amount, - from: ContractAddress, - to: ContractAddress, - ) -> Result { - // Ensure the `to` contract exists. - if !self.chain.contracts.contains_key(&to) { - return Err(TransferError::ToMissing); - } + // Save changes to changeset. + if state_changed { + self.save_state_changes( + invocation_data.address, + &mut invocation_data.state, + false, + ); + } - // Make the transfer. - self.change_contract_balance(from, AmountDelta::Negative(amount))?; - let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; - Ok(new_balance) - } + break 'result InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Success { + new_balance: self.contract_balance_unchecked(invocation_data.address), + data: Some(return_value), + }, + logs, + }; + } + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => { + // Update the remaining_energy field. + self.update_energy(remaining_energy); + // Create the interrupt event, which will be included for transfers, calls, and + // upgrades, but not for the remaining interrupts. + let interrupt_event = ContractTraceElement::Interrupted { + address: invocation_data.address, + events: contract_events_from_logs(logs), + }; + match interrupt { + v1::Interrupt::Transfer { to, amount } => { + // Add the interrupt event + invocation_data.trace_elements.push(interrupt_event); - /// Make a transfer from an account to a contract in the changeset. - /// - /// Returns the new balance of `to`. - /// - /// **Preconditions:** - /// - Assumes that `from` account exists. - fn transfer_from_account_to_contract( - &mut self, - amount: Amount, - from: AccountAddress, - to: ContractAddress, - ) -> Result { - // Ensure the `to` account exists. - if !self.chain.contracts.contains_key(&to) { - return Err(TransferError::ToMissing); - } + let response = match self.transfer_from_contract_to_account( + amount, + invocation_data.address, + to, + ) { + Ok(new_balance) => v1::InvokeResponse::Success { + new_balance, + data: None, + }, + Err(err) => { + let kind = match err { + TransferError::ToMissing => { + v1::InvokeFailure::NonExistentAccount + } - // Make the transfer. - self.change_account_balance(from, AmountDelta::Negative(amount))?; - let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; - Ok(new_balance) - } + TransferError::BalanceError { + error: BalanceError::Insufficient, + } => v1::InvokeFailure::InsufficientAmount, - /// Changes the contract balance in the topmost checkpoint on the changeset. - /// - /// If contract isn't already present in the changeset, it is added. - /// - /// Returns an error if the change in balance would go below `0`, which is a - /// valid error, or if the amounts would overflow, which is an unrecoverable - /// configuration error in the tests. - /// Otherwise, it returns the new balance of the contract. - /// - /// The precondition is not part of the error type, as this is an internal - /// helper function that is only called when the precondition is met. - /// - /// Returns the new balance. - /// - /// **Preconditions:** - /// - Contract must exist. - fn change_contract_balance( - &mut self, - address: ContractAddress, - delta: AmountDelta, - ) -> Result { - match self.changeset.current_mut().contracts.entry(address) { - btree_map::Entry::Vacant(vac) => { - // get original balance - let original_balance = self - .chain - .contracts - .get(&address) - .expect("Precondition violation: contract assumed to exist") - .self_balance; - // Try to apply the balance or return an error if insufficient funds. - let new_contract_balance = delta.apply_to_balance(original_balance)?; - // Insert the changes into the changeset. - vac.insert(ContractChanges { - self_balance_delta: delta, - ..ContractChanges::new(original_balance) - }); - Ok(new_contract_balance) - } - btree_map::Entry::Occupied(mut occ) => { - let contract_changes = occ.get_mut(); - let new_delta = contract_changes.self_balance_delta.add_delta(delta); - // Try to apply the balance or return an error if insufficient funds. - let new_contract_balance = - new_delta.apply_to_balance(contract_changes.self_balance_original)?; - contract_changes.self_balance_delta = new_delta; - Ok(new_contract_balance) - } - } - } + TransferError::BalanceError { + error: BalanceError::Overflow, + } => { + // Balance overflows are unrecoverable and short + // circuit. + return Err(TestConfigurationError::BalanceOverflow); + } + }; + v1::InvokeResponse::Failure { kind } + } + }; - /// Changes the account balance in the topmost checkpoint on the changeset. - /// - /// If account isn't already present in the changeset, it is added. - /// - /// Returns an error if a negative delta is provided which exceeds the - /// available balance of the account. Otherwise, it returns the new - /// available balance of the account. - /// Otherwise, it returns the new balance of the account. - /// - /// The precondition is not part of the error type, as this is an internal - /// helper function that is only called when the precondition is met. - /// - /// **Preconditions:** - /// - Account must exist. - fn change_account_balance( - &mut self, - address: AccountAddress, - delta: AmountDelta, - ) -> Result { - match self.changeset.current_mut().accounts.entry(address.into()) { - btree_map::Entry::Vacant(vac) => { - // get original balance - let mut original_balance = self - .chain - .accounts - .get(&address.into()) - .expect("Precondition violation: account assumed to exist") - .balance - .available(); - if self.invoker == address { - // It has been checked that the invoker account has sufficient balance for - // paying. - original_balance -= self.reserved_amount; - } - // Try to apply the balance or return an error if insufficient funds. - let new_account_balance = delta.apply_to_balance(original_balance)?; - // Insert the changes into the changeset. - vac.insert(AccountChanges { - original_balance, - balance_delta: delta, - }); - Ok(new_account_balance) - } - btree_map::Entry::Occupied(mut occ) => { - let account_changes = occ.get_mut(); - let new_delta = account_changes.balance_delta.add_delta(delta); - // Try to apply the balance or return an error if insufficient funds. - let new_account_balance = - new_delta.apply_to_balance(account_changes.original_balance)?; - account_changes.balance_delta = new_delta; - Ok(new_account_balance) - } - } - } + let success = matches!(response, v1::InvokeResponse::Success { .. }); + if success { + // Add transfer event + invocation_data.trace_elements.push( + ContractTraceElement::Transferred { + from: invocation_data.address, + amount, + to, + }, + ); + } + // Add resume event + invocation_data + .trace_elements + .push(ContractTraceElement::Resumed { + address: invocation_data.address, + success, + }); - /// Returns the contract balance from the topmost checkpoint on the - /// changeset. Or, alternatively, from persistence. - /// - /// **Preconditions:** - /// - Contract must exist. - fn contract_balance_unchecked(&self, address: ContractAddress) -> Amount { - self.contract_balance(address) - .expect("Precondition violation: contract must exist") - } + self.remaining_energy.tick_energy( + concordium_base::transactions::cost::SIMPLE_TRANSFER, + )?; - /// Looks up the contract balance from the topmost checkpoint on the - /// changeset. Or, alternatively, from persistence. - fn contract_balance(&self, address: ContractAddress) -> Option { - match self.changeset.current().contracts.get(&address) { - Some(changes) => Some(changes.current_balance()), - None => self.chain.contracts.get(&address).map(|c| c.self_balance), - } - } + let resume_res = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut invocation_data.state, + false, // never changes on transfers + // An empty loader is fine currently, as we do not use caching + // in this lib. + v1::trie::Loader::new(&[][..]), + ) + })?; - /// Returns the contract module from the topmost checkpoint on - /// the changeset. Or, alternatively, from persistence. - /// - /// **Preconditions:** - /// - Contract instance must exist (and therefore also the artifact). - /// - If the changeset contains a module reference, then it must refer a - /// deployed module. - fn contract_module(&self, contract: &Contract) -> ContractModule { - match self - .changeset - .current() - .contracts - .get(&contract.address) - .and_then(|c| c.module) - { - // Contract has been upgrade, new module exists. - Some(new_module) => self - .chain - .modules - .get(&new_module) - .expect("Precondition violation: module must exist.") - .clone(), - // Contract hasn't been upgraded. Use persisted module. - None => self - .chain - .modules - .get(&contract.module_reference) - .expect("Precondition violation: module must exist.") - .clone(), - } - } + // Resume + receive_result = resume_res; + } + v1::Interrupt::Call { + address, + parameter, + name, + amount, + } => { + // Add the interrupt event + invocation_data.trace_elements.push(interrupt_event); - /// Get the contract state, either from the changeset or by thawing it from - /// persistence. - /// - /// **Preconditions:** - /// - Contract instance must exist. - fn contract_state(&self, address: ContractAddress) -> trie::MutableState { - match self - .changeset - .current() - .contracts - .get(&address) - .and_then(|c| c.state.clone()) - { - // Contract state has been modified. - Some(modified_state) => modified_state, - // Contract state hasn't been modified. Thaw from persistence. - None => self - .chain - .contracts - .get(&address) - .expect("Precondition violation: contract must exist") - .state - .thaw(), - } - } + if state_changed { + self.save_state_changes( + invocation_data.address, + &mut invocation_data.state, + true, + ); + } - /// Looks up the account balance for an account by first checking - /// the changeset, then the persisted values. - fn account_balance(&self, address: AccountAddress) -> Option { - let mut account_balance = self - .chain - .accounts - .get(&address.into()) - .map(|a| a.balance)?; - match self - .changeset - .current() - .accounts - .get(&address.into()) - .map(|a| a.current_balance()) - { - // Account exists in changeset. - // Return the staked and locked balances from persistence, as they can't change during - // entrypoint invocation. - Some(total) => Some(AccountBalance { - total, - staked: account_balance.staked, - locked: account_balance.locked, - }), - // Account doesn't exist in changeset. - None => { - if self.invoker == address { - account_balance.total -= self.reserved_amount; - } - Some(account_balance) - } - } - } + // Save the modification index before the invoke. + let mod_idx_before_invoke = + self.modification_index(invocation_data.address); - /// Saves a mutable state for a contract in the changeset. - /// - /// If `with_fresh_generation`, then it will use the - /// [`MutableState::make_fresh_generation`][make_fresh_generation] - /// function, otherwise it will make a clone. - /// - /// If the contract already has an entry in the changeset, the old state - /// will be replaced. Otherwise, the entry is created and the state is - /// added. - /// - /// This also increments the modification index. It will be set to 1 if the - /// contract has no entry in the changeset. - /// - /// **Preconditions:** - /// - Contract must exist. - /// - /// [make_fresh_generation]: trie::MutableState::make_fresh_generation - fn save_state_changes( - &mut self, - address: ContractAddress, - state: &mut trie::MutableState, - with_fresh_generation: bool, - ) { - let state = if with_fresh_generation { - let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. - state.make_fresh_generation(&mut loader) - } else { - state.clone() - }; - match self.changeset.current_mut().contracts.entry(address) { - btree_map::Entry::Vacant(vac) => { - let original_balance = self - .chain - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .self_balance; - vac.insert(ContractChanges { - state: Some(state), - modification_index: 1, // Increment from default, 0, to 1. - ..ContractChanges::new(original_balance) - }); - } - btree_map::Entry::Occupied(mut occ) => { - let changes = occ.get_mut(); - changes.state = Some(state); - changes.modification_index += 1; - } - } - } + // Make a checkpoint before calling another contract so that we may roll + // back. + self.checkpoint(); - /// Saves a new module reference for the contract in the changeset. - /// - /// If the contract already has an entry in the changeset, the old module is - /// replaced. Otherwise, the entry is created and the module is added. - /// - /// Returns the previous module, which is either the one from persistence, - /// or the most recent one from the changeset. - /// - /// **Preconditions:** - /// - Contract must exist. - /// - Module must exist. - fn save_module_upgrade( - &mut self, - address: ContractAddress, - module_reference: ModuleReference, - ) -> ModuleReference { - match self.changeset.current_mut().contracts.entry(address) { - btree_map::Entry::Vacant(vac) => { - let contract = self - .chain - .contracts - .get(&address) - .expect("Precondition violation: contract must exist."); - let old_module_ref = contract.module_reference; - let original_balance = contract.self_balance; - vac.insert(ContractChanges { - module: Some(module_reference), - ..ContractChanges::new(original_balance) - }); - old_module_ref - } - btree_map::Entry::Occupied(mut occ) => { - let changes = occ.get_mut(); - let old_module_ref = match changes.module { - Some(old_module) => old_module, - None => { - self.chain - .contracts - .get(&address) - .expect("Precondition violation: contract must exist.") - .module_reference - } - }; - changes.module = Some(module_reference); - old_module_ref - } - } - } + let (success, invoke_response) = match self + .chain + .contracts + .get(&address) + .map(|c| c.contract_name.as_contract_name()) + { + // The contract to call does not exist. + None => { + let invoke_response = v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }; + (false, invoke_response) + } + Some(contract_name) => { + let receive_name = OwnedReceiveName::construct_unchecked( + contract_name, + name.as_entrypoint_name(), + ); + let message = OwnedParameter::new_unchecked(parameter); + let res = self.invoke_entrypoint( + self.invoker, + Address::Contract(invocation_data.address), + UpdateContractPayload { + amount, + address, + receive_name, + message, + }, + &mut invocation_data.trace_elements, + )?; + match res.invoke_response { + v1::InvokeResponse::Success { data, .. } => { + let invoke_response = v1::InvokeResponse::Success { + // The balance returned by `invoke_entrypoint` is + // the balance of the contract called. But we are + // interested in the new balance of the caller. + new_balance: self.contract_balance_unchecked( + invocation_data.address, + ), + data, + }; + (true, invoke_response) + } + failure => (false, failure), + } + } + }; - /// Returns the modification index for a contract. - /// - /// It looks it up in the changeset, and if it isn't there, it will return - /// `0`. - fn modification_index(&self, address: ContractAddress) -> u32 { - self.changeset - .current() - .contracts - .get(&address) - .map_or(0, |c| c.modification_index) - } + // Remove the last state changes if the invocation failed. + let state_changed = if !success { + // TODO: we should likely need to roll back traces as well. + self.rollback(); + false // We rolled back, so no changes were made + // to this contract. + } else { + let mod_idx_after_invoke = + self.modification_index(invocation_data.address); + let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + invocation_data.state = + self.contract_state(invocation_data.address); + } + state_changed + }; - /// Makes a new checkpoint. - fn checkpoint(&mut self) { self.changeset.checkpoint(); } + // Add resume event + let resume_event = ContractTraceElement::Resumed { + address: invocation_data.address, + success, + }; - /// Roll back to the previous checkpoint. - fn rollback(&mut self) { self.changeset.rollback(); } + invocation_data.trace_elements.push(resume_event); - /// Update the `remaining_energy` field by converting the input to - /// [`InterpreterEnergy`] and then [`Energy`]. - fn update_energy(&mut self, remaining_energy: u64) { - *self.remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); - } + receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + invoke_response, + energy, + &mut invocation_data.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + })?; + } + v1::Interrupt::Upgrade { module_ref } => { + // Add the interrupt event. + invocation_data.trace_elements.push(interrupt_event); - /// Run the interpreter with the provided function and the - /// `self.remaining_energy`. - /// - /// This function ensures that the energy calculations is handled as in the - /// node. - fn run_interpreter( - &mut self, - f: F, - ) -> Result, InsufficientEnergy> - where - F: FnOnce(InterpreterEnergy) -> ExecResult>, - { - let available_interpreter_energy = to_interpreter_energy(*self.remaining_energy); - let res = match f(available_interpreter_energy) { - Ok(res) => res, - Err(err) => { - // An error occured in the interpreter and it doesn't return the remaining - // energy. We convert this to a trap and set the energy to the - // last known amount. - return Ok(v1::ReceiveResult::Trap { - error: err, - remaining_energy: available_interpreter_energy.energy, - }); + // Charge a base cost. + self.remaining_energy + .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; + + let response = match self.chain.modules.get(&module_ref) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidModuleRef, + }, + Some(module) => { + // Charge for the module lookup. + self.remaining_energy + .tick_energy(lookup_module_cost(module))?; + + if module.artifact.export.contains_key( + invocation_data + .contract_name + .as_contract_name() + .get_chain_name(), + ) { + // Update module reference in the changeset. + let old_module_ref = self.save_module_upgrade( + invocation_data.address, + module_ref, + ); + + // Charge for the initialization cost. + self.remaining_energy.tick_energy( + constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, + )?; + + let upgrade_event = ContractTraceElement::Upgraded { + address: invocation_data.address, + from: old_module_ref, + to: module_ref, + }; + + invocation_data.trace_elements.push(upgrade_event); + + v1::InvokeResponse::Success { + new_balance: self.contract_balance_unchecked( + invocation_data.address, + ), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::UpgradeInvalidContractName, + } + } + } + }; + + let success = matches!(response, v1::InvokeResponse::Success { .. }); + invocation_data + .trace_elements + .push(ContractTraceElement::Resumed { + address: invocation_data.address, + success, + }); + + let resume_res = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut invocation_data.state, + state_changed, + v1::trie::Loader::new(&[][..]), + ) + })?; + receive_result = resume_res; + } + v1::Interrupt::QueryAccountBalance { address } => { + let response = match self.account_balance(address) { + Some(balance) => v1::InvokeResponse::Success { + new_balance: self + .contract_balance_unchecked(invocation_data.address), + data: Some(to_bytes(&balance)), + }, + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + + self.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, + )?; + + receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut invocation_data.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } + v1::Interrupt::QueryContractBalance { address } => { + let response = match self.contract_balance(address) { + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + Some(bal) => v1::InvokeResponse::Success { + // Balance of contract querying. Won't change. Notice the + // `data.address`. + new_balance: self + .contract_balance_unchecked(invocation_data.address), + data: Some(to_bytes(&bal)), + }, + }; + + self.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, + )?; + + receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut invocation_data.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } + v1::Interrupt::QueryExchangeRates => { + let exchange_rates = ExchangeRates { + euro_per_energy: self.chain.parameters.euro_per_energy, + micro_ccd_per_euro: self.chain.parameters.micro_ccd_per_euro, + }; + + let response = v1::InvokeResponse::Success { + new_balance: self + .contract_balance_unchecked(invocation_data.address), + data: Some(to_bytes(&exchange_rates)), + }; + + self.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, + )?; + + receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut invocation_data.state, + false, // State never changes on queries. + v1::trie::Loader::new(&[][..]), + ) + })?; + } + } + } + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => { + self.update_energy(remaining_energy); + break 'result InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, + }, + }, + logs: v0::Logs::new(), + }; + } + v1::ReceiveResult::Trap { + error: _, // FIXME: This would ideally be propagated to the caller. + remaining_energy, + } => { + self.update_energy(remaining_energy); + break 'result InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, + }, + logs: v0::Logs::new(), + }; + } + // Convert this to an error here, so that we will short circuit processing. + v1::ReceiveResult::OutOfEnergy => return Err(TestConfigurationError::OutOfEnergy), } }; - let mut subtract_then_convert = |remaining_energy| -> Result { - let remaining_energy = InterpreterEnergy::from(remaining_energy); - // Using `saturating_sub` here should be ok since we should never be able to use - // more energy than what is available. - let used_energy = from_interpreter_energy( - available_interpreter_energy.saturating_sub(remaining_energy), - ); - self.remaining_energy.tick_energy(used_energy)?; - Ok(to_interpreter_energy(*self.remaining_energy).energy) - }; - match res { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => Ok(v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy: subtract_then_convert(remaining_energy)?, - }), - - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs, - config, - interrupt, - } => Ok(v1::ReceiveResult::Interrupt { - remaining_energy: subtract_then_convert(remaining_energy)?, - state_changed, - logs, - config, - interrupt, - }), - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => Ok(v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy: subtract_then_convert(remaining_energy)?, - }), - v1::ReceiveResult::Trap { - error, - remaining_energy, - } => Ok(v1::ReceiveResult::Trap { - error, - remaining_energy: subtract_then_convert(remaining_energy)?, - }), - // Convert this to an error so that we will short-circuit the processing. - v1::ReceiveResult::OutOfEnergy => Err(InsufficientEnergy), - } - } -} + let mut new_trace_elements = invocation_data.trace_elements; -impl ChangeSet { - /// Creates a new changeset with one empty [`Changes`] element on the - /// stack.. - pub(crate) fn new() -> Self { - Self { - stack: vec![Changes { - contracts: BTreeMap::new(), - accounts: BTreeMap::new(), - }], + // Append the new trace elements if the invocation succeeded. + if result.is_success() { + trace_elements.append(&mut new_trace_elements); } - } - /// Make a checkpoint by putting a clone of the top element onto the stack. - fn checkpoint(&mut self) { - let cloned_top_element = self.current().clone(); - self.stack.push(cloned_top_element); + Ok(result) } - /// Perform a rollback by popping the top element of the stack. - fn rollback(&mut self) { - self.stack - .pop() - .expect("Internal error: change set stack should never be empty."); - } + /// Make a transfer from a contract to an account in the changeset. + /// + /// Returns the new balance of `from`. + /// + /// **Preconditions:** + /// - Assumes that `from` contract exists. + fn transfer_from_contract_to_account( + &mut self, + amount: Amount, + from: ContractAddress, + to: AccountAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.chain.accounts.contains_key(&to.into()) { + return Err(TransferError::ToMissing); + } - /// Get an immutable reference the current (latest) checkpoint. - fn current(&self) -> &Changes { - self.stack - .last() - .expect("Internal error: change set stack should never be empty.") - } + // Make the transfer. + let new_balance = self.change_contract_balance(from, AmountDelta::Negative(amount))?; + self.change_account_balance(to, AmountDelta::Positive(amount)) + .expect("Cannot fail when adding"); - /// Get a mutable reference to the current (latest) checkpoint. - fn current_mut(&mut self) -> &mut Changes { - self.stack - .last_mut() - .expect("Internal error: change set stack should never be empty.") + Ok(new_balance) } - /// Try to persist all changes from the changeset. - /// - /// If the energy needed for storing extra state is larger than the - /// `remaining_energy`, then: - /// - no changes will be persisted, - /// - an [`OutOfEnergy`] error is returned. + /// Make a transfer between contracts in the changeset. /// - /// Otherwise, it returns whether the state of the provided - /// `invoked_contract` was changed. + /// Returns the new balance of `to`. /// /// **Preconditions:** - /// - All contracts, modules, accounts referred must exist in persistence. - /// - All amount deltas must be valid (i.e. not cause underflows when added - /// to balance). - pub(crate) fn persist( - mut self, - remaining_energy: &mut Energy, - invoked_contract: ContractAddress, - persisted_accounts: &mut BTreeMap, - persisted_contracts: &mut BTreeMap, - ) -> Result { - let current = self.current_mut(); - let mut invoked_contract_has_state_changes = false; - // Persist contract changes and collect the total increase in states sizes. - let mut collector = v1::trie::SizeCollector::default(); - let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. - - let mut frozen_states: BTreeMap = BTreeMap::new(); - - // Create frozen versions of all the states, to compute the energy needed. - for (addr, changes) in current.contracts.iter_mut() { - if let Some(modified_state) = &mut changes.state { - frozen_states.insert(*addr, modified_state.freeze(&mut loader, &mut collector)); - } + /// - Assumes that `from` contract exists. + fn transfer_from_contract_to_contract( + &mut self, + amount: Amount, + from: ContractAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` contract exists. + if !self.chain.contracts.contains_key(&to) { + return Err(TransferError::ToMissing); } - // One energy per extra byte of state. - let energy_for_state_increase = Energy::from(collector.collect()); - - // Return an error if out of energy. - remaining_energy.tick_energy(energy_for_state_increase)?; + // Make the transfer. + self.change_contract_balance(from, AmountDelta::Negative(amount))?; + let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; + Ok(new_balance) + } - // Then persist all the changes. - for (addr, changes) in current.contracts.iter_mut() { - let mut contract = persisted_contracts - .get_mut(addr) - .expect("Precondition violation: contract must exist"); - // Update balance. - if !changes.self_balance_delta.is_zero() { - contract.self_balance = changes - .self_balance_delta - .apply_to_balance(changes.self_balance_original) - .expect("Precondition violation: amount delta causes underflow"); - } - // Update module reference. - if let Some(new_module_ref) = changes.module { - contract.module_reference = new_module_ref; - } - // Update state. - if changes.state.is_some() { - if *addr == invoked_contract { - invoked_contract_has_state_changes = true; - } - // Replace with the frozen state we created earlier. - contract.state = frozen_states - .remove(addr) - .expect("Known to exist since we just added it."); - } - } - // Persist account changes. - for (addr, changes) in current.accounts.iter() { - let mut account = persisted_accounts - .get_mut(addr) - .expect("Precondition violation: account must exist"); - // Update balance. - if !changes.balance_delta.is_zero() { - account.balance.total = changes - .balance_delta - .apply_to_balance(account.balance.total) - .expect("Precondition violation: amount delta causes underflow"); - } + /// Make a transfer from an account to a contract in the changeset. + /// + /// Returns the new balance of `to`. + /// + /// **Preconditions:** + /// - Assumes that `from` account exists. + fn transfer_from_account_to_contract( + &mut self, + amount: Amount, + from: AccountAddress, + to: ContractAddress, + ) -> Result { + // Ensure the `to` account exists. + if !self.chain.contracts.contains_key(&to) { + return Err(TransferError::ToMissing); } - Ok(invoked_contract_has_state_changes) + // Make the transfer. + self.change_account_balance(from, AmountDelta::Negative(amount))?; + let new_balance = self.change_contract_balance(to, AmountDelta::Positive(amount))?; + Ok(new_balance) } - /// Traverses the last checkpoint in the changeset and collects the energy - /// needed to be charged for additional state bytes. + /// Changes the contract balance in the topmost checkpoint on the changeset. /// - /// Returns an [`OutOfEnergy`] error if the energy needed for storing the - /// extra state is larger than `remaining_energy`. + /// If contract isn't already present in the changeset, it is added. /// - /// Otherwise, it returns whether the state of the provided - /// `invoked_contract` has changed. - pub(crate) fn collect_energy_for_state( - mut self, - remaining_energy: &mut Energy, - invoked_contract: ContractAddress, - ) -> Result { - let mut invoked_contract_has_state_changes = false; - let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. - let mut collector = v1::trie::SizeCollector::default(); - for (addr, changes) in self.current_mut().contracts.iter_mut() { - if let Some(modified_state) = &mut changes.state { - if *addr == invoked_contract { - invoked_contract_has_state_changes = true; - } - modified_state.freeze(&mut loader, &mut collector); + /// Returns an error if the change in balance would go below `0`, which is a + /// valid error, or if the amounts would overflow, which is an unrecoverable + /// configuration error in the tests. + /// Otherwise, it returns the new balance of the contract. + /// + /// The precondition is not part of the error type, as this is an internal + /// helper function that is only called when the precondition is met. + /// + /// Returns the new balance. + /// + /// **Preconditions:** + /// - Contract must exist. + fn change_contract_balance( + &mut self, + address: ContractAddress, + delta: AmountDelta, + ) -> Result { + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + // get original balance + let original_balance = self + .chain + .contracts + .get(&address) + .expect("Precondition violation: contract assumed to exist") + .self_balance; + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(ContractChanges { + self_balance_delta: delta, + ..ContractChanges::new(original_balance) + }); + Ok(new_contract_balance) + } + btree_map::Entry::Occupied(mut occ) => { + let contract_changes = occ.get_mut(); + let new_delta = contract_changes.self_balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_contract_balance = + new_delta.apply_to_balance(contract_changes.self_balance_original)?; + contract_changes.self_balance_delta = new_delta; + Ok(new_contract_balance) } } - - // One energy per extra byte in the state. - let energy_for_state_increase = Energy::from(collector.collect()); - - // Return an error if we run out of energy. - remaining_energy.tick_energy(energy_for_state_increase)?; - - Ok(invoked_contract_has_state_changes) } -} - -impl Default for ChangeSet { - fn default() -> Self { Self::new() } -} - -impl AmountDelta { - /// Create a new [`Self`], with the value `+0`. - fn new() -> Self { Self::Positive(Amount::zero()) } - /// Subtract an [`Amount`] from [`Self`]. - fn subtract_amount(self, amount: Amount) -> Self { - match self { - Self::Positive(current) => { - if current >= amount { - Self::Positive(current.subtract_micro_ccd(amount.micro_ccd)) - } else { - Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)) + /// Changes the account balance in the topmost checkpoint on the changeset. + /// + /// If account isn't already present in the changeset, it is added. + /// + /// Returns an error if a negative delta is provided which exceeds the + /// available balance of the account. Otherwise, it returns the new + /// available balance of the account. + /// Otherwise, it returns the new balance of the account. + /// + /// The precondition is not part of the error type, as this is an internal + /// helper function that is only called when the precondition is met. + /// + /// **Preconditions:** + /// - Account must exist. + fn change_account_balance( + &mut self, + address: AccountAddress, + delta: AmountDelta, + ) -> Result { + match self.changeset.current_mut().accounts.entry(address.into()) { + btree_map::Entry::Vacant(vac) => { + // get original balance + let mut original_balance = self + .chain + .accounts + .get(&address.into()) + .expect("Precondition violation: account assumed to exist") + .balance + .available(); + if self.invoker == address { + // It has been checked that the invoker account has sufficient balance for + // paying. + original_balance -= self.reserved_amount; } + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = delta.apply_to_balance(original_balance)?; + // Insert the changes into the changeset. + vac.insert(AccountChanges { + original_balance, + balance_delta: delta, + }); + Ok(new_account_balance) + } + btree_map::Entry::Occupied(mut occ) => { + let account_changes = occ.get_mut(); + let new_delta = account_changes.balance_delta.add_delta(delta); + // Try to apply the balance or return an error if insufficient funds. + let new_account_balance = + new_delta.apply_to_balance(account_changes.original_balance)?; + account_changes.balance_delta = new_delta; + Ok(new_account_balance) } - Self::Negative(current) => Self::Negative(current.add_micro_ccd(amount.micro_ccd)), } } - /// Add an [`Amount`] from [`Self`]. - fn add_amount(self, amount: Amount) -> Self { - match self { - Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), - Self::Negative(current) => { - if current >= amount { - Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)) - } else { - Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)) - } - } - } + /// Returns the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + /// + /// **Preconditions:** + /// - Contract must exist. + fn contract_balance_unchecked(&self, address: ContractAddress) -> Amount { + self.contract_balance(address) + .expect("Precondition violation: contract must exist") } - /// Add two [`Self`] to create a new one. - fn add_delta(self, delta: AmountDelta) -> Self { - match delta { - AmountDelta::Positive(d) => self.add_amount(d), - AmountDelta::Negative(d) => self.subtract_amount(d), + /// Looks up the contract balance from the topmost checkpoint on the + /// changeset. Or, alternatively, from persistence. + fn contract_balance(&self, address: ContractAddress) -> Option { + match self.changeset.current().contracts.get(&address) { + Some(changes) => Some(changes.current_balance()), + None => self.chain.contracts.get(&address).map(|c| c.self_balance), } } - /// Whether the [`Self`] is zero (either `+0` or `-0`). - fn is_zero(&self) -> bool { - match self { - AmountDelta::Positive(d) => d.micro_ccd == 0, - AmountDelta::Negative(d) => d.micro_ccd == 0, + /// Returns the contract module from the topmost checkpoint on + /// the changeset. Or, alternatively, from persistence. + /// + /// **Preconditions:** + /// - Contract instance must exist (and therefore also the artifact). + /// - If the changeset contains a module reference, then it must refer a + /// deployed module. + fn contract_module(&self, contract: &Contract) -> ContractModule { + match self + .changeset + .current() + .contracts + .get(&contract.address) + .and_then(|c| c.module) + { + // Contract has been upgrade, new module exists. + Some(new_module) => self + .chain + .modules + .get(&new_module) + .expect("Precondition violation: module must exist.") + .clone(), + // Contract hasn't been upgraded. Use persisted module. + None => self + .chain + .modules + .get(&contract.module_reference) + .expect("Precondition violation: module must exist.") + .clone(), } } - /// Returns a new balance with the `AmountDelta` applied, or a - /// [`BalanceError`] error if the change would result in a negative - /// balance or an overflow. - fn apply_to_balance(&self, balance: Amount) -> Result { - match self { - AmountDelta::Positive(d) => balance.checked_add(*d).ok_or(BalanceError::Overflow), - AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(BalanceError::Insufficient), + /// Get the contract state, either from the changeset or by thawing it from + /// persistence. + /// + /// **Preconditions:** + /// - Contract instance must exist. + fn contract_state(&self, address: ContractAddress) -> trie::MutableState { + match self + .changeset + .current() + .contracts + .get(&address) + .and_then(|c| c.state.clone()) + { + // Contract state has been modified. + Some(modified_state) => modified_state, + // Contract state hasn't been modified. Thaw from persistence. + None => self + .chain + .contracts + .get(&address) + .expect("Precondition violation: contract must exist") + .state + .thaw(), } } -} -impl ContractChanges { - /// Create a new `Self`. The original balance must be provided, all other - /// fields take on default values. - fn new(original_balance: Amount) -> Self { - Self { - modification_index: 0, - self_balance_delta: AmountDelta::new(), - self_balance_original: original_balance, - state: None, - module: None, + /// Looks up the account balance for an account by first checking + /// the changeset, then the persisted values. + fn account_balance(&self, address: AccountAddress) -> Option { + let mut account_balance = self + .chain + .accounts + .get(&address.into()) + .map(|a| a.balance)?; + match self + .changeset + .current() + .accounts + .get(&address.into()) + .map(|a| a.current_balance()) + { + // Account exists in changeset. + // Return the staked and locked balances from persistence, as they can't change during + // entrypoint invocation. + Some(total) => Some(AccountBalance { + total, + staked: account_balance.staked, + locked: account_balance.locked, + }), + // Account doesn't exist in changeset. + None => { + if self.invoker == address { + account_balance.total -= self.reserved_amount; + } + Some(account_balance) + } } } - /// Get the current balance by adding the original balance and the balance - /// delta. + /// Saves a mutable state for a contract in the changeset. /// - /// **Preconditions:** - /// - `balance_delta + original_balance` must be larger than `0`. - fn current_balance(&self) -> Amount { - self.self_balance_delta - .apply_to_balance(self.self_balance_original) - .expect("Precondition violation: invalid `balance_delta`.") - } -} - -impl AccountChanges { - /// Get the current balance by adding the original balance and the balance - /// delta. + /// If `with_fresh_generation`, then it will use the + /// [`MutableState::make_fresh_generation`][make_fresh_generation] + /// function, otherwise it will make a clone. + /// + /// If the contract already has an entry in the changeset, the old state + /// will be replaced. Otherwise, the entry is created and the state is + /// added. + /// + /// This also increments the modification index. It will be set to 1 if the + /// contract has no entry in the changeset. /// /// **Preconditions:** - /// - `balance_delta + original_balance` must be larger than `0`. - fn current_balance(&self) -> Amount { - self.balance_delta - .apply_to_balance(self.original_balance) - .expect("Precondition violation: invalid `balance_delta`.") + /// - Contract must exist. + /// + /// [make_fresh_generation]: trie::MutableState::make_fresh_generation + fn save_state_changes( + &mut self, + address: ContractAddress, + state: &mut trie::MutableState, + with_fresh_generation: bool, + ) { + let state = if with_fresh_generation { + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. + state.make_fresh_generation(&mut loader) + } else { + state.clone() + }; + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + let original_balance = self + .chain + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .self_balance; + vac.insert(ContractChanges { + state: Some(state), + modification_index: 1, // Increment from default, 0, to 1. + ..ContractChanges::new(original_balance) + }); + } + btree_map::Entry::Occupied(mut occ) => { + let changes = occ.get_mut(); + changes.state = Some(state); + changes.modification_index += 1; + } + } } -} -impl InvocationData { - /// Process a receive function until completion. + /// Saves a new module reference for the contract in the changeset. + /// + /// If the contract already has an entry in the changeset, the old module is + /// replaced. Otherwise, the entry is created and the module is added. /// - /// **Preconditions**: - /// - Contract instance exists in `invocation_handler.contracts`. - /// - Account exists in `invocation_handler.accounts`. - fn process( + /// Returns the previous module, which is either the one from persistence, + /// or the most recent one from the changeset. + /// + /// **Preconditions:** + /// - Contract must exist. + /// - Module must exist. + fn save_module_upgrade( &mut self, - invocation_handler: &mut EntrypointInvocationHandler, - mut receive_result: v1::ReceiveResult, - ) -> Result { - loop { - match receive_result { - v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy, - } => { - // Update the remaining_energy field. - invocation_handler.update_energy(remaining_energy); - - let update_event = ContractTraceElement::Updated { - data: InstanceUpdatedEvent { - contract_version: WasmVersion::V1, - address: self.address, - instigator: self.sender, - amount: self.amount, - message: self.parameter.clone(), - receive_name: OwnedReceiveName::construct_unchecked( - self.contract_name.as_contract_name(), - self.entrypoint.as_entrypoint_name(), - ), - events: contract_events_from_logs(logs.clone()), - }, - }; - // Add update event - self.trace_elements.push(update_event); - - // Save changes to changeset. - if state_changed { - invocation_handler.save_state_changes(self.address, &mut self.state, false); + address: ContractAddress, + module_reference: ModuleReference, + ) -> ModuleReference { + match self.changeset.current_mut().contracts.entry(address) { + btree_map::Entry::Vacant(vac) => { + let contract = self + .chain + .contracts + .get(&address) + .expect("Precondition violation: contract must exist."); + let old_module_ref = contract.module_reference; + let original_balance = contract.self_balance; + vac.insert(ContractChanges { + module: Some(module_reference), + ..ContractChanges::new(original_balance) + }); + old_module_ref + } + btree_map::Entry::Occupied(mut occ) => { + let changes = occ.get_mut(); + let old_module_ref = match changes.module { + Some(old_module) => old_module, + None => { + self.chain + .contracts + .get(&address) + .expect("Precondition violation: contract must exist.") + .module_reference } + }; + changes.module = Some(module_reference); + old_module_ref + } + } + } - return Ok(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Success { - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data: Some(return_value), - }, - logs, - }); - } - v1::ReceiveResult::Interrupt { - remaining_energy, - state_changed, - logs, - config, - interrupt, - } => { - // Update the remaining_energy field. - invocation_handler.update_energy(remaining_energy); - // Create the interrupt event, which will be included for transfers, calls, and - // upgrades, but not for the remaining interrupts. - let interrupt_event = ContractTraceElement::Interrupted { - address: self.address, - events: contract_events_from_logs(logs), - }; - match interrupt { - v1::Interrupt::Transfer { to, amount } => { - // Add the interrupt event - self.trace_elements.push(interrupt_event); + /// Returns the modification index for a contract. + /// + /// It looks it up in the changeset, and if it isn't there, it will return + /// `0`. + fn modification_index(&self, address: ContractAddress) -> u32 { + self.changeset + .current() + .contracts + .get(&address) + .map_or(0, |c| c.modification_index) + } - let response = match invocation_handler - .transfer_from_contract_to_account(amount, self.address, to) - { - Ok(new_balance) => v1::InvokeResponse::Success { - new_balance, - data: None, - }, - Err(err) => { - let kind = match err { - TransferError::ToMissing => { - v1::InvokeFailure::NonExistentAccount - } + /// Makes a new checkpoint. + fn checkpoint(&mut self) { self.changeset.checkpoint(); } - TransferError::BalanceError { - error: BalanceError::Insufficient, - } => v1::InvokeFailure::InsufficientAmount, + /// Roll back to the previous checkpoint. + fn rollback(&mut self) { self.changeset.rollback(); } - TransferError::BalanceError { - error: BalanceError::Overflow, - } => { - // Balance overflows are unrecoverable and short - // circuit. - return Err(TestConfigurationError::BalanceOverflow); - } - }; - v1::InvokeResponse::Failure { kind } - } - }; + /// Update the `remaining_energy` field by converting the input to + /// [`InterpreterEnergy`] and then [`Energy`]. + fn update_energy(&mut self, remaining_energy: u64) { + *self.remaining_energy = from_interpreter_energy(InterpreterEnergy::from(remaining_energy)); + } - let success = matches!(response, v1::InvokeResponse::Success { .. }); - if success { - // Add transfer event - self.trace_elements.push(ContractTraceElement::Transferred { - from: self.address, - amount, - to, - }); - } - // Add resume event - self.trace_elements.push(ContractTraceElement::Resumed { - address: self.address, - success, - }); + /// Run the interpreter with the provided function and the + /// `self.remaining_energy`. + /// + /// This function ensures that the energy calculations is handled as in the + /// node. + fn run_interpreter( + &mut self, + f: F, + ) -> Result, InsufficientEnergy> + where + F: FnOnce(InterpreterEnergy) -> ExecResult>, + { + let available_interpreter_energy = to_interpreter_energy(*self.remaining_energy); + let res = match f(available_interpreter_energy) { + Ok(res) => res, + Err(err) => { + // An error occured in the interpreter and it doesn't return the remaining + // energy. We convert this to a trap and set the energy to the + // last known amount. + return Ok(v1::ReceiveResult::Trap { + error: err, + remaining_energy: available_interpreter_energy.energy, + }); + } + }; + let mut subtract_then_convert = |remaining_energy| -> Result { + let remaining_energy = InterpreterEnergy::from(remaining_energy); + // Using `saturating_sub` here should be ok since we should never be able to use + // more energy than what is available. + let used_energy = from_interpreter_energy( + available_interpreter_energy.saturating_sub(remaining_energy), + ); + self.remaining_energy.tick_energy(used_energy)?; + Ok(to_interpreter_energy(*self.remaining_energy).energy) + }; + match res { + v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + } => Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy: subtract_then_convert(remaining_energy)?, + }), - invocation_handler.remaining_energy.tick_energy( - concordium_base::transactions::cost::SIMPLE_TRANSFER, - )?; + v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + } => Ok(v1::ReceiveResult::Interrupt { + remaining_energy: subtract_then_convert(remaining_energy)?, + state_changed, + logs, + config, + interrupt, + }), + v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy, + } => Ok(v1::ReceiveResult::Reject { + reason, + return_value, + remaining_energy: subtract_then_convert(remaining_energy)?, + }), + v1::ReceiveResult::Trap { + error, + remaining_energy, + } => Ok(v1::ReceiveResult::Trap { + error, + remaining_energy: subtract_then_convert(remaining_energy)?, + }), + // Convert this to an error so that we will short-circuit the processing. + v1::ReceiveResult::OutOfEnergy => Err(InsufficientEnergy), + } + } +} - let resume_res = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // never changes on transfers - // An empty loader is fine currently, as we do not use caching - // in this lib. - v1::trie::Loader::new(&[][..]), - ) - })?; +impl ChangeSet { + /// Creates a new changeset with one empty [`Changes`] element on the + /// stack.. + pub(crate) fn new() -> Self { + Self { + stack: vec![Changes { + contracts: BTreeMap::new(), + accounts: BTreeMap::new(), + }], + } + } - // Resume - receive_result = resume_res; - } - v1::Interrupt::Call { - address, - parameter, - name, - amount, - } => { - // Add the interrupt event - self.trace_elements.push(interrupt_event); + /// Make a checkpoint by putting a clone of the top element onto the stack. + fn checkpoint(&mut self) { + let cloned_top_element = self.current().clone(); + self.stack.push(cloned_top_element); + } - if state_changed { - invocation_handler.save_state_changes( - self.address, - &mut self.state, - true, - ); - } + /// Perform a rollback by popping the top element of the stack. + fn rollback(&mut self) { + self.stack + .pop() + .expect("Internal error: change set stack should never be empty."); + } - // Save the modification index before the invoke. - let mod_idx_before_invoke = - invocation_handler.modification_index(self.address); + /// Get an immutable reference the current (latest) checkpoint. + fn current(&self) -> &Changes { + self.stack + .last() + .expect("Internal error: change set stack should never be empty.") + } - // Make a checkpoint before calling another contract so that we may roll - // back. - invocation_handler.checkpoint(); + /// Get a mutable reference to the current (latest) checkpoint. + fn current_mut(&mut self) -> &mut Changes { + self.stack + .last_mut() + .expect("Internal error: change set stack should never be empty.") + } - let (success, invoke_response) = match invocation_handler - .chain - .contracts - .get(&address) - .map(|c| c.contract_name.as_contract_name()) - { - // The contract to call does not exist. - None => { - let invoke_response = v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }; - (false, invoke_response) - } - Some(contract_name) => { - let receive_name = OwnedReceiveName::construct_unchecked( - contract_name, - name.as_entrypoint_name(), - ); - let message = OwnedParameter::new_unchecked(parameter); - let res = invocation_handler.invoke_entrypoint( - self.invoker, - Address::Contract(self.address), - UpdateContractPayload { - amount, - address, - receive_name, - message, - }, - &mut self.trace_elements, - )?; - match res.invoke_response { - v1::InvokeResponse::Success { data, .. } => { - let invoke_response = v1::InvokeResponse::Success { - // The balance returned by `invoke_entrypoint` is - // the balance of the contract called. But we are - // interested in the new balance of the caller. - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data, - }; - (true, invoke_response) - } - failure => (false, failure), - } - } - }; + /// Try to persist all changes from the changeset. + /// + /// If the energy needed for storing extra state is larger than the + /// `remaining_energy`, then: + /// - no changes will be persisted, + /// - an [`OutOfEnergy`] error is returned. + /// + /// Otherwise, it returns whether the state of the provided + /// `invoked_contract` was changed. + /// + /// **Preconditions:** + /// - All contracts, modules, accounts referred must exist in persistence. + /// - All amount deltas must be valid (i.e. not cause underflows when added + /// to balance). + pub(crate) fn persist( + mut self, + remaining_energy: &mut Energy, + invoked_contract: ContractAddress, + persisted_accounts: &mut BTreeMap, + persisted_contracts: &mut BTreeMap, + ) -> Result { + let current = self.current_mut(); + let mut invoked_contract_has_state_changes = false; + // Persist contract changes and collect the total increase in states sizes. + let mut collector = v1::trie::SizeCollector::default(); + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. - // Remove the last state changes if the invocation failed. - let state_changed = if !success { - // TODO: we should likely need to roll back traces as well. - invocation_handler.rollback(); - false // We rolled back, so no changes were made - // to this contract. - } else { - let mod_idx_after_invoke = - invocation_handler.modification_index(self.address); - let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; - if state_changed { - // Update the state field with the newest value from the - // changeset. - self.state = invocation_handler.contract_state(self.address); - } - state_changed - }; + let mut frozen_states: BTreeMap = BTreeMap::new(); - // Add resume event - let resume_event = ContractTraceElement::Resumed { - address: self.address, - success, - }; + // Create frozen versions of all the states, to compute the energy needed. + for (addr, changes) in current.contracts.iter_mut() { + if let Some(modified_state) = &mut changes.state { + frozen_states.insert(*addr, modified_state.freeze(&mut loader, &mut collector)); + } + } - self.trace_elements.push(resume_event); + // One energy per extra byte of state. + let energy_for_state_increase = Energy::from(collector.collect()); - receive_result = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - invoke_response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; - } - v1::Interrupt::Upgrade { module_ref } => { - // Add the interrupt event. - self.trace_elements.push(interrupt_event); + // Return an error if out of energy. + remaining_energy.tick_energy(energy_for_state_increase)?; - // Charge a base cost. - invocation_handler - .remaining_energy - .tick_energy(constants::INITIALIZE_CONTRACT_INSTANCE_BASE_COST)?; + // Then persist all the changes. + for (addr, changes) in current.contracts.iter_mut() { + let mut contract = persisted_contracts + .get_mut(addr) + .expect("Precondition violation: contract must exist"); + // Update balance. + if !changes.self_balance_delta.is_zero() { + contract.self_balance = changes + .self_balance_delta + .apply_to_balance(changes.self_balance_original) + .expect("Precondition violation: amount delta causes underflow"); + } + // Update module reference. + if let Some(new_module_ref) = changes.module { + contract.module_reference = new_module_ref; + } + // Update state. + if changes.state.is_some() { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } + // Replace with the frozen state we created earlier. + contract.state = frozen_states + .remove(addr) + .expect("Known to exist since we just added it."); + } + } + // Persist account changes. + for (addr, changes) in current.accounts.iter() { + let mut account = persisted_accounts + .get_mut(addr) + .expect("Precondition violation: account must exist"); + // Update balance. + if !changes.balance_delta.is_zero() { + account.balance.total = changes + .balance_delta + .apply_to_balance(account.balance.total) + .expect("Precondition violation: amount delta causes underflow"); + } + } - let response = match invocation_handler.chain.modules.get(&module_ref) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidModuleRef, - }, - Some(module) => { - // Charge for the module lookup. - invocation_handler - .remaining_energy - .tick_energy(lookup_module_cost(module))?; + Ok(invoked_contract_has_state_changes) + } - if module.artifact.export.contains_key( - self.contract_name.as_contract_name().get_chain_name(), - ) { - // Update module reference in the changeset. - let old_module_ref = invocation_handler - .save_module_upgrade(self.address, module_ref); + /// Traverses the last checkpoint in the changeset and collects the energy + /// needed to be charged for additional state bytes. + /// + /// Returns an [`OutOfEnergy`] error if the energy needed for storing the + /// extra state is larger than `remaining_energy`. + /// + /// Otherwise, it returns whether the state of the provided + /// `invoked_contract` has changed. + pub(crate) fn collect_energy_for_state( + mut self, + remaining_energy: &mut Energy, + invoked_contract: ContractAddress, + ) -> Result { + let mut invoked_contract_has_state_changes = false; + let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. + let mut collector = v1::trie::SizeCollector::default(); + for (addr, changes) in self.current_mut().contracts.iter_mut() { + if let Some(modified_state) = &mut changes.state { + if *addr == invoked_contract { + invoked_contract_has_state_changes = true; + } + modified_state.freeze(&mut loader, &mut collector); + } + } - // Charge for the initialization cost. - invocation_handler.remaining_energy.tick_energy( - constants::INITIALIZE_CONTRACT_INSTANCE_CREATE_COST, - )?; + // One energy per extra byte in the state. + let energy_for_state_increase = Energy::from(collector.collect()); - let upgrade_event = ContractTraceElement::Upgraded { - address: self.address, - from: old_module_ref, - to: module_ref, - }; + // Return an error if we run out of energy. + remaining_energy.tick_energy(energy_for_state_increase)?; - self.trace_elements.push(upgrade_event); + Ok(invoked_contract_has_state_changes) + } +} - v1::InvokeResponse::Success { - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data: None, - } - } else { - v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::UpgradeInvalidContractName, - } - } - } - }; +impl Default for ChangeSet { + fn default() -> Self { Self::new() } +} - let success = matches!(response, v1::InvokeResponse::Success { .. }); - self.trace_elements.push(ContractTraceElement::Resumed { - address: self.address, - success, - }); +impl AmountDelta { + /// Create a new [`Self`], with the value `+0`. + fn new() -> Self { Self::Positive(Amount::zero()) } - let resume_res = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; - receive_result = resume_res; - } - v1::Interrupt::QueryAccountBalance { address } => { - let response = match invocation_handler.account_balance(address) { - Some(balance) => v1::InvokeResponse::Success { - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&balance)), - }, - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentAccount, - }, - }; + /// Subtract an [`Amount`] from [`Self`]. + fn subtract_amount(self, amount: Amount) -> Self { + match self { + Self::Positive(current) => { + if current >= amount { + Self::Positive(current.subtract_micro_ccd(amount.micro_ccd)) + } else { + Self::Negative(amount.subtract_micro_ccd(current.micro_ccd)) + } + } + Self::Negative(current) => Self::Negative(current.add_micro_ccd(amount.micro_ccd)), + } + } - invocation_handler.remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, - )?; + /// Add an [`Amount`] from [`Self`]. + fn add_amount(self, amount: Amount) -> Self { + match self { + Self::Positive(current) => Self::Positive(current.add_micro_ccd(amount.micro_ccd)), + Self::Negative(current) => { + if current >= amount { + Self::Negative(current.subtract_micro_ccd(amount.micro_ccd)) + } else { + Self::Positive(amount.subtract_micro_ccd(current.micro_ccd)) + } + } + } + } - receive_result = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - } - v1::Interrupt::QueryContractBalance { address } => { - let response = match invocation_handler.contract_balance(address) { - None => v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, - Some(bal) => v1::InvokeResponse::Success { - // Balance of contract querying. Won't change. Notice the - // `self.address`. - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&bal)), - }, - }; + /// Add two [`Self`] to create a new one. + fn add_delta(self, delta: AmountDelta) -> Self { + match delta { + AmountDelta::Positive(d) => self.add_amount(d), + AmountDelta::Negative(d) => self.subtract_amount(d), + } + } - invocation_handler.remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, - )?; + /// Whether the [`Self`] is zero (either `+0` or `-0`). + fn is_zero(&self) -> bool { + match self { + AmountDelta::Positive(d) => d.micro_ccd == 0, + AmountDelta::Negative(d) => d.micro_ccd == 0, + } + } - receive_result = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - } - v1::Interrupt::QueryExchangeRates => { - let exchange_rates = ExchangeRates { - euro_per_energy: invocation_handler - .chain - .parameters - .euro_per_energy, - micro_ccd_per_euro: invocation_handler - .chain - .parameters - .micro_ccd_per_euro, - }; + /// Returns a new balance with the `AmountDelta` applied, or a + /// [`BalanceError`] error if the change would result in a negative + /// balance or an overflow. + fn apply_to_balance(&self, balance: Amount) -> Result { + match self { + AmountDelta::Positive(d) => balance.checked_add(*d).ok_or(BalanceError::Overflow), + AmountDelta::Negative(d) => balance.checked_sub(*d).ok_or(BalanceError::Insufficient), + } + } +} - let response = v1::InvokeResponse::Success { - new_balance: invocation_handler - .contract_balance_unchecked(self.address), - data: Some(to_bytes(&exchange_rates)), - }; +impl ContractChanges { + /// Create a new `Self`. The original balance must be provided, all other + /// fields take on default values. + fn new(original_balance: Amount) -> Self { + Self { + modification_index: 0, + self_balance_delta: AmountDelta::new(), + self_balance_original: original_balance, + state: None, + module: None, + } + } - invocation_handler.remaining_energy.tick_energy( - constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, - )?; + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// **Preconditions:** + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.self_balance_delta + .apply_to_balance(self.self_balance_original) + .expect("Precondition violation: invalid `balance_delta`.") + } +} - receive_result = invocation_handler.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut self.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; - } - } - } - v1::ReceiveResult::Reject { - reason, - return_value, - remaining_energy, - } => { - invocation_handler.update_energy(remaining_energy); - return Ok(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, - }, - logs: v0::Logs::new(), - }); - } - v1::ReceiveResult::Trap { - error: _, // FIXME: This would ideally be propagated to the caller. - remaining_energy, - } => { - invocation_handler.update_energy(remaining_energy); - return Ok(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, - logs: v0::Logs::new(), - }); - } - // Convert this to an error here, so that we will short circuit processing. - v1::ReceiveResult::OutOfEnergy => return Err(TestConfigurationError::OutOfEnergy), - } - } +impl AccountChanges { + /// Get the current balance by adding the original balance and the balance + /// delta. + /// + /// **Preconditions:** + /// - `balance_delta + original_balance` must be larger than `0`. + fn current_balance(&self) -> Amount { + self.balance_delta + .apply_to_balance(self.original_balance) + .expect("Precondition violation: invalid `balance_delta`.") } } diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index cdbee425..631546b6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -97,8 +97,6 @@ pub(super) struct ContractChanges { /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. pub(super) struct InvocationData { - /// The invoker. - pub(super) invoker: AccountAddress, /// The sender. pub(super) sender: Address, /// The contract being called. From 68dfd3a23b9d0b1585d2d74d1469a2964c75b0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 10:37:51 +0200 Subject: [PATCH 139/208] Remove recursion from invoke_entrypoint. --- contract-testing/src/impls.rs | 7 +- contract-testing/src/invocation/impls.rs | 427 +++++++++++++---------- contract-testing/src/invocation/types.rs | 43 ++- contract-testing/tests/upgrades.rs | 4 +- 4 files changed, 276 insertions(+), 205 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index f50e3834..694131e8 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -655,9 +655,10 @@ impl Chain { invoker, }; - let mut trace_elements = Vec::new(); - match contract_invocation.invoke_entrypoint(invoker, sender, payload, &mut trace_elements) { - Ok(result) => Ok((result, contract_invocation.changeset, trace_elements)), + match contract_invocation.invoke_entrypoint(invoker, sender, payload) { + Ok((result, trace_elements)) => { + Ok((result, contract_invocation.changeset, trace_elements)) + } Err(err) => { Err(self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy)) } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 32bb1659..84a6bd2b 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -33,6 +33,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, + trace_elements_checkpoint: usize, ) -> Result< Result<(v1::ReceiveResult, InvocationData), InvokeEntrypointResponse>, TestConfigurationError, @@ -167,6 +168,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, }; + let mod_idx_before_invoke = self.modification_index(payload.address); + // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut mutable_state = self.contract_state(payload.address); @@ -193,7 +196,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, ) })?; - // Set up some data needed for recursively processing the receive until the end, // i.e. beyond interrupts. Ok(Ok((initial_result, InvocationData { @@ -204,7 +206,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { parameter: payload.message, amount: payload.amount, state: mutable_state, - trace_elements: Vec::new(), + trace_elements_checkpoint, + mod_idx_before_invoke, }))) } @@ -221,17 +224,130 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - trace_elements: &mut Vec, /* TODO: Are trace elements - * appropriately rolled back? */ - ) -> Result { - let (mut receive_result, mut invocation_data) = - match self.invoke_entrypoint_initial(invoker, sender, payload)? { - Ok(x) => x, - Err(ier) => return Ok(ier), + ) -> Result<(InvokeEntrypointResponse, Vec), TestConfigurationError> { + let mut stack = Vec::new(); + let mut trace_elements = Vec::new(); + stack.push(Next::Initial { + sender, + payload, + trace_elements_checkpoint: 0, + }); + // Initialized to a dummy value. This will always be set or the function will + // terminate with an Err. + let mut invoke_response: Option = None; + while let Some(invocation_data) = stack.pop() { + let (receive_result, mut invocation_data) = match invocation_data { + Next::Resume { + mut data, + config, + response, + } => { + match response { + Some(response) => { + let receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + response, + energy, + &mut data.state, + false, /* never changes on interrupts that have immediate + * handlers */ + // An empty loader is fine currently, as we do not use + // caching + // in this lib. + v1::trie::Loader::new(&[][..]), + ) + })?; + (receive_result, data) + } + None => { + // we are resuming from a contract call + let (success, call_response) = match invoke_response + .take() + .expect("Response should be available") + .invoke_response + { + v1::InvokeResponse::Success { + data: return_value, .. + } => { + let invoke_response = v1::InvokeResponse::Success { + // The balance returned by `invoke_entrypoint` + // is + // the balance of the contract called. But we + // are + // interested in the new balance of the caller. + new_balance: self.contract_balance_unchecked(data.address), + data: return_value, + }; + (true, invoke_response) + } + failure => (false, failure), + }; + + // Remove the last state changes if the invocation failed. + let state_changed = if !success { + self.rollback(); + false // We rolled back, so no changes were + // made + // to this contract. + } else { + let mod_idx_after_invoke = self.modification_index(data.address); + let state_changed = + mod_idx_after_invoke != data.mod_idx_before_invoke; + if state_changed { + // Update the state field with the newest value from the + // changeset. + data.state = self.contract_state(data.address); + } + state_changed + }; + + // Add resume event + let resume_event = ContractTraceElement::Resumed { + address: data.address, + success, + }; + + trace_elements.push(resume_event); + let receive_result = self.run_interpreter(|energy| { + v1::resume_receive( + config, + call_response, + energy, + &mut data.state, + state_changed, + // An empty loader is fine currently, as we do not use + // caching + // in this lib. + v1::trie::Loader::new(&[][..]), + ) + })?; + (receive_result, data) + } + } + } + Next::Initial { + sender, + payload, + trace_elements_checkpoint, + } => { + match self.invoke_entrypoint_initial( + invoker, + sender, + payload, + trace_elements_checkpoint, + )? { + Ok(x) => x, + Err(ier) => { + // invocation has failed. No more to do for this call. + // Since no traces were produced we don't have to roll them back. + invoke_response = Some(ier); + continue; + } + } + } }; - // Process the receive invocation to the completion. - let result = 'result: loop { match receive_result { v1::ReceiveResult::Success { logs, @@ -257,8 +373,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, }; // Add update event - invocation_data.trace_elements.push(update_event); - + trace_elements.push(update_event); // Save changes to changeset. if state_changed { self.save_state_changes( @@ -268,13 +383,13 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { ); } - break 'result InvokeEntrypointResponse { + invoke_response = Some(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Success { new_balance: self.contract_balance_unchecked(invocation_data.address), data: Some(return_value), }, logs, - }; + }); } v1::ReceiveResult::Interrupt { remaining_energy, @@ -285,8 +400,9 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } => { // Update the remaining_energy field. self.update_energy(remaining_energy); - // Create the interrupt event, which will be included for transfers, calls, and - // upgrades, but not for the remaining interrupts. + // Create the interrupt event, which will be included for transfers, calls, + // and upgrades, but not for the remaining + // interrupts. let interrupt_event = ContractTraceElement::Interrupted { address: invocation_data.address, events: contract_events_from_logs(logs), @@ -294,7 +410,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match interrupt { v1::Interrupt::Transfer { to, amount } => { // Add the interrupt event - invocation_data.trace_elements.push(interrupt_event); + trace_elements.push(interrupt_event); let response = match self.transfer_from_contract_to_account( amount, @@ -330,41 +446,28 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let success = matches!(response, v1::InvokeResponse::Success { .. }); if success { // Add transfer event - invocation_data.trace_elements.push( - ContractTraceElement::Transferred { - from: invocation_data.address, - amount, - to, - }, - ); + trace_elements.push(ContractTraceElement::Transferred { + from: invocation_data.address, + amount, + to, + }); } // Add resume event - invocation_data - .trace_elements - .push(ContractTraceElement::Resumed { - address: invocation_data.address, - success, - }); + trace_elements.push(ContractTraceElement::Resumed { + address: invocation_data.address, + success, + }); self.remaining_energy.tick_energy( concordium_base::transactions::cost::SIMPLE_TRANSFER, )?; - let resume_res = self.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut invocation_data.state, - false, // never changes on transfers - // An empty loader is fine currently, as we do not use caching - // in this lib. - v1::trie::Loader::new(&[][..]), - ) - })?; - - // Resume - receive_result = resume_res; + stack.push(Next::Resume { + data: invocation_data, + // the state never changes on transfers + config, + response: Some(response), + }); } v1::Interrupt::Call { address, @@ -373,25 +476,9 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { amount, } => { // Add the interrupt event - invocation_data.trace_elements.push(interrupt_event); - - if state_changed { - self.save_state_changes( - invocation_data.address, - &mut invocation_data.state, - true, - ); - } - - // Save the modification index before the invoke. - let mod_idx_before_invoke = - self.modification_index(invocation_data.address); + trace_elements.push(interrupt_event); - // Make a checkpoint before calling another contract so that we may roll - // back. - self.checkpoint(); - - let (success, invoke_response) = match self + match self .chain .contracts .get(&address) @@ -399,87 +486,71 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { { // The contract to call does not exist. None => { - let invoke_response = v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, + invoke_response = Some(InvokeEntrypointResponse { + invoke_response: v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }, + logs: v0::Logs::new(), + }); + // Add resume event + let resume_event = ContractTraceElement::Resumed { + address: invocation_data.address, + success: false, }; - (false, invoke_response) + + trace_elements.push(resume_event); } Some(contract_name) => { + if state_changed { + self.save_state_changes( + invocation_data.address, + &mut invocation_data.state, + true, + ); + } + + // Make a checkpoint before calling another contract so that we + // may roll back. + self.checkpoint(); + + // Remember what state we are in before invoking. + // This is used to report, upon resume, whether the contracts's + // state has changed. + invocation_data.mod_idx_before_invoke = + self.modification_index(invocation_data.address); + let receive_name = OwnedReceiveName::construct_unchecked( contract_name, name.as_entrypoint_name(), ); let message = OwnedParameter::new_unchecked(parameter); - let res = self.invoke_entrypoint( - self.invoker, - Address::Contract(invocation_data.address), - UpdateContractPayload { + + let sender = Address::Contract(invocation_data.address); + // Remember to continue the current execution after handling the + // call. + stack.push(Next::Resume { + data: invocation_data, + config, + response: None, + }); + + // Add the call to the stack to execute. + stack.push(Next::Initial { + sender, + payload: UpdateContractPayload { amount, address, receive_name, message, }, - &mut invocation_data.trace_elements, - )?; - match res.invoke_response { - v1::InvokeResponse::Success { data, .. } => { - let invoke_response = v1::InvokeResponse::Success { - // The balance returned by `invoke_entrypoint` is - // the balance of the contract called. But we are - // interested in the new balance of the caller. - new_balance: self.contract_balance_unchecked( - invocation_data.address, - ), - data, - }; - (true, invoke_response) - } - failure => (false, failure), - } + trace_elements_checkpoint: trace_elements.len(), + }); } }; - - // Remove the last state changes if the invocation failed. - let state_changed = if !success { - // TODO: we should likely need to roll back traces as well. - self.rollback(); - false // We rolled back, so no changes were made - // to this contract. - } else { - let mod_idx_after_invoke = - self.modification_index(invocation_data.address); - let state_changed = mod_idx_after_invoke != mod_idx_before_invoke; - if state_changed { - // Update the state field with the newest value from the - // changeset. - invocation_data.state = - self.contract_state(invocation_data.address); - } - state_changed - }; - - // Add resume event - let resume_event = ContractTraceElement::Resumed { - address: invocation_data.address, - success, - }; - - invocation_data.trace_elements.push(resume_event); - - receive_result = self.run_interpreter(|energy| { - v1::resume_receive( - config, - invoke_response, - energy, - &mut invocation_data.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; } v1::Interrupt::Upgrade { module_ref } => { // Add the interrupt event. - invocation_data.trace_elements.push(interrupt_event); + trace_elements.push(interrupt_event); // Charge a base cost. self.remaining_energy @@ -517,7 +588,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { to: module_ref, }; - invocation_data.trace_elements.push(upgrade_event); + trace_elements.push(upgrade_event); v1::InvokeResponse::Success { new_balance: self.contract_balance_unchecked( @@ -534,24 +605,15 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }; let success = matches!(response, v1::InvokeResponse::Success { .. }); - invocation_data - .trace_elements - .push(ContractTraceElement::Resumed { - address: invocation_data.address, - success, - }); - - let resume_res = self.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut invocation_data.state, - state_changed, - v1::trie::Loader::new(&[][..]), - ) - })?; - receive_result = resume_res; + trace_elements.push(ContractTraceElement::Resumed { + address: invocation_data.address, + success, + }); + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); } v1::Interrupt::QueryAccountBalance { address } => { let response = match self.account_balance(address) { @@ -568,17 +630,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { self.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_BALANCE_COST, )?; - - receive_result = self.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut invocation_data.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); } v1::Interrupt::QueryContractBalance { address } => { let response = match self.contract_balance(address) { @@ -597,17 +653,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { self.remaining_energy.tick_energy( constants::CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST, )?; - - receive_result = self.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut invocation_data.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); } v1::Interrupt::QueryExchangeRates => { let exchange_rates = ExchangeRates { @@ -625,16 +675,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { constants::CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST, )?; - receive_result = self.run_interpreter(|energy| { - v1::resume_receive( - config, - response, - energy, - &mut invocation_data.state, - false, // State never changes on queries. - v1::trie::Loader::new(&[][..]), - ) - })?; + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); } } } @@ -644,7 +689,10 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { remaining_energy, } => { self.update_energy(remaining_energy); - break 'result InvokeEntrypointResponse { + // Delete the trace of the failed part of the execution. + // This is the current behaviour of the on-chain execution. + trace_elements.truncate(invocation_data.trace_elements_checkpoint); + invoke_response = Some(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::ContractReject { code: reason, @@ -652,32 +700,31 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, }, logs: v0::Logs::new(), - }; + }); } v1::ReceiveResult::Trap { error: _, // FIXME: This would ideally be propagated to the caller. remaining_energy, } => { self.update_energy(remaining_energy); - break 'result InvokeEntrypointResponse { + // Delete the trace of the failed part of the execution. + // This is the current behaviour of the on-chain execution. + trace_elements.truncate(invocation_data.trace_elements_checkpoint); + invoke_response = Some(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, logs: v0::Logs::new(), - }; + }); } // Convert this to an error here, so that we will short circuit processing. v1::ReceiveResult::OutOfEnergy => return Err(TestConfigurationError::OutOfEnergy), } - }; - let mut new_trace_elements = invocation_data.trace_elements; - - // Append the new trace elements if the invocation succeeded. - if result.is_success() { - trace_elements.append(&mut new_trace_elements); } - - Ok(result) + Ok(( + invoke_response.expect("Response should have been set."), + trace_elements, + )) } /// Make a transfer from a contract to an account in the changeset. diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 631546b6..678181b5 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -5,15 +5,18 @@ use concordium_base::{ AccountAddress, Address, Amount, ContractAddress, ModuleReference, OwnedContractName, OwnedEntrypointName, }, - smart_contracts::{ContractTraceElement, OwnedParameter}, + smart_contracts::OwnedParameter, + transactions::UpdateContractPayload, }; use concordium_smart_contract_engine::{ v0, - v1::{trie::MutableState, InvokeResponse}, + v1::{trie::MutableState, InvokeResponse, ReceiveContext, ReceiveInterruptedState}, }; +use concordium_wasm::artifact::CompiledFunction; use std::collections::BTreeMap; /// The response from invoking an entrypoint. +#[derive(Debug)] pub(crate) struct InvokeEntrypointResponse { /// The result from the invoke. pub(crate) invoke_response: InvokeResponse, @@ -96,23 +99,26 @@ pub(super) struct ContractChanges { /// /// One `InvocationData` is created for each time /// [`EntrypointInvocationHandler::invoke_entrypoint`] is called. +#[derive(Debug)] pub(super) struct InvocationData { /// The sender. - pub(super) sender: Address, + pub(super) sender: Address, /// The contract being called. - pub(super) address: ContractAddress, + pub(super) address: ContractAddress, /// The name of the contract. - pub(super) contract_name: OwnedContractName, + pub(super) contract_name: OwnedContractName, /// The entrypoint to execute. - pub(super) entrypoint: OwnedEntrypointName, + pub(super) entrypoint: OwnedEntrypointName, /// The amount sent from the sender to the contract. - pub(super) amount: Amount, + pub(super) amount: Amount, /// The parameter given to the entrypoint. - pub(super) parameter: OwnedParameter, + pub(super) parameter: OwnedParameter, /// The current state. - pub(super) state: MutableState, - /// Trace elements that have occurred during the execution. - pub(super) trace_elements: Vec, + pub(super) state: MutableState, + /// A checkpoint in the list of trace elements. + /// We reset to this size in case of failure of execution. + pub(super) trace_elements_checkpoint: usize, + pub(super) mod_idx_before_invoke: u32, } /// A positive or negative delta in for an [`Amount`]. @@ -134,3 +140,18 @@ pub(crate) enum TestConfigurationError { /// means that overflows of amounts cannot occur. BalanceOverflow, } + +pub(super) enum Next { + Resume { + data: InvocationData, + config: Box>>>, + /// This is none if we are going to resume after a call to a contract. + /// And Some if we have an immediate handler. + response: Option, + }, + Initial { + sender: Address, + payload: UpdateContractPayload, + trace_elements_checkpoint: usize, + }, +} diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 06e13653..64ecd6ff 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -427,7 +427,9 @@ fn test_chained_contract() { ) .expect("Initializing valid contract should work"); - let number_of_upgrades: u32 = 74; // TODO: Stack will overflow if set larger. + // High number here tests we don't have stack overflows due to recursion or + // other accidental stack usage. + let number_of_upgrades: u32 = 1_000_000; let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain From b10902dc6973f3fde9afcba689b655feef5ae809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 12:51:10 +0200 Subject: [PATCH 140/208] Simplify NRG/CCD conversion. --- contract-testing/Cargo.toml | 1 + contract-testing/src/impls.rs | 13 ++++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index acc9732c..feac5f9c 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -14,3 +14,4 @@ sha2 = "0.10" anyhow = "1" thiserror = "1.0" num-bigint = "0.4" +num-integer = "0.1" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 0feda99d..ee7c0ca2 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -21,6 +21,7 @@ use concordium_base::{ }; use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; use num_bigint::BigUint; +use num_integer::Integer; use std::{collections::BTreeMap, path::Path, sync::Arc}; impl Default for Chain { @@ -1275,18 +1276,12 @@ pub fn energy_to_amount( BigUint::from(euro_per_energy.numerator()) * micro_ccd_per_euro.numerator(); let micro_ccd_per_energy_denominator: BigUint = BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); - let remainder: BigUint = (micro_ccd_per_energy_numerator.clone() * energy.energy) - % micro_ccd_per_energy_denominator.clone(); - let cost: BigUint = - (micro_ccd_per_energy_numerator * energy.energy) / micro_ccd_per_energy_denominator; - let mut cost: u64 = u64::try_from(cost).expect( + let cost: BigUint = (micro_ccd_per_energy_numerator * energy.energy) + .div_ceil(µ_ccd_per_energy_denominator); + let cost: u64 = u64::try_from(cost).expect( "Should never overflow since reasonable exchange rates are ensured when constructing the \ [`Chain`].", ); - // Apply ceiling. - if remainder != 0u8.into() { - cost += 1 - } Amount::from_micro_ccd(cost) } From bf4a55d8f100bf4879bc954ae8cca1a271a9cba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 13:23:51 +0200 Subject: [PATCH 141/208] Make sure to return the correct error. --- contract-testing/Cargo.toml | 2 +- contract-testing/src/invocation/impls.rs | 15 ++++++++------- contract-testing/tests/upgrades.rs | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index feac5f9c..cc724bcd 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -14,4 +14,4 @@ sha2 = "0.10" anyhow = "1" thiserror = "1.0" num-bigint = "0.4" -num-integer = "0.1" +num-integer = "0.1" \ No newline at end of file diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 84a6bd2b..a69872a2 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -486,19 +486,20 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { { // The contract to call does not exist. None => { - invoke_response = Some(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, - logs: v0::Logs::new(), - }); + let response = v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, + }; // Add resume event let resume_event = ContractTraceElement::Resumed { address: invocation_data.address, success: false, }; - trace_elements.push(resume_event); + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); } Some(contract_name) => { if state_changed { diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 64ecd6ff..72c31d11 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -429,7 +429,7 @@ fn test_chained_contract() { // High number here tests we don't have stack overflows due to recursion or // other accidental stack usage. - let number_of_upgrades: u32 = 1_000_000; + let number_of_upgrades: u32 = 1_000; let input_param = (number_of_upgrades, res_deploy.module_reference); let res_update = chain @@ -437,7 +437,7 @@ fn test_chained_contract() { Signer::with_one_key(), ACC_0, Address::Account(ACC_0), - Energy::from(1000000), + Energy::from(1_000_000_000), UpdateContractPayload { address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("contract.upgrade".into()), From 44a8d442a8a2ee61a946f3edb349909db5284708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 13:29:27 +0200 Subject: [PATCH 142/208] Address clippy suggestions. --- contract-testing/src/impls.rs | 36 +++++++++++------------- contract-testing/src/invocation/impls.rs | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index ee7c0ca2..3c1ab969 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -303,8 +303,8 @@ impl Chain { let mut buffer = Vec::new(); std::io::Read::read_to_end(&mut reader, &mut buffer).map_err(|e| ModuleLoadError { path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::OpenFile(e.into()), /* This is unlikely to happen, since - * we already opened it. */ + kind: ModuleLoadErrorKind::OpenFile(e), /* This is unlikely to happen, since + * we already opened it. */ })?; Ok(WasmModule { version: WasmVersion::V1, @@ -361,7 +361,7 @@ impl Chain { ) -> Result { let mut remaining_energy = energy_reserved; if !self.account_exists(sender) { - return Err(self.from_init_error_kind( + return Err(self.convert_to_init_error( ContractInitErrorKind::SenderDoesNotExist(AccountDoesNotExist { address: sender }), energy_reserved, remaining_energy, @@ -382,7 +382,7 @@ impl Chain { (Ok(s), transaction_fee) } Err(e) => { - let err = self.from_init_error_kind(e, energy_reserved, remaining_energy); + let err = self.convert_to_init_error(e, energy_reserved, remaining_energy); let transaction_fee = err.transaction_fee; (Err(err), transaction_fee) } @@ -455,7 +455,7 @@ impl Chain { // Ensure the module contains the provided init name. let init_name = payload.init_name.as_contract_name().get_chain_name(); - if !module.artifact.export.get(init_name).is_some() { + if module.artifact.export.get(init_name).is_none() { return Err(ContractInitErrorKind::ContractNotPresentInModule { name: payload.init_name, }); @@ -613,7 +613,7 @@ impl Chain { > { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { - return Err(self.from_invocation_error_kind( + return Err(self.convert_to_invoke_error( ContractDoesNotExist { address: payload.address, } @@ -625,7 +625,7 @@ impl Chain { // Ensure that the parameter has a valid size. if payload.message.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { - return Err(self.from_invocation_error_kind( + return Err(self.convert_to_invoke_error( ContractInvokeErrorKind::ParameterTooLarge, energy_reserved, *remaining_energy, @@ -641,7 +641,7 @@ impl Chain { .available() < amount_reserved_for_energy + payload.amount { - return Err(self.from_invocation_error_kind( + return Err(self.convert_to_invoke_error( ContractInvokeErrorKind::AmountTooLarge, energy_reserved, *remaining_energy, @@ -661,7 +661,7 @@ impl Chain { Ok((result, contract_invocation.changeset, trace_elements)) } Err(err) => { - Err(self.from_invocation_error_kind(err.into(), energy_reserved, *remaining_energy)) + Err(self.convert_to_invoke_error(err.into(), energy_reserved, *remaining_energy)) } } } @@ -688,7 +688,7 @@ impl Chain { events: contract_events_from_logs(result.logs), }) } - v1::InvokeResponse::Failure { kind } => Err(self.from_invocation_error_kind( + v1::InvokeResponse::Failure { kind } => Err(self.convert_to_invoke_error( ContractInvokeErrorKind::ExecutionError { failure_kind: kind }, energy_reserved, remaining_energy, @@ -886,7 +886,7 @@ impl Chain { payload, &mut remaining_energy, ); - let res = match res { + match res { Ok((result, changeset, trace_elements)) => { // Charge energy for contract storage. Or return an error if out // of energy. @@ -907,9 +907,7 @@ impl Chain { ) } Err(e) => Err(e), - }; - - res + } } /// Create an account. @@ -1032,7 +1030,7 @@ impl Chain { /// If the `kind` is an out of energy, then `0` is used instead of the /// `remaining_energy` parameter, as it will likely not be `0` due to short /// circuiting during execution. - fn from_invocation_error_kind( + fn convert_to_invoke_error( &self, kind: ContractInvokeErrorKind, energy_reserved: Energy, @@ -1056,7 +1054,7 @@ impl Chain { /// the energy and transaction fee fields based on the `energy_reserved` /// parameter. fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvokeError { - self.from_invocation_error_kind( + self.convert_to_invoke_error( ContractInvokeErrorKind::OutOfEnergy, energy_reserved, Energy::from(0), @@ -1066,7 +1064,7 @@ impl Chain { /// Convert a [`ContractInitErrorKind`] to a /// [`ContractInitError`] by calculating the `energy_used` and /// `transaction_fee`. - fn from_init_error_kind( + fn convert_to_init_error( &self, kind: ContractInitErrorKind, energy_reserved: Energy, @@ -1198,9 +1196,9 @@ impl ContractInvokeSuccess { ) -> BTreeMap> { let mut map: BTreeMap> = BTreeMap::new(); for event in self.trace_elements.iter() { - map.entry(Self::extract_contract_address(&event)) + map.entry(Self::extract_contract_address(event)) .and_modify(|v| v.push(event.clone())) - .or_insert(vec![event.clone()]); + .or_insert_with(|| vec![event.clone()]); } map } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index a69872a2..80fdf04c 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -161,7 +161,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { sender_policies: to_bytes( &self .chain - .account(invoker.into()) + .account(invoker) .expect("Precondition violation: invoker must exist.") .policy, ), From 64658b90cef95ad811faad2db145ca91573b702d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 13:47:25 +0200 Subject: [PATCH 143/208] Documentation. --- contract-testing/src/invocation/impls.rs | 28 +++++++++++++++++++++++- contract-testing/src/invocation/types.rs | 19 +--------------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 80fdf04c..0e98f254 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -21,12 +21,38 @@ use concordium_base::{ }; use concordium_smart_contract_engine::{ v0, - v1::{self, trie}, + v1::{self, trie, InvokeResponse, ReceiveContext, ReceiveInterruptedState}, ExecResult, InterpreterEnergy, }; use concordium_wasm::artifact::{self, CompiledFunction}; use std::collections::{btree_map, BTreeMap}; +/// This auxiliary type is used in `invoke_entrypoint` below to keep track of +/// the "to do list" in the form of a stack. It stores the necessary information +/// to continue execution until all actions have been processed. +enum Next { + /// The next action is to resume execution after handling the interrupt. + Resume { + data: InvocationData, + config: Box>>>, + /// This is [`None`] if we are going to resume after a call to a + /// contract. And [`Some`] if we have an immediate handler that + /// immediately produces a response. + response: Option, + }, + /// The next action is to start executing an entrypoint. + Initial { + sender: Address, + payload: UpdateContractPayload, + /// If execution of the entrypoint fails then it does not produce any + /// trace. We store the trace in one "global" (per transaction) + /// vector. This field is used to determine how far back we need + /// to roll back (i.e., clean up) the elements in that trace in + /// case of contract invocation failure. + trace_elements_checkpoint: usize, + }, +} + impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { fn invoke_entrypoint_initial( &mut self, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 678181b5..1467db66 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -6,13 +6,11 @@ use concordium_base::{ OwnedEntrypointName, }, smart_contracts::OwnedParameter, - transactions::UpdateContractPayload, }; use concordium_smart_contract_engine::{ v0, - v1::{trie::MutableState, InvokeResponse, ReceiveContext, ReceiveInterruptedState}, + v1::{trie::MutableState, InvokeResponse}, }; -use concordium_wasm::artifact::CompiledFunction; use std::collections::BTreeMap; /// The response from invoking an entrypoint. @@ -140,18 +138,3 @@ pub(crate) enum TestConfigurationError { /// means that overflows of amounts cannot occur. BalanceOverflow, } - -pub(super) enum Next { - Resume { - data: InvocationData, - config: Box>>>, - /// This is none if we are going to resume after a call to a contract. - /// And Some if we have an immediate handler. - response: Option, - }, - Initial { - sender: Address, - payload: UpdateContractPayload, - trace_elements_checkpoint: usize, - }, -} From b1a4bcd1bc3e36705bf13a2d24f9bdd4d61589b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Apr 2023 14:28:48 +0200 Subject: [PATCH 144/208] Remove logs since their semantics is awkward and they can be retrieved. --- contract-testing/src/impls.rs | 1 - contract-testing/src/invocation/impls.rs | 8 +----- contract-testing/src/invocation/types.rs | 10 ++----- contract-testing/src/types.rs | 33 ++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 3c1ab969..ae50257c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -685,7 +685,6 @@ impl Chain { return_value: data.unwrap_or_default(), state_changed, new_balance, - events: contract_events_from_logs(result.logs), }) } v1::InvokeResponse::Failure { kind } => Err(self.convert_to_invoke_error( diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 0e98f254..c31aa96a 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -101,7 +101,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Return early. return Ok(Err(InvokeEntrypointResponse { invoke_response: v1::InvokeResponse::Failure { kind }, - logs: v0::Logs::new(), })); } } @@ -114,7 +113,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentContract, }, - logs: v0::Logs::new(), })); } } @@ -160,7 +158,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::NonExistentEntrypoint, }, - logs: v0::Logs::new(), })); } }; @@ -395,7 +392,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invocation_data.contract_name.as_contract_name(), invocation_data.entrypoint.as_entrypoint_name(), ), - events: contract_events_from_logs(logs.clone()), + events: contract_events_from_logs(logs), }, }; // Add update event @@ -414,7 +411,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { new_balance: self.contract_balance_unchecked(invocation_data.address), data: Some(return_value), }, - logs, }); } v1::ReceiveResult::Interrupt { @@ -726,7 +722,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { data: return_value, }, }, - logs: v0::Logs::new(), }); } v1::ReceiveResult::Trap { @@ -741,7 +736,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoke_response: v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }, - logs: v0::Logs::new(), }); } // Convert this to an error here, so that we will short circuit processing. diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 1467db66..4fda1fef 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -7,10 +7,7 @@ use concordium_base::{ }, smart_contracts::OwnedParameter, }; -use concordium_smart_contract_engine::{ - v0, - v1::{trie::MutableState, InvokeResponse}, -}; +use concordium_smart_contract_engine::v1::{trie::MutableState, InvokeResponse}; use std::collections::BTreeMap; /// The response from invoking an entrypoint. @@ -18,9 +15,6 @@ use std::collections::BTreeMap; pub(crate) struct InvokeEntrypointResponse { /// The result from the invoke. pub(crate) invoke_response: InvokeResponse, - /// Logs created during the invocation. - /// Has entries if and only if `invoke_response` is `Success`. - pub(crate) logs: v0::Logs, } /// A type that supports invoking a contract entrypoint. @@ -65,7 +59,7 @@ pub(super) struct AccountChanges { /// The original balance. /// /// For the `invoker`, this will be the `original_balance - reserved_amount` - /// (from `EntrypointInvocationHandler`). + /// (from [`EntrypointInvocationHandler`]). /// /// Should never be modified. pub(super) original_balance: Amount, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index b6e3f73a..a0416c91 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -277,8 +277,37 @@ pub struct ContractInvokeSuccess { pub state_changed: bool, /// The new balance of the smart contract. pub new_balance: Amount, - /// The contract events (logs) produced since the last interrupt. - pub events: Vec, +} + +impl ContractInvokeSuccess { + /// Extract all the events logged by all the contracts in the invocation. + /// The events are returned in the order that they are emitted, and are + /// paired with the address of the contract that emitted it. + pub fn events(&self) -> impl Iterator { + self.trace_elements.iter().flat_map(|cte| { + if let ContractTraceElement::Updated { data } = cte { + Some((data.address, data.events.as_slice())) + } else { + None + } + }) + } + + /// Extract the transfers **to accounts** that occurred during + /// invocation. The return value is an iterator over triples `(from, amount, + /// to)` where `from` is the sender contract, and `to` is the receiver + /// account. The transfers are returned in the order that they occurred. + pub fn account_transfers( + &self, + ) -> impl Iterator + '_ { + self.trace_elements.iter().flat_map(|cte| { + if let ContractTraceElement::Transferred { from, amount, to } = cte { + Some((*from, *amount, *to)) + } else { + None + } + }) + } } /// An error that occured during a [`Chain::contract_update`] or From 01a0712068ee5be8cb4f1f4669a844c58d62163d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Tue, 11 Apr 2023 13:10:47 +0200 Subject: [PATCH 145/208] Remove wrapper type. --- contract-testing/src/impls.rs | 27 +++++------- contract-testing/src/invocation/impls.rs | 54 ++++++++---------------- contract-testing/src/invocation/mod.rs | 4 +- contract-testing/src/invocation/types.rs | 9 +--- 4 files changed, 30 insertions(+), 64 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index ae50257c..3e637b96 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1,8 +1,6 @@ use crate::{ constants, - invocation::{ - ChangeSet, EntrypointInvocationHandler, InvokeEntrypointResponse, TestConfigurationError, - }, + invocation::{ChangeSet, EntrypointInvocationHandler, TestConfigurationError}, types::*, }; use anyhow::anyhow; @@ -19,7 +17,11 @@ use concordium_base::{ }, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; -use concordium_smart_contract_engine::{v0, v1, InterpreterEnergy}; +use concordium_smart_contract_engine::{ + v0, + v1::{self, InvokeResponse}, + InterpreterEnergy, +}; use num_bigint::BigUint; use num_integer::Integer; use std::{collections::BTreeMap, path::Path, sync::Arc}; @@ -603,14 +605,7 @@ impl Chain { amount_reserved_for_energy: Amount, payload: UpdateContractPayload, remaining_energy: &mut Energy, - ) -> Result< - ( - InvokeEntrypointResponse, - ChangeSet, - Vec, - ), - ContractInvokeError, - > { + ) -> Result<(InvokeResponse, ChangeSet, Vec), ContractInvokeError> { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { return Err(self.convert_to_invoke_error( @@ -668,13 +663,13 @@ impl Chain { fn contract_invocation_process_response( &self, - result: InvokeEntrypointResponse, + result: InvokeResponse, trace_elements: Vec, energy_reserved: Energy, remaining_energy: Energy, state_changed: bool, ) -> Result { - match result.invoke_response { + match result { v1::InvokeResponse::Success { new_balance, data } => { let energy_used = energy_reserved - remaining_energy; let transaction_fee = self.parameters.calculate_energy_cost(energy_used); @@ -786,7 +781,7 @@ impl Chain { Ok((result, changeset, trace_elements)) => { // Charge energy for contract storage. Or return an error if out // of energy. - let state_changed = if result.is_success() { + let state_changed = if matches!(result, v1::InvokeResponse::Success { .. }) { let res = changeset.persist( &mut remaining_energy, contract_address, @@ -889,7 +884,7 @@ impl Chain { Ok((result, changeset, trace_elements)) => { // Charge energy for contract storage. Or return an error if out // of energy. - let state_changed = if result.is_success() { + let state_changed = if matches!(result, v1::InvokeResponse::Success { .. }) { changeset .collect_energy_for_state(&mut remaining_energy, contract_address) .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))? diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index c31aa96a..4c7d4a10 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -61,7 +61,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { payload: UpdateContractPayload, trace_elements_checkpoint: usize, ) -> Result< - Result<(v1::ReceiveResult, InvocationData), InvokeEntrypointResponse>, + Result<(v1::ReceiveResult, InvocationData), InvokeResponse>, TestConfigurationError, > { // Charge the base cost for updating a contract. @@ -99,9 +99,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { TransferError::ToMissing => v1::InvokeFailure::NonExistentContract, }; // Return early. - return Ok(Err(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { kind }, - })); + return Ok(Err(v1::InvokeResponse::Failure { kind })); } } } else { @@ -109,10 +107,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { Some(self_balance) => self_balance, None => { // Return early. - return Ok(Err(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentContract, - }, + return Ok(Err(v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentContract, })); } } @@ -154,10 +150,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { ) } else { // Return early. - return Ok(Err(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::NonExistentEntrypoint, - }, + return Ok(Err(v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentEntrypoint, })); } }; @@ -247,7 +241,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - ) -> Result<(InvokeEntrypointResponse, Vec), TestConfigurationError> { + ) -> Result<(InvokeResponse, Vec), TestConfigurationError> { let mut stack = Vec::new(); let mut trace_elements = Vec::new(); stack.push(Next::Initial { @@ -257,7 +251,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }); // Initialized to a dummy value. This will always be set or the function will // terminate with an Err. - let mut invoke_response: Option = None; + let mut invoke_response: Option = None; while let Some(invocation_data) = stack.pop() { let (receive_result, mut invocation_data) = match invocation_data { Next::Resume { @@ -288,7 +282,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let (success, call_response) = match invoke_response .take() .expect("Response should be available") - .invoke_response { v1::InvokeResponse::Success { data: return_value, .. @@ -406,11 +399,9 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { ); } - invoke_response = Some(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Success { - new_balance: self.contract_balance_unchecked(invocation_data.address), - data: Some(return_value), - }, + invoke_response = Some(v1::InvokeResponse::Success { + new_balance: self.contract_balance_unchecked(invocation_data.address), + data: Some(return_value), }); } v1::ReceiveResult::Interrupt { @@ -715,12 +706,10 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Delete the trace of the failed part of the execution. // This is the current behaviour of the on-chain execution. trace_elements.truncate(invocation_data.trace_elements_checkpoint); - invoke_response = Some(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::ContractReject { - code: reason, - data: return_value, - }, + invoke_response = Some(v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::ContractReject { + code: reason, + data: return_value, }, }); } @@ -732,10 +721,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Delete the trace of the failed part of the execution. // This is the current behaviour of the on-chain execution. trace_elements.truncate(invocation_data.trace_elements_checkpoint); - invoke_response = Some(InvokeEntrypointResponse { - invoke_response: v1::InvokeResponse::Failure { - kind: v1::InvokeFailure::RuntimeError, - }, + invoke_response = Some(v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::RuntimeError, }); } // Convert this to an error here, so that we will short circuit processing. @@ -1503,13 +1490,6 @@ impl AccountChanges { } } -impl InvokeEntrypointResponse { - /// Whether the invoke was successful. - pub(crate) fn is_success(&self) -> bool { - matches!(self.invoke_response, v1::InvokeResponse::Success { .. }) - } -} - impl From for TestConfigurationError { fn from(_: InsufficientEnergy) -> Self { Self::OutOfEnergy } } diff --git a/contract-testing/src/invocation/mod.rs b/contract-testing/src/invocation/mod.rs index 4d4b61e1..27bfafd0 100644 --- a/contract-testing/src/invocation/mod.rs +++ b/contract-testing/src/invocation/mod.rs @@ -14,6 +14,4 @@ mod impls; mod types; -pub(crate) use types::{ - ChangeSet, EntrypointInvocationHandler, InvokeEntrypointResponse, TestConfigurationError, -}; +pub(crate) use types::{ChangeSet, EntrypointInvocationHandler, TestConfigurationError}; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 4fda1fef..f9f94a59 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -7,16 +7,9 @@ use concordium_base::{ }, smart_contracts::OwnedParameter, }; -use concordium_smart_contract_engine::v1::{trie::MutableState, InvokeResponse}; +use concordium_smart_contract_engine::v1::trie::MutableState; use std::collections::BTreeMap; -/// The response from invoking an entrypoint. -#[derive(Debug)] -pub(crate) struct InvokeEntrypointResponse { - /// The result from the invoke. - pub(crate) invoke_response: InvokeResponse, -} - /// A type that supports invoking a contract entrypoint. pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// Amount reserved for execution. This is used to return the correct From a8ef8f62b6f20697ac9b649dd88affadda4775b2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 11 Apr 2023 13:37:44 +0200 Subject: [PATCH 146/208] Move `Next` to `types.rs` and fix a few comments --- contract-testing/src/invocation/impls.rs | 55 +++++++----------------- contract-testing/src/invocation/types.rs | 30 ++++++++++++- contract-testing/src/types.rs | 1 + 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 0e98f254..9c9d382c 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -21,39 +21,21 @@ use concordium_base::{ }; use concordium_smart_contract_engine::{ v0, - v1::{self, trie, InvokeResponse, ReceiveContext, ReceiveInterruptedState}, + v1::{self, trie}, ExecResult, InterpreterEnergy, }; use concordium_wasm::artifact::{self, CompiledFunction}; use std::collections::{btree_map, BTreeMap}; -/// This auxiliary type is used in `invoke_entrypoint` below to keep track of -/// the "to do list" in the form of a stack. It stores the necessary information -/// to continue execution until all actions have been processed. -enum Next { - /// The next action is to resume execution after handling the interrupt. - Resume { - data: InvocationData, - config: Box>>>, - /// This is [`None`] if we are going to resume after a call to a - /// contract. And [`Some`] if we have an immediate handler that - /// immediately produces a response. - response: Option, - }, - /// The next action is to start executing an entrypoint. - Initial { - sender: Address, - payload: UpdateContractPayload, - /// If execution of the entrypoint fails then it does not produce any - /// trace. We store the trace in one "global" (per transaction) - /// vector. This field is used to determine how far back we need - /// to roll back (i.e., clean up) the elements in that trace in - /// case of contract invocation failure. - trace_elements_checkpoint: usize, - }, -} - impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { + /// Used for handling the *initial* part of invoking an entrypoint. + /// + /// **Preconditions:** + /// - `invoker` exists + /// - `invoker` has sufficient balance to pay for `remaining_energy` + /// - `sender` exists + /// - if the contract (`contract_address`) exists, then its `module` must + /// also exist. fn invoke_entrypoint_initial( &mut self, invoker: AccountAddress, @@ -276,11 +258,10 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { response, energy, &mut data.state, - false, /* never changes on interrupts that have immediate - * handlers */ + false, /* the state never changes on interrupts that have + * immediate handlers */ // An empty loader is fine currently, as we do not use - // caching - // in this lib. + // caching in this lib. v1::trie::Loader::new(&[][..]), ) })?; @@ -298,10 +279,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } => { let invoke_response = v1::InvokeResponse::Success { // The balance returned by `invoke_entrypoint` - // is - // the balance of the contract called. But we - // are - // interested in the new balance of the caller. + // is the balance of the contract called. But we + // are interested in the new balance of the caller. new_balance: self.contract_balance_unchecked(data.address), data: return_value, }; @@ -314,8 +293,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let state_changed = if !success { self.rollback(); false // We rolled back, so no changes were - // made - // to this contract. + // made to this contract. } else { let mod_idx_after_invoke = self.modification_index(data.address); let state_changed = @@ -343,8 +321,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { &mut data.state, state_changed, // An empty loader is fine currently, as we do not use - // caching - // in this lib. + // caching in this lib. v1::trie::Loader::new(&[][..]), ) })?; diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 1467db66..ae6ba001 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -6,11 +6,13 @@ use concordium_base::{ OwnedEntrypointName, }, smart_contracts::OwnedParameter, + transactions::UpdateContractPayload, }; use concordium_smart_contract_engine::{ v0, - v1::{trie::MutableState, InvokeResponse}, + v1::{trie::MutableState, InvokeResponse, ReceiveContext, ReceiveInterruptedState}, }; +use concordium_wasm::artifact::CompiledFunction; use std::collections::BTreeMap; /// The response from invoking an entrypoint. @@ -40,6 +42,32 @@ pub(crate) struct EntrypointInvocationHandler<'a, 'b> { pub(crate) chain: &'b Chain, } +/// This auxiliary type is used in `invoke_entrypoint` from impls.rs to keep +/// track of the "to do list" in the form of a stack. It stores the necessary +/// information to continue execution until all actions have been processed. +pub(super) enum Next { + /// The next action is to resume execution after handling the interrupt. + Resume { + data: InvocationData, + config: Box>>>, + /// This is [`None`] if we are going to resume after a call to a + /// contract. And [`Some`] if we have an immediate handler that + /// immediately produces a response. + response: Option, + }, + /// The next action is to start executing an entrypoint. + Initial { + sender: Address, + payload: UpdateContractPayload, + /// If execution of the entrypoint fails then it does not produce any + /// trace. We store the trace in one "global" (per transaction) + /// vector. This field is used to determine how far back we need + /// to roll back (i.e., clean up) the elements in that trace in + /// case of contract invocation failure. + trace_elements_checkpoint: usize, + }, +} + /// The set of [`Changes`] represented as a stack. #[derive(Debug, Clone)] pub(crate) struct ChangeSet { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index b6e3f73a..40ea1c08 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -56,6 +56,7 @@ pub struct Chain { /// A smart contract instance. #[derive(Clone, Debug)] pub struct Contract { + /// The address of this contract. pub address: ContractAddress, /// The module which contains this contract. pub module_reference: ModuleReference, From 10612efe977342f1c2c9a3f7130eda26f6a803d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Tue, 11 Apr 2023 19:49:43 +0200 Subject: [PATCH 147/208] Improve checkpointing test to make sure the right updates happened. Set rust version to 1.65. --- contract-testing/src/impls.rs | 14 ++++++++++++++ contract-testing/tests/checkpointing.rs | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 3e637b96..7a3367fb 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1211,6 +1211,20 @@ impl ContractInvokeSuccess { ContractTraceElement::Transferred { from, .. } => *from, } } + + /// Get the contract updates that happened in the transaction. + /// The order is the order of returns. Concretely, if A calls B (and no + /// other calls are made) then first there will be "B updated" event, + /// followed by "A updated", assuming the invocation of both succeeded. + pub fn updates(&self) -> impl Iterator { + self.trace_elements.iter().filter_map(|e| { + if let ContractTraceElement::Updated { data } = e { + Some(data) + } else { + None + } + }) + } } impl From for ContractInitErrorKind { diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 208be2ca..77647db8 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -168,7 +168,7 @@ fn test_case_2() { Amount::zero(), ); - chain + let trace = chain .contract_update( Signer::with_one_key(), ACC_0, @@ -188,6 +188,20 @@ fn test_case_2() { }, ) .expect("Updating contract should succeed"); + // Make sure that we have exactly 3 updates with the right entrypoints (meaning + // that all calls succeeded). + let mut updates = trace.updates(); + // first the inner "a_no_modify" executed to completion. + let update_a_no_modify = updates.next().expect("Expected an event."); + assert_eq!(update_a_no_modify.address, res_init_a.contract_address); + assert_eq!(update_a_no_modify.receive_name, "a.a_no_modify"); + let update_b = updates.next().expect("Expected an event."); + assert_eq!(update_b.address, res_init_b.contract_address); + assert_eq!(update_b.receive_name, "b.b_forward"); + let update_a_modify_proxy = updates.next().expect("Expected an event."); + assert_eq!(update_a_modify_proxy.address, res_init_a.contract_address); + assert_eq!(update_a_modify_proxy.receive_name, "a.a_modify_proxy"); + assert!(updates.next().is_none(), "No more updates expected."); } /// This test has the following call pattern: From cbc9c7d6de91a34c5649aa18a1b4fabe97fc1eb2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 12 Apr 2023 09:54:03 +0200 Subject: [PATCH 148/208] Fix clippy warnings --- contract-testing/tests/upgrades.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 72c31d11..de997e77 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -223,7 +223,7 @@ fn test_missing_module() { // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if success == false)); + ] if !success )); } /// Test upgrading to a module where there isn't a matching contract @@ -295,7 +295,7 @@ fn test_missing_contract() { // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if success == false)); + ] if !success )); } /// Test upgrading twice in the same transaction. The effect of the From d8e00ec281712075629da09b8bc6cb638f9dd250 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 19 Apr 2023 15:59:27 +0200 Subject: [PATCH 149/208] Add helper to extract return values from `ContractInvokeError` --- contract-testing/src/impls.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 7a3367fb..973e787c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1227,6 +1227,22 @@ impl ContractInvokeSuccess { } } +impl ContractInvokeError { + /// Try to extract the value returned. + /// + /// This only returns `Some` if the contract rejected on its own. + /// As opposed to when it runs out of energy, traps, or similar, in which + /// case there won't be a return value. + pub fn return_value(&self) -> Option<&[u8]> { + match &self.kind { + ContractInvokeErrorKind::ExecutionError { + failure_kind: v1::InvokeFailure::ContractReject { data, .. }, + } => Some(data), + _ => None, + } + } +} + impl From for ContractInitErrorKind { #[inline(always)] fn from(_: InsufficientEnergy) -> Self { Self::OutOfEnergy } From fc7c449d7c885a2189e61cbc82875fd79f266745 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 19 Apr 2023 16:07:52 +0200 Subject: [PATCH 150/208] Remove redundant `transfers` method and combine impl blocks --- contract-testing/src/impls.rs | 65 +---------------------------------- contract-testing/src/types.rs | 43 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 65 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 973e787c..aa9f88c0 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -11,10 +11,7 @@ use concordium_base::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, - smart_contracts::{ - ContractEvent, ContractTraceElement, InstanceUpdatedEvent, ModuleSource, WasmModule, - WasmVersion, - }, + smart_contracts::{ContractEvent, ContractTraceElement, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; use concordium_smart_contract_engine::{ @@ -1167,66 +1164,6 @@ impl Signer { } } -impl ContractInvokeSuccess { - /// Get an iterator of all transfers that were made from contracts to - /// accounts. - pub fn transfers(&self) -> impl Iterator + '_ { - self.trace_elements.iter().filter_map(|e| { - if let ContractTraceElement::Transferred { from, amount, to } = e { - Some(Transfer { - from: *from, - amount: *amount, - to: *to, - }) - } else { - None - } - }) - } - - /// Get the trace elements grouped by which contract they originated from. - pub fn trace_elements_per_contract( - &self, - ) -> BTreeMap> { - let mut map: BTreeMap> = BTreeMap::new(); - for event in self.trace_elements.iter() { - map.entry(Self::extract_contract_address(event)) - .and_modify(|v| v.push(event.clone())) - .or_insert_with(|| vec![event.clone()]); - } - map - } - - /// Get the contract address that this event relates to. - /// This means the `address` field for all variant except `Transferred`, - /// where it returns the `from`. - fn extract_contract_address(element: &ContractTraceElement) -> ContractAddress { - match element { - ContractTraceElement::Interrupted { address, .. } => *address, - ContractTraceElement::Resumed { address, .. } => *address, - ContractTraceElement::Upgraded { address, .. } => *address, - ContractTraceElement::Updated { - data: InstanceUpdatedEvent { address, .. }, - } => *address, - ContractTraceElement::Transferred { from, .. } => *from, - } - } - - /// Get the contract updates that happened in the transaction. - /// The order is the order of returns. Concretely, if A calls B (and no - /// other calls are made) then first there will be "B updated" event, - /// followed by "A updated", assuming the invocation of both succeeded. - pub fn updates(&self) -> impl Iterator { - self.trace_elements.iter().filter_map(|e| { - if let ContractTraceElement::Updated { data } = e { - Some(data) - } else { - None - } - }) - } -} - impl ContractInvokeError { /// Try to extract the value returned. /// diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index eff84e63..b001c2f7 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -4,7 +4,7 @@ use concordium_base::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, - smart_contracts::{ContractEvent, ContractTraceElement, WasmVersion}, + smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, }; use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; use concordium_wasm::artifact; @@ -309,6 +309,47 @@ impl ContractInvokeSuccess { } }) } + + /// Get the trace elements grouped by which contract they originated + /// from. + pub fn trace_elements(&self) -> BTreeMap> { + let mut map: BTreeMap> = BTreeMap::new(); + for event in self.trace_elements.iter() { + map.entry(Self::extract_contract_address(event)) + .and_modify(|v| v.push(event.clone())) + .or_insert_with(|| vec![event.clone()]); + } + map + } + + /// Get the contract address that this event relates to. + /// This means the `address` field for all variant except `Transferred`, + /// where it returns the `from`. + fn extract_contract_address(element: &ContractTraceElement) -> ContractAddress { + match element { + ContractTraceElement::Interrupted { address, .. } => *address, + ContractTraceElement::Resumed { address, .. } => *address, + ContractTraceElement::Upgraded { address, .. } => *address, + ContractTraceElement::Updated { + data: InstanceUpdatedEvent { address, .. }, + } => *address, + ContractTraceElement::Transferred { from, .. } => *from, + } + } + + /// Get the contract updates that happened in the transaction. + /// The order is the order of returns. Concretely, if A calls B (and no + /// other calls are made) then first there will be "B updated" event, + /// followed by "A updated", assuming the invocation of both succeeded. + pub fn updates(&self) -> impl Iterator { + self.trace_elements.iter().filter_map(|e| { + if let ContractTraceElement::Updated { data } = e { + Some(data) + } else { + None + } + }) + } } /// An error that occured during a [`Chain::contract_update`] or From 5d8fef7908f1727ed850379cd4e7574fbbe71765 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 19 Apr 2023 16:08:33 +0200 Subject: [PATCH 151/208] Make `Signer` constructors `const` --- contract-testing/src/impls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index aa9f88c0..d57af12e 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1153,10 +1153,10 @@ impl Account { impl Signer { /// Create a signer which always signs with one key. - pub fn with_one_key() -> Self { Self { num_keys: 1 } } + pub const fn with_one_key() -> Self { Self { num_keys: 1 } } /// Create a signer with a non-zero number of keys. - pub fn with_keys(num_keys: u32) -> Result { + pub const fn with_keys(num_keys: u32) -> Result { if num_keys == 0 { return Err(ZeroKeysError); } From 41378b887a0b9fb5567df0322f224f8c5dfadb08 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 19 Apr 2023 16:09:21 +0200 Subject: [PATCH 152/208] Make `module_load_v1*` methods into free functions --- contract-testing/src/impls.rs | 124 +++++++++--------- contract-testing/src/lib.rs | 3 +- .../tests/all_new_host_functions.rs | 2 +- contract-testing/tests/basics.rs | 2 +- contract-testing/tests/checkpointing.rs | 8 +- contract-testing/tests/counter.rs | 2 +- contract-testing/tests/error_codes.rs | 2 +- contract-testing/tests/fallback.rs | 2 +- contract-testing/tests/iterator.rs | 2 +- contract-testing/tests/queries.rs | 20 +-- contract-testing/tests/recorder.rs | 2 +- .../tests/relaxed_restrictions.rs | 4 +- contract-testing/tests/self_balance.rs | 2 +- contract-testing/tests/transfer.rs | 2 +- contract-testing/tests/upgrades.rs | 30 ++--- 15 files changed, 103 insertions(+), 104 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index d57af12e..55a26823 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -155,7 +155,7 @@ impl Chain { /// Deploy a smart contract module. /// /// The `WasmModule` can be loaded from disk with either - /// [`Chain::module_load_v1`] or [`Chain::module_load_v1_raw`]. + /// [`module_load_v1`] or [`module_load_v1_raw`]. /// /// Parameters: /// - `signer`: the signer with a number of keys, which affects the cost. @@ -274,68 +274,6 @@ impl Chain { }) } - /// Load a raw wasm module, i.e. one **without** the prefix of 4 version - /// bytes and 4 module length bytes. - /// The module still has to be a valid V1 smart contract module. - pub fn module_load_v1_raw( - module_path: impl AsRef, - ) -> Result { - let module_path = module_path.as_ref(); - // To avoid reading a giant file, we open the file for reading, check its size - // and then load the contents. - let (mut reader, metadata) = std::fs::File::open(module_path) - .and_then(|reader| reader.metadata().map(|metadata| (reader, metadata))) - .map_err(|e| ModuleLoadError { - path: module_path.to_path_buf(), - kind: e.into(), - })?; - if metadata.len() > MAX_WASM_MODULE_SIZE.into() { - return Err(ModuleLoadError { - path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::ReadModule( - anyhow!("Maximum size of a Wasm module is {}", MAX_WASM_MODULE_SIZE).into(), - ), - }); - } - // We cannot deserialize directly to [`ModuleSource`] as it expects the first - // four bytes to be the length, which it isn't for this raw file. - let mut buffer = Vec::new(); - std::io::Read::read_to_end(&mut reader, &mut buffer).map_err(|e| ModuleLoadError { - path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::OpenFile(e), /* This is unlikely to happen, since - * we already opened it. */ - })?; - Ok(WasmModule { - version: WasmVersion::V1, - source: ModuleSource::from(buffer), - }) - } - - /// Load a v1 wasm module as it is output from `cargo concordium build`, - /// i.e. **including** the prefix of 4 version bytes and 4 module length - /// bytes. - pub fn module_load_v1(module_path: impl AsRef) -> Result { - let module_path = module_path.as_ref(); - // To avoid reading a giant file, we just open the file for reading and then - // parse it as a wasm module, which checks the length up front. - let mut reader = std::fs::File::open(module_path).map_err(|e| ModuleLoadError { - path: module_path.to_path_buf(), - kind: e.into(), - })?; - let module: WasmModule = - concordium_base::common::from_bytes(&mut reader).map_err(|e| ModuleLoadError { - path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::ReadModule(e.into()), - })?; - if module.version != WasmVersion::V1 { - return Err(ModuleLoadError { - path: module_path.to_path_buf(), - kind: ModuleLoadErrorKind::UnsupportedModuleVersion(module.version), - }); - } - Ok(module) - } - /// Initialize a contract. /// /// **Parameters:** @@ -1151,6 +1089,66 @@ impl Account { } } +/// Load a raw wasm module, i.e. one **without** the prefix of 4 version +/// bytes and 4 module length bytes. +/// The module still has to be a valid V1 smart contract module. +pub fn module_load_v1_raw(module_path: impl AsRef) -> Result { + let module_path = module_path.as_ref(); + // To avoid reading a giant file, we open the file for reading, check its size + // and then load the contents. + let (mut reader, metadata) = std::fs::File::open(module_path) + .and_then(|reader| reader.metadata().map(|metadata| (reader, metadata))) + .map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: e.into(), + })?; + if metadata.len() > MAX_WASM_MODULE_SIZE.into() { + return Err(ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::ReadModule( + anyhow!("Maximum size of a Wasm module is {}", MAX_WASM_MODULE_SIZE).into(), + ), + }); + } + // We cannot deserialize directly to [`ModuleSource`] as it expects the first + // four bytes to be the length, which it isn't for this raw file. + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut reader, &mut buffer).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::OpenFile(e), /* This is unlikely to happen, since + * we already opened it. */ + })?; + Ok(WasmModule { + version: WasmVersion::V1, + source: ModuleSource::from(buffer), + }) +} + +/// Load a v1 wasm module as it is output from `cargo concordium build`, +/// i.e. **including** the prefix of 4 version bytes and 4 module length +/// bytes. +pub fn module_load_v1(module_path: impl AsRef) -> Result { + let module_path = module_path.as_ref(); + // To avoid reading a giant file, we just open the file for reading and then + // parse it as a wasm module, which checks the length up front. + let mut reader = std::fs::File::open(module_path).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: e.into(), + })?; + let module: WasmModule = + concordium_base::common::from_bytes(&mut reader).map_err(|e| ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::ReadModule(e.into()), + })?; + if module.version != WasmVersion::V1 { + return Err(ModuleLoadError { + path: module_path.to_path_buf(), + kind: ModuleLoadErrorKind::UnsupportedModuleVersion(module.version), + }); + } + Ok(module) +} + impl Signer { /// Create a signer which always signs with one key. pub const fn with_one_key() -> Self { Self { num_keys: 1 } } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index b4f474ea..93c55953 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -22,7 +22,7 @@ //! .module_deploy_v1( //! Signer::with_one_key(), //! ACC, -//! Chain::module_load_v1("path/to/contract.wasm.v1").unwrap()) +//! module_load_v1("path/to/contract.wasm.v1").unwrap()) //! .unwrap(); //! //! // Initialize a smart contract from the deployed module. @@ -76,6 +76,7 @@ mod constants; mod impls; mod invocation; mod types; +pub use impls::{module_load_v1, module_load_v1_raw}; pub use types::*; // Re-export types. diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 2065fed3..8a67c7d9 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -18,7 +18,7 @@ fn test_all_new_host_functions() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 39c4ece1..2789ee13 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -22,7 +22,7 @@ fn fib_reentry_and_cost_test() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/fib.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/fib.wasm", WASM_TEST_FOLDER)) .expect("Module should exist."), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 77647db8..34a04567 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -30,7 +30,7 @@ fn test_case_1() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -120,7 +120,7 @@ fn test_case_2() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -224,7 +224,7 @@ fn test_case_3() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -297,7 +297,7 @@ fn test_case_4() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index e276d2bf..4f6d4b16 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -17,7 +17,7 @@ fn test_counter() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 8d7416d8..27356061 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -17,7 +17,7 @@ fn test_error_codes() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index 0d78fa81..61e9b6af 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -15,7 +15,7 @@ fn test_fallback() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index de07cef7..941af2c4 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -19,7 +19,7 @@ fn test_iterator() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 292060a2..12e93b73 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -27,7 +27,7 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-account-balance.wasm", WASM_TEST_FOLDER )) @@ -97,7 +97,7 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-account-balance.wasm", WASM_TEST_FOLDER )) @@ -172,7 +172,7 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-account-balance-transfer.wasm", WASM_TEST_FOLDER )) @@ -255,7 +255,7 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-account-balance.wasm", WASM_TEST_FOLDER )) @@ -323,7 +323,7 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-account-balance-missing-account.wasm", WASM_TEST_FOLDER )) @@ -396,7 +396,7 @@ mod query_contract_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", WASM_TEST_FOLDER )) @@ -471,7 +471,7 @@ mod query_contract_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-contract-balance.wasm", WASM_TEST_FOLDER )) @@ -532,7 +532,7 @@ mod query_contract_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-contract-balance-transfer.wasm", WASM_TEST_FOLDER )) @@ -596,7 +596,7 @@ mod query_contract_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-contract-balance-missing-contract.wasm", WASM_TEST_FOLDER )) @@ -658,7 +658,7 @@ mod query_exchange_rates { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER )) diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index fa416b7f..8142cd54 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -15,7 +15,7 @@ fn test_recorder() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 876486db..6b636960 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -29,7 +29,7 @@ fn test_new_parameter_limit() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -119,7 +119,7 @@ fn deploy_and_init() -> (Chain, ContractAddress) { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/self_balance.rs b/contract-testing/tests/self_balance.rs index 814ad91f..be658623 100644 --- a/contract-testing/tests/self_balance.rs +++ b/contract-testing/tests/self_balance.rs @@ -105,7 +105,7 @@ fn deploy_and_init( .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/{}", WASM_TEST_FOLDER, file_name)) + module_load_v1_raw(format!("{}/{}", WASM_TEST_FOLDER, file_name)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 1168bd52..0df9f762 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -15,7 +15,7 @@ fn test_transfer() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index de997e77..4a54e6d1 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -18,7 +18,7 @@ fn test() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -27,7 +27,7 @@ fn test() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -107,7 +107,7 @@ fn test_self_invoke() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -115,7 +115,7 @@ fn test_self_invoke() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -181,7 +181,7 @@ fn test_missing_module() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/upgrading-missing-module.wasm", WASM_TEST_FOLDER )) @@ -239,7 +239,7 @@ fn test_missing_contract() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/upgrading-missing-contract0.wasm", WASM_TEST_FOLDER )) @@ -251,7 +251,7 @@ fn test_missing_contract() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/upgrading-missing-contract1.wasm", WASM_TEST_FOLDER )) @@ -310,7 +310,7 @@ fn test_twice_in_one_transaction() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -319,7 +319,7 @@ fn test_twice_in_one_transaction() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -328,7 +328,7 @@ fn test_twice_in_one_transaction() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -408,7 +408,7 @@ fn test_chained_contract() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -470,7 +470,7 @@ fn test_reject() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -479,7 +479,7 @@ fn test_reject() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) + module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -561,7 +561,7 @@ fn test_changing_entrypoint() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints0.wasm", WASM_TEST_FOLDER )) @@ -573,7 +573,7 @@ fn test_changing_entrypoint() { .module_deploy_v1( Signer::with_one_key(), ACC_0, - Chain::module_load_v1_raw(format!( + module_load_v1_raw(format!( "{}/upgrading-changing-entrypoints1.wasm", WASM_TEST_FOLDER )) From 638787cbd8001968975fc96c63956470b57267b2 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 19 Apr 2023 16:14:31 +0200 Subject: [PATCH 153/208] Explain how to add lib to project including the 2021 edition requirement --- contract-testing/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 93c55953..c65a2013 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -3,6 +3,19 @@ //! This library supports writing integration tests in Rust for Concordium smart //! contracts. //! +//! To use the library, you must add it to your `Cargo.toml` file under the +//! `[dev-dependencies]` section. The library requries the rust edition `2021` +//! or greater. +//! +//! ```toml +//! [package] +//! # ... +//! edition = "2021" +//! +//! [dev-dependencies] +//! concordium-smart-contract-testing = "1.0" +//! ``` +//! //! ## Basic usage //! //! ```no_run From 2a365589fd92e06beb8ddbd6887c0f7cc8008041 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 21 Apr 2023 11:09:47 +0200 Subject: [PATCH 154/208] Derive `Error` where missing --- contract-testing/src/types.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index b001c2f7..16695422 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -111,7 +111,8 @@ pub struct ModuleDeploySuccess { } /// An error that occured while deploying a [`ContractModule`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Module deployment failed due to: {kind}")] pub struct ModuleDeployError { /// The energy used for deployment. pub energy_used: Energy, @@ -193,7 +194,8 @@ pub struct ContractInitSuccess { } /// An error that occured in [`Chain::contract_init`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Contract initialization failed due to: {kind}")] pub struct ContractInitError { /// Energy used. pub energy_used: Energy, @@ -354,7 +356,8 @@ impl ContractInvokeSuccess { /// An error that occured during a [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("Contract invoke or update failed due to: {kind}")] pub struct ContractInvokeError { /// The energy used. pub energy_used: Energy, From 3b1639baed8839d2d6fd2fb5d143204eba4e1aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 24 Apr 2023 08:26:42 +0200 Subject: [PATCH 155/208] FIx some bugs related to policies and state generation management. --- contract-testing/src/impls.rs | 27 +++++- contract-testing/src/invocation/impls.rs | 114 +++++++++++++---------- contract-testing/src/invocation/types.rs | 2 + contract-testing/src/types.rs | 17 +++- 4 files changed, 104 insertions(+), 56 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 55a26823..dd48d15a 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -398,13 +398,35 @@ impl Chain { }); } + + // Sender policies have a very bespoke serialization in + // order to allow skipping portions of them in smart contracts. + let sender_policies = { + // TODO: Add this to where policies are defined. + let policy = + &account_info + .policy; + // There is only a single policy. This is not the same as on the chain. + let mut out = vec![1,0]; // there is a single policy, encoded in little endian. + out.extend_from_slice(&policy.identity_provider.to_le_bytes()); + out.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); + out.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); + out.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); + for (tag, value) in &policy.items { + out.push(tag.0); + out.push(value.len() as u8); + out.extend_from_slice(value.as_ref()); + } + out + }; + // Construct the context. let init_ctx = v0::InitContext { metadata: ChainMetadata { slot_time: self.parameters.block_time, }, init_origin: sender, - sender_policies: contracts_common::to_bytes(&account_info.policy), + sender_policies, }; // Initialize contract // We create an empty loader as no caching is used in this testing library @@ -584,6 +606,7 @@ impl Chain { chain: self, reserved_amount: amount_reserved_for_energy, invoker, + next_contract_modification_index: 1, }; match contract_invocation.invoke_entrypoint(invoker, sender, payload) { @@ -842,7 +865,7 @@ impl Chain { /// Create an account. /// /// If an account with a matching address already exists this method will - /// replace it and return it. + /// replace it and return the old account. /// /// Note that if the first 29-bytes of an account are identical, then /// they are *considered aliases* on each other in all methods. diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index ba9d550a..f1e12679 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -142,6 +142,29 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { self.remaining_energy .tick_energy(lookup_module_cost(&module))?; + // Sender policies have a very bespoke serialization in + // order to allow skipping portions of them in smart contracts. + let sender_policies = { + // TODO: Add this to where policies are defined. + let policy = &self + .chain + .account(invoker) + .expect("Precondition violation: invoker must exist.") + .policy; + // There is only a single policy. This is not the same as on the chain. + let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. + out.extend_from_slice(&policy.identity_provider.to_le_bytes()); + out.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); + out.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); + out.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); + for (tag, value) in &policy.items { + out.push(tag.0); + out.push(value.len() as u8); + out.extend_from_slice(value.as_ref()); + } + out + }; + // Construct the receive context let receive_ctx = v1::ReceiveContext { // This should be the entrypoint specified, even if we end up @@ -157,21 +180,16 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { self_balance: instance_self_balance, sender, owner: instance.owner, - sender_policies: to_bytes( - &self - .chain - .account(invoker) - .expect("Precondition violation: invoker must exist.") - .policy, - ), + sender_policies, }, }; - let mod_idx_before_invoke = self.modification_index(payload.address); + let mod_idx_before_invoke = self.next_contract_modification_index; // Construct the instance state let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. let mut mutable_state = self.contract_state(payload.address); + let mut mutable_state = mutable_state.make_fresh_generation(&mut loader); let inner = mutable_state.get_inner(&mut loader); let instance_state = v1::InstanceState::new(loader, inner); @@ -292,6 +310,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Update the state field with the newest value from the // changeset. data.state = self.contract_state(data.address); + // TODO: This change fixes some costs, but maybe should not be necessary. + data.state = self.contract_state(data.address); } state_changed }; @@ -397,6 +417,14 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { address: invocation_data.address, events: contract_events_from_logs(logs), }; + // Remember what state we are in before invoking. + // This is used to report, upon resume, whether the contracts's + // state has changed. + invocation_data.mod_idx_before_invoke = if state_changed { + self.save_state_changes(invocation_data.address, &mut invocation_data.state) + } else { + self.modification_index(invocation_data.address) + }; match interrupt { v1::Interrupt::Transfer { to, amount } => { // Add the interrupt event @@ -492,24 +520,10 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }); } Some(contract_name) => { - if state_changed { - self.save_state_changes( - invocation_data.address, - &mut invocation_data.state, - true, - ); - } - // Make a checkpoint before calling another contract so that we // may roll back. self.checkpoint(); - // Remember what state we are in before invoking. - // This is used to report, upon resume, whether the contracts's - // state has changed. - invocation_data.mod_idx_before_invoke = - self.modification_index(invocation_data.address); - let receive_name = OwnedReceiveName::construct_unchecked( contract_name, name.as_entrypoint_name(), @@ -1001,10 +1015,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// Saves a mutable state for a contract in the changeset. /// - /// If `with_fresh_generation`, then it will use the - /// [`MutableState::make_fresh_generation`][make_fresh_generation] - /// function, otherwise it will make a clone. - /// /// If the contract already has an entry in the changeset, the old state /// will be replaced. Otherwise, the entry is created and the state is /// added. @@ -1014,20 +1024,13 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// /// **Preconditions:** /// - Contract must exist. - /// - /// [make_fresh_generation]: trie::MutableState::make_fresh_generation fn save_state_changes( &mut self, address: ContractAddress, state: &mut trie::MutableState, - with_fresh_generation: bool, - ) { - let state = if with_fresh_generation { - let mut loader = v1::trie::Loader::new(&[][..]); // An empty loader is fine currently, as we do not use caching in this lib. - state.make_fresh_generation(&mut loader) - } else { - state.clone() - }; + ) -> u32 { + let state = state.clone(); + let modification_index = self.next_contract_modification_index; match self.changeset.current_mut().contracts.entry(address) { btree_map::Entry::Vacant(vac) => { let original_balance = self @@ -1038,16 +1041,18 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { .self_balance; vac.insert(ContractChanges { state: Some(state), - modification_index: 1, // Increment from default, 0, to 1. + modification_index, ..ContractChanges::new(original_balance) }); } btree_map::Entry::Occupied(mut occ) => { let changes = occ.get_mut(); changes.state = Some(state); - changes.modification_index += 1; + changes.modification_index = modification_index; } } + self.next_contract_modification_index += 1; + modification_index } /// Saves a new module reference for the contract in the changeset. @@ -1164,12 +1169,15 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { state_changed, return_value, remaining_energy, - } => Ok(v1::ReceiveResult::Success { - logs, - state_changed, - return_value, - remaining_energy: subtract_then_convert(remaining_energy)?, - }), + } => { + let remaining_energy = subtract_then_convert(remaining_energy)?; + Ok(v1::ReceiveResult::Success { + logs, + state_changed, + return_value, + remaining_energy, + }) + } v1::ReceiveResult::Interrupt { remaining_energy, @@ -1177,13 +1185,17 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { logs, config, interrupt, - } => Ok(v1::ReceiveResult::Interrupt { - remaining_energy: subtract_then_convert(remaining_energy)?, - state_changed, - logs, - config, - interrupt, - }), + } => { + let remaining_energy = subtract_then_convert(remaining_energy)?; + + Ok(v1::ReceiveResult::Interrupt { + remaining_energy, + state_changed, + logs, + config, + interrupt, + }) + } v1::ReceiveResult::Reject { reason, return_value, diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 18808640..01e8b0df 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -29,6 +29,8 @@ pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// The energy remaining for execution. pub(crate) remaining_energy: &'a mut Energy, pub(crate) chain: &'b Chain, + // TODO: Need to revert on failures. + pub(crate) next_contract_modification_index: u32, } /// This auxiliary type is used in `invoke_entrypoint` from impls.rs to keep diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index b001c2f7..1230fb0a 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -111,7 +111,10 @@ pub struct ModuleDeploySuccess { } /// An error that occured while deploying a [`ContractModule`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error( + "Module deployment failed after consuming {energy_used} ({transaction_fee}) with error {kind}." +)] pub struct ModuleDeployError { /// The energy used for deployment. pub energy_used: Energy, @@ -193,7 +196,11 @@ pub struct ContractInitSuccess { } /// An error that occured in [`Chain::contract_init`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error( + "Contract initialization failed after consuming {energy_used} ({transaction_fee}) energy with \ + error {kind}." +)] pub struct ContractInitError { /// Energy used. pub energy_used: Energy, @@ -354,7 +361,11 @@ impl ContractInvokeSuccess { /// An error that occured during a [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. -#[derive(Debug)] +#[derive(Debug, Error)] +#[error( + "Contract invocation failed after using {energy_used} ({transaction_fee}) energy with error \ + {kind}." +)] pub struct ContractInvokeError { /// The energy used. pub energy_used: Energy, From 59570c111ceecb8b1c84705a77f34cacd1c7ff72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 24 Apr 2023 08:37:58 +0200 Subject: [PATCH 156/208] Format. --- contract-testing/src/impls.rs | 11 ++++------ contract-testing/src/invocation/impls.rs | 3 ++- contract-testing/src/invocation/types.rs | 8 +++---- contract-testing/tests/queries.rs | 28 +++++++----------------- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index dd48d15a..41bf573a 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -398,16 +398,13 @@ impl Chain { }); } - // Sender policies have a very bespoke serialization in // order to allow skipping portions of them in smart contracts. let sender_policies = { // TODO: Add this to where policies are defined. - let policy = - &account_info - .policy; + let policy = &account_info.policy; // There is only a single policy. This is not the same as on the chain. - let mut out = vec![1,0]; // there is a single policy, encoded in little endian. + let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. out.extend_from_slice(&policy.identity_provider.to_le_bytes()); out.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); out.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); @@ -422,10 +419,10 @@ impl Chain { // Construct the context. let init_ctx = v0::InitContext { - metadata: ChainMetadata { + metadata: ChainMetadata { slot_time: self.parameters.block_time, }, - init_origin: sender, + init_origin: sender, sender_policies, }; // Initialize contract diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 83787bd5..f8146aa1 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -310,7 +310,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Update the state field with the newest value from the // changeset. data.state = self.contract_state(data.address); - // TODO: This change fixes some costs, but maybe should not be necessary. + // TODO: This change fixes some costs, but maybe should not be + // necessary. data.state = self.contract_state(data.address); } state_changed diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 01e8b0df..7e3c6ed6 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -18,17 +18,17 @@ use std::collections::BTreeMap; pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// Amount reserved for execution. This is used to return the correct /// balance of the invoker account. - pub(crate) reserved_amount: Amount, + pub(crate) reserved_amount: Amount, /// Address of the invoker of the transaction. This is used to return the /// correct balance of the invoker account. - pub(crate) invoker: AccountAddress, + pub(crate) invoker: AccountAddress, /// The changeset which keeps track of /// changes to accounts, modules, and contracts that occur during an /// invocation. - pub(crate) changeset: ChangeSet, + pub(crate) changeset: ChangeSet, /// The energy remaining for execution. pub(crate) remaining_energy: &'a mut Energy, - pub(crate) chain: &'b Chain, + pub(crate) chain: &'b Chain, // TODO: Need to revert on failures. pub(crate) next_contract_modification_index: u32, } diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 12e93b73..2c6702ae 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -27,11 +27,8 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - module_load_v1_raw(format!( - "{}/queries-account-balance.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -97,11 +94,8 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - module_load_v1_raw(format!( - "{}/queries-account-balance.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -255,11 +249,8 @@ mod query_account_balance { .module_deploy_v1( Signer::with_one_key(), ACC_0, - module_load_v1_raw(format!( - "{}/queries-account-balance.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -658,11 +649,8 @@ mod query_exchange_rates { .module_deploy_v1( Signer::with_one_key(), ACC_0, - module_load_v1_raw(format!( - "{}/queries-exchange-rates.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + module_load_v1_raw(format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER)) + .expect("module should exist"), ) .expect("Deploying valid module should work"); From 44b6eb67c2f755f53c4d909f1bf528029602fd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 24 Apr 2023 09:14:24 +0200 Subject: [PATCH 157/208] Fix policy serialization. --- contract-testing/src/impls.rs | 17 ++++++++++------- contract-testing/src/invocation/impls.rs | 17 ++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 41bf573a..5fcd4a08 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -405,15 +405,18 @@ impl Chain { let policy = &account_info.policy; // There is only a single policy. This is not the same as on the chain. let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. - out.extend_from_slice(&policy.identity_provider.to_le_bytes()); - out.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); - out.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); - out.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); + let mut inner = Vec::new(); + inner.extend_from_slice(&policy.identity_provider.to_le_bytes()); + inner.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); + inner.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); + inner.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); for (tag, value) in &policy.items { - out.push(tag.0); - out.push(value.len() as u8); - out.extend_from_slice(value.as_ref()); + inner.push(tag.0); + inner.push(value.len() as u8); + inner.extend_from_slice(value.as_ref()); } + out.extend_from_slice(&(inner.len() as u16).to_le_bytes()); + out.extend_from_slice(&inner); out }; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index f8146aa1..4999a2c7 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -153,15 +153,18 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { .policy; // There is only a single policy. This is not the same as on the chain. let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. - out.extend_from_slice(&policy.identity_provider.to_le_bytes()); - out.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); - out.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); - out.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); + let mut inner = Vec::new(); + inner.extend_from_slice(&policy.identity_provider.to_le_bytes()); + inner.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); + inner.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); + inner.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); for (tag, value) in &policy.items { - out.push(tag.0); - out.push(value.len() as u8); - out.extend_from_slice(value.as_ref()); + inner.push(tag.0); + inner.push(value.len() as u8); + inner.extend_from_slice(value.as_ref()); } + out.extend_from_slice(&(inner.len() as u16).to_le_bytes()); + out.extend_from_slice(&inner); out }; From d0690fc1fca616e9ab00fa0c331273d2c207f599 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 2 May 2023 14:44:02 +0200 Subject: [PATCH 158/208] Use helper function for policy serial and roll back next mod idx --- contract-testing/src/impls.rs | 23 ++++++----------- contract-testing/src/invocation/impls.rs | 32 +++++++++--------------- contract-testing/src/invocation/types.rs | 7 ++++++ 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 5fcd4a08..413a8cf3 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -401,22 +401,11 @@ impl Chain { // Sender policies have a very bespoke serialization in // order to allow skipping portions of them in smart contracts. let sender_policies = { - // TODO: Add this to where policies are defined. - let policy = &account_info.policy; - // There is only a single policy. This is not the same as on the chain. - let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. - let mut inner = Vec::new(); - inner.extend_from_slice(&policy.identity_provider.to_le_bytes()); - inner.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); - inner.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); - inner.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); - for (tag, value) in &policy.items { - inner.push(tag.0); - inner.push(value.len() as u8); - inner.extend_from_slice(value.as_ref()); - } - out.extend_from_slice(&(inner.len() as u16).to_le_bytes()); - out.extend_from_slice(&inner); + let mut out = Vec::new(); + account_info + .policy + .serial_for_smart_contract(&mut out) + .expect("Writing to a vector should succeed."); out }; @@ -606,6 +595,8 @@ impl Chain { chain: self, reserved_amount: amount_reserved_for_energy, invoker, + // Starts at 1 since 0 is the "initial state" of all contracts in the current + // transaction. next_contract_modification_index: 1, }; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 4999a2c7..c5164e0b 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -145,26 +145,15 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Sender policies have a very bespoke serialization in // order to allow skipping portions of them in smart contracts. let sender_policies = { - // TODO: Add this to where policies are defined. + let mut out = Vec::new(); let policy = &self .chain .account(invoker) .expect("Precondition violation: invoker must exist.") .policy; - // There is only a single policy. This is not the same as on the chain. - let mut out = vec![1, 0]; // there is a single policy, encoded in little endian. - let mut inner = Vec::new(); - inner.extend_from_slice(&policy.identity_provider.to_le_bytes()); - inner.extend_from_slice(&policy.created_at.timestamp_millis().to_le_bytes()); - inner.extend_from_slice(&policy.valid_to.timestamp_millis().to_le_bytes()); - inner.extend_from_slice(&(policy.items.len() as u16).to_le_bytes()); - for (tag, value) in &policy.items { - inner.push(tag.0); - inner.push(value.len() as u8); - inner.extend_from_slice(value.as_ref()); - } - out.extend_from_slice(&(inner.len() as u16).to_le_bytes()); - out.extend_from_slice(&inner); + policy + .serial_for_smart_contract(&mut out) + .expect("Writing to a vector should succeed."); out }; @@ -227,6 +216,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { amount: payload.amount, state: mutable_state, trace_elements_checkpoint, + next_mod_idx_checkpoint: mod_idx_before_invoke, mod_idx_before_invoke, }))) } @@ -313,9 +303,6 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Update the state field with the newest value from the // changeset. data.state = self.contract_state(data.address); - // TODO: This change fixes some costs, but maybe should not be - // necessary. - data.state = self.contract_state(data.address); } state_changed }; @@ -700,6 +687,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Delete the trace of the failed part of the execution. // This is the current behaviour of the on-chain execution. trace_elements.truncate(invocation_data.trace_elements_checkpoint); + // Reset the next modification index as well. + self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { kind: v1::InvokeFailure::ContractReject { code: reason, @@ -715,6 +704,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // Delete the trace of the failed part of the execution. // This is the current behaviour of the on-chain execution. trace_elements.truncate(invocation_data.trace_elements_checkpoint); + // Reset the next modification index as well. + self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { kind: v1::InvokeFailure::RuntimeError, }); @@ -1022,8 +1013,9 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// will be replaced. Otherwise, the entry is created and the state is /// added. /// - /// This also increments the modification index. It will be set to 1 if the - /// contract has no entry in the changeset. + /// This method also increments the `self.next_contract_modification_index`. + /// + /// Returns the `modification_index` set for the contract. /// /// **Preconditions:** /// - Contract must exist. diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 7e3c6ed6..f42db92a 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -135,6 +135,13 @@ pub(super) struct InvocationData { /// A checkpoint in the list of trace elements. /// We reset to this size in case of failure of execution. pub(super) trace_elements_checkpoint: usize, + /// A checkpoint on the next modification index. + /// We reset `next_modification_index` to this value in case of failure of + /// execution. + pub(super) next_mod_idx_checkpoint: u32, + /// The modification index before making an invocation. + /// Differs from the `next_mod_idx_checkpoint` in that this value can be + /// altered during the execution of a single entrypoint. pub(super) mod_idx_before_invoke: u32, } From 7ee97a63ee42c4940512abce20936f604aa4bc53 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 2 May 2023 16:52:28 +0200 Subject: [PATCH 159/208] Fix name as it refers to multiple rates --- contract-testing/src/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 413a8cf3..5159ae3f 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1026,7 +1026,7 @@ impl Chain { /// /// Will fail if they result in the cost of one energy being larger than /// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. - pub fn set_exchange_rate( + pub fn set_exchange_rates( &mut self, micro_ccd_per_euro: ExchangeRate, euro_per_energy: ExchangeRate, From 6cadbc3d47bd8af4ee3a1de05494e8da544f5257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Fri, 5 May 2023 21:10:54 +0200 Subject: [PATCH 160/208] Merge remote-tracking branch 'origin/main' into misc-bugfixes --- contract-testing/Cargo.toml | 6 +++--- contract-testing/src/invocation/types.rs | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index cc724bcd..86d59a39 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -7,9 +7,9 @@ rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {version = "1.0", path = "../concordium-base/rust-src/concordium_base"} -concordium-smart-contract-engine = {version = "1.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} -concordium-wasm = {version = "1.0", path = "../concordium-base/smart-contracts/wasm-transform"} +concordium_base = {version = "1.2", path = "../concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {version = "1.2", path = "../concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {version = "1.1", path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index f42db92a..f8ccb320 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -29,7 +29,6 @@ pub(crate) struct EntrypointInvocationHandler<'a, 'b> { /// The energy remaining for execution. pub(crate) remaining_energy: &'a mut Energy, pub(crate) chain: &'b Chain, - // TODO: Need to revert on failures. pub(crate) next_contract_modification_index: u32, } From 922446ce6a840b22e8c0d27812a0be4267a227f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Fri, 5 May 2023 21:55:20 +0200 Subject: [PATCH 161/208] Add metadata to the crate to prepare for publication. --- contract-testing/Cargo.toml | 5 +++++ contract-testing/src/invocation/types.rs | 4 ++++ contract-testing/src/types.rs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 86d59a39..a39ba46f 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -3,6 +3,11 @@ name = "concordium-smart-contract-testing" version = "1.0.0" edition = "2021" rust-version = "1.65" +license = "MPL 2.0" +description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." +homepage = "https://github.com/Concordium/concordium-smart-contract-tools" +reposity = "https://github.com/Concordium/concordium-smart-contract-tools" +exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index f8ccb320..09502fc9 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -59,6 +59,10 @@ pub(super) enum Next { } /// The set of [`Changes`] represented as a stack. +// For maintainers. It would be better if `Changes` had a form of copy-on-write. +// At the moment we make a full clone of the changes when we need to checkpoint. +// This is OKish, since people generally don't have that complex protocols, but can be made more +// robust, resistant to malicious input. #[derive(Debug, Clone)] pub(crate) struct ChangeSet { /// The stack of changes. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 938e338a..a6962ee2 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -32,7 +32,7 @@ pub(crate) struct ChainParameters { /// Euro per Energy ratio. // This is not public because we ensure a reasonable value during the construction of the // [`Chain`]. - pub(crate) euro_per_energy: ExchangeRate, + pub(crate) euro_per_energy: ExchangeRate, } /// Represents the blockchain and supports a number of operations, including From e150bd692068819042134b4bd48ec6cabae6e4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 8 May 2023 09:55:22 +0200 Subject: [PATCH 162/208] Add README for the testing library. --- contract-testing/Cargo.toml | 1 + contract-testing/README.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 contract-testing/README.md diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index a39ba46f..9de17414 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -4,6 +4,7 @@ version = "1.0.0" edition = "2021" rust-version = "1.65" license = "MPL 2.0" +readme = "README.md" description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." homepage = "https://github.com/Concordium/concordium-smart-contract-tools" reposity = "https://github.com/Concordium/concordium-smart-contract-tools" diff --git a/contract-testing/README.md b/contract-testing/README.md new file mode 100644 index 00000000..a2ff49f5 --- /dev/null +++ b/contract-testing/README.md @@ -0,0 +1,17 @@ +## Concordium Smart Contract Testing + +A library that supports writing integration tests in Rust for Concordium smart +contracts. + +This is a companion to [concordium-std](https://crates.io/crates/concordium-std) +which is used to write smart contracts in Rust. + +### Documentation + +- [docs.rs](https://docs.rs/concordium-smart-contract-testing) + + +### MSRV + +The minimum supported Rust version can be found in the `Cargo.toml` manifest +file. Changes in MSRV will be accompanied by at least a minor version bump. From 6bf7f31edd95c1aade02384b29e2b7f74cdacb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 8 May 2023 09:57:56 +0200 Subject: [PATCH 163/208] Fix license identifier. --- contract-testing/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 9de17414..4bef7f29 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -3,7 +3,7 @@ name = "concordium-smart-contract-testing" version = "1.0.0" edition = "2021" rust-version = "1.65" -license = "MPL 2.0" +license = "MPL-2.0" readme = "README.md" description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." homepage = "https://github.com/Concordium/concordium-smart-contract-tools" From 08e28681928bea7355813ae798507f18124ba44b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 12 May 2023 12:01:47 +0200 Subject: [PATCH 164/208] Include trace elements for failures --- contract-testing/src/impls.rs | 30 ++++- contract-testing/src/invocation/impls.rs | 136 +++++++++++++++++++---- contract-testing/src/invocation/types.rs | 8 ++ contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 85 ++++++++++++-- contract-testing/tests/checkpointing.rs | 27 ++++- contract-testing/tests/queries.rs | 86 ++++++++------ contract-testing/tests/transfer.rs | 53 ++++----- contract-testing/tests/upgrades.rs | 112 ++++++++++++------- 9 files changed, 397 insertions(+), 142 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 5159ae3f..8f2e61bc 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -11,7 +11,7 @@ use concordium_base::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, - smart_contracts::{ContractEvent, ContractTraceElement, ModuleSource, WasmModule, WasmVersion}, + smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; use concordium_smart_contract_engine::{ @@ -551,7 +551,7 @@ impl Chain { amount_reserved_for_energy: Amount, payload: UpdateContractPayload, remaining_energy: &mut Energy, - ) -> Result<(InvokeResponse, ChangeSet, Vec), ContractInvokeError> { + ) -> Result<(InvokeResponse, ChangeSet, Vec), ContractInvokeError> { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { return Err(self.convert_to_invoke_error( @@ -559,6 +559,7 @@ impl Chain { address: payload.address, } .into(), + Vec::new(), energy_reserved, *remaining_energy, )); @@ -568,6 +569,7 @@ impl Chain { if payload.message.as_ref().len() > contracts_common::constants::MAX_PARAMETER_LEN { return Err(self.convert_to_invoke_error( ContractInvokeErrorKind::ParameterTooLarge, + Vec::new(), energy_reserved, *remaining_energy, )); @@ -584,6 +586,7 @@ impl Chain { { return Err(self.convert_to_invoke_error( ContractInvokeErrorKind::AmountTooLarge, + Vec::new(), energy_reserved, *remaining_energy, )); @@ -592,6 +595,7 @@ impl Chain { let mut contract_invocation = EntrypointInvocationHandler { changeset: ChangeSet::new(), remaining_energy, + energy_reserved, chain: self, reserved_amount: amount_reserved_for_energy, invoker, @@ -604,16 +608,19 @@ impl Chain { Ok((result, trace_elements)) => { Ok((result, contract_invocation.changeset, trace_elements)) } - Err(err) => { - Err(self.convert_to_invoke_error(err.into(), energy_reserved, *remaining_energy)) - } + Err(err) => Err(self.convert_to_invoke_error( + err.into(), + Vec::new(), + energy_reserved, + *remaining_energy, + )), } } fn contract_invocation_process_response( &self, result: InvokeResponse, - trace_elements: Vec, + trace_elements: Vec, energy_reserved: Energy, remaining_energy: Energy, state_changed: bool, @@ -633,6 +640,7 @@ impl Chain { } v1::InvokeResponse::Failure { kind } => Err(self.convert_to_invoke_error( ContractInvokeErrorKind::ExecutionError { failure_kind: kind }, + trace_elements, energy_reserved, remaining_energy, )), @@ -671,6 +679,7 @@ impl Chain { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), }); } @@ -680,6 +689,7 @@ impl Chain { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), @@ -701,6 +711,7 @@ impl Chain { .ok_or(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::OutOfEnergy, })?; @@ -713,6 +724,7 @@ impl Chain { return Err(ContractInvokeError { energy_used, transaction_fee: self.parameters.calculate_energy_cost(energy_used), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -791,6 +803,7 @@ impl Chain { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), }); } @@ -799,6 +812,7 @@ impl Chain { return Err(ContractInvokeError { energy_used: Energy::from(0), transaction_fee: Amount::zero(), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), @@ -813,6 +827,7 @@ impl Chain { return Err(ContractInvokeError { energy_used, transaction_fee: self.parameters.calculate_energy_cost(energy_used), + trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InsufficientFunds, }); } @@ -976,6 +991,7 @@ impl Chain { fn convert_to_invoke_error( &self, kind: ContractInvokeErrorKind, + trace_elements: Vec, energy_reserved: Energy, remaining_energy: Energy, ) -> ContractInvokeError { @@ -989,6 +1005,7 @@ impl Chain { ContractInvokeError { energy_used, transaction_fee, + trace_elements, kind, } } @@ -999,6 +1016,7 @@ impl Chain { fn invocation_out_of_energy_error(&self, energy_reserved: Energy) -> ContractInvokeError { self.convert_to_invoke_error( ContractInvokeErrorKind::OutOfEnergy, + Vec::new(), energy_reserved, Energy::from(0), ) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index c5164e0b..4f6d70eb 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -6,13 +6,14 @@ use crate::{ to_interpreter_energy, }, types::{Account, BalanceError, Contract, ContractModule, TransferError}, + DebugTraceElement, ExecutionError, InvokeExecutionError, }; use concordium_base::{ base::{AccountAddressEq, Energy, InsufficientEnergy}, constants::MAX_PARAMETER_LEN, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ExchangeRates, ModuleReference, OwnedReceiveName, + ExchangeRates, ModuleReference, OwnedEntrypointName, OwnedReceiveName, }, smart_contracts::{ ContractTraceElement, InstanceUpdatedEvent, OwnedContractName, OwnedParameter, WasmVersion, @@ -234,7 +235,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { invoker: AccountAddress, sender: Address, payload: UpdateContractPayload, - ) -> Result<(InvokeResponse, Vec), TestConfigurationError> { + ) -> Result<(InvokeResponse, Vec), TestConfigurationError> { let mut stack = Vec::new(); let mut trace_elements = Vec::new(); stack.push(Next::Initial { @@ -313,7 +314,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { success, }; - trace_elements.push(resume_event); + self.push_regular_trace_element( + &mut trace_elements, + resume_event, + data.entrypoint.clone(), + ); let receive_result = self.run_interpreter(|energy| { v1::resume_receive( config, @@ -377,7 +382,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, }; // Add update event - trace_elements.push(update_event); + self.push_regular_trace_element( + &mut trace_elements, + update_event, + invocation_data.entrypoint.clone(), + ); + // Save changes to changeset. if state_changed { self.save_state_changes( @@ -418,7 +428,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match interrupt { v1::Interrupt::Transfer { to, amount } => { // Add the interrupt event - trace_elements.push(interrupt_event); + self.push_regular_trace_element( + &mut trace_elements, + interrupt_event, + invocation_data.entrypoint.clone(), + ); let response = match self.transfer_from_contract_to_account( amount, @@ -454,17 +468,27 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let success = matches!(response, v1::InvokeResponse::Success { .. }); if success { // Add transfer event - trace_elements.push(ContractTraceElement::Transferred { + let transfer_event = ContractTraceElement::Transferred { from: invocation_data.address, amount, to, - }); + }; + self.push_regular_trace_element( + &mut trace_elements, + transfer_event, + invocation_data.entrypoint.clone(), + ); } // Add resume event - trace_elements.push(ContractTraceElement::Resumed { + let resume_event = ContractTraceElement::Resumed { address: invocation_data.address, success, - }); + }; + self.push_regular_trace_element( + &mut trace_elements, + resume_event, + invocation_data.entrypoint.clone(), + ); self.remaining_energy.tick_energy( concordium_base::transactions::cost::SIMPLE_TRANSFER, @@ -484,7 +508,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { amount, } => { // Add the interrupt event - trace_elements.push(interrupt_event); + self.push_regular_trace_element( + &mut trace_elements, + interrupt_event, + invocation_data.entrypoint.clone(), + ); match self .chain @@ -502,7 +530,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { address: invocation_data.address, success: false, }; - trace_elements.push(resume_event); + self.push_regular_trace_element( + &mut trace_elements, + resume_event, + invocation_data.entrypoint.clone(), + ); stack.push(Next::Resume { data: invocation_data, config, @@ -545,7 +577,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } v1::Interrupt::Upgrade { module_ref } => { // Add the interrupt event. - trace_elements.push(interrupt_event); + self.push_regular_trace_element( + &mut trace_elements, + interrupt_event, + invocation_data.entrypoint.clone(), + ); // Charge a base cost. self.remaining_energy @@ -583,7 +619,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { to: module_ref, }; - trace_elements.push(upgrade_event); + self.push_regular_trace_element( + &mut trace_elements, + upgrade_event, + invocation_data.entrypoint.clone(), + ); v1::InvokeResponse::Success { new_balance: self.contract_balance_unchecked( @@ -600,10 +640,15 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }; let success = matches!(response, v1::InvokeResponse::Success { .. }); - trace_elements.push(ContractTraceElement::Resumed { + let resumed_event = ContractTraceElement::Resumed { address: invocation_data.address, success, - }); + }; + self.push_regular_trace_element( + &mut trace_elements, + resumed_event, + invocation_data.entrypoint.clone(), + ); stack.push(Next::Resume { data: invocation_data, config, @@ -684,9 +729,23 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { remaining_energy, } => { self.update_energy(remaining_energy); - // Delete the trace of the failed part of the execution. - // This is the current behaviour of the on-chain execution. - trace_elements.truncate(invocation_data.trace_elements_checkpoint); + // Remove the failure stack traces from the list and include them in a failure + // element. + let failure_traces = + trace_elements.split_off(invocation_data.trace_elements_checkpoint); + if !failure_traces.is_empty() { + let with_failure = DebugTraceElement::WithFailures { + contract_address: invocation_data.address, + entrypoint: invocation_data.entrypoint.clone(), + error: InvokeExecutionError::Reject { + reason, + return_value: return_value.clone(), + }, + trace_elements: failure_traces, + energy_used: self.energy_used(), + }; + trace_elements.push(with_failure); + } // Reset the next modification index as well. self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { @@ -697,13 +756,26 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }); } v1::ReceiveResult::Trap { - error: _, // FIXME: This would ideally be propagated to the caller. + error, remaining_energy, } => { self.update_energy(remaining_energy); - // Delete the trace of the failed part of the execution. - // This is the current behaviour of the on-chain execution. - trace_elements.truncate(invocation_data.trace_elements_checkpoint); + // Remove the failure stack traces from the list and include them in a failure + // element. + let failure_traces = + trace_elements.split_off(invocation_data.trace_elements_checkpoint); + if !failure_traces.is_empty() { + let with_failure = DebugTraceElement::WithFailures { + contract_address: invocation_data.address, + entrypoint: invocation_data.entrypoint.clone(), + error: InvokeExecutionError::Trap { + error: ExecutionError(error), + }, + trace_elements: failure_traces, + energy_used: self.energy_used(), + }; + trace_elements.push(with_failure); + } // Reset the next modification index as well. self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { @@ -1211,6 +1283,26 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { v1::ReceiveResult::OutOfEnergy => Err(InsufficientEnergy), } } + + /// The energy used so far in this transaction. + fn energy_used(&self) -> Energy { self.energy_reserved - *self.remaining_energy } + + /// Helper for that constructs and pushes a [`DebugTraceElement::Regular`] + /// to the `trace_elements` list provided. + fn push_regular_trace_element( + &self, + trace_elements: &mut Vec, + trace_element: ContractTraceElement, + entrypoint: OwnedEntrypointName, + ) { + let energy_used = self.energy_used(); + let new = DebugTraceElement::Regular { + entrypoint, + trace_element, + energy_used, + }; + trace_elements.push(new); + } } impl ChangeSet { diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 09502fc9..2ba24136 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -28,7 +28,15 @@ pub(crate) struct EntrypointInvocationHandler<'a, 'b> { pub(crate) changeset: ChangeSet, /// The energy remaining for execution. pub(crate) remaining_energy: &'a mut Energy, + /// The energy reserved for the execution. Used for calculating intermediate + /// energy usages in contract trace elements. + pub(crate) energy_reserved: Energy, + /// An immutable reference to the chain, used for looking up information, + /// including contracts, modules, and accounts. pub(crate) chain: &'b Chain, + /// The next contract modification index to be given out. + /// The index is global per transaction, which is why this field is + /// needed. pub(crate) next_contract_modification_index: u32, } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index c65a2013..7de8a9f3 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -70,7 +70,7 @@ //! .unwrap(); //! //! // Check the trace elements produced (updates, interrupts, resumes, transfers, etc.). -//! assert!(matches!(update.trace_elements[..], [ContractTraceElement::Updated{..}])); +//! assert!(matches!(update.success_trace_elements().collect::>()[..], [ContractTraceElement::Updated{..}])); //! //! // Check the return value. //! assert_eq!(update.return_value, to_bytes(&84u8)); diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index a6962ee2..8cfe57c7 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -275,7 +275,7 @@ pub struct ExecutionError(#[from] pub(crate) anyhow::Error); pub struct ContractInvokeSuccess { /// Host events that occured. This includes interrupts, resumes, and /// upgrades. - pub trace_elements: Vec, + pub trace_elements: Vec, /// Energy used. pub energy_used: Energy, /// Cost of transaction. @@ -292,8 +292,11 @@ impl ContractInvokeSuccess { /// Extract all the events logged by all the contracts in the invocation. /// The events are returned in the order that they are emitted, and are /// paired with the address of the contract that emitted it. + /// + /// Only events from successful trace elements are included. See + /// [`Self::success_trace_elements`] for more details. pub fn events(&self) -> impl Iterator { - self.trace_elements.iter().flat_map(|cte| { + self.success_trace_elements().flat_map(|cte| { if let ContractTraceElement::Updated { data } = cte { Some((data.address, data.events.as_slice())) } else { @@ -309,7 +312,7 @@ impl ContractInvokeSuccess { pub fn account_transfers( &self, ) -> impl Iterator + '_ { - self.trace_elements.iter().flat_map(|cte| { + self.success_trace_elements().flat_map(|cte| { if let ContractTraceElement::Transferred { from, amount, to } = cte { Some((*from, *amount, *to)) } else { @@ -318,11 +321,26 @@ impl ContractInvokeSuccess { }) } - /// Get the trace elements grouped by which contract they originated - /// from. + /// Get an iterator over all the successful [`ContractTraceElement`]s. + /// + /// That is, elements from `self.trace_elements`, which exist in + /// [`DebugTraceElement::Regular`] and is not a child of a + /// [`DebugTraceElement::WithFailures`]. + /// + /// The trace elements returned here corresponds to the ones returned by the + /// node. + pub fn success_trace_elements(&self) -> impl Iterator { + self.trace_elements.iter().filter_map(|cte| match cte { + DebugTraceElement::Regular { trace_element, .. } => Some(trace_element), + DebugTraceElement::WithFailures { .. } => None, + }) + } + + /// Get the successful trace elements grouped by which contract they + /// originated from. pub fn trace_elements(&self) -> BTreeMap> { let mut map: BTreeMap> = BTreeMap::new(); - for event in self.trace_elements.iter() { + for event in self.success_trace_elements() { map.entry(Self::extract_contract_address(event)) .and_modify(|v| v.push(event.clone())) .or_insert_with(|| vec![event.clone()]); @@ -345,12 +363,12 @@ impl ContractInvokeSuccess { } } - /// Get the contract updates that happened in the transaction. + /// Get the successful contract updates that happened in the transaction. /// The order is the order of returns. Concretely, if A calls B (and no /// other calls are made) then first there will be "B updated" event, /// followed by "A updated", assuming the invocation of both succeeded. pub fn updates(&self) -> impl Iterator { - self.trace_elements.iter().filter_map(|e| { + self.success_trace_elements().filter_map(|e| { if let ContractTraceElement::Updated { data } = e { Some(data) } else { @@ -360,6 +378,55 @@ impl ContractInvokeSuccess { } } +/// A wrapper for [`ContractTraceElement`], which provides additional +/// information for testing and debugging. Most notably, it contains trace +/// elements for failures, which are normally discarded by the node. +#[derive(Debug)] +pub enum DebugTraceElement { + /// A regular trace element with some additional data, e.g., energy usage + /// and the entrypoint. + /// This variant may be included included in the `WithFailures` list of + /// trace elements. + Regular { + /// The entrypoint. + entrypoint: OwnedEntrypointName, + /// The trace element. + trace_element: ContractTraceElement, + /// The energy used so far. + energy_used: Energy, + }, + /// One or multiple trace elements that fail. Useful for debugging. + /// This variant also contains additional information, such as the error, + /// entrypoint, and energy usage. + WithFailures { + /// The address of the contract which failed. + contract_address: ContractAddress, + /// The entrypoint which failed. + entrypoint: OwnedEntrypointName, + /// The error returned. + error: InvokeExecutionError, + /// Intermediate [`DebugTraceElement`]s which occured prior to failing. + /// These are the elements which are normally discared by the node. + trace_elements: Vec, + /// The energy used so far. + energy_used: Energy, + }, +} + +/// The reason for why a contract invocation failed during execution. +#[derive(Debug)] +pub enum InvokeExecutionError { + /// The contract rejected. + Reject { + /// The error code for why it rejected. + reason: i32, + /// The return value. + return_value: ReturnValue, + }, + /// The contract trapped. + Trap { error: ExecutionError }, +} + /// An error that occured during a [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. #[derive(Debug, Error)] @@ -373,6 +440,8 @@ pub struct ContractInvokeError { /// The transaction fee. For [`Chain::contract_update`], this is the amount /// charged to the `invoker` account. pub transaction_fee: Amount, + /// Trace elements that occured before the contract failed. + pub trace_elements: Vec, /// The specific reason for why the invocation failed. pub kind: ContractInvokeErrorKind, } diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 34a04567..7e176a5a 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -78,7 +78,7 @@ fn test_case_1() { Amount::zero(), ); - chain + let update = chain .contract_update( Signer::with_one_key(), ACC_0, @@ -95,6 +95,31 @@ fn test_case_1() { }, ) .expect("Updating contract should succeed"); + + // Check that all the trace elements are as expected, including the ones + // resulting in a failure. Some imports to simplify the names in the assert. + use ContractTraceElement::*; + use DebugTraceElement::*; + use InvokeExecutionError::*; + assert!(matches!(&update.trace_elements[..], [ + Regular { + trace_element: Interrupted { .. }, + .. + }, + WithFailures { + error: Trap { .. }, + trace_elements, + .. + }, + Regular { + trace_element: Resumed { .. }, + .. + }, + Regular { + trace_element: Updated { .. }, + .. + } + ] if matches!(trace_elements[..], [Regular { trace_element: Interrupted {..}, ..}, Regular { trace_element: Updated {..}, .. }, Regular { trace_element: Resumed {..}, .. }]))); } /// This test has the following call pattern: diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 2c6702ae..ceee1f56 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -75,9 +75,10 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// Queries the balance of the invoker account, which will have have the @@ -148,9 +149,10 @@ mod query_account_balance { // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) ); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// Makes a transfer to an account, then queries its balance and asserts @@ -230,12 +232,15 @@ mod query_account_balance { chain.account_balance_available(ACC_1), Some(initial_balance + amount_to_send) ); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Transferred { .. }, - ContractTraceElement::Resumed { .. }, - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Transferred { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. } + ] + )); } #[test] @@ -297,9 +302,10 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// Queries the balance of a missing account and asserts that it returns @@ -364,9 +370,10 @@ mod query_account_balance { - res_update.transaction_fee ) ); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } } @@ -442,9 +449,10 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// Test querying the balance of the contract instance itself. This @@ -503,9 +511,10 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// Test querying the balance after a transfer of CCD. @@ -568,12 +577,15 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Transferred { .. }, - ContractTraceElement::Resumed { .. }, - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Transferred { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. } + ] + )); } /// Test querying the balance of a contract that doesn't exist. @@ -628,9 +640,10 @@ mod query_contract_balance { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } } @@ -687,8 +700,9 @@ mod query_exchange_rates { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 0df9f762..f49213f1 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -96,30 +96,33 @@ fn test_transfer() { Amount::from_micro_ccd(1000 - 17), chain.get_contract(contract_address).unwrap().self_balance ); - assert_eq!(res_update.trace_elements[..], [ - ContractTraceElement::Interrupted { - address: contract_address, - events: Vec::new(), - }, - ContractTraceElement::Transferred { - from: contract_address, - amount: Amount::from_micro_ccd(17), - to: ACC_0, - }, - ContractTraceElement::Resumed { - address: contract_address, - success: true, - }, - ContractTraceElement::Updated { - data: InstanceUpdatedEvent { - address: contract_address, - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), - contract_version: concordium_base::smart_contracts::WasmVersion::V1, - instigator: Address::Account(ACC_0), - message: parameter, - events: Vec::new(), + assert_eq!( + res_update.success_trace_elements().collect::>()[..], + [ + &ContractTraceElement::Interrupted { + address: contract_address, + events: Vec::new(), + }, + &ContractTraceElement::Transferred { + from: contract_address, + amount: Amount::from_micro_ccd(17), + to: ACC_0, + }, + &ContractTraceElement::Resumed { + address: contract_address, + success: true, }, - } - ]) + &ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + address: contract_address, + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), + contract_version: concordium_base::smart_contracts::WasmVersion::V1, + instigator: Address::Account(ACC_0), + message: parameter, + events: Vec::new(), + }, + } + ] + ) } diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 4a54e6d1..5d1bf9d4 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -83,15 +83,18 @@ fn test() { ) .expect("Updating the `newfun` from the `upgrading_1` module should work"); - assert!(matches!(res_update_upgrade.trace_elements[..], [ + assert!( + matches!(res_update_upgrade.success_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Upgraded { from, to, .. }, ContractTraceElement::Resumed { .. }, ContractTraceElement::Updated { .. }, - ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference)); - assert!(matches!(res_update_new.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + ] if *from == res_deploy_0.module_reference && *to == res_deploy_1.module_reference) + ); + assert!(matches!( + res_update_new.success_trace_elements().collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } /// The contract in this test, triggers an upgrade and then in the same @@ -150,22 +153,25 @@ fn test_self_invoke() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ - // Invoking `contract.name` - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Updated { .. }, - ContractTraceElement::Resumed { .. }, - // Making the upgrade - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Upgraded { .. }, - ContractTraceElement::Resumed { .. }, - // Invoking contract.name again - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Updated { .. }, - ContractTraceElement::Resumed { .. }, - // The successful update - ContractTraceElement::Updated { .. }, - ])); + assert!(matches!( + res_update.success_trace_elements().collect::>()[..], + [ + // Invoking `contract.name` + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, + // Making the upgrade + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { .. }, + ContractTraceElement::Resumed { .. }, + // Invoking contract.name again + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Updated { .. }, + ContractTraceElement::Resumed { .. }, + // The successful update + ContractTraceElement::Updated { .. }, + ] + )); } /// Test upgrading to a module that doesn't exist (it uses module @@ -218,12 +224,14 @@ fn test_missing_module() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ + assert!( + matches!(res_update.success_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if !success )); + ] if !success ) + ); } /// Test upgrading to a module where there isn't a matching contract @@ -290,12 +298,14 @@ fn test_missing_contract() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ + assert!( + matches!(res_update.success_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if !success )); + ] if !success ) + ); } /// Test upgrading twice in the same transaction. The effect of the @@ -366,7 +376,8 @@ fn test_twice_in_one_transaction() { ) .expect("Updating valid contract should work"); - assert!(matches!(res_update.trace_elements[..], [ + assert!( + matches!(res_update.success_trace_elements().collect::>()[..], [ // Invoke the contract itself to check the name entrypoint return value. ContractTraceElement::Interrupted { .. }, ContractTraceElement::Updated { .. }, @@ -389,10 +400,11 @@ fn test_twice_in_one_transaction() { ContractTraceElement::Resumed { .. }, // Final update event ContractTraceElement::Updated { .. }, - ] if first_from == res_deploy_0.module_reference - && first_to == res_deploy_1.module_reference - && second_from == res_deploy_1.module_reference - && second_to == res_deploy_2.module_reference)); + ] if *first_from == res_deploy_0.module_reference + && *first_to == res_deploy_1.module_reference + && *second_from == res_deploy_1.module_reference + && *second_to == res_deploy_2.module_reference) + ); } /// Test upgrading to a module where there isn't a matching contract @@ -452,7 +464,10 @@ fn test_chained_contract() { // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful // update. assert_eq!( - res_update.trace_elements.len() as u32, + res_update + .success_trace_elements() + .collect::>() + .len() as u32, 6 * number_of_upgrades + 4 ) } @@ -672,28 +687,39 @@ fn test_changing_entrypoint() { ) .expect("Updating new_feature on _new_ module should work"); - assert!(matches!(res_update_old_feature_0.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update_old_feature_0 + .success_trace_elements() + .collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); assert!(matches!( res_update_new_feature_0.kind, ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); - assert!(matches!(res_update_upgrade.trace_elements[..], [ - ContractTraceElement::Interrupted { .. }, - ContractTraceElement::Upgraded { .. }, - ContractTraceElement::Resumed { .. }, - ContractTraceElement::Updated { .. }, - ])); + assert!(matches!( + res_update_upgrade + .success_trace_elements() + .collect::>()[..], + [ + ContractTraceElement::Interrupted { .. }, + ContractTraceElement::Upgraded { .. }, + ContractTraceElement::Resumed { .. }, + ContractTraceElement::Updated { .. }, + ] + )); assert!(matches!( res_update_old_feature_1.kind, ContractInvokeErrorKind::ExecutionError { failure_kind: InvokeFailure::NonExistentEntrypoint, } )); - assert!(matches!(res_update_new_feature_1.trace_elements[..], [ - ContractTraceElement::Updated { .. } - ])); + assert!(matches!( + res_update_new_feature_1 + .success_trace_elements() + .collect::>()[..], + [ContractTraceElement::Updated { .. }] + )); } From b6fc125d57a49b8b5d5619368ba98efe2cf43d1c Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 16 May 2023 14:42:09 +0200 Subject: [PATCH 165/208] Address review comments and update changelog --- contract-testing/CHANGELOG.md | 15 +++++++ contract-testing/src/impls.rs | 4 +- contract-testing/src/invocation/impls.rs | 56 ++++++++++++----------- contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 57 +++++++++++++++--------- contract-testing/tests/checkpointing.rs | 11 ++++- contract-testing/tests/queries.rs | 20 ++++----- contract-testing/tests/transfer.rs | 2 +- contract-testing/tests/upgrades.rs | 20 ++++----- 9 files changed, 116 insertions(+), 71 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 6bbd5c82..8496c049 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Unreleased + +- Include `ContractTraceElement`s for failed contract executions, including internal failures. + - Introduce the enum `DebugTraceElement`, which has information about trace elements, including failed ones and the cause of error, and the energy usage. + - On the `ContractInvokeSuccess` type: + - Change the type of the `trace_elements` to be `Vec` instead of `Vec`. (breaking change) + - Add a helper method, `effective_trace_elements()`, to retrieve the "effective" trace elements, i.e., elements that were *not* rolled back. + - These are the elements that were previously returned in the `trace_elements` field. + - To migrate existing code, replace `some_update.trace_elements` with `some_update.effective_trace_elements().collect::>()`, + and dereference elements of the list if necessary. + - Add a helper method, `rollbacks_occurred()`, to determine whether any internal failures occurred. + - On the `ContractInvokeError` type: + - Include the field `trace_elements` of type `Vec` with the trace elements that were hitherto discarded. (breaking change) + - To migrate, include the new field or use `..` when pattern matching on the type. + ## 1.0.0 - The initial release of the library. diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 8f2e61bc..c32a4990 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -751,7 +751,7 @@ impl Chain { ); res.map_err(|_| self.invocation_out_of_energy_error(energy_reserved))? } else { - // An error occured, so state hasn't changed. + // An error occurred, so state hasn't changed. false }; self.contract_invocation_process_response( @@ -853,7 +853,7 @@ impl Chain { .collect_energy_for_state(&mut remaining_energy, contract_address) .map_err(|_| self.invocation_out_of_energy_error(energy_reserved))? } else { - // An error occured, so state hasn't changed. + // An error occurred, so state hasn't changed. false }; self.contract_invocation_process_response( diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 4f6d70eb..ca780503 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -733,19 +733,21 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // element. let failure_traces = trace_elements.split_off(invocation_data.trace_elements_checkpoint); - if !failure_traces.is_empty() { - let with_failure = DebugTraceElement::WithFailures { - contract_address: invocation_data.address, - entrypoint: invocation_data.entrypoint.clone(), - error: InvokeExecutionError::Reject { - reason, - return_value: return_value.clone(), - }, - trace_elements: failure_traces, - energy_used: self.energy_used(), - }; - trace_elements.push(with_failure); - } + + // Create a `WithFailures` element even if `failure_traces` is empty, as the + // reject reason and energy usage is still relevant. + let with_failure = DebugTraceElement::WithFailures { + contract_address: invocation_data.address, + entrypoint: invocation_data.entrypoint.clone(), + error: InvokeExecutionError::Reject { + reason, + return_value: return_value.clone(), + }, + trace_elements: failure_traces, + energy_used: self.energy_used(), + }; + trace_elements.push(with_failure); + // Reset the next modification index as well. self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { @@ -764,18 +766,20 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // element. let failure_traces = trace_elements.split_off(invocation_data.trace_elements_checkpoint); - if !failure_traces.is_empty() { - let with_failure = DebugTraceElement::WithFailures { - contract_address: invocation_data.address, - entrypoint: invocation_data.entrypoint.clone(), - error: InvokeExecutionError::Trap { - error: ExecutionError(error), - }, - trace_elements: failure_traces, - energy_used: self.energy_used(), - }; - trace_elements.push(with_failure); - } + + // Create a `WithFailures` element even if `failure_traces` is empty, as the + // error and energy usage is still relevant. + let with_failure = DebugTraceElement::WithFailures { + contract_address: invocation_data.address, + entrypoint: invocation_data.entrypoint.clone(), + error: InvokeExecutionError::Trap { + error: ExecutionError(error), + }, + trace_elements: failure_traces, + energy_used: self.energy_used(), + }; + trace_elements.push(with_failure); + // Reset the next modification index as well. self.next_contract_modification_index = invocation_data.next_mod_idx_checkpoint; invoke_response = Some(v1::InvokeResponse::Failure { @@ -1211,7 +1215,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { let res = match f(available_interpreter_energy) { Ok(res) => res, Err(err) => { - // An error occured in the interpreter and it doesn't return the remaining + // An error occurred in the interpreter and it doesn't return the remaining // energy. We convert this to a trap and set the energy to the // last known amount. return Ok(v1::ReceiveResult::Trap { diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 7de8a9f3..9891cfd5 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -70,7 +70,7 @@ //! .unwrap(); //! //! // Check the trace elements produced (updates, interrupts, resumes, transfers, etc.). -//! assert!(matches!(update.success_trace_elements().collect::>()[..], [ContractTraceElement::Updated{..}])); +//! assert!(matches!(update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated{..}])); //! //! // Check the return value. //! assert_eq!(update.return_value, to_bytes(&84u8)); diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 8cfe57c7..0d696d07 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -110,7 +110,7 @@ pub struct ModuleDeploySuccess { pub transaction_fee: Amount, } -/// An error that occured while deploying a [`ContractModule`]. +/// An error that occurred while deploying a [`ContractModule`]. #[derive(Debug, Error)] #[error( "Module deployment failed after consuming {energy_used}NRG ({transaction_fee} microCCD) with \ @@ -126,7 +126,7 @@ pub struct ModuleDeployError { pub kind: ModuleDeployErrorKind, } -/// The specific kind of error that occured while deploying a +/// The specific kind of error that occurred while deploying a /// [`ContractModule`]. #[derive(Debug, Error)] pub enum ModuleDeployErrorKind { @@ -196,7 +196,7 @@ pub struct ContractInitSuccess { pub transaction_fee: Amount, } -/// An error that occured in [`Chain::contract_init`]. +/// An error that occurred in [`Chain::contract_init`]. #[derive(Debug, Error)] #[error( "Contract initialization failed after consuming {energy_used}NRG ({transaction_fee} microCCD) \ @@ -265,7 +265,7 @@ pub enum InitExecutionError { OutOfEnergy, } -/// An error that occured while executing a contract init or receive function. +/// An error that occurred while executing a contract init or receive function. #[derive(Debug, Error)] #[error("The contract execution halted due to: {0}")] pub struct ExecutionError(#[from] pub(crate) anyhow::Error); @@ -273,7 +273,7 @@ pub struct ExecutionError(#[from] pub(crate) anyhow::Error); /// Represents a successful contract update (or invocation). #[derive(Debug)] pub struct ContractInvokeSuccess { - /// Host events that occured. This includes interrupts, resumes, and + /// Host events that occurred. This includes interrupts, resumes, and /// upgrades. pub trace_elements: Vec, /// Energy used. @@ -293,10 +293,10 @@ impl ContractInvokeSuccess { /// The events are returned in the order that they are emitted, and are /// paired with the address of the contract that emitted it. /// - /// Only events from successful trace elements are included. See - /// [`Self::success_trace_elements`] for more details. + /// Only events from effective trace elements are included. See + /// [`Self::effective_trace_elements`] for more details. pub fn events(&self) -> impl Iterator { - self.success_trace_elements().flat_map(|cte| { + self.effective_trace_elements().flat_map(|cte| { if let ContractTraceElement::Updated { data } = cte { Some((data.address, data.events.as_slice())) } else { @@ -309,10 +309,13 @@ impl ContractInvokeSuccess { /// invocation. The return value is an iterator over triples `(from, amount, /// to)` where `from` is the sender contract, and `to` is the receiver /// account. The transfers are returned in the order that they occurred. + /// + /// Only tranfers from effective trace elements are included. See + /// [`Self::effective_trace_elements`] for more details. pub fn account_transfers( &self, ) -> impl Iterator + '_ { - self.success_trace_elements().flat_map(|cte| { + self.effective_trace_elements().flat_map(|cte| { if let ContractTraceElement::Transferred { from, amount, to } = cte { Some((*from, *amount, *to)) } else { @@ -321,15 +324,12 @@ impl ContractInvokeSuccess { }) } - /// Get an iterator over all the successful [`ContractTraceElement`]s. - /// - /// That is, elements from `self.trace_elements`, which exist in - /// [`DebugTraceElement::Regular`] and is not a child of a - /// [`DebugTraceElement::WithFailures`]. + /// Get an iterator over all the [`ContractTraceElement`]s that have *not* + /// been rolled back. /// /// The trace elements returned here corresponds to the ones returned by the /// node. - pub fn success_trace_elements(&self) -> impl Iterator { + pub fn effective_trace_elements(&self) -> impl Iterator { self.trace_elements.iter().filter_map(|cte| match cte { DebugTraceElement::Regular { trace_element, .. } => Some(trace_element), DebugTraceElement::WithFailures { .. } => None, @@ -340,7 +340,7 @@ impl ContractInvokeSuccess { /// originated from. pub fn trace_elements(&self) -> BTreeMap> { let mut map: BTreeMap> = BTreeMap::new(); - for event in self.success_trace_elements() { + for event in self.effective_trace_elements() { map.entry(Self::extract_contract_address(event)) .and_modify(|v| v.push(event.clone())) .or_insert_with(|| vec![event.clone()]); @@ -368,7 +368,7 @@ impl ContractInvokeSuccess { /// other calls are made) then first there will be "B updated" event, /// followed by "A updated", assuming the invocation of both succeeded. pub fn updates(&self) -> impl Iterator { - self.success_trace_elements().filter_map(|e| { + self.effective_trace_elements().filter_map(|e| { if let ContractTraceElement::Updated { data } = e { Some(data) } else { @@ -376,6 +376,19 @@ impl ContractInvokeSuccess { } }) } + + /// Check whether any rollbacks occurred. + /// + /// That is, whether any contract calls failed which lead to state and + /// balances being rolled back. + /// + /// If `true` is returned, the relevant traces can be seen in the + /// `self.trace_elements` vector. + pub fn rollbacks_occurred(&self) -> bool { + self.trace_elements + .iter() + .any(|element| matches!(element, DebugTraceElement::WithFailures { .. })) + } } /// A wrapper for [`ContractTraceElement`], which provides additional @@ -400,12 +413,16 @@ pub enum DebugTraceElement { /// entrypoint, and energy usage. WithFailures { /// The address of the contract which failed. + /// This will always match the address in the last element in + /// `trace_elements` if the vector isn't empty. contract_address: ContractAddress, /// The entrypoint which failed. + /// This will always match the address in the last element in + /// `trace_elements` if the vector isn't empty. entrypoint: OwnedEntrypointName, /// The error returned. error: InvokeExecutionError, - /// Intermediate [`DebugTraceElement`]s which occured prior to failing. + /// Intermediate [`DebugTraceElement`]s which occurred prior to failing. /// These are the elements which are normally discared by the node. trace_elements: Vec, /// The energy used so far. @@ -427,7 +444,7 @@ pub enum InvokeExecutionError { Trap { error: ExecutionError }, } -/// An error that occured during a [`Chain::contract_update`] or +/// An error that occurred during a [`Chain::contract_update`] or /// [`Chain::contract_invoke`]. #[derive(Debug, Error)] #[error( @@ -440,7 +457,7 @@ pub struct ContractInvokeError { /// The transaction fee. For [`Chain::contract_update`], this is the amount /// charged to the `invoker` account. pub transaction_fee: Amount, - /// Trace elements that occured before the contract failed. + /// Trace elements that occurred before the contract failed. pub trace_elements: Vec, /// The specific reason for why the invocation failed. pub kind: ContractInvokeErrorKind, diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 7e176a5a..197ed5c5 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -96,6 +96,9 @@ fn test_case_1() { ) .expect("Updating contract should succeed"); + // Check that rollbacks occurred. + assert!(update.rollbacks_occurred()); + // Check that all the trace elements are as expected, including the ones // resulting in a failure. Some imports to simplify the names in the assert. use ContractTraceElement::*; @@ -227,6 +230,9 @@ fn test_case_2() { assert_eq!(update_a_modify_proxy.address, res_init_a.contract_address); assert_eq!(update_a_modify_proxy.receive_name, "a.a_modify_proxy"); assert!(updates.next().is_none(), "No more updates expected."); + + // Check that no rollbacks occurred. + assert!(!trace.rollbacks_occurred()); } /// This test has the following call pattern: @@ -370,7 +376,7 @@ fn test_case_4() { Amount::zero(), ); - chain + let update = chain .contract_update( Signer::with_one_key(), ACC_0, @@ -389,4 +395,7 @@ fn test_case_4() { }, ) .expect("Updating contract should succeed"); + + // Check that no rollbacks occurred. + assert!(!update.rollbacks_occurred()); } diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index ceee1f56..1a4c37c4 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -76,7 +76,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -150,7 +150,7 @@ mod query_account_balance { Some(initial_balance - res_update.transaction_fee - update_amount) ); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -233,7 +233,7 @@ mod query_account_balance { Some(initial_balance + amount_to_send) ); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Transferred { .. }, @@ -303,7 +303,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -371,7 +371,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -450,7 +450,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -512,7 +512,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -578,7 +578,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Transferred { .. }, @@ -641,7 +641,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -701,7 +701,7 @@ mod query_exchange_rates { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index f49213f1..4666da2f 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -97,7 +97,7 @@ fn test_transfer() { chain.get_contract(contract_address).unwrap().self_balance ); assert_eq!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ &ContractTraceElement::Interrupted { address: contract_address, diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index 5d1bf9d4..b0ecd6cc 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -84,7 +84,7 @@ fn test() { .expect("Updating the `newfun` from the `upgrading_1` module should work"); assert!( - matches!(res_update_upgrade.success_trace_elements().collect::>()[..], [ + matches!(res_update_upgrade.effective_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Upgraded { from, to, .. }, ContractTraceElement::Resumed { .. }, @@ -92,7 +92,7 @@ fn test() { ] if *from == res_deploy_0.module_reference && *to == res_deploy_1.module_reference) ); assert!(matches!( - res_update_new.success_trace_elements().collect::>()[..], + res_update_new.effective_trace_elements().collect::>()[..], [ContractTraceElement::Updated { .. }] )); } @@ -154,7 +154,7 @@ fn test_self_invoke() { .expect("Updating valid contract should work"); assert!(matches!( - res_update.success_trace_elements().collect::>()[..], + res_update.effective_trace_elements().collect::>()[..], [ // Invoking `contract.name` ContractTraceElement::Interrupted { .. }, @@ -225,7 +225,7 @@ fn test_missing_module() { .expect("Updating valid contract should work"); assert!( - matches!(res_update.success_trace_elements().collect::>()[..], [ + matches!(res_update.effective_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, @@ -299,7 +299,7 @@ fn test_missing_contract() { .expect("Updating valid contract should work"); assert!( - matches!(res_update.success_trace_elements().collect::>()[..], [ + matches!(res_update.effective_trace_elements().collect::>()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, @@ -377,7 +377,7 @@ fn test_twice_in_one_transaction() { .expect("Updating valid contract should work"); assert!( - matches!(res_update.success_trace_elements().collect::>()[..], [ + matches!(res_update.effective_trace_elements().collect::>()[..], [ // Invoke the contract itself to check the name entrypoint return value. ContractTraceElement::Interrupted { .. }, ContractTraceElement::Updated { .. }, @@ -465,7 +465,7 @@ fn test_chained_contract() { // update. assert_eq!( res_update - .success_trace_elements() + .effective_trace_elements() .collect::>() .len() as u32, 6 * number_of_upgrades + 4 @@ -689,7 +689,7 @@ fn test_changing_entrypoint() { assert!(matches!( res_update_old_feature_0 - .success_trace_elements() + .effective_trace_elements() .collect::>()[..], [ContractTraceElement::Updated { .. }] )); @@ -701,7 +701,7 @@ fn test_changing_entrypoint() { )); assert!(matches!( res_update_upgrade - .success_trace_elements() + .effective_trace_elements() .collect::>()[..], [ ContractTraceElement::Interrupted { .. }, @@ -718,7 +718,7 @@ fn test_changing_entrypoint() { )); assert!(matches!( res_update_new_feature_1 - .success_trace_elements() + .effective_trace_elements() .collect::>()[..], [ContractTraceElement::Updated { .. }] )); From c714931d0df7593104ce5bde60afa23b967984bc Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 17 May 2023 10:57:29 +0200 Subject: [PATCH 166/208] Add cloned version of helper to ease migration and usage --- contract-testing/CHANGELOG.md | 8 ++--- contract-testing/src/types.rs | 25 ++++++++++++-- contract-testing/tests/queries.rs | 20 +++++------ contract-testing/tests/transfer.rs | 53 ++++++++++++++---------------- contract-testing/tests/upgrades.rs | 49 ++++++++++----------------- 5 files changed, 79 insertions(+), 76 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 8496c049..6f87acfe 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -5,12 +5,12 @@ - Include `ContractTraceElement`s for failed contract executions, including internal failures. - Introduce the enum `DebugTraceElement`, which has information about trace elements, including failed ones and the cause of error, and the energy usage. - On the `ContractInvokeSuccess` type: - - Change the type of the `trace_elements` to be `Vec` instead of `Vec`. (breaking change) + - Change the type of the `trace_elements` field to be `Vec` instead of `Vec`. (breaking change) - Add a helper method, `effective_trace_elements()`, to retrieve the "effective" trace elements, i.e., elements that were *not* rolled back. - These are the elements that were previously returned in the `trace_elements` field. - - To migrate existing code, replace `some_update.trace_elements` with `some_update.effective_trace_elements().collect::>()`, - and dereference elements of the list if necessary. - - Add a helper method, `rollbacks_occurred()`, to determine whether any internal failures occurred. + - There is also a version of the method which clones: `effective_trace_elements_cloned()`. + - To migrate existing code, replace `some_update.trace_elements()` with `some_update.effective_trace_elements_cloned()`. + - Add a helper method, `rollbacks_occurred()`, to determine whether any internal failures or rollbacks occurred. - On the `ContractInvokeError` type: - Include the field `trace_elements` of type `Vec` with the trace elements that were hitherto discarded. (breaking change) - To migrate, include the new field or use `..` when pattern matching on the type. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 0d696d07..42523eba 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -324,11 +324,14 @@ impl ContractInvokeSuccess { }) } - /// Get an iterator over all the [`ContractTraceElement`]s that have *not* - /// been rolled back. + /// Get an iterator over references of all the [`ContractTraceElement`]s + /// that have *not* been rolled back. /// /// The trace elements returned here corresponds to the ones returned by the /// node. + /// + /// See also [`Self::effective_trace_elements_cloned`] for a version with + /// clones. pub fn effective_trace_elements(&self) -> impl Iterator { self.trace_elements.iter().filter_map(|cte| match cte { DebugTraceElement::Regular { trace_element, .. } => Some(trace_element), @@ -336,6 +339,24 @@ impl ContractInvokeSuccess { }) } + /// Get an iterator over clones of all the [`ContractTraceElement`]s that + /// have *not* been rolled back. + /// + /// The trace elements returned here corresponds to the ones returned by the + /// node. + /// + /// See also [`Self::effective_trace_elements`] for a version with + /// references. + pub fn effective_trace_elements_cloned(&self) -> Vec { + self.trace_elements + .iter() + .filter_map(|cte| match cte { + DebugTraceElement::Regular { trace_element, .. } => Some(trace_element.clone()), + DebugTraceElement::WithFailures { .. } => None, + }) + .collect() + } + /// Get the successful trace elements grouped by which contract they /// originated from. pub fn trace_elements(&self) -> BTreeMap> { diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index 1a4c37c4..bf655cd6 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -76,7 +76,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -150,7 +150,7 @@ mod query_account_balance { Some(initial_balance - res_update.transaction_fee - update_amount) ); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -233,7 +233,7 @@ mod query_account_balance { Some(initial_balance + amount_to_send) ); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Transferred { .. }, @@ -303,7 +303,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -371,7 +371,7 @@ mod query_account_balance { ) ); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -450,7 +450,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -512,7 +512,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -578,7 +578,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Transferred { .. }, @@ -641,7 +641,7 @@ mod query_contract_balance { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -701,7 +701,7 @@ mod query_exchange_rates { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 4666da2f..4122874a 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -96,33 +96,30 @@ fn test_transfer() { Amount::from_micro_ccd(1000 - 17), chain.get_contract(contract_address).unwrap().self_balance ); - assert_eq!( - res_update.effective_trace_elements().collect::>()[..], - [ - &ContractTraceElement::Interrupted { - address: contract_address, - events: Vec::new(), - }, - &ContractTraceElement::Transferred { - from: contract_address, - amount: Amount::from_micro_ccd(17), - to: ACC_0, - }, - &ContractTraceElement::Resumed { - address: contract_address, - success: true, + assert_eq!(res_update.effective_trace_elements_cloned()[..], [ + ContractTraceElement::Interrupted { + address: contract_address, + events: Vec::new(), + }, + ContractTraceElement::Transferred { + from: contract_address, + amount: Amount::from_micro_ccd(17), + to: ACC_0, + }, + ContractTraceElement::Resumed { + address: contract_address, + success: true, + }, + ContractTraceElement::Updated { + data: InstanceUpdatedEvent { + address: contract_address, + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), + contract_version: concordium_base::smart_contracts::WasmVersion::V1, + instigator: Address::Account(ACC_0), + message: parameter, + events: Vec::new(), }, - &ContractTraceElement::Updated { - data: InstanceUpdatedEvent { - address: contract_address, - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), - contract_version: concordium_base::smart_contracts::WasmVersion::V1, - instigator: Address::Account(ACC_0), - message: parameter, - events: Vec::new(), - }, - } - ] - ) + } + ]) } diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index b0ecd6cc..d1181494 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -84,15 +84,15 @@ fn test() { .expect("Updating the `newfun` from the `upgrading_1` module should work"); assert!( - matches!(res_update_upgrade.effective_trace_elements().collect::>()[..], [ + matches!(res_update_upgrade.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Upgraded { from, to, .. }, ContractTraceElement::Resumed { .. }, ContractTraceElement::Updated { .. }, - ] if *from == res_deploy_0.module_reference && *to == res_deploy_1.module_reference) + ] if from == res_deploy_0.module_reference && to == res_deploy_1.module_reference) ); assert!(matches!( - res_update_new.effective_trace_elements().collect::>()[..], + res_update_new.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } @@ -154,7 +154,7 @@ fn test_self_invoke() { .expect("Updating valid contract should work"); assert!(matches!( - res_update.effective_trace_elements().collect::>()[..], + res_update.effective_trace_elements_cloned()[..], [ // Invoking `contract.name` ContractTraceElement::Interrupted { .. }, @@ -224,14 +224,12 @@ fn test_missing_module() { ) .expect("Updating valid contract should work"); - assert!( - matches!(res_update.effective_trace_elements().collect::>()[..], [ + assert!(matches!(res_update.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if !success ) - ); + ] if !success )); } /// Test upgrading to a module where there isn't a matching contract @@ -298,14 +296,12 @@ fn test_missing_contract() { ) .expect("Updating valid contract should work"); - assert!( - matches!(res_update.effective_trace_elements().collect::>()[..], [ + assert!(matches!(res_update.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, // No upgrade event, as it is supposed to fail. ContractTraceElement::Resumed { success, .. }, ContractTraceElement::Updated { .. }, - ] if !success ) - ); + ] if !success )); } /// Test upgrading twice in the same transaction. The effect of the @@ -376,8 +372,7 @@ fn test_twice_in_one_transaction() { ) .expect("Updating valid contract should work"); - assert!( - matches!(res_update.effective_trace_elements().collect::>()[..], [ + assert!(matches!(res_update.effective_trace_elements_cloned()[..], [ // Invoke the contract itself to check the name entrypoint return value. ContractTraceElement::Interrupted { .. }, ContractTraceElement::Updated { .. }, @@ -400,11 +395,10 @@ fn test_twice_in_one_transaction() { ContractTraceElement::Resumed { .. }, // Final update event ContractTraceElement::Updated { .. }, - ] if *first_from == res_deploy_0.module_reference - && *first_to == res_deploy_1.module_reference - && *second_from == res_deploy_1.module_reference - && *second_to == res_deploy_2.module_reference) - ); + ] if first_from == res_deploy_0.module_reference + && first_to == res_deploy_1.module_reference + && second_from == res_deploy_1.module_reference + && second_to == res_deploy_2.module_reference)); } /// Test upgrading to a module where there isn't a matching contract @@ -464,10 +458,7 @@ fn test_chained_contract() { // Ends with 4 extra events: 3 events for an upgrade and 1 event for succesful // update. assert_eq!( - res_update - .effective_trace_elements() - .collect::>() - .len() as u32, + res_update.effective_trace_elements_cloned().len() as u32, 6 * number_of_upgrades + 4 ) } @@ -688,9 +679,7 @@ fn test_changing_entrypoint() { .expect("Updating new_feature on _new_ module should work"); assert!(matches!( - res_update_old_feature_0 - .effective_trace_elements() - .collect::>()[..], + res_update_old_feature_0.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); assert!(matches!( @@ -700,9 +689,7 @@ fn test_changing_entrypoint() { } )); assert!(matches!( - res_update_upgrade - .effective_trace_elements() - .collect::>()[..], + res_update_upgrade.effective_trace_elements_cloned()[..], [ ContractTraceElement::Interrupted { .. }, ContractTraceElement::Upgraded { .. }, @@ -717,9 +704,7 @@ fn test_changing_entrypoint() { } )); assert!(matches!( - res_update_new_feature_1 - .effective_trace_elements() - .collect::>()[..], + res_update_new_feature_1.effective_trace_elements_cloned()[..], [ContractTraceElement::Updated { .. }] )); } From 3bf943036ccb241a89ffe583983bdddde58c2f31 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 17 May 2023 10:59:06 +0200 Subject: [PATCH 167/208] Fix changelog --- contract-testing/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 6f87acfe..270c12a3 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -9,7 +9,7 @@ - Add a helper method, `effective_trace_elements()`, to retrieve the "effective" trace elements, i.e., elements that were *not* rolled back. - These are the elements that were previously returned in the `trace_elements` field. - There is also a version of the method which clones: `effective_trace_elements_cloned()`. - - To migrate existing code, replace `some_update.trace_elements()` with `some_update.effective_trace_elements_cloned()`. + - To migrate existing code, replace `some_update.trace_elements` with `some_update.effective_trace_elements_cloned()`. - Add a helper method, `rollbacks_occurred()`, to determine whether any internal failures or rollbacks occurred. - On the `ContractInvokeError` type: - Include the field `trace_elements` of type `Vec` with the trace elements that were hitherto discarded. (breaking change) From 09bc909b4b5364876211d223d75dbff4ddf3268e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Fri, 16 Jun 2023 15:37:15 +0200 Subject: [PATCH 168/208] Bump versions for release. --- contract-testing/CHANGELOG.md | 2 +- contract-testing/Cargo.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 270c12a3..35b3cdf5 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 2.0.0 - Include `ContractTraceElement`s for failed contract executions, including internal failures. - Introduce the enum `DebugTraceElement`, which has information about trace elements, including failed ones and the cause of error, and the energy usage. diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 4bef7f29..52924bce 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-smart-contract-testing" -version = "1.0.0" +version = "2.0.0" edition = "2021" rust-version = "1.65" license = "MPL-2.0" @@ -13,9 +13,9 @@ exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {version = "1.2", path = "../concordium-base/rust-src/concordium_base"} -concordium-smart-contract-engine = {version = "1.2", path = "../concordium-base/smart-contracts/wasm-chain-integration"} -concordium-wasm = {version = "1.1", path = "../concordium-base/smart-contracts/wasm-transform"} +concordium_base = {version = "2.0", path = "../concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" From d1f63ae485ab71b692c19dabe9794c6e57c49072 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 21 Jun 2023 11:40:34 +0200 Subject: [PATCH 169/208] Add methods for setting exchange rates via external node Also adds: - A method for setting up the connection to an external node - A method for setting the block time --- contract-testing/CHANGELOG.md | 7 ++ contract-testing/Cargo.toml | 6 +- contract-testing/src/impls.rs | 196 ++++++++++++++++++++++++++++++++-- contract-testing/src/types.rs | 56 +++++++++- 4 files changed, 252 insertions(+), 13 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 35b3cdf5..a3e78afb 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased changes + +- Add methods to `Chain` for setting the exchange rates and block time based on queries from an external node. + - Add the method `setup_external_node_connection` for configuring the external node to use. + - Add the method `set_exchange_rates_via_external_node` for setting the exchange rates. + - Add the method `set_block_time_via_external_node` for setting the block time. + ## 2.0.0 - Include `ContractTraceElement`s for failed contract executions, including internal failures. diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 52924bce..dfacb9fa 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -7,7 +7,7 @@ license = "MPL-2.0" readme = "README.md" description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." homepage = "https://github.com/Concordium/concordium-smart-contract-tools" -reposity = "https://github.com/Concordium/concordium-smart-contract-tools" +repository = "https://github.com/Concordium/concordium-smart-contract-tools" exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,8 +16,10 @@ exclude = ["tests"] # Do not publish tests. concordium_base = {version = "2.0", path = "../concordium-base/rust-src/concordium_base"} concordium-smart-contract-engine = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} concordium-wasm = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-transform"} +concordium-rust-sdk = "2.4" +tokio = { version = "1.28", features = ["rt-multi-thread", "time"] } sha2 = "0.10" anyhow = "1" thiserror = "1.0" num-bigint = "0.4" -num-integer = "0.1" \ No newline at end of file +num-integer = "0.1" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index c32a4990..206f96e8 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -14,6 +14,7 @@ use concordium_base::{ smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; +use concordium_rust_sdk as sdk; use concordium_smart_contract_engine::{ v0, v1::{self, InvokeResponse}, @@ -21,7 +22,14 @@ use concordium_smart_contract_engine::{ }; use num_bigint::BigUint; use num_integer::Integer; -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::{collections::BTreeMap, path::Path, str::FromStr, sync::Arc}; +use tokio::{ + runtime, + time::{timeout, Duration}, +}; + +/// The duration set for timeouts when communicating with an external node. +const EXTERNAL_NODE_TIMEOUT_DURATION: Duration = Duration::from_secs(10); impl Default for Chain { fn default() -> Self { Self::new() } @@ -93,15 +101,16 @@ impl Chain { euro_per_energy: ExchangeRate, ) -> Result { Ok(Self { - parameters: ChainParameters::new_with_time_and_rates( + parameters: ChainParameters::new_with_time_and_rates( block_time, micro_ccd_per_euro, euro_per_energy, )?, - accounts: BTreeMap::new(), - modules: BTreeMap::new(), - contracts: BTreeMap::new(), - next_contract_index: 0, + accounts: BTreeMap::new(), + modules: BTreeMap::new(), + contracts: BTreeMap::new(), + next_contract_index: 0, + external_node_connection: None, }) } @@ -1056,6 +1065,167 @@ impl Chain { Ok(()) } + /// Set the exchange rates for Euro, CCD, and Energy by querying the + /// external node. + /// + /// The external node must be setup prior to this call via the method + /// [`Chain::setup_external_node_connection`], otherwise an error is + /// returned. + /// + /// *Example:* + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// let mut chain = Chain::new(); + /// chain + /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") + /// .unwrap(); + /// chain.set_exchange_rates_via_external_node().unwrap(); + /// ``` + pub fn set_exchange_rates_via_external_node(&mut self) -> Result<(), ExternalNodeError> { + let connection = self.external_node_connection()?; + + // A future for getting the exchange rates. Executed below. + let get_exchange_rates = async { + let chain_parameters = connection + .client + .get_block_chain_parameters(sdk::v2::BlockIdentifier::LastFinal) + .await? + .response; + let (euro_per_energy, micro_ccd_per_euro) = match chain_parameters { + sdk::v2::ChainParameters::V0(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V1(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V2(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + }; + Ok::<_, ExternalNodeError>((euro_per_energy, micro_ccd_per_euro)) + }; + + // Get the values from the external node with a timeout. + let (euro_per_energy, micro_ccd_per_euro) = connection.runtime.block_on(async { + timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_exchange_rates) + .await + .map_err(|_| ExternalNodeError::Timeout)? + })?; + + // We assume that the queryied values are valid. + self.parameters.micro_ccd_per_euro = micro_ccd_per_euro; + self.parameters.euro_per_energy = euro_per_energy; + + Ok(()) + } + + /// Set the block time by querying the external node. + /// + /// The external node must be setup prior to this call via the method + /// [`Chain::setup_external_node_connection`], otherwise an error is + /// returned. + /// + /// If the queried block time is prior to January 1, 1970 0:00:00 UTC (aka + /// “UNIX timestamp”), a block time of `0` will be used instead. + /// + /// *Example:* + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// let mut chain = Chain::new(); + /// chain + /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") + /// .unwrap(); + /// chain.set_block_time_via_external_node().unwrap(); + /// ``` + pub fn set_block_time_via_external_node(&mut self) -> Result<(), ExternalNodeError> { + let connection = self.external_node_connection()?; + + // A future for getting the block time. Executed below. + let get_block_time = async { + let block_info = connection + .client + .get_block_info(sdk::v2::BlockIdentifier::LastFinal) + .await? + .response; + let block_time = block_info.block_slot_time; + Ok::<_, ExternalNodeError>(block_time) + }; + + // Get the values from the external node with a timeout. + let block_time = connection.runtime.block_on(async { + timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_block_time) + .await + .map_err(|_| ExternalNodeError::Timeout)? + })?; + + // Get the timestamp in milliseconds *before or after* the unix epoch. + let timestamp_before_after: i64 = block_time.timestamp_millis(); + // Our `Timestamp` only supports times *after* the unix epoch, so we use `0` if + // the queried timestamp is negative. + let timestamp_after: u64 = i64::max(timestamp_before_after, 0) as u64; + self.parameters.block_time = Timestamp::from_timestamp_millis(timestamp_after); + + Ok(()) + } + + /// Set up a connection to an external Concordium node. + /// + /// The connection can be used for getting the current exchange rates + /// between CCD, Euro and Energy. + /// + /// *Example:* + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// # let mut chain = Chain::new(); + /// chain + /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") + /// .unwrap(); + /// ``` + pub fn setup_external_node_connection( + &mut self, + endpoint: &str, + ) -> Result<(), SetupExternalNodeError> { + let endpoint: sdk::v2::Endpoint = FromStr::from_str(endpoint)?; + // Create the Tokio runtime. This should never fail, unless nested runtimes are + // created. + let runtime = runtime::Builder::new_multi_thread() + // Enable time, so timeouts can be used. + .enable_time() + // Enable I/O, so networking and other types of calls are possible. + .enable_io() + .thread_keep_alive(Duration::from_secs(2)) + .build() + .expect("Internal error: Could not create Tokio runtime."); + + // A future for getting the client. Executed below. + let get_client = async { + // Try to create the client, which also checks that the connection is valid. + let client = sdk::v2::Client::new(endpoint).await?; + Ok::(client) + }; + + // Get the client synchronously by blocking until the async returns. + let client = runtime.block_on(async { + timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_client) + .await + .map_err(|_| SetupExternalNodeError::Timeout)? + })?; + + // Set or replace the node connection. + self.external_node_connection = Some(ExternalNodeConnection { client, runtime }); + + Ok(()) + } + + /// Try to get the [`ExternalNodeConnection`] or return an error. + /// + /// The connection is only available, if the [`Chain`] has been created with + fn external_node_connection( + &mut self, + ) -> Result<&mut ExternalNodeConnection, ExternalNodeNotConfigured> { + match &mut self.external_node_connection { + None => Err(ExternalNodeNotConfigured), + Some(data) => Ok(data), + } + } + /// Return the current microCCD per euro exchange rate. pub fn micro_ccd_per_euro(&self) -> ExchangeRate { self.parameters.micro_ccd_per_euro } @@ -1301,6 +1471,20 @@ pub(crate) fn contract_events_from_logs(logs: v0::Logs) -> Vec { logs.logs.into_iter().map(ContractEvent::from).collect() } +impl From for ExternalNodeError { + fn from(_: concordium_rust_sdk::endpoints::QueryError) -> Self { ExternalNodeError::QueryError } +} + +impl From for SetupExternalNodeError { + fn from(_: concordium_rust_sdk::endpoints::Error) -> Self { + SetupExternalNodeError::CannotConnect + } +} + +impl From for ExternalNodeError { + fn from(_: ExternalNodeNotConfigured) -> Self { Self::NotConfigured } +} + #[cfg(test)] mod tests { use concordium_base::base::AccountAddressEq; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 42523eba..67044b98 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -35,22 +35,34 @@ pub(crate) struct ChainParameters { pub(crate) euro_per_energy: ExchangeRate, } +#[derive(Debug)] +/// The connection and runtime needed for communicating with an external node. +pub(crate) struct ExternalNodeConnection { + /// An instantiated v2 Client from the Rust SDK. Used for communicating with + /// a node. + pub(crate) client: concordium_rust_sdk::v2::Client, + /// A Tokio runtime used to execute the async methods of the `client`. + pub(crate) runtime: tokio::runtime::Runtime, +} + /// Represents the blockchain and supports a number of operations, including /// creating accounts, deploying modules, initializing contract, updating /// contracts and invoking contracts. #[derive(Debug)] pub struct Chain { - pub(crate) parameters: ChainParameters, + pub(crate) parameters: ChainParameters, /// Accounts and info about them. /// This uses [`AccountAddressEq`] to ensure that account aliases are seen /// as one account. - pub(crate) accounts: BTreeMap, + pub(crate) accounts: BTreeMap, /// Smart contract modules. - pub(crate) modules: BTreeMap, + pub(crate) modules: BTreeMap, /// Smart contract instances. - pub(crate) contracts: BTreeMap, + pub(crate) contracts: BTreeMap, /// Next contract index to use when creating a new instance. - pub(crate) next_contract_index: u64, + pub(crate) next_contract_index: u64, + /// An optional connection to an external node. + pub(crate) external_node_connection: Option, } /// A smart contract instance. @@ -600,3 +612,37 @@ pub struct ExchangeRateError; #[derive(Debug, Error)] #[error("Any signer must have at least one key.")] pub struct ZeroKeysError; + +/// Errors that occur while setting up the connection to an external node. +#[derive(Debug, thiserror::Error)] +pub enum SetupExternalNodeError { + /// It was not possible to connect to a node on the provided endpoint. + #[error("Could not connect to the provided endpoint.")] + CannotConnect, + /// The endpoint provided was malformed. + #[error("The provided endpoint is malformed: '{endpoint}'.")] + MalformedEndpoint { endpoint: String }, + /// Request timed out. + #[error("The request timed out.")] + Timeout, +} + +/// Errors that occur while trying to communicate with an external node. +#[derive(Debug, Error)] +pub enum ExternalNodeError { + /// An external node has not been configured. + #[error("An external node has not been configured.")] + NotConfigured, + /// The query could not be performed. + #[error("Could not perform the query.")] + QueryError, + /// Request timed out. + #[error("The request timed out.")] + Timeout, +} + +/// The error returned when an external node has not been configured prior to +/// using it. +#[derive(Debug, Error)] +#[error("An external node has not been configured.")] +pub struct ExternalNodeNotConfigured; From 98d9488ef2c48260bcf748324d31b02bd9fca51e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 21 Jun 2023 14:26:51 +0200 Subject: [PATCH 170/208] Add method to get the block time --- contract-testing/CHANGELOG.md | 1 + contract-testing/src/impls.rs | 3 +++ contract-testing/src/types.rs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index a3e78afb..6f846d5c 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -6,6 +6,7 @@ - Add the method `setup_external_node_connection` for configuring the external node to use. - Add the method `set_exchange_rates_via_external_node` for setting the exchange rates. - Add the method `set_block_time_via_external_node` for setting the block time. + - Add the method `block_time` to get the current block time. ## 2.0.0 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 206f96e8..6a7f8684 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1231,6 +1231,9 @@ impl Chain { /// Return the current euro per energy exchange rate. pub fn euro_per_energy(&self) -> ExchangeRate { self.parameters.euro_per_energy } + + /// Return the current block time. + pub fn block_time(&self) -> Timestamp { self.parameters.block_time } } impl Account { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 67044b98..f393ad01 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -24,7 +24,7 @@ pub struct ContractModule { pub(crate) struct ChainParameters { /// The block time viewable inside the smart contracts. /// Defaults to `0`. - pub block_time: SlotTime, + pub(crate) block_time: SlotTime, /// MicroCCD per Euro ratio. // This is not public because we ensure a reasonable value during the construction of the // [`Chain`]. From cce3c19b83b8d80142f914d08921491301fff268 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 22 Jun 2023 10:59:46 +0200 Subject: [PATCH 171/208] Add block parameter and a "sticky block" functionality --- contract-testing/CHANGELOG.md | 2 + contract-testing/src/impls.rs | 195 +++++++++++++++++++++++++--------- contract-testing/src/lib.rs | 1 + contract-testing/src/types.rs | 10 +- 4 files changed, 156 insertions(+), 52 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 6f846d5c..543f75d2 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -7,6 +7,8 @@ - Add the method `set_exchange_rates_via_external_node` for setting the exchange rates. - Add the method `set_block_time_via_external_node` for setting the block time. - Add the method `block_time` to get the current block time. + - Add the method `sticky_block` for viewing the "sticky block", a concept explained in the `*via_external_node` methods. +- Add the `BlockHash` type (re-exported from internal crate) for use in the methods above. ## 2.0.0 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 6a7f8684..62dca53a 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -11,6 +11,7 @@ use concordium_base::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, + hashes::BlockHash, smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; @@ -22,7 +23,7 @@ use concordium_smart_contract_engine::{ }; use num_bigint::BigUint; use num_integer::Integer; -use std::{collections::BTreeMap, path::Path, str::FromStr, sync::Arc}; +use std::{collections::BTreeMap, future::Future, path::Path, str::FromStr, sync::Arc}; use tokio::{ runtime, time::{timeout, Duration}, @@ -1068,44 +1069,59 @@ impl Chain { /// Set the exchange rates for Euro, CCD, and Energy by querying the /// external node. /// + /// Use the `block` parameter to specify the block used when querying the + /// block time. For consistency between queries, the first block hash + /// used will be "sticky", which means that it is saved and used for + /// subsequent queries, unless overridden by specifying a `block`. If + /// `block == None` for the first query, it will use the *last final + /// block* instead. In either case, the block hash will be saved and + /// used for future queries. + /// To see the "sticky block", use [`sticky_block`][Chain::sticky_block]. + /// /// The external node must be setup prior to this call via the method /// [`Chain::setup_external_node_connection`], otherwise an error is /// returned. /// - /// *Example:* + /// # Example /// /// ```no_run /// # use concordium_smart_contract_testing::*; + /// use std::str::FromStr; + /// /// let mut chain = Chain::new(); /// chain /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") /// .unwrap(); - /// chain.set_exchange_rates_via_external_node().unwrap(); + /// + /// // Use the last final block and set the "sticky block" to it. + /// chain.set_exchange_rates_via_external_node(None).unwrap(); + /// + /// // Use a specific block instead of the sticky block. + /// // This does not change the sticky block. + /// chain + /// .set_exchange_rates_via_external_node(Some( + /// BlockHash::from_str("95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0") + /// .unwrap(), + /// )) + /// .unwrap(); /// ``` - pub fn set_exchange_rates_via_external_node(&mut self) -> Result<(), ExternalNodeError> { + pub fn set_exchange_rates_via_external_node( + &mut self, + block: Option, + ) -> Result<(), ExternalNodeError> { let connection = self.external_node_connection()?; - // A future for getting the exchange rates. Executed below. - let get_exchange_rates = async { - let chain_parameters = connection - .client - .get_block_chain_parameters(sdk::v2::BlockIdentifier::LastFinal) - .await? - .response; - let (euro_per_energy, micro_ccd_per_euro) = match chain_parameters { - sdk::v2::ChainParameters::V0(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - sdk::v2::ChainParameters::V1(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - sdk::v2::ChainParameters::V2(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - }; - Ok::<_, ExternalNodeError>((euro_per_energy, micro_ccd_per_euro)) - }; - - // Get the values from the external node with a timeout. - let (euro_per_energy, micro_ccd_per_euro) = connection.runtime.block_on(async { - timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_exchange_rates) - .await - .map_err(|_| ExternalNodeError::Timeout)? - })?; + // Get the values from the external node. + let (euro_per_energy, micro_ccd_per_euro) = + connection.with_client(block, |block_identifier, mut client| async move { + let response = client.get_block_chain_parameters(block_identifier).await?; + let (euro_per_energy, micro_ccd_per_euro) = match response.response { + sdk::v2::ChainParameters::V0(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V1(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V2(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + }; + Ok((response.block_hash, (euro_per_energy, micro_ccd_per_euro))) + })?; // We assume that the queryied values are valid. self.parameters.micro_ccd_per_euro = micro_ccd_per_euro; @@ -1116,6 +1132,15 @@ impl Chain { /// Set the block time by querying the external node. /// + /// Use the `block` parameter to specify the block used when querying the + /// block time. For consistency between queries, the first block hash + /// used will be "sticky", which means that it is saved and used for + /// subsequent queries, unless overridden by specifying a `block`. If + /// `block == None` for the first query, it will use the *last final + /// block* instead. In either case, the block hash will be saved and + /// used for future queries. + /// To see the "sticky block", use [`sticky_block`][Chain::sticky_block]. + /// /// The external node must be setup prior to this call via the method /// [`Chain::setup_external_node_connection`], otherwise an error is /// returned. @@ -1123,42 +1148,46 @@ impl Chain { /// If the queried block time is prior to January 1, 1970 0:00:00 UTC (aka /// “UNIX timestamp”), a block time of `0` will be used instead. /// - /// *Example:* + /// # Example /// /// ```no_run /// # use concordium_smart_contract_testing::*; + /// use std::str::FromStr; + /// /// let mut chain = Chain::new(); /// chain /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") /// .unwrap(); - /// chain.set_block_time_via_external_node().unwrap(); + /// // Use the last final block and set the "sticky block" to it. + /// chain.set_block_time_via_external_node(None).unwrap(); + /// + /// // Use a specific block instead of the sticky block. + /// // This does not change the sticky block. + /// chain + /// .set_block_time_via_external_node(Some( + /// BlockHash::from_str("95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0") + /// .unwrap(), + /// )) + /// .unwrap(); /// ``` - pub fn set_block_time_via_external_node(&mut self) -> Result<(), ExternalNodeError> { + pub fn set_block_time_via_external_node( + &mut self, + block: Option, + ) -> Result<(), ExternalNodeError> { let connection = self.external_node_connection()?; - // A future for getting the block time. Executed below. - let get_block_time = async { - let block_info = connection - .client - .get_block_info(sdk::v2::BlockIdentifier::LastFinal) - .await? - .response; - let block_time = block_info.block_slot_time; - Ok::<_, ExternalNodeError>(block_time) - }; - - // Get the values from the external node with a timeout. - let block_time = connection.runtime.block_on(async { - timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_block_time) - .await - .map_err(|_| ExternalNodeError::Timeout)? - })?; + // Get the timestamp in milliseconds *before or after* the unix epoch from the + // external node. + let timestamp_before_or_after = + connection.with_client(block, |block_identifier, mut client| async move { + let response = client.get_block_info(block_identifier).await?; + let block_time = response.response.block_slot_time.timestamp_millis(); + Ok((response.block_hash, block_time)) + })?; - // Get the timestamp in milliseconds *before or after* the unix epoch. - let timestamp_before_after: i64 = block_time.timestamp_millis(); // Our `Timestamp` only supports times *after* the unix epoch, so we use `0` if // the queried timestamp is negative. - let timestamp_after: u64 = i64::max(timestamp_before_after, 0) as u64; + let timestamp_after: u64 = i64::max(timestamp_before_or_after, 0) as u64; self.parameters.block_time = Timestamp::from_timestamp_millis(timestamp_after); Ok(()) @@ -1169,7 +1198,7 @@ impl Chain { /// The connection can be used for getting the current exchange rates /// between CCD, Euro and Energy. /// - /// *Example:* + /// # Example /// /// ```no_run /// # use concordium_smart_contract_testing::*; @@ -1209,7 +1238,11 @@ impl Chain { })?; // Set or replace the node connection. - self.external_node_connection = Some(ExternalNodeConnection { client, runtime }); + self.external_node_connection = Some(ExternalNodeConnection { + client, + runtime, + sticky_block: None, + }); Ok(()) } @@ -1234,6 +1267,68 @@ impl Chain { /// Return the current block time. pub fn block_time(&self) -> Timestamp { self.parameters.block_time } + + /// Return the "sticky block", i.e. the block that is saved and will be used + /// by default for future queries. + /// + /// See [`set_block_time_via_external_node`][Chain::set_block_time_via_external_node] for a more detailed explanation. + pub fn sticky_block(&self) -> Option { + self.external_node_connection + .as_ref() + .and_then(|conn| conn.sticky_block) + } +} + +impl ExternalNodeConnection { + /// Execute an async task with the [`sdk::v2::Client`]. + /// + /// The task will be performed using one of the following three blocks: + /// 1. If a `block` hash is provided, it will be used. + /// 2. Otherwise, if `self.sticky_block` is `Some`, it will be used. + /// 3. Lastly, if neither are present, the last final block is used. + /// + /// If `self.sticky_block` is `None`, then it will be set to the block used + /// in cases 1 and 3. + /// + /// If the task takes longer than [`EXTERNAL_NODE_TIMEOUT_DURATION`] then + /// the connection times out and an [`Err(ExternalNodeError::Timeout)`] is + /// returned. + fn with_client( + &mut self, + block: Option, + f: F, + ) -> Result + where + F: FnOnce(sdk::v2::BlockIdentifier, sdk::v2::Client) -> Fut, + Fut: Future>, { + // Get the block identifier in one of the three ways. + let block_identifier = match (block, self.sticky_block) { + // Case 1 + (Some(block), _) => sdk::v2::BlockIdentifier::Given(block), + // Case 2 + (_, Some(sticky_block)) => sdk::v2::BlockIdentifier::Given(sticky_block), + // Case 3 + _ => sdk::v2::BlockIdentifier::LastFinal, + }; + // Clone the client so it can be moved to the async block. + let client = self.client.clone(); + // Run the future and timeout if it takes too long. + let res = self.runtime.block_on(async move { + timeout(EXTERNAL_NODE_TIMEOUT_DURATION, f(block_identifier, client)) + .await + .map_err(|_| ExternalNodeError::Timeout)? + }); + // Extract the return value and maybe set the stick block. + match res { + Ok((block_hash, return_value)) => { + if self.sticky_block.is_none() { + self.sticky_block = Some(block_hash) + } + Ok(return_value) + } + Err(e) => Err(e), + } + } } impl Account { diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 9891cfd5..5bc864bd 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -100,6 +100,7 @@ pub use concordium_base::{ EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, }, + hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, transactions::{InitContractPayload, UpdateContractPayload}, }; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index f393ad01..2bac36ed 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -4,6 +4,7 @@ use concordium_base::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, + hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, }; use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; @@ -40,9 +41,14 @@ pub(crate) struct ChainParameters { pub(crate) struct ExternalNodeConnection { /// An instantiated v2 Client from the Rust SDK. Used for communicating with /// a node. - pub(crate) client: concordium_rust_sdk::v2::Client, + pub(crate) client: concordium_rust_sdk::v2::Client, /// A Tokio runtime used to execute the async methods of the `client`. - pub(crate) runtime: tokio::runtime::Runtime, + pub(crate) runtime: tokio::runtime::Runtime, + /// The block hash to use for queries. + /// The hash is "sticky" in the sense that the first query without a + /// provided blockhash will set this field and further queries without a + /// hash will continue to use this value. + pub(crate) sticky_block: Option, } /// Represents the blockchain and supports a number of operations, including From a1583b08221c048b704a27bbfe2c11a535096b7a Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 22 Jun 2023 11:04:12 +0200 Subject: [PATCH 172/208] Correct docs --- contract-testing/src/types.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 2bac36ed..deb9f801 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -44,10 +44,10 @@ pub(crate) struct ExternalNodeConnection { pub(crate) client: concordium_rust_sdk::v2::Client, /// A Tokio runtime used to execute the async methods of the `client`. pub(crate) runtime: tokio::runtime::Runtime, - /// The block hash to use for queries. - /// The hash is "sticky" in the sense that the first query without a - /// provided blockhash will set this field and further queries without a - /// hash will continue to use this value. + /// The sticky block hash to use for queries without specific blocks. + /// + /// The hash is "sticky" in the sense that it is set on the first query and + /// will be used for subsequent queries that do *not* specify block hash. pub(crate) sticky_block: Option, } From f310e3eb212ea4138478f26a05705cb732fd702f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 22 Jun 2023 16:39:43 +0200 Subject: [PATCH 173/208] Add `ChainBuilder` and tests - Some of the tests use I/O and they are placed in a `io_tests` module which is skipped by the CI. --- contract-testing/CHANGELOG.md | 5 +- contract-testing/src/impls.rs | 374 ++++++++++++++++++++++++++++++++-- contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 40 +++- 4 files changed, 392 insertions(+), 29 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 543f75d2..8f5f98ee 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -3,12 +3,13 @@ ## Unreleased changes - Add methods to `Chain` for setting the exchange rates and block time based on queries from an external node. - - Add the method `setup_external_node_connection` for configuring the external node to use. + - Add a builder pattern via `Chain::builder` for setting up the connection to an external node and other parameters. - Add the method `set_exchange_rates_via_external_node` for setting the exchange rates. - Add the method `set_block_time_via_external_node` for setting the block time. - - Add the method `block_time` to get the current block time. + - Add the methods `set_block_time` and `block_time` to set and get the block time. - Add the method `sticky_block` for viewing the "sticky block", a concept explained in the `*via_external_node` methods. - Add the `BlockHash` type (re-exported from internal crate) for use in the methods above. +- Expose the `Timestamp` type, which `SlotTime` is an alias of. ## 2.0.0 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 62dca53a..07fdf625 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -90,12 +90,163 @@ impl ChainParameters { } } +impl ChainBuilder { + /// Create a new [`ChainBuilder`] for constructing the [`Chain`]. + /// + /// Can also be created via the [`Chain::builder`] method. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// + /// let chain = ChainBuilder::new() + /// // Use zero or more builder methods, for example: + /// .micro_ccd_per_euro(ExchangeRate::new_unchecked(50000, 1)) + /// .block_time(Timestamp::from_timestamp_millis(123)) + /// // Then build: + /// .build() + /// .unwrap(); + /// ``` + pub fn new() -> Self { + Self { + external_node_endpoint: None, + micro_ccd_per_euro: None, + euro_per_energy: None, + block_time: None, + } + } + + /// Configure a connection to an external Concordium node. + /// + /// The connection can be used for getting the current exchange rates + /// between CCD, Euro and Energy. + /// + /// # Example + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// let chain = Chain::builder() + /// .external_node_connection("http://node.testnet.concordium.com:20000") + /// .build() + /// .unwrap(); + /// ``` + pub fn external_node_connection(mut self, endpoint: &str) -> Self { + self.external_node_endpoint = Some(endpoint.to_owned()); + self + } + + /// Configure the exchange rate between microCCD and euro. + /// + /// By default the rate is `50000 / 1`. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .micro_ccd_per_euro(ExchangeRate::new_unchecked(50000, 1)) + /// .build() + /// .unwrap(); + /// ``` + pub fn micro_ccd_per_euro(mut self, exchange_rate: ExchangeRate) -> Self { + self.micro_ccd_per_euro = Some(exchange_rate); + self + } + + /// Configure the exchange rate between microCCD and euro. + /// + /// By default the rate is `1 / 50000`. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .euro_per_energy(ExchangeRate::new_unchecked(1, 50000)) + /// .build() + /// .unwrap(); + /// ``` + pub fn euro_per_energy(mut self, exchange_rate: ExchangeRate) -> Self { + self.euro_per_energy = Some(exchange_rate); + self + } + + /// Configure the block time. + /// + /// By default the block time is `0`. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .block_time(Timestamp::from_timestamp_millis(1687440701000)) + /// .build() + /// .unwrap(); + /// ``` + pub fn block_time(mut self, block_time: Timestamp) -> Self { + self.block_time = Some(block_time); + self + } + + /// Build the [`Chain`] with the configured options. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// + /// let chain = Chain::builder() + /// // Use zero or more builder methods, for example: + /// .euro_per_energy(ExchangeRate::new_unchecked(1, 50000)) + /// .micro_ccd_per_euro(ExchangeRate::new_unchecked(50000, 1)) + /// // Then build: + /// .build() + /// .unwrap(); + /// ``` + pub fn build(self) -> Result { + // Create the chain with default parameters. + let mut chain = Chain::new(); + + // Replace the default exchange rate parameters if any are provided. + if let Some(euro_per_energy) = self.euro_per_energy { + chain.parameters.euro_per_energy = euro_per_energy; + } + if let Some(micro_ccd_per_euro) = self.micro_ccd_per_euro { + chain.parameters.micro_ccd_per_euro = micro_ccd_per_euro + }; + + // Check the exchange rates and return early if they are invalid. + check_exchange_rates( + chain.parameters.euro_per_energy, + chain.parameters.micro_ccd_per_euro, + )?; + + // Replace the default block time if provided. + if let Some(block_time) = self.block_time { + chain.parameters.block_time = block_time; + } + + // Setup the external node connection if provided. + if let Some(endpoint) = self.external_node_endpoint { + chain.setup_external_node_connection(&endpoint)?; + } + + Ok(chain) + } +} + impl Chain { + /// Get a [`ChainBuilder`] for constructing a new [`Chain`] with a builder + /// pattern. + /// + /// See the [`ChainBuilder`] for more details. + pub fn builder() -> ChainBuilder { ChainBuilder::new() } + /// Create a new [`Chain`](Self) where all the configurable parameters are /// provided. /// /// Returns an error if the exchange rates provided makes one energy cost /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. + /// + /// *For more configuration options and flexibility, use the builder + /// pattern. See [`Chain::builder`].* pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -118,6 +269,9 @@ impl Chain { /// Create a new [`Chain`](Self) with a specified `block_time` where /// - `micro_ccd_per_euro` defaults to `50000 / 1` /// - `euro_per_energy` defaults to `1 / 50000`. + /// + /// *For more configuration options and flexibility, use the builder + /// pattern. See [`Chain::builder`].* pub fn new_with_time(block_time: SlotTime) -> Self { Self { parameters: ChainParameters::new_with_time(block_time), @@ -131,6 +285,9 @@ impl Chain { /// - `euro_per_energy` defaults to `1 / 50000`. /// /// With these exchange rates, one energy costs one microCCD. + /// + /// *For more configuration options and flexibility, use the builder + /// pattern. See [`Chain::builder`].* pub fn new() -> Self { Self::new_with_time_and_rates( Timestamp::from_timestamp_millis(0), @@ -1088,9 +1245,9 @@ impl Chain { /// # use concordium_smart_contract_testing::*; /// use std::str::FromStr; /// - /// let mut chain = Chain::new(); - /// chain - /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") + /// let mut chain = Chain::builder() + /// .external_node_connection("http://node.testnet.concordium.com:20000") + /// .build() /// .unwrap(); /// /// // Use the last final block and set the "sticky block" to it. @@ -1130,6 +1287,23 @@ impl Chain { Ok(()) } + /// Set the block time on the [`Chain`]. + /// + /// You can also set the block time via the builder pattern, see + /// [`ChainBuilder::block_time`]. + /// + /// # Example + /// + /// ``` + /// # use concordium_smart_contract_testing::*; + /// + /// let mut chain = Chain::new(); + /// chain.set_block_time(Timestamp::from_timestamp_millis(123)); + /// ``` + pub fn set_block_time(&mut self, block_time: Timestamp) { + self.parameters.block_time = block_time; + } + /// Set the block time by querying the external node. /// /// Use the `block` parameter to specify the block used when querying the @@ -1154,10 +1328,11 @@ impl Chain { /// # use concordium_smart_contract_testing::*; /// use std::str::FromStr; /// - /// let mut chain = Chain::new(); - /// chain - /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") + /// let mut chain = Chain::builder() + /// .external_node_connection("http://node.testnet.concordium.com:20000") + /// .build() /// .unwrap(); + /// /// // Use the last final block and set the "sticky block" to it. /// chain.set_block_time_via_external_node(None).unwrap(); /// @@ -1197,17 +1372,7 @@ impl Chain { /// /// The connection can be used for getting the current exchange rates /// between CCD, Euro and Energy. - /// - /// # Example - /// - /// ```no_run - /// # use concordium_smart_contract_testing::*; - /// # let mut chain = Chain::new(); - /// chain - /// .setup_external_node_connection("http://node.testnet.concordium.com:20000") - /// .unwrap(); - /// ``` - pub fn setup_external_node_connection( + fn setup_external_node_connection( &mut self, endpoint: &str, ) -> Result<(), SetupExternalNodeError> { @@ -1570,19 +1735,21 @@ pub(crate) fn contract_events_from_logs(logs: v0::Logs) -> Vec { } impl From for ExternalNodeError { - fn from(_: concordium_rust_sdk::endpoints::QueryError) -> Self { ExternalNodeError::QueryError } + fn from(_: concordium_rust_sdk::endpoints::QueryError) -> Self { Self::QueryError } } impl From for SetupExternalNodeError { - fn from(_: concordium_rust_sdk::endpoints::Error) -> Self { - SetupExternalNodeError::CannotConnect - } + fn from(_: concordium_rust_sdk::endpoints::Error) -> Self { Self::CannotConnect } } impl From for ExternalNodeError { fn from(_: ExternalNodeNotConfigured) -> Self { Self::NotConfigured } } +impl From for ChainBuilderError { + fn from(_: ExchangeRateError) -> Self { Self::ExchangeRateError } +} + #[cfg(test)] mod tests { use concordium_base::base::AccountAddressEq; @@ -1660,4 +1827,169 @@ mod tests { Some(expected_amount_other) ); } + + /// Test that building a chain with valid parameters succeeds. + /// + /// This test does *not* include external node endpoint, see + /// [`test_chain_builder_with_valid_parameters_and_with_io`] for the reason. + #[test] + fn test_chain_builder_with_valid_parameters() { + let micro_ccd_per_euro = ExchangeRate::new_unchecked(123, 1); + let euro_per_energy = ExchangeRate::new_unchecked(1, 1234); + let block_time = Timestamp::from_timestamp_millis(12345); + let chain = Chain::builder() + .micro_ccd_per_euro(micro_ccd_per_euro) + .euro_per_energy(euro_per_energy) + .block_time(block_time) + .build() + .unwrap(); + + assert_eq!(chain.micro_ccd_per_euro(), micro_ccd_per_euro); + assert_eq!(chain.euro_per_energy(), euro_per_energy); + assert_eq!(chain.block_time(), block_time); + } + + /// Test that building a chain with exchange rates that are out of bounds + /// fails with the right error. + #[test] + fn test_chain_builder_with_invalid_exchange_rates() { + let micro_ccd_per_euro = ExchangeRate::new_unchecked(1000000, 1); + let euro_per_energy = ExchangeRate::new_unchecked(100000000, 1); + let chain = Chain::builder() + .micro_ccd_per_euro(micro_ccd_per_euro) + .euro_per_energy(euro_per_energy) + .build() + .unwrap_err(); + + assert_eq!(chain, ChainBuilderError::ExchangeRateError); + } +} + +/// Tests that use I/O (network) and should therefore *not* be run in the CI. +/// +/// To skip the tests use `cargo test -- --skip io_tests` +#[cfg(test)] +mod io_tests { + use super::*; + + /// Test that building a chain using all the parameters available, + /// *including* the I/O parameter of external node connection. + #[test] + fn test_chain_builder_with_valid_parameters_and_external_node() { + let micro_ccd_per_euro = ExchangeRate::new_unchecked(123, 1); + let euro_per_energy = ExchangeRate::new_unchecked(1, 1234); + let block_time = Timestamp::from_timestamp_millis(12345); + let chain = Chain::builder() + .micro_ccd_per_euro(micro_ccd_per_euro) + .euro_per_energy(euro_per_energy) + .block_time(block_time) + .external_node_connection("http://node.testnet.concordium.com:20000") + .build() + .unwrap(); + + assert_eq!(chain.micro_ccd_per_euro(), micro_ccd_per_euro); + assert_eq!(chain.euro_per_energy(), euro_per_energy); + assert_eq!(chain.block_time(), block_time); + } + + /// Test that setting the exchange rates via an external node succeeds. + /// + /// Instead of relying on a the rate from a specific block, the test just + /// checks that the exchange differs from the default afterwards. + #[test] + fn test_setting_exchange_rates_via_external_node() { + let mut chain = Chain::builder() + .external_node_connection("http://node.testnet.concordium.com:20000") + .build() + .unwrap(); + + let default_exchange_rate = chain.micro_ccd_per_euro(); + + chain.set_exchange_rates_via_external_node(None).unwrap(); + assert_ne!(chain.micro_ccd_per_euro(), default_exchange_rate); + } + + /// Test that the sticky block is kept when using + #[test] + fn test_sticky_block_from_last_final() { + let mut chain = Chain::builder() + .external_node_connection("http://node.testnet.concordium.com:20000") + .build() + .unwrap(); + // Don't provide a block, so last final is used. + chain.set_block_time_via_external_node(None).unwrap(); + let block_time_1 = chain.block_time(); + let sticky_block_1 = chain.sticky_block().unwrap(); + + // Set block time using a specific block that exists. + chain + .set_block_time_via_external_node(Some( + BlockHash::from_str( + "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", + ) + .unwrap(), + )) + .unwrap(); + // Check that this does not mutate the sticky block. + let sticky_block_2 = chain.sticky_block().unwrap(); + // Get the changed block time. + let block_time_2 = chain.block_time(); + assert_eq!(sticky_block_1, sticky_block_2); + // The block time in the specified block should be lower than last final. + assert!(block_time_2 < block_time_1); + } + + #[test] + fn test_sticky_block_from_specific() { + let mut chain = Chain::builder() + .external_node_connection("http://node.testnet.concordium.com:20000") + .build() + .unwrap(); + chain + .set_block_time_via_external_node(Some( + BlockHash::from_str( + "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", + ) + .unwrap(), + )) + .unwrap(); + let sticky_block_1 = chain.sticky_block().unwrap(); + let block_time_1 = chain.block_time(); + // This does not mutate the sticky block already set. + chain.set_block_time_via_external_node(None).unwrap(); + let sticky_block_2 = chain.sticky_block().unwrap(); + let block_time_2 = chain.block_time(); + // Check that the sticky block hasn't changed. + assert_eq!(sticky_block_1, sticky_block_2); + // Check that the block matches the one specified initially. + assert_eq!(sticky_block_1, + BlockHash::from_str( + "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", + ) + .unwrap()); + // The block times should be identical since both the sticky block is used for + // the second call to the external node due to `None` being passed in. + assert_eq!(block_time_2, block_time_1); + } + + /// Test that the correct error is returned when an unknown block is given. + /// + /// The block used is one from mainnet, which is extremely unlikely to also + /// appear on testnet. + #[test] + fn test_block_time_from_unknown_block() { + let mut chain = Chain::builder() + .external_node_connection("http://node.testnet.concordium.com:20000") + .build() + .unwrap(); + let err = chain + .set_block_time_via_external_node(Some( + BlockHash::from_str( + "4f38c7e63645c59e9bf32f7ca837a029810b21c439f7492c3cebe229a2e3ea07", // A block from mainnet. + ) + .unwrap(), + )) + .unwrap_err(); + assert_eq!(err, ExternalNodeError::QueryError); + } } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 5bc864bd..83e0e4a9 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -98,7 +98,7 @@ pub use concordium_base::{ contracts_common::{ from_bytes, to_bytes, AccountAddress, Address, Amount, ContractAddress, ContractName, EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, - OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, + OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, Timestamp, }, hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index deb9f801..3830cdbe 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -2,7 +2,7 @@ use concordium_base::{ base::{AccountAddressEq, Energy}, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, - ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, + ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, Timestamp, }, hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, @@ -21,6 +21,7 @@ pub struct ContractModule { pub(crate) artifact: Arc>, } +/// The chain parameters. #[derive(Debug)] pub(crate) struct ChainParameters { /// The block time viewable inside the smart contracts. @@ -36,8 +37,8 @@ pub(crate) struct ChainParameters { pub(crate) euro_per_energy: ExchangeRate, } -#[derive(Debug)] /// The connection and runtime needed for communicating with an external node. +#[derive(Debug)] pub(crate) struct ExternalNodeConnection { /// An instantiated v2 Client from the Rust SDK. Used for communicating with /// a node. @@ -71,6 +72,19 @@ pub struct Chain { pub(crate) external_node_connection: Option, } +/// A builder for the [`Chain`]. +#[derive(Debug, Default)] +pub struct ChainBuilder { + /// The configured endpoint for an external node connection. + pub(crate) external_node_endpoint: Option, + /// The configured exchange rate between microCCD and euro. + pub(crate) micro_ccd_per_euro: Option, + /// The configured exchange rate between euro and energy. + pub(crate) euro_per_energy: Option, + /// The configured block time. + pub(crate) block_time: Option, +} + /// A smart contract instance. #[derive(Clone, Debug)] pub struct Contract { @@ -620,7 +634,7 @@ pub struct ExchangeRateError; pub struct ZeroKeysError; /// Errors that occur while setting up the connection to an external node. -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum SetupExternalNodeError { /// It was not possible to connect to a node on the provided endpoint. #[error("Could not connect to the provided endpoint.")] @@ -634,7 +648,7 @@ pub enum SetupExternalNodeError { } /// Errors that occur while trying to communicate with an external node. -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq, Eq)] pub enum ExternalNodeError { /// An external node has not been configured. #[error("An external node has not been configured.")] @@ -649,6 +663,22 @@ pub enum ExternalNodeError { /// The error returned when an external node has not been configured prior to /// using it. -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq, Eq)] #[error("An external node has not been configured.")] pub struct ExternalNodeNotConfigured; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ChainBuilderError { + /// The provided exchange rates are not valid. + /// Meaning that they do not correspond to one energy costing less than + /// `u64::MAX / ` + /// [`concordium_base::constants::MAX_ALLOWED_INVOKE_ENERGY`]`. + #[error("An exchange rate was too high.")] + ExchangeRateError, + /// An error occurred while setting up the connection to an external node. + #[error("Error occurred while setting up the connection to an external node: {error}")] + SetupExternalNodeError { + #[from] + error: SetupExternalNodeError, + }, +} From ad7a71dfd88fa27bcb9abe97c52e7ce26c56e554 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 27 Jun 2023 13:56:05 +0200 Subject: [PATCH 174/208] Allow external chain parameters only in Builder - Also makes a number of other small changes, see the changes in changelog file. --- contract-testing/CHANGELOG.md | 19 +- contract-testing/src/impls.rs | 651 ++++++++++++++++++---------------- contract-testing/src/lib.rs | 6 +- contract-testing/src/types.rs | 118 ++++-- 4 files changed, 458 insertions(+), 336 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 8f5f98ee..82b2f444 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -2,14 +2,17 @@ ## Unreleased changes -- Add methods to `Chain` for setting the exchange rates and block time based on queries from an external node. - - Add a builder pattern via `Chain::builder` for setting up the connection to an external node and other parameters. - - Add the method `set_exchange_rates_via_external_node` for setting the exchange rates. - - Add the method `set_block_time_via_external_node` for setting the block time. - - Add the methods `set_block_time` and `block_time` to set and get the block time. - - Add the method `sticky_block` for viewing the "sticky block", a concept explained in the `*via_external_node` methods. -- Add the `BlockHash` type (re-exported from internal crate) for use in the methods above. -- Expose the `Timestamp` type, which `SlotTime` is an alias of. +- Add functionality for setting the exchange rates and block time of the chain based on queries from an external node. + - Configured via a builder pattern, see `Chain::builder`. +- Add methods to `Chain`: + - `external_query_block` to get the default block used for external queries + - `block_time` to get the block time + - `tick_block_time` to increase the block time by a `Duration` +- Add the following types by re-exporting them from internal crates: + - `BlockHash` + - `Timestamp` which `SlotTime` is an alias of. + - `Duration` + - `Endpoint` ## 2.0.0 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 07fdf625..8b6957bd 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -9,13 +9,13 @@ use concordium_base::{ constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, + Duration, ExchangeRate, ExchangeRates, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, hashes::BlockHash, smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, transactions::{self, cost, InitContractPayload, UpdateContractPayload}, }; -use concordium_rust_sdk as sdk; +use concordium_rust_sdk::{self as sdk, v2::Endpoint}; use concordium_smart_contract_engine::{ v0, v1::{self, InvokeResponse}, @@ -23,14 +23,11 @@ use concordium_smart_contract_engine::{ }; use num_bigint::BigUint; use num_integer::Integer; -use std::{collections::BTreeMap, future::Future, path::Path, str::FromStr, sync::Arc}; -use tokio::{ - runtime, - time::{timeout, Duration}, -}; +use std::{collections::BTreeMap, future::Future, path::Path, sync::Arc}; +use tokio::{runtime, time::timeout}; /// The duration set for timeouts when communicating with an external node. -const EXTERNAL_NODE_TIMEOUT_DURATION: Duration = Duration::from_secs(10); +const EXTERNAL_NODE_TIMEOUT_DURATION: tokio::time::Duration = tokio::time::Duration::from_secs(10); impl Default for Chain { fn default() -> Self { Self::new() } @@ -95,6 +92,9 @@ impl ChainBuilder { /// /// Can also be created via the [`Chain::builder`] method. /// + /// To complete the building process, use [`ChainBuilder::build`], see the + /// example below. + /// /// # Example /// ``` /// # use concordium_smart_contract_testing::*; @@ -110,9 +110,13 @@ impl ChainBuilder { pub fn new() -> Self { Self { external_node_endpoint: None, - micro_ccd_per_euro: None, - euro_per_energy: None, - block_time: None, + external_query_block: None, + micro_ccd_per_euro: None, + micro_ccd_per_euro_from_external: false, + euro_per_energy: None, + euro_per_energy_from_external: false, + block_time: None, + block_time_from_external: false, } } @@ -126,12 +130,45 @@ impl ChainBuilder { /// ```no_run /// # use concordium_smart_contract_testing::*; /// let chain = Chain::builder() - /// .external_node_connection("http://node.testnet.concordium.com:20000") + /// .external_node_connection(Endpoint::from_static( + /// "http://node.testnet.concordium.com:20000", + /// )) /// .build() /// .unwrap(); /// ``` - pub fn external_node_connection(mut self, endpoint: &str) -> Self { - self.external_node_endpoint = Some(endpoint.to_owned()); + pub fn external_node_connection(mut self, endpoint: impl Into) -> Self { + self.external_node_endpoint = Some(endpoint.into()); + self + } + + /// Configure the block to be used for all external queries. + /// + /// If this is not set, then the last final block will be queried during + /// [`ChainBuilder::build`] and saved, so it can be used for future queries. + /// + /// This can only be used in combination with + /// [`external_node_connection`][Self::external_node_connection]. + /// + /// To view the configured block, see [`Chain::external_query_block`]. + /// + /// # Example + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// let chain = Chain::builder() + /// .external_node_connection(Endpoint::from_static( + /// "http://node.testnet.concordium.com:20000", + /// )) + /// .external_query_block( + /// "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0" + /// .parse() + /// .unwrap(), + /// ) + /// .build() + /// .unwrap(); + /// ``` + pub fn external_query_block(mut self, query_block: BlockHash) -> Self { + self.external_query_block = Some(query_block); self } @@ -139,6 +176,9 @@ impl ChainBuilder { /// /// By default the rate is `50000 / 1`. /// + /// This cannot be used together with + /// [`ChainBuilder::micro_ccd_per_euro_from_external`]. + /// /// # Example /// ``` /// # use concordium_smart_contract_testing::*; @@ -156,6 +196,9 @@ impl ChainBuilder { /// /// By default the rate is `1 / 50000`. /// + /// This cannot be used together with + /// [`ChainBuilder::euro_per_energy_from_external`]. + /// /// # Example /// ``` /// # use concordium_smart_contract_testing::*; @@ -169,10 +212,61 @@ impl ChainBuilder { self } + /// Configure the exchange rate between microCCD and euro using the external + /// node connection. + /// + /// This can only be used in combination with + /// [`external_node_connection`][Self::external_node_connection], and it + /// cannot be used together with + /// [`micro_ccd_per_euro`][Self::micro_ccd_per_euro]. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .external_node_connection(Endpoint::from_static( + /// "http://node.testnet.concordium.com:20000", + /// )) + /// .micro_ccd_per_euro_from_external() + /// .build() + /// .unwrap(); + /// ``` + pub fn micro_ccd_per_euro_from_external(mut self) -> Self { + self.micro_ccd_per_euro_from_external = true; + self + } + + /// Configure the exchange rate between euro and energy using the external + /// node connection. + /// + /// This can only be used in combination with + /// [`external_node_connection`][Self::external_node_connection], and it + /// cannot be used together with + /// [`euro_per_energy`][Self::euro_per_energy]. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .external_node_connection(Endpoint::from_static( + /// "http://node.testnet.concordium.com:20000", + /// )) + /// .euro_per_energy_from_external() + /// .build() + /// .unwrap(); + /// ``` + pub fn euro_per_energy_from_external(mut self) -> Self { + self.euro_per_energy_from_external = true; + self + } + /// Configure the block time. /// /// By default the block time is `0`. /// + /// This cannot be used in combination with + /// [`ChainBuilder::block_time_from_external`]. + /// /// # Example /// ``` /// # use concordium_smart_contract_testing::*; @@ -186,6 +280,29 @@ impl ChainBuilder { self } + /// Configure the block time using the external node connection. + /// + /// This can only be used in combination with + /// [`external_node_connection`][Self::external_node_connection], and it + /// cannot be used together with + /// [`block_time`][Self::block_time]. + /// + /// # Example + /// ``` + /// # use concordium_smart_contract_testing::*; + /// let chain = ChainBuilder::new() + /// .external_node_connection(Endpoint::from_static( + /// "http://node.testnet.concordium.com:20000", + /// )) + /// .block_time_from_external() + /// .build() + /// .unwrap(); + /// ``` + pub fn block_time_from_external(mut self) -> Self { + self.block_time_from_external = true; + self + } + /// Build the [`Chain`] with the configured options. /// /// # Example @@ -204,13 +321,38 @@ impl ChainBuilder { // Create the chain with default parameters. let mut chain = Chain::new(); - // Replace the default exchange rate parameters if any are provided. + // Setup the external node connection if provided. This also forwards and sets + // the external query block. + if let Some(endpoint) = self.external_node_endpoint { + chain.setup_external_node_connection(endpoint, self.external_query_block)?; + } + + // Check for conflicting exchange rate configurations. + if self.micro_ccd_per_euro.is_some() && self.micro_ccd_per_euro_from_external { + return Err(ChainBuilderError::ConflictingMicroCCDPerEuro); + } + if self.euro_per_energy.is_some() && self.euro_per_energy_from_external { + return Err(ChainBuilderError::ConflictingEuroPerEnergy); + } + + // Set the exchange rates via an external query. + if self.micro_ccd_per_euro_from_external || self.euro_per_energy_from_external { + let exchange_rates = chain.get_exchange_rates_via_external_node()?; + if self.micro_ccd_per_euro_from_external { + chain.parameters.micro_ccd_per_euro = exchange_rates.micro_ccd_per_euro; + } + if self.euro_per_energy_from_external { + chain.parameters.euro_per_energy = exchange_rates.euro_per_energy; + } + } + + // Set the exchange rates directly. + if let Some(micro_ccd_per_euro) = self.micro_ccd_per_euro { + chain.parameters.micro_ccd_per_euro = micro_ccd_per_euro; + } if let Some(euro_per_energy) = self.euro_per_energy { chain.parameters.euro_per_energy = euro_per_energy; } - if let Some(micro_ccd_per_euro) = self.micro_ccd_per_euro { - chain.parameters.micro_ccd_per_euro = micro_ccd_per_euro - }; // Check the exchange rates and return early if they are invalid. check_exchange_rates( @@ -218,16 +360,22 @@ impl ChainBuilder { chain.parameters.micro_ccd_per_euro, )?; + match (self.block_time, self.block_time_from_external) { + (Some(_), true) => return Err(ChainBuilderError::ConflictingBlockTime), + (Some(block_time), _) => { + chain.parameters.block_time = block_time; + } + (_, true) => { + chain.set_block_time_via_external_node()?; + } + _ => (), + } + // Replace the default block time if provided. if let Some(block_time) = self.block_time { chain.parameters.block_time = block_time; } - // Setup the external node connection if provided. - if let Some(endpoint) = self.external_node_endpoint { - chain.setup_external_node_connection(&endpoint)?; - } - Ok(chain) } } @@ -1223,160 +1371,95 @@ impl Chain { Ok(()) } - /// Set the exchange rates for Euro, CCD, and Energy by querying the - /// external node. - /// - /// Use the `block` parameter to specify the block used when querying the - /// block time. For consistency between queries, the first block hash - /// used will be "sticky", which means that it is saved and used for - /// subsequent queries, unless overridden by specifying a `block`. If - /// `block == None` for the first query, it will use the *last final - /// block* instead. In either case, the block hash will be saved and - /// used for future queries. - /// To see the "sticky block", use [`sticky_block`][Chain::sticky_block]. - /// - /// The external node must be setup prior to this call via the method - /// [`Chain::setup_external_node_connection`], otherwise an error is - /// returned. - /// - /// # Example - /// - /// ```no_run - /// # use concordium_smart_contract_testing::*; - /// use std::str::FromStr; - /// - /// let mut chain = Chain::builder() - /// .external_node_connection("http://node.testnet.concordium.com:20000") - /// .build() - /// .unwrap(); - /// - /// // Use the last final block and set the "sticky block" to it. - /// chain.set_exchange_rates_via_external_node(None).unwrap(); - /// - /// // Use a specific block instead of the sticky block. - /// // This does not change the sticky block. - /// chain - /// .set_exchange_rates_via_external_node(Some( - /// BlockHash::from_str("95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0") - /// .unwrap(), - /// )) - /// .unwrap(); - /// ``` - pub fn set_exchange_rates_via_external_node( - &mut self, - block: Option, - ) -> Result<(), ExternalNodeError> { - let connection = self.external_node_connection()?; + /// Get the microCCD per euro and euro per energy exchange rates by querying + /// an external node using the external query block. + fn get_exchange_rates_via_external_node(&mut self) -> Result { + let connection = self.external_node_connection_mut()?; // Get the values from the external node. - let (euro_per_energy, micro_ccd_per_euro) = - connection.with_client(block, |block_identifier, mut client| async move { - let response = client.get_block_chain_parameters(block_identifier).await?; - let (euro_per_energy, micro_ccd_per_euro) = match response.response { - sdk::v2::ChainParameters::V0(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - sdk::v2::ChainParameters::V1(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - sdk::v2::ChainParameters::V2(p) => (p.euro_per_energy, p.micro_ccd_per_euro), - }; - Ok((response.block_hash, (euro_per_energy, micro_ccd_per_euro))) - })?; - - // We assume that the queryied values are valid. - self.parameters.micro_ccd_per_euro = micro_ccd_per_euro; - self.parameters.euro_per_energy = euro_per_energy; - - Ok(()) + connection.with_client(None, |block_identifier, mut client| async move { + let (euro_per_energy, micro_ccd_per_euro) = match client + .get_block_chain_parameters(block_identifier) + .await? + .response + { + sdk::v2::ChainParameters::V0(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V1(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + sdk::v2::ChainParameters::V2(p) => (p.euro_per_energy, p.micro_ccd_per_euro), + }; + Ok(ExchangeRates { + euro_per_energy, + micro_ccd_per_euro, + }) + }) } - /// Set the block time on the [`Chain`]. + /// Tick the block time on the [`Chain`] by a [`Duration`]. /// - /// You can also set the block time via the builder pattern, see - /// [`ChainBuilder::block_time`]. + /// Returns an error if ticking causes the block time to overflow. /// /// # Example /// /// ``` /// # use concordium_smart_contract_testing::*; /// + /// // Block time defaults to 0. /// let mut chain = Chain::new(); - /// chain.set_block_time(Timestamp::from_timestamp_millis(123)); + /// + /// // Increase block time by 123 milliseconds. + /// chain.tick_block_time(Duration::from_millis(123)).unwrap(); + /// + /// // Block time has now increased. + /// assert_eq!(chain.block_time(), Timestamp::from_timestamp_millis(123)); /// ``` - pub fn set_block_time(&mut self, block_time: Timestamp) { - self.parameters.block_time = block_time; + pub fn tick_block_time(&mut self, duration: Duration) -> Result<(), BlockTimeOverflow> { + self.parameters.block_time = self + .parameters + .block_time + .checked_add(duration) + .ok_or(BlockTimeOverflow)?; + Ok(()) } /// Set the block time by querying the external node. /// - /// Use the `block` parameter to specify the block used when querying the - /// block time. For consistency between queries, the first block hash - /// used will be "sticky", which means that it is saved and used for - /// subsequent queries, unless overridden by specifying a `block`. If - /// `block == None` for the first query, it will use the *last final - /// block* instead. In either case, the block hash will be saved and - /// used for future queries. - /// To see the "sticky block", use [`sticky_block`][Chain::sticky_block]. + /// The default query block is always used. /// /// The external node must be setup prior to this call via the method /// [`Chain::setup_external_node_connection`], otherwise an error is /// returned. - /// - /// If the queried block time is prior to January 1, 1970 0:00:00 UTC (aka - /// “UNIX timestamp”), a block time of `0` will be used instead. - /// - /// # Example - /// - /// ```no_run - /// # use concordium_smart_contract_testing::*; - /// use std::str::FromStr; - /// - /// let mut chain = Chain::builder() - /// .external_node_connection("http://node.testnet.concordium.com:20000") - /// .build() - /// .unwrap(); - /// - /// // Use the last final block and set the "sticky block" to it. - /// chain.set_block_time_via_external_node(None).unwrap(); - /// - /// // Use a specific block instead of the sticky block. - /// // This does not change the sticky block. - /// chain - /// .set_block_time_via_external_node(Some( - /// BlockHash::from_str("95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0") - /// .unwrap(), - /// )) - /// .unwrap(); - /// ``` - pub fn set_block_time_via_external_node( - &mut self, - block: Option, - ) -> Result<(), ExternalNodeError> { - let connection = self.external_node_connection()?; - - // Get the timestamp in milliseconds *before or after* the unix epoch from the - // external node. - let timestamp_before_or_after = - connection.with_client(block, |block_identifier, mut client| async move { - let response = client.get_block_info(block_identifier).await?; - let block_time = response.response.block_slot_time.timestamp_millis(); - Ok((response.block_hash, block_time)) + fn set_block_time_via_external_node(&mut self) -> Result<(), ExternalNodeError> { + let connection = self.external_node_connection_mut()?; + + // Get the timestamp in milliseconds. + let timestamp = + connection.with_client(None, |block_identifier, mut client| async move { + Ok(client + .get_block_info(block_identifier) + .await? + .response + .block_slot_time + .timestamp_millis() as u64) // The node never returns + // timestamps < 0, so it is safe + // to cast it to `u64`. })?; - - // Our `Timestamp` only supports times *after* the unix epoch, so we use `0` if - // the queried timestamp is negative. - let timestamp_after: u64 = i64::max(timestamp_before_or_after, 0) as u64; - self.parameters.block_time = Timestamp::from_timestamp_millis(timestamp_after); + // Update the block time. + self.parameters.block_time = Timestamp::from_timestamp_millis(timestamp); Ok(()) } /// Set up a connection to an external Concordium node. /// - /// The connection can be used for getting the current exchange rates - /// between CCD, Euro and Energy. + /// This method also queries the block info for one of two reasons: + /// 1) If `query_block` is provided, its existence is checked. + /// 2) Otherwise, the last final block is queried to get its blockhash which + /// will be saved in [`ExternalNodeConnection`]. fn setup_external_node_connection( &mut self, - endpoint: &str, + endpoint: Endpoint, + query_block: Option, ) -> Result<(), SetupExternalNodeError> { - let endpoint: sdk::v2::Endpoint = FromStr::from_str(endpoint)?; // Create the Tokio runtime. This should never fail, unless nested runtimes are // created. let runtime = runtime::Builder::new_multi_thread() @@ -1384,7 +1467,6 @@ impl Chain { .enable_time() // Enable I/O, so networking and other types of calls are possible. .enable_io() - .thread_keep_alive(Duration::from_secs(2)) .build() .expect("Internal error: Could not create Tokio runtime."); @@ -1395,27 +1477,64 @@ impl Chain { Ok::(client) }; + // A future for checking the block. + let get_block_info = |mut client: sdk::v2::Client| async move { + let block_identifier = if let Some(query_block) = query_block { + sdk::v2::BlockIdentifier::Given(query_block) + } else { + sdk::v2::BlockIdentifier::LastFinal + }; + + let block_hash = match client.get_block_info(block_identifier).await { + Ok(res) => res.block_hash, + Err(sdk::v2::QueryError::NotFound) => { + return Err(SetupExternalNodeError::QueryBlockDoesNotExist { + // It should never be possible to get `NotFound` when querying `LastFinal`, + // and so, the `query_block` must be `Some`. + query_block: query_block.expect( + "Internal error: Got `QueryError::NotFound` for when querying last \ + final block.", + ), + }); + } + Err(sdk::v2::QueryError::RPCError(error)) => { + return Err(SetupExternalNodeError::CannotCheckQueryBlockExistence { error }) + } + }; + Ok(block_hash) + }; + // Get the client synchronously by blocking until the async returns. - let client = runtime.block_on(async { - timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_client) + let (client, checked_query_block) = runtime.block_on(async { + let client = timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_client) .await - .map_err(|_| SetupExternalNodeError::Timeout)? + .map_err(|_| SetupExternalNodeError::Timeout)??; + let checked_query_block = timeout( + EXTERNAL_NODE_TIMEOUT_DURATION, + get_block_info(client.clone()), + ) + .await + .map_err(|_| SetupExternalNodeError::Timeout)??; + Ok::<_, SetupExternalNodeError>((client, checked_query_block)) })?; // Set or replace the node connection. self.external_node_connection = Some(ExternalNodeConnection { client, runtime, - sticky_block: None, + query_block: checked_query_block, }); Ok(()) } - /// Try to get the [`ExternalNodeConnection`] or return an error. + /// Try to get a mutable reference to [`ExternalNodeConnection`] or return + /// an error. /// - /// The connection is only available, if the [`Chain`] has been created with - fn external_node_connection( + /// The connection is only available, if the [`Chain`] has been set up with + /// an external node connection via + /// [`ChainBuilder::external_node_connection`] in the [`ChainBuilder`]. + fn external_node_connection_mut( &mut self, ) -> Result<&mut ExternalNodeConnection, ExternalNodeNotConfigured> { match &mut self.external_node_connection { @@ -1424,6 +1543,21 @@ impl Chain { } } + /// Try to get an immutable reference to [`ExternalNodeConnection`] or + /// return an error. + /// + /// The connection is only available, if the [`Chain`] has been set up with + /// an external node connection via + /// [`ChainBuilder::external_node_connection`] in the [`ChainBuilder`]. + fn external_node_connection( + &self, + ) -> Result<&ExternalNodeConnection, ExternalNodeNotConfigured> { + match &self.external_node_connection { + None => Err(ExternalNodeNotConfigured), + Some(data) => Ok(data), + } + } + /// Return the current microCCD per euro exchange rate. pub fn micro_ccd_per_euro(&self) -> ExchangeRate { self.parameters.micro_ccd_per_euro } @@ -1433,27 +1567,23 @@ impl Chain { /// Return the current block time. pub fn block_time(&self) -> Timestamp { self.parameters.block_time } - /// Return the "sticky block", i.e. the block that is saved and will be used - /// by default for future queries. + /// Return the block used for external queries by default. + /// + /// The block can be set with [`ChainBuilder::external_query_block`] when + /// building the [`Chain`]. /// - /// See [`set_block_time_via_external_node`][Chain::set_block_time_via_external_node] for a more detailed explanation. - pub fn sticky_block(&self) -> Option { - self.external_node_connection - .as_ref() - .and_then(|conn| conn.sticky_block) + /// This method returns an error if the external node has not been + /// configured. + pub fn external_query_block(&self) -> Result { + self.external_node_connection().map(|conn| conn.query_block) } } impl ExternalNodeConnection { /// Execute an async task with the [`sdk::v2::Client`]. /// - /// The task will be performed using one of the following three blocks: - /// 1. If a `block` hash is provided, it will be used. - /// 2. Otherwise, if `self.sticky_block` is `Some`, it will be used. - /// 3. Lastly, if neither are present, the last final block is used. - /// - /// If `self.sticky_block` is `None`, then it will be set to the block used - /// in cases 1 and 3. + /// If a block is provided, it will be used for the query. Otherwise, it + /// will use the default query block. /// /// If the task takes longer than [`EXTERNAL_NODE_TIMEOUT_DURATION`] then /// the connection times out and an [`Err(ExternalNodeError::Timeout)`] is @@ -1465,34 +1595,22 @@ impl ExternalNodeConnection { ) -> Result where F: FnOnce(sdk::v2::BlockIdentifier, sdk::v2::Client) -> Fut, - Fut: Future>, { - // Get the block identifier in one of the three ways. - let block_identifier = match (block, self.sticky_block) { - // Case 1 - (Some(block), _) => sdk::v2::BlockIdentifier::Given(block), - // Case 2 - (_, Some(sticky_block)) => sdk::v2::BlockIdentifier::Given(sticky_block), - // Case 3 - _ => sdk::v2::BlockIdentifier::LastFinal, + Fut: Future>, { + // Get the block identifier, either using the provided block or the default + // query block. + let block_identifier = if let Some(block) = block { + sdk::v2::BlockIdentifier::Given(block) + } else { + sdk::v2::BlockIdentifier::Given(self.query_block) }; // Clone the client so it can be moved to the async block. let client = self.client.clone(); // Run the future and timeout if it takes too long. - let res = self.runtime.block_on(async move { + self.runtime.block_on(async move { timeout(EXTERNAL_NODE_TIMEOUT_DURATION, f(block_identifier, client)) .await .map_err(|_| ExternalNodeError::Timeout)? - }); - // Extract the return value and maybe set the stick block. - match res { - Ok((block_hash, return_value)) => { - if self.sticky_block.is_none() { - self.sticky_block = Some(block_hash) - } - Ok(return_value) - } - Err(e) => Err(e), - } + }) } } @@ -1734,14 +1852,6 @@ pub(crate) fn contract_events_from_logs(logs: v0::Logs) -> Vec { logs.logs.into_iter().map(ContractEvent::from).collect() } -impl From for ExternalNodeError { - fn from(_: concordium_rust_sdk::endpoints::QueryError) -> Self { Self::QueryError } -} - -impl From for SetupExternalNodeError { - fn from(_: concordium_rust_sdk::endpoints::Error) -> Self { Self::CannotConnect } -} - impl From for ExternalNodeError { fn from(_: ExternalNodeNotConfigured) -> Self { Self::NotConfigured } } @@ -1855,13 +1965,13 @@ mod tests { fn test_chain_builder_with_invalid_exchange_rates() { let micro_ccd_per_euro = ExchangeRate::new_unchecked(1000000, 1); let euro_per_energy = ExchangeRate::new_unchecked(100000000, 1); - let chain = Chain::builder() + let error = Chain::builder() .micro_ccd_per_euro(micro_ccd_per_euro) .euro_per_energy(euro_per_energy) .build() .unwrap_err(); - assert_eq!(chain, ChainBuilderError::ExchangeRateError); + assert!(matches!(error, ChainBuilderError::ExchangeRateError)); } } @@ -1872,124 +1982,59 @@ mod tests { mod io_tests { use super::*; - /// Test that building a chain using all the parameters available, - /// *including* the I/O parameter of external node connection. + /// Test that building a chain using the external node parameters works. #[test] fn test_chain_builder_with_valid_parameters_and_external_node() { - let micro_ccd_per_euro = ExchangeRate::new_unchecked(123, 1); - let euro_per_energy = ExchangeRate::new_unchecked(1, 1234); - let block_time = Timestamp::from_timestamp_millis(12345); let chain = Chain::builder() - .micro_ccd_per_euro(micro_ccd_per_euro) - .euro_per_energy(euro_per_energy) - .block_time(block_time) - .external_node_connection("http://node.testnet.concordium.com:20000") - .build() - .unwrap(); - - assert_eq!(chain.micro_ccd_per_euro(), micro_ccd_per_euro); - assert_eq!(chain.euro_per_energy(), euro_per_energy); - assert_eq!(chain.block_time(), block_time); - } - - /// Test that setting the exchange rates via an external node succeeds. - /// - /// Instead of relying on a the rate from a specific block, the test just - /// checks that the exchange differs from the default afterwards. - #[test] - fn test_setting_exchange_rates_via_external_node() { - let mut chain = Chain::builder() - .external_node_connection("http://node.testnet.concordium.com:20000") - .build() - .unwrap(); - - let default_exchange_rate = chain.micro_ccd_per_euro(); - - chain.set_exchange_rates_via_external_node(None).unwrap(); - assert_ne!(chain.micro_ccd_per_euro(), default_exchange_rate); - } - - /// Test that the sticky block is kept when using - #[test] - fn test_sticky_block_from_last_final() { - let mut chain = Chain::builder() - .external_node_connection("http://node.testnet.concordium.com:20000") - .build() - .unwrap(); - // Don't provide a block, so last final is used. - chain.set_block_time_via_external_node(None).unwrap(); - let block_time_1 = chain.block_time(); - let sticky_block_1 = chain.sticky_block().unwrap(); - - // Set block time using a specific block that exists. - chain - .set_block_time_via_external_node(Some( - BlockHash::from_str( - "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", - ) - .unwrap(), + .micro_ccd_per_euro_from_external() + .euro_per_energy_from_external() + .external_query_block( + "45c53a19cd782a8de981941feb5e0f875cefaba8d2cda958e76f471a4710a797" // A block from testnet. + .parse() + .unwrap(), + ) + .external_node_connection(Endpoint::from_static( + "http://node.testnet.concordium.com:20000", )) - .unwrap(); - // Check that this does not mutate the sticky block. - let sticky_block_2 = chain.sticky_block().unwrap(); - // Get the changed block time. - let block_time_2 = chain.block_time(); - assert_eq!(sticky_block_1, sticky_block_2); - // The block time in the specified block should be lower than last final. - assert!(block_time_2 < block_time_1); - } - - #[test] - fn test_sticky_block_from_specific() { - let mut chain = Chain::builder() - .external_node_connection("http://node.testnet.concordium.com:20000") + .block_time_from_external() .build() .unwrap(); - chain - .set_block_time_via_external_node(Some( - BlockHash::from_str( - "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", - ) - .unwrap(), - )) - .unwrap(); - let sticky_block_1 = chain.sticky_block().unwrap(); - let block_time_1 = chain.block_time(); - // This does not mutate the sticky block already set. - chain.set_block_time_via_external_node(None).unwrap(); - let sticky_block_2 = chain.sticky_block().unwrap(); - let block_time_2 = chain.block_time(); - // Check that the sticky block hasn't changed. - assert_eq!(sticky_block_1, sticky_block_2); - // Check that the block matches the one specified initially. - assert_eq!(sticky_block_1, - BlockHash::from_str( - "95ff82f26892a2327c3e7ac582224a54d75c367341fbff209bce552d81349eb0", - ) - .unwrap()); - // The block times should be identical since both the sticky block is used for - // the second call to the external node due to `None` being passed in. - assert_eq!(block_time_2, block_time_1); + + // These values are queried manually from the node. + assert_eq!( + chain.micro_ccd_per_euro(), + ExchangeRate::new_unchecked(10338559485590134784, 79218205097) + ); + assert_eq!( + chain.euro_per_energy(), + ExchangeRate::new_unchecked(1, 50000) + ); + assert_eq!( + chain.block_time(), + Timestamp::from_timestamp_millis(1687865059500) + ); } - /// Test that the correct error is returned when an unknown block is given. + /// Test that the correct error is returned when an unknown query block is + /// given. /// /// The block used is one from mainnet, which is extremely unlikely to also /// appear on testnet. #[test] fn test_block_time_from_unknown_block() { - let mut chain = Chain::builder() - .external_node_connection("http://node.testnet.concordium.com:20000") - .build() - .unwrap(); - let err = chain - .set_block_time_via_external_node(Some( - BlockHash::from_str( - "4f38c7e63645c59e9bf32f7ca837a029810b21c439f7492c3cebe229a2e3ea07", // A block from mainnet. - ) - .unwrap(), + let err = Chain::builder() + .external_node_connection(Endpoint::from_static( + "http://node.testnet.concordium.com:20000", )) + .external_query_block( + "4f38c7e63645c59e9bf32f7ca837a029810b21c439f7492c3cebe229a2e3ea07" + .parse() + .unwrap(), // A block from mainnet. + ) + .build() .unwrap_err(); - assert_eq!(err, ExternalNodeError::QueryError); + assert!(matches!(err, ChainBuilderError::SetupExternalNodeError { + error: SetupExternalNodeError::CannotCheckQueryBlockExistence { .. }, + })); } } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 83e0e4a9..cbf31036 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -97,11 +97,13 @@ pub use concordium_base::{ base::Energy, contracts_common::{ from_bytes, to_bytes, AccountAddress, Address, Amount, ContractAddress, ContractName, - EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, - OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, Timestamp, + Duration, EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, + OwnedEntrypointName, OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, + Timestamp, }, hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, transactions::{InitContractPayload, UpdateContractPayload}, }; +pub use concordium_rust_sdk::v2::Endpoint; pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 3830cdbe..70d651bf 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -7,6 +7,7 @@ use concordium_base::{ hashes::BlockHash, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, }; +use concordium_rust_sdk as sdk; use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; use concordium_wasm::artifact; use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; @@ -42,14 +43,11 @@ pub(crate) struct ChainParameters { pub(crate) struct ExternalNodeConnection { /// An instantiated v2 Client from the Rust SDK. Used for communicating with /// a node. - pub(crate) client: concordium_rust_sdk::v2::Client, + pub(crate) client: concordium_rust_sdk::v2::Client, /// A Tokio runtime used to execute the async methods of the `client`. - pub(crate) runtime: tokio::runtime::Runtime, - /// The sticky block hash to use for queries without specific blocks. - /// - /// The hash is "sticky" in the sense that it is set on the first query and - /// will be used for subsequent queries that do *not* specify block hash. - pub(crate) sticky_block: Option, + pub(crate) runtime: tokio::runtime::Runtime, + /// The block used for queries. + pub(crate) query_block: BlockHash, } /// Represents the blockchain and supports a number of operations, including @@ -73,16 +71,27 @@ pub struct Chain { } /// A builder for the [`Chain`]. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ChainBuilder { /// The configured endpoint for an external node connection. - pub(crate) external_node_endpoint: Option, + pub(crate) external_node_endpoint: Option, + /// The block hash to be used for external queries. If this is not set, then + /// the last final block hash is used instead. + pub(crate) external_query_block: Option, /// The configured exchange rate between microCCD and euro. - pub(crate) micro_ccd_per_euro: Option, + pub(crate) micro_ccd_per_euro: Option, + /// Whether the microCCD/euro exchange rate should be set via the external + /// node. + pub(crate) micro_ccd_per_euro_from_external: bool, /// The configured exchange rate between euro and energy. - pub(crate) euro_per_energy: Option, + pub(crate) euro_per_energy: Option, + /// Whether the euro/energy exchange rate should be set via the external + /// node. + pub(crate) euro_per_energy_from_external: bool, /// The configured block time. - pub(crate) block_time: Option, + pub(crate) block_time: Option, + /// Whether the block time should be set via the external node. + pub(crate) block_time_from_external: bool, } /// A smart contract instance. @@ -634,28 +643,45 @@ pub struct ExchangeRateError; pub struct ZeroKeysError; /// Errors that occur while setting up the connection to an external node. -#[derive(Debug, thiserror::Error, PartialEq, Eq)] +#[derive(Debug, thiserror::Error)] pub enum SetupExternalNodeError { /// It was not possible to connect to a node on the provided endpoint. - #[error("Could not connect to the provided endpoint.")] - CannotConnect, - /// The endpoint provided was malformed. - #[error("The provided endpoint is malformed: '{endpoint}'.")] - MalformedEndpoint { endpoint: String }, + #[error("Could not connect to the provided endpoint due to: {error}")] + CannotConnect { + /// The inner error. + #[from] + error: sdk::endpoints::Error, + }, /// Request timed out. #[error("The request timed out.")] Timeout, + /// The specified external query block does not exist. + #[error("The specified external query block {query_block} does not exist.")] + QueryBlockDoesNotExist { query_block: BlockHash }, + /// Could not check the existence of the specified query block or the last + /// final block. + #[error( + "Could not check the existence of the specified query block or the last final block due \ + to: {error}" + )] + CannotCheckQueryBlockExistence { + /// The inner error. + error: sdk::v2::RPCError, + }, } /// Errors that occur while trying to communicate with an external node. -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Debug, Error)] pub enum ExternalNodeError { /// An external node has not been configured. #[error("An external node has not been configured.")] NotConfigured, /// The query could not be performed. - #[error("Could not perform the query.")] - QueryError, + #[error("Could not perform the query: {error}")] + QueryError { + #[from] + error: sdk::endpoints::QueryError, + }, /// Request timed out. #[error("The request timed out.")] Timeout, @@ -667,7 +693,7 @@ pub enum ExternalNodeError { #[error("An external node has not been configured.")] pub struct ExternalNodeNotConfigured; -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Debug, Error)] pub enum ChainBuilderError { /// The provided exchange rates are not valid. /// Meaning that they do not correspond to one energy costing less than @@ -681,4 +707,50 @@ pub enum ChainBuilderError { #[from] error: SetupExternalNodeError, }, -} + /// Error occurred while using the external node for querying chain + /// parameters such as the block time or exchange rates. + #[error("Error occurred while using the external node for querying chain parameters: {error}")] + ExternalNodeError { + #[from] + error: ExternalNodeError, + }, + /// Could not configure the block time because both the + /// [`ChainBuilder::block_time`] and + /// [`ChainBuilder::block_time_from_external`] was provided, which is not + /// allowed. + #[error( + "Conflicting block time configuration: `block_time` and `block_time_from_external` cannot \ + both be used." + )] + ConflictingBlockTime, + /// Could not configure the microCCD/euro exchange rate because both the + /// [`ChainBuilder::micro_ccd_per_euro`] and + /// [`ChainBuilder::micro_ccd_per_euro_from_external`] was provided, which + /// is not allowed. + #[error( + "Conflicting microCCD per euro configuration: `micro_ccd_per_euro` and \ + `micro_ccd_per_euro_from_external` cannot both be used." + )] + ConflictingMicroCCDPerEuro, + /// Could not configure the euro/energy exchange rate because both the + /// [`ChainBuilder::euro_per_energy`] and + /// [`ChainBuilder::euro_per_energy_from_external`] was provided, which is + /// not allowed. + #[error( + "Conflicting euro per energy configuration: `euro_per_energy` and \ + `euro_per_energy_from_external` cannot both be used." + )] + ConflictingEuroPerEnergy, + /// A configuration option that requires an external node connection was + /// used without [`ChainBuilder::external_node_connection`]. + #[error( + "A configuration method that requires an external node connection was called without \ + `ChainBuilder::external_node_connection`." + )] + MissingExternalConnection, +} + +/// The block time overflowed during a call to `Chain::tick_block_time`. +#[derive(Debug, Error, PartialEq, Eq)] +#[error("The block time overflowed during a call to `Chain::tick_block_time`.")] +pub struct BlockTimeOverflow; From 606b73db4940f97ffd96fd97c91cd607a10913ac Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 30 Jun 2023 10:16:27 +0200 Subject: [PATCH 175/208] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aleš Bizjak --- contract-testing/src/types.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 70d651bf..6134c144 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -716,7 +716,7 @@ pub enum ChainBuilderError { }, /// Could not configure the block time because both the /// [`ChainBuilder::block_time`] and - /// [`ChainBuilder::block_time_from_external`] was provided, which is not + /// [`ChainBuilder::block_time_from_external`] were provided, which is not /// allowed. #[error( "Conflicting block time configuration: `block_time` and `block_time_from_external` cannot \ @@ -725,7 +725,7 @@ pub enum ChainBuilderError { ConflictingBlockTime, /// Could not configure the microCCD/euro exchange rate because both the /// [`ChainBuilder::micro_ccd_per_euro`] and - /// [`ChainBuilder::micro_ccd_per_euro_from_external`] was provided, which + /// [`ChainBuilder::micro_ccd_per_euro_from_external`] were provided, which /// is not allowed. #[error( "Conflicting microCCD per euro configuration: `micro_ccd_per_euro` and \ @@ -734,7 +734,7 @@ pub enum ChainBuilderError { ConflictingMicroCCDPerEuro, /// Could not configure the euro/energy exchange rate because both the /// [`ChainBuilder::euro_per_energy`] and - /// [`ChainBuilder::euro_per_energy_from_external`] was provided, which is + /// [`ChainBuilder::euro_per_energy_from_external`] were provided, which is /// not allowed. #[error( "Conflicting euro per energy configuration: `euro_per_energy` and \ From 6d32cb53697a91df81c88fcab8bdedb8c9d6a91a Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 30 Jun 2023 10:22:11 +0200 Subject: [PATCH 176/208] Update the Rust-SDK dependency to v3.0 --- contract-testing/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index dfacb9fa..4232236b 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -16,7 +16,7 @@ exclude = ["tests"] # Do not publish tests. concordium_base = {version = "2.0", path = "../concordium-base/rust-src/concordium_base"} concordium-smart-contract-engine = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} concordium-wasm = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-transform"} -concordium-rust-sdk = "2.4" +concordium-rust-sdk = "3.0" tokio = { version = "1.28", features = ["rt-multi-thread", "time"] } sha2 = "0.10" anyhow = "1" From 6d5932fd1708dce83feed1a2c3305766e50d9a34 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 30 Jun 2023 12:58:43 +0200 Subject: [PATCH 177/208] Make timeout errors more precise and set shorter limit for connecting --- contract-testing/src/impls.rs | 25 +++++++++++++------------ contract-testing/src/types.rs | 15 +++++++++------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 8b6957bd..23e6d596 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -26,8 +26,11 @@ use num_integer::Integer; use std::{collections::BTreeMap, future::Future, path::Path, sync::Arc}; use tokio::{runtime, time::timeout}; -/// The duration set for timeouts when communicating with an external node. -const EXTERNAL_NODE_TIMEOUT_DURATION: tokio::time::Duration = tokio::time::Duration::from_secs(10); +/// The timeout duration set for queries with an external node. +const EXTERNAL_NODE_QUERY_TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(10); + +/// The timeout duration set for connecting to an external node. +const EXTERNAL_NODE_CONNECT_TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(3); impl Default for Chain { fn default() -> Self { Self::new() } @@ -1506,15 +1509,13 @@ impl Chain { // Get the client synchronously by blocking until the async returns. let (client, checked_query_block) = runtime.block_on(async { - let client = timeout(EXTERNAL_NODE_TIMEOUT_DURATION, get_client) + let client = timeout(EXTERNAL_NODE_CONNECT_TIMEOUT, get_client) .await - .map_err(|_| SetupExternalNodeError::Timeout)??; - let checked_query_block = timeout( - EXTERNAL_NODE_TIMEOUT_DURATION, - get_block_info(client.clone()), - ) - .await - .map_err(|_| SetupExternalNodeError::Timeout)??; + .map_err(|_| SetupExternalNodeError::ConnectTimeout)??; + let checked_query_block = + timeout(EXTERNAL_NODE_QUERY_TIMEOUT, get_block_info(client.clone())) + .await + .map_err(|_| SetupExternalNodeError::CheckQueryBlockTimeout)??; Ok::<_, SetupExternalNodeError>((client, checked_query_block)) })?; @@ -1607,9 +1608,9 @@ impl ExternalNodeConnection { let client = self.client.clone(); // Run the future and timeout if it takes too long. self.runtime.block_on(async move { - timeout(EXTERNAL_NODE_TIMEOUT_DURATION, f(block_identifier, client)) + timeout(EXTERNAL_NODE_QUERY_TIMEOUT, f(block_identifier, client)) .await - .map_err(|_| ExternalNodeError::Timeout)? + .map_err(|_| ExternalNodeError::QueryTimeout)? }) } } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 6134c144..f13eb695 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -652,9 +652,12 @@ pub enum SetupExternalNodeError { #[from] error: sdk::endpoints::Error, }, - /// Request timed out. - #[error("The request timed out.")] - Timeout, + /// The attempt to connect to an external node timed out. + #[error("The attempt to connect to an external node timed out.")] + ConnectTimeout, + /// The query to check the `external_query_block` timed out. + #[error("The query to check the `external_query_block` timed out.")] + CheckQueryBlockTimeout, /// The specified external query block does not exist. #[error("The specified external query block {query_block} does not exist.")] QueryBlockDoesNotExist { query_block: BlockHash }, @@ -682,9 +685,9 @@ pub enum ExternalNodeError { #[from] error: sdk::endpoints::QueryError, }, - /// Request timed out. - #[error("The request timed out.")] - Timeout, + /// The query timed out. + #[error("The query timed out.")] + QueryTimeout, } /// The error returned when an external node has not been configured prior to From f886bfa4d6b4c2ac36b6cbacfd1734f6dfefea83 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 30 Jun 2023 15:54:55 +0200 Subject: [PATCH 178/208] Support invoking external contracts --- contract-testing/CHANGELOG.md | 4 +- contract-testing/src/impls.rs | 233 ++++++++++++++++++++++++++++++++-- contract-testing/src/lib.rs | 2 +- contract-testing/src/types.rs | 124 +++++++++++++++++- 4 files changed, 343 insertions(+), 20 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 82b2f444..7b149382 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -5,7 +5,7 @@ - Add functionality for setting the exchange rates and block time of the chain based on queries from an external node. - Configured via a builder pattern, see `Chain::builder`. - Add methods to `Chain`: - - `external_query_block` to get the default block used for external queries + - `external_query_block` to get the default block used for external - `block_time` to get the block time - `tick_block_time` to increase the block time by a `Duration` - Add the following types by re-exporting them from internal crates: @@ -13,6 +13,8 @@ - `Timestamp` which `SlotTime` is an alias of. - `Duration` - `Endpoint` +- Add methods to the `Chain` for adding external accounts and contracts and for invoking contracts on an external node. + - See the `Chain` method `contract_invoke_external` for more details. ## 2.0.0 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 23e6d596..89ce0d90 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -23,7 +23,13 @@ use concordium_smart_contract_engine::{ }; use num_bigint::BigUint; use num_integer::Integer; -use std::{collections::BTreeMap, future::Future, path::Path, sync::Arc}; +use sdk::types::smart_contracts::InvokeContractResult; +use std::{ + collections::{BTreeMap, BTreeSet}, + future::Future, + path::Path, + sync::Arc, +}; use tokio::{runtime, time::timeout}; /// The timeout duration set for queries with an external node. @@ -970,17 +976,17 @@ impl Chain { /// If successful, all changes will be saved. /// /// **Parameters:** + /// - `signer`: a [`Signer`] with a number of keys. The number of keys + /// affects the cost of the transaction. /// - `invoker`: the account paying for the transaction. /// - `sender`: the sender of the message, can be an account or contract. /// For top-level invocations, such as those caused by sending a contract /// update transaction on the chain, the `sender` is always the /// `invoker`. Here we provide extra freedom for testing invocations /// where the sender differs. - /// - `contract_address`: the contract to update. - /// - `entrypoint`: the entrypoint to call. - /// - `parameter`: the contract parameter. - /// - `amount`: the amount sent to the contract. /// - `energy_reserved`: the maximum energy that can be used in the update. + /// - `payload`: The data detailing which contract contract and receive + /// method to call etc. pub fn contract_update( &mut self, signer: Signer, @@ -1102,13 +1108,12 @@ impl Chain { /// functions. /// /// **Parameters:** - /// - `invoker`: the account paying for the transaction. - /// - `sender`: the sender of the transaction, can also be a contract. - /// - `contract_address`: the contract to update. - /// - `entrypoint`: the entrypoint to call. - /// - `parameter`: the contract parameter. - /// - `amount`: the amount sent to the contract. + /// - `invoker`: the account used as invoker. Since this isn't a + /// transaction, it won't be charged. + /// - `sender`: the sender, can also be a contract. /// - `energy_reserved`: the maximum energy that can be used in the update. + /// - `payload`: The data detailing which contract contract and receive + /// method to call etc. pub fn contract_invoke( &self, invoker: AccountAddress, @@ -1186,6 +1191,107 @@ impl Chain { } } + /// Invoke an external contract entrypoint. + /// + /// Similar to [`Chain::contract_invoke`](Self::contract_invoke) except that + /// it invokes a contract on the external node. + /// + /// **Parameters:** + /// - `invoker`: the account used as invoker. + /// - The account must exist on the connected node. + /// - `sender`: the sender, can also be a contract. + /// - The sender exist on the connected node. + /// - `energy_reserved`: the maximum energy that can be used in the update. + /// - `payload`: The data detailing which contract contract and receive + /// method to call etc. + /// - `block`: The block in which the invocation will be simulated, as if + /// it was at the end of the block. If `None` is provided, the + /// `external_query_block` is used instead. + /// + /// # Example: + /// + /// ```no_run + /// # use concordium_smart_contract_testing::*; + /// let mut chain = Chain::builder() + /// .external_node_connection(Endpoint::from_static("http://node.testnet.concordium.com:20000")) + /// .build() + /// .unwrap(); + /// + /// // Set up an external contract. + /// let external_contract = + /// chain.add_external_contract(ContractAddress::new(1010, 0)).unwrap(); + /// + /// // Set up an external account. + /// let external_acc = + /// chain.add_external_account(" + /// 3U4sfVSqGG6XK8g6eho2qRYtnHc4MWJBG1dfxdtPGbfHwFxini".parse().unwrap()). + /// unwrap(); + /// + /// let res = chain.contract_invoke_external( + /// Some(ExternalAddress::Account(external_acc)), + /// 10000.into(), + /// InvokeExternalContractPayload { + /// amount: Amount::zero(), + /// address: external_contract, + /// receive_name: + /// OwnedReceiveName::new_unchecked("my_contract.view".to_string()), + /// message: OwnedParameter::empty(), + /// }, + /// None, + /// ); + /// ``` + pub fn contract_invoke_external( + &mut self, + sender: Option, + energy_reserved: Energy, + payload: InvokeExternalContractPayload, + block: Option, + ) -> Result { + let connection = self.external_node_connection_mut().unwrap(); + + // Make the invocation. + let invoke_result: InvokeContractResult = + connection.with_client(block, |block_identifier, mut client| async move { + let invoke_result = client + .invoke_instance( + block_identifier, + &sdk::types::smart_contracts::ContractContext { + invoker: sender.map(|ext_addr| ext_addr.to_address()), + contract: payload.address.address, + amount: payload.amount, + method: payload.receive_name, + parameter: payload.message, + energy: energy_reserved, + }, + ) + .await? + .response; + Ok::<_, ExternalNodeError>(invoke_result) + })?; + + // Convert the result. + match invoke_result { + InvokeContractResult::Success { + return_value, + events, + used_energy, + } => Ok(ContractInvokeExternalSuccess { + trace_elements: events, + energy_used: used_energy, + return_value: return_value.map(|rv| rv.value).unwrap_or_default(), + }), + InvokeContractResult::Failure { + return_value, + reason, + used_energy, + } => Err(ContractInvokeExternalError::Failure { + reason, + energy_used: used_energy, + return_value: return_value.map(|rv| rv.value).unwrap_or_default(), + }), + } + } + /// Create an account. /// /// If an account with a matching address already exists this method will @@ -1217,6 +1323,68 @@ impl Chain { self.accounts.insert(account.address.into(), account) } + /// Add an external account from a connected external node. + /// + /// If the account exists on the external node at the time of the + /// `external_query_block`, then an [`ExernalAccountAddress`] is returned. + /// The address can be used with [`Chain::contract_invoke_external`]. + /// Otherwise, an error is returned. + /// + /// Barring external node errors, the method is idempotent, and so it can be + /// called multiple times with the same effect. + pub fn add_external_account( + &mut self, + address: AccountAddress, + ) -> Result { + let connection = self.external_node_connection_mut()?; + + let external_addr = + connection.with_client(None, |block_identifier, mut client| async move { + // Try to get the account info to verify the existence of the account, but + // discard the result. + client + .get_account_info( + &sdk::v2::AccountIdentifier::Address(address), + block_identifier, + ) + .await?; + Ok::<_, ExternalNodeError>(ExternalAccountAddress { address }) + })?; + + connection.accounts.insert(external_addr); + + Ok(external_addr) + } + + /// Add an external contract from a connected external node. + /// + /// If the contract exists on the external node at the time of the + /// `external_query_block`, then an [`ExernalContractAddress`] is returned. + /// The address can be used with [`Chain::contract_invoke_external`]. + /// Otherwise, an error is returned. + /// + /// Barring external node errors, the method is idempotent, and so it can be + /// called multiple times with the same effect. + pub fn add_external_contract( + &mut self, + address: ContractAddress, + ) -> Result { + let connection = self.external_node_connection_mut()?; + + let external_addr = + connection.with_client(None, |block_identifier, mut client| async move { + // Try to get the contract instance info to verify the existence of the + // contract, but discard the result. + client.get_instance_info(address, block_identifier).await?; + Ok::<_, ExternalNodeError>(ExternalContractAddress { address }) + })?; + + // TODO: Mention that this is idempotent. + connection.contracts.insert(external_addr); + + Ok(external_addr) + } + /// Create a contract address by giving it the next available index. fn create_contract_address(&mut self) -> ContractAddress { let index = self.next_contract_index; @@ -1524,6 +1692,8 @@ impl Chain { client, runtime, query_block: checked_query_block, + accounts: BTreeSet::new(), + contracts: BTreeSet::new(), }); Ok(()) @@ -1982,6 +2152,7 @@ mod tests { #[cfg(test)] mod io_tests { use super::*; + use crate::*; /// Test that building a chain using the external node parameters works. #[test] @@ -2038,4 +2209,44 @@ mod io_tests { error: SetupExternalNodeError::CannotCheckQueryBlockExistence { .. }, })); } + + /// Invoke an external contract and check that it succeeds. Also check that + /// the energy is correct. + #[test] + fn test_contract_invoke_external() { + let mut chain = Chain::builder() + .external_node_connection(Endpoint::from_static( + "http://node.testnet.concordium.com:20000", + )) + .build() + .unwrap(); + + // A CIS-2 contract. + let external_contr = chain + .add_external_contract(ContractAddress::new(5089, 0)) + .unwrap(); + + let external_acc = chain + .add_external_account( + "3U4sfVSqGG6XK8g6eho2qRYtnHc4MWJBG1dfxdtPGbfHwFxini" + .parse() + .unwrap(), + ) + .unwrap(); + + let res = chain + .contract_invoke_external( + Some(external_acc.into()), + 10000.into(), + InvokeExternalContractPayload { + amount: Amount::zero(), + address: external_contr, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".into()), + message: OwnedParameter::empty(), + }, + None, + ) + .unwrap(); + assert_eq!(res.energy_used, 1851.into()); + } } diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index cbf31036..665522e6 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -105,5 +105,5 @@ pub use concordium_base::{ smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, transactions::{InitContractPayload, UpdateContractPayload}, }; -pub use concordium_rust_sdk::v2::Endpoint; +pub use concordium_rust_sdk::{types::RejectReason, v2::Endpoint}; pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index f13eb695..1f370a58 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -5,12 +5,19 @@ use concordium_base::{ ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, Timestamp, }, hashes::BlockHash, - smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, + smart_contracts::{ + ContractEvent, ContractTraceElement, InstanceUpdatedEvent, OwnedParameter, + OwnedReceiveName, WasmVersion, + }, }; use concordium_rust_sdk as sdk; use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; use concordium_wasm::artifact; -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + sync::Arc, +}; use thiserror::Error; /// A smart contract module. @@ -29,13 +36,9 @@ pub(crate) struct ChainParameters { /// Defaults to `0`. pub(crate) block_time: SlotTime, /// MicroCCD per Euro ratio. - // This is not public because we ensure a reasonable value during the construction of the - // [`Chain`]. pub(crate) micro_ccd_per_euro: ExchangeRate, /// Euro per Energy ratio. - // This is not public because we ensure a reasonable value during the construction of the - // [`Chain`]. - pub(crate) euro_per_energy: ExchangeRate, + pub(crate) euro_per_energy: ExchangeRate, } /// The connection and runtime needed for communicating with an external node. @@ -48,6 +51,10 @@ pub(crate) struct ExternalNodeConnection { pub(crate) runtime: tokio::runtime::Runtime, /// The block used for queries. pub(crate) query_block: BlockHash, + /// External accounts that are verified to exist in the `query_block`. + pub(crate) accounts: BTreeSet, + /// External contracts that are verified to exist in the `query_block`. + pub(crate) contracts: BTreeSet, } /// Represents the blockchain and supports a number of operations, including @@ -329,6 +336,18 @@ pub struct ContractInvokeSuccess { pub new_balance: Amount, } +/// Represents a successful external contract invocation. +#[derive(Debug)] +pub struct ContractInvokeExternalSuccess { + /// Host events that occurred. This includes interrupts, resumes, and + /// upgrades. + pub trace_elements: Vec, + /// The energy used. + pub energy_used: Energy, + /// The returned value. + pub return_value: ReturnValue, +} + impl ContractInvokeSuccess { /// Extract all the events logged by all the contracts in the invocation. /// The events are returned in the order that they are emitted, and are @@ -567,6 +586,32 @@ pub enum ContractInvokeErrorKind { ParameterTooLarge, } +/// The error returned when external contract invocations fail. +#[derive(Debug, Error)] +pub enum ContractInvokeExternalError { + /// The external contract invocation was executed, but resulted in a + /// failure. + #[error( + "External contract invocation was executed, but failed after using {energy_used}NRG with \ + error {reason:?}." + )] + Failure { + /// The reason why the invoke failed. + reason: sdk::types::RejectReason, + /// The energy used before failure. + energy_used: Energy, + /// The value returned. + return_value: ReturnValue, + }, + /// The external contract invocation failed due to an external node error. + #[error("External contract invocation failed due to an external node error: {error}")] + ExternalNodeError { + #[from] + /// The external node error. + error: ExternalNodeError, + }, +} + /// A balance error which can occur when transferring [`Amount`]s. #[derive(Debug, PartialEq, Eq, Error)] pub(crate) enum BalanceError { @@ -757,3 +802,68 @@ pub enum ChainBuilderError { #[derive(Debug, Error, PartialEq, Eq)] #[error("The block time overflowed during a call to `Chain::tick_block_time`.")] pub struct BlockTimeOverflow; + +/// The contract address of an contract on an external node. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExternalContractAddress { + /// The contract address. + pub(crate) address: ContractAddress, +} + +/// The address of an account on an external node. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExternalAccountAddress { + /// The account address. + pub(crate) address: AccountAddress, +} + +/// Either an external contract address or an external account address. +/// +/// External means that it is an entity that exists on an external node. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum ExternalAddress { + Account(ExternalAccountAddress), + Contract(ExternalContractAddress), +} + +impl ExternalAddress { + /// Convert to an [`Address`]. + /// + /// This is an internal method instead of a [`From`] implementation, as it + /// should be difficult to conflate external and regular addresses. + pub(crate) fn to_address(&self) -> Address { + match self { + ExternalAddress::Account(ExternalAccountAddress { address }) => { + Address::Account(*address) + } + ExternalAddress::Contract(ExternalContractAddress { address }) => { + Address::Contract(*address) + } + } + } +} + +/// Data needed to invoke an external smart contract instance. +/// +/// This is nearly identical to [`UpdateContractPayload`] except that it uses an +/// [`ExternalContractAddress`] instead of an [`ContractAddress`]. +#[derive(Debug, Clone)] +pub struct InvokeExternalContractPayload { + /// Send the given amount of CCD together with the message to the + /// contract instance. + pub amount: Amount, + /// Address of the external contract instance to invoke. + pub address: ExternalContractAddress, + /// Name of the method to invoke on the contract. + pub receive_name: OwnedReceiveName, + /// Message to send to the contract instance. + pub message: OwnedParameter, +} + +impl From for ExternalAddress { + fn from(addr: ExternalAccountAddress) -> Self { Self::Account(addr) } +} + +impl From for ExternalAddress { + fn from(addr: ExternalContractAddress) -> Self { Self::Contract(addr) } +} From 93729330bff338bdf471508e63114982628b1f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 3 Jul 2023 19:33:36 +0200 Subject: [PATCH 179/208] Work on migrating testing library to P6. --- contract-testing/src/impls.rs | 11 +++++++++-- contract-testing/src/invocation/impls.rs | 7 +------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index c32a4990..f6dd984b 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -19,6 +19,7 @@ use concordium_smart_contract_engine::{ v1::{self, InvokeResponse}, InterpreterEnergy, }; +use concordium_wasm::validate::ValidationConfig; use num_bigint::BigUint; use num_integer::Integer; use std::{collections::BTreeMap, path::Path, sync::Arc}; @@ -238,6 +239,7 @@ impl Chain { // Construct the artifact. let artifact = match concordium_wasm::utils::instantiate_with_metering::( + ValidationConfig::V1, &v1::ConcordiumAllowedImports { support_upgrade: true, }, @@ -264,8 +266,13 @@ impl Chain { }); } self.modules.insert(module_reference, ContractModule { - size: wasm_module.source.size(), - artifact: Arc::new(artifact), + // we follow protocol 6 semantics, and don't count the custom section size towards + // module size. + size: wasm_module + .source + .size() + .saturating_sub(artifact.custom_sections_size), + artifact: Arc::new(artifact.artifact), }); Ok(ModuleDeploySuccess { module_reference, diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index ca780503..033a5402 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -10,7 +10,6 @@ use crate::{ }; use concordium_base::{ base::{AccountAddressEq, Energy, InsufficientEnergy}, - constants::MAX_PARAMETER_LEN, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedEntrypointName, OwnedReceiveName, @@ -199,11 +198,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { energy, }, instance_state, - v1::ReceiveParams { - max_parameter_size: MAX_PARAMETER_LEN, - limit_logs_and_return_values: false, - support_queries: true, - }, + v1::ReceiveParams::new_p6(), ) })?; // Set up some data needed for recursively processing the receive until the end, From 00b75cf7ee4202f030a0d52934061312c31cca73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 6 Jul 2023 16:04:01 +0200 Subject: [PATCH 180/208] Support getting account keys, and verifying signatures in the testing library. --- contract-testing/src/constants.rs | 12 ++ contract-testing/src/impls.rs | 56 ++++++-- contract-testing/src/invocation/impls.rs | 172 ++++++++++++++++++++++- contract-testing/src/types.rs | 3 + 4 files changed, 233 insertions(+), 10 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index a4d0fe36..7361be2a 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -14,6 +14,18 @@ pub(crate) const CONTRACT_INSTANCE_QUERY_CONTRACT_BALANCE_COST: Energy = Energy /// instance. pub(crate) const CONTRACT_INSTANCE_QUERY_EXCHANGE_RATE_COST: Energy = Energy { energy: 100 }; +/// Base cost querying account keys. In addition to this cost there is a cost +/// based on the number of returned keys. +pub(crate) const CONTRACT_INSTANCE_QUERY_ACCOUNT_KEYS_BASE_COST: Energy = Energy { energy: 200 }; + +/// Cost of returning the account keys, based on the number of keys. +/// Each key is 32 bytes, and there is a bit of administrative overhead. +pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> Energy { + Energy { + energy: u64::from(num_keys) * 3, + } +} + /// The base cost of initializing a contract instance to cover administrative /// costs. Even if no code is run and no instance created. pub(crate) const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index f6dd984b..cae11cd0 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -5,14 +5,16 @@ use crate::{ }; use anyhow::anyhow; use concordium_base::{ - base::{Energy, InsufficientEnergy}, - constants::{MAX_ALLOWED_INVOKE_ENERGY, MAX_WASM_MODULE_SIZE}, + base::{AccountThreshold, Energy, InsufficientEnergy}, + constants::MAX_WASM_MODULE_SIZE, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRate, ModuleReference, OwnedPolicy, SlotTime, Timestamp, }, smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, - transactions::{self, cost, InitContractPayload, UpdateContractPayload}, + transactions::{ + self, cost, AccountAccessStructure, InitContractPayload, UpdateContractPayload, + }, }; use concordium_smart_contract_engine::{ v0, @@ -1071,21 +1073,53 @@ impl Chain { } impl Account { - /// Create new [`Account`](Self) with the provided account policy. - pub fn new_with_policy( + /// Create new [`Account`](Self) with the provided account policy and keys. + pub fn new_with_policy_and_keys( address: AccountAddress, balance: AccountBalance, policy: OwnedPolicy, + keys: AccountAccessStructure, ) -> Self { Self { balance, policy, address, + keys, } } + /// Create new [`Account`](Self) with the provided account keys and balance. + /// See [`new`][Self::new] for what the default policy is. + pub fn new_with_keys( + address: AccountAddress, + balance: AccountBalance, + keys: AccountAccessStructure, + ) -> Self { + Self { + balance, + policy: Self::empty_policy(), + address, + keys, + } + } + + /// Create new [`Account`](Self) with the provided account policy. + /// The account keys are initialized with an [`AccountAccessStructure`] + /// with a threshold of 1, and no keys. So it is impossible to verify any + /// signatures with the access structure. + pub fn new_with_policy( + address: AccountAddress, + balance: AccountBalance, + policy: OwnedPolicy, + ) -> Self { + Self::new_with_policy_and_keys(address, balance, policy, AccountAccessStructure { + threshold: AccountThreshold::try_from(1u8).expect("1 is a valid threshold."), + keys: BTreeMap::new(), + }) + } + /// Create a new [`Account`](Self) with the provided balance and a default - /// account policy. + /// account policy and default account access structure. /// /// See [`new`][Self::new] for what the default policy is. pub fn new_with_balance(address: AccountAddress, balance: AccountBalance) -> Self { @@ -1100,6 +1134,10 @@ impl Account { /// - `valid_to`: unix epoch + `u64::MAX` milliseconds, /// - `items`: none, /// + /// The account keys are initialized with an [`AccountAccessStructure`] + /// with a threshold of 1, and no keys. So it is impossible to verify any + /// signatures with the access structure. + /// /// The [`AccountBalance`] will be created with the provided /// `total_balance`. pub fn new(address: AccountAddress, total_balance: Amount) -> Self { @@ -1284,7 +1322,7 @@ pub fn energy_to_amount( /// Helper function that checks the validity of the exchange rates. /// /// More specifically, it checks that the cost of one energy is <= `u64::MAX / -/// [`MAX_ALLOWED_INVOKE_ENERGY`]`, which ensures that overflows won't occur. +/// `100_000_000_000`, which ensures that overflows won't occur. fn check_exchange_rates( euro_per_energy: ExchangeRate, micro_ccd_per_euro: ExchangeRate, @@ -1293,7 +1331,7 @@ fn check_exchange_rates( BigUint::from(euro_per_energy.numerator()) * micro_ccd_per_euro.numerator(); let micro_ccd_per_energy_denominator: BigUint = BigUint::from(euro_per_energy.denominator()) * micro_ccd_per_euro.denominator(); - let max_allowed_micro_ccd_to_energy = u64::MAX / MAX_ALLOWED_INVOKE_ENERGY.energy; + let max_allowed_micro_ccd_to_energy = u64::MAX / 100_000_000_000u64; let micro_ccd_per_energy = u64::try_from(micro_ccd_per_energy_numerator / micro_ccd_per_energy_denominator) .map_err(|_| ExchangeRateError)?; @@ -1318,7 +1356,7 @@ mod tests { /// comments. #[test] fn check_exchange_rates_works() { - let max_allowed_micro_ccd_per_energy = u64::MAX / MAX_ALLOWED_INVOKE_ENERGY.energy; + let max_allowed_micro_ccd_per_energy = u64::MAX / 100_000_000_000; check_exchange_rates( ExchangeRate::new_unchecked(max_allowed_micro_ccd_per_energy + 1, 1), ExchangeRate::new_unchecked(1, 1), diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 033a5402..b314d68a 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -10,16 +10,23 @@ use crate::{ }; use concordium_base::{ base::{AccountAddressEq, Energy, InsufficientEnergy}, + common::{ + self, + types::{CredentialIndex, KeyIndex, Signature}, + }, + constants::ED25519_SIGNATURE_LENGTH, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedEntrypointName, OwnedReceiveName, }, + id::types::SchemeId, smart_contracts::{ ContractTraceElement, InstanceUpdatedEvent, OwnedContractName, OwnedParameter, WasmVersion, }, - transactions::UpdateContractPayload, + transactions::{verify_data_signature, AccountAccessStructure, UpdateContractPayload}, }; use concordium_smart_contract_engine::{ + constants::verify_ed25519_cost, v0, v1::{self, trie, InvokeResponse}, ExecResult, InterpreterEnergy, @@ -716,6 +723,96 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { response: Some(response), }); } + v1::Interrupt::CheckAccountSignature { address, payload } => { + self.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_KEYS_BASE_COST, + )?; + // Due to borrow checker limitations we don't use self.account_keys here + // since it leads to clashes with the mutable borrow of + // self.remaining_energy below. + let response = + match self.chain.accounts.get(&address.into()).map(|a| &a.keys) { + Some(keys) => { + // attempt to deserialize the payload after + // looking up the account. + // This is the order in the scheduler as + // well, and the order matters + // since the response to the contract is + // different depending on failure kind. + match deserial_signature_and_data_from_contract(&payload) { + Ok((num_sigs, ref sigs, data)) => { + self.remaining_energy.tick_energy( + // This cannot overflow on any data that can be + // supplied. + // Data_len will always be at most u32, and the + // number of + // signatures is at most 256*256. + Energy::from( + u64::from(num_sigs) + * verify_ed25519_cost(data.len() as u32), + ), + )?; + if verify_data_signature(keys, data, sigs) { + v1::InvokeResponse::Success { + new_balance: self + .contract_balance_unchecked( + invocation_data.address, + ), + data: None, + } + } else { + v1::InvokeResponse::Failure { + kind: + v1::InvokeFailure::SignatureCheckFailed, + } + } + } + Err(_) => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::SignatureDataMalformed, + }, + } + } + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); + } + v1::Interrupt::QueryAccountKeys { address } => { + self.remaining_energy.tick_energy( + constants::CONTRACT_INSTANCE_QUERY_ACCOUNT_KEYS_BASE_COST, + )?; + let response = match self.account_keys(address) { + Some(keys) => { + let response_data = common::to_bytes(&keys); + let num_keys = keys.num_keys(); + self.remaining_energy.tick_energy( + constants::contract_instance_query_account_keys_return_cost( + num_keys, + ), + )?; + v1::InvokeResponse::Success { + // Balance of contract querying. Does not change for this + // request. + new_balance: self + .contract_balance_unchecked(invocation_data.address), + data: Some(response_data), + } + } + None => v1::InvokeResponse::Failure { + kind: v1::InvokeFailure::NonExistentAccount, + }, + }; + stack.push(Next::Resume { + data: invocation_data, + config, + response: Some(response), + }); + } } } v1::ReceiveResult::Reject { @@ -1078,6 +1175,13 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } } + /// Looks up the account keys. + fn account_keys(&self, address: AccountAddress) -> Option<&AccountAccessStructure> { + // The account keys cannot change during a smart contract transaction, so + // there is no need to check in the changeset. + self.chain.accounts.get(&address.into()).map(|a| &a.keys) + } + /// Saves a mutable state for a contract in the changeset. /// /// If the contract already has an entry in the changeset, the old state @@ -1304,6 +1408,72 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } } +/// Return a triple of +/// - the number of signatures +/// - the signatures +/// - the data +fn deserial_signature_and_data_from_contract( + payload: &[u8], +) -> anyhow::Result<( + u32, + BTreeMap>, + &[u8], +)> { + let mut source = std::io::Cursor::new(payload); + use concordium_base::common::Get; + use std::io::Read; + let mut out = BTreeMap::new(); + let mut last = None; + let outer_len: u8 = source.get()?; + let mut num_sigs = 0; + for _ in 0..outer_len { + let idx = source.get()?; + anyhow::ensure!( + last < Some(idx), + "Credential indices must be strictly increasing." + ); + last = Some(idx); + let inner_len: u8 = source.get()?; + let mut inner_map = BTreeMap::new(); + let mut x = None; + for _ in 0..inner_len { + let k = source.get()?; + num_sigs += 1; + let sig = match source.get()? { + SchemeId::Ed25519 => { + let mut sig = vec![0u8; ED25519_SIGNATURE_LENGTH]; + source.read_exact(&mut sig)?; + Signature { sig } + } + }; + + if let Some((old_k, old_v)) = x.take() { + anyhow::ensure!( + k > old_k, + "Next key {k} not larger than previous key {old_k}." + ); + inner_map.insert(old_k, old_v); + } + x = Some((k, sig)); + } + if let Some((k, v)) = x { + inner_map.insert(k, v); + } + out.insert(idx, inner_map); + } + // + let data_len = { + let mut buf = [0u8; 4]; + source.read_exact(&mut buf)?; + u32::from_le_bytes(buf) + }; + Ok(( + num_sigs, + out, + &payload[source.position() as usize..][..data_len as usize], + )) +} + impl ChangeSet { /// Creates a new changeset with one empty [`Changes`] element on the /// stack.. diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 42523eba..ce898dd8 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -5,6 +5,7 @@ use concordium_base::{ ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, + transactions::AccountAccessStructure, }; use concordium_smart_contract_engine::v1::{self, trie, ReturnValue}; use concordium_wasm::artifact; @@ -78,6 +79,8 @@ pub struct Account { pub balance: AccountBalance, /// Account policy. pub policy: OwnedPolicy, + /// Account's public keys. + pub keys: AccountAccessStructure, } /// A signer with a number of keys, the amount of which affects the cost of From 7e00a3d6f4de0c61def5c794233fd56ea6827c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Jul 2023 12:47:56 +0200 Subject: [PATCH 181/208] Name a complex type. --- contract-testing/src/invocation/impls.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b314d68a..232944c1 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1408,17 +1408,24 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } } +/// A triple of number of signatures, +/// the signatures, and the data. +/// The number of signatures is in principle recoverable from the second +/// component, but since that is nested map it is awkward to do, and since we +/// learn the number during deserialization anyhow we return it here. +type DeserializedSignatureAndData<'a> = ( + u32, + BTreeMap>, + &'a [u8], +); + /// Return a triple of /// - the number of signatures /// - the signatures /// - the data fn deserial_signature_and_data_from_contract( payload: &[u8], -) -> anyhow::Result<( - u32, - BTreeMap>, - &[u8], -)> { +) -> anyhow::Result { let mut source = std::io::Cursor::new(payload); use concordium_base::common::Get; use std::io::Read; From f7b8e68c9ddf1e70060ed90f0e786d4de3f34903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 9 Jul 2023 13:03:38 +0200 Subject: [PATCH 182/208] Fix costs of signature checks. --- contract-testing/src/constants.rs | 11 +++++++++++ contract-testing/src/invocation/impls.rs | 23 +++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 7361be2a..8a4d4049 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -26,6 +26,17 @@ pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> } } +/// Cost **in energy** of verification of an ed25519. +/// This should match the cost of +/// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants::verify_ed25519_cost) +/// except the latter is the cost in interpreter energy, and this on is in +/// [`Energy`]. +pub fn verify_ed25519_energy_cost(num_sigs: u32, message_len: u32) -> Energy { + Energy { + energy: u64::from(num_sigs) * (100 + u64::from(message_len) / 10), + } +} + /// The base cost of initializing a contract instance to cover administrative /// costs. Even if no code is run and no instance created. pub(crate) const INITIALIZE_CONTRACT_INSTANCE_BASE_COST: Energy = Energy { energy: 300 }; diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 232944c1..dff637dd 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1,6 +1,6 @@ use super::types::*; use crate::{ - constants, + constants::{self, verify_ed25519_energy_cost}, impls::{ contract_events_from_logs, from_interpreter_energy, lookup_module_cost, to_interpreter_energy, @@ -26,7 +26,6 @@ use concordium_base::{ transactions::{verify_data_signature, AccountAccessStructure, UpdateContractPayload}, }; use concordium_smart_contract_engine::{ - constants::verify_ed25519_cost, v0, v1::{self, trie, InvokeResponse}, ExecResult, InterpreterEnergy, @@ -742,16 +741,16 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { match deserial_signature_and_data_from_contract(&payload) { Ok((num_sigs, ref sigs, data)) => { self.remaining_energy.tick_energy( - // This cannot overflow on any data that can be - // supplied. - // Data_len will always be at most u32, and the - // number of - // signatures is at most 256*256. - Energy::from( - u64::from(num_sigs) - * verify_ed25519_cost(data.len() as u32), - ), - )?; + // This cannot overflow on any data that can be + // supplied. + // Data_len will always be at most u32, and the + // number of + // signatures is at most 256*256. + verify_ed25519_energy_cost( + num_sigs, + data.len() as u32, + ), + )?; if verify_data_signature(keys, data, sigs) { v1::InvokeResponse::Success { new_balance: self From 79931786d94a0e55fef4736228a396392968ee20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 10 Jul 2023 15:52:17 +0200 Subject: [PATCH 183/208] Tests for new functionality. --- contract-testing/Cargo.toml | 5 +- contract-testing/src/invocation/impls.rs | 70 ++------- contract-testing/src/types.rs | 90 +++++++++++ .../tests/account-signature-checks.rs | 140 ++++++++++++++++++ contract-testing/tests/basics.rs | 2 +- 5 files changed, 245 insertions(+), 62 deletions(-) create mode 100644 contract-testing/tests/account-signature-checks.rs diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 52924bce..bb80b4af 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -20,4 +20,7 @@ sha2 = "0.10" anyhow = "1" thiserror = "1.0" num-bigint = "0.4" -num-integer = "0.1" \ No newline at end of file +num-integer = "0.1" + +[dev-dependencies] +rand = "0.7" \ No newline at end of file diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index dff637dd..b0a1ed40 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -6,20 +6,15 @@ use crate::{ to_interpreter_energy, }, types::{Account, BalanceError, Contract, ContractModule, TransferError}, - DebugTraceElement, ExecutionError, InvokeExecutionError, + AccountSignatures, DebugTraceElement, ExecutionError, InvokeExecutionError, }; use concordium_base::{ base::{AccountAddressEq, Energy, InsufficientEnergy}, - common::{ - self, - types::{CredentialIndex, KeyIndex, Signature}, - }, - constants::ED25519_SIGNATURE_LENGTH, + common, contracts_common::{ to_bytes, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, ExchangeRates, ModuleReference, OwnedEntrypointName, OwnedReceiveName, }, - id::types::SchemeId, smart_contracts::{ ContractTraceElement, InstanceUpdatedEvent, OwnedContractName, OwnedParameter, WasmVersion, }, @@ -739,7 +734,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { // since the response to the contract is // different depending on failure kind. match deserial_signature_and_data_from_contract(&payload) { - Ok((num_sigs, ref sigs, data)) => { + Ok((sigs, data)) => { + let num_sigs = sigs.num_signatures(); self.remaining_energy.tick_energy( // This cannot overflow on any data that can be // supplied. @@ -751,7 +747,7 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { data.len() as u32, ), )?; - if verify_data_signature(keys, data, sigs) { + if verify_data_signature(keys, data, &sigs.into()) { v1::InvokeResponse::Success { new_balance: self .contract_balance_unchecked( @@ -1407,16 +1403,8 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { } } -/// A triple of number of signatures, -/// the signatures, and the data. -/// The number of signatures is in principle recoverable from the second -/// component, but since that is nested map it is awkward to do, and since we -/// learn the number during deserialization anyhow we return it here. -type DeserializedSignatureAndData<'a> = ( - u32, - BTreeMap>, - &'a [u8], -); +/// A pair of the signatures, and the data. +type DeserializedSignatureAndData<'a> = (AccountSignatures, &'a [u8]); /// Return a triple of /// - the number of signatures @@ -1426,47 +1414,10 @@ fn deserial_signature_and_data_from_contract( payload: &[u8], ) -> anyhow::Result { let mut source = std::io::Cursor::new(payload); - use concordium_base::common::Get; + use common::Deserial; use std::io::Read; - let mut out = BTreeMap::new(); - let mut last = None; - let outer_len: u8 = source.get()?; - let mut num_sigs = 0; - for _ in 0..outer_len { - let idx = source.get()?; - anyhow::ensure!( - last < Some(idx), - "Credential indices must be strictly increasing." - ); - last = Some(idx); - let inner_len: u8 = source.get()?; - let mut inner_map = BTreeMap::new(); - let mut x = None; - for _ in 0..inner_len { - let k = source.get()?; - num_sigs += 1; - let sig = match source.get()? { - SchemeId::Ed25519 => { - let mut sig = vec![0u8; ED25519_SIGNATURE_LENGTH]; - source.read_exact(&mut sig)?; - Signature { sig } - } - }; - if let Some((old_k, old_v)) = x.take() { - anyhow::ensure!( - k > old_k, - "Next key {k} not larger than previous key {old_k}." - ); - inner_map.insert(old_k, old_v); - } - x = Some((k, sig)); - } - if let Some((k, v)) = x { - inner_map.insert(k, v); - } - out.insert(idx, inner_map); - } + let signatures = AccountSignatures::deserial(&mut source)?; // let data_len = { let mut buf = [0u8; 4]; @@ -1474,8 +1425,7 @@ fn deserial_signature_and_data_from_contract( u32::from_le_bytes(buf) }; Ok(( - num_sigs, - out, + signatures, &payload[source.position() as usize..][..data_len as usize], )) } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index ce898dd8..615cd75e 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -1,9 +1,15 @@ use concordium_base::{ base::{AccountAddressEq, Energy}, + common::{ + self, + types::{CredentialIndex, KeyIndex, Signature}, + }, + constants::ED25519_SIGNATURE_LENGTH, contracts_common::{ AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, + id::types::SchemeId, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, transactions::AccountAccessStructure, }; @@ -83,6 +89,90 @@ pub struct Account { pub keys: AccountAccessStructure, } +/// A signature with account's keys. +#[derive(Debug, Clone)] +pub struct AccountSignatures { + /// It is assumed that the inner `Signature` will always be for ed25519 + /// scheme, so will have length 64 bytes. + pub(crate) sigs: BTreeMap>, +} + +impl From for BTreeMap> { + fn from(value: AccountSignatures) -> Self { value.sigs } +} + +impl From>> for AccountSignatures { + fn from(sigs: BTreeMap>) -> Self { + Self { sigs } + } +} + +impl AccountSignatures { + /// Return the number of signatures contained in the structure. + pub fn num_signatures(&self) -> u32 { self.sigs.values().map(|v| v.len() as u32).sum() } +} + +impl common::Serial for AccountSignatures { + fn serial(&self, out: &mut B) { + (self.sigs.len() as u8).serial(out); + for (k, v) in self.sigs.iter() { + k.serial(out); + (v.len() as u8).serial(out); + for (ki, sig) in v.iter() { + ki.serial(out); + // ed25519 scheme tag. + 0u8.serial(out); + out.write_all(&sig.sig) + .expect("Writing to buffer does not fail."); + } + } + } +} + +impl common::Deserial for AccountSignatures { + fn deserial(source: &mut R) -> common::ParseResult { + use common::Get; + let outer_len = u8::deserial(source)?; + let mut last = None; + let mut sigs = BTreeMap::new(); + for _ in 0..outer_len { + let idx = source.get()?; + anyhow::ensure!( + last < Some(idx), + "Credential indices must be strictly increasing." + ); + last = Some(idx); + let inner_len: u8 = source.get()?; + let mut inner_map = BTreeMap::new(); + let mut x = None; + for _ in 0..inner_len { + let k = source.get()?; + let sig = match source.get()? { + SchemeId::Ed25519 => { + let mut sig = vec![0u8; ED25519_SIGNATURE_LENGTH]; + source.read_exact(&mut sig)?; + Signature { sig } + } + }; + + if let Some((old_k, old_v)) = x.take() { + anyhow::ensure!( + k > old_k, + "Next key {k} not larger than previous key {old_k}." + ); + inner_map.insert(old_k, old_v); + } + x = Some((k, sig)); + } + if let Some((k, v)) = x { + inner_map.insert(k, v); + } + sigs.insert(idx, inner_map); + } + Ok(Self { sigs }) + } +} + /// A signer with a number of keys, the amount of which affects the cost of /// transactions. #[derive(Copy, Clone, Debug)] diff --git a/contract-testing/tests/account-signature-checks.rs b/contract-testing/tests/account-signature-checks.rs new file mode 100644 index 00000000..45183f69 --- /dev/null +++ b/contract-testing/tests/account-signature-checks.rs @@ -0,0 +1,140 @@ +//! This module contains tests for checking account signatures, and retrieving +//! account's public keys. +use concordium_base::{ + common, + contracts_common::{self, AccountBalance, AccountThreshold, SignatureThreshold}, + id::types::AccountKeys, + transactions::AccountAccessStructure, +}; +use concordium_smart_contract_testing::*; + +const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; +const ACC_0: AccountAddress = AccountAddress([0; 32]); + +/// Test that we can query the correct account keys. +#[test] +fn test() { + let mut chain = Chain::new(); + let initial_balance = Amount::from_ccd(1000000); + let mut csprng = rand::thread_rng(); + let acc_keys = AccountKeys::generate( + AccountThreshold::TWO, + &[ + (3.into(), SignatureThreshold::TWO, &[ + 7.into(), + 8.into(), + 17.into(), + ]), + (7.into(), SignatureThreshold::ONE, &[ + 3.into(), + 8.into(), + 33.into(), + ]), + (37.into(), SignatureThreshold::ONE, &[ + 2.into(), + 8.into(), + 255.into(), + ]), + (254.into(), SignatureThreshold::TWO, &[ + 1.into(), + 2.into(), + 3.into(), + ]), + ], + &mut csprng, + ); + let acc_structure: AccountAccessStructure = (&acc_keys).into(); + chain.create_account(Account::new_with_keys( + ACC_0, + AccountBalance { + total: initial_balance, + staked: Amount::zero(), + locked: Amount::zero(), + }, + acc_structure.clone(), + )); + + let res_deploy = chain + .module_deploy_v1( + Signer::with_one_key(), + ACC_0, + module_load_v1_raw(format!( + "{}/account-signature-checks.wasm", + WASM_TEST_FOLDER + )) + .expect("module should exist"), + ) + .expect("Deploying valid module should work"); + + let res_init = chain + .contract_init( + Signer::with_one_key(), + ACC_0, + Energy::from(10000), + InitContractPayload { + init_name: OwnedContractName::new_unchecked("init_contract".into()), + mod_ref: res_deploy.module_reference, + + param: OwnedParameter::empty(), + amount: Amount::zero(), + }, + ) + .expect("Initializing valid contract should work"); + + let res_invoke_get_keys = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.get_keys".into()), + message: OwnedParameter::from_serial(&ACC_0) + .expect("Parameter has valid size"), + amount: Amount::zero(), + }, + ) + .expect("Querying contract should work"); + let rv = common::from_bytes::(&mut std::io::Cursor::new( + &res_invoke_get_keys.return_value, + )) + .expect("Return value should be deserializable."); + assert_eq!( + rv, acc_structure, + "Retrieved account structure does not match the expected one." + ); + + let data: [u8; 34] = [ + 30, 0, 0, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, + ]; + let signatures = AccountSignatures::from(acc_keys.sign_data(&data[4..])); + let parameter = { + let mut parameter = ACC_0.0.to_vec(); + use common::Serial; + signatures.serial(&mut parameter); + data.serial(&mut parameter); + parameter + }; + + let res_invoke_check_signature = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + Energy::from(100000), + UpdateContractPayload { + address: res_init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("contract.check_signature".into()), + message: OwnedParameter::new_unchecked(parameter), + amount: Amount::zero(), + }, + ) + .expect("Querying contract should work"); + let rv = contracts_common::from_bytes::(&res_invoke_check_signature.return_value) + .expect("Return value should be deserializable."); + assert_eq!( + rv, 0, + "Signature check should succeed, the return value should be 0." + ); + println!("{}", res_invoke_check_signature.energy_used); +} diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 2789ee13..25c0690f 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -6,7 +6,7 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; #[test] -fn fib_reentry_and_cost_test() { +fn basics() { let mut chain = Chain::new_with_time_and_rates( SlotTime::from_timestamp_millis(0), // Set a specific value, taken from testnet, to compare the exact amounts charged. From e1a234d22e670109c8eaa81071f4bc26c40dad58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 10 Jul 2023 19:01:13 +0200 Subject: [PATCH 184/208] Remove prints. --- contract-testing/tests/account-signature-checks.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contract-testing/tests/account-signature-checks.rs b/contract-testing/tests/account-signature-checks.rs index 45183f69..02a6199f 100644 --- a/contract-testing/tests/account-signature-checks.rs +++ b/contract-testing/tests/account-signature-checks.rs @@ -136,5 +136,4 @@ fn test() { rv, 0, "Signature check should succeed, the return value should be 0." ); - println!("{}", res_invoke_check_signature.energy_used); } From 6f1fb26b6d2fe0810605dc19234b2ad8c786461d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 10 Jul 2023 20:34:37 +0200 Subject: [PATCH 185/208] Simplify tests. --- contract-testing/src/invocation/impls.rs | 10 ++--- contract-testing/src/types.rs | 45 +++++++++---------- .../tests/account-signature-checks.rs | 20 +++------ 3 files changed, 30 insertions(+), 45 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index b0a1ed40..6f0da92a 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1413,9 +1413,8 @@ type DeserializedSignatureAndData<'a> = (AccountSignatures, &'a [u8]); fn deserial_signature_and_data_from_contract( payload: &[u8], ) -> anyhow::Result { - let mut source = std::io::Cursor::new(payload); - use common::Deserial; - use std::io::Read; + use concordium_base::contracts_common::{Deserial, Read}; + let mut source = concordium_base::contracts_common::Cursor::new(payload); let signatures = AccountSignatures::deserial(&mut source)?; // @@ -1424,10 +1423,7 @@ fn deserial_signature_and_data_from_contract( source.read_exact(&mut buf)?; u32::from_le_bytes(buf) }; - Ok(( - signatures, - &payload[source.position() as usize..][..data_len as usize], - )) + Ok((signatures, &payload[source.offset..][..data_len as usize])) } impl ChangeSet { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 615cd75e..ce8d5ede 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -1,12 +1,9 @@ use concordium_base::{ base::{AccountAddressEq, Energy}, - common::{ - self, - types::{CredentialIndex, KeyIndex, Signature}, - }, + common::types::{CredentialIndex, KeyIndex, Signature}, constants::ED25519_SIGNATURE_LENGTH, contracts_common::{ - AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, + self, AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, }, id::types::SchemeId, @@ -112,35 +109,34 @@ impl AccountSignatures { pub fn num_signatures(&self) -> u32 { self.sigs.values().map(|v| v.len() as u32).sum() } } -impl common::Serial for AccountSignatures { - fn serial(&self, out: &mut B) { - (self.sigs.len() as u8).serial(out); +impl contracts_common::Serial for AccountSignatures { + fn serial(&self, out: &mut W) -> Result<(), W::Err> { + (self.sigs.len() as u8).serial(out)?; for (k, v) in self.sigs.iter() { - k.serial(out); - (v.len() as u8).serial(out); + k.serial(out)?; + (v.len() as u8).serial(out)?; for (ki, sig) in v.iter() { - ki.serial(out); + ki.serial(out)?; // ed25519 scheme tag. - 0u8.serial(out); - out.write_all(&sig.sig) - .expect("Writing to buffer does not fail."); + 0u8.serial(out)?; + out.write_all(&sig.sig)?; } } + Ok(()) } } -impl common::Deserial for AccountSignatures { - fn deserial(source: &mut R) -> common::ParseResult { - use common::Get; +impl contracts_common::Deserial for AccountSignatures { + fn deserial(source: &mut R) -> contracts_common::ParseResult { + use contracts_common::Get; let outer_len = u8::deserial(source)?; let mut last = None; let mut sigs = BTreeMap::new(); for _ in 0..outer_len { let idx = source.get()?; - anyhow::ensure!( - last < Some(idx), - "Credential indices must be strictly increasing." - ); + if last >= Some(idx) { + return Err(contracts_common::ParseError {}); + } last = Some(idx); let inner_len: u8 = source.get()?; let mut inner_map = BTreeMap::new(); @@ -156,10 +152,9 @@ impl common::Deserial for AccountSignatures { }; if let Some((old_k, old_v)) = x.take() { - anyhow::ensure!( - k > old_k, - "Next key {k} not larger than previous key {old_k}." - ); + if k <= old_k { + return Err(contracts_common::ParseError {}); + } inner_map.insert(old_k, old_v); } x = Some((k, sig)); diff --git a/contract-testing/tests/account-signature-checks.rs b/contract-testing/tests/account-signature-checks.rs index 02a6199f..9688f2c0 100644 --- a/contract-testing/tests/account-signature-checks.rs +++ b/contract-testing/tests/account-signature-checks.rs @@ -1,7 +1,6 @@ //! This module contains tests for checking account signatures, and retrieving //! account's public keys. use concordium_base::{ - common, contracts_common::{self, AccountBalance, AccountThreshold, SignatureThreshold}, id::types::AccountKeys, transactions::AccountAccessStructure, @@ -95,27 +94,21 @@ fn test() { }, ) .expect("Querying contract should work"); - let rv = common::from_bytes::(&mut std::io::Cursor::new( - &res_invoke_get_keys.return_value, - )) - .expect("Return value should be deserializable."); + let rv = + contracts_common::from_bytes::(&res_invoke_get_keys.return_value) + .expect("Return value should be deserializable."); assert_eq!( rv, acc_structure, "Retrieved account structure does not match the expected one." ); + // Data is a serialization of a 30-element byte array with 4 byte length prefix + // (in little endian). let data: [u8; 34] = [ 30, 0, 0, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, ]; let signatures = AccountSignatures::from(acc_keys.sign_data(&data[4..])); - let parameter = { - let mut parameter = ACC_0.0.to_vec(); - use common::Serial; - signatures.serial(&mut parameter); - data.serial(&mut parameter); - parameter - }; let res_invoke_check_signature = chain .contract_invoke( @@ -125,7 +118,8 @@ fn test() { UpdateContractPayload { address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("contract.check_signature".into()), - message: OwnedParameter::new_unchecked(parameter), + message: OwnedParameter::from_serial(&(ACC_0, &signatures, data)) + .expect("Enough space."), amount: Amount::zero(), }, ) From 53a14eb88d68fc3f920f5a8567bcb7c227e07826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 12 Jul 2023 20:05:22 +0200 Subject: [PATCH 186/208] Address review comments, improve documentation. --- contract-testing/src/constants.rs | 2 +- contract-testing/src/invocation/impls.rs | 18 +++++++----------- contract-testing/src/types.rs | 11 +++++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 8a4d4049..9f171f6d 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -26,7 +26,7 @@ pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> } } -/// Cost **in energy** of verification of an ed25519. +/// Cost **in energy** of verification of an ed25519 signature. /// This should match the cost of /// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants::verify_ed25519_cost) /// except the latter is the cost in interpreter energy, and this on is in diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 6f0da92a..cff34bfa 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1406,23 +1406,19 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { /// A pair of the signatures, and the data. type DeserializedSignatureAndData<'a> = (AccountSignatures, &'a [u8]); -/// Return a triple of -/// - the number of signatures -/// - the signatures -/// - the data +/// Deserialize the signatures and data from the slice. +/// Note that this does not ensure that the entire data is read, i.e., there can +/// be leftover data in the slice, which matches the behaviour in the node. fn deserial_signature_and_data_from_contract( payload: &[u8], ) -> anyhow::Result { - use concordium_base::contracts_common::{Deserial, Read}; + // Imported locally only since it is critical that we use the right Deserial + // trait. + use concordium_base::contracts_common::Deserial; let mut source = concordium_base::contracts_common::Cursor::new(payload); let signatures = AccountSignatures::deserial(&mut source)?; - // - let data_len = { - let mut buf = [0u8; 4]; - source.read_exact(&mut buf)?; - u32::from_le_bytes(buf) - }; + let data_len = u32::deserial(&mut source)?; Ok((signatures, &payload[source.offset..][..data_len as usize])) } diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index ce8d5ede..d898505d 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -128,6 +128,9 @@ impl contracts_common::Serial for AccountSignatures { impl contracts_common::Deserial for AccountSignatures { fn deserial(source: &mut R) -> contracts_common::ParseResult { + // We essentially unroll the definitions of `deserial_map_no_length` here since + // the inner type, the Signature, does not have exactly the right + // serialization instance that we need. use contracts_common::Get; let outer_len = u8::deserial(source)?; let mut last = None; @@ -140,7 +143,7 @@ impl contracts_common::Deserial for AccountSignatures { last = Some(idx); let inner_len: u8 = source.get()?; let mut inner_map = BTreeMap::new(); - let mut x = None; + let mut last_inner = None; for _ in 0..inner_len { let k = source.get()?; let sig = match source.get()? { @@ -151,15 +154,15 @@ impl contracts_common::Deserial for AccountSignatures { } }; - if let Some((old_k, old_v)) = x.take() { + if let Some((old_k, old_v)) = last_inner.take() { if k <= old_k { return Err(contracts_common::ParseError {}); } inner_map.insert(old_k, old_v); } - x = Some((k, sig)); + last_inner = Some((k, sig)); } - if let Some((k, v)) = x { + if let Some((k, v)) = last_inner { inner_map.insert(k, v); } sigs.insert(idx, inner_map); From 8bf87bbaf076a48e2173f346b681b947aecdafd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 12 Jul 2023 20:33:43 +0200 Subject: [PATCH 187/208] Changelog and hiding. --- contract-testing/CHANGELOG.md | 10 ++++++++++ contract-testing/src/constants.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 35b3cdf5..dfa91884 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased changes + +- Support protocol 6 semantics, in particular validation assumes protocol 6 + semantics now, allowing sign extension instructions and new operations for + querying account's public keys, and checking signatures with account's keys. +- Add `AccountSignatures` structure. +- Add an `AccountAccessStructure` to the `Account` structure. This is a breaking + change. Extend the constructors of `Account` to allow constructing accounts + with this access structure. + ## 2.0.0 - Include `ContractTraceElement`s for failed contract executions, including internal failures. diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 9f171f6d..0d18726a 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -31,7 +31,7 @@ pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> /// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants::verify_ed25519_cost) /// except the latter is the cost in interpreter energy, and this on is in /// [`Energy`]. -pub fn verify_ed25519_energy_cost(num_sigs: u32, message_len: u32) -> Energy { +pub(crate) fn verify_ed25519_energy_cost(num_sigs: u32, message_len: u32) -> Energy { Energy { energy: u64::from(num_sigs) * (100 + u64::from(message_len) / 10), } From dfd14764993fb29e4b0e98286cb71752983e639d Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 14 Jul 2023 15:41:49 +0300 Subject: [PATCH 188/208] Re-export types --- contract-testing/src/constants.rs | 6 +++--- contract-testing/src/lib.rs | 11 +++++++---- contract-testing/src/types.rs | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index 0d18726a..bfc2441a 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -28,9 +28,9 @@ pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> /// Cost **in energy** of verification of an ed25519 signature. /// This should match the cost of -/// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants::verify_ed25519_cost) -/// except the latter is the cost in interpreter energy, and this on is in -/// [`Energy`]. +/// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants:: +/// verify_ed25519_cost) except the latter is the cost in interpreter energy, +/// and this on is in [`Energy`]. pub(crate) fn verify_ed25519_energy_cost(num_sigs: u32, message_len: u32) -> Energy { Energy { energy: u64::from(num_sigs) * (100 + u64::from(message_len) / 10), diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index 9891cfd5..74a8edce 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -95,12 +95,15 @@ pub use types::*; // Re-export types. pub use concordium_base::{ base::Energy, + common::types::{CredentialIndex, KeyIndex}, contracts_common::{ - from_bytes, to_bytes, AccountAddress, Address, Amount, ContractAddress, ContractName, - EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, - OwnedParameter, OwnedReceiveName, Parameter, ReceiveName, SlotTime, + from_bytes, to_bytes, AccountAddress, AccountBalance, AccountThreshold, Address, Amount, + ContractAddress, ContractName, EntrypointName, ExchangeRate, ModuleReference, + OwnedContractName, OwnedEntrypointName, OwnedParameter, OwnedReceiveName, Parameter, + ReceiveName, SignatureThreshold, SlotTime, }, + id::types::{CredentialPublicKeys, VerifyKey}, smart_contracts::{ContractEvent, ContractTraceElement, InstanceUpdatedEvent, WasmVersion}, - transactions::{InitContractPayload, UpdateContractPayload}, + transactions::{AccountAccessStructure, InitContractPayload, UpdateContractPayload}, }; pub use concordium_smart_contract_engine::v1::InvokeFailure; diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index d898505d..3e9ad591 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -36,7 +36,7 @@ pub(crate) struct ChainParameters { /// Euro per Energy ratio. // This is not public because we ensure a reasonable value during the construction of the // [`Chain`]. - pub(crate) euro_per_energy: ExchangeRate, + pub(crate) euro_per_energy: ExchangeRate, } /// Represents the blockchain and supports a number of operations, including From 9e3d0751e074eb9e50f923d290d67bfd691701b4 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 18 Jul 2023 09:48:09 +0300 Subject: [PATCH 189/208] Undo linting changes --- contract-testing/src/constants.rs | 6 +++--- contract-testing/src/types.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contract-testing/src/constants.rs b/contract-testing/src/constants.rs index bfc2441a..0d18726a 100644 --- a/contract-testing/src/constants.rs +++ b/contract-testing/src/constants.rs @@ -28,9 +28,9 @@ pub(crate) fn contract_instance_query_account_keys_return_cost(num_keys: u32) -> /// Cost **in energy** of verification of an ed25519 signature. /// This should match the cost of -/// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants:: -/// verify_ed25519_cost) except the latter is the cost in interpreter energy, -/// and this on is in [`Energy`]. +/// [`verify_ed22519_cost`](concordium_smart_contract_engine::constants::verify_ed25519_cost) +/// except the latter is the cost in interpreter energy, and this on is in +/// [`Energy`]. pub(crate) fn verify_ed25519_energy_cost(num_sigs: u32, message_len: u32) -> Energy { Energy { energy: u64::from(num_sigs) * (100 + u64::from(message_len) / 10), diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 3e9ad591..d898505d 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -36,7 +36,7 @@ pub(crate) struct ChainParameters { /// Euro per Energy ratio. // This is not public because we ensure a reasonable value during the construction of the // [`Chain`]. - pub(crate) euro_per_energy: ExchangeRate, + pub(crate) euro_per_energy: ExchangeRate, } /// Represents the blockchain and supports a number of operations, including From adb5c4d4cd5bcb57928fa509312aeea667a83233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Wed, 19 Jul 2023 15:30:17 +0200 Subject: [PATCH 190/208] Fix the order of expected arguments to match the node. --- contract-testing/src/invocation/impls.rs | 11 ++++++----- contract-testing/tests/account-signature-checks.rs | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index cff34bfa..a84eabb4 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -1416,10 +1416,11 @@ fn deserial_signature_and_data_from_contract( // trait. use concordium_base::contracts_common::Deserial; let mut source = concordium_base::contracts_common::Cursor::new(payload); - - let signatures = AccountSignatures::deserial(&mut source)?; let data_len = u32::deserial(&mut source)?; - Ok((signatures, &payload[source.offset..][..data_len as usize])) + let data = &payload[source.offset..][..data_len as usize]; + source.offset += data_len as usize; + let signatures = AccountSignatures::deserial(&mut source)?; + Ok((signatures, data)) } impl ChangeSet { @@ -1505,7 +1506,7 @@ impl ChangeSet { // Then persist all the changes. for (addr, changes) in current.contracts.iter_mut() { - let mut contract = persisted_contracts + let contract = persisted_contracts .get_mut(addr) .expect("Precondition violation: contract must exist"); // Update balance. @@ -1532,7 +1533,7 @@ impl ChangeSet { } // Persist account changes. for (addr, changes) in current.accounts.iter() { - let mut account = persisted_accounts + let account = persisted_accounts .get_mut(addr) .expect("Precondition violation: account must exist"); // Update balance. diff --git a/contract-testing/tests/account-signature-checks.rs b/contract-testing/tests/account-signature-checks.rs index 9688f2c0..bec6951d 100644 --- a/contract-testing/tests/account-signature-checks.rs +++ b/contract-testing/tests/account-signature-checks.rs @@ -118,7 +118,7 @@ fn test() { UpdateContractPayload { address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("contract.check_signature".into()), - message: OwnedParameter::from_serial(&(ACC_0, &signatures, data)) + message: OwnedParameter::from_serial(&(ACC_0, data, &signatures)) .expect("Enough space."), amount: Amount::zero(), }, From 036b2320f2c82d201fb92aab36aae8a6ebb7d200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 21 Aug 2023 12:27:51 +0200 Subject: [PATCH 191/208] Prepare release. --- contract-testing/CHANGELOG.md | 2 ++ contract-testing/Cargo.toml | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index dfa91884..7ae0d3ac 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased changes +## 3.0.0 + - Support protocol 6 semantics, in particular validation assumes protocol 6 semantics now, allowing sign extension instructions and new operations for querying account's public keys, and checking signatures with account's keys. diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index bb80b4af..8b4e6d22 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-smart-contract-testing" -version = "2.0.0" +version = "3.0.0" edition = "2021" rust-version = "1.65" license = "MPL-2.0" @@ -13,9 +13,9 @@ exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {version = "2.0", path = "../concordium-base/rust-src/concordium_base"} -concordium-smart-contract-engine = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} -concordium-wasm = {version = "2.0", path = "../concordium-base/smart-contracts/wasm-transform"} +concordium_base = {version = "3.0", path = "../concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {version = "3.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {version = "3.0", path = "../concordium-base/smart-contracts/wasm-transform"} sha2 = "0.10" anyhow = "1" thiserror = "1.0" From 2777725b2265acb8167b317c73954553c4b8b3fb Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 12 Sep 2023 10:44:20 +0200 Subject: [PATCH 192/208] Add rust-sdk submodule and remove base submodule The rust-sdk already has base as a submodule. --- contract-testing/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index ae9e1dc9..ad38e3fd 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -13,10 +13,10 @@ exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {version = "3.0", path = "../concordium-base/rust-src/concordium_base"} -concordium-smart-contract-engine = {version = "3.0", path = "../concordium-base/smart-contracts/wasm-chain-integration"} -concordium-wasm = {version = "3.0", path = "../concordium-base/smart-contracts/wasm-transform"} -concordium-rust-sdk = "3.0" +concordium_base = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/smart-contracts/wasm-chain-integration"} +concordium-wasm = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/smart-contracts/wasm-transform"} +concordium-rust-sdk = {version = "3.0", path = "../concordium-rust-sdk"} tokio = { version = "1.28", features = ["rt-multi-thread", "time"] } sha2 = "0.10" anyhow = "1" From 60fce85dcd7d4b07d1ebe36656ad97103a739353 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 12 Sep 2023 10:47:47 +0200 Subject: [PATCH 193/208] Stop referring to delete constant The constant was deleted because the node behaves differently now and does not have a fixed maximum of 10 billion energy. But it still makes sense to check that the configured values are reasonable for testing to avoid overflows when executing contracts. --- contract-testing/src/impls.rs | 9 +++++---- contract-testing/src/types.rs | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 2a9b3287..e2b1d06c 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -70,7 +70,7 @@ impl ChainParameters { /// parameters are provided. /// /// Returns an error if the exchange rates provided makes one energy cost - /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. + /// more than `u64::MAX / 100_000_000_000`. pub fn new_with_time_and_rates( block_time: SlotTime, micro_ccd_per_euro: ExchangeRate, @@ -397,7 +397,7 @@ impl Chain { /// provided. /// /// Returns an error if the exchange rates provided makes one energy cost - /// more than `u64::MAX / ` [`MAX_ALLOWED_INVOKE_ENERGY`]. + /// more than `u64::MAX / 100_000_000_000`. /// /// *For more configuration options and flexibility, use the builder /// pattern. See [`Chain::builder`].* @@ -1370,7 +1370,7 @@ impl Chain { /// Try to set the exchange rates on the chain. /// /// Will fail if they result in the cost of one energy being larger than - /// `u64::MAX / MAX_ALLOWED_INVOKE_ENERGY`. + /// `u64::MAX / 100_000_000_000`. pub fn set_exchange_rates( &mut self, micro_ccd_per_euro: ExchangeRate, @@ -1848,7 +1848,8 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// ``` /// /// To convert the `energy` parameter to mCCD (the vertical lines represent -/// ceiling): ```markdown +/// ceiling): +/// ```markdown /// ⌈ mCCD ⌉ ⌈ NRG * mCCD ⌉ /// | NRG * ---- | = | ---------- | = mCCD /// | NRG | | NRG | diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index 29310ff4..65e62c9d 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -723,7 +723,7 @@ pub struct AccountDoesNotExist { /// The provided exchange rates are not valid. /// Meaning that they do not correspond to one energy costing less than -/// `u64::MAX / ` [`concordium_base::constants::MAX_ALLOWED_INVOKE_ENERGY`]`. +/// `u64::MAX / 100_000_000_000`. #[derive(Debug, Error)] #[error("An exchange rate was too high.")] pub struct ExchangeRateError; @@ -791,8 +791,7 @@ pub struct ExternalNodeNotConfigured; pub enum ChainBuilderError { /// The provided exchange rates are not valid. /// Meaning that they do not correspond to one energy costing less than - /// `u64::MAX / ` - /// [`concordium_base::constants::MAX_ALLOWED_INVOKE_ENERGY`]`. + /// `u64::MAX / 100_000_000_000`. #[error("An exchange rate was too high.")] ExchangeRateError, /// An error occurred while setting up the connection to an external node. From d7c4489df56b2d7a99ef886b68313a7e620b718f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 12 Sep 2023 11:14:46 +0200 Subject: [PATCH 194/208] Fix fmt and clippy warnings --- contract-testing/Cargo.toml | 1 + contract-testing/src/impls.rs | 4 ++++ contract-testing/src/lib.rs | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index ad38e3fd..72879168 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -23,6 +23,7 @@ anyhow = "1" thiserror = "1.0" num-bigint = "0.4" num-integer = "0.1" +toml_edit = "=0.19.14" # Fix the version to .14, as .15 has MSRV 1.66. Remove this line when upgrading out MSRV. [dev-dependencies] rand = "0.7" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index e2b1d06c..617533e1 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -386,6 +386,10 @@ impl ChainBuilder { } } +impl Default for ChainBuilder { + fn default() -> Self { Self::new() } +} + impl Chain { /// Get a [`ChainBuilder`] for constructing a new [`Chain`] with a builder /// pattern. diff --git a/contract-testing/src/lib.rs b/contract-testing/src/lib.rs index e2b2ee97..a1005b97 100644 --- a/contract-testing/src/lib.rs +++ b/contract-testing/src/lib.rs @@ -98,9 +98,9 @@ pub use concordium_base::{ common::types::{CredentialIndex, KeyIndex}, contracts_common::{ from_bytes, to_bytes, AccountAddress, AccountBalance, AccountThreshold, Address, Amount, - ContractAddress, Duration, Timestamp, ContractName, EntrypointName, ExchangeRate, ModuleReference, + ContractAddress, ContractName, Duration, EntrypointName, ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedParameter, OwnedReceiveName, Parameter, - ReceiveName, SignatureThreshold, SlotTime, + ReceiveName, SignatureThreshold, SlotTime, Timestamp, }, hashes::BlockHash, id::types::{CredentialPublicKeys, VerifyKey}, From 94650fc7b68f6e1a13fa62968abd8867e9569a6f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 12 Sep 2023 14:03:53 +0200 Subject: [PATCH 195/208] Fix test warnings --- .../tests/account-signature-checks.rs | 29 +-- .../tests/all_new_host_functions.rs | 10 +- contract-testing/tests/basics.rs | 23 +- contract-testing/tests/checkpointing.rs | 65 +++--- contract-testing/tests/counter.rs | 24 +- contract-testing/tests/error_codes.rs | 36 ++- contract-testing/tests/fallback.rs | 22 +- contract-testing/tests/helpers/mod.rs | 25 +++ contract-testing/tests/iterator.rs | 20 +- contract-testing/tests/queries.rs | 210 +++++++++--------- contract-testing/tests/recorder.rs | 20 +- .../tests/relaxed_restrictions.rs | 32 ++- contract-testing/tests/self_balance.rs | 23 +- contract-testing/tests/transfer.rs | 34 ++- contract-testing/tests/upgrades.rs | 173 +++++++-------- 15 files changed, 365 insertions(+), 381 deletions(-) create mode 100644 contract-testing/tests/helpers/mod.rs diff --git a/contract-testing/tests/account-signature-checks.rs b/contract-testing/tests/account-signature-checks.rs index bec6951d..0dc3b2c8 100644 --- a/contract-testing/tests/account-signature-checks.rs +++ b/contract-testing/tests/account-signature-checks.rs @@ -6,9 +6,7 @@ use concordium_base::{ transactions::AccountAccessStructure, }; use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; /// Test that we can query the correct account keys. #[test] @@ -44,7 +42,7 @@ fn test() { ); let acc_structure: AccountAccessStructure = (&acc_keys).into(); chain.create_account(Account::new_with_keys( - ACC_0, + helpers::ACC_0, AccountBalance { total: initial_balance, staked: Amount::zero(), @@ -56,19 +54,16 @@ fn test() { let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/account-signature-checks.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("account-signature-checks.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), @@ -82,13 +77,13 @@ fn test() { let res_invoke_get_keys = chain .contract_invoke( - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("contract.get_keys".into()), - message: OwnedParameter::from_serial(&ACC_0) + message: OwnedParameter::from_serial(&helpers::ACC_0) .expect("Parameter has valid size"), amount: Amount::zero(), }, @@ -112,13 +107,13 @@ fn test() { let res_invoke_check_signature = chain .contract_invoke( - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, receive_name: OwnedReceiveName::new_unchecked("contract.check_signature".into()), - message: OwnedParameter::from_serial(&(ACC_0, data, &signatures)) + message: OwnedParameter::from_serial(&(helpers::ACC_0, data, &signatures)) .expect("Enough space."), amount: Amount::zero(), }, diff --git a/contract-testing/tests/all_new_host_functions.rs b/contract-testing/tests/all_new_host_functions.rs index 8a67c7d9..c523ddb0 100644 --- a/contract-testing/tests/all_new_host_functions.rs +++ b/contract-testing/tests/all_new_host_functions.rs @@ -4,21 +4,19 @@ //! they require more complex interactions with the chain. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_all_new_host_functions() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/all-new-host-functions.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("all-new-host-functions.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index 25c0690f..7f0e526b 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -1,9 +1,7 @@ //! This module contains tests that test various basic things, such as state //! reentry and energy usage and amounts charged. use concordium_smart_contract_testing::*; - -const ACC_0: AccountAddress = AccountAddress([0; 32]); -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; +mod helpers; #[test] fn basics() { @@ -16,21 +14,20 @@ fn basics() { .expect("Values known to be in range."); let initial_balance = Amount::from_ccd(100_000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let deployment = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/fib.wasm", WASM_TEST_FOLDER)) - .expect("Module should exist."), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("fib.wasm")).expect("Module should exist."), ) .expect("Deploying valid module should work"); let init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { amount: Amount::zero(), @@ -44,8 +41,8 @@ fn basics() { let update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { amount: Amount::zero(), @@ -58,8 +55,8 @@ fn basics() { let view = chain .contract_invoke( - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -81,7 +78,7 @@ fn basics() { // Check that the account was correctly charged for all transactions. // This also asserts that the account wasn't charged for the invoke. assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some( initial_balance - deployment.transaction_fee diff --git a/contract-testing/tests/checkpointing.rs b/contract-testing/tests/checkpointing.rs index 197ed5c5..db096b9e 100644 --- a/contract-testing/tests/checkpointing.rs +++ b/contract-testing/tests/checkpointing.rs @@ -5,10 +5,7 @@ //! made must be rolled back. That is also the case if a nested contract call //! fails. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); -const ACC_1: AccountAddress = AccountAddress([1; 32]); +mod helpers; /// This test has the following call pattern: /// A @@ -24,13 +21,13 @@ const ACC_1: AccountAddress = AccountAddress([1; 32]); fn test_case_1() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("checkpointing.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -38,7 +35,7 @@ fn test_case_1() { let res_init_a = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -52,7 +49,7 @@ fn test_case_1() { let res_init_b = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -81,8 +78,8 @@ fn test_case_1() { let update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_a.contract_address, @@ -142,13 +139,13 @@ fn test_case_1() { fn test_case_2() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("checkpointing.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -156,7 +153,7 @@ fn test_case_2() { let res_init_a = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -170,7 +167,7 @@ fn test_case_2() { let res_init_b = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -199,8 +196,8 @@ fn test_case_2() { let trace = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_a.contract_address, @@ -248,14 +245,14 @@ fn test_case_2() { fn test_case_3() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); - chain.create_account(Account::new(ACC_1, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("checkpointing.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -263,7 +260,7 @@ fn test_case_3() { let res_init_a = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -277,7 +274,7 @@ fn test_case_3() { chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -291,13 +288,13 @@ fn test_case_3() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_a.contract_address, receive_name: OwnedReceiveName::new_unchecked("a.a_modify_proxy".into()), - message: OwnedParameter::from_serial(&ACC_1) + message: OwnedParameter::from_serial(&helpers::ACC_1) .expect("Parameter has valid size"), // We supply three micro CCDs as we're instructing the contract to carry out a // transfer instead of a call. See the contract for @@ -322,13 +319,13 @@ fn test_case_3() { fn test_case_4() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/checkpointing.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("checkpointing.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -336,7 +333,7 @@ fn test_case_4() { let res_init_a = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -350,7 +347,7 @@ fn test_case_4() { let res_init_b = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -379,8 +376,8 @@ fn test_case_4() { let update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_a.contract_address, diff --git a/contract-testing/tests/counter.rs b/contract-testing/tests/counter.rs index 4f6d4b16..c6b9845e 100644 --- a/contract-testing/tests/counter.rs +++ b/contract-testing/tests/counter.rs @@ -3,21 +3,19 @@ //! 64-bit counter in its state. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_counter() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/call-counter.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("call-counter.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -25,7 +23,7 @@ fn test_counter() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -39,8 +37,8 @@ fn test_counter() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -55,8 +53,8 @@ fn test_counter() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -77,8 +75,8 @@ fn test_counter() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, diff --git a/contract-testing/tests/error_codes.rs b/contract-testing/tests/error_codes.rs index 27356061..64eeff82 100644 --- a/contract-testing/tests/error_codes.rs +++ b/contract-testing/tests/error_codes.rs @@ -3,21 +3,19 @@ //! contract. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_error_codes() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/caller.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("caller.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -25,7 +23,7 @@ fn test_error_codes() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -54,8 +52,8 @@ fn test_error_codes() { let res_update_0 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -88,8 +86,8 @@ fn test_error_codes() { let res_update_1 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -120,8 +118,8 @@ fn test_error_codes() { let res_update_2 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -154,8 +152,8 @@ fn test_error_codes() { let res_update_3 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -188,8 +186,8 @@ fn test_error_codes() { let res_update_4 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -225,8 +223,8 @@ fn test_error_codes() { let res_update_6 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, diff --git a/contract-testing/tests/fallback.rs b/contract-testing/tests/fallback.rs index 61e9b6af..34e9c2c0 100644 --- a/contract-testing/tests/fallback.rs +++ b/contract-testing/tests/fallback.rs @@ -1,21 +1,19 @@ //! Tests for the contract default method/fallback functionality. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_fallback() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/fallback.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("fallback.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -23,7 +21,7 @@ fn test_fallback() { let res_init_two = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -37,7 +35,7 @@ fn test_fallback() { let res_init_one = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -55,8 +53,8 @@ fn test_fallback() { // and the fallback will try to look up a non-existing return value. let res_invoke_1 = chain .contract_invoke( - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_one.contract_address, @@ -78,8 +76,8 @@ fn test_fallback() { let parameter = OwnedParameter::from_serial(&"ASDF").expect("Parameter has valid size."); let res_invoke_2 = chain .contract_invoke( - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init_one.contract_address, diff --git a/contract-testing/tests/helpers/mod.rs b/contract-testing/tests/helpers/mod.rs new file mode 100644 index 00000000..f5a92ac5 --- /dev/null +++ b/contract-testing/tests/helpers/mod.rs @@ -0,0 +1,25 @@ +//! Some helpers and constants that are used in most or all of the tests in this +//! folder. +use concordium_smart_contract_testing::*; + +/// Relative path to the wasm test contracts. +pub(crate) const WASM_TEST_FOLDER: &str = + "../concordium-rust-sdk/concordium-base/smart-contracts/testdata/contracts/v1"; + +/// Test account 0. +pub(crate) const ACC_0: AccountAddress = AccountAddress([0; 32]); + +/// Test account 1. +/// Dead code is allowed to avoid a warning when running `cargo test`. +/// `cargo test` compiles each test module independently and for the ones that +/// do not use `ACC_1` a warning is produced. +#[allow(dead_code)] +pub(crate) const ACC_1: AccountAddress = AccountAddress([1; 32]); + +/// Get the path to a wasm test file in wasm test folder. +/// +/// This is simply prepends the test folder path to the file name: +/// `WASM_TEST_FOLDER/{file_name}`. +pub(crate) fn wasm_test_file(file_name: &str) -> String { + format!("{WASM_TEST_FOLDER}/{file_name}") +} diff --git a/contract-testing/tests/iterator.rs b/contract-testing/tests/iterator.rs index 941af2c4..957a1926 100644 --- a/contract-testing/tests/iterator.rs +++ b/contract-testing/tests/iterator.rs @@ -5,21 +5,19 @@ //! wrt. the state etc. after execution etc. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_iterator() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/iterator.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("iterator.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -27,7 +25,7 @@ fn test_iterator() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -41,8 +39,8 @@ fn test_iterator() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -55,8 +53,8 @@ fn test_iterator() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, diff --git a/contract-testing/tests/queries.rs b/contract-testing/tests/queries.rs index bf655cd6..3ae8e61c 100644 --- a/contract-testing/tests/queries.rs +++ b/contract-testing/tests/queries.rs @@ -6,10 +6,7 @@ //! - the balances of an account, //! - the exhange rates. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); -const ACC_1: AccountAddress = AccountAddress([1; 32]); +mod helpers; mod query_account_balance { use super::*; @@ -20,14 +17,14 @@ mod query_account_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - chain.create_account(Account::new(ACC_1, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-account-balance.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -35,7 +32,7 @@ mod query_account_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -46,15 +43,20 @@ mod query_account_balance { ) .expect("Initializing valid contract should work"); - // The contract will query the balance of ACC_1 and assert that the three - // balances match this input. - let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); + // The contract will query the balance of helpers::ACC_1 and assert that the + // three balances match this input. + let input_param = ( + helpers::ACC_1, + initial_balance, + Amount::zero(), + Amount::zero(), + ); let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -67,7 +69,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -88,14 +90,14 @@ mod query_account_balance { fn invoker_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - chain.create_account(Account::new(ACC_1, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-account-balance.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -103,7 +105,7 @@ mod query_account_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -118,16 +120,21 @@ mod query_account_balance { let energy_limit = Energy::from(100000); let invoker_reserved_amount = update_amount + chain.calculate_energy_cost(energy_limit); - // The contract will query the balance of ACC_1, which is also the invoker, and - // assert that the three balances match this input. + // The contract will query the balance of helpers::ACC_1, which is also the + // invoker, and assert that the three balances match this input. let expected_balance = initial_balance - invoker_reserved_amount; - let input_param = (ACC_1, expected_balance, Amount::zero(), Amount::zero()); + let input_param = ( + helpers::ACC_1, + expected_balance, + Amount::zero(), + Amount::zero(), + ); let res_update = chain .contract_update( Signer::with_one_key(), - ACC_1, - Address::Account(ACC_1), + helpers::ACC_1, + Address::Account(helpers::ACC_1), energy_limit, UpdateContractPayload { address: res_init.contract_address, @@ -140,11 +147,11 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some(initial_balance - res_deploy.transaction_fee - res_init.transaction_fee) ); assert_eq!( - chain.account_balance_available(ACC_1), + chain.account_balance_available(helpers::ACC_1), // Differs from `expected_balance` as it only includes the actual amount charged // for the NRG use. Not the reserved amount. Some(initial_balance - res_update.transaction_fee - update_amount) @@ -161,16 +168,15 @@ mod query_account_balance { fn transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - chain.create_account(Account::new(ACC_1, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-account-balance-transfer.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "queries-account-balance-transfer.wasm", )) .expect("module should exist"), ) @@ -181,7 +187,7 @@ mod query_account_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -195,7 +201,7 @@ mod query_account_balance { let amount_to_send = Amount::from_ccd(123); let expected_balance = initial_balance + amount_to_send; let input_param = ( - ACC_1, + helpers::ACC_1, amount_to_send, expected_balance, Amount::zero(), @@ -205,8 +211,8 @@ mod query_account_balance { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: res_init.contract_address, @@ -219,7 +225,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -229,7 +235,7 @@ mod query_account_balance { ) ); assert_eq!( - chain.account_balance_available(ACC_1), + chain.account_balance_available(helpers::ACC_1), Some(initial_balance + amount_to_send) ); assert!(matches!( @@ -247,14 +253,14 @@ mod query_account_balance { fn balance_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); - chain.create_account(Account::new(ACC_1, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_1, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/queries-account-balance.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-account-balance.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -262,7 +268,7 @@ mod query_account_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -273,15 +279,20 @@ mod query_account_balance { ) .expect("Initializing valid contract should work"); - // The contract will query the balance of ACC_1 and assert that the three - // balances match this input. - let input_param = (ACC_1, initial_balance, Amount::zero(), Amount::zero()); + // The contract will query the balance of helpers::ACC_1 and assert that the + // three balances match this input. + let input_param = ( + helpers::ACC_1, + initial_balance, + Amount::zero(), + Amount::zero(), + ); let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -294,7 +305,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -314,15 +325,14 @@ mod query_account_balance { fn missing_account_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-account-balance-missing-account.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "queries-account-balance-missing-account.wasm", )) .expect("module should exist"), ) @@ -331,7 +341,7 @@ mod query_account_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -343,13 +353,13 @@ mod query_account_balance { .expect("Initializing valid contract should work"); // The account to query, which doesn't exist in this test case. - let input_param = ACC_1; + let input_param = helpers::ACC_1; let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -362,7 +372,7 @@ mod query_account_balance { .expect("Updating valid contract should work"); assert_eq!( - chain.account_balance_available(ACC_0), + chain.account_balance_available(helpers::ACC_0), Some( initial_balance - res_deploy.transaction_fee @@ -386,26 +396,23 @@ mod query_contract_balance { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-contract-balance.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-contract-balance.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -419,7 +426,7 @@ mod query_contract_balance { let res_init_other = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -436,8 +443,8 @@ mod query_contract_balance { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -461,7 +468,7 @@ mod query_contract_balance { fn query_self_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -469,19 +476,16 @@ mod query_contract_balance { let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-contract-balance.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-contract-balance.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -498,8 +502,8 @@ mod query_contract_balance { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -522,7 +526,7 @@ mod query_contract_balance { fn query_self_after_transfer_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let init_amount = Amount::from_ccd(123); let update_amount = Amount::from_ccd(456); @@ -531,10 +535,9 @@ mod query_contract_balance { let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-contract-balance-transfer.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "queries-contract-balance-transfer.wasm", )) .expect("module should exist"), ) @@ -543,7 +546,7 @@ mod query_contract_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -555,7 +558,7 @@ mod query_contract_balance { .expect("Initializing valid contract should work"); let input_param = ( - ACC_0, + helpers::ACC_0, transfer_amount, res_init.contract_address, init_amount + update_amount - transfer_amount, @@ -564,8 +567,8 @@ mod query_contract_balance { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -593,15 +596,14 @@ mod query_contract_balance { fn missing_contract_test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/queries-contract-balance-missing-contract.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "queries-contract-balance-missing-contract.wasm", )) .expect("module should exist"), ) @@ -610,7 +612,7 @@ mod query_contract_balance { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -627,8 +629,8 @@ mod query_contract_balance { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -656,13 +658,13 @@ mod query_exchange_rates { fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/queries-exchange-rates.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("queries-exchange-rates.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -670,7 +672,7 @@ mod query_exchange_rates { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -687,8 +689,8 @@ mod query_exchange_rates { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, diff --git a/contract-testing/tests/recorder.rs b/contract-testing/tests/recorder.rs index 8142cd54..9b70afa5 100644 --- a/contract-testing/tests/recorder.rs +++ b/contract-testing/tests/recorder.rs @@ -1,21 +1,19 @@ //! This module tests basic V1 state operations with the recorder contract. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_recorder() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/record-parameters.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("record-parameters.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -23,7 +21,7 @@ fn test_recorder() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -37,8 +35,8 @@ fn test_recorder() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -52,8 +50,8 @@ fn test_recorder() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, diff --git a/contract-testing/tests/relaxed_restrictions.rs b/contract-testing/tests/relaxed_restrictions.rs index 6b636960..ef841076 100644 --- a/contract-testing/tests/relaxed_restrictions.rs +++ b/contract-testing/tests/relaxed_restrictions.rs @@ -12,24 +12,22 @@ //! - Of size <= 1kb: base cost + 1NRG / 1 *kilobyte* (same as before P5) //! - Of size > 1 kb: base cost + 1NRG / 1 *byte* use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; /// Test the new parameter size limit on both init and update. #[test] fn test_new_parameter_limit() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let parameter = mk_parameter(65535, 65535); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("relaxed-restrictions.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -37,7 +35,7 @@ fn test_new_parameter_limit() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(80000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -51,8 +49,8 @@ fn test_new_parameter_limit() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(700000), UpdateContractPayload { address: res_init.contract_address, @@ -72,8 +70,8 @@ fn test_new_return_value_limit() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -94,8 +92,8 @@ fn test_new_log_limit() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -113,13 +111,13 @@ fn test_new_log_limit() { fn deploy_and_init() -> (Chain, ContractAddress) { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/relaxed-restrictions.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("relaxed-restrictions.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -127,7 +125,7 @@ fn deploy_and_init() -> (Chain, ContractAddress) { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, diff --git a/contract-testing/tests/self_balance.rs b/contract-testing/tests/self_balance.rs index be658623..5ae74d7d 100644 --- a/contract-testing/tests/self_balance.rs +++ b/contract-testing/tests/self_balance.rs @@ -4,9 +4,7 @@ //! See more details about the specific test inside the `self-balance.wat` and //! `self-blaance-nested.wat` files. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; /// Invoke an entrypoint and transfer to ourselves. /// The before and after self-balances are the same. @@ -23,8 +21,8 @@ fn test_invoke_1() { ); let result = chain.contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -53,7 +51,7 @@ fn test_invoke_2() { let res_init_another = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref, @@ -73,8 +71,8 @@ fn test_invoke_2() { ); let result = chain.contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: self_address, @@ -99,21 +97,20 @@ fn deploy_and_init( ) -> (Chain, ContractAddress, ModuleReference) { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/{}", WASM_TEST_FOLDER, file_name)) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file(file_name)).expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, diff --git a/contract-testing/tests/transfer.rs b/contract-testing/tests/transfer.rs index 4122874a..428b050f 100644 --- a/contract-testing/tests/transfer.rs +++ b/contract-testing/tests/transfer.rs @@ -1,21 +1,19 @@ //! This module contains tests for transfers fr&om a contract to an account. //! See more details about the specific test inside the `transfer.wat` file. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; #[test] fn test_transfer() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(10000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/transfer.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("transfer.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -23,7 +21,7 @@ fn test_transfer() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -39,13 +37,13 @@ fn test_transfer() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, receive_name: OwnedReceiveName::new_unchecked("transfer.forward".into()), - message: OwnedParameter::from_serial(&ACC_0) + message: OwnedParameter::from_serial(&helpers::ACC_0) .expect("Parameter has valid size"), amount: Amount::from_micro_ccd(123), }, @@ -61,8 +59,8 @@ fn test_transfer() { chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -73,15 +71,15 @@ fn test_transfer() { ) .expect("Updating contract should succeed"); - // Tell it to send 17 mCCD to ACC_0. - let parameter = OwnedParameter::from_serial(&(ACC_0, Amount::from_micro_ccd(17))) + // Tell it to send 17 mCCD to helpers::ACC_0. + let parameter = OwnedParameter::from_serial(&(helpers::ACC_0, Amount::from_micro_ccd(17))) .expect("Parameter has valid size"); let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -104,7 +102,7 @@ fn test_transfer() { ContractTraceElement::Transferred { from: contract_address, amount: Amount::from_micro_ccd(17), - to: ACC_0, + to: helpers::ACC_0, }, ContractTraceElement::Resumed { address: contract_address, @@ -116,7 +114,7 @@ fn test_transfer() { amount: Amount::zero(), receive_name: OwnedReceiveName::new_unchecked("transfer.send".into()), contract_version: concordium_base::smart_contracts::WasmVersion::V1, - instigator: Address::Account(ACC_0), + instigator: Address::Account(helpers::ACC_0), message: parameter, events: Vec::new(), }, diff --git a/contract-testing/tests/upgrades.rs b/contract-testing/tests/upgrades.rs index d1181494..bd9d4328 100644 --- a/contract-testing/tests/upgrades.rs +++ b/contract-testing/tests/upgrades.rs @@ -1,9 +1,7 @@ //! This module contains tests for the native smart contract upgrade //! functionality. use concordium_smart_contract_testing::*; - -const WASM_TEST_FOLDER: &str = "../concordium-base/smart-contracts/testdata/contracts/v1"; -const ACC_0: AccountAddress = AccountAddress([0; 32]); +mod helpers; /// Test a basic upgrade, ensuring that the new module is in place by /// checking the available entrypoints. @@ -11,14 +9,14 @@ const ACC_0: AccountAddress = AccountAddress([0; 32]); fn test() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); // Deploy the two modules `upgrading_0`, `upgrading_1` let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading_0.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading_0.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -26,8 +24,8 @@ fn test() { let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading_1.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading_1.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -36,7 +34,7 @@ fn test() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_a".into()), @@ -53,8 +51,8 @@ fn test() { let res_update_upgrade = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -70,8 +68,8 @@ fn test() { let res_update_new = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -104,21 +102,21 @@ fn test() { fn test_self_invoke() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-self-invoke0.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-self-invoke0.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-self-invoke1.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-self-invoke1.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -126,7 +124,7 @@ fn test_self_invoke() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), @@ -140,8 +138,8 @@ fn test_self_invoke() { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -181,24 +179,21 @@ fn test_self_invoke() { fn test_missing_module() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/upgrading-missing-module.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-missing-module.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -212,8 +207,8 @@ fn test_missing_module() { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -239,36 +234,30 @@ fn test_missing_module() { fn test_missing_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/upgrading-missing-contract0.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-missing-contract0.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/upgrading-missing-contract1.wasm", - WASM_TEST_FOLDER - )) - .expect("module should exist"), + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-missing-contract1.wasm")) + .expect("module should exist"), ) .expect("Deploying valid module should work"); let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), @@ -283,8 +272,8 @@ fn test_missing_contract() { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -310,13 +299,13 @@ fn test_missing_contract() { fn test_twice_in_one_transaction() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-twice0.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-twice0.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -324,8 +313,8 @@ fn test_twice_in_one_transaction() { let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-twice1.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-twice1.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -333,8 +322,8 @@ fn test_twice_in_one_transaction() { let res_deploy_2 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-twice2.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-twice2.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -342,7 +331,7 @@ fn test_twice_in_one_transaction() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), @@ -359,8 +348,8 @@ fn test_twice_in_one_transaction() { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(100000), UpdateContractPayload { address: res_init.contract_address, @@ -408,13 +397,13 @@ fn test_twice_in_one_transaction() { fn test_chained_contract() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-chained0.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-chained0.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -422,7 +411,7 @@ fn test_chained_contract() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy.module_reference, @@ -441,8 +430,8 @@ fn test_chained_contract() { let res_update = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1_000_000_000), UpdateContractPayload { address: res_init.contract_address, @@ -470,13 +459,13 @@ fn test_chained_contract() { fn test_reject() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-reject0.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-reject0.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -484,8 +473,8 @@ fn test_reject() { let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!("{}/upgrading-reject1.wasm", WASM_TEST_FOLDER)) + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file("upgrading-reject1.wasm")) .expect("module should exist"), ) .expect("Deploying valid module should work"); @@ -493,7 +482,7 @@ fn test_reject() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { mod_ref: res_deploy_0.module_reference, @@ -507,8 +496,8 @@ fn test_reject() { let res_update_upgrade = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -523,8 +512,8 @@ fn test_reject() { let res_update_new_feature = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -561,15 +550,14 @@ fn test_reject() { fn test_changing_entrypoint() { let mut chain = Chain::new(); let initial_balance = Amount::from_ccd(1000000); - chain.create_account(Account::new(ACC_0, initial_balance)); + chain.create_account(Account::new(helpers::ACC_0, initial_balance)); let res_deploy_0 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/upgrading-changing-entrypoints0.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "upgrading-changing-entrypoints0.wasm", )) .expect("module should exist"), ) @@ -578,10 +566,9 @@ fn test_changing_entrypoint() { let res_deploy_1 = chain .module_deploy_v1( Signer::with_one_key(), - ACC_0, - module_load_v1_raw(format!( - "{}/upgrading-changing-entrypoints1.wasm", - WASM_TEST_FOLDER + helpers::ACC_0, + module_load_v1_raw(helpers::wasm_test_file( + "upgrading-changing-entrypoints1.wasm", )) .expect("module should exist"), ) @@ -590,7 +577,7 @@ fn test_changing_entrypoint() { let res_init = chain .contract_init( Signer::with_one_key(), - ACC_0, + helpers::ACC_0, Energy::from(10000), InitContractPayload { init_name: OwnedContractName::new_unchecked("init_contract".into()), @@ -605,8 +592,8 @@ fn test_changing_entrypoint() { let res_update_old_feature_0 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -620,8 +607,8 @@ fn test_changing_entrypoint() { let res_update_new_feature_0 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -635,8 +622,8 @@ fn test_changing_entrypoint() { let res_update_upgrade = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -651,8 +638,8 @@ fn test_changing_entrypoint() { let res_update_old_feature_1 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, @@ -666,8 +653,8 @@ fn test_changing_entrypoint() { let res_update_new_feature_1 = chain .contract_update( Signer::with_one_key(), - ACC_0, - Address::Account(ACC_0), + helpers::ACC_0, + Address::Account(helpers::ACC_0), Energy::from(1000000), UpdateContractPayload { address: res_init.contract_address, From f82b17f03fec150d39b4559508a9c7ef458acae5 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Tue, 12 Sep 2023 14:26:19 +0200 Subject: [PATCH 196/208] Take ownership to match convention of `to_*` methods --- contract-testing/src/types.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index a32922ec..93fdaf48 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -921,13 +921,13 @@ impl ExternalAddress { /// /// This is an internal method instead of a [`From`] implementation, as it /// should be difficult to conflate external and regular addresses. - pub(crate) fn to_address(&self) -> Address { + pub(crate) fn to_address(self) -> Address { match self { ExternalAddress::Account(ExternalAccountAddress { address }) => { - Address::Account(*address) + Address::Account(address) } ExternalAddress::Contract(ExternalContractAddress { address }) => { - Address::Contract(*address) + Address::Contract(address) } } } From 92808d604f3536c4b3ea2d028f86b05c0f42956e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 13 Sep 2023 15:51:58 +0200 Subject: [PATCH 197/208] Bump MSRV to `1.66` and use immutable references --- contract-testing/CHANGELOG.md | 1 + contract-testing/Cargo.toml | 3 +-- contract-testing/src/impls.rs | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 691fc30a..3fa10973 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -13,6 +13,7 @@ - `Timestamp` which `SlotTime` is an alias of. - `Duration` - `Endpoint` +- Bump minimum supported Rust version to `1.66`. ## 3.0.0 diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 72879168..f4bf3fc8 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -2,7 +2,7 @@ name = "concordium-smart-contract-testing" version = "3.0.0" edition = "2021" -rust-version = "1.65" +rust-version = "1.66" license = "MPL-2.0" readme = "README.md" description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." @@ -23,7 +23,6 @@ anyhow = "1" thiserror = "1.0" num-bigint = "0.4" num-integer = "0.1" -toml_edit = "=0.19.14" # Fix the version to .14, as .15 has MSRV 1.66. Remove this line when upgrading out MSRV. [dev-dependencies] rand = "0.7" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 617533e1..fbb89f79 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1389,8 +1389,8 @@ impl Chain { /// Get the microCCD per euro and euro per energy exchange rates by querying /// an external node using the external query block. - fn get_exchange_rates_via_external_node(&mut self) -> Result { - let connection = self.external_node_connection_mut()?; + fn get_exchange_rates_via_external_node(&self) -> Result { + let connection = self.external_node_connection()?; // Get the values from the external node. connection.with_client(None, |block_identifier, mut client| async move { @@ -1602,8 +1602,10 @@ impl ExternalNodeConnection { /// If the task takes longer than [`EXTERNAL_NODE_TIMEOUT_DURATION`] then /// the connection times out and an [`Err(ExternalNodeError::Timeout)`] is /// returned. + /// + /// *This method cannot be nested, as that will cause a panic.* fn with_client( - &mut self, + &self, block: Option, f: F, ) -> Result From 2e0c53ba191c296e553f5d7c68552619f2ad700e Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Wed, 13 Sep 2023 15:56:22 +0200 Subject: [PATCH 198/208] Use correct reference type --- contract-testing/src/impls.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 2b6b226b..313be6ae 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1254,13 +1254,13 @@ impl Chain { /// ); /// ``` pub fn contract_invoke_external( - &mut self, + &self, sender: Option, energy_reserved: Energy, payload: InvokeExternalContractPayload, block: Option, ) -> Result { - let connection = self.external_node_connection_mut().unwrap(); + let connection = self.external_node_connection().unwrap(); // Make the invocation. let invoke_result: InvokeContractResult = @@ -1392,7 +1392,6 @@ impl Chain { Ok::<_, ExternalNodeError>(ExternalContractAddress { address }) })?; - // TODO: Mention that this is idempotent. connection.contracts.insert(external_addr); Ok(external_addr) From 3c73241616e88f220cfdfcc0de344de47b17dd3b Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 14 Sep 2023 09:39:12 +0200 Subject: [PATCH 199/208] Make small clarifications --- contract-testing/src/impls.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index fbb89f79..d1c16d69 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -178,7 +178,7 @@ impl ChainBuilder { self } - /// Configure the exchange rate between microCCD and euro. + /// Configure the 'microCCD per euro' exchange rate. /// /// By default the rate is `50000 / 1`. /// @@ -198,7 +198,7 @@ impl ChainBuilder { self } - /// Configure the exchange rate between microCCD and euro. + /// Configure the 'euro per energy' exchange rate. /// /// By default the rate is `1 / 50000`. /// @@ -368,13 +368,13 @@ impl ChainBuilder { match (self.block_time, self.block_time_from_external) { (Some(_), true) => return Err(ChainBuilderError::ConflictingBlockTime), - (Some(block_time), _) => { + (Some(block_time), false) => { chain.parameters.block_time = block_time; } - (_, true) => { + (None, true) => { chain.set_block_time_via_external_node()?; } - _ => (), + (None, false) => (), } // Replace the default block time if provided. From ed3749a9f5feb9f346cf919c7167575d3095e1ac Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Thu, 14 Sep 2023 09:41:44 +0200 Subject: [PATCH 200/208] Align lines --- contract-testing/src/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index d1c16d69..34ef0be4 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -1856,7 +1856,7 @@ pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { /// To convert the `energy` parameter to mCCD (the vertical lines represent /// ceiling): /// ```markdown -/// ⌈ mCCD ⌉ ⌈ NRG * mCCD ⌉ +/// ⌈ mCCD ⌉ ⌈ NRG * mCCD ⌉ /// | NRG * ---- | = | ---------- | = mCCD /// | NRG | | NRG | /// ``` From a3a664c112888821f20082fec744e664806d665f Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 09:31:02 +0200 Subject: [PATCH 201/208] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Doris Benda Co-authored-by: Emil Holm Gjørup --- contract-testing/CHANGELOG.md | 2 +- contract-testing/src/impls.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 578a1b9d..7e2c529e 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -5,7 +5,7 @@ - Add functionality for setting the exchange rates and block time of the chain based on queries from an external node. - Configured via a builder pattern, see `Chain::builder`. - Add methods to `Chain`: - - `external_query_block` to get the default block used for external + - `external_query_block` to get the default block used for external queries - `block_time` to get the block time - `tick_block_time` to increase the block time by a `Duration` - Add the following types by re-exporting them from internal crates: diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 313be6ae..5ed5f3fb 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -998,7 +998,7 @@ impl Chain { /// `invoker`. Here we provide extra freedom for testing invocations /// where the sender differs. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract contract and receive + /// - `payload`: The data detailing which contract and receive /// method to call etc. pub fn contract_update( &mut self, @@ -1123,9 +1123,9 @@ impl Chain { /// **Parameters:** /// - `invoker`: the account used as invoker. Since this isn't a /// transaction, it won't be charged. - /// - `sender`: the sender, can also be a contract. + /// - `sender`: the sender. Can be either a contract address or an account address. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract contract and receive + /// - `payload`: The data detailing which contract and receive /// method to call etc. pub fn contract_invoke( &self, @@ -1207,15 +1207,15 @@ impl Chain { /// Invoke an external contract entrypoint. /// /// Similar to [`Chain::contract_invoke`](Self::contract_invoke) except that - /// it invokes a contract on the external node. + /// it invokes/dry runs a contract on the external node. /// /// **Parameters:** /// - `invoker`: the account used as invoker. /// - The account must exist on the connected node. /// - `sender`: the sender, can also be a contract. - /// - The sender exist on the connected node. + /// - The sender must exist on the connected node. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract contract and receive + /// - `payload`: The data detailing which contract and receive /// method to call etc. /// - `block`: The block in which the invocation will be simulated, as if /// it was at the end of the block. If `None` is provided, the From c753779a57ad528f5e2a93dd5cd988080dc25b54 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 09:38:01 +0200 Subject: [PATCH 202/208] Fix formatting --- contract-testing/src/impls.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 5ed5f3fb..bae98403 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -998,8 +998,8 @@ impl Chain { /// `invoker`. Here we provide extra freedom for testing invocations /// where the sender differs. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract and receive - /// method to call etc. + /// - `payload`: The data detailing which contract and receive method to + /// call etc. pub fn contract_update( &mut self, signer: Signer, @@ -1123,10 +1123,11 @@ impl Chain { /// **Parameters:** /// - `invoker`: the account used as invoker. Since this isn't a /// transaction, it won't be charged. - /// - `sender`: the sender. Can be either a contract address or an account address. + /// - `sender`: the sender. Can be either a contract address or an account + /// address. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract and receive - /// method to call etc. + /// - `payload`: The data detailing which contract and receive method to + /// call etc. pub fn contract_invoke( &self, invoker: AccountAddress, @@ -1215,8 +1216,8 @@ impl Chain { /// - `sender`: the sender, can also be a contract. /// - The sender must exist on the connected node. /// - `energy_reserved`: the maximum energy that can be used in the update. - /// - `payload`: The data detailing which contract and receive - /// method to call etc. + /// - `payload`: The data detailing which contract and receive method to + /// call etc. /// - `block`: The block in which the invocation will be simulated, as if /// it was at the end of the block. If `None` is provided, the /// `external_query_block` is used instead. From 4c3a061bdb82046f91ab416c0aba2a0dbaab7bec Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 10:44:27 +0200 Subject: [PATCH 203/208] Add rust-sdk submodule and update paths to contracts-common --- .gitmodules | 6 +++--- concordium-contracts-common | 1 - concordium-rust-sdk | 1 + concordium-std/Cargo.toml | 2 +- contract-testing/.gitignore | 1 - .../smart-contract-upgrade/contract-version1/Cargo.toml | 4 ++-- 6 files changed, 7 insertions(+), 8 deletions(-) delete mode 160000 concordium-contracts-common create mode 160000 concordium-rust-sdk delete mode 100644 contract-testing/.gitignore diff --git a/.gitmodules b/.gitmodules index cea4ff1f..e48adb56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "concordium-contracts-common"] - path = concordium-contracts-common - url = ../concordium-contracts-common.git +[submodule "concordium-rust-sdk"] + path = concordium-rust-sdk + url = git@github.com:Concordium/concordium-rust-sdk.git diff --git a/concordium-contracts-common b/concordium-contracts-common deleted file mode 160000 index 9d1f254e..00000000 --- a/concordium-contracts-common +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9d1f254e52a6bc730e4f8d92e353096cebe02f0a diff --git a/concordium-rust-sdk b/concordium-rust-sdk new file mode 160000 index 00000000..0a2f899d --- /dev/null +++ b/concordium-rust-sdk @@ -0,0 +1 @@ +Subproject commit 0a2f899de9996825ec2447f21d2b57f46516421a diff --git a/concordium-std/Cargo.toml b/concordium-std/Cargo.toml index dd3f624f..b40020b3 100644 --- a/concordium-std/Cargo.toml +++ b/concordium-std/Cargo.toml @@ -22,7 +22,7 @@ quickcheck = {version = "1", optional = true } getrandom = { version = "0.2", features = ["custom"], optional = true } [dependencies.concordium-contracts-common] -path = "../concordium-contracts-common/concordium-contracts-common" +path = "../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common" version = "8.0" default-features = false features = ["smart-contract"] diff --git a/contract-testing/.gitignore b/contract-testing/.gitignore deleted file mode 100644 index 03314f77..00000000 --- a/contract-testing/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Cargo.lock diff --git a/examples/smart-contract-upgrade/contract-version1/Cargo.toml b/examples/smart-contract-upgrade/contract-version1/Cargo.toml index 1dd277af..e75dc4ff 100644 --- a/examples/smart-contract-upgrade/contract-version1/Cargo.toml +++ b/examples/smart-contract-upgrade/contract-version1/Cargo.toml @@ -25,8 +25,8 @@ crate-type=["cdylib", "rlib"] #`concordium-smart-contract-testing` uses the below libraries as well. We overwrite them with the ones used #in this repository so we can test the newest version of the libraries with the Ci pipelines. [patch.crates-io] -concordium-contracts-common = {path = "../../../concordium-contracts-common/concordium-contracts-common"} -concordium-contracts-common-derive = {path = "../../../concordium-contracts-common/concordium-contracts-common-derive"} +concordium-contracts-common = {path = "../../../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common"} +concordium-contracts-common-derive = {path = "../../../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common-derive"} [profile.release] opt-level = "s" From 30d3dfe013ed612c2d33471154316c8931152919 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 11:29:30 +0200 Subject: [PATCH 204/208] Add CI job for the testing library --- .github/workflows/linter-testing-lib.yaml | 106 ++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/linter-testing-lib.yaml diff --git a/.github/workflows/linter-testing-lib.yaml b/.github/workflows/linter-testing-lib.yaml new file mode 100644 index 00000000..8b3e4284 --- /dev/null +++ b/.github/workflows/linter-testing-lib.yaml @@ -0,0 +1,106 @@ +name: "Testing library: Format, lint, build, and test" + +# This job runs: +# - rustfmt +# - clippy linting +# - tests +# for the smart contract testing library. + +on: + push: + branches: main + + pull_request: + branches: main + types: + - opened + - reopened + - synchronize + - ready_for_review + paths: + - 'contract-testing/**/*.rs' + - 'contract-testing/**/*.toml' + - 'rustfmt.toml' + - 'concordium-rust-sdk' + + workflow_dispatch: # allows manual trigger + +env: + RUST_FMT: nightly-2023-04-01-x86_64-unknown-linux-gnu + RUST_CLIPPY: 1.66 + +jobs: + "lint_fmt": + name: ${{ matrix.build-dir }} lint:fmt + # Don't run on draft pull requests + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + strategy: + matrix: + build-dir: + - 'concordium-smart-contract-testing' + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ env.RUST_FMT }} + override: true + components: rustfmt + - name: Format + working-directory: ${{ matrix.build-dir }} + run: | + cargo fmt -- --color=always --check + + "lint_clippy": + name: ${{ matrix.build-dir }} lint:clippy + # Don't run on draft pull requests + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + strategy: + matrix: + build-dir: + - 'concordium-smart-contract-testing' + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ env.RUST_CLIPPY }} + override: true + components: clippy + - name: Clippy + working-directory: ${{ matrix.build-dir }} + run: | + git config --global url."https://github.com/".insteadOf "git@github.com:" + cargo clippy --color=always --tests --benches -- -Dclippy::all + + "cargo_test": + name: ${{ matrix.build-dir }} cargo:test + runs-on: ubuntu-latest + needs: + - cargo-concordium_cargo_build-bench + strategy: + matrix: + build-dir: + - 'concordium-smart-contract-testing' + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ env.RUST_CLIPPY }} + override: true + - name: Test + working-directory: ${{ matrix.build-dir }} + run: cargo test -- --skip io_tests # Skip the I/O tests in the testing library. From 37519e8e690c5b1a686a805f36fad840ed0453c3 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 11:35:21 +0200 Subject: [PATCH 205/208] Update README with the new changes --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aaa71066..8b923f14 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](https://github.com/Concordium/.github/blob/main/.github/CODE_OF_CONDUCT.md) -This repository consists of the core standard library for writing smart -contracts for the Concordium blockchain in the Rust programming languages, as -well as some sample smart contracts. The core library is -[concordium-std](./concordium-std). +This repository consists of libraries for writing and testing smart contracts for the Concordium blockchain in the Rust programming language. +It also contains some sample smart contracts. +The core library is +[concordium-std](./concordium-std), and the testing library is [concordium-smart-contract-testing](./contract-testing). The core library provides procedural macros to reduce the amount of boilerplate the user needs to write, while the `concordium-std` library exposes a high-level API that smart contract @@ -35,9 +35,9 @@ the logic of the contract is reasonable, or safe. ## Submodules The repository has -[concordium-contracts-common](https://github.com/Concordium/concordium-contracts-common) +[concordium-rust-sdk](https://github.com/Concordium/concordium-rust-sdk) as a submodule, and testing and builds are set-up to use the submodule version. -When changes are made in `concordium-contracts-common` they should be propagated +When changes are made in `concordium-rust-sdk` they should be propagated to this repository. ## Contributing @@ -50,7 +50,7 @@ Changes to any of the packages must be such that - ```cargo clippy --all``` produces no warnings - ```rustfmt``` makes no changes. -Everything in this repository should build with rust version 1.65 however the `fmt` tool must be from a nightly release since some of the configuration options are not stable. One way to run the `fmt` tool is +Everything in this repository should build with rust version 1.66 however the `fmt` tool must be from a nightly release since some of the configuration options are not stable. One way to run the `fmt` tool is ``` cargo +nightly-2023-04-01 fmt ``` From 2ec652e09416dc6e20fc7f09d2cb715f090fadcb Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 13:41:02 +0200 Subject: [PATCH 206/208] Recursively checkout submodules in CI --- .github/workflows/linter.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 85a6f353..5e57ec39 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -53,7 +53,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install nightly toolchain with rustfmt available uses: actions-rs/toolchain@v1 @@ -86,7 +86,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # we use v0.17.5 here because the minimal supported version is 0.17.0 and the closest @@ -130,7 +130,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # we use v0.17.5 here because the minimal supported version is 0.17.0 and the closest @@ -179,7 +179,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # we use v0.17.5 here because the minimal supported version is 0.17.0 and the closest @@ -231,7 +231,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # we use v0.17.5 here because the minimal supported version is 0.17.0 and the closest @@ -277,7 +277,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # we use v0.17.5 here because the minimal supported version is 0.17.0 and the closest @@ -323,7 +323,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Run cargo-generate # at least version 0.17.0 is required for the rhai pre-script to work correctly; @@ -375,7 +375,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -414,7 +414,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -451,7 +451,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -490,7 +490,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -524,7 +524,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install nightly toolchain with check available uses: actions-rs/toolchain@v1 @@ -566,7 +566,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install nightly toolchain with check available uses: actions-rs/toolchain@v1 @@ -605,7 +605,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain uses: actions-rs/toolchain@v1 @@ -660,7 +660,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -697,7 +697,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain uses: actions-rs/toolchain@v1 @@ -733,7 +733,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain uses: actions-rs/toolchain@v1 @@ -790,7 +790,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Install toolchain uses: actions-rs/toolchain@v1 From 1c589e370899fbe3d36216f1ede0a61c1a165dc1 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Fri, 15 Sep 2023 13:54:14 +0200 Subject: [PATCH 207/208] Update URLs --- contract-testing/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index f4bf3fc8..1d39ca72 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -6,8 +6,8 @@ rust-version = "1.66" license = "MPL-2.0" readme = "README.md" description = "A companion crate to `concordium-std` that supports off-chain end-to-end testing of smart contracts." -homepage = "https://github.com/Concordium/concordium-smart-contract-tools" -repository = "https://github.com/Concordium/concordium-smart-contract-tools" +homepage = "https://github.com/Concordium/concordium-rust-smart-contracts" +repository = "https://github.com/Concordium/concordium-rust-smart-contracts" exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From fea36350b223718b9e4f76d53c5477cfce2a7951 Mon Sep 17 00:00:00 2001 From: Kasper Dissing Bargsteen Date: Mon, 18 Sep 2023 09:36:45 +0200 Subject: [PATCH 208/208] Use relative path for submodule. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows the user to use either https or ssh. Co-authored-by: Emil Holm Gjørup --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index e48adb56..25947d00 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "concordium-rust-sdk"] path = concordium-rust-sdk - url = git@github.com:Concordium/concordium-rust-sdk.git + url = ../concordium-rust-sdk.git