Skip to content

Add state commitment functions for persistent storage via Taproot - #46

Open
schoen wants to merge 5 commits into
BlockstreamResearch:masterfrom
schoen:feat/storage
Open

Add state commitment functions for persistent storage via Taproot#46
schoen wants to merge 5 commits into
BlockstreamResearch:masterfrom
schoen:feat/storage

Conversation

@schoen

@schoen schoen commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Add storage.simf with state commitment functions load and store.

This is based on @apoelstra's stage management MVP functions, originally written around December 2025, with minor name changes and comment edits by me. Versions of these functions have been used in many covenants over the last several months. I'd like them to be in the standard library so that everyone can use them easily without copying and pasting them as "folklore" about how to do basic Simplicity state management.

There are 13 Simplex test cases created by Claude. I asked Claude for each individual test case, and I'm satisfied that they test the most important cases and that they work properly. We test that the storage does work when it should, and that loading or storing the wrong state (or storing to a Taproot or non-Taproot output other than a copy of the current program) fails. The other good thing is that we also test interoperability between state commitments generated by rust-elements calls and state commitments generated by the store function's jets calls (which, as Claude pointed out to me, are two different implementations of portions of the relevant logic). This confirms that we can assert commitments from outside of a contract (like with hal-simplicity or LWK or a wallet) and verify them from inside of the contract. We already know this because we already do this in some actual contracts, but it's also directly tested here.

As in #45, Claude wrote comments about what the various tests and helper functions do, which could be unnecessarily verbose, and which I'm happy to trim down if that would be preferable.

There may be a further discussion about whether we can usefully extend state commitments to let cooperating covenants load or store one another's state values, but this should wait for the covenant convention stuff (to include documented recommendations for how to accomplish this).

Comment thread simf/lib/storage.simf
Comment on lines +1 to +18
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)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to break this function down into two types of functions:

  1. Functions that calculate the leaf for different types
  2. A function that takes an internal_key and a tap_node and calculates the resulting script_hash

The thing is, the Taproot Storage structure can vary across different contracts, so std needs to provide universal functions

Comment thread simf/lib/storage.simf
Comment on lines +20 to +53
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))
));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are also very useful, but we can only provide a universal interface for them if the user passes the pre-calculated tap_node and internal_key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I certainly see the problem that these functions are not as general as they could be and I think the instinct to generalize them more is a very good one. I was talking to an AI last week about this, and it pointed out the related issue that, once we have a way to recognize a specific other contract, we may want to have covenants that have an "exit through other covenant" (e.g. you can withdraw from the existing covenant into a specified new covenant with a derived state). In a sense @apoelstra is currently actively working on the "way to recognize a specific other contract" for these purposes, and the mechanisms for multiple covenants to deliberately cooperate on cospending and recognizing each other.

I also think it's a good catch that the internal key hard-coding is questionable (as another example of the generality issue).

I'm a little torn about this: I really regret not having a straightforward and officially documented way for people to do covenant enforcement right now. I can see that this way is possibly making too many assumptions and is not general enough, but I would love to figure out how to get covenant-enforcement mechanisms into the standard library early on so that people can make use of them, since covenants are such an important use case for Simplicity!

The greatest level of generality for these functions would presumably also not assume that the destination contract has the same identity as the current contract (but allow that to be specified). The possible disadvantages of this generality are (1) @apoelstra hasn't finished specifying some of the tools that we might need for that, and (2) understanding how to use it might be more intimidating for developers, as instead of an interface like

store(state: u256, index: u32)

we might get something more like

store(tap_node: u256, version: u32, internal_key: u256, covenant_id: u256, some_other_thing: u256, index: u32, state: u256)

This is great in a way. because it has more flexibility and more use cases, but also potentially far more challenging for people to understand. Perhaps we should implement that more general mechanism, and then also wrap it with a basic_covenant_store() and basic_covenant_load() or something that fill in the normal defaults for "a recursive covenant with the default NUMS key on this specific blockchain", etc.?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that there are a lot of things to consider in this matter, which will need to be resolved in the future or are already being resolved.

Therefore, std will continue to change many more times, depending on the language's capabilities and the needs of developers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants