-
Notifications
You must be signed in to change notification settings - Fork 11.3k
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
[sui-verifier] Added support for characteristic types #3563
Conversation
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.
Overall looks great!
Mostly just layout/naming nits :) (sorry)
We should have one other test where you do something like
module a::m {
struct N has drop {}
}
module a::n {
fun init(_witness: N, ctx: &mut TxContext) { ... }
}
Where you are using the upper case, but from a different module
// Copyright (c) 2022, Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
//! A module can define a "characteristic type", that is a type that is instantiated only once, and |
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.
Bit of a naming nit, I think we are going with "one-time-witness type" for anything user facing. As it sort of describes what it is supposed to be used for.
So a general s/characteristic/one-time-witness/
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.
With or without the hyphens, doesn't matter too much
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.
And open to discussion here :) I just think calling this thing a one-time witness everywhere might help people understand what is going on and why they want to use it (assuming they understand witnesses)
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.
To me a characteristic type was a way to implement one-time witness. For now I have updated the comment to reflect that but I also open for a discussion :-)
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 would agree with that description, but I think it is the only way to implement a one-time witness. At least the only safe way.
And I've been pushing more for naming in the Sui Framework for things to be named what they are, not how they work. Really, the name should reflect the most important and distinguishing part of the thing.
That being said, I could see one-time witness being confusing as there are lots of ways to implement one-time witness, though all those other ways would be not as safe.
Thoughts given this perspective?
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.
As you say (and as the comment now say), there are different ways to implement one-time-witness so I think I'd stick to what we have now in the verifier but use one-time-witness in the native function name. We can always change it later if people find this confusing.
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.
Fine with landing the naming as is. I will be curious what @sblackshear thinks, as he came up with the initial name
let mut char_type_candidate = None; | ||
for def in struct_defs { | ||
let struct_handle = module.struct_handle_at(def.struct_handle); | ||
let struct_name = view.identifier_at(struct_handle.name).as_str(); | ||
if struct_name.to_ascii_uppercase() == struct_name | ||
&& mod_name.to_ascii_uppercase() == struct_name | ||
{ | ||
verify_char_type(module, struct_name, struct_handle, def) | ||
.map_err(verification_failure)?; | ||
// if we reached this point, it means we have a legitimate characteristic type candidate | ||
// and we have to make sure that both the init function's signature reflects this and | ||
// that this type is not instantiated in any function of the module | ||
char_type_candidate = Some((struct_name, struct_handle, def)); | ||
break; // no reason to look any further | ||
} | ||
} |
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 this would be a bit cleaner with struct_defs.iter().find
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.
Possibly, but then we'd have to deal with a return value being a vector, explain why it would only have one or zero elements, etc. Overall, I am not sure it's a huge win, but happy to do the rewrite if requested one more time :-)
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.
find
gives the first element that satisfies the predicate, so what you have here is equivalent to find
, no vectors or anything
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.
Good point! I was thinking about find_map
(there are so many of these little utility functions...). While attempting the rewrite, I realized that (I think...) there is no easy way to propagate error from verify_char_type
if it's used inside of `find's closure.
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 didn't realize the Result
in here. Really I think you need try_find
, which is still in nightly
// unwrap below is safe as it will always be successful if declared_field_count call above is | ||
// successful | ||
if field_count != 1 | ||
|| struct_def.field(0).unwrap().signature != TypeSignature(SignatureToken::Bool) |
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.
signature.0
to avoid the TypeSignature(...)
wrapper
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.
You explicitly tried to avoid it as if I remember correctly you don't love .0
syntax :-) Changed now!
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'm here and there, maybe this is going back to my love for my little sp!()
macro in the source language? I do tend to dislike field access instead of pattern matching
sui_programmability/examples/fungible_tokens/sources/regulated_coin.move
Show resolved
Hide resolved
One other thing, and we don't have to decide on it in this PR, but should we optionally allow the witness type to have And do we want the signature of The former forces you to use it correctly, but the second gives you a tiny bit more leeway. However we cannot do the by-ref version with A) optional |
Agreed! I went with the most conservative option for now to avoid this discussion before this PR lands. Will likely do the same when implementing the native method as it's easy to change (though we should decide soon!). |
struct_name: &str, | ||
struct_def: &StructDefinition, |
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.
canddiate_
fn is_char_type( | ||
view: &BinaryIndexedView, | ||
tok: &SignatureToken, | ||
struct_handle: &StructHandle, |
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.
candidate_
struct_name: &str, | ||
struct_handle: &StructHandle, |
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.
candidate_
struct_name: &str, | ||
struct_handle: &StructHandle, | ||
struct_def: &StructDefinition, |
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.
candidate_
if let SignatureToken::Struct(idx) = tok { | ||
if view.struct_handle_at(*idx) == struct_handle { |
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.
if you want to be fancy, `matches(tok, SignatureToken::Struct(idx) if view.struct_handle_at(*idx) == struct_handle)
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.
It is fancy indeed!
let fn_handle = module.function_handle_at(fn_def.function); | ||
let fn_name = module.identifier_at(fn_handle.name); | ||
return Err(format!( | ||
"characteristic type {}::{} is instantiated in the {}::{} function and must never be", |
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.
is this what rustfmt gave? Is the string maybe too long to not format this?
let fn_handle = module.function_handle_at(fn_def.function); | ||
let fn_name = module.identifier_at(fn_handle.name); | ||
if fn_name == INIT_FN_NAME { | ||
if let Some((candidate_name, candidate_handle, _)) = char_type_candidate { |
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.
Maybe someday we should just move the init
verification over here as it is a bit strange to have it split. But probably fine for now
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.
Any other naming discussions can happen after this lands. Thanks for the changes!
This PR is the first step towards implementing so called one-time-witness in the form of so called characteristic type. The idea is to guarantee that a given witness type can be used only once.
An example use is to guarantee that there can be only one instance of a treasure capability for a given coin (a "permission-less coin)- treasure capability uses a witness type to create a new coin.
This is realized by having a very specific type (optionally) defined in a module, and guarantee that this type can be instantiated only once and the instance cannot be stored or copied. This type must fulfill the following properties which are checked in a verifier pass implemented in this PR:
A followup to this PR will be another one that implements a native function checking if a given type is a characteristic type and providing an example of how all this can be used to implement a permission-less coin.