This issue describes the basic wallet interface which we should implement for the testnet. It could evolve into a reference wallet implementation, though, at this stage it is rather simplistic.
The interface defines 4 methods:
receive_asset
send_asset
auth_tx
set_pub_key
The first two of the above methods should probably be an interface on their own, and we should recommend that most accounts implement these methods.
The goals is to provide a wallet with the following capabilities
- The wallet is controlled by a single key. The signature scheme is assumed to be Falcon.
- However, sending assets to the wallet does not require knowing which signature scheme is used by the recipient.
- The user can send, receive, and exchange assets stored in the wallet with other users. All operations (including receiving assets) must be authenticated by the account owner.
- The user can update the public key associated with the account as long as they are still in possession of the current private key.
Not supported in this implementation:
- Multi-sig wallets.
- Ability to create notes with multiple assets.
The implementation probably could go into Miden lib, maybe under miden::wallets::simple
namespace - but there could be other options as well.
Interface method description
Below, we provide high-level details about each of the interface methods.
receive_asset
method
The purpose of this method is to add a single asset to an account's vault. Pseudo-code for this method could look like so:
receive_asset(asset)
self.add_asset(asset)
end
In the above, add_asset
is a kernel procedure miden::sat::account::add_asset
described in #3 (comment).
Note: this method does not increment account nonce. The nonce will be incremented in auth_tx
method described below. Thus, receiving assets requires authentication.
send_asset
method
The purpose of this method is to create a note which sends a single asset to the specified recipient. Pseudo-code for this method could look like so:
send_asset(asset, recipient)
self.remove_asset(asset)
tx.create_note(recipient, asset)
end
In the above, remove_asset
is a kernel procedure miden::sat::account::remove_asset
and create_note
is a kernel procedure miden::sat::tx::create_note
, both described in #3 (comment).
recipient
is a partial hash of the created note computed outside the VM as hash(hash(hash(serial_num), script_hash), input_hash)
. This allows computing note hash as hash(recipient, vault_hash)
where the vault_hash
can be computed inside the VM based on the specified asset.
Note: this method also does not increment account nonce. The nonce will be incremented in auth_tx
method described below. Thus, sending assets requires authentication.
auth_tx
method
The purpose of this method is to authenticate a transaction. For the purposes of this method we make the following assumptions:
- Public key of the account is stored in account storage at index 0.
- To authenticate a transaction we sign
hash(account_id || account_nonce || input_note_hash || output_note_hash)
using Falcon signature scheme.
Pseudo-code for this method could look like so:
auth_tx()
# compute the message to sign
let account_id = self.get_id()
let account_nonce = self.get_nonce()
let input_notes_hash = tx.get_input_notes_hash()
let output_notes_hash = tx.get_output_notes_hash()
let m = hash(account_id, account_nonce, input_notes_hash, output_notes_hash)
# get public key from account storage and verify signature
let pub_key = self.get_item(0)
falcon::verify_sig(pub_key, m)
# increment account nonce
self.increment_nonce()
end
It is assumed that the signature for falcon::verify_sig
procedure will be provided non-deterministically via the advice provider. Thus, the above procedure can succeed only if the prover has a valid Falcon signature over hash(account_id || account_nonce || input_note_hash || output_note_hash)
for the public key stored in the account.
All procedures invoked as a part of this method, except for falcon::verify_sig
have equivalent kernel procedures described in #3 (comment). We assume that falcon::verify_sig
is a part of Miden standard library.
Open question: should the signed message be different? For example, maybe we should include hash of the entire account state (initial and final) into the message hash as well?
set_pub_key
method
The purpose of this method is to rotate an account's public key (i.e., replace the current key with a new value). For the purposes of this method we make the following assumptions:
- Public key of the account is stored in account storage at index 0.
- To authenticate the update we sign
hash(account_id || account_nonce || old_key || new_key)
using Falcon signature scheme.
Pseudo-code for this method could look like so:
set_pub_key(new_key)
# compute message to sign
let account_id = self.get_id()
let account_nonce = self.get_nonce()
let old_key = self.get_item(0)
let m = hash(account_id, account_nonce, old_key, new_key)
# verify signature
falcon::verify_sig(old_key, m)
# update the key at storage location 0 to a new value
self.set_item(0, new_key)
# increment account nonce
self.increment_nonce()
end
It is assumed that the signature for falcon::verify_sig
procedure will be provided non-deterministically via the advice provider. Thus, the above procedure can succeed only if the prover has a valid Falcon signature over hash(account_id || account_nonce || old_key || new_key)
for the public key stored in the account.
All procedures invoked as a part of this method, except for falcon::verify_sig
have equivalent kernel procedures described in #3 (comment). We assume that falcon::verify_sig
is a part of Miden standard library.
Usage examples
Examples of using the above interface are described below.
Receiving funds
To receive funds into an account we'd need a note which invokes receive_asset
method. Script for this note could look something like this (this is actually identical to P2ID script):
note_script()
let target_account_id = self.get_input(0)
assert(account.get_id() == target_account_id)
for asset in self.get_assets()
account.receive_asset(asset)
end
end
The above script assumes that the recipient account ID is specified via note inputs.
In addition to the note, transaction consuming it would need to have a tx script which invokes auth_tx
method like so:
tx_script()
account.auth_tx()
end
To execute this transaction, the user will need to provide a signature over hash(account_id || account_nonce || input_note_hash || output_note_hash)
against the public key stored in the account.
Sending funds
To send funds from an account we'd need to create a transaction which invokes send_asset
method as a part of its tx script. Tx script for such a transaction could look like so:
tx_script()
account.send_asset(<asset1>, <recipient1>)
account.send_asset(<asset2>, <recipient2>)
account.auth_tx()
end
To execute this transaction, the user will need to provide a signature over hash(account_id || account_nonce || input_note_hash || output_note_hash)
against the public key stored in the account.
Swapping assets
We can also combine receive_asset
and send_asset
methods to execute an atomic swap. A script for a note involved in the swap could look like so:
note_script()
let target_account_id = self.get_input(0)
assert(account.get_id() == target_account_id)
account.receive_asset(<asset1>)
account.send_asset(<asset2>, <recipient>)
end
In the above case, asset1
, asset2
, and recipient
are hardcoded into the note script. Anyone consuming this note will add asset1
into their account and will have to create carrying asset2
addressed to the specified recipient
.
To consume this note in a transaction, the transaction will also need to include a tx script which looks something like this:
tx_script()
account.auth_tx()
end
To execute this transaction, the user will need to provide a signature over hash(account_id || account_nonce || input_note_hash || output_note_hash)
against the public key stored in the account.
Updating public key
To update public key of an account, we'd need to create a transaction which invokes set_pub_key
method as a part of its tx script. Tx script for such a transaction could look like so:
tx_script()
account.set_pub_key(<new_key>)
end
To execute this transaction, the user will need to provide a signature over hash(account_id || account_nonce || old_key || new_key)
against the public key stored in the account prior to the update.