-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More and Better Resource Validation #415
Comments
Hey @devmannic, thank you for starting this Github issue and for continuing the discussion on
So, in your proposal, would somebody who does not know the resource address of a I can see both sides of the argument in this case, on one hand, a The one example that comes to mind where not being able to access the proof data becomes a problem is the following: say that you have some kind of dApp which allows people to register. The dApp has two methods for registration: /// Registers a user and returns to them a user badge and their change
pub fn register(&mut self, payment: Bucket) -> (Bucket, Bucket);
/// Allows the user to register using a badge that they own (to minimize transaction fees)
pub fn register_with_owned_badge(&mut self, payment: Bucket, badge: Proof) -> Bucket; In the above code example, the pub fn register_with_owned_badge(&mut self, payment: Bucket, badge: Proof, badge_resource_address: ResourceAddress) -> Bucket; But the above feels weird to me for two reasons:
To be clear, I am not against there not being any code paths to get the content of a proof prior to validation, it is just that this is a big change and I'm trying to see if there are any use-cases that break if this change is made, and if they do, how can we support them or enable them.
I have thought about this a little bit more and I quite like it, perhaps the proof validation could be expressed through something like: enum ProofValidationCriteria {
/// Validates that the passed proof is of the given resource address
ResourceAddress(ResourceAddress),
/// Validates that the passed proof is of any of the given resource addresses
AnyOfResourceAddress(BTreeSet<ResourceAddress>),
/// Validates that the proof contains the specified amount
Amount(ResourceAddress, Decimal),
/// Validates that the proof contains at least the specified amount
AtLeast(ResourceAddress, Decimal),
/// Validates that the proof contains at most the specified amount
AtMost(ResourceAddress, Decimal),
/// Validates that the proof contains a Non Fungible with a given Id
NonFungible(ResourceAddress, NonFungibleId),
/// Validates that the proof contains a Non Fungible in a given set of Ids
NonFungible(ResourceAddress, BTreeSet<ResourceAddress>),
/// Validates that the proof contains a Non Fungible in a given set of Ids
AnyOfNonFungibles(BTreeSet<NonFungibleAddress>),
}
impl From<ResourceAddress> for ProofValidationCriteria {
...
}
// Other useful From traits go here too
impl Proof {
pub fn validate_proof(self, validation_criteria: ProofValidationCriteria) -> Result<ValidatedProof>
} What do you think? Another enum variant which could be added is
I can't immediately comment on that and will have to check out your implementation of it first.
Can you please elaborate a little bit more on what was missed?
The only problem I have with this is that there is no way to enforce, at compile-time, that a rule can be realistically fulfilled. As an example, I will use the code that I provided as an example: pub fn func5(self, proof: ProofToValidate, bucket: Bucket) {
let requirement = ResourceRules::new(rule!(require(admin_badge) && require(super_admin_badge) ));
let proof: ValidatedProof = proof.validate_proof_against(requirement).unwrap();
if bucket.check_rules(requirement) {
// ...
}
} The above code is syntactically correct, but logically incorrect as a If we wanted to do this, we could most likely make the |
Well @0xOmarA I just realized I totally misread a big part of your PR. The inline diff did not make it clear to me but I see now while I'm comparing the entire new file that you have in fact removed a bunch of methods from So, I'm mostly ok with doing it the way you've done it but it changes a lot of what I've been saying related to the needs for ProofToValidate and it does bring about your exact questions related to "what if the dev can't validate it"). Also, it makes your changes massively not backwards compatible for everybody. If the mindset is now that a So, I think I still like my design better where
EDIT: ironically, I think your changes already are actually a much bigger change than I was proposing or envisioning as it requires everyone to move to I think you're making this out to be a bigger change then it really is, and maybe our assumptions are not lined up. Your questions seem to imply that the change I'm suggestion somehow globally replaces EDIT: So again, I think we've basically arrived at the same place with this two different approaches, but mine is backwards compatible, and does not require "unsafe" trapdoor methods, and provides a separation for low-level (or even in RE) and high-level interactions with Proofs.
EDIT: again, read this as if I like the
OH... and this is where I realize inline diffs sucks. It looked lik you hadn't added things like "contains" to
Yes, that's exactly what i meant. Along the same lines of |
Continuing the discussion started in #406
@0xOmarA There are a few different related ideas. I'll try to restate much of what I said in the other PR but maybe a little more organized here with some small examples to make it more clear.
Thanks for keeping the discussion going. I'm willing to contribute some code on this to get any parts of this over the finish line for v0.5 if needed.
ProofToValidate
(aka
UnValidatedProof
, but I like the new name better)Start with example code showing how it would be used
The goal here is for developers to have a way to indicate in the function signature (or shortly after) that they intent to validate the Proof, and the implementation guarantees there will be no code paths where the proof contents (address, amount, ids, data, etc) can be accessed prior to the validation.
Then
ProofToValidate
can be extended with more complex validation functionality in addition to checking just the resource. And this is separate from the "low level"Proof
.NOTE It's worth saying that I think after having
ProofToValidate
you might consider completely removingValidatedProof
and simply haveProofToValidate
transition back to aProof
. Why keep around the validated wrapper type if you know validation must have happened upfront and is complete? Makes for lots less code to maintain or one less type for devs to understand. The biggest downside to this removal is it makes implementing Drop much harder. Se the next section.impl Drop for ValidatedProof and also maybe Deref
Since
ValidatedProof
is a simple wrapper it's a good time to implement the Drop trait so developers can stop worrying about manually calling.drop()
and all the weird issues that crop up in complex code. Rust can manage all this, we should let it. I already have an implementation that would work with almost no changes in myProofOf
type inscrypto_statictypes
. Do note that it also makes use of theDeref
trait to forward method calls to the wrapped Proof instead of reimplementing all thesfunctions!
stuff manually. I like my approach there better since it will always be in sync with Proof without future code changes and keeping things in sync. But since fees are coming I don't know exactly what the full tradeoff is. As it is it looks like @0xOmarA may have missed some functions in PR #406 that are part of Proof, but now not exposed byValidatedProof
.You can see my existing implementation here: https://github.com/devmannic/scrypto_statictypes/blob/89472d7e2f6b5dc585981c8b78739a6dfcb22430/src/proofof.rs#L97
It requires the wrapper to have a
RefCell<Option<Proof>>
instead of just aProof
and thus a check in every method call as a result. There is a performance (and thus a fee) hit for this type of implementation but I can't quantify it yet.More Validation
One approach I could see is a
ResourceValidation
trait (or family of traits, probably with generics) created which can be implemented for ProofToValidate and maybe there's also a BucketToValidate that works similarly, but also implement these for Proof and for Bucket (and for Vault), all so that devs can more declaratively indicate requirements (aka constraints) and check them correctly. These would provide consistancy with all the same methods and probably with very similar or even common implementation with macros or generics. Having devs continuing to implementing comparisons againstamount()
or specific tests against addresses, or nonfungible ids, really just seems like grunt work. They should be able to declare their constraints and be assured that they are checked properly as much as possible.What that trait's methods look like is still worth discussing but I really liked my HareSwap
BucketRequirement
implementation here:https://github.com/devmannic/scrypto-challenges/blob/main/1-exchanges/hareswap/src/requirement.rsIt needs to be updated for scrypto > 0.3 but it's mostly just renaming. The idea being that a dev would instantiate a
ResourceRequirement
and then pass that instance intoProofToValidate::validate_proof
.To that end we would then make
validate_proof
part of these traits (maybe a more specific name instead).Then for more complex validation, you simply add more to the trait, and implement it. If the fees need to be subsidized or for some reason it's better to do the implementation on the RE side, then do it there, otherwise do it on the "user" side. Our discussion about the iterator gets to this point. You can add a method which takes an iterator. It's a nice approach, but it's also less efficient to check since it require lots of wasm boundary crossings since the iterator is by design lazy evaluated. Instead (or in addition) providing a method which takes a set of
ResourceAddress
es probably the way to go. Then that can be a single RE call and have whatever fee schedule you want.Example
AccessRule-style validation
Taking this further or maybe in a slightly different direction, another option: I think having some parallels to the AccessRule's design would be really really nice, but that might be harder to develop, I"m not sure. But in return you get a single common interface to talk about the contents of Proofs (and Buckets, and Vaults) for checking access, or for checking for business logic which I just think makes a lot of sense. Less for devs to learn, more consistency so easier to build and audit, etc.
This might look something like:
no more "unsafe"
I'll say it again. I don't like
unsafe_skip_proof_validation
.It serves almost no purpose, even now before more advanced validation is implemented. If a dev wants to or needs to do their own validation anyway, then they get no value from using a
ValidatedProof
. It doesn't have any methods aProof
doesn't have. It really only adds semantic confusion. The "unsafe" method literally only exists to potentially signpost that a complex check might be done, and make it easier to not validate things. Wouldn't it be better to just let the complex code exist, and have the lack of aValidatedProof
actually be a better indication the code is not safe because you cannot actually ensure it's been validated?In any case, even if we disagree here, the better answer is to provide more advanced validation options for devs and for sure after that remove the "unsafe" method as it really wont be needed.
Love to hear any thoughts anyone has on these ideas. Thanks.
The text was updated successfully, but these errors were encountered: