Cryptocurrency UX and Key Management

Aodhgan Gleeson
Coinmonks

--

User experience is one of the biggest hurdles to mass adoption of cryptocurrencies. Similar to the sudden widespread emergence of Whatsapp as the dominant secure messaging app in many countries, there is an opportunity in the cryptocurrency space to gain mass adoption through simplicity of user experience. Included within the deliberately broad topic of UX are:

  • Onboarding (painful KYC experiences, confusion surrounding what is required to start)
  • User Interaction (addresses, unfamiliar logins/patterns/actions, finality effects, overly cluttered views)
  • Education (wtf is Cryptocurrency? Blockchain? Wallets? Exchanges? Gas?)
  • Price Stability (why would I use this? It’s too volatile)
  • Key Management (storage, usage and addition/removal of private keys)
  • Cost (scalability, eth2.0/”Caspar McShardface” can be considered an “ultimate” UX long term goal)

Key management is perhaps a plausible starting point (in comparison to scalability and stability!) Key management has many different flavours and has existed as a field of study since the early days of cryptography. Indeed many off-chain solutions can be provided to cryptocurrencies key management issues such as Shamir’s Secret Sharing, Distributed Key Generation and Storage services. However each of these comes with its own set of challenges, of which UX is certainly included.

A simpler, more immediately realisable set of solutions may lie in the category of on-chain key management. Many users are familiar with the concept of a private key, or at least after a brief explanation understand the concept. A real pain point lies within the practical management of these keys — users are generally not interested undertaking the effort required to securely store, backup, recover and change these keys. Multisig inspired on-chain key management presents a solution to some of these challenges.

Multi-what?

Multisignature wallets are a familiar concept to many within the cryptocurrency space but despite their simplistic concept, many people not familiar with the space do not understand what this term means, nor the value of a functional multisig construct. For clarity in this article we consider a multisig contract (typically) to be a setup whereby m-of-n signatures (authorisations) are required before a transaction can be sent/spent. These constructs exist in traditional banking - in a common simple case two partners may have a joint bank account, each with their own debit/credit card that spends from this joint account. In crypto-speak this would be considered a 1-of-2 multisig. The flexible start contract language of Ethereum allows us to construct much more advanced multisignature contracts, impose arbitrary limitations and allowances as well as adjustable permissioning.

An interesting approach to key management is using the same concept as multisignature constructs but applied to the private keys themselves —for example instead of m-of-n authorising a spend, an addition of another private key can be authorised. This can be reffered to as “on-chain key management”. This idea (plus more) is expressed through ERC-725 Identity standard proposal. This proposal covers much more than the ability to manage keys onchain; it can be considered closer to a specification for a self-soverign identity (these concepts are all closely interlinked), but the key management principles are of primary interest for this article. The standard has gained significant industry traction, with an alliance formed specifically to encourage the growth of the standard. We shall start by discussing the standard, look at a sample implementation of the standard, examine how it impacts on UX and discuss extensions and limitations of this standard.

ERC-725

From the perspective of traditional key management, ERC-725 is an onchain key management system. Each key involved in this draft standard is either an public key or corresponding ethereum address, corresponding to an off-chain private key. Each key has a corresponding keyType, purpose and actual key value:

struct Key {
uint256 purpose;
uint256 keyType;
bytes32 key;
}

So far, four key purposes have been defined: MANAGEMENT, ACTION, CLAIM & ENCRYPTION, in ascending value. MANAGEMENT keys are permitted to perform administration type operations, such as adding or removing another key. ACTION keys are less “powerful” and may be limited to sending non-administration transactions. keyType specifies the cryptographic primitive the key relates to; RSA, Elliptic Curve, etc., although concerns lie around the number of key types that can fit inside 32 bytes (256 bits). In ERC-725 there are no expiration dates associated with each key, which is in contrast to uPort’s ERC-1056 proposal.

