-
Notifications
You must be signed in to change notification settings - Fork 4
Refactoring of getters/setters/offset methods of types #197
Conversation
The change is definitely positive. I discussed several times about converting from enum into concrete structs, but never really had the chance. Consider also applying the same strategy for inputs/outputs (ie transform the variants into concrete structs and have a single enum I would refrain however from creating a trait for |
Would it be better to just impl the methods on the internal structs instead of using a trait? The issue with using an enum is that it's always a runtime value and though the above approach does make things cleaner it also just moves the potential panic inside the trait impl. |
I meant not traits only for We need traits to be able to write a generic code. I agree that it will increase the complexity of the definition. As an alternative, we can create traits only for common fields. Something like: pub trait CreateAndScriptFields {
fn gas_price(&self) -> &Word;
fn gas_price_mut(&self) -> &mut Word;
fn gas_price_offset(&self) -> usize;
fn gas_limit(&self) -> &Word;
fn gas_limit_mut(&self) -> &mut Word;
fn gas_limit_offset(&self) -> usize;
fn maturity(&self) -> &Word;
fn maturity_mut(&self) -> &mutWord;
fn maturity_offset(&self) -> usize;
fn inputs(&self) -> &Vec<Input>;
fn inputs_mut(&self) -> &mut Vec<Input>;
fn inputs_offset(&self) -> usize;
fn outputs(&self) -> &Vec<Output>;
fn outputs_mut(&self) -> &mut Vec<Output>;
fn outputs_offset(&self) -> usize;
fn witnesses(&self) -> &Vec<Witness>;
fn witnesses_mut(&self) -> &mut Vec<Witness>;
fn witnesses_offset(&self) -> usize;
} But it means that Moving each field to a separate trait allows us to provide only implementation, and how to use it should be defined by the crate that uses it. |
I'm proposing to implement those methods directly only for the inner structs(the same), not for
No, we don't have any panics inside because we work directly with the type that implements fields' traits. |
After a chat with @xgreenx I get the reason this is needed and think it's probably the best idea.
Macros really break the IDE's ability to do things like go to definition. I'd personally be in favor of avoiding macros and just implementing manually. |
My 2 cents: Breaking down the enum into constituent structs is nice! We separate the definition of It sounds like the idea of implementing each member function as a trait is so that we can compose the polymorphic type to functions that are designed to operate solely on interfaces, rather than types. I am a big fan of programming against interfaces, rather than types! However, my questions would be:
I would also like to avoid the use of macros where possible. As already mentioned by others in this PR, macros do not play nicely with the IDEs or |
Yep!=)
It will save us and crates that depend on In the It adds more flexibility overall.
We can create a trait like pub trait Internal {
fn validate_without_signature(&self) -> Result<(), ValidationError>;
}
impl<T: InputsField + OutputField + ... > Internal for T {
fn validate_without_signature(&self) -> Result<(), ValidationError> {
...
}
} In this case,
Could you describe what problems you have in detail, please? Yea, it doesn't navigate to the line of method definition in the macro, only to the place of the macro use, but it is already enough to get the implementation because logic will be simple=) Three methods with simple implementation. I can implement it manually for each field, but in this case, I need to spend 10-12 lines instead of 1. |
I see the advantage of having a high-level However, I agree with @vlopes11 and @bvrooman that having a trait for each field seems a little overkill. Wouldn't a function like |
It is an example of fields composition which would be nice to have: // The trait for `CoinPredicate` and `CoinSigned`
// `fuel-vm` can have a method in `ExecutableTransaction` trait like:
// `fn coin_at(index: usize) -> Option<&dyn CoinFields>;`
// Useful here: https://github.com/FuelLabs/fuel-vm/blob/master/src/interpreter/metadata.rs#L111
pub trait CoinFields {
fn utxo_id(self) -> &UtxoId;
fn utxo_id_mut(self) -> &mut UtxoId;
fn utxo_id_offset(self) -> usize;
fn owner(self) -> &Address;
fn owner_mut(self) -> &mut Address;
fn owner_offset(self) -> usize;
fn amount(self) -> &Word;
fn amount_mut(self) -> &mut Word;
fn amount_offset(self) -> usize;
fn asset_id(self) -> &AssetId;
fn asset_id_mut(self) -> &mut AssetId;
fn asset_id_offset(self) -> usize;
fn tx_pointer(self) -> &TxPointer;
fn tx_pointer_mut(self) -> &mut TxPointer;
fn tx_pointer_offset(self) -> usize;
fn maturity(self) -> &Word;
fn maturity_mut(self) -> &mut Word;
fn maturity_offset(self) -> usize;
}
// The trait for `MessagePredicate` and `MessageSigned`
// `fuel-vm` can have a method in `ExecutableTransaction` trait like:
// `fn message_at(index: usize) -> Option<&dyn MessageFields>;`
// Useful here: https://github.com/FuelLabs/fuel-vm/blob/master/src/interpreter/metadata.rs#L260
pub trait MessageFields {
fn message_id(self) -> &MessageId;
fn message_id_mut(self) -> &mut MessageId;
fn message_id_offset(self) -> usize;
fn sender(self) -> &Address;
fn sender_mut(self) -> &mut Address;
fn sender_offset(self) -> usize;
fn recipient(self) -> &Address;
fn recipient_mut(self) -> &mut Address;
fn recipient_offset(self) -> usize;
fn amount(self) -> &Word;
fn amount_mut(self) -> &mut Word;
fn amount_offset(self) -> usize;
fn nonce(self) -> &Word;
fn nonce_mut(self) -> &mut Word;
fn nonce_offset(self) -> usize;
fn data(self) -> &Vec<u8>;
fn data_mut(self) -> &mut Vec<u8>;
fn data_offset(self) -> usize;
}
// The trait for `MessagePredicate` and `CoinPredicate`
// `fuel-vm` can have a method in `ExecutableTransaction` trait like:
// `fn predicate_at(index: usize) -> Option<&dyn PredicateFields>;`
pub trait PredicateFields {
fn predicate(self) -> &Vec<u8>;
fn predicate_mut(self) -> &mut Vec<u8>;
fn predicate_offset(self) -> usize;
fn predicate_data(self) -> &Vec<u8>;
fn predicate_data_mut(self) -> &mut Vec<u8>;
fn predicate_data_offset(self) -> usize;
}
// The trait for `MessagePredicate`, `CoinPredicate`, `MessageSigned` and `CoinSigned`.
// This trait can be useful to calculate the sum of spendable inputs.
pub trait AmountField {
fn amount(self) -> &Word;
fn amount_mut(self) -> &mut Word;
fn amount_offset(self) -> usize;
} The idea with separate traits is that the user of We can avoid duplication with super traits We can do a one-time overkill change with a separate trait for each field and minimize future changes in this direction right now, or we can try to solve problems on demand and, for now, have traits from the example above=) |
…nd `Create`. Moved all getters from `Transaction` to `Create` and `Script`. Splitted the previous logic of the transaction into traits and implemented traits for `Create` and `Script`. Adapted tests to use generic code or code with exactly expected types. Prepared the code to easily integrate a new `Mint` transaction. TODO: Add support of metadata
b0e812e
to
dcbe301
Compare
use core::hash::Hash; | ||
|
||
// TODO https://github.com/FuelLabs/fuel-tx/issues/148 | ||
pub(crate) fn next_duplicate<U>(iter: impl Iterator<Item = U>) -> Option<U> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to validation.rs
because it is used there.
} | ||
|
||
#[cfg(feature = "internals")] | ||
impl Transaction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of those functions are used in the builder.rs
, so I moved them there.
use fuel_types::bytes::{self, SizedBytes}; | ||
use fuel_types::Bytes32; | ||
|
||
impl Transaction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All offsets moved to Create
and Script
accordingly. But those offsets are relative to Create
and Script
instead of Transaction
.
To get the relative offset to the Transaction
, you need to add Transaction::offset
to this offset.
For example, Create::gas_price_offset
right now is 0
, when previously Transaction::gas_price_offset
was 8
because 8 bytes were reserved by default for TransactionRepr
. So right now Transaction::gas_price_offset
= Create::gas_price_offset
+ Transaction::offset
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to remove this change, and now all offset and serialization include 8 bytes for the Transaction
enum discriminant.
So Script
and Create
are serialized as before the change. Transaction
is only a wrapper that calls methods of inner type.
Maybe it is unclear why Script
and Create
have 8 bytes in the canonical serialization/deserialization, but it is simpler for the user and doesn't need to remember that offsets are relative. Also serialization_size
and metered_bytes
are correct=)
During refactoring, I noticed that we don't charge the user for witnesses. The maximum number of witnesses is 255. Can it cause a problem? The user can always fill in 255 witnesses, 254 are random, and 1 is valid. Do we handle this case somehow in the |
We will need to setup our own rules to protect against witness data abuse since the specs don't prescribe any solution here. |
Re-worked `CheckedTransaction`. Introduced a new `Checked` wrapper that allows to wrap any data in generic way. Added covertion to "checked" enum for `Transaction`
Overall this change looks good to me. It's a little hard to tell what has changed vs what has just been moved. @xgreenx Is there anything specific that has changed in behavior or is it purely a refactor? |
Fixed bug with size of the `Transaciton`
It is purely refactoring, and most of the code is moved from one place to another. And with this adaptation, I potentially could introduce bugs=) But I hope our tests are good enough to catch all possible bugs/not expected behaviors |
Implemented `Default` for `Script`. Made checked metadata public. Made `Stage` also public to allow writing a generic code. Added input argument to `ConsensusParameters::tx_offset` to highlight breaking work with offsets.
The refactor looks good. I like the introduction of the separate I'm wondering about the introduction of the
My understanding is that calling the Tx's first I have a couple of questions:
|
Const generic enums is not allowed=( Playground We can use something like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no more questions about the code at this time. I think that we can get the changes in and start to observe how these patterns play out in practice.
…te a `Checked` transaction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, thanks for the quick feedback turnaround!
The organizational changes make sense to me and have also cleaned up some duplicate processing as well, nice work!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
Problem
In the
fuel-tx
andfuel-vm
we have cases where we do the same logic for different variants ofTransaction
because the fields are the same and processing rules are the same. We have many places requiring a different set of common fields.During work on #193, I faced a problem with getters and setters of
Transaction
.Previously, almost all fields of
Transaction::Script
were available in theTransaction::Create
. It is whyTransaction
had getters/setters/offsets for all fields. It was simpler to write the code liketransaction.inputs()
ortransaction.inputs_offset()
and write common logic . Only several fields likescript
andbytecode
returned theOption
type because if the type isTransaction::Script
, it doesn't contain thebytecode
field and vice versa, so we need to write specific logic for those fields.With the introduction of
Transaction::Mint
, almost all fields exceptoutputs
are optional.fuel-vm
problem example#196 is implementing the change to return
Result
(but it may be better to returnOption
) for each field in theTransaction
. It already added someunwrap
complexity into tests, but I tried to minimize it.After, I started applying this change to
fuel-vm
, creating a lot ofunwrap
there.The code doesn't look good=)
validate_without_signature_internal
problem exampleWe can't write a generic code if methods are implemented only for
Transaction
or inner structures without traits. As an examplevalidate_without_signature_internal
method. The function checks that all common fields ofCreate
andScript
are valid(according to the rules in the spec). We must pass each field as an argument if we can't write a generic code.The signature of the
validate_without_signature_internal
:And is how to use this function with the current code:
But if methods are implemented for the inner struct, we will have the same problem. So it is better to move the implementation of getters/setters/offsets into traits.
Proposed solution
Move the anonymous enum variants into typed structures.
Introduce new traits for each field, something like:
Implement those traits for each new struct
Create
,Script
,Mint
.Because we have the same problem with the
Input
andOutput
types, we can apply the same strategy as forTransaction
to them.Pros
Separate types allow as avoid getters/setters/offsets with
Option
/Result
return type and cast to exact type.Traits allow writing a generic code or code that returns a
dyn
object. And in the code, you can have genericsT: ExecutableTransaction
or castTransaction
into&dyn ExecutableTransaction
and work with fields withoutOption
.Separate trait for each field allows composing traits you need based on your logic. For example, you can create your trait with super traits like:
The example with
validate_without_signature_internal
will look like:The same can be applied for
Input
type, and we can have a traitCoinFields
andMessageFields
and have one functionto_coin(&self) -> Option<&dyn CointFields>
.Also, it can simplify tests because
TransactionBuilder
can beTransactionBuilder<T>
whereT
may beScript
,Create
, orMint
. So the builder will return a concrete type than theTransaction
enum.Cons
macro_rule
but it still is a lot of code).