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

Private view methods #1026

Open
agostbiro opened this issue Jun 21, 2023 · 8 comments
Open

Private view methods #1026

agostbiro opened this issue Jun 21, 2023 · 8 comments

Comments

@agostbiro
Copy link
Contributor

The purpose of this issue is to follow up the conversation from #890 and #1025 on private view methods. I quote here the comments to make it easier to overview. Related issue: #937.

In #890 @itegulov asked:

Should view functions be allowed to be private? Wouldn't such a function only be callable from a transaction?

In #890 @austinabell replied:

No, even calling env::predecessor_account_id fails in view contexts at runtime. This should be restricted at compile time. Good callout!

In #1025 @agostbiro wrote:

A reviewer in #890 commented that private view functions shouldn't be allowed. This wasn't implemented in #890 and when I made the change several tests failed, because methods that take &self as argument are considered private view. Example failing test case:

#[private] pub fn method(&self, #[callback_unwrap] x: &mut u64, y: String, #[callback_unwrap] z: Vec<u8>) { }

On a related note, at first I was surprised that methods that take self by value are view, but I guess that it makes sense since they cannot mutate the object. As I haven't found a test case for this I added one to make sure that method(self) isn't payable in 56394ec.

In #1025 @itegulov replied:

I think you meant that methods that take &self argument are considered view, right? So, as @austinabell mentioned in the original PR, view methods can never be private. That being said, the current near_bindgen API is flawed in that it does not provide clear control of which methods are view or call. There are a couple of ways we could resolve this that we have discussed in the past but never :

  1. Make private, payable and other such modifiers automatically mean that this method is call
  2. Create #[call] and #[view] modifiers and slowly phase out the old self-based deduction system
  3. Just leave everything as is, it has been working so far

In any case, probably out of scope for this PR, just though I would share this here since its related to the work you are doing. I propose we do not prohibit private view functions for now and move this conversation to a separate issue.

@agostbiro
Copy link
Contributor Author

I did a bit a of testing now with this contract:

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
    message: String,
}

#[near_bindgen]
impl Contract {
    pub fn get_greeting(&self) -> String {
        return self.private_get_greeting();
    }

    #[private] pub fn private_get_greeting(&self) -> String {
        return self.message.clone();
    }
}

And I found that I can call get_greeting both with an Account::call and a Contract::view, so maybe it's fine as it is and we don't have to disallow private view methods?

@austinabell
Copy link
Contributor

austinabell commented Jun 21, 2023

And I found that I can call get_greeting both with an Account::call and a Contract::view, so maybe it's fine as it is and we don't have to disallow private view methods?

This is because you're not using the #[private] annotation for what it's intended for. The use case is making a cross-contract call from the current contract (commonly used for callbacks). If you call private_get_greeting as a view call (you have to call directly because you can't schedule the call in view context), you'll see it will fail with handler error: [Function call returned an error: wasm execution failed with error: HostError(ProhibitedInView { method_name: "predecessor_account_id" })].

For reference, the code I'm referring to:

    pub fn get_greeting() -> Promise {
        Self::ext(env::current_account_id()).private_get_greeting()
    }

    #[private] pub fn private_get_greeting(&self) -> String {
        "test".to_string()
    }

Where I don't think there is an issue, since these can both just be called in a transaction rather than a view call, but the semantics are a bit muddied because &self is documented as meaning a view call. This just works because view calls can be called as transactions (as there is no view/transaction context passed from the runtime).

@agostbiro
Copy link
Contributor Author

agostbiro commented Jun 21, 2023

Thanks for clarifying @austinabell ! I managed to reproduce the error with my code when calling private_get_greeting directly as view, and with the code that you provided by calling get_greeting as view.

I'm still not 100% clear on why the error doesn't occur when calling get_greeting as view in my code. Is the code executed different or is the runtime environment different? Is there anywhere I can read more about this?

Btw while testing this I ran some circles, because I didn't realize that the test:integration command doesn't rebuild the contract, so I'm submitting a PR for that in create-near-app (edit: it's near/create-near-app#2014).

@frol
Copy link
Collaborator

frol commented Jun 26, 2023

I'm still not 100% clear on why the error doesn't occur when calling get_greeting as view in my code. Is the code executed different or is the runtime environment different?

@agostbiro self.private_get_greeting() just calls the function on the struct, but calling view function from RPC, it goes through an onramp created by near_bindgen:

Is there anywhere I can read more about this?

I only trust the source code:

#[test]
fn trait_implt() {
let impl_type: Type = syn::parse_str("Hello").unwrap();
let mut method: ImplItemMethod = syn::parse_str("fn method(&self) { }").unwrap();
let method_info = ImplItemMethodInfo::new(&mut method, true, impl_type).unwrap().unwrap();
let actual = method_info.method_wrapper();
let expected = quote!(
#[cfg(target_arch = "wasm32")]
#[no_mangle]
pub extern "C" fn method() {
near_sdk::env::setup_panic_hook();
let contract: Hello = near_sdk::env::state_read().unwrap_or_default();
contract.method();
}
);
assert_eq!(expected.to_string(), actual.to_string());
}

@agostbiro
Copy link
Contributor Author

Ah got it, thanks @frol !

So the conclusion seems to me that there are no changes needed regarding private view functions. Did I understand that correctly?

@frol
Copy link
Collaborator

frol commented Jun 28, 2023

@agostbiro Well, given that there is no use for private view functions (they will just fail), it would be great to forbid them at compilation time (#private + &self should be forbidden). Would it be hard to implement? (sounds like a trivial assert after the refactoring, but I don't have the full context in my head at the moment)

@agostbiro
Copy link
Contributor Author

agostbiro commented Jun 28, 2023

@frol yeah it's a very small change. I tried it now and the following tests are failing after disallowing private view methods:

Failing examples

#[private]
pub fn b(fail: bool) -> &'static str {

#[private]
pub fn handle_callbacks(
#[callback_unwrap] a: u8,
#[callback_result] b: Result<String, PromiseError>,
#[callback_result] c: Result<u8, PromiseError>,
#[callback_result] d: Result<(), PromiseError>,
) -> (bool, bool, bool) {

#[private]
pub fn factorial_mult(&self, n: u32, #[callback_unwrap] cur: u32) -> u32 {

Failing unit tests

let mut method: ImplItemMethod = parse_quote! {

#[private] pub fn method(&self, #[callback_unwrap] x: &mut u64, #[callback_unwrap] y: String) { }

#[private] pub fn method(&self, #[callback_result] x: &mut Result<u64, PromiseError>, #[callback_result] y: Result<String, PromiseError>) { }

#[private] pub fn method(&self, #[callback_vec] x: Vec<String>, y: String) { }

#[private] pub fn method(&self, #[callback_unwrap] #[serializer(borsh)] x: &mut u64, #[serializer(borsh)] y: String, #[callback_unwrap] #[serializer(json)] z: Vec<u8>) { }


With this many tests failing I'm a little concerned about breaking a lot of code in the wild if we disallow private view methods, but if that's unfounded I'm happy to adjust the tests.

@itegulov
Copy link
Contributor

@frol the main issue here is that a lot of contracts in the wild have methods that are view according to their self usage, but not view semantically and are always called in transaction context (@agostbiro provided some examples above). Making this change will break the existing code for them which is why this would have to be a multistep process where we have to decide whether we want to bother with enforcing the view/call unambiguity first.

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

No branches or pull requests

4 participants