Claims (or attestations) and management of these claims (ERC-735 Claim Holder) are a large intertwined part of ERC-725 but as mentioned the primary focus is to examine the key management aspects and the increase in usability these bring. The ERC-725 standard specifies high level functionality that implementations should adhere to, to be considered compliant to the standard. For purposes of illustration it is easier to focus on one implementation; that of Status, a prominent company in the industry aiming to be a type of mobile gateway to the Ethereum ecosystem. Of interest in their github repository are the ERC725 and Identity Solidity contracts. This is not the only way to implement the standard, but highlights some of the features, patterns and challenges associated with writing smart contracts in Solidity.

mynameisidentity.sol

Identity.sol inherits both ERC725 and ERC735 (which we shall ignore in this analysis), two abstract “interface” style contracts. This means Identity.sol must define the following functions:

function getKey(bytes32 _key, uint256 _purpose) public view returns(uint256 purpose, uint256 keyType, bytes32 key); function getKeyPurpose(bytes32 _key) public view returns(uint256[] purpose); function getKeysByPurpose(uint256 _purpose) public view returns(bytes32[] keys); function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public returns (bool success); function removeKey(bytes32 _key, uint256 _purpose) public returns (bool success); function execute(address _to, uint256 _value, bytes _data) public returns (uint256 executionId); function approve(uint256 _id, bool _approve) public returns (bool success);

Before we get to these functions we define some contract level variables for storage of keys, their corresponding purposes and various thresholds that are required or associated with each key:

mapping(bytes32 => Key) keys; //keccak256(key, purpose)=> Key Structmapping(uint256 => bytes32[]) keysByPurpose; //keys corresponding to each key purpose type (MANAGEMENT,ACTION, etc)mapping(bytes32 => uint256) indexes; //indices of active keysmapping(uint256 => uint256) purposeThreshold; //how many of keys are required to sign for that key purpose type (example: min of 1 for MANAGEMENT key)

Additionally a transaction is defined:

struct Transaction { 
bool valid; // flag to mark if tx is valid
address to;
uint256 value;
bytes data;
uint256 nonce; // tx nonce, not contract nonce
uint256 approverCount; // incremented by approve function
mapping(bytes32 => bool) approvals; //which keys have approved tx
}

Transactions are stored in a mapping txx — transactions may be awaiting approval from other keys, and this also prevents a form of replay attack vulnerability alongside a contract level nonce. For the purpose of examination of key management functionality, the recovery associated fields/functionality (recoveryContract, recoveryManager) of this contract are also ignored in this analysis.

mapping (uint256 => Transaction) txx; // generally should correspond to nonce => Transactionuint256 nonce; // tx nonce to prevent replay attacks

Adding, Removing and Replacing Keys

Starting with addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public managementOnly returns (bool success) , an externally callable “wrapper” function, we recall that the _key parameter can be either a generic public key or an Ethereum address. Practically speaking, this function is equivalent to adding a authorised spender, in the multisig context discussed earlier.

function addKey(bytes32 _key, uint256 _purpose, uint256 _type)
public
managementOnly
returns (bool success)
{
_addKey(_key, _purpose, _type);
return true;
}

The managementOnly modifier restricts access to this function to internal calls or ensuring the msg.sender is a MANAGEMENT key. It does so through the ubiquitously called function isKeyPurpose(bytes32 _key, uint256 _purpose). This function checks if keys[keccak256(_key, _purpose)].purpose == _purpose . Note that the keys mapping does not store a direct mapping of key.value => Key , but rather a keccak hash (keyHash) of the keys value and the purpose type. isKeyPurpose() is called throughout the contract, leading to reduced redundant code, an essential trait in smart contract design (due to high cost of deployment/storage and increased ease of security analysis/auditing).

If the managementOnly modifier allows the function call to continue, the internally restricted _addKey(bytes32 _key, uint256 _purpose, uint256 _type) function is called. This function calculates the keyHash for the passed key, ensures the key has not already been added, ensures the Key.keyType is defined, then adds the Key to the key mapping, indexed by keyHash.

