Deriving the Eth Address from Caller indetnficiation `shared({caller})`

Hi there,

I’m implementing in Motoko some cross chain code. It’s supposed to lookup certain information about the caller on remote evm chain. I read the docs from https://internetcomputer.org/docs/current/motoko/main/writing-motoko/caller-id and have tried the ecdsa_public_key method to retrieve the public key to do this:

Principal -> PublicKey -> (hashing) -> EthAddress

Unfortunately it looks like I can only fetch the PublicKey from existing canisters but not from the caller identification. Am I correct that there is no way to derive the EthAddress from the caller identification alone at this time? What have others done to alleviate this issue for chain fusion solutions?

Users/principals do not have ETH addresses by default. Only canisters can hold funds. But, canisters can have multiple addresses. If you have a look at this Rust example, you will see that the public key (from which the ETH address is derived) takes a derivation path. Often, projects use the caller principal as a derivation path and then derive an address from that public key.

Side note: The user will then not hold ETH directly. The ETH will be held by the canister on behalf of the user

2 Likes

Thanks for the answer. I think it’s somewhat a misunderstanding. My question was if it’s still possible to access the public key from which the caller id is stemming - since in the caller canister there is a signature and thus in case of secp256k1 callees also a recoverable public key. It seems though that from Motoko only the already hash derived ICP id “Principal” is available from which the Ethereum Address is not recoverable.

I’ve now built a work-around in which we require callees to first register themselves with the Canister using their secp256k1 public key. Using the sha2 and sha3 mops libraries Mops • Motoko Package Manager we then calculate from the long-form of the secp256k1 pubkey the correct ethereum address and icp principal to create a map we can use later to make the caller lookup.

Flattened pseudo-code to see the critical parts:

  public func register(public_key : Blob) : async Nat {
    if (Array.size(pubkey) != 65) {
      Debug.trap("Invalid public key size: " # debug_show(Array.size(pubkey)));
    };
    let eth_address = address_from_pubkey(public_key)
    let icp_principal = principal_from_pubkey(public_key)

    ... make cross-chain call to know if permissed and store in cache ...
  }

  public func address_from_pubkey(public_key : Blob) : Blob {
    var pubkey = Blob.toArray(public_key);

    let subject = Array.subArray(pubkey, 1, 64);
    var sha = SHA3.Keccak(256);
    sha.update(subject);
    let result = sha.finalize();
    Blob.fromArray(Array.subArray(result, 12, 20))
  };

  public func principal_from_pubkey(public_key : Blob) : Principal {
    var pubkey = Blob.toArray(public_key);
    let hash = Blob.toArray(Sha256.fromBlob(#sha224, derFromPublicKey(public_key)));
    let id = Blob.fromArray(Array.append<Nat8>(hash, [2]));
    Principal.fromBlob(id)
  };

And then at runtime we check access using the caller identification:

  public shared(msg) func add_message(key_id : Blob, ciphertext : Blob) : async Result.Result<(), Text> {
    assert_membership(msg.caller);

    ... logic ...
  };

  func assert_membership(member : Principal) {
    if <not principal is stored in cache as allowed> {
      Debug.print("Not allowed");
      assert false;
    };
  };