Bitcoin Integration: How to set a unique derivation path for each authenticated user

TLDR: is it ok to use authClient.getIdentity().getPrincipal()._arr as derivation path to generate a unique bitcoin address for each authenticated user?

The aim is to modify the get_p2pkh_address() function in the basic_bitcoin example (examples/motoko/basic_bitcoin at master · dfinity/examples · GitHub) so that the derivation path is unique to each authenticated user (so that the function returns a unique bitcoin address for each user):

  /// Returns the P2PKH address of this canister at a specific derivation path.
  public func get_p2pkh_address() : async BitcoinAddress {
    await BitcoinWallet.get_p2pkh_address(NETWORK, KEY_NAME, DERIVATION_PATH);
  };

The derivation path is of this form:

// The derivation path to use for ECDSA secp256k1.
  let DERIVATION_PATH : [[Nat8]] = [];

I’m logging the authClient and derivatives this way:

async function handleAuthenticated(authClient) {
  console.log("authClient is: ", authClient);

  const identity = await authClient.getIdentity();
  console.log("identity is: ", identity);

  const agent = new HttpAgent({ identity });
  console.log("agent is: ", agent);

  const principal = await authClient.getIdentity().getPrincipal().toText()
  console.log("principal is: ", principal);

}

And I’m seeing:

The question is, which entry in those responses can I use to set as DERIVATION_PATH, so that

  • each authenticated user will have a unique derivation path, and

  • such DERIVATION_PATH will be compatible in form and size with the one used by the get_p2pkh_address() function?

In particular: no two users should produce the same derivation path, and the same derivation path should be produced by the same user at all times.

if there is a smarter way of achieving the mapping
unique authenticated user → unique canister-controlled bitcoin address,
interested too.

I also need to do a mapping
unique authenticated user → unique canister-controlled IC address,
so if the same piece of data from the user could be used to also be used to produce this second mapping, it would be ideal.

2 Likes

A simple approach is to use the bytes of the user’s principal ID itself as the derivation path.
Each user has a different principal ID, so the derivation paths will be different. Moreover, the derivation path doesn’t change if the user keeps using the same principal ID.

The ecdsa_public_key function returns a valid public key, which can be converted into a Bitcoin address, for any derivation path in the form of a byte array. So you don’t need to worry about any formatting of the derivation path.

With respect to the mapping to a canister-controlled (ledger) address, you can use the user’s principal ID as the subaccount to derive a ledger address.

1 Like

I had started using the user’s principal as derivation path. Good to have confirmation.

The problem I’ve had with using the user’s principal for a canister-controlled ICRC address is that the principal is not a valid subaccount type, because it’s not, at least not always, 32 bytes, and the subbacount requires size 32. Eg this line here: ICRC-1/Account.mo at main · dfinity/ICRC-1 · GitHub

Someone suggesting using a sha2 checksum to turn the user’s principal into a 32 byte output, then use that as subaccount.

Would that be a good approach?

1 Like

SHAing the principal is a good approach.

You may want to add some “magic” to the front so that the subaccount is unique/a bit more private to your application. Something like:

let  x = SHA.New();
x.write(Encode.toBytes("com.myapp");
x.write(Principal.toArray(msg.sender);
let subaccount = x.sum([]);
1 Like

I like the principle but if all the frontend and backend code of the dapp is open source, would this still make sense?