function _addKey( bytes32 _key, uint256 _purpose, uint256 _type ) private 
{
bytes32 keyHash = keccak256(_key, _purpose);
require(keys[keyHash].purpose == 0);

require( _purpose == MANAGEMENT_KEY || _purpose == ACTION_KEY ||
_purpose == CLAIM_SIGNER_KEY || _purpose == ENCRYPTION_KEY );

keys[keyHash] = Key(_purpose, _type, _key);
indexes[keyHash] = keysByPurpose[_purpose].push(_key) — 1;
emit KeyAdded(_key, _purpose, _type);
}

The _key is also pushed into the keysByPurpose mapping, using _purpose as its index. For example, if it is an ACTION key that is being added, this increases the number of keys stored under the ACTION index (2 in the case of ACTION).

The KeyAdded event is also emitted so that any wallets/infrastructure outside of the Ethereum blockchain can react/trigger.

The removeKey(bytes32 _key, uint256 _purpose) public returns (bool success) function works much the same as addKey(), but (as expected) in reverse. The same managementOnly restrictor is utilised. The key is deleted from the key mapping and removed from the keysByPurpose mapping. A KeyRemoved event is also fired.

Additional functionality is provided over the ERC-725 standard in the form of a replaceKey function, which calls _addKey(), then _removeKey().

Other ERC-725 Required Key Functions

Both getKeyPurpose(bytes32 _key) and getKeysByPurpose(uint256 _purpose) behave exactly as expected, with getKeyPurpose returning the purposes stored for the _key queried.getKeysByPurpose returns all the keys the contract associates with that _purpose.

Approve and Execute

Approve and execute are the two externally callable functions which result in execution of a transaction, provided sufficient approval from the appropriate key types is obtained. These functions are the main interface via which to send transactions.

function execute(address _to, uint256 _value, bytes _data)         public 
returns (uint256 executionId)
{
uint256 requiredKey = _to == address(this) ? MANAGEMENT_KEY :
ACTION_KEY;
if (purposeThreshold[requiredKey] == 1) {
executionId = nonce;
nonce++;
require(isKeyPurpose(bytes32(msg.sender), requiredKey));
_to.call.value(_value)(_data);
emit Executed(executionId, _to, _value, _data);
}
else {
executionId = _execute(_to, _value, _data);
approve(executionId, true);
}
}

execute() checks if the transaction destination is to the contract itself, if so a MANAGEMENT key is required, if not an ACTION key. The purposeThreshold for the key type is checked — if only one key is required for that key purpose type, the key is checked to be of that purpose using isKeyPurpose and the transaction is sent with a low-level call: _to.call.value(_value)(_data) . If the purposeThreshold for that particular key purpose type is not equal to one, the internal _execute() is called. This internal function constructs a Transaction , sets approvalCount=0 , inserts it into the txx map, emits an ExecutionRequested event and returns the executionID, which is the contract level nonce.

The approve() “wrapper” function is then called with executionID as a parameter. Upon satisfying the managerOrActor modifier, _approve(bytes32(msg.sender), executionID, _approval=true) is called.

function _approve(bytes32 _key, uint256 _id, bool _approval)         private         
returns(bool success)
{
Transaction memory trx = txx[_id];
require(trx.valid);
uint256 requiredKeyPurpose = trx.to == address(this) ?
MANAGEMENT_KEY : ACTION_KEY;

require(isKeyPurpose(_key, requiredKeyPurpose));
bytes32 keyHash = keccak256(_key, requiredKeyPurpose);
require(txx[_id].approvals[keyHash] != _approval);

if (_approval) {
trx.approverCount++;
}
else {
trx.approverCount--;
}
emit Approved(_id, _approval);

if (trx.approverCount<purposeThreshold[requiredKeyPurpose]) {
txx[_id].approvals[keyHash] = _approval;
txx[_id] = trx;
}
else {
delete txx[_id];
success = address(trx.to).call.value(trx.value)(trx.data);
emit Executed(_id, trx.to, trx.value, trx.data);
}
}

