Skip to content

Commit a8aa32b

Browse files
authored
Merge pull request #6448 from hstove/fix/cost-error-undefined-var
fix: early exit for top-level undefined errors
2 parents 4747f97 + 424ec11 commit a8aa32b

File tree

8 files changed

+184
-3
lines changed

8 files changed

+184
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
2525
expressive (#6337)
2626
- Removed affirmation maps logic throughout, upgrading chainstate DB schema to 11 and burnchain DB schema to 3 (#6314)
2727

28+
### Changed
29+
30+
- When a contract deploy is analyzed, it will no longer throw a `CostError` when the contract contains an undefined top-level variable. Instead, it will throw a `UndefinedVariable` error.
31+
2832
## [3.2.0.0.1]
2933

3034
### Added

clarity/src/vm/analysis/type_checker/v2_05/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,13 @@ impl<'a, 'b> TypeChecker<'a, 'b> {
711711
} else if let Some(type_result) = context.lookup_trait_reference_type(name) {
712712
Ok(TypeSignature::TraitReferenceType(type_result.clone()))
713713
} else {
714+
// Special case where a top-level is being looked up, which must
715+
// be undefined. This early error prevents a cost function error
716+
// due to `context.depth` being 0.
717+
if context.depth == 0 {
718+
return Err(CheckErrors::UndefinedVariable(name.to_string()).into());
719+
}
720+
714721
runtime_cost(
715722
ClarityCostFunction::AnalysisLookupVariableDepth,
716723
self,

clarity/src/vm/analysis/type_checker/v2_1/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,13 @@ impl<'a, 'b> TypeChecker<'a, 'b> {
14231423
type_result.clone(),
14241424
)))
14251425
} else {
1426+
// Special case where a top-level is being looked up, which must
1427+
// be undefined. This early error prevents a cost function error
1428+
// due to `context.depth` being 0.
1429+
if context.depth == 0 {
1430+
return Err(CheckErrors::UndefinedVariable(name.to_string()).into());
1431+
}
1432+
14261433
runtime_cost(
14271434
ClarityCostFunction::AnalysisLookupVariableDepth,
14281435
self,

clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ fn test_names_tokens_contracts_interface() {
283283
{{ "name": "tn1", "type": "bool" }},
284284
{{ "name": "tn2", "type": "int128" }},
285285
{{ "name": "tn3", "type": {{ "buffer": {{ "length": 1 }} }}}}
286-
] }} }}
286+
] }} }}
287287
}},
288288
{{ "name": "f11",
289289
"access": "private",
@@ -416,7 +416,7 @@ fn test_names_tokens_contracts_interface() {
416416
"name": "n2",
417417
"type": "bool"
418418
}}
419-
]
419+
]
420420
}}
421421
}}]
422422
}}
@@ -3537,6 +3537,19 @@ fn clarity_trait_experiments_cross_epochs(
35373537
};
35383538
}
35393539

3540+
#[apply(test_clarity_versions)]
3541+
fn clarity_trait_experiments_undefined_top_variable(
3542+
#[case] version: ClarityVersion,
3543+
#[case] epoch: StacksEpochId,
3544+
) {
3545+
let mut marf = MemoryBackingStore::new();
3546+
let mut db = marf.as_analysis_db();
3547+
let err = db
3548+
.execute(|db| load_versioned(db, "undefined-top-variable", version, epoch))
3549+
.unwrap_err();
3550+
assert!(err.starts_with("UndefinedVariable(\"foo\")"));
3551+
}
3552+
35403553
/// Pass various types to `contract-hash?`
35413554
#[apply(test_clarity_versions)]
35423555
fn test_contract_hash(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

stacks-node/src/tests/signer/v0.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::sync::{Arc, Mutex};
2020
use std::time::{Duration, Instant};
2121
use std::{env, thread};
2222

23+
use clarity::vm::costs::ExecutionCost;
2324
use clarity::vm::types::PrincipalData;
2425
use libsigner::v0::messages::{
2526
BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MinerSlotID, PeerInfo, RejectCode,
@@ -18385,6 +18386,94 @@ fn multiversioned_signer_protocol_version_calculation() {
1838518386
signer_test.shutdown();
1838618387
}
1838718388

18389+
/// This is a test for backwards compatibility regarding
18390+
/// how contracts with an undefined top-level variable are handled.
18391+
///
18392+
/// Critically, we want to ensure that the cost of the block, along with
18393+
/// the resulting block hash, are the same.
18394+
#[test]
18395+
#[ignore]
18396+
fn contract_with_undefined_variable_compat() {
18397+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
18398+
return;
18399+
}
18400+
18401+
let num_signers = 5;
18402+
let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes());
18403+
let sender_addr = tests::to_addr(&sender_sk);
18404+
let send_amt = 100;
18405+
let send_fee = 180;
18406+
let deploy_fee = 1000000;
18407+
let call_fee = 1000;
18408+
let signer_test: SignerTest<SpawnedSigner> =
18409+
SignerTest::new_with_config_modifications_and_snapshot(
18410+
num_signers,
18411+
vec![(
18412+
sender_addr.clone(),
18413+
(send_amt + send_fee) * 10 + deploy_fee + call_fee,
18414+
)],
18415+
|c| {
18416+
c.validate_with_replay_tx = true;
18417+
},
18418+
|node_config| {
18419+
node_config.miner.block_commit_delay = Duration::from_secs(1);
18420+
node_config.miner.replay_transactions = true;
18421+
node_config.miner.activated_vrf_key_path =
18422+
Some(format!("{}/vrf_key", node_config.node.working_dir));
18423+
},
18424+
None,
18425+
None,
18426+
Some(function_name!()),
18427+
);
18428+
18429+
if signer_test.bootstrap_snapshot() {
18430+
signer_test.shutdown_and_snapshot();
18431+
return;
18432+
}
18433+
18434+
info!("------------------------- Beginning test -------------------------");
18435+
18436+
signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
18437+
18438+
let (txid, deploy_nonce) = signer_test
18439+
.submit_contract_deploy(&sender_sk, deploy_fee, "foo", "undefined-var")
18440+
.expect("Failed to submit contract deploy");
18441+
18442+
signer_test
18443+
.wait_for_nonce_increase(&sender_addr, deploy_nonce)
18444+
.expect("Failed to wait for nonce increase");
18445+
18446+
let blocks = test_observer::get_mined_nakamoto_blocks();
18447+
18448+
let block = blocks.last().unwrap();
18449+
18450+
let tx_event = block
18451+
.tx_events
18452+
.iter()
18453+
.find(|event| event.txid().to_hex() == txid)
18454+
.expect("Failed to find deploy event");
18455+
18456+
info!("Tx event: {:?}", tx_event);
18457+
18458+
let TransactionEvent::Success(success_event) = tx_event else {
18459+
panic!("Failed: Expected success event");
18460+
};
18461+
18462+
let block_cost = block.cost.clone();
18463+
let expected_cost = ExecutionCost {
18464+
runtime: 346,
18465+
write_length: 2,
18466+
write_count: 1,
18467+
read_length: 1,
18468+
read_count: 1,
18469+
};
18470+
18471+
assert_eq!(block_cost, expected_cost.clone());
18472+
assert_eq!(success_event.execution_cost, expected_cost);
18473+
18474+
signer_test.shutdown();
18475+
}
18476+
1838818477
/// Ensure that signers can immediately start participating in signing when starting up
1838918478
/// after crashing mid reward cycle.
1839018479
///

