Skip to content
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

refactor(types): change account id to newtype and deprecate ValidAccountId #448

Merged
merged 31 commits into from
Jul 12, 2021

Conversation

austinabell
Copy link
Contributor

@austinabell austinabell commented Jun 24, 2021

Definitely more involved than I would have liked and it's unclear what is the best way to initialize an AccountId but here is what changes:

  • Changes AccountId from a String alias to a newtype which performs the validation, JSON/borsh serialization, and other necessary interactions
  • Removes ValidAccountId
    • I was planning on aliasing AccountId for this, but it caused a lot of confusing errors linking to it instead of AccountId, and should probably be removed sooner than later

This is quite a large breaking change, because using String is quite nested within everything, and interactions from ValidAccountId specifically will cause breakages with existing contracts.

closes #446

near-sdk/src/types/account_id.rs Show resolved Hide resolved
near-sdk/src/types/account_id.rs Show resolved Hide resolved
env::is_valid_account_id(account_id.as_bytes()),
"Given account ID is invalid"
);
pub fn get_status(&self, account_id: AccountId) -> Option::<String> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub fn get_status(&self, account_id: AccountId) -> Option::<String> {
pub fn get_status(&self, account_id: AccountId) -> Option<String> {

Or is the turbofish actually required by the macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is required, I haven't dug into why specifically it's needed yet

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's needed because of the metadata macro generation, because it calls Option::<String>::schema_container()

@austinabell austinabell marked this pull request as ready for review June 24, 2021 20:25
Copy link
Contributor

@mikedotexe mikedotexe left a comment

Choose a reason for hiding this comment

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

Looks quite thorough. And I believe you'd addressed what Kladov has brought up. My only silly suggestion is to have the one error begin with Invalid instead of In but 🤷🏼

@frol
Copy link
Collaborator

frol commented Jun 25, 2021

Hey-hey! Node Interfaces team is working on a similar thing in nearcore (@miraclx has it in a branch state for now: near/nearcore@master...miraclx:strict-account-id-validity) and even decided to create a separate crate out of it (near-account-id), so we can merge our efforts here.

@austinabell
Copy link
Contributor Author

Hey-hey! Node Interfaces team is working on a similar thing in nearcore (@miraclx has it in a branch state for now: near/nearcore@master...miraclx:strict-account-id-validity) and even decided to create a separate crate out of it (near-account-id), so we can merge our efforts here.

We can't use that implementation in its current state because it pulls in a few dependencies and uses regex for validation which is too bulky for the SDK (especially since an OKR for the last and upcoming quarter is minimizing contract size)

@austinabell austinabell mentioned this pull request Jun 25, 2021
10 tasks
@austinabell austinabell requested a review from matklad June 25, 2021 15:15
@austinabell
Copy link
Contributor Author

Because this is such a breaking change, it'd be great to get input from others.

Also, the primary remaining design decision is if we change the underlying type to Box<[u8]> which on one hand could allow for smaller code sizes but also does not hold up the compiler guarantee that it will always be utf8 bytes. It would also require calling from_utf8 on the bytes for every reference, and I'm not sure how that gets optimized.

@frol
Copy link
Collaborator

frol commented Jun 25, 2021

We can definitely onboard this implementation as I also don't feel very good about pulling the whole regex machinery to validate the account ID format.

Have you measured the difference of Box<[u8]> vs Box<str>?

@austinabell
Copy link
Contributor Author

austinabell commented Jun 25, 2021

We can definitely onboard this implementation as I also don't feel very good about pulling the whole regex machinery to validate the account ID format.

Have you measured the difference of Box<[u8]> vs Box<str>?

The size of memory is the same, not sure about the compiled code (as it depends a lot on the use case). I am also noticing when switching to the Boxed versions that BorshSchema is broken for them. I can look into fixing this if it's possible.

@austinabell
Copy link
Contributor Author

@frol wrt above near/borsh-rs#36 which is just a benefit to borsh, but would allow these alternatives to be used here (I checked)

Copy link
Contributor

@matklad matklad left a comment

Choose a reason for hiding this comment

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

Looks good to me, left a whole bunch of nits, but only three serious comments:

  • deriving BorshDeserialize for AccountId is wrong
  • still not sure that extra conversions are worth it, but don't see anything particularly problematic there
  • i don't undrestand the reason for AsRef-ing promise API

HELP.md Outdated Show resolved Hide resolved
HELP.md Outdated Show resolved Hide resolved
HELP.md Outdated Show resolved Hide resolved
@@ -7,6 +7,7 @@ use near_sdk::{
// callback_vec,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need cross_contract_high_level.wasm and cross_contract_low_level.wasm blobs here, and why they grew by several kbs?

@@ -39,7 +39,7 @@ fn check_promise() {
let (master_account, contract, _alice) = init(to_yocto("10000"));
let res = view!(contract.promise_checked());
assert_eq!(res.unwrap_json::<bool>(), false);
let status_id = "status".to_string();
let status_id: near_sdk::AccountId = "status".parse().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if these unwraps are to blame for additional bloat?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll check around the code size in a bit. Committing the wasm files was a mistake and how I built them also causes a difference on master. Definitely want to look into making it part of the CI in some way to check differences

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, and as we discussed in this morning's meeting, this is capture in #465

near-sdk/src/types/account_id.rs Outdated Show resolved Hide resolved
near-sdk/src/types/account_id.rs Show resolved Hide resolved
near-sdk/src/types/account_id.rs Outdated Show resolved Hide resolved
near-sdk/src/types/mod.rs Outdated Show resolved Hide resolved
near-sdk/src/types/account_id.rs Outdated Show resolved Hide resolved
@austinabell
Copy link
Contributor Author

  • deriving BorshDeserialize for AccountId is wrong

Think I've addressed everything else, but curious why you think this is wrong? Any contract using the old AccountId will have the ID borsh serialized as a string, and it would be a state-breaking change when they update the SDK. What would you suggest is a better format? I don't see how string would be a bad borsh format either, or am I missing something about borsh storing more metadata for strings over byte slices?

@matklad
Copy link
Contributor

matklad commented Jul 6, 2021

why you think this is wrong?

It allows creating AccoundId which is not valid

    #[test]
    fn deser_checks_validity() {
        let r1 = "💩".parse::<AccountId>();
        assert!(r1.is_err()); // correct, 💩 is not a valid account name

        let bytes = "💩".try_to_vec().unwrap();
        let r2 = AccountId::try_from_slice(&bytes);
        let acc = r2.unwrap();
        assert_eq!(acc.as_str(), "💩") // BUG, 💩 is treated as a valid account
    }

As a rule of thumb, type's invariants should be checked in every constructor, and deserialization provides an extra constructor.

We shouldn't change serialization format, but we should add an extra validation to deserialization.

@austinabell
Copy link
Contributor Author

As a rule of thumb, type's invariants should be checked in every constructor, and deserialization provides an extra constructor.

We shouldn't change serialization format, but we should add an extra validation to deserialization.

Ah, of course, oops

@austinabell austinabell requested a review from matklad July 8, 2021 18:44
near-sdk/src/types/account_id.rs Outdated Show resolved Hide resolved
near-sdk/src/utils/mod.rs Outdated Show resolved Hide resolved
@mikedotexe mikedotexe merged commit e4abb73 into master Jul 12, 2021
@mikedotexe mikedotexe deleted the austin/account_id_newtype branch July 12, 2021 23:20
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.

Remove ValidAccountId and make AccountId a newtype
4 participants