I thought I could present here how I was going about hiding users identity in the Compact contract when storing data associated/owned by them, in an attempt to not only share it here for other devs., but also to get some feedback and additional ideas you may have regarding this.
As part of my learning when trying to write a contract which stores NFTs, I wanted owners to be hidden.
The first option I was experimenting with is the one I learned from the bboard example app, which is declaring a witness for the user to provide a secret/password and then have the posts to be owned by the hashed secret/password. Thus I was doing something like the following to have NFTs owners hidden:
export ledger assets: Map<Uint<128>, Bytes<32>>;
witness localSecretKey(): Bytes<32>;
export circuit mint(assetId: Uint<128>): [] {
assert(!assets.member(disclose(assetId)), "Asset ID already exists");
const owner = disclose(generateOwnerId(localSecretKey(), assetId));
assets.insert(disclose(assetId), owner);
}
export circuit generateOwnerId(sk: Bytes<32>, assetId: Uint<128>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>(
[pad(32, "owner:"), sk, assetId as Field as Bytes<32>]);
}
This obviously works fine, and you can have users to own assets and eventually transfer them as long as they provide the corresponding secret/password.
Then I thought that perhaps it would be better to have users to own their assets with the public-key from their wallet. In this way users don’t need to remember their secret/password, and assets are somehow owned by the wallet itself, just like with any other coin. Thus I came up with this other version of the owner ID and circuits:
export circuit mint(assetId: Uint<128>): [] {
assert(!assets.member(disclose(assetId)), "Asset ID already exists");
const owner = disclose(generateOwnerId(ownPublicKey(), assetId));
assets.insert(disclose(assetId), owner);
}
export circuit generateOwnerId(pk: ZswapCoinPublicKey, assetId: Uint<128>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>(
[pad(32, "owner:"), pk.bytes, assetId as Field as Bytes<32>]);
}
When I gave this a try, I faced another challenge, which I eventually figured out: how to properly obtain the public-key on the client side, to generate the owner ID, to then inspect the ledger if I wanted to list the assets currently owned:
import { MidnightBech32m } from '@midnight-ntwrk/wallet-sdk-address-format';
import { encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime';
const cpk = providers.walletProvider.coinPublicKey;
const bech32m = MidnightBech32m.parse(cpk);
const hexPk = bech32m.data.toString('hex');
const pk = encodeCoinPublicKey(hexPk);
for (const [assetId, ownerId] of ledgerState.assets) {
const owner = pureCircuits.generateOwnerId({ bytes: pk }, assetId);
if (toHex(owner) == toHex(ownerId)) {
// This asset was minted by me
}
}
I believe this is better, and I’m still wondering if it’s effectively good enough, could anybody transfer somebody else’s asset by simply knowing the public-key …?
My assumption, and what I’m trying to confirm here, is that such an attack is not possible since the circuits make use of ownPublicKey() and this already implies that some signature verification has been performed when executing the contract/circuit call…? is this correct ?
Some alternatives I was thinking of, is to try to use a signature generated from the user’s wallet/PK, or use the wallet encryption key, or perhaps use one of the wallet’s secret-keys directly, to generate the owner ID so others cannot claim/transfer somebody else’s assets. I still don’t know how to do any of this using current JS API.
I’ll appreciate any ideas you may have, including any issues you see with all this and with my current understanding.