Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- Added `marf_compress` as a node configuration parameter to enable MARF compression feature ([#6811](https://github.com/stacks-network/stacks-core/pull/6811))
- Effective in epoch 3.4 `contract-call?`s can accept a constant as the contract to be called
- Added post-condition enhancements for epoch 3.4 (SIP-040): `Originator` post-condition mode (`0x03`) and NFT `MAY SEND` condition code (`0x12`), including serialization support and epoch-gated validation/enforcement.
- Disabled `at-block` starting from Epoch 3.4 (see SIP-042). New contracts referencing `at-block` are rejected during static analysis. Existing contracts that invoke it will fail at runtime with an `AtBlockUnavailable` error.

### Fixed

Expand Down
5 changes: 5 additions & 0 deletions clarity-types/src/errors/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,8 @@ pub enum StaticCheckErrorKind {
WriteAttemptedInReadOnly,
/// `at-block` closure must be read-only but contains write operations.
AtBlockClosureMustBeReadOnly,
/// `at-block` is not available in this epoch.
AtBlockUnavailable,

// contract post-conditions
/// Post-condition expects a list of asset allowances but received invalid input.
Expand Down Expand Up @@ -609,6 +611,8 @@ pub enum RuntimeCheckErrorKind {
/// Referenced function is not defined in the current scope.
/// The `String` wraps the non-existent function name.
UndefinedFunction(String),
/// `at-block` is not available in this epoch.
AtBlockUnavailable,

// Argument counts
/// Incorrect number of arguments provided to a function.
Expand Down Expand Up @@ -1181,6 +1185,7 @@ impl DiagnosableError for StaticCheckErrorKind {
StaticCheckErrorKind::TooManyFunctionParameters(found, allowed) => format!("too many function parameters specified: found {found}, the maximum is {allowed}"),
StaticCheckErrorKind::WriteAttemptedInReadOnly => "expecting read-only statements, detected a writing operation".into(),
StaticCheckErrorKind::AtBlockClosureMustBeReadOnly => "(at-block ...) closures expect read-only statements, but detected a writing operation".into(),
StaticCheckErrorKind::AtBlockUnavailable => "(at-block ...) is not available in this epoch".into(),
StaticCheckErrorKind::BadTokenName => "expecting an token name as an argument".into(),
StaticCheckErrorKind::DefineNFTBadSignature => "(define-asset ...) expects an asset name and an asset identifier type signature as arguments".into(),
StaticCheckErrorKind::NoSuchNFT(asset_name) => format!("tried to use asset function with a undefined asset ('{asset_name}')"),
Expand Down
3 changes: 3 additions & 0 deletions clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ fn check_special_at_block(
context: &TypingContext,
) -> Result<TypeSignature, StaticCheckError> {
check_argument_count(2, args)?;
if !checker.epoch.supports_at_block() {
return Err(StaticCheckErrorKind::AtBlockUnavailable.into());
}
checker.type_check_expects(&args[0], context, &TypeSignature::BUFFER_32)?;
checker.type_check(&args[1], context)
}
Expand Down
43 changes: 41 additions & 2 deletions clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,12 +927,51 @@ fn test_at_block() {
for (good_test, expected) in good.iter() {
assert_eq!(
expected,
&format!("{}", type_check_helper(good_test).unwrap())
&format!(
"{}",
type_check_helper_version(
good_test,
ClarityVersion::Clarity4,
StacksEpochId::Epoch33
)
.unwrap()
)
);
}

for (bad_test, expected) in bad.iter() {
assert_eq!(*expected, *type_check_helper(bad_test).unwrap_err().err);
assert_eq!(
*expected,
*type_check_helper_version(bad_test, ClarityVersion::Clarity4, StacksEpochId::Epoch33)
.unwrap_err()
.err
);
}

assert_eq!(
StaticCheckErrorKind::AtBlockUnavailable,
*type_check_helper_version(
"(at-block (sha256 u0) u1)",
ClarityVersion::Clarity4,
StacksEpochId::Epoch34
)
.unwrap_err()
.err
);

let mut versions_gt_clarity4 = ClarityVersion::ALL.to_vec();
versions_gt_clarity4.retain(|version| *version > ClarityVersion::Clarity4);
for version in versions_gt_clarity4 {
assert_eq!(
StaticCheckErrorKind::UnknownFunction("at-block".to_string()),
*type_check_helper_version(
"(at-block (sha256 u0) u1)",
version,
StacksEpochId::latest()
)
.unwrap_err()
.err
);
}
}

Expand Down
4 changes: 3 additions & 1 deletion clarity/src/vm/docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,9 @@ const AT_BLOCK: SpecialAPI = SpecialAPI {
snippet: "at-block ${1:id-header-hash} ${2:expr}",
output_type: "A",
signature: "(at-block id-block-hash expr)",
description: "The `at-block` function evaluates the expression `expr` _as if_ it were evaluated at the end of the
description: "Removed in Epoch 3.4 (see SIP-042).

The `at-block` function evaluates the expression `expr` _as if_ it were evaluated at the end of the
block indicated by the _block-hash_ argument. The `expr` closure must be read-only.

Note: The block identifying hash must be a hash returned by the `id-header-hash` block information
Expand Down
3 changes: 3 additions & 0 deletions clarity/src/vm/functions/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ pub fn special_at_block(
env: &mut Environment,
context: &LocalContext,
) -> Result<Value, VmExecutionError> {
if !env.epoch().supports_at_block() {
return Err(RuntimeCheckErrorKind::AtBlockUnavailable.into());
}
check_argument_count(2, args)?;

runtime_cost(ClarityCostFunction::AtBlock, env, 0)?;
Expand Down
2 changes: 1 addition & 1 deletion clarity/src/vm/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) {
AsContract("as-contract", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity3)),
ContractOf("contract-of", ClarityVersion::Clarity1, None),
PrincipalOf("principal-of?", ClarityVersion::Clarity1, None),
AtBlock("at-block", ClarityVersion::Clarity1, None),
AtBlock("at-block", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity4)),
GetBlockInfo("get-block-info?", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity2)),
GetBurnBlockInfo("get-burn-block-info?", ClarityVersion::Clarity2, None),
ConsError("err", ClarityVersion::Clarity1, None),
Expand Down
23 changes: 17 additions & 6 deletions clarity/src/vm/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,12 +994,23 @@ fn test_at_unknown_block(
)
.unwrap_err();
eprintln!("{err}");
match err {
ClarityEvalError::Vm(VmExecutionError::Runtime(x, _)) => assert_eq!(
x,
RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from(vec![2_u8; 32].as_slice()))
),
e => panic!("Unexpected error: {e}"),
if epoch.supports_at_block() {
match err {
ClarityEvalError::Vm(VmExecutionError::Runtime(x, _)) => assert_eq!(
x,
RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from(
vec![2_u8; 32].as_slice()
))
),
e => panic!("Unexpected error: {e}"),
}
} else {
match err {
ClarityEvalError::Vm(VmExecutionError::RuntimeCheck(x)) => {
assert_eq!(x, RuntimeCheckErrorKind::AtBlockUnavailable)
}
e => panic!("Unexpected error: {e}"),
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,11 @@ impl StacksEpochId {
self >= &StacksEpochId::Epoch34
}

/// Whether `at-block` is available in this epoch.
pub fn supports_at_block(&self) -> bool {
self < &StacksEpochId::Epoch34
}

/// Return the network epoch associated with the StacksEpochId
pub fn network_epoch(epoch: StacksEpochId) -> u8 {
match epoch {
Expand Down
28 changes: 27 additions & 1 deletion stacks-node/src/tests/nakamoto_integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7833,6 +7833,19 @@ fn check_block_times() {
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1;
// Keep this test in Epoch 3.3 so `at-block` remains available.
{
let epochs = naka_conf
.burnchain
.epochs
.as_mut()
.expect("Missing burnchain epochs in config");
epochs.truncate_after(StacksEpochId::Epoch33);
epochs
.get_mut(StacksEpochId::Epoch33)
.expect("Missing epoch 3.3 in config")
.end_height = STACKS_EPOCH_MAX;
}
let sender_sk = Secp256k1PrivateKey::random();
let sender_signer_sk = Secp256k1PrivateKey::random();
let sender_signer_addr = tests::to_addr(&sender_signer_sk);
Expand Down Expand Up @@ -15442,6 +15455,19 @@ fn check_block_time_keyword() {
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1;
// Keep this test below Epoch 3.4 so `at-block` stays valid.
{
let epochs = naka_conf
.burnchain
.epochs
.as_mut()
.expect("Missing burnchain epochs in config");
epochs.truncate_after(StacksEpochId::Epoch33);
epochs
.get_mut(StacksEpochId::Epoch33)
.expect("Missing epoch 3.3 in config")
.end_height = STACKS_EPOCH_MAX;
}
let sender_sk = Secp256k1PrivateKey::random();
let sender_signer_sk = Secp256k1PrivateKey::random();
let sender_signer_addr = tests::to_addr(&sender_signer_sk);
Expand Down Expand Up @@ -15565,7 +15591,7 @@ fn check_block_time_keyword() {
naka_conf.burnchain.chain_id,
contract_name,
contract,
Some(ClarityVersion::latest()),
Comment thread
jcnelson marked this conversation as resolved.
Some(ClarityVersion::Clarity4),
);
sender_nonce += 1;
submit_tx(&http_origin, &contract_tx);
Expand Down
29 changes: 29 additions & 0 deletions stackslib/src/chainstate/tests/runtime_analysis_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ fn variant_coverage_report(variant: RuntimeCheckErrorKind) {
runtime_check_error_kind_name_already_used_ccall
]),
UndefinedFunction(_) => Tested(vec![runtime_check_error_kind_undefined_function_ccall]),
AtBlockUnavailable => Tested(vec![runtime_check_error_kind_at_block_unavailable_ccall]),
IncorrectArgumentCount(_, _) => {
Tested(vec![runtime_check_error_kind_incorrect_argument_count_ccall])
}
Expand Down Expand Up @@ -495,6 +496,8 @@ fn runtime_check_error_kind_type_signature_too_deep_ccall() {
/// that `OptionalType(NoType)` value into `is-eq` against `u0`, triggering the
/// runtime `TypeError(UIntType, OptionalType(NoType))`.
/// Outcome: block accepted.
/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a
/// [`RuntimeCheckErrorKind::AtBlockUnavailable`].
#[test]
fn runtime_check_error_kind_type_error_cdeploy() {
let contract_1 = SetupContract::new(
Expand Down Expand Up @@ -541,6 +544,7 @@ fn runtime_check_error_kind_type_error_cdeploy() {
(ok shares)))

(define-constant result (get-shares u999 .pool))",
deploy_epochs: &[StacksEpochId::Epoch33],
setup_contracts: &[contract_1, contract_2],
);
}
Expand All @@ -551,6 +555,8 @@ fn runtime_check_error_kind_type_error_cdeploy() {
/// that `OptionalType(NoType)` value into `is-eq` against `u0`, triggering the
/// runtime `TypeError(UIntType, OptionalType(NoType))`.
/// Outcome: block accepted.
/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a
/// [`RuntimeCheckErrorKind::AtBlockUnavailable`].
#[test]
fn runtime_check_error_kind_type_error_ccall() {
let contract_1 = SetupContract::new(
Expand All @@ -577,6 +583,9 @@ fn runtime_check_error_kind_type_error_ccall() {
)
.with_clarity_version(ClarityVersion::Clarity1); // Only works with clarity 1 or 2

let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec();
deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33);

contract_call_consensus_test!(
contract_name: "value-too-large",
contract_code: "
Expand All @@ -599,6 +608,8 @@ fn runtime_check_error_kind_type_error_ccall() {
(get-shares u999 .pool))",
function_name: "trigger-error",
function_args: &[],
deploy_epochs: &deploy_epochs,
call_epochs: &[StacksEpochId::Epoch33],
setup_contracts: &[contract_1, contract_2],
);
}
Expand Down Expand Up @@ -925,6 +936,24 @@ fn runtime_check_error_kind_undefined_function_ccall() {
);
}

/// RuntimeCheckErrorKind: [`RuntimeCheckErrorKind::AtBlockUnavailable`]
/// Caused by: invoking `at-block` after crossing into Epoch 3.4, where the built-in is disabled.
/// Outcome: block accepted.
#[test]
fn runtime_check_error_kind_at_block_unavailable_ccall() {
contract_call_consensus_test!(
contract_name: "at-block-unavail",
contract_code: "
(define-public (trigger-error)
(ok (at-block 0x0101010101010101010101010101010101010101010101010101010101010101
u1)))",
function_name: "trigger-error",
function_args: &[],
deploy_epochs: &[StacksEpochId::Epoch33],
call_epochs: &[StacksEpochId::Epoch34],
);
}

/// RuntimeCheckErrorKind: [`RuntimeCheckErrorKind::NoSuchContract`]
/// Caused by: calling a contract that does not exist.
/// Outcome: block accepted.
Expand Down
21 changes: 20 additions & 1 deletion stackslib/src/chainstate/tests/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,13 @@ fn stack_depth_too_deep_call_chain_ccall() {
/// Error: [`RuntimeError::UnknownBlockHeaderHash`]
/// Caused by: calling `at-block` with a block hash that doesn't exist on the current fork
/// Outcome: block accepted
/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a
/// [`StaticCheckErrorKind::AtBlockUnavailable`].
#[test]
fn unknown_block_header_hash_fork() {
let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec();
deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33);

contract_call_consensus_test!(
contract_name: "unknown-hash",
contract_code: "
Expand All @@ -679,14 +684,22 @@ fn unknown_block_header_hash_fork() {
)",
function_name: "trigger",
function_args: &[],
deploy_epochs: &deploy_epochs,
call_epochs: &[StacksEpochId::Epoch33],
);
}

/// Error: [`RuntimeError::BadBlockHash`]
/// Caused by: calling `at-block` with a 31-byte block hash
/// Outcome: block accepted
/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a
/// [`RuntimeCheckErrorKind::AtBlockUnavailable`] during calls, and
/// [`StaticCheckErrorKind::AtBlockUnavailable`] during deployment.
#[test]
fn bad_block_hash() {
let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec();
deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33);

contract_call_consensus_test!(
contract_name: "bad-block-hash",
contract_code: "
Expand All @@ -700,6 +713,8 @@ fn bad_block_hash() {
)",
function_name: "trigger",
function_args: &[],
deploy_epochs: &deploy_epochs,
call_epochs: &[StacksEpochId::Epoch33],
);
}

Expand Down Expand Up @@ -839,6 +854,9 @@ fn defunct_pox_contracts() {
/// Error: [`RuntimeError::BlockTimeNotAvailable`]
/// Caused by: attempting to retrieve the stacks-block-time from a pre-3.3 height
/// Outcome: block accepted
/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a
/// [`RuntimeCheckErrorKind::AtBlockUnavailable`] during calls, and
/// [`StaticCheckErrorKind::AtBlockUnavailable`] during deployment.
#[test]
fn block_time_not_available() {
contract_call_consensus_test!(
Expand All @@ -851,7 +869,8 @@ fn block_time_not_available() {
)",
function_name: "trigger",
function_args: &[ClarityValue::UInt(1)],
deploy_epochs: &StacksEpochId::since(StacksEpochId::Epoch33),
deploy_epochs: &[StacksEpochId::Epoch33],
call_epochs: &[StacksEpochId::Epoch33],
exclude_clarity_versions: &[ClarityVersion::Clarity1, ClarityVersion::Clarity2, ClarityVersion::Clarity3],
)
}
Loading