You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In several places, most recently BlockstreamResearch/SimplicityHL#224, we've wanted to switch jets at runtime or at least deal with multiple programs whose jet types might differ or be unknown until runtime.
To make this work we need to stop using compile-time generics for the jet. This may mean a Box<dyn Jet> or maybe it means a concrete type. In either case, we need to deal with the transaction environment. Our existing trait has
/// Obtains a C FFI compatible environment for the jet.fnc_jet_env(env:&Self::Environment) -> &Self::CJetEnvironment;/// Obtain the FFI C pointer for the jet.fnc_jet_ptr(&self) -> &dynFn(&mutCFrameItem,CFrameItem,&Self::CJetEnvironment) -> bool;
Reflecting that depending on the type of jets used, the transaction environment should be a different struct (e.g. data for a Bitcoin vs Elements transaction). We need to invert the generics somehow so that Jet can be object-safe, and presumably we would have some sort of Environment object which was generic over the jet.
It may be that we want to use the ghostcell pattern that we use for types::Context (or maybe we even want to combine this with types::Context...though I don't think so because the type context is something that exists only at construction time, while the transaction environment exists for the whole lifecycle of a program. So probably we just want to do runtime checks.
trait JetEnvironment (
type Jet; // maybe we need a Jet trait bound?
type CJetEnvironment;
fn c_env(&self) -> &Self::CJetEnvironment;
// do we actually need a `dyn Fn`? I think a `fn` would suffice.)
fn c_jet_ptr(&self, jet: &Self::Jet) -> &dyn Fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool;
)
// The Jet trait now has no knowledge of environments
pub trait Jet:
Copy + Eq + Ord + Hash + std::fmt::Debug + std::fmt::Display + std::str::FromStr + 'static
{
/// Return the CMR of the jet.
fn cmr(&self) -> Cmr;
/// Return the source type of the jet.
fn source_ty(&self) -> TypeName;
/// Return the target type of the jet.
fn target_ty(&self) -> TypeName;
/// Encode the jet to bits.
fn encode<W: Write>(&self, w: &mut BitWriter<W>) -> std::io::Result<usize>;
/// Decode a jet from bits.
fn decode<I: Iterator<Item = u8>>(bits: &mut BitIter<I>) -> Result<Self, decode::Error>;
/// Return the cost of the jet.
fn cost(&self) -> Cost;
This actually doesn't get us all the way to object-safety because we still have the decode method which returns Self. It may be that we just want to move the decode method off of Jet and into JetEnvironment. (And then we could put a JetEnvironment into types::Context, and make this be responsible for constructing jets, which would go some way toward preventing users from mixing different jet types during construction.)
Some Jet supertraits are not object-safe, so I was forced to came up with a workaround. Also encode needs to be constrained to a specific type, so I thought to use &mut BitWriter<&mut Vec> (Don't know if this will be good ideas, as this also raices a lot of work to do).
With this, I started updating the affected areas and later attempted to change the Node types. After a few hours, I noticed that the changes had already grown to several thousand lines. Even my generic hell stopped to look that bad, and there were still more changes to come, since everything depends on J: Jet, and we also changed decoding logic.
I was thinking about whether this could be implemented in several small PRs. Perhaps we could gradually move functionality from the Jet trait into JetEnvironment. Doing everything in a single step forces us to update a large amount of related code. Another think we maybe can create a separate version and switch to it at some point.
I was also considering when types::Context should come into play. Would it be better to pass it directly into the decode function, or should it instead be exposed via a separate ctx() method on JetEnvironment?
pubtraitJetEnvironment{typeJet:Jet;typeCJetEnvironment;/// Obtains a C FFI compatible environment for the jet.fnc_jet_env(&self) -> &Self::CJetEnvironment;/// Decode a jet from bits.fndecode_jet<I:Iterator<Item = u8>>(ctx: types::Context<'_>,bits:&mutBitIter<I>,) -> Result<Box<dynJet>, decode::Error>;/// Obtain the FFI C pointer for the jet.fnc_jet_ptr(jet:&Self::Jet,) -> fn(&mutCFrameItem,CFrameItem,&Self::CJetEnvironment) -> bool;}/// Helper trait to enable `Clone`, `Hash`, `Eq`, and `Ord` for `Box<dyn Jet>`.traitDynJet{fndyn_clone(&self) -> Box<dynJet>;fndyn_hash(&self,state:&mutdynHasher);fndyn_eq(&self,other:&dynJet) -> bool;fndyn_cmp(&self,other:&dynJet) -> std::cmp::Ordering;}impl<T:Clone + Hash + Jet>DynJetforT{fndyn_clone(&self) -> Box<dynJet>{Box::new(self.clone())}fndyn_hash(&self,mutstate:&mutdynHasher){self.hash(&mut state)}fndyn_eq(&self,other:&dynJet) -> bool{self.cmr() == other.cmr()}fndyn_cmp(&self,other:&dynJet) -> std::cmp::Ordering{self.cmr().cmp(&other.cmr())}}implCloneforBox<dynJet>{fnclone(&self) -> Self{(**self).dyn_clone()}}implHashforBox<dynJet>{fnhash<H:Hasher>(&self,state:&mutH){(**self).dyn_hash(state)}}implPartialEqforBox<dynJet>{fneq(&self,other:&Self) -> bool{(**self).dyn_eq(&**other)}}implEqforBox<dynJet>{}implPartialOrdforBox<dynJet>{fnpartial_cmp(&self,other:&Self) -> Option<std::cmp::Ordering>{Some(self.cmp(other))}}implOrdforBox<dynJet>{fncmp(&self,other:&Self) -> std::cmp::Ordering{(**self).dyn_cmp(&**other)}}/// Family of jets that share an encoding scheme and execution environment.////// Jets are single nodes that read an input,/// process it internally using foreign C code _(black box)_,/// and produce an output./// Jets may read values from their _environment_.////// Jets are **always** leaves in a Simplicity DAG.pubtraitJet:DynJet + std::fmt::Display + std::fmt::Debug + 'static{/// Return the CMR of the jet.fncmr(&self) -> Cmr;/// Return the source type of the jet.fnsource_ty(&self) -> TypeName;/// Return the target type of the jet.fntarget_ty(&self) -> TypeName;/// Encode the jet to bits.fnencode(&self,w:&mutBitWriter<&mutVec<u8>>) -> std::io::Result<usize>;/// Return the cost of the jet.fncost(&self) -> Cost;}
I was thinking about whether this could be implemented in several small PRs. Perhaps we could gradually move functionality from the Jet trait into JetEnvironment.
Yeah. So, we know that we've gotta move the c_jet_env and c_jet_ptr methods, so we could move those one by one in separate PRs, which shouldn't be too noisy since they're not super common methods. Creating and using the JetEnvironment trait might also be its own PR, even if it's initially an empty trait. (Unsure.)
As you observe, some supertraits are not object-safe (I see FromStr but probably there are others) so we would also need to e.g. replace FromStr with a dedicated parse method or something. This can also be its own PR.
I was also considering when types::Context should come into play.
Let's try to do the "easy" stuff like the above, and then once we've gotten the trait to be object-safe, we can re-assess how to use it.
With this, I started updating the affected areas and later attempted to change the Node types. After a few hours, I noticed that the changes had already grown to several thousand lines
So, unfortunately changing Node is going to be a hell of a change. I hope we can remove the generic and replace Jet(J) with Jet(Box<dyn Jet>) and make no other changes in a single PR, since that PR will have a huge diff but hopefully be mechanical and simple. Then once the dust clears from having removed the generic, we can try to use the jet type "properly" by pulling types::Context in and doing compatibility checks and so on.
Yeah. So, we know that we've gotta move the c_jet_env and c_jet_ptr methods, so we could move those one by one in separate PRs, which shouldn't be too noisy since they're not super common methods. Creating and using the JetEnvironment trait might also be its own PR, even if it's initially an empty trait. (Unsure.)
@apoelstra I was thinking of opening a followup PR to #351 before the next rust-simplicity release (if it is not in the near future), so we don't have to wait for the autogenerated files to be updated to continue iteration.
In this PR, I would temporarily update the autogenerated files with the latest changes from the simplicity repo, and move decoding into the JetEnvironment, adding logic to parse strings into jets (perhaps I will curse it as "jet codec update toward object safety"), along with a corresponding PR to update autogeneration the simplicity repo.
Then, similar to #351, after a review, I would revert the autogenerated files and temporary switch to using the existing logic.
@ivanlele great idea. Can you mark the PR as draft and @-mention me? I will try to get these done quickly because I agree, the cross-repo iteration is horribly slow and we want to get done with it quickly.
Activity
apoelstra commentedon Mar 3, 2026
A straightforward inversion would be:
This actually doesn't get us all the way to object-safety because we still have the
decodemethod which returnsSelf. It may be that we just want to move thedecodemethod off ofJetand intoJetEnvironment. (And then we could put aJetEnvironmentintotypes::Context, and make this be responsible for constructing jets, which would go some way toward preventing users from mixing different jet types during construction.)ivanlele commentedon Mar 4, 2026
I was playing with this today to see in what form it might work. Rust’s rules about what can be placed inside
Box<dyn T>are quite strict (https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility).Some Jet supertraits are not object-safe, so I was forced to came up with a workaround. Also
encodeneeds to be constrained to a specific type, so I thought to use &mut BitWriter<&mut Vec> (Don't know if this will be good ideas, as this also raices a lot of work to do).With this, I started updating the affected areas and later attempted to change the Node types. After a few hours, I noticed that the changes had already grown to several thousand lines. Even my generic hell stopped to look that bad, and there were still more changes to come, since everything depends on J: Jet, and we also changed decoding logic.
I was thinking about whether this could be implemented in several small PRs. Perhaps we could gradually move functionality from the Jet trait into JetEnvironment. Doing everything in a single step forces us to update a large amount of related code. Another think we maybe can create a separate version and switch to it at some point.
I was also considering when
types::Contextshould come into play. Would it be better to pass it directly into the decode function, or should it instead be exposed via a separatectx()method onJetEnvironment?apoelstra commentedon Mar 6, 2026
Yeah. So, we know that we've gotta move the
c_jet_envandc_jet_ptrmethods, so we could move those one by one in separate PRs, which shouldn't be too noisy since they're not super common methods. Creating and using theJetEnvironmenttrait might also be its own PR, even if it's initially an empty trait. (Unsure.)As you observe, some supertraits are not object-safe (I see
FromStrbut probably there are others) so we would also need to e.g. replaceFromStrwith a dedicatedparsemethod or something. This can also be its own PR.Let's try to do the "easy" stuff like the above, and then once we've gotten the trait to be object-safe, we can re-assess how to use it.
So, unfortunately changing
Nodeis going to be a hell of a change. I hope we can remove the generic and replaceJet(J)withJet(Box<dyn Jet>)and make no other changes in a single PR, since that PR will have a huge diff but hopefully be mechanical and simple. Then once the dust clears from having removed the generic, we can try to use the jet type "properly" by pullingtypes::Contextin and doing compatibility checks and so on.ivanlele commentedon Mar 6, 2026
I made #351 which should cover all these.
Merge #351: Separate Jet env from Jet trait
ivanlele commentedon Apr 6, 2026
@apoelstra I was thinking of opening a followup PR to #351 before the next rust-simplicity release (if it is not in the near future), so we don't have to wait for the autogenerated files to be updated to continue iteration.
In this PR, I would temporarily update the autogenerated files with the latest changes from the simplicity repo, and move decoding into the JetEnvironment, adding logic to parse strings into jets (perhaps I will curse it as "jet codec update toward object safety"), along with a corresponding PR to update autogeneration the simplicity repo.
Then, similar to #351, after a review, I would revert the autogenerated files and temporary switch to using the existing logic.
apoelstra commentedon Apr 8, 2026
@ivanlele great idea. Can you mark the PR as draft and @-mention me? I will try to get these done quickly because I agree, the cross-repo iteration is horribly slow and we want to get done with it quickly.
Merge #358: Move the Jet codec and parser towards the object-safaty
Merge BlockstreamResearch#360: Migrate execution to dynamic dispatch