diff --git a/Cargo.toml b/Cargo.toml index c68dd866d8..6472e9a1b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,6 @@ path = "src/blockstack_cli.rs" name = "marf_bench" harness = false -[features] -developer-mode = [] -default = ["developer-mode"] - [dependencies] byteorder = "1.1" rust-ini = "0.13" @@ -46,6 +42,8 @@ mio = "0.6.16" libc = "0.2" lazy_static = "1.4.0" toml = "0.5.6" +sha2 = { version = "0.8.0", optional = true } +sha2-asm = { version="0.5.3", optional = true } [dependencies.serde_json] version = "1.0" @@ -67,10 +65,6 @@ features = ["serde"] version = "=2.0.0" features = ["serde"] -[dependencies.sha2] -version = "0.8.0" -features = ["asm"] - [dependencies.time] version = "0.2.1" features = ["std"] @@ -78,3 +72,9 @@ features = ["std"] [dev-dependencies] assert-json-diff = "1.0.0" criterion = "0.3" + +[features] +developer-mode = [] +asm = ["sha2", "sha2-asm"] +aarch64 = ["developer-mode", "sha2"] +default = ["developer-mode", "asm"] \ No newline at end of file diff --git a/Dockerfile.memtest b/Dockerfile.memtest new file mode 100644 index 0000000000..3246f3a7c3 --- /dev/null +++ b/Dockerfile.memtest @@ -0,0 +1,15 @@ +FROM rust:latest + +WORKDIR /src/blockstack-core + +RUN apt-get update +RUN apt-get install valgrind heaptrack -y +RUN apt-get install less + +RUN rustup install stable + +COPY . . + +RUN cargo test --no-run + +CMD ["bash"] diff --git a/README.md b/README.md index f75008805d..404db54833 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ Reference implementation of the [Blockstack Technical Whitepaper](https://blocks ## Repository -| Blockstack Topic/Tech | Where to learn more more | -|---------------------------------|------------------------------------------------------------------------------| -| Stacks 2.0 | [master branch](https://github.com/blockstack/stacks-blockchain/tree/master) | -| Stacks 1.0 | [legacy branch](https://github.com/blockstack/stacks-blockchain/tree/stacks-1.0) | -| Use the package | [our core docs](https://docs.blockstack.org/core/naming/introduction.html) | -| Develop a Blockstack App | [our developer docs](https://docs.blockstack.org/browser/hello-blockstack.html) | -| Use a Blockstack App | [our browser docs](https://docs.blockstack.org/browser/browser-introduction.html) | -| Blockstack the company | [our website](https://blockstack.org) | +| Blockstack Topic/Tech | Where to learn more more | +| ------------------------ | --------------------------------------------------------------------------------- | +| Stacks 2.0 | [master branch](https://github.com/blockstack/stacks-blockchain/tree/master) | +| Stacks 1.0 | [legacy branch](https://github.com/blockstack/stacks-blockchain/tree/stacks-1.0) | +| Use the package | [our core docs](https://docs.blockstack.org/core/naming/introduction.html) | +| Develop a Blockstack App | [our developer docs](https://docs.blockstack.org/browser/hello-blockstack.html) | +| Use a Blockstack App | [our browser docs](https://docs.blockstack.org/browser/browser-introduction.html) | +| Blockstack the company | [our website](https://blockstack.org) | ## Design Thesis @@ -21,17 +21,16 @@ Stacks 2.0 is an open-membership replicated state machine produced by the coordi To unpack this definition: -- A replicated state machine is two or more copies (“replicas”) of a given set of rules (a “machine”) that, in processing a common input (such as the same sequence of transactions), will arrive at the same configuration (“state”). Bitcoin is a replicated state machine — its state is the set of UTXOs, which each peer has a full copy of, and given a block, all peers will independently calculate the same new UTXO set from the existing one. +- A replicated state machine is two or more copies (“replicas”) of a given set of rules (a “machine”) that, in processing a common input (such as the same sequence of transactions), will arrive at the same configuration (“state”). Bitcoin is a replicated state machine — its state is the set of UTXOs, which each peer has a full copy of, and given a block, all peers will independently calculate the same new UTXO set from the existing one. - Open-membership means that any host on the Internet can join the blockchain and independently calculate the same full replica as all other peers. -- Non-enumerable means that the set of peers that are producing the blocks don’t know about one another — they don’t know their identities, or even how many exist and are online. They are indistinguishable. +- Non-enumerable means that the set of peers that are producing the blocks don’t know about one another — they don’t know their identities, or even how many exist and are online. They are indistinguishable. ## Roadmap - [x] [SIP 001: Burn Election](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-001-burn-election.md) - [x] [SIP 002: Clarity, a language for predictable smart contracts](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-002-smart-contract-language.md) - [x] [SIP 004: Cryptographic Committment to Materialized Views](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-004-materialized-view.md) -- [x] [SIP 005: Blocks, Transactions, and Accounts](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-005-blocks-and-transactions.md -) +- [x] [SIP 005: Blocks, Transactions, and Accounts](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-005-blocks-and-transactions.md) - [ ] [SIP 003: Peer Network](https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-003-peer-network.md) (Q1 2020) - [ ] SIP 006: Clarity Execution Cost Assessment (Q1 2020) @@ -71,7 +70,13 @@ Then build the project: cargo build ``` -And run the tests: +Building the project on ARM: + +```bash +cargo build --features "aarch64" --no-default-features +``` + +Run the tests: ```bash cargo test testnet -- --test-threads=1 @@ -213,17 +218,19 @@ Congratulations, you can now [write your own smart contracts with Clarity](https Beyond this Github project, Blockstack maintains a public [forum](https://forum.blockstack.org) and an -opened [Discord](https://discordapp.com/invite/9r94Xkj) channel. In addition, the project +opened [Discord](https://discordapp.com/invite/9r94Xkj) channel. In addition, the project maintains a [mailing list](https://blockstack.org/signup) which sends out community announcements. The greater Blockstack community regularly hosts in-person -[meetups](https://www.meetup.com/topics/blockstack/). The project's +[meetups](https://www.meetup.com/topics/blockstack/). The project's [YouTube channel](https://www.youtube.com/channel/UC3J2iHnyt2JtOvtGVf_jpHQ) includes videos from some of these meetups, as well as video tutorials to help new users get started and help developers wrap their heads around the system's design. +For help cross-compiling on memory-constrained devices, please see the community supported documentation here: [Cross Compiling](https://github.com/dantrevino/cross-compiling-stacks-blockchain/blob/master/README.md). + ## Further Reading You can learn more by visiting [the Blockstack Website](https://blockstack.org) and checking out the in-depth articles and documentation: diff --git a/circle.yml b/circle.yml index 051d0ee01d..37e8b8f7fd 100644 --- a/circle.yml +++ b/circle.yml @@ -37,6 +37,10 @@ jobs: working_directory: ~/blockstack steps: - checkout + - run: + name: Install perftools + command: | + sudo apt-get update -y && sudo apt-get install libgoogle-perftools4 -y - run: name: Fetch latest grcov command: | diff --git a/sip/sip-006-runtime-cost-assessment.md b/sip/sip-006-runtime-cost-assessment.md index 69cf0a3e7f..f6b33d0934 100644 --- a/sip/sip-006-runtime-cost-assessment.md +++ b/sip/sip-006-runtime-cost-assessment.md @@ -23,6 +23,9 @@ constants will necessarily be measured via benchmark harnesses and regression analyses. Furthermore, the _analysis_ cost associated with this code will not be covered by this proposal. +This document also describes the memory limit imposed during contract +execution, and the memory model for enforcing that limit. + # Measurements for Execution Cost Execution cost of a block of Clarity code is broken into 5 categories: @@ -661,3 +664,85 @@ hashed, the longer the hashing function takes. where X is the size of the input. + +# Memory Model and Limits + +Clarity contract execution imposes a maximum memory usage limit for applications. +For any given Clarity value, the memory usage of that value is counted using +the _size_ of the Clarity value. + +Memory is consumed by the following variable bindings: + +* `let` - each value bound in the `let` consumes that amount of memory + during the execution of the `let` block. +* `match` - the bound value in a `match` statement consumes memory during + the execution of the `match` branch. +* function arguments - each bound value consumes memory during the execution + of the function. this includes user-defined functions _as well as_ native + functions. + +Additionally, functions that perform _context changes_ also consume memory, +though they consume a constant amount: + +* `as-contract` +* `at-block` + +## Type signature size + +Types in Clarity may be described using type signatures. For example, +`(tuple (a int) (b int))` describes a tuple with two keys `a` and `b` +of type `int`. These type descriptions are used by the Clarity analysis +passes to check the type correctness of Clarity code. Clarity type signatures +have varying size, e.g., the signature `int` is smaller than the signature for a +list of integers. + +The size of a Clarity value is defined as follows: + +``` +type_size(x) := + if x = + int => 16 + uint => 16 + bool => 1 + principal => 148 + (buff y) => 4 + y + (some y) => 1 + size(y) + (ok y) => 1 + size(y) + (err y) => 1 + size(y) + (list ...) => 4 + sum(size(z) for z in list) + (tuple ...) => 1 + 2*(count(entries)) + + sum(size(z) for each value z in tuple) +``` + +## Contract Memory Consumption + +Contract execution requires loading the contract's program state in +memory. That program state counts towards the memory limit when +executed via a programmatic `contract-call!` or invoked by a +contract-call transaction. + +The memory consumed by a contract is equal to: + +``` +a + b*contract_length + sum(size(x) for each constant x defined in the contract) +``` + +That is, a contract consumes memory which is linear in the contract's +length _plus_ the amount of memory consumed by any constants defined +using `define-constant`. + +## Database Writes + +While data stored in the database itself does _not_ count against the +memory limit, supporting public function abort/commit behavior requires +holding a write log in memory during the processing of a transaction. + +Operations that write data to the data store therefore consume memory +_until the transaction completes_, and the write log is written to the +database. The amount of memory consumed by operations on persisted data +types is defined as: + +* `data-var`: the size of the stored data var's value. +* `map`: the size of stored key + the size of the stored value. +* `nft`: the size of the NFT key +* `ft`: the size of a Clarity uint value. diff --git a/src/blockstack_cli.rs b/src/blockstack_cli.rs index 8bf79b06a0..6a3e9f31f6 100644 --- a/src/blockstack_cli.rs +++ b/src/blockstack_cli.rs @@ -343,9 +343,9 @@ fn generate_secret_key(args: &[String], version: TransactionVersion) -> Result sn.clone(), - None => BurnDB::get_last_snapshot_with_sortition(tx, self.block_height - 1, &self.parent_snapshot.burn_header_hash) - .expect("FATAL: failed to read last snapshot with sortition") + None => BurnDB::get_first_block_snapshot(tx).unwrap() }; - + // prove on the last-ever sortition's hash to produce the new seed let proof = miner.make_proof(&leader_key.public_key, &last_snapshot.sortition_hash) .expect(&format!("FATAL: no private key for {}", leader_key.public_key.to_hex())); diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 4f4402a08e..d5e7beb26c 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -1185,6 +1185,22 @@ impl StacksChainState { } }) } + + /// Is a block orphaned? + pub fn is_block_orphaned(blocks_conn: &DBConn, burn_hash: &BurnchainHeaderHash, block_hash: &BlockHeaderHash) -> Result { + StacksChainState::read_i64s(blocks_conn, "SELECT orphaned FROM staging_blocks WHERE anchored_block_hash = ?1 AND burn_header_hash = ?2", &[block_hash, burn_hash]) + .and_then(|orphaned| { + if orphaned.len() == 0 { + Ok(false) + } + else if orphaned.len() == 1 { + Ok(orphaned[0] != 0) + } + else { + Err(Error::DBError(db_error::Overflow)) + } + }) + } /// Do we have a microblock queued up, and if so, is it being processed? /// Return Some(processed) if the microblock is queued up @@ -1203,6 +1219,22 @@ impl StacksChainState { } }) } + + /// Is a microblock orphaned? + pub fn is_microblock_orphaned(blocks_conn: &DBConn, burn_hash: &BurnchainHeaderHash, block_hash: &BlockHeaderHash, microblock_hash: &BlockHeaderHash) -> Result { + StacksChainState::read_i64s(blocks_conn, "SELECT orphaned FROM staging_microblocks WHERE anchored_block_hash = ?1 AND microblock_hash = ?2 AND burn_header_hash = ?3", &[block_hash, microblock_hash, burn_hash]) + .and_then(|orphaned| { + if orphaned.len() == 0 { + Ok(false) + } + else if orphaned.len() == 1 { + Ok(orphaned[0] != 0) + } + else { + Err(Error::DBError(db_error::Overflow)) + } + }) + } /// What's the first microblock hash in a stream? fn get_microblock_stream_head_hash(blocks_conn: &DBConn, burn_hash: &BurnchainHeaderHash, anchored_header_hash: &BlockHeaderHash) -> Result, Error> { @@ -1357,6 +1389,7 @@ impl StacksChainState { else { // Otherwise, all descendents of this processed block are never attacheable. // Mark this block's children as orphans, blow away its data, and blow away its descendent microblocks. + test_debug!("Orphan block {}/{}", burn_hash, anchored_block_hash); StacksChainState::delete_orphaned_epoch_data(tx, burn_hash, anchored_block_hash)?; } @@ -1958,22 +1991,29 @@ impl StacksChainState { test_debug!("Block already stored and/or processed: {}/{}", burn_header_hash, &block.block_hash()); return Ok(false); } - + + // find all user burns that supported this block + let user_burns = BurnDB::get_winning_user_burns_by_block(burn_tx, burn_header_hash) + .map_err(Error::DBError)?; + + let mainnet = self.mainnet; + let chain_id = self.chain_id; + let mut block_tx = self.blocks_tx_begin()?; + // does this block match the burnchain state? skip if not - let (commit_burn, sortition_burn) = match StacksChainState::validate_anchored_block_burnchain(burn_tx, burn_header_hash, block, self.mainnet, self.chain_id)? { + let (commit_burn, sortition_burn) = match StacksChainState::validate_anchored_block_burnchain(burn_tx, burn_header_hash, block, mainnet, chain_id)? { Some((commit_burn, sortition_burn)) => (commit_burn, sortition_burn), None => { let msg = format!("Invalid block {}: does not correspond to burn chain state", block.block_hash()); warn!("{}", &msg); + + // orphan it + StacksChainState::set_block_processed(&mut block_tx, burn_header_hash, &block.block_hash(), false)?; + + block_tx.commit().map_err(Error::DBError)?; return Err(Error::InvalidStacksBlock(msg)); } }; - - // find all user burns that supported this block - let user_burns = BurnDB::get_winning_user_burns_by_block(burn_tx, burn_header_hash) - .map_err(Error::DBError)?; - - let mut block_tx = self.blocks_tx_begin()?; // queue block up for processing StacksChainState::store_staging_block(&mut block_tx, burn_header_hash, burn_header_timestamp, &block, parent_burn_header_hash, commit_burn, sortition_burn)?; @@ -2158,6 +2198,7 @@ impl StacksChainState { let sql = "SELECT * FROM staging_blocks WHERE processed = 0 AND orphaned = 1 ORDER BY RANDOM() LIMIT 1".to_string(); let mut rows = query_rows::(blocks_tx, &sql, NO_PARAMS).map_err(Error::DBError)?; if rows.len() == 0 { + test_debug!("No orphans to remove"); return Ok(false); } @@ -2592,7 +2633,10 @@ impl StacksChainState { debug!("Block already processed: {}/{}", &next_staging_block.burn_header_hash, &next_staging_block.anchored_block_hash); // clear out - StacksChainState::set_block_processed(&mut chainstate_tx.blocks_tx, &next_staging_block.burn_header_hash, &next_staging_block.anchored_block_hash, true)?; + StacksChainState::set_block_processed(&mut chainstate_tx.blocks_tx, &next_staging_block.burn_header_hash, &next_staging_block.anchored_block_hash, true)?; + chainstate_tx.commit() + .map_err(Error::DBError)?; + return Ok((None, None)); } @@ -2665,6 +2709,7 @@ impl StacksChainState { // something's wrong with this epoch -- either a microblock was invalid, or the // anchored block was invalid. Either way, the anchored block will _never be_ // valid, so we can drop it from the chunk store and orphan all of its descendents. + test_debug!("Failed to append {}/{}", &next_staging_block.burn_header_hash, &block.block_hash()); StacksChainState::set_block_processed(&mut chainstate_tx.blocks_tx, &next_staging_block.burn_header_hash, &block.header.block_hash(), false) .expect(&format!("FATAL: failed to clear invalid block {}/{}", next_staging_block.burn_header_hash, &block.header.block_hash())); @@ -2684,6 +2729,10 @@ impl StacksChainState { // leave them in the staging database. } } + + chainstate_tx.commit() + .map_err(Error::DBError)?; + return Err(e); } }; @@ -2720,21 +2769,39 @@ impl StacksChainState { for i in 0..max_blocks { // process up to max_blocks pending blocks - let (next_tip_opt, next_microblock_poison_opt) = self.process_next_staging_block()?; - match next_tip_opt { - Some(next_tip) => { - ret.push((Some(next_tip), next_microblock_poison_opt)); - }, - None => { - match next_microblock_poison_opt { - Some(poison) => { - ret.push((None, Some(poison))); - }, - None => { - debug!("No more staging blocks -- processed {} in total", i); - break; + match self.process_next_staging_block() { + Ok((next_tip_opt, next_microblock_poison_opt)) => match next_tip_opt { + Some(next_tip) => { + ret.push((Some(next_tip), next_microblock_poison_opt)); + }, + None => { + match next_microblock_poison_opt { + Some(poison) => { + ret.push((None, Some(poison))); + }, + None => { + debug!("No more staging blocks -- processed {} in total", i); + break; + } } } + }, + Err(Error::InvalidStacksBlock(msg)) => { + warn!("Encountered invalid block: {}", &msg); + continue; + }, + Err(Error::InvalidStacksMicroblock(msg, hash)) => { + warn!("Encountered invalid microblock {}: {}", hash, &msg); + continue; + }, + Err(Error::NetError(net_error::DeserializeError(msg))) => { + // happens if we load a zero-sized block (i.e. an invalid block) + warn!("Encountered invalid block: {}", &msg); + continue; + }, + Err(e) => { + error!("Unrecoverable error when processing blocks: {:?}", &e); + return Err(e); } } } @@ -2748,6 +2815,7 @@ impl StacksChainState { } } + block_tx.commit().map_err(|e| Error::DBError(e))?; Ok(ret) } @@ -3963,7 +4031,6 @@ pub mod test { if i + 1 < blocks.len() { // block i+1 should be marked as an orphan, but its data should still be there assert!(StacksChainState::load_staging_block(&chainstate.blocks_db, &chainstate.blocks_path, &burn_headers[i+1], &blocks[i+1].block_hash()).unwrap().is_none()); - // assert!(StacksChainState::load_staging_block_bytes(&chainstate.blocks_db, &burn_headers[i+1], &blocks[i+1].block_hash()).unwrap().unwrap().len() > 0); assert!(StacksChainState::load_block_bytes(&chainstate.blocks_path, &burn_headers[i+1], &blocks[i+1].block_hash()).unwrap().unwrap().len() > 0); for mblock in microblocks[i+1].iter() { diff --git a/src/chainstate/stacks/miner.rs b/src/chainstate/stacks/miner.rs index d94054be29..3b324a0b1d 100644 --- a/src/chainstate/stacks/miner.rs +++ b/src/chainstate/stacks/miner.rs @@ -137,6 +137,40 @@ impl StacksBlockBuilder { Ok(()) } + /// Append a transaction if doing so won't exceed the epoch data size. + /// Does not check for errors + #[cfg(test)] + pub fn force_mine_tx<'a>(&mut self, clarity_tx: &mut ClarityTx<'a>, tx: &StacksTransaction) -> Result<(), Error> { + let mut tx_bytes = vec![]; + tx.consensus_serialize(&mut tx_bytes).map_err(Error::NetError)?; + let tx_len = tx_bytes.len() as u64; + + if !self.anchored_done { + // save + match StacksChainState::process_transaction(clarity_tx, tx) { + Ok(_) => {}, + Err(e) => { + warn!("Invalid transaction {} in anchored block, but forcing inclusion (error: {:?})", &tx.txid(), &e); + } + } + + self.txs.push(tx.clone()); + } + else { + match StacksChainState::process_transaction(clarity_tx, tx) { + Ok(_) => {}, + Err(e) => { + warn!("Invalid transaction {} in microblock, but forcing inclusion (error: {:?})", &tx.txid(), &e); + } + } + + self.micro_txs.push(tx.clone()); + } + + self.bytes_so_far += tx_len; + Ok(()) + } + /// Finish building the anchored block. /// TODO: expand to deny mining a block whose anchored static checks fail (and allow the caller /// to disable this, in order to test mining invalid blocks) @@ -629,6 +663,23 @@ pub mod test { } } + pub fn get_last_accepted_anchored_block(&self, miner: &TestMiner) -> Option { + for bc in miner.block_commits.iter().rev() { + if StacksChainState::has_stored_block(&self.chainstate.blocks_db, &self.chainstate.blocks_path, &bc.burn_header_hash, &bc.block_header_hash).unwrap() && + !StacksChainState::is_block_orphaned(&self.chainstate.blocks_db, &bc.burn_header_hash, &bc.block_header_hash).unwrap() { + match self.commit_ops.get(&bc.block_header_hash) { + None => { + continue; + } + Some(idx) => { + return Some(self.anchored_blocks[*idx].clone()); + } + } + } + } + return None; + } + pub fn get_microblock_stream(&self, miner: &TestMiner, block_hash: &BlockHeaderHash) -> Option> { match self.commit_ops.get(block_hash) { None => None, @@ -910,9 +961,10 @@ pub mod test { /// Simplest end-to-end test: create 1 fork of N Stacks epochs, mined on 1 burn chain fork, /// all from the same miner. - fn mine_stacks_blocks_1_fork_1_miner_1_burnchain(test_name: &String, rounds: usize, mut block_builder: F) -> TestMinerTrace + fn mine_stacks_blocks_1_fork_1_miner_1_burnchain(test_name: &String, rounds: usize, mut block_builder: F, mut check_oracle: G) -> TestMinerTrace where - F: FnMut(&mut ClarityTx, &mut StacksBlockBuilder, &mut TestMiner, usize, Option<&StacksMicroblockHeader>) -> (StacksBlock, Vec) + F: FnMut(&mut ClarityTx, &mut StacksBlockBuilder, &mut TestMiner, usize, Option<&StacksMicroblockHeader>) -> (StacksBlock, Vec), + G: FnMut(&StacksBlock, &Vec) -> bool { let full_test_name = format!("{}-1_fork_1_miner_1_burnchain", test_name); let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); @@ -943,7 +995,7 @@ pub mod test { }; let last_key = node.get_last_key(&miner); - let parent_block_opt = node.get_last_anchored_block(&miner); + let parent_block_opt = node.get_last_accepted_anchored_block(&miner); let last_microblock_header = get_last_microblock_header(&node, &miner, parent_block_opt.as_ref()); // next key @@ -975,20 +1027,23 @@ pub mod test { test_debug!("Process Stacks block {} and {} microblocks", &stacks_block.block_hash(), microblocks.len()); let tip_info_list = node.chainstate.process_blocks(1).unwrap(); - // processed _this_ block - assert_eq!(tip_info_list.len(), 1); - let (chain_tip_opt, poison_opt) = tip_info_list[0].clone(); + let expect_success = check_oracle(&stacks_block, µblocks); + if expect_success { + // processed _this_ block + assert_eq!(tip_info_list.len(), 1); + let (chain_tip_opt, poison_opt) = tip_info_list[0].clone(); - assert!(chain_tip_opt.is_some()); - assert!(poison_opt.is_none()); + assert!(chain_tip_opt.is_some()); + assert!(poison_opt.is_none()); - let (chain_tip, _) = chain_tip_opt.unwrap(); + let (chain_tip, _) = chain_tip_opt.unwrap(); - assert_eq!(chain_tip.anchored_header.block_hash(), stacks_block.block_hash()); - assert_eq!(chain_tip.burn_header_hash, fork_snapshot.burn_header_hash); + assert_eq!(chain_tip.anchored_header.block_hash(), stacks_block.block_hash()); + assert_eq!(chain_tip.burn_header_hash, fork_snapshot.burn_header_hash); - // MARF trie exists for the block header's chain state, so we can make merkle proofs on it - assert!(check_block_state_index_root(&mut node.chainstate, &fork_snapshot.burn_header_hash, &chain_tip.anchored_header)); + // MARF trie exists for the block header's chain state, so we can make merkle proofs on it + assert!(check_block_state_index_root(&mut node.chainstate, &fork_snapshot.burn_header_hash, &chain_tip.anchored_header)); + } let mut next_miner_trace = TestMinerTracePoint::new(); next_miner_trace.add(miner.id, full_test_name.clone(), fork_snapshot, stacks_block, microblocks, block_commit_op); @@ -2466,6 +2521,51 @@ pub mod test { (stacks_block, microblocks) } + /// make a token transfer + pub fn make_token_transfer<'a>(clarity_tx: &mut ClarityTx<'a>, builder: &mut StacksBlockBuilder, miner: &mut TestMiner, burnchain_height: usize, nonce: Option, recipient: &StacksAddress, amount: u64, memo: &TokenTransferMemo) -> StacksTransaction { + let addr = miner.origin_address().unwrap(); + let mut tx_stx_transfer = StacksTransaction::new(TransactionVersion::Testnet, + miner.as_transaction_auth().unwrap(), + TransactionPayload::TokenTransfer((*recipient).clone(), amount, (*memo).clone())); + + tx_stx_transfer.chain_id = 0x80000000; + tx_stx_transfer.auth.set_origin_nonce(nonce.unwrap_or(miner.get_nonce())); + tx_stx_transfer.set_fee_rate(0); + + let mut tx_signer = StacksTransactionSigner::new(&tx_stx_transfer); + miner.sign_as_origin(&mut tx_signer); + let tx_stx_transfer_signed = tx_signer.get_tx().unwrap(); + tx_stx_transfer_signed + } + + /// Mine invalid token transfers + pub fn mine_invalid_token_transfers_block<'a>(clarity_tx: &mut ClarityTx<'a>, builder: &mut StacksBlockBuilder, miner: &mut TestMiner, burnchain_height: usize, parent_microblock_header: Option<&StacksMicroblockHeader>) -> (StacksBlock, Vec) { + let miner_account = StacksChainState::get_account(clarity_tx, &miner.origin_address().unwrap().to_account_principal()); + miner.set_nonce(miner_account.nonce); + + // make a coinbase for this miner + let tx_coinbase_signed = mine_coinbase(clarity_tx, builder, miner, burnchain_height); + builder.try_mine_tx(clarity_tx, &tx_coinbase_signed).unwrap(); + + let recipient = StacksAddress::new(C32_ADDRESS_VERSION_TESTNET_SINGLESIG, Hash160([0xff; 20])); + let tx1 = make_token_transfer(clarity_tx, builder, miner, burnchain_height, Some(1), &recipient, 11111, &TokenTransferMemo([1u8; 34])); + builder.force_mine_tx(clarity_tx, &tx1).unwrap(); + + let tx2 = make_token_transfer(clarity_tx, builder, miner, burnchain_height, Some(2), &recipient, 22222, &TokenTransferMemo([2u8; 34])); + builder.force_mine_tx(clarity_tx, &tx2).unwrap(); + + let tx3 = make_token_transfer(clarity_tx, builder, miner, burnchain_height, Some(1), &recipient, 33333, &TokenTransferMemo([3u8; 34])); + builder.force_mine_tx(clarity_tx, &tx3).unwrap(); + + let tx4 = make_token_transfer(clarity_tx, builder, miner, burnchain_height, Some(2), &recipient, 44444, &TokenTransferMemo([4u8; 34])); + builder.force_mine_tx(clarity_tx, &tx4).unwrap(); + + let stacks_block = builder.mine_anchored_block(clarity_tx); + + test_debug!("Produce anchored stacks block {} with invalid token transfers at burnchain height {} stacks height {}", stacks_block.block_hash(), burnchain_height, stacks_block.header.total_work.work); + (stacks_block, vec![]) + } + /* // TODO: blocked on get-block-info's reliance on get_simmed_block_height @@ -2531,12 +2631,12 @@ pub mod test { #[test] fn mine_anchored_empty_blocks_single() { - mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"empty-anchored-blocks".to_string(), 10, mine_empty_anchored_block); + mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"empty-anchored-blocks".to_string(), 10, mine_empty_anchored_block, |_, _| true); } #[test] fn mine_anchored_empty_blocks_random() { - let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"empty-anchored-blocks-random".to_string(), 10, mine_empty_anchored_block); + let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"empty-anchored-blocks-random".to_string(), 10, mine_empty_anchored_block, |_, _| true); miner_trace_replay_randomized(&mut miner_trace); } @@ -2586,12 +2686,12 @@ pub mod test { #[test] fn mine_anchored_smart_contract_contract_call_blocks_single() { - mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-contract-call-anchored-blocks".to_string(), 10, mine_smart_contract_contract_call_block); + mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-contract-call-anchored-blocks".to_string(), 10, mine_smart_contract_contract_call_block, |_, _| true); } #[test] fn mine_anchored_smart_contract_contract_call_blocks_single_random() { - let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-contract-call-anchored-blocks-random".to_string(), 10, mine_smart_contract_contract_call_block); + let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-contract-call-anchored-blocks-random".to_string(), 10, mine_smart_contract_contract_call_block, |_, _| true); miner_trace_replay_randomized(&mut miner_trace); } @@ -2641,12 +2741,12 @@ pub mod test { #[test] fn mine_anchored_smart_contract_block_contract_call_microblock_single() { - mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock".to_string(), 10, mine_smart_contract_block_contract_call_microblock); + mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock".to_string(), 10, mine_smart_contract_block_contract_call_microblock, |_, _| true); } #[test] fn mine_anchored_smart_contract_block_contract_call_microblock_single_random() { - let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-random".to_string(), 10, mine_smart_contract_block_contract_call_microblock); + let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-random".to_string(), 10, mine_smart_contract_block_contract_call_microblock, |_, _| true); miner_trace_replay_randomized(&mut miner_trace); } @@ -2696,12 +2796,12 @@ pub mod test { #[test] fn mine_anchored_smart_contract_block_contract_call_microblock_exception_single() { - mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-exception".to_string(), 10, mine_smart_contract_block_contract_call_microblock_exception); + mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-exception".to_string(), 10, mine_smart_contract_block_contract_call_microblock_exception, |_, _| true); } #[test] fn mine_anchored_smart_contract_block_contract_call_microblock_exception_single_random() { - let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-exception-random".to_string(), 10, mine_smart_contract_block_contract_call_microblock_exception); + let mut miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"smart-contract-block-contract-call-microblock-exception-random".to_string(), 10, mine_smart_contract_block_contract_call_microblock_exception, |_, _| true); miner_trace_replay_randomized(&mut miner_trace); } @@ -2749,6 +2849,21 @@ pub mod test { miner_trace_replay_randomized(&mut miner_trace); } + #[test] + fn mine_anchored_invalid_token_transfer_blocks_single() { + let miner_trace = mine_stacks_blocks_1_fork_1_miner_1_burnchain(&"invalid-token-transfers".to_string(), 10, mine_invalid_token_transfers_block, |_, _| false); + + let full_test_name = "invalid-token-transfers-1_fork_1_miner_1_burnchain"; + let chainstate = open_chainstate(false, 0x80000000, full_test_name); + + // each block must be orphaned + for point in miner_trace.points.iter() { + for (height, bc) in point.block_commits.iter() { + assert!(StacksChainState::is_block_orphaned(&chainstate.blocks_db, &bc.burn_header_hash, &bc.block_header_hash).unwrap()); + } + } + } + // TODO: (BLOCKED) build off of different points in the same microblock stream // TODO; skipped blocks // TODO: missing blocks diff --git a/src/clarity.rs b/src/clarity.rs index 56c80ffc4f..28dbfbdbf2 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -88,6 +88,7 @@ fn run_analysis(contract_identifier: &QualifiedContractIdentifier, analysis_db: &mut AnalysisDatabase, save_contract: bool) -> CheckResult { analysis::run_analysis(contract_identifier, expressions, analysis_db, save_contract, LimitedCostTracker::new_max_limit()) + .map_err(|(e, _)| e) } diff --git a/src/vm/analysis/errors.rs b/src/vm/analysis/errors.rs index 1a9aec86cc..5115269f65 100644 --- a/src/vm/analysis/errors.rs +++ b/src/vm/analysis/errors.rs @@ -12,6 +12,7 @@ pub enum CheckErrors { // cost checker errors CostOverflow, CostBalanceExceeded(ExecutionCost, ExecutionCost), + MemoryBalanceExceeded(u64, u64), ValueTooLarge, TypeSignatureTooDeep, @@ -211,6 +212,7 @@ impl From for CheckErrors { match err { CostErrors::CostOverflow => CheckErrors::CostOverflow, CostErrors::CostBalanceExceeded(a, b) => CheckErrors::CostBalanceExceeded(a, b), + CostErrors::MemoryBalanceExceeded(a, b) => CheckErrors::MemoryBalanceExceeded(a, b), } } } @@ -277,6 +279,7 @@ impl DiagnosableError for CheckErrors { CheckErrors::TypeAnnotationExpectedFailure => "analysis expected type to already be annotated for expression".into(), CheckErrors::CostOverflow => "contract execution cost overflowed cost counter".into(), CheckErrors::CostBalanceExceeded(a, b) => format!("contract execution cost exceeded budget: {:?} > {:?}", a, b), + CheckErrors::MemoryBalanceExceeded(a, b) => format!("contract execution cost exceeded memory budget: {:?} > {:?}", a, b), CheckErrors::InvalidTypeDescription => "supplied type description is invalid".into(), CheckErrors::EmptyTuplesNotAllowed => "tuple types may not be empty".into(), CheckErrors::BadSyntaxExpectedListOfPairs => "bad syntax: function expects a list of pairs to bind names, e.g., ((name-0 a) (name-1 b) ...)".into(), diff --git a/src/vm/analysis/mod.rs b/src/vm/analysis/mod.rs index 69be36f9d4..cebab2d46d 100644 --- a/src/vm/analysis/mod.rs +++ b/src/vm/analysis/mod.rs @@ -42,23 +42,28 @@ pub fn type_check(contract_identifier: &QualifiedContractIdentifier, analysis_db: &mut AnalysisDatabase, insert_contract: bool) -> CheckResult { run_analysis(&contract_identifier, expressions, analysis_db, insert_contract, LimitedCostTracker::new_max_limit()) + .map_err(|(e, _cost_tracker)| e) } pub fn run_analysis(contract_identifier: &QualifiedContractIdentifier, expressions: &mut [SymbolicExpression], analysis_db: &mut AnalysisDatabase, save_contract: bool, - cost_tracker: LimitedCostTracker) -> CheckResult { - analysis_db.execute(|db| { - let mut contract_analysis = ContractAnalysis::new(contract_identifier.clone(), expressions.to_vec(), cost_tracker); + cost_tracker: LimitedCostTracker) -> Result { + let mut contract_analysis = ContractAnalysis::new(contract_identifier.clone(), expressions.to_vec(), cost_tracker); + let result = analysis_db.execute(|db| { ReadOnlyChecker::run_pass(&mut contract_analysis, db)?; TypeChecker::run_pass(&mut contract_analysis, db)?; TraitChecker::run_pass(&mut contract_analysis, db)?; if save_contract { db.insert_contract(&contract_identifier, &contract_analysis)?; } - Ok(contract_analysis) - }) + Ok(()) + }); + match result { + Ok(_) => Ok(contract_analysis), + Err(e) => Err((e, contract_analysis.take_contract_cost_tracker())) + } } #[cfg(test)] diff --git a/src/vm/analysis/read_only_checker/mod.rs b/src/vm/analysis/read_only_checker/mod.rs index 9c0bcbae1b..2e937fa96c 100644 --- a/src/vm/analysis/read_only_checker/mod.rs +++ b/src/vm/analysis/read_only_checker/mod.rs @@ -190,18 +190,6 @@ impl <'a, 'b> ReadOnlyChecker <'a, 'b> { }; res }, - FetchContractEntry => { - check_argument_count(3, args)?; - let res = match tuples::get_definition_type_of_tuple_argument(&args[2]) { - Implicit(ref tuple_expr) => { - self.is_implicit_tuple_definition_read_only(tuple_expr) - }, - Explicit => { - self.check_all_read_only(args) - } - }; - res - }, StxTransfer | StxBurn | SetEntry | DeleteEntry | InsertEntry | SetVar | MintAsset | MintToken | TransferAsset | TransferToken => { Ok(false) diff --git a/src/vm/analysis/read_only_checker/tests.rs b/src/vm/analysis/read_only_checker/tests.rs index 5324bd9057..906fd7d740 100644 --- a/src/vm/analysis/read_only_checker/tests.rs +++ b/src/vm/analysis/read_only_checker/tests.rs @@ -9,7 +9,6 @@ fn test_argument_count_violations() { ("(define-private (foo-bar) (at-block))", CheckErrors::IncorrectArgumentCount(2, 0)), ("(define-private (foo-bar) (map-get?))", CheckErrors::IncorrectArgumentCount(2, 0)), - ("(define-private (foo-bar) (contract-map-get?))", CheckErrors::IncorrectArgumentCount(3, 0)), ]; for (contract, expected) in examples.iter() { diff --git a/src/vm/analysis/tests/mod.rs b/src/vm/analysis/tests/mod.rs index a1ccc68dac..bef0cdfcd0 100644 --- a/src/vm/analysis/tests/mod.rs +++ b/src/vm/analysis/tests/mod.rs @@ -169,13 +169,6 @@ fn test_return_types_must_match() { assert!(format!("{}", err.diagnostic).contains("detected two execution paths, returning two different expression types")); } -#[test] -fn test_no_such_contract() { - let snippet = "(contract-map-get? .unicorn map ((value 0)))"; - let err = mem_type_check(snippet).unwrap_err(); - assert!(format!("{}", err.diagnostic).contains("use of unresolved contract")); -} - #[test] fn test_contract_call_expect_name() { let snippet = "(contract-call? 1 fn)"; diff --git a/src/vm/analysis/type_checker/mod.rs b/src/vm/analysis/type_checker/mod.rs index 7621f78500..a3a25e242d 100644 --- a/src/vm/analysis/type_checker/mod.rs +++ b/src/vm/analysis/type_checker/mod.rs @@ -57,6 +57,15 @@ impl CostTracker for TypeChecker<'_, '_> { fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { self.cost_track.add_cost(cost) } + fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> { + self.cost_track.add_memory(memory) + } + fn drop_memory(&mut self, memory: u64) { + self.cost_track.drop_memory(memory) + } + fn reset_memory(&mut self) { + self.cost_track.reset_memory() + } } impl AnalysisPass for TypeChecker <'_, '_> { diff --git a/src/vm/analysis/type_checker/natives/maps.rs b/src/vm/analysis/type_checker/natives/maps.rs index e82e629737..2a835fa3f6 100644 --- a/src/vm/analysis/type_checker/natives/maps.rs +++ b/src/vm/analysis/type_checker/natives/maps.rs @@ -48,39 +48,6 @@ pub fn check_special_fetch_entry(checker: &mut TypeChecker, args: &[SymbolicExpr } } -pub fn check_special_fetch_contract_entry(checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext) -> TypeResult { - check_arguments_at_least(3, args)?; - - - let contract_identifier = match args[0].expr { - SymbolicExpressionType::LiteralValue(Value::Principal(PrincipalData::Contract(ref contract_identifier))) => contract_identifier, - _ => return Err(CheckError::new(CheckErrors::ContractCallExpectName)) - }; - - let map_name = args[1].match_atom() - .ok_or(CheckErrors::BadMapName)?; - - checker.type_map.set_type(&args[1], no_type())?; - - let key_type = check_and_type_map_arg_tuple(checker, &args[2], context)?; - - let (expected_key_type, value_type) = checker.db.get_map_type(&contract_identifier, map_name)?; - - let fetch_size = expected_key_type.type_size()? - .checked_add(value_type.type_size()?) - .ok_or_else(|| CheckErrors::CostOverflow)?; - runtime_cost!(cost_functions::ANALYSIS_FETCH_CONTRACT_ENTRY, &mut checker.cost_track, fetch_size)?; - analysis_typecheck_cost(&mut checker.cost_track, &expected_key_type, &key_type)?; - - let option_type = TypeSignature::new_option(value_type)?; - - if !expected_key_type.admits_type(&key_type) { - return Err(CheckError::new(CheckErrors::TypeError(expected_key_type.clone(), key_type))) - } else { - return Ok(option_type) - } -} - pub fn check_special_delete_entry(checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext) -> TypeResult { check_arguments_at_least(2, args)?; diff --git a/src/vm/analysis/type_checker/natives/mod.rs b/src/vm/analysis/type_checker/natives/mod.rs index 061d4b15c8..c39dcf7549 100644 --- a/src/vm/analysis/type_checker/natives/mod.rs +++ b/src/vm/analysis/type_checker/natives/mod.rs @@ -395,7 +395,6 @@ impl TypedNativeFunction { Len => Special(SpecialNativeFunction(&iterables::check_special_len)), ListCons => Special(SpecialNativeFunction(&check_special_list_cons)), FetchEntry => Special(SpecialNativeFunction(&maps::check_special_fetch_entry)), - FetchContractEntry => Special(SpecialNativeFunction(&maps::check_special_fetch_contract_entry)), SetEntry => Special(SpecialNativeFunction(&maps::check_special_set_entry)), InsertEntry => Special(SpecialNativeFunction(&maps::check_special_insert_entry)), DeleteEntry => Special(SpecialNativeFunction(&maps::check_special_delete_entry)), diff --git a/src/vm/analysis/type_checker/tests/contracts.rs b/src/vm/analysis/type_checker/tests/contracts.rs index 6b561d475d..5eda68b7cd 100644 --- a/src/vm/analysis/type_checker/tests/contracts.rs +++ b/src/vm/analysis/type_checker/tests/contracts.rs @@ -47,9 +47,7 @@ const SIMPLE_NAMES: &str = ((buyer principal) (paid uint))) (define-private (check-balance) - (default-to u0 - (get balance (contract-map-get? - .tokens tokens (tuple (account tx-sender)))))) + (contract-call? .tokens my-get-token-balance tx-sender)) (define-public (preorder (name-hash (buff 20)) @@ -411,43 +409,6 @@ fn test_names_tokens_contracts_bad() { }); } -#[test] -fn test_names_tokens_contracts_bad_fetch_contract_entry() { - let broken_public = " - (define-private (check-balance) - (default-to 0 - (get balance (contract-map-get? - .tokens tokens (tuple (accnt tx-sender)))))) ;; should be a non-admissable tuple! - "; - - let names_contract = - format!("{} - {}", SIMPLE_NAMES, broken_public); - - let tokens_contract_id = QualifiedContractIdentifier::local("tokens").unwrap(); - let names_contract_id = QualifiedContractIdentifier::local("names").unwrap(); - - let mut tokens_contract = parse(&tokens_contract_id, SIMPLE_TOKENS).unwrap(); - let mut names_contract = parse(&names_contract_id, &names_contract).unwrap(); - let mut marf = MemoryBackingStore::new(); - let mut db = marf.as_analysis_db(); - - db.execute(|db| { - db.test_insert_contract_hash(&tokens_contract_id); - type_check(&tokens_contract_id, &mut tokens_contract, db, true) - }).unwrap(); - - let err = db.execute(|db| type_check(&names_contract_id, &mut names_contract, db, true)).unwrap_err(); - assert!(match &err.err { - &CheckErrors::TypeError(ref expected_type, ref actual_type) => { - eprintln!("Received TypeError on: {} {}", expected_type, actual_type); - format!("{} {}", expected_type, actual_type) == "(tuple (account principal)) (tuple (accnt principal))" - }, - _ => false - }); -} - - #[test] fn test_bad_map_usage() { let bad_fetch = diff --git a/src/vm/analysis/type_checker/tests/mod.rs b/src/vm/analysis/type_checker/tests/mod.rs index 581eaf33ab..3eeb84d1eb 100644 --- a/src/vm/analysis/type_checker/tests/mod.rs +++ b/src/vm/analysis/type_checker/tests/mod.rs @@ -1671,129 +1671,3 @@ fn test_set_entry_unbound_variables() { }); } } - -#[test] -fn test_fetch_contract_entry_matching_type_signatures() { - let kv_store_contract_src = r#" - (define-map kv-store ((key int)) ((value int))) - (define-read-only (kv-get (key int)) - (unwrap! (get value (map-get? kv-store ((key key)))) 0)) - (begin (map-insert kv-store ((key 42)) ((value 42))))"#; - - let mut marf = MemoryBackingStore::new(); - let mut analysis_db = marf.as_analysis_db(); - - let contract_id = QualifiedContractIdentifier::local("kv-store-contract").unwrap(); - - let mut kv_store_contract = parse(&contract_id, &kv_store_contract_src).unwrap(); - analysis_db.execute(|db| { - db.test_insert_contract_hash(&contract_id); - type_check(&contract_id, &mut kv_store_contract, db, true) - }).unwrap(); - - let cases = [ - "contract-map-get? .kv-store-contract kv-store ((key key))", - "contract-map-get? .kv-store-contract kv-store ((key 0))", - "contract-map-get? .kv-store-contract kv-store (tuple (key 0))", - "contract-map-get? .kv-store-contract kv-store (compatible-tuple)", - ]; - - let transient_contract_id = QualifiedContractIdentifier::transient(); - - for case in cases.iter() { - let contract_src = format!(r#" - (define-private (compatible-tuple) (tuple (key 1))) - (define-private (kv-get (key int)) ({}))"#, case); - let mut contract = parse(&transient_contract_id, &contract_src).unwrap(); - analysis_db.execute(|db| { - type_check(&transient_contract_id, &mut contract, db, false) - }).unwrap(); - } -} - -#[test] -fn test_fetch_contract_entry_mismatching_type_signatures() { - let kv_store_contract_src = r#" - (define-map kv-store ((key int)) ((value int))) - (define-read-only (kv-get (key int)) - (unwrap! (get value (map-get? kv-store ((key key)))) 0)) - (begin (map-insert kv-store ((key 42)) ((value 42))))"#; - - let contract_id = QualifiedContractIdentifier::local("kv-store-contract").unwrap(); - let mut marf = MemoryBackingStore::new(); - let mut analysis_db = marf.as_analysis_db(); - - let mut kv_store_contract = parse(&contract_id, &kv_store_contract_src).unwrap(); - analysis_db.execute(|db| { - db.test_insert_contract_hash(&contract_id); - type_check(&contract_id, &mut kv_store_contract, db, true) - }).unwrap(); - - let cases = [ - "contract-map-get? .kv-store-contract kv-store ((incomptible-key key))", - "contract-map-get? .kv-store-contract kv-store ((key 'true))", - "contract-map-get? .kv-store-contract kv-store (incompatible-tuple)", - ]; - - let transient_contract_id = QualifiedContractIdentifier::transient(); - - for case in cases.iter() { - let contract_src = format!( - "(define-map kv-store ((key int)) ((value int))) - (define-private (incompatible-tuple) (tuple (k 1))) - (define-private (kv-get (key int)) - ({}))", case); - let mut contract = parse(&transient_contract_id, &contract_src).unwrap(); - let res = - analysis_db.execute(|db| { - type_check(&transient_contract_id, &mut contract, db, false) - }).unwrap_err(); - - assert!(match &res.err { - &CheckErrors::TypeError(_, _) => true, - _ => false - }); - } -} - -#[test] -fn test_fetch_contract_entry_unbound_variables() { - let kv_store_contract_src = r#" - (define-map kv-store ((key int)) ((value int))) - (define-read-only (kv-get (key int)) - (unwrap! (get value (map-get? kv-store ((key key)))) 0)) - (begin (map-insert kv-store ((key 42)) ((value 42))))"#; - - let contract_id = QualifiedContractIdentifier::local("kv-store-contract").unwrap(); - let mut marf = MemoryBackingStore::new(); - let mut analysis_db = marf.as_analysis_db(); - - let mut kv_store_contract = parse(&contract_id, &kv_store_contract_src).unwrap(); - analysis_db.execute(|db| { - db.test_insert_contract_hash(&contract_id); - type_check(&contract_id, &mut kv_store_contract, db, true) - }).unwrap(); - - let cases = [ - "contract-map-get? .kv-store-contract kv-store ((key unknown-value))", - ]; - - let transient_contract_id = QualifiedContractIdentifier::transient(); - - for case in cases.iter() { - let contract_src = format!( - "(define-map kv-store ((key int)) ((value int))) - (define-private (kv-get (key int)) - ({}))", case); - let mut contract = parse(&transient_contract_id, &contract_src).unwrap(); - let res = - analysis_db.execute(|db| { - type_check(&transient_contract_id, &mut contract, db, false) - }).unwrap_err(); - - assert!(match &res.err { - &CheckErrors::UndefinedVariable(_) => true, - _ => false - }); - } -} diff --git a/src/vm/ast/definition_sorter/mod.rs b/src/vm/ast/definition_sorter/mod.rs index 2a08252682..d8ec7dd44a 100644 --- a/src/vm/ast/definition_sorter/mod.rs +++ b/src/vm/ast/definition_sorter/mod.rs @@ -175,13 +175,6 @@ impl <'a> DefinitionSorter { } return Ok(()); }, - NativeFunctions::FetchContractEntry => { - // Args: [contract-name, map-name, tuple-predicate]: ignore contract-name, map-name, handle tuple-predicate as tuple - if function_args.len() == 3 { - self.probe_for_dependencies_in_tuple(&function_args[2], tle_index)?; - } - return Ok(()); - }, NativeFunctions::Let => { // Args: [((name-1 value-1) (name-2 value-2)), ...]: handle 1st arg as a tuple if function_args.len() > 1 { diff --git a/src/vm/ast/definition_sorter/tests.rs b/src/vm/ast/definition_sorter/tests.rs index 1951e6be7e..c74855007f 100644 --- a/src/vm/ast/definition_sorter/tests.rs +++ b/src/vm/ast/definition_sorter/tests.rs @@ -221,16 +221,6 @@ fn should_raise_dependency_cycle_case_insert_entry() { assert!(match err.err { ParseErrors::CircularReference(_) => true, _ => false}) } -#[test] -fn should_not_raise_dependency_cycle_case_fetch_contract_entry() { - let contract = r#" - (define-private (foo (x int)) (begin (bar 1) 1)) - (define-private (bar (x int)) (contract-map-get? .contract1 kv-store ((foo 1)))) - "#; - - run_scoped_parsing_helper(contract).unwrap(); -} - #[test] fn should_raise_dependency_cycle_case_fetch_contract_entry() { let contract = r#" diff --git a/src/vm/ast/errors.rs b/src/vm/ast/errors.rs index c5e8615e6a..45eafce2f7 100644 --- a/src/vm/ast/errors.rs +++ b/src/vm/ast/errors.rs @@ -12,6 +12,7 @@ pub type ParseResult = Result; pub enum ParseErrors { CostOverflow, CostBalanceExceeded(ExecutionCost, ExecutionCost), + MemoryBalanceExceeded(u64, u64), TooManyExpressions, ExpressionStackDepthTooDeep, FailedCapturingInput, @@ -107,6 +108,7 @@ impl From for ParseError { match err { CostErrors::CostOverflow => ParseError::new(ParseErrors::CostOverflow), CostErrors::CostBalanceExceeded(a,b) => ParseError::new(ParseErrors::CostBalanceExceeded(a,b)), + CostErrors::MemoryBalanceExceeded(a,b) => ParseError::new(ParseErrors::MemoryBalanceExceeded(a,b)), } } } @@ -117,6 +119,7 @@ impl DiagnosableError for ParseErrors { match &self { ParseErrors::CostOverflow => format!("Used up cost budget during the parse"), ParseErrors::CostBalanceExceeded(bal, used) => format!("Used up cost budget during the parse: {} balance, {} used", bal, used), + ParseErrors::MemoryBalanceExceeded(bal, used) => format!("Used up memory budget during the parse: {} balance, {} used", bal, used), ParseErrors::TooManyExpressions => format!("Too many expressions"), ParseErrors::FailedCapturingInput => format!("Failed to capture value from input"), ParseErrors::SeparatorExpected(found) => format!("Expected whitespace or a close parens. Found: '{}'", found), diff --git a/src/vm/clarity.rs b/src/vm/clarity.rs index 11b2c150ff..3f5cc1ce0f 100644 --- a/src/vm/clarity.rs +++ b/src/vm/clarity.rs @@ -8,7 +8,7 @@ use vm::ast::{ContractAST, errors::ParseError}; use vm::analysis::{ContractAnalysis, errors::CheckError, errors::CheckErrors}; use vm::ast; use vm::analysis; -use vm::costs::{LimitedCostTracker, ExecutionCost}; +use vm::costs::{LimitedCostTracker, ExecutionCost, CostTracker}; use chainstate::burn::BlockHeaderHash; use chainstate::stacks::index::marf::MARF; @@ -343,13 +343,24 @@ impl <'a> ClarityBlockConnection <'a> { let cost_track = self.cost_track.take() .expect("Failed to get ownership of cost tracker in ClarityBlockConnection"); - let mut contract_analysis = analysis::run_analysis(identifier, &mut contract_ast.expressions, - &mut db, false, cost_track)?; + let result = analysis::run_analysis( + identifier, &mut contract_ast.expressions, + &mut db, false, cost_track); - let cost_track = contract_analysis.take_contract_cost_tracker(); + let (mut cost_track, result) = match result { + Ok(mut contract_analysis) => { + let cost_track = contract_analysis.take_contract_cost_tracker(); + (cost_track, Ok((contract_ast, contract_analysis))) + }, + Err((e, cost_track)) => { + (cost_track, Err(e.into())) + } + }; + + cost_track.reset_memory(); self.cost_track.replace(cost_track); - Ok((contract_ast, contract_analysis)) + result } fn with_abort_callback(&mut self, to_do: F, abort_call_back: A) -> Result<(R, AssetMap, Vec), Error> @@ -363,8 +374,11 @@ impl <'a> ClarityBlockConnection <'a> { .expect("Failed to get ownership of cost tracker in ClarityBlockConnection"); let mut vm_env = OwnedEnvironment::new_cost_limited(db, cost_track); let result = to_do(&mut vm_env); - let (mut db, cost_track) = vm_env.destruct() + let (mut db, mut cost_track) = vm_env.destruct() .expect("Failed to recover database reference after executing transaction"); + // reset memory usage in cost_track -- this wipes out + // the memory tracking used for the k-v wrapper / replay log + cost_track.reset_memory(); self.cost_track.replace(cost_track); match result { @@ -467,6 +481,32 @@ mod tests { use chainstate::stacks::index::storage::{TrieFileStorage}; use rusqlite::NO_PARAMS; + #[test] + pub fn bad_syntax_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + + let contract_identifier = QualifiedContractIdentifier::local("foo").unwrap(); + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let contract = "(define-public (foo (x int) (y uint)) (ok (+ x y)))"; + + let _e = conn.analyze_smart_contract(&contract_identifier, &contract) + .unwrap_err(); + + // okay, let's try it again: + + let _e = conn.analyze_smart_contract(&contract_identifier, &contract) + .unwrap_err(); + + conn.commit_block(); + } + } + #[test] pub fn simple_test() { let marf = MarfedKV::temporary(); diff --git a/src/vm/contexts.rs b/src/vm/contexts.rs index becbd8d64c..29f6cee7ba 100644 --- a/src/vm/contexts.rs +++ b/src/vm/contexts.rs @@ -88,6 +88,7 @@ pub struct ContractContext { // tracks the names of NFTs, FTs, Maps, and Data Vars. // used for ensuring that they never are defined twice. pub persisted_names: HashSet, + pub data_size: u64 } pub struct LocalContext <'a> { @@ -484,12 +485,30 @@ impl CostTracker for Environment<'_,'_> { fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { self.global_context.cost_track.add_cost(cost) } + fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> { + self.global_context.cost_track.add_memory(memory) + } + fn drop_memory(&mut self, memory: u64) { + self.global_context.cost_track.drop_memory(memory) + } + fn reset_memory(&mut self) { + self.global_context.cost_track.reset_memory() + } } impl CostTracker for GlobalContext<'_> { fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { self.cost_track.add_cost(cost) } + fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> { + self.cost_track.add_memory(memory) + } + fn drop_memory(&mut self, memory: u64) { + self.cost_track.drop_memory(memory) + } + fn reset_memory(&mut self) { + self.cost_track.reset_memory() + } } impl <'a,'b> Environment <'a,'b> { @@ -580,33 +599,37 @@ impl <'a,'b> Environment <'a,'b> { let contract_size = self.global_context.database.get_contract_size(contract_identifier)?; runtime_cost!(cost_functions::LOAD_CONTRACT, self, contract_size)?; - let contract = self.global_context.database.get_contract(contract_identifier)?; + self.global_context.add_memory(contract_size)?; - let func = contract.contract_context.lookup_function(tx_name) - .ok_or_else(|| { CheckErrors::UndefinedFunction(tx_name.to_string()) })?; - if !func.is_public() { - return Err(CheckErrors::NoSuchPublicFunction(contract_identifier.to_string(), tx_name.to_string()).into()); - } + finally_drop_memory!(self.global_context, contract_size; { + let contract = self.global_context.database.get_contract(contract_identifier)?; - let args: Result> = args.iter() - .map(|arg| { - let value = arg.match_atom_value() - .ok_or_else(|| InterpreterError::InterpreterError(format!("Passed non-value expression to exec_tx on {}!", - tx_name)))?; - Ok(value.clone()) - }) - .collect(); + let func = contract.contract_context.lookup_function(tx_name) + .ok_or_else(|| { CheckErrors::UndefinedFunction(tx_name.to_string()) })?; + if !func.is_public() { + return Err(CheckErrors::NoSuchPublicFunction(contract_identifier.to_string(), tx_name.to_string()).into()); + } - let args = args?; + let args: Result> = args.iter() + .map(|arg| { + let value = arg.match_atom_value() + .ok_or_else(|| InterpreterError::InterpreterError(format!("Passed non-value expression to exec_tx on {}!", + tx_name)))?; + Ok(value.clone()) + }) + .collect(); - let func_identifier = func.get_identifier(); - if self.call_stack.contains(&func_identifier) { - return Err(CheckErrors::CircularReference(vec![func_identifier.to_string()]).into()) - } - self.call_stack.insert(&func_identifier, true); - let res = self.execute_function_as_transaction(&func, &args, Some(&contract.contract_context)); - self.call_stack.remove(&func_identifier, true)?; - res + let args = args?; + + let func_identifier = func.get_identifier(); + if self.call_stack.contains(&func_identifier) { + return Err(CheckErrors::CircularReference(vec![func_identifier.to_string()]).into()) + } + self.call_stack.insert(&func_identifier, true); + let res = self.execute_function_as_transaction(&func, &args, Some(&contract.contract_context)); + self.call_stack.remove(&func_identifier, true)?; + res + }) } pub fn execute_function_as_transaction(&mut self, function: &DefinedFunction, args: &[Value], @@ -667,13 +690,21 @@ impl <'a,'b> Environment <'a,'b> { // this is necessary before creating and accessing metadata fields in the data store, // --or-- storing any analysis metadata in the data store. self.global_context.database.insert_contract_hash(&contract_identifier, contract_string)?; + let memory_use = contract_string.len() as u64; + self.add_memory(memory_use)?; let result = Contract::initialize_from_ast(contract_identifier.clone(), contract_content, &mut self.global_context); + self.drop_memory(memory_use); + match result { Ok(contract) => { + let data_size = contract.contract_context.data_size; self.global_context.database.insert_contract(&contract_identifier, contract); + self.global_context.database.set_contract_data_size( + &contract_identifier, data_size)?; + self.global_context.commit()?; Ok(()) }, @@ -937,6 +968,7 @@ impl ContractContext { defined_traits: HashMap::new(), implemented_traits: HashSet::new(), persisted_names: HashSet::new(), + data_size: 0 } } diff --git a/src/vm/costs/constants.rs b/src/vm/costs/constants.rs new file mode 100644 index 0000000000..c9310cfe55 --- /dev/null +++ b/src/vm/costs/constants.rs @@ -0,0 +1,2 @@ +pub const AS_CONTRACT_MEMORY: u64 = 1; +pub const AT_BLOCK_MEMORY: u64 = 1; diff --git a/src/vm/costs/mod.rs b/src/vm/costs/mod.rs index eae02adff1..0ce9e20f8c 100644 --- a/src/vm/costs/mod.rs +++ b/src/vm/costs/mod.rs @@ -1,11 +1,15 @@ pub mod cost_functions; +pub mod constants; use std::{fmt, cmp}; use vm::types::TypeSignature; +use vm::Value; use std::convert::TryFrom; type Result = std::result::Result; +pub const CLARITY_MEMORY_LIMIT: u64 = 100 * 1000 * 1000; + macro_rules! runtime_cost { ( $cost_spec:expr, $env:expr, $input:expr ) => { { @@ -24,6 +28,16 @@ macro_rules! runtime_cost { } } +macro_rules! finally_drop_memory { + ( $env: expr, $used_mem:expr; $exec:expr ) => { + { + let result = (|| { $exec })(); + $env.drop_memory($used_mem); + result + } + } +} + pub fn analysis_typecheck_cost(track: &mut T, t1: &TypeSignature, t2: &TypeSignature) -> Result<()> { let t1_size = t1.type_size() .map_err(|_| CostErrors::CostOverflow)?; @@ -35,8 +49,22 @@ pub fn analysis_typecheck_cost(track: &mut T, t1: &TypeSignature pub struct TypeCheckCost {} +pub trait MemoryConsumer { + fn get_memory_use(&self) -> u64; +} + +impl MemoryConsumer for Value { + fn get_memory_use(&self) -> u64 { + self.size().into() + } +} + pub trait CostTracker { fn add_cost(&mut self, cost: ExecutionCost) -> Result<()>; + + fn add_memory(&mut self, memory: u64) -> Result<()>; + fn drop_memory(&mut self, memory: u64); + fn reset_memory(&mut self); } // Don't track! @@ -44,51 +72,93 @@ impl CostTracker for () { fn add_cost(&mut self, _cost: ExecutionCost) -> std::result::Result<(), CostErrors> { Ok(()) } + + fn add_memory(&mut self, _memory: u64) -> std::result::Result<(), CostErrors> { + Ok(()) + } + fn drop_memory(&mut self, _memory: u64) {} + fn reset_memory(&mut self) {} } #[derive(Debug, Clone, PartialEq)] pub struct LimitedCostTracker { total: ExecutionCost, - limit: ExecutionCost + limit: ExecutionCost, + memory: u64, + memory_limit: u64 } #[derive(Debug, PartialEq, Eq)] pub enum CostErrors { CostOverflow, CostBalanceExceeded(ExecutionCost, ExecutionCost), + MemoryBalanceExceeded(u64, u64), } impl LimitedCostTracker { pub fn new(limit: ExecutionCost) -> LimitedCostTracker { - LimitedCostTracker { limit, total: ExecutionCost::zero() } + LimitedCostTracker { limit, memory_limit: CLARITY_MEMORY_LIMIT, + total: ExecutionCost::zero(), memory: 0 } } pub fn new_max_limit() -> LimitedCostTracker { - LimitedCostTracker { limit: ExecutionCost::max_value(), total: ExecutionCost::zero() } + LimitedCostTracker { limit: ExecutionCost::max_value(), total: ExecutionCost::zero(), + memory: 0, memory_limit: CLARITY_MEMORY_LIMIT } } pub fn get_total(&self) -> ExecutionCost { self.total.clone() } } +fn add_cost(s: &mut LimitedCostTracker, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { + s.total.add(&cost)?; + if s.total.exceeds(&s.limit) { + Err(CostErrors::CostBalanceExceeded(s.total.clone(), s.limit.clone())) + } else { + Ok(()) + } +} + +fn add_memory(s: &mut LimitedCostTracker, memory: u64) -> std::result::Result<(), CostErrors> { + s.memory = s.memory.cost_overflow_add(memory)?; + if s.memory > s.memory_limit { + Err(CostErrors::MemoryBalanceExceeded(s.memory, s.memory_limit)) + } else { + Ok(()) + } +} + +fn drop_memory(s: &mut LimitedCostTracker, memory: u64) { + s.memory = s.memory.checked_sub(memory) + .expect("Underflowed dropped memory"); +} + impl CostTracker for LimitedCostTracker { fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { - self.total.add(&cost)?; - if self.total.exceeds(&self.limit) { - Err(CostErrors::CostBalanceExceeded(self.total.clone(), self.limit.clone())) - } else { - Ok(()) - } + add_cost(self, cost) + } + fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> { + add_memory(self, memory) + } + fn drop_memory(&mut self, memory: u64) { + drop_memory(self, memory) + } + fn reset_memory(&mut self) { + self.memory = 0; } } impl CostTracker for &mut LimitedCostTracker { fn add_cost(&mut self, cost: ExecutionCost) -> std::result::Result<(), CostErrors> { - self.total.add(&cost)?; - if self.total.exceeds(&self.limit) { - Err(CostErrors::CostBalanceExceeded(self.total.clone(), self.limit.clone())) - } else { - Ok(()) - } + add_cost(self, cost) + } + fn add_memory(&mut self, memory: u64) -> std::result::Result<(), CostErrors> { + add_memory(self, memory) + } + fn drop_memory(&mut self, memory: u64) { + drop_memory(self, memory) + } + fn reset_memory(&mut self) { + self.memory = 0; } } @@ -245,16 +315,6 @@ impl CostFunctions { } impl SimpleCostSpecification { - pub fn new_diskless(runtime: CostFunctions) -> SimpleCostSpecification { - SimpleCostSpecification { - write_length: CostFunctions::Constant(0), - write_count: CostFunctions::Constant(0), - read_count: CostFunctions::Constant(0), - read_length: CostFunctions::Constant(0), - runtime - } - } - pub fn compute_cost(&self, input: u64) -> Result { Ok(ExecutionCost { write_length: self.write_length.compute_cost(input)?, diff --git a/src/vm/database/clarity_db.rs b/src/vm/database/clarity_db.rs index a3c78b402f..c5455dacaf 100644 --- a/src/vm/database/clarity_db.rs +++ b/src/vm/database/clarity_db.rs @@ -20,6 +20,7 @@ use vm::database::structures::{ use vm::database::RollbackWrapper; use util::db::{DBConn, FromRow}; use chainstate::stacks::StacksAddress; +use vm::costs::CostOverflowingMath; const SIMMED_BLOCK_TIME: u64 = 10 * 60; // 10 min @@ -169,13 +170,11 @@ impl <'a> ClarityDatabase <'a> { } fn get (&mut self, key: &str) -> Option where T: ClarityDeserializable { - self.store.get(&key) - .map(|x| T::deserialize(&x)) + self.store.get::(key) } pub fn get_value (&mut self, key: &str, expected: &TypeSignature) -> Option { - self.store.get(&key) - .map(|json| Value::deserialize(&json, expected)) + self.store.get_value(key, expected) } pub fn make_key_for_trip(contract_identifier: &QualifiedContractIdentifier, data: StoreType, var_name: &str) -> String { @@ -216,9 +215,26 @@ impl <'a> ClarityDatabase <'a> { pub fn get_contract_size(&mut self, contract_identifier: &QualifiedContractIdentifier) -> Result { let key = ClarityDatabase::make_metadata_key(StoreType::Contract, "contract-size"); - let data = self.fetch_metadata(contract_identifier, &key)? + let contract_size: u64 = self.fetch_metadata(contract_identifier, &key)? .expect("Failed to read non-consensus contract metadata, even though contract exists in MARF."); - Ok(data) + let key = ClarityDatabase::make_metadata_key(StoreType::Contract, "contract-data-size"); + let data_size: u64 = self.fetch_metadata(contract_identifier, &key)? + .expect("Failed to read non-consensus contract metadata, even though contract exists in MARF."); + + // u64 overflow is _checked_ on insert into contract-data-size + Ok(data_size + contract_size) + } + + /// used for adding the memory usage of `define-constant` variables. + pub fn set_contract_data_size(&mut self, contract_identifier: &QualifiedContractIdentifier, data_size: u64) -> Result<()> { + let key = ClarityDatabase::make_metadata_key(StoreType::Contract, "contract-size"); + let contract_size: u64 = self.fetch_metadata(contract_identifier, &key)? + .expect("Failed to read non-consensus contract metadata, even though contract exists in MARF."); + contract_size.cost_overflow_add(data_size)?; + + let key = ClarityDatabase::make_metadata_key(StoreType::Contract, "contract-data-size"); + self.insert_metadata(contract_identifier, &key, &data_size); + Ok(()) } pub fn insert_contract(&mut self, contract_identifier: &QualifiedContractIdentifier, contract: Contract) { diff --git a/src/vm/database/key_value_wrapper.rs b/src/vm/database/key_value_wrapper.rs index 6bce226b36..4b92088a31 100644 --- a/src/vm/database/key_value_wrapper.rs +++ b/src/vm/database/key_value_wrapper.rs @@ -1,14 +1,71 @@ -use super::{MarfedKV, ClarityBackingStore}; +use super::{MarfedKV, ClarityBackingStore, ClarityDeserializable}; +use vm::Value; use vm::errors::{ InterpreterResult as Result }; use chainstate::burn::BlockHeaderHash; use std::collections::{HashMap}; use util::hash::{Sha512Trunc256Sum}; -use vm::types::QualifiedContractIdentifier; +use vm::types::{QualifiedContractIdentifier, TypeSignature}; use std::{cmp::Eq, hash::Hash, clone::Clone}; +#[cfg(rollback_value_check)] +type RollbackValueCheck = String; +#[cfg(not(rollback_value_check))] +type RollbackValueCheck = (); + +#[cfg(not(rollback_value_check))] +fn rollback_value_check(_value: &String, _check: &RollbackValueCheck) {} + +#[cfg(not(rollback_value_check))] +fn rollback_edits_push(edits: &mut Vec<(T, RollbackValueCheck)>, key: T, _value: &String) { + edits.push((key, ())); +} +// this function is used to check the lookup map when committing at the "bottom" of the +// wrapper -- i.e., when committing to the underlying store. for the _unchecked_ implementation +// this is used to get the edit _value_ out of the lookupmap, for used in the subsequent `put_all` +// command. +#[cfg(not(rollback_value_check))] +fn rollback_check_pre_bottom_commit(edits: Vec<(T, RollbackValueCheck)>, lookup_map: &mut HashMap>) -> Vec<(T, String)> +where T: Eq + Hash + Clone { + for (_, edit_history) in lookup_map.iter_mut() { + edit_history.reverse(); + } + + let output = edits.into_iter().map(|(key, _)| { + let value = rollback_lookup_map(&key, &(), lookup_map); + (key, value) + }).collect(); + + assert!(lookup_map.len() == 0); + output +} + +#[cfg(rollback_value_check)] +fn rollback_value_check(value: &String, check: &RollbackValueCheck) { + assert_eq!(value, check) +} +#[cfg(rollback_value_check)] +fn rollback_edits_push(edits: &mut Vec<(T, RollbackValueCheck)>, key: T, value: &String) +where T: Eq + Hash + Clone { + edits.push((key, value.clone())); +} +// this function is used to check the lookup map when committing at the "bottom" of the +// wrapper -- i.e., when committing to the underlying store. +#[cfg(rollback_value_check)] +fn rollback_check_pre_bottom_commit(edits: Vec<(T, RollbackValueCheck)>, lookup_map: &mut HashMap>) -> Vec<(T, String)> +where T: Eq + Hash + Clone { + for (_, edit_history) in lookup_map.iter_mut() { + edit_history.reverse(); + } + for (key, value) in edits.iter() { + rollback_lookup_map(key, &value, lookup_map); + } + assert!(lookup_map.len() == 0); + edits +} + pub struct RollbackContext { - edits: Vec<(String, String)>, - metadata_edits: Vec<((QualifiedContractIdentifier, String), String)>, + edits: Vec<(String, RollbackValueCheck)>, + metadata_edits: Vec<((QualifiedContractIdentifier, String), RollbackValueCheck)>, } pub struct RollbackWrapper <'a> { @@ -29,18 +86,20 @@ pub struct RollbackWrapper <'a> { stack: Vec } -fn rollback_lookup_map(key: &T, value: &String, lookup_map: &mut HashMap>) +fn rollback_lookup_map(key: &T, value: &RollbackValueCheck, lookup_map: &mut HashMap>) -> String where T: Eq + Hash + Clone { + let popped_value; let remove_edit_deque = { let key_edit_history = lookup_map.get_mut(key) .expect("ERROR: Clarity VM had edit log entry, but not lookup_map entry"); - let popped_value = key_edit_history.pop(); - assert_eq!(popped_value.as_ref(), Some(value)); + popped_value = key_edit_history.pop().unwrap(); + rollback_value_check(&popped_value, value); key_edit_history.len() == 0 }; if remove_edit_deque { lookup_map.remove(key); } + popped_value } impl <'a> RollbackWrapper <'a> { @@ -83,30 +142,16 @@ impl <'a> RollbackWrapper <'a> { if self.stack.len() == 0 { // committing to the backing store - // reverse the lookup_map entries, because we want to commit them - // in the order they were performed, but we want to use pop() - // rather than remove(0) - for (_, edit_history) in self.lookup_map.iter_mut() { - edit_history.reverse(); - } - for (key, value) in last_item.edits.iter() { - rollback_lookup_map(key, &value, &mut self.lookup_map); + let all_edits = rollback_check_pre_bottom_commit( + last_item.edits, &mut self.lookup_map); + if all_edits.len() > 0 { + self.store.put_all(all_edits); } - assert!(self.lookup_map.len() == 0); - if last_item.edits.len() > 0 { - self.store.put_all(last_item.edits); - } - - for (_, edit_history) in self.metadata_lookup_map.iter_mut() { - edit_history.reverse(); - } - for (key, value) in last_item.metadata_edits.iter() { - rollback_lookup_map(key, &value, &mut self.metadata_lookup_map); - } - assert!(self.metadata_lookup_map.len() == 0); - if last_item.metadata_edits.len() > 0 { - self.store.put_all_metadata(last_item.metadata_edits); + let metadata_edits = rollback_check_pre_bottom_commit( + last_item.metadata_edits, &mut self.metadata_lookup_map); + if metadata_edits.len() > 0 { + self.store.put_all_metadata(metadata_edits); } } else { // bubble up to the next item in the stack @@ -121,15 +166,14 @@ impl <'a> RollbackWrapper <'a> { } } -fn inner_put(lookup_map: &mut HashMap>, edits: &mut Vec<(T, String)>, key: T, value: String) +fn inner_put(lookup_map: &mut HashMap>, edits: &mut Vec<(T, RollbackValueCheck)>, key: T, value: String) where T: Eq + Hash + Clone { if !lookup_map.contains_key(&key) { lookup_map.insert(key.clone(), Vec::new()); } let key_edit_deque = lookup_map.get_mut(&key).unwrap(); - key_edit_deque.push(value.clone()); - - edits.push((key, value)); + rollback_edits_push(edits, key, &value); + key_edit_deque.push(value); } impl <'a> RollbackWrapper <'a> { @@ -144,15 +188,28 @@ impl <'a> RollbackWrapper <'a> { self.store.set_block_hash(bhh) } - pub fn get(&mut self, key: &str) -> Option { + pub fn get(&mut self, key: &str) -> Option where T: ClarityDeserializable { self.stack.last() .expect("ERROR: Clarity VM attempted GET on non-nested context."); let lookup_result = self.lookup_map.get(key) - .and_then(|x| x.last().cloned()); + .and_then(|x| x.last()) + .map(|x| T::deserialize(x)); + + lookup_result + .or_else(|| self.store.get(key).map(|x| T::deserialize(&x))) + } + + pub fn get_value(&mut self, key: &str, expected: &TypeSignature) -> Option { + self.stack.last() + .expect("ERROR: Clarity VM attempted GET on non-nested context."); + + let lookup_result = self.lookup_map.get(key) + .and_then(|x| x.last()) + .map(|x| Value::deserialize(x, expected)); lookup_result - .or_else(|| self.store.get(key)) + .or_else(|| self.store.get(key).map(|x| Value::deserialize(&x, expected))) } pub fn get_current_block_height(&mut self) -> u32 { diff --git a/src/vm/database/sqlite.rs b/src/vm/database/sqlite.rs index 2afa34964b..1bd232bf94 100644 --- a/src/vm/database/sqlite.rs +++ b/src/vm/database/sqlite.rs @@ -91,18 +91,18 @@ impl SqliteConnection { /// ClarityDatabase or AnalysisDatabase -- this is done at the backing store level. pub fn begin(&mut self, key: &BlockHeaderHash) { - self.conn.execute(&format!("SAVEPOINT SP{};", key), NO_PARAMS) + self.conn.execute(&format!("SAVEPOINT SP{}", key), NO_PARAMS) .expect(SQL_FAIL_MESSAGE); } pub fn rollback(&mut self, key: &BlockHeaderHash) { - self.conn.execute(&format!("ROLLBACK TO SAVEPOINT SP{};", key), NO_PARAMS) + self.conn.execute_batch(&format!("ROLLBACK TO SAVEPOINT SP{}; RELEASE SAVEPOINT SP{}", key, key)) .expect(SQL_FAIL_MESSAGE); } pub fn commit(&mut self, key: &BlockHeaderHash) { - self.conn.execute(&format!("RELEASE SAVEPOINT SP{};", key), NO_PARAMS) - .expect(SQL_FAIL_MESSAGE); + self.conn.execute(&format!("RELEASE SAVEPOINT SP{}", key), NO_PARAMS) + .expect("PANIC: Failed to SQL commit in Smart Contract VM."); } } @@ -154,3 +154,15 @@ impl SqliteConnection { &mut self.conn } } + + +#[cfg(test)] +#[test] +#[should_panic(expected = "Failed to SQL commit")] +fn test_rollback() { + let mut conn = SqliteConnection::memory().unwrap(); + let bhh = BlockHeaderHash([1; 32]); + conn.begin(&bhh); + conn.rollback(&bhh); + conn.commit(&bhh); // shouldn't be on the stack! +} diff --git a/src/vm/docs/mod.rs b/src/vm/docs/mod.rs index b23e4fb028..c2585f935d 100644 --- a/src/vm/docs/mod.rs +++ b/src/vm/docs/mod.rs @@ -82,7 +82,7 @@ const NONE_KEYWORD: KeywordAPI = KeywordAPI { output_type: "(optional ?)", description: "Represents the _none_ option indicating no value for a given optional (analogous to a null value).", example: " -(define (only-if-positive (a int)) +(define-public (only-if-positive (a int)) (if (> a 0) (some a) none)) @@ -485,19 +485,6 @@ If a value did not exist for this key in the data map, the function returns `fal ", }; -const FETCH_CONTRACT_API: SpecialAPI = SpecialAPI { - input_type: "ContractName, MapName, tuple", - output_type: "(optional (tuple))", - signature: "(contract-map-get? .contract-name map-name key-tuple)", - description: "The `contract-map-get?` function looks up and returns an entry from a -contract other than the current contract's data map. The value is looked up using `key-tuple`. -If there is no value associated with that key in the data map, the function returns a `none` option. Otherwise, -it returns `(some value)`.", - example: "(unwrap-panic (contract-map-get? .names-contract names-map (tuple (name \"blockstack\"))) ;; Returns (tuple (id 1337)) -(unwrap-panic (contract-map-get? .names-contract names-map ((name \"blockstack\"))));; Same command, using a shorthand for constructing the tuple -", -}; - const TUPLE_CONS_API: SpecialAPI = SpecialAPI { input_type: "(key-name A), (key-name-2 B), ...", output_type: "(tuple (key-name A) (key-name-2 B) ...)", @@ -645,7 +632,7 @@ option. If the argument is a response type, and the argument is an `(ok ...)` re `try!` _returns_ either `none` or the `(err ...)` value from the current function and exits the current control-flow.", example: "(try! (map-get? names-map (tuple (name \"blockstack\"))) (err 1)) ;; Returns (tuple (id 1337)) (define-private (checked-even (x int)) - (if (eq? (mod x 2) 0) + (if (is-eq (mod x 2) 0) (ok x) (err 'false))) (define-private (double-if-even (x int)) @@ -973,7 +960,7 @@ Like other kinds of definition statements, `define-map` may only be used at the definition (i.e., you cannot put a define statement in the middle of a function body).", example: " (define-map squares ((x int)) ((square int))) -(define (add-entry (x int)) +(define-private (add-entry (x int)) (map-insert squares ((x 2)) ((square (* x x))))) (add-entry 1) (add-entry 2) @@ -996,7 +983,7 @@ Like other kinds of definition statements, `define-data-var` may only be used at definition (i.e., you cannot put a define statement in the middle of a function body).", example: " (define-data-var size int 0) -(define (set-size (value int)) +(define-private (set-size (value int)) (var-set size value)) (set-size 1) (set-size 2) @@ -1245,7 +1232,6 @@ fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { Len => make_for_special(&LEN_API, name), ListCons => make_for_special(&LIST_API, name), FetchEntry => make_for_special(&FETCH_ENTRY_API, name), - FetchContractEntry => make_for_special(&FETCH_CONTRACT_API, name), SetEntry => make_for_special(&SET_ENTRY_API, name), InsertEntry => make_for_special(&INSERT_ENTRY_API, name), DeleteEntry => make_for_special(&DELETE_ENTRY_API, name), diff --git a/src/vm/functions/assets.rs b/src/vm/functions/assets.rs index ce600171d8..450bdd52e6 100644 --- a/src/vm/functions/assets.rs +++ b/src/vm/functions/assets.rs @@ -5,7 +5,7 @@ use vm::types::{Value, OptionalData, BuffData, PrincipalData, BlockInfoProperty, use vm::representations::{SymbolicExpression}; use vm::errors::{Error, InterpreterError, CheckErrors, RuntimeErrorType, InterpreterResult as Result, check_argument_count}; use vm::{eval, LocalContext, Environment}; -use vm::costs::cost_functions; +use vm::costs::{cost_functions, CostTracker}; use std::convert::{TryFrom}; enum MintAssetErrorCodes { ALREADY_EXIST = 1 } @@ -55,6 +55,11 @@ pub fn special_stx_transfer(args: &[SymbolicExpression], let final_to_bal = to_bal.checked_add(amount) .ok_or(RuntimeErrorType::ArithmeticOverflow)?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.global_context.database.set_account_stx_balance(&from, final_from_bal); env.global_context.database.set_account_stx_balance(&to, final_to_bal); @@ -94,6 +99,9 @@ pub fn special_stx_burn(args: &[SymbolicExpression], let final_from_bal = from_bal - amount; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.global_context.database.set_account_stx_balance(&from, final_from_bal); env.global_context.log_stx_burn(&from, amount)?; @@ -133,6 +141,9 @@ pub fn special_mint_token(args: &[SymbolicExpression], let final_to_bal = to_bal.checked_add(amount) .ok_or(RuntimeErrorType::ArithmeticOverflow)?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.global_context.database.set_ft_balance(&env.contract_context.contract_identifier, token_name, to_principal, final_to_bal)?; let asset_identifier = AssetIdentifier { @@ -173,6 +184,9 @@ pub fn special_mint_asset(args: &[SymbolicExpression], Err(e) => Err(e) }?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(expected_asset_type.size() as u64)?; + env.global_context.database.set_nft_owner(&env.contract_context.contract_identifier, asset_name, &asset, to_principal)?; let asset_identifier = AssetIdentifier { @@ -227,6 +241,9 @@ pub fn special_transfer_asset(args: &[SymbolicExpression], return clarity_ecode!(TransferAssetErrorCodes::NOT_OWNED_BY) } + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(expected_asset_type.size() as u64)?; + env.global_context.database.set_nft_owner(&env.contract_context.contract_identifier, asset_name, &asset, to_principal)?; env.global_context.log_asset_transfer(from_principal, &env.contract_context.contract_identifier, asset_name, asset.clone()); @@ -281,6 +298,11 @@ pub fn special_transfer_token(args: &[SymbolicExpression], let final_to_bal = to_bal.checked_add(amount) .ok_or(RuntimeErrorType::ArithmeticOverflow)?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + env.global_context.database.set_ft_balance(&env.contract_context.contract_identifier, token_name, from_principal, final_from_bal)?; env.global_context.database.set_ft_balance(&env.contract_context.contract_identifier, token_name, to_principal, final_to_bal)?; diff --git a/src/vm/functions/database.rs b/src/vm/functions/database.rs index 0dc65cc440..d8b3d24ca7 100644 --- a/src/vm/functions/database.rs +++ b/src/vm/functions/database.rs @@ -8,7 +8,7 @@ use vm::types::{Value, OptionalData, BuffData, PrincipalData, BlockInfoProperty, use vm::representations::{SymbolicExpression, SymbolicExpressionType}; use vm::errors::{CheckErrors, InterpreterError, RuntimeErrorType, InterpreterResult as Result, check_argument_count, check_arguments_at_least}; -use vm::costs::cost_functions; +use vm::costs::{cost_functions, constants as cost_constants, CostTracker, MemoryConsumer}; use vm::{eval, LocalContext, Environment}; use vm::callables::{DefineType}; use chainstate::burn::{BlockHeaderHash}; @@ -153,6 +153,8 @@ pub fn special_set_variable(args: &[SymbolicExpression], let data_types = env.global_context.database.load_variable(contract, var_name)?; runtime_cost!(cost_functions::SET_VAR, env, data_types.value_type.size())?; + env.add_memory(value.get_memory_use())?; + env.global_context.database.set_variable(contract, var_name, value) } @@ -199,35 +201,11 @@ pub fn special_at_block(args: &[SymbolicExpression], x => return Err(CheckErrors::TypeValueError(BUFF_32.clone(), x).into()) }; - env.evaluate_at_block(bhh, &args[1], context) -} - -pub fn special_fetch_contract_entry(args: &[SymbolicExpression], - env: &mut Environment, - context: &LocalContext) -> Result { - check_argument_count(3, args)?; - - let contract_identifier = match args[0].expr { - SymbolicExpressionType::LiteralValue(Value::Principal(PrincipalData::Contract(ref contract_identifier))) => contract_identifier, - _ => return Err(CheckErrors::ContractCallExpectName.into()) - }; - - let map_name = args[1].match_atom() - .ok_or(CheckErrors::ExpectedName)?; - - let key = match tuples::get_definition_type_of_tuple_argument(&args[2]) { - Implicit(ref expr) => tuples::tuple_cons(expr, env, context)?, - Explicit => eval(&args[2], env, &context)? - }; - - // optimization todo: db metadata like this should just get stored - // in the contract object, so that it gets loaded in when the contract - // is loaded from the db. - let data_types = env.global_context.database.load_map(&contract_identifier, map_name)?; - runtime_cost!(cost_functions::FETCH_ENTRY, env, - data_types.value_type.size() + data_types.key_type.size())?; + env.add_memory(cost_constants::AT_BLOCK_MEMORY)?; + let result = env.evaluate_at_block(bhh, &args[1], context); + env.drop_memory(cost_constants::AT_BLOCK_MEMORY); - env.global_context.database.fetch_entry(&contract_identifier, map_name, &key) + result } pub fn special_set_entry(args: &[SymbolicExpression], @@ -261,6 +239,9 @@ pub fn special_set_entry(args: &[SymbolicExpression], runtime_cost!(cost_functions::SET_ENTRY, env, data_types.value_type.size() + data_types.key_type.size())?; + env.add_memory(key.get_memory_use())?; + env.add_memory(value.get_memory_use())?; + env.global_context.database.set_entry(contract, map_name, key, value) } @@ -295,6 +276,9 @@ pub fn special_insert_entry(args: &[SymbolicExpression], runtime_cost!(cost_functions::SET_ENTRY, env, data_types.value_type.size() + data_types.key_type.size())?; + env.add_memory(key.get_memory_use())?; + env.add_memory(value.get_memory_use())?; + env.global_context.database.insert_entry(contract, map_name, key, value) } @@ -323,6 +307,8 @@ pub fn special_delete_entry(args: &[SymbolicExpression], let data_types = env.global_context.database.load_map(contract, map_name)?; runtime_cost!(cost_functions::SET_ENTRY, env, data_types.key_type.size())?; + env.add_memory(key.get_memory_use())?; + env.global_context.database.delete_entry(contract, map_name, &key) } diff --git a/src/vm/functions/mod.rs b/src/vm/functions/mod.rs index a00b258a3a..fbae3b95eb 100644 --- a/src/vm/functions/mod.rs +++ b/src/vm/functions/mod.rs @@ -7,13 +7,13 @@ mod database; mod options; mod assets; -use vm::errors::{CheckErrors, RuntimeErrorType, ShortReturnType, InterpreterResult as Result, check_argument_count, check_arguments_at_least}; +use vm::errors::{Error, CheckErrors, RuntimeErrorType, ShortReturnType, InterpreterResult as Result, check_argument_count, check_arguments_at_least}; use vm::types::{Value, PrincipalData, ResponseData, TypeSignature}; use vm::callables::{CallableType, NativeHandle}; use vm::representations::{SymbolicExpression, SymbolicExpressionType, ClarityName}; use vm::representations::SymbolicExpressionType::{List, Atom}; use vm::{LocalContext, Environment, eval}; -use vm::costs::cost_functions; +use vm::costs::{cost_functions, MemoryConsumer, CostTracker, constants as cost_constants}; use util::hash; define_named_enum!(NativeFunctions { @@ -46,7 +46,6 @@ define_named_enum!(NativeFunctions { FetchVar("var-get"), SetVar("var-set"), FetchEntry("map-get?"), - FetchContractEntry("contract-map-get?"), SetEntry("map-set"), InsertEntry("map-insert"), DeleteEntry("map-delete"), @@ -124,7 +123,6 @@ pub fn lookup_reserved_functions(name: &str) -> Option { Len => NativeFunction("native_len", NativeHandle::SingleArg(&iterables::native_len), cost_functions::LEN), ListCons => SpecialFunction("special_list_cons", &iterables::list_cons), FetchEntry => SpecialFunction("special_map-get?", &database::special_fetch_entry), - FetchContractEntry => SpecialFunction("special_contract-map-get?", &database::special_fetch_contract_entry), SetEntry => SpecialFunction("special_set-entry", &database::special_set_entry), InsertEntry => SpecialFunction("special_insert-entry", &database::special_insert_entry), DeleteEntry => SpecialFunction("special_delete-entry", &database::special_delete_entry), @@ -315,32 +313,39 @@ fn special_let(args: &[SymbolicExpression], env: &mut Environment, context: &Loc let bindings = args[0].match_list() .ok_or(CheckErrors::BadLetSyntax)?; - let mut binding_results = parse_eval_bindings(bindings, env, context)?; - - runtime_cost!(cost_functions::LET, env, binding_results.len())?; + runtime_cost!(cost_functions::LET, env, bindings.len())?; // create a new context. let mut inner_context = context.extend()?; - for (binding_name, binding_value) in binding_results.drain(..) { - if is_reserved(&binding_name) || - env.contract_context.lookup_function(&binding_name).is_some() || - inner_context.lookup_variable(&binding_name).is_some() { - return Err(CheckErrors::NameAlreadyUsed(binding_name.into()).into()) + let mut memory_use = 0; + + finally_drop_memory!( env, memory_use; { + handle_binding_list::<_, Error>(bindings, |binding_name, var_sexp| { + if is_reserved(binding_name) || + env.contract_context.lookup_function(binding_name).is_some() || + inner_context.lookup_variable(binding_name).is_some() { + return Err(CheckErrors::NameAlreadyUsed(binding_name.clone().into()).into()) + } + + let binding_value = eval(var_sexp, env, context)?; + + let bind_mem_use = binding_value.get_memory_use(); + env.add_memory(bind_mem_use)?; + memory_use += bind_mem_use; // no check needed, b/c it's done in add_memory. + inner_context.variables.insert(binding_name.clone(), binding_value); + Ok(()) + })?; + + // evaluate the let-bodies + let mut last_result = None; + for body in args[1..].iter() { + let body_result = eval(&body, env, &inner_context)?; + last_result.replace(body_result); } - inner_context.variables.insert(binding_name, binding_value); - } - - // evaluate the let-bodies - - let mut last_result = None; - for body in args[1..].iter() { - let body_result = eval(&body, env, &inner_context)?; - last_result.replace(body_result); - } - - // last_result should always be Some(...), because of the arg len check above. - Ok(last_result.unwrap()) + // last_result should always be Some(...), because of the arg len check above. + Ok(last_result.unwrap()) + }) } fn special_as_contract(args: &[SymbolicExpression], env: &mut Environment, context: &LocalContext) -> Result { @@ -349,8 +354,14 @@ fn special_as_contract(args: &[SymbolicExpression], env: &mut Environment, conte check_argument_count(1, args)?; // nest an environment. + env.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; + let contract_principal = Value::Principal(PrincipalData::Contract(env.contract_context.contract_identifier.clone())); let mut nested_env = env.nest_as_principal(contract_principal); - eval(&args[0], &mut nested_env, context) + let result = eval(&args[0], &mut nested_env, context); + + env.drop_memory(cost_constants::AS_CONTRACT_MEMORY); + + result } diff --git a/src/vm/functions/options.rs b/src/vm/functions/options.rs index 43e743d32b..d307ff255c 100644 --- a/src/vm/functions/options.rs +++ b/src/vm/functions/options.rs @@ -1,7 +1,7 @@ use vm::errors::{CheckErrors, RuntimeErrorType, ShortReturnType, InterpreterResult as Result, check_argument_count, check_arguments_at_least}; use vm::types::{Value, ResponseData, OptionalData, TypeSignature}; -use vm::costs::cost_functions; +use vm::costs::{cost_functions, MemoryConsumer, CostTracker}; use vm::contexts::{LocalContext, Environment}; use vm::{SymbolicExpression, ClarityName}; use vm; @@ -110,9 +110,15 @@ fn eval_with_new_binding(body: &SymbolicExpression, bind_name: ClarityName, bind return Err(CheckErrors::NameAlreadyUsed(bind_name.into()).into()) } + let memory_use = bind_value.get_memory_use(); + env.add_memory(memory_use)?; + inner_context.variables.insert(bind_name, bind_value); + let result = vm::eval(body, env, &inner_context); + + env.drop_memory(memory_use); - vm::eval(body, env, &inner_context) + result } fn special_match_opt(input: OptionalData, args: &[SymbolicExpression], env: &mut Environment, context: &LocalContext) -> Result { diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 3b476786f2..362f1c7d57 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -33,8 +33,8 @@ use vm::contexts::{GlobalContext}; use vm::functions::define::DefineResult; use vm::errors::{Error, InterpreterError, RuntimeErrorType, CheckErrors, InterpreterResult as Result}; use vm::database::MemoryBackingStore; -use vm::types::{QualifiedContractIdentifier, TraitIdentifier, PrincipalData}; -use vm::costs::{cost_functions, CostOverflowingMath, LimitedCostTracker}; +use vm::types::{QualifiedContractIdentifier, TraitIdentifier, PrincipalData, TypeSignature}; +use vm::costs::{cost_functions, CostOverflowingMath, LimitedCostTracker, MemoryConsumer, CostTracker}; pub use vm::representations::{SymbolicExpression, SymbolicExpressionType, ClarityName, ContractName}; @@ -113,15 +113,30 @@ pub fn apply(function: &CallableType, args: &[SymbolicExpression], resp } else { env.call_stack.insert(&identifier, track_recursion); - let eval_tried: Result> = - args.iter().map(|x| eval(x, env, context)).collect(); - let evaluated_args = match eval_tried { - Ok(x) => x, - Err(e) => { - env.call_stack.remove(&identifier, track_recursion)?; - return Err(e) - } - }; + + let mut used_memory = 0; + let mut evaluated_args = vec![]; + for arg_x in args.iter() { + let arg_value = match eval(arg_x, env, context) { + Ok(x) => x, + Err(e) => { + env.drop_memory(used_memory); + env.call_stack.remove(&identifier, track_recursion)?; + return Err(e) + } + }; + let arg_use = arg_value.get_memory_use(); + match env.add_memory(arg_use) { + Ok(_x) => {}, + Err(e) => { + env.drop_memory(used_memory); + env.call_stack.remove(&identifier, track_recursion)?; + return Err(Error::from(e)) + } + }; + used_memory += arg_value.get_memory_use(); + evaluated_args.push(arg_value); + } let mut resp = match function { CallableType::NativeFunction(_, function, cost_function) => { let arg_size = evaluated_args.len(); @@ -132,6 +147,7 @@ pub fn apply(function: &CallableType, args: &[SymbolicExpression], _ => panic!("Should be unreachable.") }; add_stack_trace(&mut resp, env); + env.drop_memory(used_memory); env.call_stack.remove(&identifier, track_recursion)?; resp } @@ -174,73 +190,98 @@ fn eval_all (expressions: &[SymbolicExpression], global_context: &mut GlobalContext) -> Result> { let mut last_executed = None; let context = LocalContext::new(); + let mut total_memory_use = 0; - for exp in expressions { - let try_define = { - global_context.execute(|context| { + finally_drop_memory!(global_context, total_memory_use; { + for exp in expressions { + let try_define = global_context.execute(|context| { let mut call_stack = CallStack::new(); let mut env = Environment::new( context, contract_context, &mut call_stack, None, None); functions::define::evaluate_define(exp, &mut env) - })? - }; - match try_define { - DefineResult::Variable(name, value) => { - runtime_cost!(cost_functions::BIND_NAME, global_context, 0)?; - - contract_context.variables.insert(name, value); - }, - DefineResult::Function(name, value) => { - runtime_cost!(cost_functions::BIND_NAME, global_context, 0)?; - - contract_context.functions.insert(name, value); - }, - DefineResult::PersistedVariable(name, value_type, value) => { - runtime_cost!(cost_functions::CREATE_VAR, global_context, value_type.size())?; - contract_context.persisted_names.insert(name.clone()); - global_context.database.create_variable(&contract_context.contract_identifier, &name, value_type); - global_context.database.set_variable(&contract_context.contract_identifier, &name, value)?; - }, - DefineResult::Map(name, key_type, value_type) => { - runtime_cost!(cost_functions::CREATE_MAP, global_context, - u64::from(key_type.size()).cost_overflow_add( - u64::from(value_type.size()))?)?; - contract_context.persisted_names.insert(name.clone()); - global_context.database.create_map(&contract_context.contract_identifier, &name, key_type, value_type); - }, - DefineResult::FungibleToken(name, total_supply) => { - runtime_cost!(cost_functions::CREATE_FT, global_context, 0)?; - contract_context.persisted_names.insert(name.clone()); - global_context.database.create_fungible_token(&contract_context.contract_identifier, &name, &total_supply); - }, - DefineResult::NonFungibleAsset(name, asset_type) => { - runtime_cost!(cost_functions::CREATE_NFT, global_context, asset_type.size())?; - contract_context.persisted_names.insert(name.clone()); - global_context.database.create_non_fungible_token(&contract_context.contract_identifier, &name, &asset_type); - }, - DefineResult::Trait(name, trait_type) => { - contract_context.defined_traits.insert(name, trait_type); - }, - DefineResult::UseTrait(_name, _trait_identifier) => {}, - DefineResult::ImplTrait(trait_identifier) => { - contract_context.implemented_traits.insert(trait_identifier); - }, - DefineResult::NoDefine => { - // not a define function, evaluate normally. - global_context.execute(|global_context| { - let mut call_stack = CallStack::new(); - let mut env = Environment::new( - global_context, contract_context, &mut call_stack, None, None); - - let result = eval(exp, &mut env, &context)?; - last_executed = Some(result); - Ok(()) - })?; + })?; + match try_define { + DefineResult::Variable(name, value) => { + runtime_cost!(cost_functions::BIND_NAME, global_context, 0)?; + let value_memory_use = value.get_memory_use(); + global_context.add_memory(value_memory_use)?; + total_memory_use += value_memory_use; + + contract_context.variables.insert(name, value); + }, + DefineResult::Function(name, value) => { + runtime_cost!(cost_functions::BIND_NAME, global_context, 0)?; + + contract_context.functions.insert(name, value); + }, + DefineResult::PersistedVariable(name, value_type, value) => { + runtime_cost!(cost_functions::CREATE_VAR, global_context, value_type.size())?; + contract_context.persisted_names.insert(name.clone()); + + global_context.add_memory(value_type.type_size() + .expect("type size should be realizable") as u64)?; + + global_context.add_memory(value.size() as u64)?; + + global_context.database.create_variable(&contract_context.contract_identifier, &name, value_type); + global_context.database.set_variable(&contract_context.contract_identifier, &name, value)?; + }, + DefineResult::Map(name, key_type, value_type) => { + runtime_cost!(cost_functions::CREATE_MAP, global_context, + u64::from(key_type.size()).cost_overflow_add( + u64::from(value_type.size()))?)?; + contract_context.persisted_names.insert(name.clone()); + + global_context.add_memory(key_type.type_size() + .expect("type size should be realizable") as u64)?; + global_context.add_memory(value_type.type_size() + .expect("type size should be realizable") as u64)?; + + global_context.database.create_map(&contract_context.contract_identifier, &name, key_type, value_type); + }, + DefineResult::FungibleToken(name, total_supply) => { + runtime_cost!(cost_functions::CREATE_FT, global_context, 0)?; + contract_context.persisted_names.insert(name.clone()); + + global_context.add_memory(TypeSignature::UIntType.type_size() + .expect("type size should be realizable") as u64)?; + + global_context.database.create_fungible_token(&contract_context.contract_identifier, &name, &total_supply); + }, + DefineResult::NonFungibleAsset(name, asset_type) => { + runtime_cost!(cost_functions::CREATE_NFT, global_context, asset_type.size())?; + contract_context.persisted_names.insert(name.clone()); + + global_context.add_memory(asset_type.type_size() + .expect("type size should be realizable") as u64)?; + + global_context.database.create_non_fungible_token(&contract_context.contract_identifier, &name, &asset_type); + }, + DefineResult::Trait(name, trait_type) => { + contract_context.defined_traits.insert(name, trait_type); + }, + DefineResult::UseTrait(_name, _trait_identifier) => {}, + DefineResult::ImplTrait(trait_identifier) => { + contract_context.implemented_traits.insert(trait_identifier); + }, + DefineResult::NoDefine => { + // not a define function, evaluate normally. + global_context.execute(|global_context| { + let mut call_stack = CallStack::new(); + let mut env = Environment::new( + global_context, contract_context, &mut call_stack, None, None); + + let result = eval(exp, &mut env, &context)?; + last_executed = Some(result); + Ok(()) + })?; + } } } - } - Ok(last_executed) + contract_context.data_size = total_memory_use; + Ok(last_executed) + }) } /* Run provided program in a brand new environment, with a transient, empty diff --git a/src/vm/tests/costs.rs b/src/vm/tests/costs.rs index a35097c95c..f49b27a464 100644 --- a/src/vm/tests/costs.rs +++ b/src/vm/tests/costs.rs @@ -50,7 +50,6 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str { Len => "(len list-bar)", ListCons => "(list 1 2 3 4)", FetchEntry => "(map-get? map-foo {a 1})", - FetchContractEntry => "(contract-map-get? .contract-other map-foo {a 1})", SetEntry => "(map-set map-foo {a 1} {b 2})", InsertEntry => "(map-insert map-foo {a 2} {b 2})", DeleteEntry => "(map-delete map-foo {a 1})", diff --git a/src/vm/tests/datamaps.rs b/src/vm/tests/datamaps.rs index 825aa6b66f..810ee98fe7 100644 --- a/src/vm/tests/datamaps.rs +++ b/src/vm/tests/datamaps.rs @@ -175,43 +175,6 @@ fn test_implicit_syntax_tuple() { assert_executes(expected, &test_get); } - -#[test] -fn test_fetch_contract_entry() { - let kv_store_contract_src = r#" - (define-map kv-store ((key int)) ((value int))) - (define-read-only (kv-get (key int)) - (unwrap! (get value (map-get? kv-store {key key})) 0)) - (begin (map-insert kv-store {key 42} {value 42}))"#; - - let proxy_src = r#" - (define-private (fetch-via-conntract-call) - (contract-call? .kv-store-contract kv-get 42)) - (define-private (fetch-via-contract-map-get?-using-explicit-tuple) - (unwrap! (get value (contract-map-get? .kv-store-contract kv-store (tuple (key 42)))) 0)) - (define-private (fetch-via-contract-map-get?-using-implicit-tuple) - (unwrap! (get value (contract-map-get? .kv-store-contract kv-store {key 42})) 0)) - (define-private (fetch-via-contract-map-get?-using-bound-tuple) - (let ((t (tuple (key 42)))) - (unwrap! (get value (contract-map-get? .kv-store-contract kv-store t)) 0)))"#; - - let mut marf = MemoryBackingStore::new(); - let mut owned_env = OwnedEnvironment::new(marf.as_clarity_db()); - - let sender = StandardPrincipalData::transient().into(); - let mut env = owned_env.get_exec_environment(Some(sender)); - let kv_contract_identifier = QualifiedContractIdentifier::local("kv-store-contract").unwrap(); - let _r = env.initialize_contract(kv_contract_identifier, kv_store_contract_src).unwrap(); - - let contract_identifier = QualifiedContractIdentifier::local("proxy-contract").unwrap(); - env.initialize_contract(contract_identifier.clone(), proxy_src).unwrap(); - - assert_eq!(Value::Int(42), env.eval_read_only(&contract_identifier, "(fetch-via-conntract-call)").unwrap()); - assert_eq!(Value::Int(42), env.eval_read_only(&contract_identifier, "(fetch-via-contract-map-get?-using-implicit-tuple)").unwrap()); - assert_eq!(Value::Int(42), env.eval_read_only(&contract_identifier, "(fetch-via-contract-map-get?-using-explicit-tuple)").unwrap()); - assert_eq!(Value::Int(42), env.eval_read_only(&contract_identifier, "(fetch-via-contract-map-get?-using-bound-tuple)").unwrap()); -} - #[test] fn test_set_int_variable() { let contract_src = r#" diff --git a/src/vm/tests/forking.rs b/src/vm/tests/forking.rs index ff30b9b7ab..79f8bfb459 100644 --- a/src/vm/tests/forking.rs +++ b/src/vm/tests/forking.rs @@ -114,21 +114,6 @@ fn test_at_block_missing_defines() { e } - fn initialize_3(owned_env: &mut OwnedEnvironment) -> Error { - let c_b = QualifiedContractIdentifier::local("contract-b").unwrap(); - - let contract = - "(define-private (problematic-fetch-entry) - (at-block 0x0101010101010101010101010101010101010101010101010101010101010101 - (contract-map-get? .contract-a datum ((id 'true))))) - (problematic-fetch-entry) - "; - - eprintln!("Initializing contract..."); - let e = owned_env.initialize_contract(c_b.clone(), &contract).unwrap_err(); - e - } - with_separate_forks_environment( |_| {}, initialize_1, @@ -138,15 +123,6 @@ fn test_at_block_missing_defines() { assert_eq!(err, CheckErrors::NoSuchContract("S1G2081040G2081040G2081040G208105NK8PE5.contract-a".into()).into()); }); - with_separate_forks_environment( - |_| {}, - initialize_1, - |_| {}, - |env| { - let err = initialize_3(env); - assert_eq!(err, CheckErrors::NoSuchMap("datum".into()).into()); - }); - } // execute: diff --git a/src/vm/tests/large_contract.rs b/src/vm/tests/large_contract.rs new file mode 100644 index 0000000000..9a58add491 --- /dev/null +++ b/src/vm/tests/large_contract.rs @@ -0,0 +1,270 @@ + +use chainstate::stacks::index::storage::{TrieFileStorage}; +use vm::execute as vm_execute; +use chainstate::burn::BlockHeaderHash; +use vm::errors::{Error, CheckErrors, RuntimeErrorType}; +use vm::types::{Value, OptionalData, StandardPrincipalData, ResponseData, + TypeSignature, PrincipalData, QualifiedContractIdentifier}; +use vm::contexts::{OwnedEnvironment,GlobalContext, Environment}; +use vm::representations::SymbolicExpression; +use vm::contracts::Contract; +use util::hash::hex_bytes; +use vm::database::{MemoryBackingStore, MarfedKV, NULL_HEADER_DB, ClarityDatabase}; +use vm::clarity::ClarityInstance; +use vm::ast; + +use vm::tests::{with_memory_environment, with_marfed_environment, execute, symbols_from_values}; + + +/* + * This test exhibits memory inflation -- + * `(define-data-var var-x ...)` uses more than 1048576 bytes of memory. + * this is mainly due to using hex encoding in the sqlite storage. + */ +#[test] +pub fn rollback_log_memory_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + let EXPLODE_N = 100; + + let contract_identifier = QualifiedContractIdentifier::local("foo").unwrap(); + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let define_data_var = "(define-data-var XZ (buff 1048576) \"a\")"; + + let mut contract = define_data_var.to_string(); + for i in 0..20 { + let cur_size = format!("{}", 2u32.pow(i)); + contract.push_str("\n"); + contract.push_str( + &format!("(var-set XZ (concat (unwrap-panic (as-max-len? (var-get XZ) u{})) + (unwrap-panic (as-max-len? (var-get XZ) u{}))))", + cur_size, cur_size)); + } + for i in 0..EXPLODE_N { + let exploder = format!("(define-data-var var-{} (buff 1048576) (var-get XZ))", i); + contract.push_str("\n"); + contract.push_str(&exploder); + } + + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract).unwrap(); + assert!(format!("{:?}", + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract, |_,_| false).unwrap_err()) + .contains("MemoryBalanceExceeded")); + } +} + +/* + */ +#[test] +pub fn let_memory_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + let EXPLODE_N = 100; + + let contract_identifier = QualifiedContractIdentifier::local("foo").unwrap(); + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let define_data_var = "(define-constant buff-0 \"a\")"; + + let mut contract = define_data_var.to_string(); + for i in 0..20 { + contract.push_str("\n"); + contract.push_str( + &format!("(define-constant buff-{} (concat buff-{} buff-{}))", + i+1, i, i)); + } + + contract.push_str("\n"); + contract.push_str("(let ("); + + for i in 0..EXPLODE_N { + let exploder = format!("(var-{} buff-20) ", i); + contract.push_str(&exploder); + } + + contract.push_str(") 1)"); + + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract).unwrap(); + assert!(format!("{:?}", + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract, |_,_| false).unwrap_err()) + .contains("MemoryBalanceExceeded")); + } +} + +#[test] +pub fn argument_memory_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + let EXPLODE_N = 100; + + let contract_identifier = QualifiedContractIdentifier::local("foo").unwrap(); + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let define_data_var = "(define-constant buff-0 \"a\")"; + + let mut contract = define_data_var.to_string(); + for i in 0..20 { + contract.push_str("\n"); + contract.push_str( + &format!("(define-constant buff-{} (concat buff-{} buff-{}))", + i+1, i, i)); + } + + contract.push_str("\n"); + contract.push_str("(is-eq "); + + for _i in 0..EXPLODE_N { + let exploder = "buff-20 "; + contract.push_str(exploder); + } + + contract.push_str(")"); + + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract).unwrap(); + assert!(format!("{:?}", + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract, |_,_| false).unwrap_err()) + .contains("MemoryBalanceExceeded")); + } +} + +#[test] +pub fn fcall_memory_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + let COUNT_PER_FUNC = 10; + let FUNCS = 10; + + let contract_identifier = QualifiedContractIdentifier::local("foo").unwrap(); + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let define_data_var = "(define-constant buff-0 \"a\")"; + + let mut contract = define_data_var.to_string(); + for i in 0..20 { + contract.push_str("\n"); + contract.push_str( + &format!("(define-constant buff-{} (concat buff-{} buff-{}))", + i+1, i, i)); + } + + contract.push_str("\n"); + + for i in 0..FUNCS { + contract.push_str(&format!("(define-private (call-{})\n", i)); + + contract.push_str("(let ("); + + for j in 0..COUNT_PER_FUNC { + let exploder = format!("(var-{} buff-20) ", j); + contract.push_str(&exploder); + } + + if i == 0 { + contract.push_str(") 1) )\n"); + } else { + contract.push_str(&format!(") (call-{})) )\n", i - 1)); + } + } + + let mut contract_ok = contract.clone(); + let mut contract_err = contract.clone(); + + contract_ok.push_str("(call-0)"); + contract_err.push_str("(call-9)"); + + eprintln!("{}", contract_ok); + eprintln!("{}", contract_err); + + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract_ok).unwrap(); + conn.initialize_smart_contract( + // initialize the ok contract without errs, but still abort. + &contract_identifier, &ct_ast, &contract_ok, |_,_| true).unwrap(); + + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract_err).unwrap(); + assert!(format!("{:?}", + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract_err, |_,_| false).unwrap_err()) + .contains("MemoryBalanceExceeded")); + } +} + +#[test] +pub fn ccall_memory_test() { + let marf = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf); + let COUNT_PER_CONTRACT = 20; + let CONTRACTS = 5; + + { + let mut conn = clarity_instance.begin_block(&TrieFileStorage::block_sentinel(), + &BlockHeaderHash::from_bytes(&[0 as u8; 32]).unwrap(), + &NULL_HEADER_DB); + + let define_data_var = "(define-constant buff-0 \"a\")\n"; + + let mut contract = define_data_var.to_string(); + for i in 0..20 { + contract.push_str( + &format!("(define-constant buff-{} (concat buff-{} buff-{}))\n", + i+1, i, i)); + } + + for i in 0..COUNT_PER_CONTRACT { + contract.push_str( + &format!("(define-constant var-{} buff-20)\n", i)); + } + + contract.push_str("(define-public (call)\n"); + + let mut contracts = vec![]; + + for i in 0..CONTRACTS { + let mut my_contract = contract.clone(); + if i == 0 { + my_contract.push_str("(ok 1))\n"); + } else { + my_contract.push_str(&format!("(contract-call? .contract-{} call))\n", i - 1)); + } + my_contract.push_str("(call)\n"); + contracts.push(my_contract); + } + + for (i, contract) in contracts.into_iter().enumerate() { + let contract_name = format!("contract-{}", i); + let contract_identifier = QualifiedContractIdentifier::local(&contract_name).unwrap(); + + if i < (CONTRACTS-1) { + let (ct_ast, ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract).unwrap(); + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract, |_,_| false).unwrap(); + conn.save_analysis(&contract_identifier, &ct_analysis).unwrap(); + } else { + let (ct_ast, _ct_analysis) = conn.analyze_smart_contract(&contract_identifier, &contract).unwrap(); + assert!(format!("{:?}", + conn.initialize_smart_contract( + &contract_identifier, &ct_ast, &contract, |_,_| false).unwrap_err()) + .contains("MemoryBalanceExceeded")); + } + } + } +} diff --git a/src/vm/tests/mod.rs b/src/vm/tests/mod.rs index 630a7a74f4..c1d9e672ae 100644 --- a/src/vm/tests/mod.rs +++ b/src/vm/tests/mod.rs @@ -22,6 +22,7 @@ mod datamaps; mod contracts; pub mod costs; mod traits; +mod large_contract; pub fn with_memory_environment(f: F, top_level: bool) where F: FnOnce(&mut OwnedEnvironment) -> () diff --git a/src/vm/types/mod.rs b/src/vm/types/mod.rs index 49fc723d58..0ed4ef7d12 100644 --- a/src/vm/types/mod.rs +++ b/src/vm/types/mod.rs @@ -23,6 +23,7 @@ pub const WRAPPER_VALUE_SIZE: u32 = 1; #[derive(Debug, Clone, Eq, Serialize, Deserialize)] pub struct TupleData { + // todo: remove type_signature pub type_signature: TupleTypeSignature, pub data_map: BTreeMap } @@ -35,6 +36,7 @@ pub struct BuffData { #[derive(Debug, Clone, Eq, Serialize, Deserialize)] pub struct ListData { pub data: Vec, + // todo: remove type_signature pub type_signature: ListTypeData } diff --git a/src/vm/types/signatures.rs b/src/vm/types/signatures.rs index 815cd4ad47..f32f8165a2 100644 --- a/src/vm/types/signatures.rs +++ b/src/vm/types/signatures.rs @@ -809,7 +809,7 @@ impl TypeSignature { UIntType => Some(16), BoolType => Some(1), PrincipalType => Some(148), // 20+128 - BufferType(len) => Some(u32::from(len)), + BufferType(len) => Some(4 + u32::from(len)), TupleType(tuple_sig) => tuple_sig.inner_size(), ListType(list_type) => list_type.inner_size(), OptionalType(t) => t.size().checked_add(WRAPPER_VALUE_SIZE), @@ -884,7 +884,7 @@ impl ListTypeData { impl TupleTypeSignature { /// Tuple Size: /// size( btreemap ) = 2*map.len() + sum(names) + sum(values) - fn type_size(&self) -> Option { + pub fn type_size(&self) -> Option { let mut type_map_size = u32::try_from(self.type_map.len()) .ok()? .checked_mul(2)?;