diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 906f97c01..77710318f 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -68,6 +68,8 @@ jobs: - tests::neon_integrations::fuzzed_median_fee_rate_estimation_test_window10 - tests::neon_integrations::use_latest_tip_integration_test - tests::neon_integrations::test_flash_block_skip_tenure + - tests::neon_integrations::test_chainwork_first_intervals + - tests::neon_integrations::test_chainwork_partial_interval - tests::epoch_205::test_dynamic_db_method_costs - tests::epoch_205::transition_empty_blocks - tests::epoch_205::test_cost_limit_switch_version205 diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index 42a5d1d60..582cc722c 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -185,7 +185,7 @@ impl<'a> ArithmeticOnlyChecker<'a> { return Err(Error::FunctionNotPermitted(function)); } Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print - | AsContract | ElementAt | IndexOf | Map | Filter | Fold | Slice => { + | AsContract | ElementAt | IndexOf | Map | Filter | Fold | Slice | ReplaceAt => { return Err(Error::FunctionNotPermitted(function)); } BuffToIntLe | BuffToUIntLe | BuffToIntBe | BuffToUIntBe => { diff --git a/clarity/src/vm/analysis/arithmetic_checker/tests.rs b/clarity/src/vm/analysis/arithmetic_checker/tests.rs index f0abf0943..fa3fa6fee 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/tests.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/tests.rs @@ -290,6 +290,9 @@ fn test_functions_clarity1() { ("(define-private (foo (a (list 3 uint))) (slice a u2 u3))", Ok(())), + ("(define-private (foo (a (list 3 uint)) (b uint)) + (replace-at a u1 b))", + Ok(())), ("(buff-to-int-le 0x0001)", Ok(())), ("(buff-to-uint-le 0x0001)", @@ -350,6 +353,9 @@ fn test_functions_clarity2() { ("(define-private (foo (a (list 3 uint))) (slice a u2 u3))", Err(FunctionNotPermitted(NativeFunctions::Slice))), + ("(define-private (foo (a (list 3 uint))) + (replace-at a u2 (list u3)))", + Err(FunctionNotPermitted(NativeFunctions::ReplaceAt))), ("(buff-to-int-le 0x0001)", Err(FunctionNotPermitted(NativeFunctions::BuffToIntLe))), ("(buff-to-uint-le 0x0001)", diff --git a/clarity/src/vm/analysis/read_only_checker/mod.rs b/clarity/src/vm/analysis/read_only_checker/mod.rs index 947a5e289..1eeb075cd 100644 --- a/clarity/src/vm/analysis/read_only_checker/mod.rs +++ b/clarity/src/vm/analysis/read_only_checker/mod.rs @@ -286,7 +286,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo | TupleGet | TupleMerge | Len | Print | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount | GetTokenBalance | GetAssetOwner | GetTokenSupply - | ElementAt | IndexOf | Slice => { + | ElementAt | IndexOf | Slice | ReplaceAt => { // Check all arguments. self.check_each_expression_is_read_only(args) } diff --git a/clarity/src/vm/analysis/read_only_checker/tests.rs b/clarity/src/vm/analysis/read_only_checker/tests.rs index 24c946a6d..a1eae73b4 100644 --- a/clarity/src/vm/analysis/read_only_checker/tests.rs +++ b/clarity/src/vm/analysis/read_only_checker/tests.rs @@ -158,6 +158,10 @@ fn test_simple_read_only_violations() { (define-private (func1) (begin (map-set tokens (tuple (account tx-sender)) (tuple (balance 10))) (list 1 2))) (define-read-only (not-reading-only) (concat (func1) (func1)))", + "(define-map tokens { account: principal } { balance: int }) + (define-private (func1) (begin (map-set tokens (tuple (account tx-sender)) (tuple (balance 10))) (list 1 2))) + (define-read-only (not-reading-only) + (replace-at (func1) u0 3))", "(define-map tokens { account: principal } { balance: int }) (define-private (func1) (begin (map-set tokens (tuple (account tx-sender)) (tuple (balance 10))) (list 1 2))) (define-read-only (not-reading-only) diff --git a/clarity/src/vm/analysis/type_checker/natives/mod.rs b/clarity/src/vm/analysis/type_checker/natives/mod.rs index 444bd4575..0fde0baec 100644 --- a/clarity/src/vm/analysis/type_checker/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/natives/mod.rs @@ -850,6 +850,7 @@ impl TypedNativeFunction { ElementAt => Special(SpecialNativeFunction(&sequences::check_special_element_at)), IndexOf => Special(SpecialNativeFunction(&sequences::check_special_index_of)), Slice => Special(SpecialNativeFunction(&sequences::check_special_slice)), + ReplaceAt => Special(SpecialNativeFunction(&sequences::check_special_replace_at)), ListCons => Special(SpecialNativeFunction(&check_special_list_cons)), FetchEntry => Special(SpecialNativeFunction(&maps::check_special_fetch_entry)), SetEntry => Special(SpecialNativeFunction(&maps::check_special_set_entry)), diff --git a/clarity/src/vm/analysis/type_checker/natives/sequences.rs b/clarity/src/vm/analysis/type_checker/natives/sequences.rs index b6fba58c5..73fac1946 100644 --- a/clarity/src/vm/analysis/type_checker/natives/sequences.rs +++ b/clarity/src/vm/analysis/type_checker/natives/sequences.rs @@ -423,3 +423,28 @@ pub fn check_special_slice( Ok(seq) } + +/// This function type checks the Clarity2 function `replace-at`. +pub fn check_special_replace_at( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> TypeResult { + check_argument_count(3, args)?; + + runtime_cost(ClarityCostFunction::AnalysisIterableFunc, checker, 0)?; + // Check sequence + let input_type = checker.type_check(&args[0], context)?; + let seq_type = match &input_type { + TypeSignature::SequenceType(seq) => seq, + _ => return Err(CheckErrors::ExpectedSequence(input_type).into()), + }; + let unit_seq = seq_type.unit_type(); + // Check index argument + checker.type_check_expects(&args[1], context, &TypeSignature::UIntType)?; + // Check element argument + checker.type_check_expects(&args[2], context, &unit_seq)?; + + let final_type = TypeSignature::new_option(input_type)?; + Ok(final_type) +} diff --git a/clarity/src/vm/analysis/type_checker/tests/mod.rs b/clarity/src/vm/analysis/type_checker/tests/mod.rs index b5696c091..9416f6420 100644 --- a/clarity/src/vm/analysis/type_checker/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/tests/mod.rs @@ -1455,6 +1455,229 @@ fn test_slice_utf8() { } } +#[test] +fn test_replace_at_list() { + let good = [ + "(replace-at (list 2 3 4 5 6 7 8) u0 10)", + "(replace-at (list u0 u1 u2 u3 u4) u3 u10)", + "(replace-at (list true) u0 false)", + "(replace-at (list 2 3 4 5 6 7 8) u6 10)", + "(replace-at (list (list 1) (list 2)) u0 (list 33))", + "(replace-at (list (list 1 2) (list 3 4)) u0 (list 0))", + "(replace-at (list (list 1 2 3)) u0 (list 0))", + ]; + let expected = [ + "(optional (list 7 int))", + "(optional (list 5 uint))", + "(optional (list 1 bool))", + "(optional (list 7 int))", + "(optional (list 2 (list 1 int)))", + "(optional (list 2 (list 2 int)))", + "(optional (list 1 (list 3 int)))", + ]; + + for (good_test, expected) in good.iter().zip(expected.iter()) { + assert_eq!( + expected, + &format!("{}", type_check_helper(&good_test).unwrap()) + ); + } + + let bad = [ + "(replace-at (list 2 3) u0 (list 4))", + "(replace-at (list 2 3) u0 true)", + "(replace-at (list 2 3) 0 4)", + "(replace-at (list 2 3) u0 4 5)", + "(replace-at (list u0) u0)", + "(replace-at (list (list 1) (list 2)) u0 (list 33 44))", + ]; + + let bad_expected = [ + CheckErrors::TypeError( + IntType, + SequenceType(ListType(ListTypeData::new_list(IntType, 1).unwrap())), + ), + CheckErrors::TypeError(IntType, BoolType), + CheckErrors::TypeError(UIntType, IntType), + CheckErrors::IncorrectArgumentCount(3, 4), + CheckErrors::IncorrectArgumentCount(3, 2), + CheckErrors::TypeError( + SequenceType(ListType(ListTypeData::new_list(IntType, 1).unwrap())), + SequenceType(ListType(ListTypeData::new_list(IntType, 2).unwrap())), + ), + ]; + for (bad_test, expected) in bad.iter().zip(bad_expected.iter()) { + assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err); + } +} + +#[test] +fn test_replace_at_buff() { + let good = [ + "(replace-at 0x00112233 u0 0x44)", + "(replace-at 0x00112233 u3 0x66)", + "(replace-at 0x00 u0 0x22)", + "(replace-at 0x001122334455 u2 0x66)", + ]; + let expected = [ + "(optional (buff 4))", + "(optional (buff 4))", + "(optional (buff 1))", + "(optional (buff 6))", + ]; + + for (good_test, expected) in good.iter().zip(expected.iter()) { + assert_eq!( + expected, + &format!("{}", type_check_helper(&good_test).unwrap()) + ); + } + + let bad = [ + "(replace-at 0x0011 u0 (list 0))", + "(replace-at 0x0011 u0 \"a\")", + "(replace-at 0x0011 0 0x22)", + "(replace-at 0x0011 u0 0x44 0x55)", + "(replace-at 0x11 u0)", + "(replace-at 0x001122334455 u2 0x6677)", + ]; + + let buff_len = BufferLength::try_from(1u32).unwrap(); + let buff_len_two = BufferLength::try_from(2u32).unwrap(); + let bad_expected = [ + CheckErrors::TypeError( + SequenceType(BufferType(buff_len.clone())), + SequenceType(ListType(ListTypeData::new_list(IntType, 1).unwrap())), + ), + CheckErrors::TypeError( + SequenceType(BufferType(buff_len.clone())), + SequenceType(StringType(ASCII(buff_len.clone()))), + ), + CheckErrors::TypeError(UIntType, IntType), + CheckErrors::IncorrectArgumentCount(3, 4), + CheckErrors::IncorrectArgumentCount(3, 2), + CheckErrors::TypeError( + SequenceType(BufferType(buff_len.clone())), + SequenceType(BufferType(buff_len_two.clone())), + ), + ]; + for (bad_test, expected) in bad.iter().zip(bad_expected.iter()) { + assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err); + } +} + +#[test] +fn test_replace_at_ascii() { + let good = [ + "(replace-at \"abcd\" u0 \"f\")", + "(replace-at \"abcd\" u3 \"f\")", + "(replace-at \"a\" u0 \"f\")", + "(replace-at \"abcdefg\" u2 \"h\")", + ]; + let expected = [ + "(optional (string-ascii 4))", + "(optional (string-ascii 4))", + "(optional (string-ascii 1))", + "(optional (string-ascii 7))", + "(optional (string-ascii 7))", + ]; + + for (good_test, expected) in good.iter().zip(expected.iter()) { + assert_eq!( + expected, + &format!("{}", type_check_helper(&good_test).unwrap()) + ); + } + + let bad = [ + "(replace-at \"abcd\" u0 (list 0))", + "(replace-at \"abcd\" u0 0x00)", + "(replace-at \"abcd\" 0 \"e\")", + "(replace-at \"abcd\" u0 \"a\" \"d\")", + "(replace-at \"abcd\" u0)", + "(replace-at \"abcdefg\" u2 \"hi\")", + ]; + + let buff_len = BufferLength::try_from(1u32).unwrap(); + let buff_len_two = BufferLength::try_from(2u32).unwrap(); + let bad_expected = [ + CheckErrors::TypeError( + SequenceType(StringType(ASCII(buff_len.clone()))), + SequenceType(ListType(ListTypeData::new_list(IntType, 1).unwrap())), + ), + CheckErrors::TypeError( + SequenceType(StringType(ASCII(buff_len.clone()))), + SequenceType(BufferType(buff_len.clone())), + ), + CheckErrors::TypeError(UIntType, IntType), + CheckErrors::IncorrectArgumentCount(3, 4), + CheckErrors::IncorrectArgumentCount(3, 2), + CheckErrors::TypeError( + SequenceType(StringType(ASCII(buff_len.clone()))), + SequenceType(StringType(ASCII(buff_len_two.clone()))), + ), + ]; + for (bad_test, expected) in bad.iter().zip(bad_expected.iter()) { + assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err); + } +} + +#[test] +fn test_replace_at_utf8() { + let good = [ + "(replace-at u\"abcd\" u0 u\"f\")", + "(replace-at u\"abcd\" u3 u\"f\")", + "(replace-at u\"a\" u0 u\"f\")", + "(replace-at u\"abcdefg\" u2 u\"h\")", + ]; + let expected = [ + "(optional (string-utf8 4))", + "(optional (string-utf8 4))", + "(optional (string-utf8 1))", + "(optional (string-utf8 7))", + ]; + + for (good_test, expected) in good.iter().zip(expected.iter()) { + assert_eq!( + expected, + &format!("{}", type_check_helper(&good_test).unwrap()) + ); + } + + let bad = [ + "(replace-at u\"abcd\" u0 (list 0))", + "(replace-at u\"abcd\" u0 0x00)", + "(replace-at u\"abcd\" 0 u\"a\")", + "(replace-at u\"abcd\" u0 u\"a\" u\"d\")", + "(replace-at u\"abcd\" u0)", + "(replace-at u\"abcdefg\" u2 u\"hi\")", + ]; + + let buff_len = BufferLength::try_from(1u32).unwrap(); + let str_len = StringUTF8Length::try_from(1u32).unwrap(); + let str_len_two = StringUTF8Length::try_from(2u32).unwrap(); + let bad_expected = [ + CheckErrors::TypeError( + SequenceType(StringType(UTF8(str_len.clone()))), + SequenceType(ListType(ListTypeData::new_list(IntType, 1).unwrap())), + ), + CheckErrors::TypeError( + SequenceType(StringType(UTF8(str_len.clone()))), + SequenceType(BufferType(buff_len.clone())), + ), + CheckErrors::TypeError(UIntType, IntType), + CheckErrors::IncorrectArgumentCount(3, 4), + CheckErrors::IncorrectArgumentCount(3, 2), + CheckErrors::TypeError( + SequenceType(StringType(UTF8(str_len.clone()))), + SequenceType(StringType(UTF8(str_len_two.clone()))), + ), + ]; + for (bad_test, expected) in bad.iter().zip(bad_expected.iter()) { + assert_eq!(expected, &type_check_helper(&bad_test).unwrap_err().err); + } +} + #[test] fn test_native_concat() { let good = ["(concat (list 2 3) (list 4 5))"]; diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index a1de9c96f..de2f8e870 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2204,6 +2204,28 @@ to deserialize the type, the method returns `none`. "#, }; +const REPLACE_AT: SpecialAPI = SpecialAPI { + input_type: "sequence_A, uint, A", + output_type: "(optional sequence_A)", + snippet: "replace-at ${1:sequence} ${2:index} ${3:element}", + signature: "(replace-at sequence index element)", + description: "The `replace-at` function takes in a sequence, an index, and an element, +and returns a new sequence with the data at the index position replaced with the given element. +The given element's type must match the type of the sequence, and must correspond to a single +index of the input sequence. The return type on success is the same type as the input sequence. + +If the provided index is out of bounds, this functions returns `none`. +", + example: r#" +(replace-at u"ab" u1 u"c") ;; Returns (some u"ac") +(replace-at 0x00112233 u2 0x44) ;; Returns (some 0x00114433) +(replace-at "abcd" u3 "e") ;; Returns (some "abce") +(replace-at (list 1) u0 10) ;; Returns (some (10)) +(replace-at (list (list 1) (list 2)) u0 (list 33)) ;; Returns (some ((33) (2))) +(replace-at (list 1 2) u3 4) ;; Returns none +"#, +}; + pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { use crate::vm::functions::NativeFunctions::*; let name = function.get_name(); @@ -2307,6 +2329,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { StxBurn => make_for_simple_native(&STX_BURN, &StxBurn, name), ToConsensusBuff => make_for_special(&TO_CONSENSUS_BUFF, function), FromConsensusBuff => make_for_special(&FROM_CONSENSUS_BUFF, function), + ReplaceAt => make_for_special(&REPLACE_AT, function), } } diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index 39c110720..3a8312c1a 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -172,6 +172,7 @@ define_versioned_named_enum!(NativeFunctions(ClarityVersion) { Slice("slice", ClarityVersion::Clarity2), ToConsensusBuff("to-consensus-buff", ClarityVersion::Clarity2), FromConsensusBuff("from-consensus-buff", ClarityVersion::Clarity2), + ReplaceAt("replace-at", ClarityVersion::Clarity2), }); impl NativeFunctions { @@ -515,6 +516,7 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option FromConsensusBuff => { SpecialFunction("from_consensus_buff", &conversions::from_consensus_buff) } + ReplaceAt => SpecialFunction("replace_at", &sequences::special_replace_at), }; Some(callable) } else { diff --git a/clarity/src/vm/functions/sequences.rs b/clarity/src/vm/functions/sequences.rs index 030ff59e1..dd567277f 100644 --- a/clarity/src/vm/functions/sequences.rs +++ b/clarity/src/vm/functions/sequences.rs @@ -383,3 +383,48 @@ pub fn special_slice( }?; Ok(sliced_seq) } + +pub fn special_replace_at( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> Result { + check_argument_count(3, args)?; + + // Set the input to runtime_cost to 0, since replacing an element at an index is an O(1) operation. + runtime_cost(ClarityCostFunction::Unimplemented, env, 0)?; + + let seq = eval(&args[0], env, context)?; + let seq_type = TypeSignature::type_of(&seq); + let expected_elem_type = if let TypeSignature::SequenceType(seq_subtype) = &seq_type { + seq_subtype.unit_type() + } else { + return Err(CheckErrors::ExpectedSequence(seq_type).into()); + }; + let index_val = eval(&args[1], env, context)?; + let new_element = eval(&args[2], env, context)?; + + if expected_elem_type != TypeSignature::NoType && !expected_elem_type.admits(&new_element) { + return Err(CheckErrors::TypeValueError(expected_elem_type, new_element).into()); + } + + let index = if let Value::UInt(index_u128) = index_val { + if let Ok(index_usize) = usize::try_from(index_u128) { + index_usize + } else { + return Ok(Value::none()); + } + } else { + return Err(CheckErrors::TypeValueError(TypeSignature::UIntType, index_val).into()); + }; + + if let Value::Sequence(data) = seq { + let seq_len = data.len(); + if index >= seq_len { + return Ok(Value::none()); + } + data.replace_at(index, new_element) + } else { + return Err(CheckErrors::ExpectedSequence(seq_type).into()); + } +} diff --git a/clarity/src/vm/tests/sequences.rs b/clarity/src/vm/tests/sequences.rs index aed20ddfb..d0d1d6e1c 100644 --- a/clarity/src/vm/tests/sequences.rs +++ b/clarity/src/vm/tests/sequences.rs @@ -16,7 +16,7 @@ use crate::vm::types::signatures::{ListTypeData, SequenceSubtype}; use crate::vm::types::TypeSignature::{BoolType, IntType, SequenceType, UIntType}; -use crate::vm::types::{TypeSignature, Value}; +use crate::vm::types::{StringSubtype, StringUTF8Length, TypeSignature, Value}; #[cfg(test)] use rstest::rstest; #[cfg(test)] @@ -24,9 +24,13 @@ use rstest_reuse::{self, *}; use crate::vm::analysis::errors::CheckError; use crate::vm::errors::{CheckErrors, Error, RuntimeErrorType}; +use crate::vm::types::signatures::SequenceSubtype::{BufferType, ListType, StringType}; +use crate::vm::types::signatures::StringSubtype::ASCII; +use crate::vm::types::BufferLength; +use crate::vm::types::CharType::UTF8; use crate::vm::{execute, execute_v2, ClarityVersion}; use stacks_common::types::StacksEpochId; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; #[template] #[rstest] @@ -637,6 +641,338 @@ fn test_simple_buff_concat() { ); } +#[test] +fn test_simple_list_replace_at() { + let tests = [ + "(replace-at (list 1 2) u1 4)", + "(replace-at (list 1) u0 10)", + "(replace-at (list 1 9 0 5) u3 6)", + "(replace-at (list 4 5 6 7 8) u2 11)", + "(replace-at (list (list 1) (list 2)) u0 (list 33))", + "(replace-at (list (list 1 2) (list 3 4)) u0 (list 0))", + "(replace-at (list (list 1 2 3)) u0 (list 0))", + ]; + + let expected = [ + Value::some(Value::list_from(vec![Value::Int(1), Value::Int(4)]).unwrap()).unwrap(), + Value::some(Value::list_from(vec![Value::Int(10)]).unwrap()).unwrap(), + Value::some( + Value::list_from(vec![ + Value::Int(1), + Value::Int(9), + Value::Int(0), + Value::Int(6), + ]) + .unwrap(), + ) + .unwrap(), + Value::some( + Value::list_from(vec![ + Value::Int(4), + Value::Int(5), + Value::Int(11), + Value::Int(7), + Value::Int(8), + ]) + .unwrap(), + ) + .unwrap(), + Value::some( + Value::list_from(vec![ + Value::list_from(vec![Value::Int(33)]).unwrap(), + Value::list_from(vec![Value::Int(2)]).unwrap(), + ]) + .unwrap(), + ) + .unwrap(), + Value::some( + Value::list_from(vec![ + Value::list_from(vec![Value::Int(0)]).unwrap(), + Value::list_from(vec![Value::Int(3), Value::Int(4)]).unwrap(), + ]) + .unwrap(), + ) + .unwrap(), + Value::some( + Value::list_from(vec![Value::list_from(vec![Value::Int(0)]).unwrap()]).unwrap(), + ) + .unwrap(), + ]; + + for (test, expected) in tests.iter().zip(expected.iter()) { + assert_eq!(expected.clone(), execute_v2(test).unwrap().unwrap()); + } + + let bad_tests = [ + // index is out of bounds + "(replace-at (list 1 2) u3 4)", + // the sequence is length 0, so the index is out of bounds + "(replace-at (list) u0 6)", + ]; + + let bad_expected = [Value::none(), Value::none()]; + + for (bad_test, bad_expected) in bad_tests.iter().zip(bad_expected.iter()) { + assert_eq!(bad_expected.clone(), execute_v2(bad_test).unwrap().unwrap()); + } + + // The sequence input has the wrong type + assert_eq!( + execute_v2("(replace-at 0 u0 (list 0))").unwrap_err(), + CheckErrors::ExpectedSequence(IntType).into() + ); + + // The type of the index should be uint. + assert_eq!( + execute_v2("(replace-at (list 1) 0 0)").unwrap_err(), + CheckErrors::TypeValueError(UIntType, Value::Int(0)).into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at (list 2 3) u0 true)").unwrap_err(), + CheckErrors::TypeValueError(IntType, Value::Bool(true)).into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at (list 2 3) u0 0x00)").unwrap_err(), + CheckErrors::TypeValueError(IntType, Value::buff_from_byte(0)).into() + ); +} + +#[test] +fn test_simple_buff_replace_at() { + let tests = [ + "(replace-at 0x3031 u1 0x44)", + "(replace-at 0x00 u0 0x11)", + "(replace-at 0x00112233 u3 0x44)", + "(replace-at 0x00112233 u1 0x44)", + ]; + + let expected = [ + Value::some(Value::buff_from(vec![48, 68]).unwrap()).unwrap(), + Value::some(Value::buff_from(vec![17]).unwrap()).unwrap(), + Value::some(Value::buff_from(vec![0, 17, 34, 68]).unwrap()).unwrap(), + Value::some(Value::buff_from(vec![0, 68, 34, 51]).unwrap()).unwrap(), + ]; + + for (test, expected) in tests.iter().zip(expected.iter()) { + assert_eq!(expected.clone(), execute_v2(test).unwrap().unwrap()); + } + + let bad_tests = [ + // index is out of bounds + "(replace-at 0x0022 u3 0x44)", + // the sequence is length 0, so the index is out of bounds + "(replace-at 0x u0 0x11)", + ]; + + let bad_expected = [Value::none(), Value::none()]; + + for (bad_test, bad_expected) in bad_tests.iter().zip(bad_expected.iter()) { + assert_eq!(bad_expected.clone(), execute_v2(bad_test).unwrap().unwrap()); + } + + // The sequence input has the wrong type + assert_eq!( + execute_v2("(replace-at 33 u0 0x00)").unwrap_err(), + CheckErrors::ExpectedSequence(IntType).into() + ); + + // The type of the index should be uint. + assert_eq!( + execute_v2("(replace-at 0x002244 0 0x99)").unwrap_err(), + CheckErrors::TypeValueError(UIntType, Value::Int(0)).into() + ); + + // The element input has the wrong type + let buff_len = BufferLength::try_from(1u32).unwrap(); + assert_eq!( + execute_v2("(replace-at 0x445522 u0 55)").unwrap_err(), + CheckErrors::TypeValueError(SequenceType(BufferType(buff_len.clone())), Value::Int(55)) + .into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at 0x445522 u0 (list 5))").unwrap_err(), + CheckErrors::TypeValueError( + SequenceType(BufferType(buff_len.clone())), + Value::list_from(vec![Value::Int(5)]).unwrap() + ) + .into() + ); + + // The element input has the wrong type (not length 1) + assert_eq!( + execute_v2("(replace-at 0x445522 u0 0x0044)").unwrap_err(), + CheckErrors::TypeValueError( + SequenceType(BufferType(buff_len.clone())), + Value::buff_from(vec![0, 68]).unwrap() + ) + .into() + ); +} + +#[test] +fn test_simple_string_ascii_replace_at() { + let tests = [ + "(replace-at \"ab\" u1 \"c\")", + "(replace-at \"a\" u0 \"c\")", + "(replace-at \"abcd\" u3 \"e\")", + "(replace-at \"abcd\" u1 \"e\")", + ]; + + let expected = [ + Value::some(Value::string_ascii_from_bytes("ac".into()).unwrap()).unwrap(), + Value::some(Value::string_ascii_from_bytes("c".into()).unwrap()).unwrap(), + Value::some(Value::string_ascii_from_bytes("abce".into()).unwrap()).unwrap(), + Value::some(Value::string_ascii_from_bytes("aecd".into()).unwrap()).unwrap(), + ]; + + for (test, expected) in tests.iter().zip(expected.iter()) { + assert_eq!(expected.clone(), execute_v2(test).unwrap().unwrap()); + } + + let bad_tests = [ + // index is out of bounds + "(replace-at \"ab\" u3 \"c\")", + // the sequence is length 0, so the index is out of bounds + "(replace-at \"\" u0 \"a\")", + ]; + + let bad_expected = [Value::none(), Value::none()]; + + for (bad_test, bad_expected) in bad_tests.iter().zip(bad_expected.iter()) { + assert_eq!(bad_expected.clone(), execute_v2(bad_test).unwrap().unwrap()); + } + + // The sequence input has the wrong type + assert_eq!( + execute_v2("(replace-at 33 u0 \"c\")").unwrap_err(), + CheckErrors::ExpectedSequence(IntType).into() + ); + + // The type of the index should be uint. + assert_eq!( + execute_v2("(replace-at \"abc\" 0 \"c\")").unwrap_err(), + CheckErrors::TypeValueError(UIntType, Value::Int(0)).into() + ); + + // The element input has the wrong type + let buff_len = BufferLength::try_from(1u32).unwrap(); + assert_eq!( + execute_v2("(replace-at \"abc\" u0 55)").unwrap_err(), + CheckErrors::TypeValueError( + SequenceType(StringType(ASCII(buff_len.clone()))), + Value::Int(55) + ) + .into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at \"abc\" u0 0x00)").unwrap_err(), + CheckErrors::TypeValueError( + SequenceType(StringType(ASCII(buff_len.clone()))), + Value::buff_from_byte(0) + ) + .into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at \"abc\" u0 \"de\")").unwrap_err(), + CheckErrors::TypeValueError( + SequenceType(StringType(ASCII(buff_len.clone()))), + Value::string_ascii_from_bytes("de".into()).unwrap() + ) + .into() + ); +} + +#[test] +fn test_simple_string_utf8_replace_at() { + let tests = [ + "(replace-at u\"ab\" u1 u\"c\")", + "(replace-at u\"a\" u0 u\"c\")", + "(replace-at u\"abcd\" u3 u\"e\")", + "(replace-at u\"abcd\" u1 u\"e\")", + "(replace-at u\"hello\\u{1F98A}\" u5 u\"e\")", + "(replace-at u\"hello\\u{1F98A}\" u2 u\"e\")", + ]; + + let expected = [ + Value::some(Value::string_utf8_from_bytes("ac".into()).unwrap()).unwrap(), + Value::some(Value::string_utf8_from_bytes("c".into()).unwrap()).unwrap(), + Value::some(Value::string_utf8_from_bytes("abce".into()).unwrap()).unwrap(), + Value::some(Value::string_utf8_from_bytes("aecd".into()).unwrap()).unwrap(), + Value::some(Value::string_utf8_from_bytes("helloe".into()).unwrap()).unwrap(), + Value::some(Value::string_utf8_from_bytes("heelo🦊".into()).unwrap()).unwrap(), + ]; + + for (test, expected) in tests.iter().zip(expected.iter()) { + assert_eq!(expected.clone(), execute_v2(test).unwrap().unwrap()); + } + + let bad_tests = [ + // index is out of bounds + "(replace-at u\"ab\" u3 u\"c\")", + // the sequence is length 0, so the index is out of bounds + "(replace-at u\"\" u0 u\"a\")", + ]; + + let bad_expected = [Value::none(), Value::none()]; + + for (bad_test, bad_expected) in bad_tests.iter().zip(bad_expected.iter()) { + assert_eq!(bad_expected.clone(), execute_v2(bad_test).unwrap().unwrap()); + } + + // The sequence input has the wrong type + assert_eq!( + execute_v2("(replace-at 33 u0 u\"c\")").unwrap_err(), + CheckErrors::ExpectedSequence(IntType).into() + ); + + // The type of the index should be uint. + assert_eq!( + execute_v2("(replace-at u\"abc\" 0 u\"c\")").unwrap_err(), + CheckErrors::TypeValueError(UIntType, Value::Int(0)).into() + ); + + // The element input has the wrong type + let str_len = StringUTF8Length::try_from(1u32).unwrap(); + assert_eq!( + execute_v2("(replace-at u\"abc\" u0 55)").unwrap_err(), + CheckErrors::TypeValueError( + TypeSignature::SequenceType(StringType(StringSubtype::UTF8(str_len.clone()))), + Value::Int(55) + ) + .into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at u\"abc\" u0 0x00)").unwrap_err(), + CheckErrors::TypeValueError( + TypeSignature::SequenceType(StringType(StringSubtype::UTF8(str_len.clone()))), + Value::buff_from_byte(0) + ) + .into() + ); + + // The element input has the wrong type + assert_eq!( + execute_v2("(replace-at u\"abc\" u0 u\"de\")").unwrap_err(), + CheckErrors::TypeValueError( + TypeSignature::SequenceType(StringType(StringSubtype::UTF8(str_len.clone()))), + Value::string_utf8_from_string_utf8_literal("de".to_string()).unwrap() + ) + .into() + ); +} + #[test] fn test_simple_buff_assert_max_len() { let tests = [ diff --git a/clarity/src/vm/types/mod.rs b/clarity/src/vm/types/mod.rs index e36cd5a21..b86984c23 100644 --- a/clarity/src/vm/types/mod.rs +++ b/clarity/src/vm/types/mod.rs @@ -282,6 +282,58 @@ impl SequenceData { Some(result) } + pub fn replace_at(self, index: usize, element: Value) -> Result { + let seq_length = self.len(); + + // Check that the length of the provided element is 1. In the case that SequenceData + // is a list, we check that the provided element is the right type below. + if !self.is_list() { + if let Value::Sequence(data) = &element { + let elem_length = data.len(); + if elem_length != 1 { + return Err(RuntimeErrorType::BadTypeConstruction.into()); + } + } else { + return Err(RuntimeErrorType::BadTypeConstruction.into()); + } + } + if index >= seq_length { + return Err(CheckErrors::ValueOutOfBounds.into()); + } + + let new_seq_data = match (self, element) { + (SequenceData::Buffer(mut data), Value::Sequence(SequenceData::Buffer(elem))) => { + data.data[index] = elem.data[0]; + SequenceData::Buffer(data) + } + (SequenceData::List(mut data), elem) => { + let entry_type = data.type_signature.get_list_item_type(); + if !entry_type.admits(&elem) { + return Err(CheckErrors::ListTypesMustMatch.into()); + } + data.data[index] = elem; + SequenceData::List(data) + } + ( + SequenceData::String(CharType::ASCII(mut data)), + Value::Sequence(SequenceData::String(CharType::ASCII(elem))), + ) => { + data.data[index] = elem.data[0]; + SequenceData::String(CharType::ASCII(data)) + } + ( + SequenceData::String(CharType::UTF8(mut data)), + Value::Sequence(SequenceData::String(CharType::UTF8(mut elem))), + ) => { + data.data[index] = elem.data.swap_remove(0); + SequenceData::String(CharType::UTF8(data)) + } + _ => return Err(CheckErrors::ListTypesMustMatch.into()), + }; + + Ok(Value::some(Value::Sequence(new_seq_data))?) + } + pub fn contains(&self, to_find: Value) -> Result> { match self { SequenceData::Buffer(ref data) => { @@ -459,6 +511,14 @@ impl SequenceData { Ok(result) } + + pub fn is_list(&self) -> bool { + if let SequenceData::List(x) = self { + true + } else { + false + } + } } #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] diff --git a/clarity/src/vm/types/signatures.rs b/clarity/src/vm/types/signatures.rs index 2e9e2ca23..81458558b 100644 --- a/clarity/src/vm/types/signatures.rs +++ b/clarity/src/vm/types/signatures.rs @@ -118,6 +118,13 @@ impl SequenceSubtype { SequenceSubtype::StringType(StringSubtype::UTF8(_)) => TypeSignature::min_string_utf8(), } } + + pub fn is_list_type(&self) -> bool { + match &self { + SequenceSubtype::ListType(_) => true, + _ => false, + } + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/burnchains/bitcoin/indexer.rs b/src/burnchains/bitcoin/indexer.rs index 6509b0af2..e954988da 100644 --- a/src/burnchains/bitcoin/indexer.rs +++ b/src/burnchains/bitcoin/indexer.rs @@ -472,22 +472,36 @@ impl BitcoinIndexer { let interval_start_block = (start_block / BLOCK_DIFFICULTY_CHUNK_SIZE).saturating_sub(2); let base_block = interval_start_block * BLOCK_DIFFICULTY_CHUNK_SIZE; - let interval_headers = - canonical_spv_client.read_block_headers(base_block, start_block + 1)?; - assert!( - interval_headers.len() >= (start_block - base_block) as usize, - "BUG: missing headers for {}-{}", - base_block, - start_block - ); - test_debug!( - "Copy headers {}-{}", - base_block, - base_block + interval_headers.len() as u64 - ); - reorg_spv_client - .insert_block_headers_before(base_block - 1, interval_headers)?; + if base_block > 0 { + let interval_headers = + canonical_spv_client.read_block_headers(base_block, start_block + 1)?; + assert!( + interval_headers.len() >= (start_block - base_block) as usize, + "BUG: missing headers for {}-{}", + base_block, + start_block + ); + + debug!( + "Copy headers {}-{}", + base_block, + base_block + interval_headers.len() as u64 + ); + reorg_spv_client + .insert_block_headers_before(base_block - 1, interval_headers)?; + } else { + let interval_headers = + canonical_spv_client.read_block_headers(1, start_block + 1)?; + assert!( + interval_headers.len() >= start_block as usize, + "BUG: missing headers for 1-{}", + start_block + ); + + debug!("Copy headers 1-{}", interval_headers.len() as u64); + reorg_spv_client.insert_block_headers_before(0, interval_headers)?; + } let last_interval = canonical_spv_client.find_highest_work_score_interval()?; @@ -545,12 +559,12 @@ impl BitcoinIndexer { let mut new_tip = 0; let mut found_common_ancestor = false; - let orig_spv_client = SpvClient::new( + let mut orig_spv_client = SpvClient::new( canonical_headers_path, 0, None, self.runtime.network_id, - false, + true, false, )?; @@ -718,7 +732,7 @@ impl BitcoinIndexer { if check_chain_work { let reorg_total_work = reorg_spv_client.update_chain_work()?; - let orig_total_work = orig_spv_client.get_chain_work()?; + let orig_total_work = orig_spv_client.update_chain_work()?; debug!("Bitcoin headers history is consistent up to {}", new_tip; "Orig chainwork" => %orig_total_work, diff --git a/src/clarity_vm/tests/costs.rs b/src/clarity_vm/tests/costs.rs index 764f24fb0..dc0c73d8a 100644 --- a/src/clarity_vm/tests/costs.rs +++ b/src/clarity_vm/tests/costs.rs @@ -159,6 +159,7 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str { Slice => "(slice str-foo u1 u1)", ToConsensusBuff => "(to-consensus-buff u1)", FromConsensusBuff => "(from-consensus-buff bool 0x03)", + ReplaceAt => "(replace-at list-bar u0 5)", } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 3cd54f834..e3ca39f09 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -7367,3 +7367,73 @@ fn test_flash_block_skip_tenure() { channel.stop_chains_coordinator(); } + +#[test] +#[ignore] +fn test_chainwork_first_intervals() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let (mut conf, miner_account) = neon_integration_test_conf(); + + let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + btcd_controller + .start_bitcoind() + .map_err(|_e| ()) + .expect("Failed starting bitcoind"); + + let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); + let http_origin = format!("http://{}", &conf.node.rpc_bind); + + btc_regtest_controller.bootstrap_chain(2016 * 2 - 1); + + eprintln!("Chain bootstrapped..."); + + let mut run_loop = neon::RunLoop::new(conf); + let blocks_processed = run_loop.get_blocks_processed_arc(); + let missed_tenures = run_loop.get_missed_tenures_arc(); + + let channel = run_loop.get_coordinator_channel().unwrap(); + + thread::spawn(move || run_loop.start(None, 0)); + + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + channel.stop_chains_coordinator(); +} + +#[test] +#[ignore] +fn test_chainwork_partial_interval() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let (mut conf, miner_account) = neon_integration_test_conf(); + + let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + btcd_controller + .start_bitcoind() + .map_err(|_e| ()) + .expect("Failed starting bitcoind"); + + let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); + let http_origin = format!("http://{}", &conf.node.rpc_bind); + + btc_regtest_controller.bootstrap_chain(2016 - 1); + + eprintln!("Chain bootstrapped..."); + + let mut run_loop = neon::RunLoop::new(conf); + let blocks_processed = run_loop.get_blocks_processed_arc(); + let missed_tenures = run_loop.get_missed_tenures_arc(); + + let channel = run_loop.get_coordinator_channel().unwrap(); + + thread::spawn(move || run_loop.start(None, 0)); + + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + channel.stop_chains_coordinator(); +}