The transaction is loaded from the txx array and checked for validity. It is interesting to note here that an optimisation was carried out to load the transaction into memory (there are three memory “areas” in the 256 bit stack based EVM — stack, memory and storage, listed ascending in operation cost, and descending in persistence and accessibility). isKeyPurpose is checked and the keyHash of the passed key is found. The approvals array of the transaction is checked to be false. The approverCount field of the transaction is incremented and if there are sufficient approvals for the purposeThreshold for that requiredKeyPurpose, the transaction is sent with:address(trx.to).call.value(trx.value)(trx.data). If there are insufficient appropriate approvals from the required key purpose type, then this approval is added to the transactions approvals field and saved back into storage, awaiting further required approvals.

This implementation shows the flexibility and versatility of on-chain key management, but also shows how quickly smart contracts become complex. This implementation ignores the ability to have different keyType (RSA, EC etc. ) as this would certainly add more complexity to the contract.

Throwaway keys and Gasless Transactions

Device loss is (unfortunately) common. The recent EOS ICO token to EOS mainnet porting/transfer event showed that ~5% of users supposedly lost their private keys and were unable to transfer their funds to the EOS mainnet when the time came. The majority of these users were probably relatively tech-savy, having participated in an ICO in the first place, and this figure can be expected to increase as adoption increases and less technologically aware users come on-chain.

Throwaway keys: the more accurate terminology for these keys is “context specific application keys”. A good user experience includes suffering complete loss of funds due to the loss of a device that contained the keys. Ideally a throwaway key contains no monetary value, but may contain permissioning value. While non-crypto users may not be overly familiar with the process of key management, they may be familiar with the process of cancelling a lost bank card, which this paradigm mimics.

ERC-725 facilitates throwaway keys, naively as an ACTION key, in conjunction with the meta-transaction capability introduced by the logic in IdentityGasRelay.sol, which inherits the Identity.sol contract. The various key authorisations are utilised to support gasless transaction functionality. This contract will be analysed in another blog post.

Meta or “Gasless” Transactions: this ties into both user education and key management. The end user should not be expected to understand the Halting problem nor the role of gas to prevent this. “Someone else” needs to pay the gas for transactions initiated from a device, if that device contains a throwaway key and that key holds no Ether, thus has no ability to send a transaction. Someone else may be a pool of relayers economically incentivized, or simply a cloud based server.

There are many different methods to achieve this, with several different projects approaching the challenge from different angles. Indeed some of the on-chain architecture patterns are useful for Subscription Services. ERC-1077 is a proposal to standardise these Executable Signed Messages methods, while ERC-948 attempts to do the same for subscription based payments.

Limitations

Privacy: As with most Ethereum based on-chain computation, privacy is limited. Without considering the privacy implications by the ERC-735 public attestation/claims, within ERC-725 there are privacy leaks. For example, do I want the entire blockchain-observing-world to know that I have added a new MANAGEMENT key? This would be easily recognisable. What would the implications of this be? Does this translate to increased real world risk and how?

Ultimately many gains in privacy can be gained through the use of Zero Knowledge Proofs, which seem particularly applicable to the ERC-735 Claims rather than the key management aspects of the standard. Proofs of various flavours will be constructed, but the current state of Ethereum are expensive to verify and practically impossible to generate, despite the presence of verification precompiles.

Cost: On-chain transactions are (currently) expensive. On-chain custom cryptographic operations are even more expensive. The ultimate UX goal is for increased scalability to the point that onchain computation is so cheap that this category of transaction are effectively negligible in cost, say sub $0.05, a substantial improvement above current traditional banking charges for issuance of new debit/credit cards. Currently the example contracts cost $4.17 to deploy (@1ETH = $206 and an average transaction acceptance time gas price of 5GWei). Addition of a first key costs ~$0.14. While removal of this first key costs ~$0.04. These costs will vary slightly with the quantity of keys added due to indexing and storage associated costs. While the deployment may be considered costly, the addition and removal of keys is cost effective.

A follow up post explaining the role of Identity.sol and IdentityGasRelay.sol in gasless transactions is coming soon.

Follow me on Medium or Hit me up on Twitter: https://twitter.com/gawnieg

Get Best Software Deals Directly In Your Inbox

--

--