stackslib/src/chainstate/stacks/miner.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,18 @@ pub enum TransactionEvent {
441441
Problematic(TransactionProblematicEvent),
442442
}
443443

444+
impl TransactionEvent {
445+
/// Get the txid of the transaction result
446+
pub fn txid(&self) -> &Txid {
447+
match self {
448+
TransactionEvent::Success(TransactionSuccessEvent { txid, .. }) => txid,
449+
TransactionEvent::ProcessingError(TransactionErrorEvent { txid, .. }) => txid,
450+
TransactionEvent::Skipped(TransactionSkippedEvent { txid, .. }) => txid,
451+
TransactionEvent::Problematic(TransactionProblematicEvent { txid, .. }) => txid,
452+
}
453+
}
454+
}
455+
444456
impl TransactionResult {
445457
/// Logs a queryable message for the case where `txid` has succeeded.
446458
pub fn log_transaction_success(tx: &StacksTransaction) {

stackslib/src/clarity_vm/tests/analysis_costs.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
use clarity::vm::ast::ASTRules;
18-
use clarity::vm::clarity::TransactionConnection;
18+
use clarity::vm::clarity::{Error as ClarityError, TransactionConnection};
1919
use clarity::vm::costs::ExecutionCost;
20+
use clarity::vm::errors::CheckErrors;
2021
use clarity::vm::functions::NativeFunctions;
2122
use clarity::vm::test_util::TEST_HEADER_DB;
2223
use clarity::vm::tests::{test_only_mainnet_to_chain_id, UnitTestBurnStateDB};
2324
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, Value};
2425
use clarity::vm::{execute as vm_execute, ClarityVersion, ContractName};
26+
use rstest::rstest;
2527
use stacks_common::types::chainstate::StacksBlockId;
2628
use stacks_common::types::StacksEpochId;
2729

@@ -304,3 +306,49 @@ fn epoch_205_test_all_mainnet() {
304306
fn epoch_205_test_all_testnet() {
305307
epoch_205_test_all(false);
306308
}
309+
310+
#[rstest]
311+
#[case(true, StacksEpochId::Epoch21)]
312+
#[case(true, StacksEpochId::Epoch2_05)]
313+
#[case(false, StacksEpochId::Epoch21)]
314+
#[case(false, StacksEpochId::Epoch2_05)]
315+
fn undefined_top_variable_error(#[case] use_mainnet: bool, #[case] epoch: StacksEpochId) {
316+
let mut clarity_instance =
317+
setup_tracked_cost_test(use_mainnet, epoch, ClarityVersion::Clarity1);
318+
let contract_self = "foo";
319+
320+
let self_contract_id =
321+
QualifiedContractIdentifier::local("undefined-top-variable-error").unwrap();
322+
323+
let burn_state_db = UnitTestBurnStateDB {
324+
epoch_id: epoch,
325+
ast_rules: ASTRules::PrecheckSize,
326+
};
327+
328+
{
329+
let mut conn = clarity_instance.begin_block(
330+
&StacksBlockId([3; 32]),
331+
&StacksBlockId([4; 32]),
332+
&TEST_HEADER_DB,
333+
&burn_state_db,
334+
);
335+
336+
conn.as_transaction(|conn| {
337+
let analysis_result = conn.analyze_smart_contract(
338+
&self_contract_id,
339+
ClarityVersion::Clarity1,
340+
&contract_self,
341+
ASTRules::PrecheckSize,
342+
);
343+
let Err(ClarityError::Analysis(check_error)) = analysis_result else {
344+
panic!("Bad analysis result: {:?}", &analysis_result);
345+
};
346+
let CheckErrors::UndefinedVariable(var_name) = check_error.err else {
347+
panic!("Bad analysis error: {:?}", &check_error);
348+
};
349+
assert_eq!(var_name, "foo".to_string());
350+
});
351+
352+
conn.commit_block();
353+
}
354+
}

0 commit comments

Comments
 (0)