Request - Inter Canister Calls - Add Original Caller()

For inter-canister calls, I think it would good if in addition to caller(), we have a command like original_caller() that gets the id of the first caller in a chain of calls. This would be like the meta data in the transaction for an Ethereum transaction.

This could be useful for interop - in Ethereum, we know who the tx caller is for a set of calls, which can enable easier to use access features. We don’t know who the original caller is on Dfinity.

Thoughts, opinions?

@nomeata

When we discussed that a year ago or two, the reasoning against this feature was that it is not a good fit for a composition-of-services platform as ours. If you provide a service (say, a hotel with room booking), and someone wants to know if a room is available, then you should know who is talking to you (e.g. a user directly, or maybe another canister like a travel agency), but whether that service is doing that as a result of a button click by one particular user of that service, or maybe a button click of some admin, or maybe in response to yet another call from another canister, or in a cron job (who is the original caller now), is of no concern for the hotel.

If the hotel needs to know some original user, make it part of the interface and pass it along as data.

Also, it might potentially be pretty bad if a service that you are using from your canister suddenly changes behavior based on who your users are.

That said, what use case do you have in mind that needs this feature?

5 Likes

My use-case is pretty abstract - but I’m writing a framework for reversions. It comes down to reverting a set of changes triggered by a user through intermediate queries. I’d like to revert the changes based on the original caller, rather than having all the functions that could be reverted having an extra parameter for the caller id. So maybe not what everyone else would be using.

I’m curious why you think this is bad - “Also, it might potentially be pretty bad if a service that you are using from your canister suddenly changes behavior based on who your users are.” It doesn’t seem to be a problem with Ethereum? We manage token balances etc. based on who the user is.

Ethereum has no data privacy at all. We try to do better, hence this like pseudonyms in the Internet Identity, access control on query calls etc.

Ok that’s fair, could be done with an Option ? But I guess you are thinking of Dos attacks, tracking etc.

(Sorry if my previous comment was impolitely terse, it was late and I was already on the way to bed.)

Mostly tracking, yes, and a general aversion to abstraction leaks.

Another way of putting it is: Either you know and trust the calling canister in some sense (and then it can just tell you the original user if needed). Or you don’t know the calling canister. But then knowing who triggered that canister’s action doesn’t give you much useful information.

I would really like this as well. It gives you assurances that there was a signature on the other end of a third party call.

For example, for some things you want:
User A → CanisterB() → CanisterC(principalA)

But for other you want
User A → CanisterC().

And you never want:
CanisterBUpgraded() ->CaniserC(principalA

By the system providing orginalCaller I have some cryptographic guarantees and all kinds of intermediate services become possible. It seems like being able to access original caller ENABLES composable third party services amongst semi-untrusted containers. Now that I write that out, it sounds like a horrible idea. Ha!

So what do I really want? I need CanisterC to know that a call was authorized by UserA and that CanisterB didn’t just up and decide to call a function. I’d also like for CanisterB to not have to do much to pass this data to Canister C. What does this data structure look like and how do we implement it in a different manner? Can we sign some kind of data that B can pass along? Something like a JSON Web Token. It seems like CanisterB might need to alert the User of all the canisters that it might access and that doesn’t seem too practical once we get into composable services. This is not an easy problem to solve!

I guess ETH lets all called contracts access the original ECDSA v,r,s so that it can get the original caller.

Having more crypto secure data seems like a good thing in the long run. Other things I’d like to have access to from my motoko contracts while we are asking:

The current Tip of the main BLS Chain so that I can verify witnesses
The ‘blocknumber’ or ‘round number’ or what ever the current subnet agreement about what is being approved.
Cycle Limit remaining before a trap
Current XDR cycle to ICP Rate
Coinbase of the node provider that is running my transaction
Current Subnet ID

Why do I want these things? Some of them I don’t know yet, but they seem like important variables that my code could access in a crypto secure way and I might want to do that one day.

1 Like

Completely agree with this. The extra data could and probably would be useful to developers.

But “authorized“ implies “user approves the call”. Not “user was interacting with Canister B, and now Canister B chose to do something to Canister C” – and that’s all that “original caller” gives you. It tells you who pressed a button. But does not carry any semantic meaning, and gives you no insight into the user’s intentions. Unless you also know precisely the code of CanisterB. But then you can just believe Canister B.

I don’t see why it follows that you can just trust canister B. Canister C can’t trust B unless it only gives access to canister B which implies it knows the code of B, which is already limiting. If you know the original caller, the trust model is inverted and so you are implying the caller knows B and not the canister.

I think it’s the question more of who the canister C can trust. Canister C could be storing data specific to users and routing through intermediaries. It can’t guarantee the user data is validly updated without also knowing who the intermediary canister is in the current model, whereas this is not the case with original caller.

I’ll argue the other side here. If a user implicitly trusts an open and documented canister that only ever interacts with other open and documented canisters CDE&F, it sure would be nice if we didn’t have to relay the original caller information and the other canisters got it ‘for free’.

1 Like

I sympathize with both sides of the argument here when thinking in terms of composable inter-canister calls and tend to defer to greater minds than my own in making decisions like this (and am glad such minds have open discussions like this!).

I ended up here, though, because I was surprised to notice that the only call that has access to the “logged in” identity (via the msg.caller property that comes through as part of the public shared(msg) func definition) is the very first call that comes from my javascript frontend canister’s actor. I figured that at the very least any of my own functions (private functions in the same canister) that I call from that function would receive the msg argument. I guess I have to modify all my helper functions to pass msg (or msg.caller) as an argument instead of using the handier automatic property.

Maybe the above objections on both sides of the argument could be resolved by allowing a canister to specify who should be sent along as the caller of the downstream function: self, or msg.caller. Basically allow a canister to choose whether it trusts the downstream canister with the user’s principal on a case-by-case basis, defaulting to “no” to keep things the way they are, but allowing an easier method of enabling that behavior without having to litter your entire internal API with original caller parameters.

Or possibly at least default to sending along the original caller between intra-canister calls. I can’t imagine any reason why I would need to know which canister is calling a private function in my own canister. It’s always myself.