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

Payload exploration #120

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft

Conversation

jonjove
Copy link
Contributor

@jonjove jonjove commented Sep 21, 2022

What

This PR contains a still-in-progress exploration of having contracts expose a payload function to facilitate signing in wallets and other downstream systems. To make development as fast as possible, I've replicated/modified auth functionality in this crate rather than working in soroban-auth. The final implementation of course would be in soroban-auth.

Earlier discussions of this idea used names like f_payload but in general those names don't comply with the 10 character symbol requirement. The universal payload function is the reaction to that issue. The signature of payload is almost certain to change as this exploration develops.

This also incorporates an idea from @dmkozh about putting all signatures into a single argument at the end of the argument list. Exploration on this part is very preliminary, and is almost certain to change entirely.

cc @leighmcculloch who requested this PR

Why

Making signing easier is a huge piece of our work in the last several weeks.

Known limitations

Too many to enumerate


verify_and_consume_nonce(&e, &from_id, &nonce);

crate::auth::check_auth::<TokenPayload>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the idea to use/reuse different payloads for different functions that need auth? I suppose you omitted that from the PR, as only this fn uses the new auth.

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 exploratory work. I learned what I needed from this part so I didn't do the rest of the work (as noted, I expect everything to change). But in practice every function will use the same new auth.

fn payload(
e: Env,
function: Symbol,
args: Vec<RawVal>,
Copy link
Contributor

Choose a reason for hiding this comment

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

For me the main value of the initial proposal for having fn_payload was that we have a typed interface to contract fn we need to sign. Here it's still not clear what the args should be, which I think is on of the main issues with signing fns. Or is this complimentary to the initial proposal and contracts still would introduce fn_payload for fns with non-trivial args mapping?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's not really any easy way to do the f_payload thing as noted in the PR description. I agree typed interfaces are nice, and I would prefer that. For the sake of this exploration, I went this way because it meant I could move fast without solving other problems (like length limits). The real implementation might look different.

Comment on lines +94 to +95
#[cfg_attr(feature = "export", contractimpl)]
#[cfg_attr(not(feature = "export"), contractimpl(export = false))]
Copy link
Member

Choose a reason for hiding this comment

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

Just an fyi, this doesn't do anything anymore. contractimpl always exports.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good to know. I copied and pasted this from the token example, so it should be removed there too.

@jonjove
Copy link
Contributor Author

jonjove commented Sep 21, 2022

Just updated this in a variety of ways:

  • Changed the sigs parameter from Map<Identifier, Signature> to Map<Symbol, Signature>
  • Changed the payload return value to Map<Symbol, SaltedSignaturePayload> (ignore the weird name, it's temporary)
  • Reverted check_auth implementation back to something more like version in main
  • Used this technique to implement liquidity_pool_router_with_payload deposit

There are still a couple of pieces missing from this, notably:

  • payload return value should be Map<Symbol, (Identifier, SaltedSignaturePayload)> or something like it (how else does the signing agent know what key(s) to sign with)
  • there should be a function needs_sigs that determines whether to call payload at all

@jonjove
Copy link
Contributor Author

jonjove commented Sep 21, 2022

Added the missing pieces described above and fixed a couple of bugs. A wallet or other downstream system would use this as follows:

  1. Wallet receives request to call Contract::f(x, y, z...)
  2. Wallet calls Contract::has_sig(f). If false, stop here and just submit the request as received. If true, continue to step 3.
  3. Wallet checks that the last parameter to f is an empty map (there might be other things that could be done at this step but this is reasonable for starters)
  4. Wallet calls Contract::payload(f, [args excluding the last parameter which is an empty map], [])
  5. Wallet takes the result of payload and requests the appropriate signatures using the Identifier and SaltedSignaturePayload in each value, then packages those signatures back into a map using the Symbol in each key
  6. Wallet submits Contract::f(x, y, z...) with the last parameter (which was an empty map) replaced by the map of signatures produced in step 5

I know that this flow is pretty complex, but I think it covers an enormous number of cases including interesting cases that involve functions that forward some-but-not-all signatures to other functions. See liquidity_pool_router_with_payload deposit for context on how that works.

This is basically concept-complete at this point so feedback on the idea would be great. Sorry that the implementation is largely hideous, but I'm confident it could be cleaned up.

@jonjove
Copy link
Contributor Author

jonjove commented Sep 21, 2022

I forgot to mention that the wallet needs to do some verification on the payloads it receives before requesting signatures. Let me think about exactly what needs to be checked.

);

let mut callstack_a = callstack.clone();
callstack_a.push_back((deposit_a.token.clone(), symbol!("xfer")));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should call stack a contain this contract (i.e. liquidity pool)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great question. I think it could go either way. Contracts would probably be mildly cheaper if we defined "call stack" to mean everything before this function--this means the simple case of passing a signature to the first contract incurs no call stack cost since it would be empty. But the whole system would probably be slightly less surprising if we defined "call stack" to mean everything including this function. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't call stack here deposit->xfer ,so no matter which option do we choose deposit happens before xfer and hence should be included no matter which option we choose? IIUC our goal should be to distinguish 'xfer' from 'deposit->xfer' in order to prevent frontrunning messing up things, right?

}
}

pub trait PayloadTrait {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this trait be a part of a library? I don't see it used in auth now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I think so. It would probably be in the soroban-auth crate since implementing this trait would be part of implementing standard auth.

function: Symbol,
args: Vec<RawVal>,
callstack: Vec<(BytesN<32>, Symbol)>,
) -> Map<Symbol, (Identifier, SaltedSignaturePayload)> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks nice, pretty close to what I've imagined, albeit a bit verbose. Maybe we could provide a simple implementation for single-signature contracts, while allowing more complex contracts to be flexible.

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'm very open to having multiple versions of this, and I actually think that's one of the major strengths of this approach. For example, we could add a function fn payload_v(e: Env) -> Symbol (payload_v is short for "payload version") that returns an identifier for what payload function to call. We could even make that fn payload_v(e: Env, function: Symbol) -> Symbol if we wanted to allow it to vary by function. Then we could have a couple standards at different complexity levels, and the wallet would just invoke the right one based on the return value of payload_v.

.get_unchecked(symbol!("sig"))
.unwrap();

let to_verify = SaltedSignaturePayload::V0(SaltedSignaturePayloadV0 {
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 think this isn't quite right. I'm pretty sure I would need to add to_forward_a and to_forward_b to this signature payload.

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.

3 participants