The Locksmith contract is dependent on a functioning and compatible KeyVault. Once deployed, the KeyVault is then set by its deployer to "respect" the Locksmith's supply management wishes. In this way, the "Locksmith" becomes the minter of any and all keys.
With full single-threaded access to key management, the Locksmith exposes a singular "Trust" model for each key. New trusts are created when a user requests one and is minted a root key.
It operates simiarly as a UUPSUpgradeable and can be locked after development is complete. It stores the information about the trust, the keys that belong to it, and some metadata about the keys.
Root key holders can interact with the Locksmith by calling the operations. For each operation except createTrustAndRootKey the caller must possess a valid Locksmith root key.
Storage
The main contract storage is the registry of the Trust. Trusts have their own ID structure, and contain all of the metadata about the trust and associated keys.
The Locksmith contract is deployed with an address pointing to a KeyVault on initialization. This is the only KeyVault the Locksmith will use to manage his key requests.
struct Trust
id
The trust's global unique identifier.
name
Stores as a byte-encoded bytes32, a short descriptive name that does not require global uniqueness.
rootKeyId
Each trust has a single root key Id that can never change. This field denotes the key ID that represents the root key for that trust model instance.
keys
The array of key IDs that belong to the trust. Will always contain the root key ID.
keyNames
A mapping of key IDs to their human readable names encoded as a bytes32.
trustRegistry
A mapping from a trust's ID to the Trust structure.
trustCount
The number of trusts the locksmith has in it's registry.
keyTrustAssociations
Each key belongs to a trust, but it isn't easy to find out which one from an array of Trust structures. An additional mapping keeps this unique piece of key metadata to access the trust it belongs to.
keyCount
An internal state variable that determines immediately what is considered a "valid key" by keeping track of the number of unique keys in existence.
Operations
There are introspection methods, that will not get extensive review here. They are getKeyVault(), getTrustInfo(uint256 trustId), getKeys(uint256 trustId), inspectKey(uint256 keyId). These methods supply the key vault's contract address, the Trust metadata stored in the struct, the list of keys and their data as described in the previous section.
createTrustAndRootKey
This is the method any message sender can call to immediately create a trust and send the root key to the designated recipient. It only takes a trust name, and a destination address. The mint function is an internal one that adds the given key id (in this case, the root key), optionally soulbinds the receiver before minting, and then minting the new key.
/** * createTrustAndRootKey * * @param trustName A string defining the name of the trust, like 'My Family Trust' * @param recipient The address to receive the root key for this trust. * @return the trust ID that was created * @return the root key ID that was created */functioncreateTrustAndRootKey(bytes32 trustName,address recipient) externalreturns (uint256,uint256) {// do all state changes up front to prevent// re-entrancy issues on mint Trust storage t = trustRegistry[trustCount]; t.id = trustCount++; t.rootKeyId = keyCount++; t.name = trustName;// add the root key to the pool mapping, and associate// the key with the trust t.keys.add(t.rootKeyId); t.keyNames[t.rootKeyId] ='root'; keyTrustAssociations[t.rootKeyId] = t.id;// re-entrant// mint the root key, give it to the sender.mintKey(t, t.rootKeyId, recipient,false);// the trust was successfully createdemittrustCreated(msg.sender, t.id, t.name, recipient);return (t.id, t.rootKeyId); }
isRootKey
Definitely answers if a key is a considered root for a valid trust by making sure the key ID itself is valid, the root key matches the one determined in the trust association index, and that the key is on the trust's ring list.
/** * isRootKey * * @param keyId the key id in question * @return true if the key Id is the root key of it's associated trust */functionisRootKey(uint256 keyId) publicviewreturns(bool) {// key is validreturn (keyId < keyCount) &&// the root key for the trust is the key in question (keyId == trustRegistry[keyTrustAssociations[keyId]].rootKeyId) &&// the key is on the ring list (trustRegistry[keyTrustAssociations[keyId]].keys.contains(keyId)); }
createKey
The holder of a root key can use this method to generate brand new keys and add them to the root key's associated trust key ring. By default, the generated keys will have no attached permissions or special features. The internal method getTrustFromRootKey checks to ensure the message sender holds the root key used for the operation, and that the key is in fact root. If either are not true the transaction will revert. It will otherwise provide the trustId in return.
This method can only be successfully called by a root key holder.
/** * createKey * * @param rootKeyId key the sender is attempting to use to create new keys. * @param keyName an alias that you want to give the key * @param receiver address you want to receive an NFT key for the trust. * @param bind true if you want to bind the key to the receiver * @return the ID of the key that was created */functioncreateKey(uint256 rootKeyId,bytes32 keyName,address receiver,bool bind) externalreturns (uint256) {// Make sure the message caller is legit Trust storage t = trustRegistry[getTrustFromRootKey(rootKeyId)];// increment the number of unique keys in the systemuint256 newKeyId = keyCount++;// push the latest key ID into the trust, and// keep track of the association at O(1), along t.keys.add(newKeyId); t.keyNames[newKeyId] = keyName; keyTrustAssociations[newKeyId] = t.id;// mint the key into the target wallet.// THIS IS RE-ENTRANT!!!!mintKey(t, newKeyId, receiver, bind);return newKeyId; }
copyKey
The root key holder can call this method if they have an existing key type within their trust they want to copy. This method will work even if the key is "extinct" in that all previous copies were burnt or lost. Having multiple instances of the same key can be very useful and flexible.
This method can only be successfully called by a root key holder.
/** * copyKey * * @param rootKeyId root key to be used for this operation * @param keyId key ID the message sender wishes to copy * @param receiver addresses of the receivers for the copied key. * @param bind true if you want to bind the key to the receiver */functioncopyKey(uint256 rootKeyId,uint256 keyId,address receiver,bool bind) external { Trust storage t = trustRegistry[getTrustFromRootKey(rootKeyId)];// we can only copy a key that already exists within the// trust associated with the valid root keyrequire(t.keys.contains(keyId),'TRUST_KEY_NOT_FOUND');// the root key is valid, the message sender holds it,// and the key requested to be copied has already been// minted into that trust at least once.mintKey(t, keyId, receiver, bind); }
soulbindKey
This method can be called by a root key holder to make a key soulbound to a specific walet. When soulbinding a key, it is not required that the target address yet holds that specific key. The amount set ensures that when sending a key of a specific type, that they hold at least the amount that is bound to them after the transaction.
This method can only be successfully called by a root key holder.
/** * soulbindKey * * * @param rootKeyId the operator's root key * @param keyHolder the address to bind the key to * @param keyId the keyId they want to bind * @param amount the amount of keys to bind to the holder */functionsoulbindKey(uint256 rootKeyId,address keyHolder,uint256 keyId,uint256 amount) external { Trust storage t = trustRegistry[getTrustFromRootKey(rootKeyId)];// is keyId associated with the root key's trust?require(t.keys.contains(keyId),'TRUST_KEY_NOT_FOUND');// the root key holder has permission, so bind itIKeyVault(keyVault).soulbind(keyHolder, keyId, amount); }
burnKey
The root key holder can call this method if they want to revoke a key from a current holder. This function alone makes the Locksmith NFTs utilitarian on behalf of the root key holder and not speculative in nature.
This method can only be successfully called by a root key holder.
/** * burnKey * * @param rootKeyId root key for the associated trust * @param keyId id of the key you want to burn * @param holder address of the holder you want to burn from * @param amount the number of keys you want to burn */functionburnKey(uint256 rootKeyId,uint256 keyId,address holder,uint256 amount) external { Trust storage t = trustRegistry[getTrustFromRootKey(rootKeyId)];// is keyId associated with the root key's trust?require(t.keys.contains(keyId),'TRUST_KEY_NOT_FOUND');// burn them, and count the burn for logging.// this call is re-entrant, but we do all of// the state mutation afterwards.IKeyVault(keyVault).burn(holder, keyId, amount);emitkeyBurned(msg.sender, t.id, keyId, holder, amount); }
validateKeyRing
A method that determines if a set of keys all belong to the same trust. Optionally enables you to revert if a root key is in the list.
/** * validateKeyRing * * @param trustId the trust ID you want to validate against * @param keys the supposed keys that belong to the trust's key ring * @param allowRoot true if having the trust's root key on the ring is acceptable * @return true if valid, or will otherwise revert with a reason. */functionvalidateKeyRing(uint256 trustId,uint256[] calldata keys,bool allowRoot) externalviewreturns (bool) {// make sure the trust is validrequire(trustId < trustCount,'INVALID_TRUST');// this is safe since the trust is valid Trust storage t = trustRegistry[trustId];// invariant: make sure the root key was minted onceassert(t.keys.contains(t.rootKeyId));for(uint256 x =0; x < keys.length; x++) {// make sure the key is a valid locksmith key. This// prevents funds on the ledger being allocated to future-minted// keys within different trusts.require(keys[x] < keyCount,'INVALID_KEY_ON_RING');// in some cases a root key can't be allowed on a key ringrequire(allowRoot || (keys[x] != t.rootKeyId),'ROOT_ON_RING');// make sure this valid key belongs to the same trust. this// call is only safe after checking that the key is valid.require(t.keys.contains(keys[x]),"NON_TRUST_KEY"); }// at this point, the trust is valid, the root has been minted// at least once, every key in the array is valid, meets the// allowed root criteria, and has been validated to belong// to the trustIdreturntrue; }