HW wallet design - hadescoincom/hds-core GitHub Wiki
Hds HW wallet supports the functionality similar to other blockchain protocols, such as keeping all the secret keys within the device, and signing transactions authorized by the users.
Since Hds is based in MW, the design of such a wallet is more complex, because of the following:
- Transactions are built interactively
- No addresses
- No transactions in the common sense. Transaction elements can be added and removed
- The HW wallet is a minimalistic stateless device. It should not handle blockchain events (blocks, headers, reorgs), handle BBS traffic, fully negotiate transactions, and etc.
- The host may be compromised, yet no spend should be possible without user's permission.
- Once the user activates the HW wallet (enters the pin-code), the host can view all the user information (balance, UTXOs), but not spend funds.
- Every spend (transaction that assumes user spends funds) needs a user authorization. The user sees the following:
- The amount being-sent
- Asset type (hds or CA).
- Recipient identifier.
- Transaction kernel ID.
- Height range (min/max height) during which the transaction is valid.
- All this information is cryptographically verified, and there should be no feasible way for the host to tamper with it.
Note: Because in MW transactions are essentially (sort of) Schnorr's multisignatures, the (1) is actually tricky to implement. To create a Schnorr's signature the signer generates a nonce, which must be retained for the duration of the multi-signature ritual.
We solve this by assuming the device has a limited non-volatile memory that can be used to store nonce preimage (source material). We make sure that neither the nonce nor the appropriate secret key can be extracted by malicious host (such as attempt to use the same nonce to sign different things).
The HW wallet device should have several 256-bit slots for nonce preimages. The more slots it has - the more ongoing transactions the HW wallet may handle at the same time.
(There is also an option of using VRFs, but it's too complex for HW wallets.)
-
CoinID
- full UTXO identifier. Consists of the following:-
SubIdx
32-bit child key identifier -
Idx
64-bit coin number -
Value
(64 bits) -
AssetID
(32-bit number)
- For each
CoinID
the HW wallet generates a unique blinding factor using the master secret.
-
-
OwnerKey
- derived from the master secret, used to recognize the owned UTXOs. -
P-Kdf
- public key generator, which for aCoinID
generates an appropriate public key (EC point) that corresponds to its blinding factor. -
IdentityNumber
32-bit number -
Identity
- public key that corresponds to the unique private key generated for specificIdentityNumber
. -
PaymentProof
- Schnorr's signature, signed by the receiver, that it receives funds as a result of the transaction. The signature signs the following parameters:- Value received
- Asset type
- Kernel ID
- Sender
Identity
Note: the actual key generation is according to HKDF (rfc-5869). The generated blinding factor depends in a non-transparent way on all the fields of CoinID
, thus there's no feasible way to tamper with them and adjust the resulting blinding factor accordingly.
In addition to HKDF, each computed key is multiplied by a fixed secret scalar, so-called co-factor. The P-Kdf
contains the same seed HKDF data, but the co-factor is replaced by its image (EC point).
Note (2): To denote the UTXO Hds actually uses so-called switch commitments, which are compressed el-Gamal commitments. So that in practice the obtained blidning factor gets an addition, which depends on the committed value and asset type. The host can calculate this addition alone (without the HW wallet).
All the HW wallet functionality comes down to the following methods.
The following is returned without user permission
- Number of slots available
OwnerKey
-
P-Kdf
for arbitrary child key - Rangeproof for arbitrary
CoinID
- signature for receive transaction kernel (i.e. transaction in which no funds are lost).
The following needs user permission
- signature for send transaction kernel
- signature for split transaction kernel (i.e. only transaction fee is lost)
During the HW wallet initialization it generates a seed using the true random generator (available on most HW wallets), which is used to set initial values in all the slots.
MW transaction consists of 3 things: inputs, outputs, and transaction kernels.
- Input is denoted by its Pedersen (switch) commitment. The host can create it for any
CoinID
, since only blinding factor image is needed, andP-Kdf
is exported freely. - Output consists of Pedersen (switch) commitment and the rangeproof. The commitment is calculated by the host (as for inputs), and the rangeproof is calculated by the HW wallet without any restriction.
So, both inputs and outputs can be created by the host without any restrictions. But to build a valid (balanced) MW transaction the corresponding transaction kernel is required, and this is where HW wallet restrictions come into play. As we mentioned, HW wallet requests user permission along with the verifiable recipient ID for any spend transaction.
For all transactions the host specifies the input/output CoinID
s that belong to the user before/after the transaction. Based on them the HW wallet deduces:
- Overall value and asset which is obtained/lost in a transaction
- excess blinding factor (diff of input/output blinding factors).
The transaction type is verified according to (1), whereas the signed kernel balances the transaction according to (2). As we mentioned, by blinding factor depends on the CoinID
in an opaque way, hence there's no feasible way to substitute different inputs/outputs in the transaction after it was signed.
For all the mentioned transaction types the host supplies the following:
- List of input/output
CoinID
s. - Kernel lock height (min/max height)
- Kernel fee
Upon signing, as usual, the excess _blinding factor of the transaction is split into 2 parts in a pseudo-random way: one goes to the kernel commitment, the other goes to the offset (plain scalar). This is done for all the MW transactions for a better obfuscation.
Send and Receive are mutual transactions, that follow the following ritual:
- Sender creates its part of the kernel commitment, and sends it to the receiver
- Receiver adds its part of the commitment, signs it, and sends it back to the sender.
- Sender finishes its part.
Split transaction (where the user sends funds to itself) is signed in a single invocation.
This is the simplest case, as there's no additional transaction party. The HW wallet analyzes the input/output CoinID
s to make sure no funds "disappear". Means the user only spends the transaction fee.
If everything matches and the user authorizes it - the HW wallet generates the kernel blinding factor in a deterministic way, and then generates the kernel commitment + signature, and the appropriate offset.
As usual the HW wallet analyzes the input/output CoinID
s to make sure it receives funds.
Note: currently it's allowed to receive only one asset type at a time, i.e. exchanging different assets in the same transaction is currently not allowed (as right now we don't have appropriate PaymentProof
for this).
According to the ritual, by the time of invocation the sender has already produced its part. This includes the following:
- Kernel commitment
- Kernel signature "public nonce"
If everything matches - the HW wallet generates its part of the kernel blinding factor in a deterministic way, and then generates its part of the kernel commitment and signature, and the appropriate offset.
In addition to this, the HW wallet also signs the PaymentProof
. The Kernel ID, value and asset type are deduced automatically. The following additional parameters are specified by the host for the signature:
- Sender
Identity
- Self
IdentityNumber
, which corresponds to theIdentity
expected by the sender. The HW wallet generates the appropriate private key, and signs thePaymentProof
by it.
This is the most complex case. To sign the send transaction the HW wallet should be invoked twice (as can be seen according to the ritual). It also needs a nonce that should retain valid for the duration of the transaction.
When the HW wallet is invoked to sign the send transaction, the host, among other things, specifies the nonce slot which should be used to this transaction (the slot management, i.e. which ongoing transaction uses which slot - up to the host).
On the first invocation the HW wallet generates the kernel parameters (commitment and public nonce) based on the visible parameters and this nonce.
On the second invocation it verifies receiver part, verifies the PaymentProof
signed by the receiver, and asks for user permission to transfer the funds to this receiver. If everything is ok - the kernel is signed.
Note: just before signing, the HW wallet re-generates the value of the slot used in this transaction (re-generates its value in a deterministic way). This is to prevent malicious host make HW wallet sign different transactions using the same nonce.
Hds uses bulletproof as a rangeproof, which is computationally heavy for a device with limited capabilities. To improve the performance the host and HW wallet perform a multi-party computation. The HW wallet participates only where the blinding factor is necessary.
However, apart from being valid, the generated bulletproof must also be detectable by the user OwnerKey
. This is obviously unacceptable if a malicious host can generate an output which would be "invisible" to the user after the transaction.
Because of this there's a considerable amount of computation that HW wallet needs to do. In particular it needs to calculate a multi-exponentiation of 129 generators. Yet it's only a fairly small fraction of the overall computation.
Normally wallets negotiate over BBS system, each has a BBS address key, by which it tries to decrypt all the BBS traffic and chase the messages intended to it. Then during the transaction the BBS public address is also treated as the peer Identity
, i.e. the sender expects the receiver to sign the PaymentProof
by it.
However for the HW wallet it's not feasible to decode all the BBS traffic. Hence we decided to separate the BBS and the Identity
. Now in order to send/receive funds the users exchange Tokens (over secure channel), which optionally include both the BBS address and the Identity
.
Conceptually the BBS address belongs to the entity you are directly talking to, whereas the Identity
belongs to the final sender/recipient of the funds. In other words, the software wallet is a proxy, which may be compromised, whereas still there is a confirmation that the final recipient gets all the funds.