From de0164407b1f16f460f7bc3c99992cd4fc135d98 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 12 Aug 2026 16:36:08 -0700 Subject: [PATCH 1/5] Add storage (state commitment) functions --- WISHLIST.md | 2 +- simf/lib/storage.simf | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 simf/lib/storage.simf diff --git a/WISHLIST.md b/WISHLIST.md index 87dfc24..d24f0c0 100644 --- a/WISHLIST.md +++ b/WISHLIST.md @@ -84,7 +84,7 @@ Q. Isn't the first one directly implemented by a jet? Do we just need to `unwrap # Storage (state management) -* Store and load single uninterpreted `u256` value +√ Store and load single uninterpreted `u256` value * Merkle tree tools (maybe also codegen for Merkle tree manipulation based on a separate schema?) # Covenants (high-level) diff --git a/simf/lib/storage.simf b/simf/lib/storage.simf new file mode 100644 index 0000000..514abcb --- /dev/null +++ b/simf/lib/storage.simf @@ -0,0 +1,53 @@ +fn own_script_hash_with_state(state_data: u256) -> u256 { + // This is the bulk of our "compute state commitment" logic. + let tap_leaf: u256 = jet::tapleaf_hash(); + let state_ctx1: Ctx8 = jet::tapdata_init(); + let state_ctx2: Ctx8 = jet::sha_256_ctx_8_add_32(state_ctx1, state_data); + let state_leaf: u256 = jet::sha_256_ctx_8_finalize(state_ctx2); + let tap_node: u256 = jet::build_tapbranch(tap_leaf, state_leaf); + + // Compute a taptweak using this. + let bip0341_key: u256 = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0; + let tweaked_key: u256 = jet::build_taptweak(bip0341_key, tap_node); + + // Turn the taptweak into a script hash. + let hash_ctx1: Ctx8 = jet::sha_256_ctx_8_init(); + let hash_ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(hash_ctx1, 0x5120); // Segwit v1, length 32 + let hash_ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(hash_ctx2, tweaked_key); + jet::sha_256_ctx_8_finalize(hash_ctx3) +} + +pub fn load(state_data: u256) { + // Assert that the input state is correct, i.e. "load". + // + // Enforce that the state commitment hash in the Taptree alongside + // the current input is equal to state_data. (This must be a result + // of the transaction builder's having constructed the prior + // transaction so that this is true.) + // Panics otherwise. + assert!(jet::eq_256( + own_script_hash_with_state(state_data), + unwrap(jet::input_script_hash(jet::current_index())) + )); +} + +pub fn store(new_state: u256, index: u32) { + // Assert that the output state is correct, i.e. "store". + // + // The index parameter specifies the output index where the + // new copy of this covenant is located. Depending on the + // covenant convention, that could be jet::current_index() + // (same index as input), some other hard-coded index + // demanded by convention, or could even be flexible and + // determined by a witness parameter. + // + // Enforce that the state commitment hash in the Taptree alongside + // the specified output is equal to new_state. (This must be a + // result of the transaction builder constructing the transaction + // so that this is true.) + // Panics otherwise. + assert!(jet::eq_256( + own_script_hash_with_state(new_state), + unwrap(jet::output_script_hash(index)) + )); +} From 46f9593bb8aabf728d4af6ba71396b8812a4545d Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 12 Aug 2026 16:36:34 -0700 Subject: [PATCH 2/5] Add tests for storage functions --- simf/storage_test.simf | 14 +++ tests/common/core.rs | 68 +++++++++-- tests/storage_test.rs | 268 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+), 12 deletions(-) create mode 100644 simf/storage_test.simf create mode 100644 tests/storage_test.rs diff --git a/simf/storage_test.simf b/simf/storage_test.simf new file mode 100644 index 0000000..de0191f --- /dev/null +++ b/simf/storage_test.simf @@ -0,0 +1,14 @@ +use crate::lib::storage::{load, store}; +use crate::helper::if_test_this_function; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let state_data: u256 = witness::STATE_DATA; + let new_state: u256 = witness::NEW_STATE; + let index: u32 = witness::INDEX; + + match if_test_this_function(0, fn_idx) { true => { load(state_data); }, false => (), }; + match if_test_this_function(1, fn_idx) { true => { store(new_state, index); }, false => (), }; + match if_test_this_function(2, fn_idx) { true => { load(state_data); store(new_state, index); }, false => (), }; +} diff --git a/tests/common/core.rs b/tests/common/core.rs index ada5e42..410a402 100644 --- a/tests/common/core.rs +++ b/tests/common/core.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use simplex::program::{Program, WitnessTrait}; -use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::elements::{Script, Sequence}; use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, }; @@ -43,13 +43,17 @@ pub fn fund( Ok(script) } -/// Construct the funded UTXO with `witness`. + +/// Construct the funded UTXO with `witness`, under a caller-chosen `sequence` (nSequence) +/// and with any additional `outputs` appended (in order, before any auto-generated +/// change/fee outputs -- so the Nth entry lands at output index N). pub fn construct_final_tx( context: &simplex::TestContext, program: &impl AsRef, script: &Script, witness: W, - data: Option<&[u8]>, + sequence: Sequence, + outputs: Vec, ) -> anyhow::Result where W: WitnessTrait + 'static, @@ -60,30 +64,32 @@ where let mut ft = FinalTransaction::new(); ft.add_program_input( - PartialInput::new(utxos[0].clone()), + PartialInput::new(utxos[0].clone()).with_sequence(sequence), ProgramInput::new(Box::new(program.as_ref().clone()), Box::new(witness)), RequiredSignature::None, ); - if let Some(data) = data { - ft.add_output(PartialOutput::new_metadata(data)) - }; + for output in outputs { + ft.add_output(output); + } Ok(ft) } -/// Spend the funded UTXO with `witness`. Return the broadcast result. +/// Spend the funded UTXO with `witness`, `sequence`, and `outputs`. Return the broadcast +/// result. pub fn spend( context: &simplex::TestContext, program: &impl AsRef, script: &Script, witness: W, - data: Option<&[u8]>, + sequence: Sequence, + outputs: Vec, ) -> anyhow::Result where W: WitnessTrait + 'static, { - let ft = construct_final_tx(context, program, script, witness, data)?; + let ft = construct_final_tx(context, program, script, witness, sequence, outputs)?; Ok(context.get_default_signer().broadcast(&ft)?.to_string()) } @@ -119,7 +125,7 @@ where W: WitnessTrait + 'static, { let script = fund(context, &program)?; - let result = spend(context, &program, &script, witness, None); + let result = spend(context, &program, &script, witness, Sequence::default(), vec![]); assert_error_msg(result, expect) } @@ -137,7 +143,45 @@ where W: WitnessTrait + 'static, { let script = fund(context, &program)?; - let result = spend(context, &program, &script, witness, Some(data)); + let result = spend( + context, + &program, + &script, + witness, + Sequence::default(), + vec![PartialOutput::new_metadata(data)], + ); + + assert_error_msg(result, expect) +} + +/// Fund + spend + assert the outcome, with a list of extra `(script, amount)` outputs +/// added to the transaction. +pub fn run_with_outputs( + context: &simplex::TestContext, + program: impl AsRef, + witness: W, + outputs: Vec<(Script, u64)>, + expect: Expect, +) -> anyhow::Result<()> +where + W: WitnessTrait + 'static, +{ + let script = fund(context, &program)?; + let outputs = outputs + .into_iter() + .map(|(output_script, amount)| { + PartialOutput::new(output_script, amount, context.get_network().policy_asset()) + }) + .collect(); + let result = spend( + context, + &program, + &script, + witness, + Sequence::default(), + outputs, + ); assert_error_msg(result, expect) } diff --git a/tests/storage_test.rs b/tests/storage_test.rs new file mode 100644 index 0000000..d62ccf5 --- /dev/null +++ b/tests/storage_test.rs @@ -0,0 +1,268 @@ +mod common; + +use simplex::simplicityhl::elements::Sequence; +use simplex::transaction::PartialOutput; + +use common::core::{construct_final_tx, fund, run, run_with_outputs, Expect}; + +use simplicityhl_std::artifacts::op_return_test::OpReturnTestProgram; +use simplicityhl_std::artifacts::op_return_test::derived_op_return_test::OpReturnTestArguments; +use simplicityhl_std::artifacts::storage_test::StorageTestProgram; +use simplicityhl_std::artifacts::storage_test::derived_storage_test::{ + StorageTestArguments, StorageTestWitness, +}; + +// Dispatch indices — must match the `if_test_this_function(N, ..)` arms in +// simf/storage_test.simf. +enum FunctionToTest { + Load, + Store, + Transition, // load + store together, in one spend +} + +#[inline] +fn op(o: FunctionToTest) -> u8 { + o as u8 +} + +// Two distinct 32-byte state values, used throughout to distinguish "the state that's +// really committed" from "the state a witness falsely claims." +const STATE_A: [u8; 32] = [0xAA; 32]; +const STATE_B: [u8; 32] = [0xBB; 32]; + +fn program() -> StorageTestProgram { + StorageTestProgram::new(StorageTestArguments {}) +} + +fn build_load_witness(state_data: [u8; 32]) -> StorageTestWitness { + StorageTestWitness { + function_index: op(FunctionToTest::Load), + state_data, + new_state: [0; 32], + index: 0, + } +} + +fn build_store_witness(new_state: [u8; 32], index: u32) -> StorageTestWitness { + StorageTestWitness { + function_index: op(FunctionToTest::Store), + state_data: [0; 32], + new_state, + index, + } +} + +fn build_transition_witness(state_data: [u8; 32], new_state: [u8; 32], index: u32) -> StorageTestWitness { + StorageTestWitness { + function_index: op(FunctionToTest::Transition), + state_data, + new_state, + index, + } +} + +mod storage_tests { + use super::*; + + // ---------- load ---------- + + #[simplex::test] + fn load_happy_path(context: simplex::TestContext) -> anyhow::Result<()> { + // Fund a UTXO that commits to STATE_A, then load exactly that. + let mut funded = program().with_storage_capacity(1); + funded.set_storage_at(0, STATE_A); + + run(&context, funded, build_load_witness(STATE_A), Expect::Ok) + } + + #[simplex::test] + fn load_wrong_state_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (1) The UTXO commits to STATE_A, but the witness claims STATE_B. + let mut funded = program().with_storage_capacity(1); + funded.set_storage_at(0, STATE_A); + + run( + &context, + funded, + build_load_witness(STATE_B), + Expect::AssertFailed, + ) + } + + #[simplex::test] + fn load_no_commitment_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (2) The UTXO is a bare instance of this contract, with no state leaf at all -- + // no claimed state can ever match, since `own_script_hash_with_state` always + // computes a 2-leaf tree, never a bare single-leaf one. + run( + &context, + program(), + build_load_witness(STATE_A), + Expect::AssertFailed, + ) + } + + // ---------- store ---------- + + #[simplex::test] + fn store_happy_path_index_0(context: simplex::TestContext) -> anyhow::Result<()> { + let mut target = program().with_storage_capacity(1); + target.set_storage_at(0, STATE_A); + let target_script = target.as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 0), + vec![(target_script, 50)], + Expect::Ok, + ) + } + + #[simplex::test] + fn store_happy_path_nonzero_index(context: simplex::TestContext) -> anyhow::Result<()> { + // Same as above, but the real target is output index 1, behind a dummy filler + // output at index 0 -- exercises that `index` is a genuine parameter, not just + // always 0. + let dummy_script = program().as_ref().get_script_pubkey(context.get_network()); + + let mut target = program().with_storage_capacity(1); + target.set_storage_at(0, STATE_A); + let target_script = target.as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 1), + vec![(dummy_script, 50), (target_script, 50)], + Expect::Ok, + ) + } + + #[simplex::test] + fn store_wrong_state_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (3) The output actually commits to STATE_A, but the witness claims STATE_B. + let mut target = program().with_storage_capacity(1); + target.set_storage_at(0, STATE_A); + let target_script = target.as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_B, 0), + vec![(target_script, 50)], + Expect::AssertFailed, + ) + } + + #[simplex::test] + fn store_no_commitment_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (4) The output is a bare instance of this contract (no state leaf at all). + let bare_script = program().as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 0), + vec![(bare_script, 50)], + Expect::AssertFailed, + ) + } + + #[simplex::test] + fn store_nonexistent_index_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (5) The claimed output index is far beyond the transaction's real output + // count -> `jet::output_script_hash` returns `None` -> `unwrap` hits the pruned + // branch. + let mut target = program().with_storage_capacity(1); + target.set_storage_at(0, STATE_A); + let target_script = target.as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 99), + vec![(target_script, 50)], + Expect::PrunedBranch, + ) + } + + #[simplex::test] + fn store_wrong_cmr_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (6) The output commits to the *right* state value, but paired with a + // *different* contract's CMR (here, `OpReturnTestProgram`, reused purely as a + // conveniently different already-compiled program) -- proving `store` verifies + // which contract as well as which state, not state alone. + let mut other = OpReturnTestProgram::new(OpReturnTestArguments {}).with_storage_capacity(1); + other.set_storage_at(0, STATE_A); + let other_script = other.as_ref().get_script_pubkey(context.get_network()); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 0), + vec![(other_script, 50)], + Expect::AssertFailed, + ) + } + + #[simplex::test] + fn state_chain_store_load_store_load(context: simplex::TestContext) -> anyhow::Result<()> { + // Exercises a genuine multi-transaction state chain: genesis (store only) -> + // transition (load + store, consuming the prior commitment and creating the + // next one) -> final consumption (load only). Each step spends the *real* + // output the previous step created, looked up by script -- safe here because + // every state value is unique, so there's no ambiguity about which UTXO to grab. + const STATE_1: [u8; 32] = [0x11; 32]; + const STATE_2: [u8; 32] = [0x22; 32]; + + let mut state1_program = program().with_storage_capacity(1); + state1_program.set_storage_at(0, STATE_1); + let state1_script = state1_program.as_ref().get_script_pubkey(context.get_network()); + + let mut state2_program = program().with_storage_capacity(1); + state2_program.set_storage_at(0, STATE_2); + let state2_script = state2_program.as_ref().get_script_pubkey(context.get_network()); + + let asset = context.get_network().policy_asset(); + + // Step 1 (genesis): spend a bare instance of the covenant, storing STATE_1 into + // output 0. + let genesis_script = fund(&context, &program())?; + let ft1 = construct_final_tx( + &context, + &program(), + &genesis_script, + build_store_witness(STATE_1, 0), + Sequence::default(), + vec![PartialOutput::new(state1_script.clone(), 50, asset)], + )?; + context.get_default_signer().broadcast(&ft1)?.wait()?; + + // Step 2 (transition): spend the STATE_1-committing output, loading STATE_1 and + // storing STATE_2 into output 0. + let ft2 = construct_final_tx( + &context, + &state1_program, + &state1_script, + build_transition_witness(STATE_1, STATE_2, 0), + Sequence::default(), + vec![PartialOutput::new(state2_script.clone(), 50, asset)], + )?; + context.get_default_signer().broadcast(&ft2)?.wait()?; + + // Step 3 (final consumption): spend the STATE_2-committing output, loading + // STATE_2, with no further re-commitment. + let ft3 = construct_final_tx( + &context, + &state2_program, + &state2_script, + build_load_witness(STATE_2), + Sequence::default(), + vec![], + )?; + context.get_default_signer().broadcast(&ft3)?; + + Ok(()) + } +} From 798738417a0d53a9aa4536f1afabef5b17cd4f74 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 12 Aug 2026 17:03:52 -0700 Subject: [PATCH 3/5] Add another intentionally failing test for inconsistent state update --- tests/storage_test.rs | 56 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/storage_test.rs b/tests/storage_test.rs index d62ccf5..4e5dd4b 100644 --- a/tests/storage_test.rs +++ b/tests/storage_test.rs @@ -3,7 +3,7 @@ mod common; use simplex::simplicityhl::elements::Sequence; use simplex::transaction::PartialOutput; -use common::core::{construct_final_tx, fund, run, run_with_outputs, Expect}; +use common::core::{assert_error_msg, construct_final_tx, fund, run, run_with_outputs, Expect}; use simplicityhl_std::artifacts::op_return_test::OpReturnTestProgram; use simplicityhl_std::artifacts::op_return_test::derived_op_return_test::OpReturnTestArguments; @@ -265,4 +265,58 @@ mod storage_tests { Ok(()) } + + #[simplex::test] + fn state_chain_second_hop_wrong_state_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // Same shape as `state_chain_store_load_store_load`, but the second hop claims + // the wrong prior state -- demonstrating that a real, otherwise-legitimate chain + // genuinely halts if someone tries to continue it with a state that doesn't + // match what was actually committed, rather than the covenant just trusting + // whatever the witness happens to assert. + const STATE_1: [u8; 32] = [0x11; 32]; + const WRONG_STATE_1: [u8; 32] = [0x99; 32]; + const STATE_2: [u8; 32] = [0x22; 32]; + + let mut state1_program = program().with_storage_capacity(1); + state1_program.set_storage_at(0, STATE_1); + let state1_script = state1_program.as_ref().get_script_pubkey(context.get_network()); + + let mut state2_program = program().with_storage_capacity(1); + state2_program.set_storage_at(0, STATE_2); + let state2_script = state2_program.as_ref().get_script_pubkey(context.get_network()); + + let asset = context.get_network().policy_asset(); + + // Step 1 (genesis): spend a bare instance of the covenant, storing STATE_1 into + // output 0. This step succeeds -- the chain is genuinely underway. + let genesis_script = fund(&context, &program())?; + let ft1 = construct_final_tx( + &context, + &program(), + &genesis_script, + build_store_witness(STATE_1, 0), + Sequence::default(), + vec![PartialOutput::new(state1_script.clone(), 50, asset)], + )?; + context.get_default_signer().broadcast(&ft1)?.wait()?; + + // Step 2 (transition, with a WRONG claimed prior state): spend the + // STATE_1-committing output, but claim WRONG_STATE_1 instead of STATE_1. + // `load`'s assert fails before `store` -- or broadcast -- ever comes into play. + let ft2 = construct_final_tx( + &context, + &state1_program, + &state1_script, + build_transition_witness(WRONG_STATE_1, STATE_2, 0), + Sequence::default(), + vec![PartialOutput::new(state2_script.clone(), 50, asset)], + )?; + let result2 = context + .get_default_signer() + .broadcast(&ft2) + .map(|receipt| receipt.to_string()) + .map_err(anyhow::Error::from); + + assert_error_msg(result2, Expect::AssertFailed) + } } From a3cda02966207741eba159934fa0dceb9aeef486 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 14 Aug 2026 14:46:32 -0700 Subject: [PATCH 4/5] Add two more storage failure test cases --- tests/storage_test.rs | 52 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/storage_test.rs b/tests/storage_test.rs index 4e5dd4b..deac5a5 100644 --- a/tests/storage_test.rs +++ b/tests/storage_test.rs @@ -1,6 +1,6 @@ mod common; -use simplex::simplicityhl::elements::Sequence; +use simplex::simplicityhl::elements::{Script, Sequence}; use simplex::transaction::PartialOutput; use common::core::{assert_error_msg, construct_final_tx, fund, run, run_with_outputs, Expect}; @@ -206,6 +206,56 @@ mod storage_tests { ) } + #[simplex::test] + fn store_wrong_taproot_leaf_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (7) The output *is* a genuine Taproot output -- just not a Simplicity one. It's a + // real BIP-341 P2TR output whose sole leaf is an ordinary Bitcoin Script (OP_TRUE), + // combined with the same NUMS internal key our own contracts use, at the standard + // Elements Tapscript leaf version (0xc4, not Bitcoin's 0xc0). This scriptPubkey was + // computed once, out of band, with a throwaway generator built on the same taproot + // APIs `Program` itself uses (`TaprootBuilder`, `finalize`, `Address::p2tr`), then + // hardcoded here -- so the test doesn't need to reconstruct real BIP-341 tweaking + // machinery just to exercise this one case. + // + // `jet::output_script_hash` hashes the raw scriptPubKey bytes, whatever they are. + // So the hash here is `Some(...)`, just the wrong one, and `store`'s `assert!` + // fails on the mismatch -- unlike `store_nonexistent_index_fails`, this never reaches + // the pruned-branch case. + + let alternate_leaf_script = Script::from(vec![ + 0x51, 0x20, 0xe4, 0xd6, 0x4b, 0x74, 0x54, 0x4b, 0xe8, 0x37, 0x4e, 0x44, 0x98, 0x19, + 0xb5, 0x1c, 0xde, 0xe2, 0x9f, 0xec, 0x36, 0x3a, 0x71, 0xa8, 0x5a, 0x42, 0xb0, 0x9d, + 0xbe, 0xb7, 0x92, 0xfc, 0x53, 0xf8, + ]); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 0), + vec![(alternate_leaf_script, 50)], + Expect::AssertFailed, + ) + } + + #[simplex::test] + fn store_non_taproot_output_fails(context: simplex::TestContext) -> anyhow::Result<()> { + // (8) The output isn't a Taproot output at all -- a plain P2WPKH (SegWit v0) + // scriptPubkey, `OP_0 <20-byte-hash>`. The scriptPubKey is still wrong for our + // store() purposes, as this is another way in which an output can be "not us". + let p2wpkh_script = Script::from(vec![ + 0x00, 0x14, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, + ]); + + run_with_outputs( + &context, + program(), + build_store_witness(STATE_A, 0), + vec![(p2wpkh_script, 50)], + Expect::AssertFailed, + ) + } + #[simplex::test] fn state_chain_store_load_store_load(context: simplex::TestContext) -> anyhow::Result<()> { // Exercises a genuine multi-transaction state chain: genesis (store only) -> From 9bc94e08f482d85f7fb7703e557b735d30d7b9fd Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 14 Aug 2026 17:33:50 -0700 Subject: [PATCH 5/5] Add CHANGELOG entry for state commitment functions --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a12e66..2494c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# Unreleased + +- Add `load` and `store` functions to assert state commitments for + covenants via Taproot leaves. This allows an instance of a smart + contract to remember state information across multiple transactions. + ## [0.0.1] The initial release with checked arithmetic operations for `u8`, `u16`, `u32`, `u64`, and `u128`;