Skip to main content

ADR 0014: Signing Runtime Transactions with Hardware Wallet

Component

Oasis SDK

Changelog

  • 2023-02-24:
    • APDU: Define Oasis native and Ethereum-compatible address length.
  • 2023-02-09:
    • Encode Meta.runtime_id and Meta.orig_to as Base16.
    • Change SIG in SIGN_RT_SECP256K1 to 65-byte encoded R,S,V format.
  • 2023-01-23:
    • Fix Deoxys-II field description in Signing encrypted runtime transactions section.
    • Rename SIGN_PT_ instructions in APDUSPEC to SIGN_RT_ for consistency with oasis-core and oasis-sdk codebase.
  • 2023-01-03:
    • Add Sapphire runtime ID and consensus address on Mainnet.
  • 2022-12-13:
    • Fix Secp256k1 public key size.
  • 2022-10-12:
    • Add Sapphire runtime ID and consensus address on Testnet,
    • Remove redundant sig_context from Meta,
    • Require tx.call.format to be either 0 or 1.
  • 2022-07-15: Initial public version

Status

Proposed

Context

This document proposes additions to APDU specification, guidelines for parsing runtime transactions and general UI/UX for signing them on a hardware wallet:

  1. APDUSPEC additions
  2. Changes to Allowance transaction
  3. Signing general runtime transactions,
  4. Signing smart contract runtime transactions,
  5. Signing EVM runtime transactions.
  6. Signing encrypted runtime transactions,

Test vectors

Test vectors for all runtime transactions in this ADR can be generated by using gen_runtime_vectors tool as part of the Oasis SDK.

Runtime transaction format

The format of the runtime transaction to be signed by the hardware wallet is the following:

/// Transaction.
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub struct Transaction {
#[cbor(rename = "v")]
pub version: u16,

pub call: Call,

#[cbor(rename = "ai")]
pub auth_info: AuthInfo,
}

The transaction can be signed with Secp256k1 ("Ethereum"), Ed25519 key, or Sr25519 key! Information on this along with the gas fee is stored inside ai field.

call is defined as follows:

/// Method call.
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub struct Call {
/// Call format.
#[cbor(optional, default)]
pub format: CallFormat,
/// Method name.
#[cbor(optional, default, skip_serializing_if = "String::is_empty")]
pub method: String,
/// Method body.
pub body: cbor::Value,
/// Read-only flag.
///
/// A read-only call cannot make any changes to runtime state. Any attempt at modifying state
/// will result in the call failing.
#[cbor(optional, default, rename = "ro")]
pub read_only: bool,
}

If format is:

  • 0, the transaction is unencrypted,
  • 1, the transaction is encrypted,
  • any other, the transaction should be rejected with unsupported call format error unless implemented outside the scope of this ADR.

method contains the name of the runtime module followed by . and the method name. If format is 1, method is empty.

body contains a CBOR-encoded transaction. If format equals 1, body contains CBOR-encoded CallEnvelopeX25519DeoxysII which contains the encrypted transaction body inside its data field.

Decision

APDUSPEC additions

GET_ADDR_SECP256K1

Command
FieldTypeContentExpected
CLAbyte (1)Application Identifier0x05
INSbyte (1)Instruction ID0x04
P1byte (1)Request User confirmationNo = 0
P2byte (1)Parameter 2ignored
Lbyte (1)Bytes in payload(depends)
Path[0]byte (4)Derivation Path Data44
Path[1]byte (4)Derivation Path Data60
Path[2]byte (4)Derivation Path Data?
Path[3]byte (4)Derivation Path Data?
Path[4]byte (4)Derivation Path Data?

The first three items in the derivation path are hardened.

Response
FieldTypeContentNote
PKbyte (33)Public Key
ADDRbyte (40)Lower-case hex addr
SW1-SW2byte (2)Return codesee list of return codes

GET_ADDR_SR25519

Command
FieldTypeContentExpected
CLAbyte (1)Application Identifier0x05
INSbyte (1)Instruction ID0x03
P1byte (1)Request User confirmationNo = 0
P2byte (1)Parameter 2ignored
Lbyte (1)Bytes in payload(depends)
Path[0]byte (4)Derivation Path Data44
Path[1]byte (4)Derivation Path Data474
Path[2]byte (4)Derivation Path Data?
Path[3]byte (4)Derivation Path Data?
Path[4]byte (4)Derivation Path Data?

The first three items in the derivation path are hardened.

Response
FieldTypeContentNote
PKbyte (32)Public Key
ADDRbyte (46)Bech32 addr
SW1-SW2byte (2)Return codesee list of return codes

SIGN_RT_ED25519

Command
FieldTypeContentExpected
CLAbyte (1)Application Identifier0x05
INSbyte (1)Instruction ID0x05
P1byte (1)Payload desc0 = init
1 = add
2 = last
P2byte (1)----not used
Lbyte (1)Bytes in payload(depends)

The first packet/chunk includes only the derivation path.

All other packets/chunks should contain message to sign.

First Packet

FieldTypeContentExpected
Path[0]byte (4)Derivation Path Data44
Path[1]byte (4)Derivation Path Data474
Path[2]byte (4)Derivation Path Data?
Path[3]byte (4)Derivation Path Data?
Path[4]byte (4)Derivation Path Data?

Other Chunks/Packets

FieldTypeContentExpected
Databytes...Meta+Message

Data is defined as:

FieldTypeContentExpected
Metabytes..CBOR metadata
Messagebytes..CBOR data to sign
Response
FieldTypeContentNote
SIGbyte (64)Signature
SW1-SW2byte (2)Return codesee list of return codes

SIGN_RT_SECP256K1

Command
FieldTypeContentExpected
CLAbyte (1)Application Identifier0x05
INSbyte (1)Instruction ID0x07
P1byte (1)Payload desc0 = init
1 = add
2 = last
P2byte (1)----not used
Lbyte (1)Bytes in payload(depends)

The first packet/chunk includes only the derivation path.

All other packets/chunks should contain message to sign.

First Packet

FieldTypeContentExpected
Path[0]byte (4)Derivation Path Data44
Path[1]byte (4)Derivation Path Data60
Path[2]byte (4)Derivation Path Data?
Path[3]byte (4)Derivation Path Data?
Path[4]byte (4)Derivation Path Data?

Other Chunks/Packets

FieldTypeContentExpected
Databytes...Meta+Message

Data is defined as:

FieldTypeContentExpected
Metabytes..CBOR metadata
Messagebytes..CBOR data to sign
Response
FieldTypeContentNote
SIGbyte (65)SignatureR,S,V bigendian integers
SW1-SW2byte (2)Return codesee list of return codes

SIGN_RT_SR25519

Command
FieldTypeContentExpected
CLAbyte (1)Application Identifier0x05
INSbyte (1)Instruction ID0x06
P1byte (1)Payload desc0 = init
1 = add
2 = last
P2byte (1)----not used
Lbyte (1)Bytes in payload(depends)

The first packet/chunk includes only the derivation path.

All other packets/chunks should contain message to sign.

First Packet

FieldTypeContentExpected
Path[0]byte (4)Derivation Path Data44
Path[1]byte (4)Derivation Path Data474
Path[2]byte (4)Derivation Path Data?
Path[3]byte (4)Derivation Path Data?
Path[4]byte (4)Derivation Path Data?

Other Chunks/Packets

FieldTypeContentExpected
Databytes...Meta+Message

Data is defined as:

FieldTypeContentExpected
Metabytes..CBOR metadata
Messagebytes..CBOR data to sign
Response
FieldTypeContentNote
SIGbyte (64)Signature
SW1-SW2byte (2)Return codesee list of return codes

Meta parameter

Meta is a CBOR-encoded string → string map with the following fields:

  • runtime_id: Base16-encoded runtime ID (64-byte string)
  • chain_context: chain ID (64-byte string)
  • orig_to (optional): Base16-encoded ethereum destination address (40-byte string)

Changes to Allowance transaction

staking.Allow transaction already exists on the consensus layer. We propose the following improvement to the UI:

|     Type     > | <    To    > | <    Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <             > | <              |
| Allowance | <TO> | ROSE +-<AMOUNT> | ROSE <FEE> | <GAS LIMIT> | <NETWORK> | APPROVE | REJECT |
| | | | | | | | |

IMPROVEMENT: The hardware wallet renders the following literals in place of TO for specific NETWORK and addresses:

  • Network: Mainnet, To: oasis1qrnu9yhwzap7rqh6tdcdcpz0zf86hwhycchkhvt8Cipher
  • Network: Testnet, To: oasis1qqdn25n5a2jtet2s5amc7gmchsqqgs4j0qcg5k0tCipher
  • Network: Mainnet, To: oasis1qzvlg0grjxwgjj58tx2xvmv26era6t2csqn22pteEmerald
  • Network: Testnet, To: oasis1qr629x0tg9gm5fyhedgs9lw5eh3d8ycdnsxf0runEmerald
  • Network: Mainnet, To: oasis1qrd3mnzhhgst26hsp96uf45yhq6zlax0cuzdgcfcSapphire
  • Network: Testnet, To: oasis1qqczuf3x6glkgjuf0xgtcpjjw95r3crf7y2323xdSapphire

For more information on how the addresses above are derived from the runtime ID check the runtime accounts section.

Signing general runtime transactions

Deposit

We propose the following UI for consensus.Deposit runtime transaction:

|     Type     > | <   To (1/1)  > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Deposit | <MIXED_TO> | <SYM> <AMOUNT> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | |

MIXED_TO can either be oasis1 or the Ethereum's 0x address. If Meta does not contain orig_to field, render the tx.call.body.to value in oasis1 format in place of MIXED_TO. If Meta.orig_to is set, then:

  1. Check that the ethereum address stored in orig_to field maps to the native address in tx.call.body.to according to the reference implementation of the mapping.
  2. Render orig_to value in 0x format in place of MIXED_TO.

In addition, if tx.call.body.to is empty, then the deposit is made to the signer's account inside the runtime. In this case Self literal is rendered in place of MIXED_TO.

AMOUNT and FEE show the amount of tokens transferred in the transaction and the transaction fee. The number must be formatted according to the number of decimal places and showing a corresponding symbol SYM beside. These are determined by the following mapping hardcoded in the hardware wallet:

(Network, Runtime ID, Denomination) → (Number of decimals, SYM)

Denomination information is stored in tx.part.body.amount[1] or tx.ai.fee.amount[1] for the tokens transferred in the transaction or the fee respectively. Empty Denomination is valid and signifies the native token for the known networks and runtime IDs (see below).

The hardware wallet should have at least the following mappings hardcoded:

  • Network: Mainnet, runtime ID: Cipher, Denomination: "" → 9, ROSE
  • Network: Testnet, runtime ID: Cipher, Denomination: "" → 9, TEST
  • Network: Mainnet, runtime ID: Emerald, Denomination: "" → 18, ROSE
  • Network: Testnet, runtime ID: Emerald, Denomination: "" → 18, TEST
  • Network: Mainnet, runtime ID: Sapphire, Denomination: "" → 18, ROSE
  • Network: Testnet, runtime ID: Sapphire, Denomination: "" → 18, TEST

If the lookup fails, the following policy should be respected:

  1. SYM is rendered as empty string.
  2. The number of decimals is 18, if runtime ID matches any Emerald or Sapphire runtime on any network.
  3. Otherwise, the number of decimals is 9.

RUNTIME shows the 32-byte hex encoded runtime ID stored in Meta.runtime_id. If NETWORK matches Mainnet or Testnet, then human-readable version of RUNTIME is shown:

  • Network: Mainnet, runtime ID: 000000000000000000000000000000000000000000000000e199119c992377cbCipher
  • Network: Testnet, runtime ID: 0000000000000000000000000000000000000000000000000000000000000000Cipher
  • Network: Mainnet, runtime ID: 000000000000000000000000000000000000000000000000e2eaa99fc008f87fEmerald
  • Network: Testnet, runtime ID: 00000000000000000000000000000000000000000000000072c8215e60d5bca7Emerald
  • Network: Mainnet, runtime ID: 000000000000000000000000000000000000000000000000f80306c9858e7279Sapphire
  • Network: Testnet, runtime ID: 000000000000000000000000000000000000000000000000a6d1e3ebf60dff6cSapphire

SIGNATURE CONTEXT COMPUTATION: Chain domain separation context for runtime transactions beginning with oasis-runtime-sdk/tx: v0 for chain and followed by the hash derived from Meta.runtime_id and Meta.chain_context. See golang implementation for the reference implementation.

Withdraw

We propose the following UI for consensus.Withdraw method:

|     Type     > | <   To (1/1)  > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Withdraw | <TO> | <SYM> <AMOUNT> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | |

If tx.call.body.to is empty, then the withdrawal is made to the signer's consensus account. In this case Self literal is rendered in place of TO.

Transfer

We propose the following UI for the accounts.Transfer method:

|     Type     > | <   To (1/1)  > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Transfer | <MIXED_TO> | <SYM> <AMOUNT> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | |

Example

The user wants to deposit 100 ROSE to 0xDce075E1C39b1ae0b75D554558b6451A226ffe00 account on Emerald on the Mainnet. First they sign the deposit allowance transaction for Emerald.

|     Type     > | <    To    > | <   Amount   > | < Gas limit > | <     Fee     > | <  Network  > | <             > | <              |
| Allowance | Emerald | ROSE +100.0 | 1277 | ROSE 0.0 | Mainnet | APPROVE | REJECT |
| | Mainnet | | | | | | |

Next, they sign the runtime deposit transaction.

|     Type     > | <   To (1/2)  > | <    To (2/2)   > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Deposit | 0xDce075E1C39b1 | 451A226ffe00 | ROSE 100.0 | ROSE 0.0 | 11310 | Mainnet | Emerald | APPROVE | REJECT |
| (ParaTime) | ae0b75D554558b6 | | | | | | | | |

Then, they transfer some tokens to another account inside the runtime:

|     Type     > | <    To (1/2)  > | <    To (2/2)   > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Transfer | oasis1qpupfu7e2n | m8anj64ytrayne | ROSE 10.0 | ROSE 0.00015 | 11311 | Mainnet | Emerald | APPROVE | REJECT |
| (ParaTime) | 6pkezeaw0yhj8mce | | | | | | | | |

Finally, the user withdraws the remainder of tokens back to the Mainnet.

|     Type     > | <    To (1/2)  > | <    To (2/2)   > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Withdraw | oasis1qrec770vre | 504k68svq7kzve | ROSE 99.9997 | ROSE 0.00015 | 11311 | Mainnet | Emerald | APPROVE | REJECT |
| (ParaTime) | k0a9a5lcrv0zvt22 | | | | | | | | |

Signing smart contract runtime transactions

Uploading smart contract

contracts.Upload method will not be signed by the hardware wallet because the size of the Wasm byte code to sign may easily exceed the maximum size of the available encrypted memory.

Instantiating smart contract

We propose the following UI for the contracts.Instantiate method:

|  Review Contract > | < Code ID > | < Amount (1/1) > | < Data (1/1) > | ... | <    Fee    > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Instantiation | <CODE ID> | <AMOUNT...> | <DATA> | ... | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | ... | | | | | | |

DATA is a JSON-like representation of tx.call.body.data, if the latter is a CBOR-encoded map. If tx.call.body.data is empty or not present, then Data screen is hidden. If tx.call.body.data is in some other format, require blind signing mode and hide Data screen.

Blind signing means that the user does not see all contract information. In some cases - as is this - not even the amount or the contract address! When signing blindly, it is crucial that the user trusts the client application that it generated a non-malicious transaction!

AMOUNT... is the amount of tokens sent. Contract SDK supports sending multiple tokens at once, each with its own denomination symbol. The hardware wallet should render all of them, one per page. For rendering rules of each AMOUNT consult the runtime deposit behavior.

There can be multiple Data screens Data 1, Data 2, ..., Data N for each key in tx.call.body.data map. DATA can be one of the following types:

  • string
  • number (integer, float)
  • array
  • map
  • boolean
  • null

Strings are rendered as UTF-8 strings and the following characters need to be escaped: :, ,, }, ], .

Numbers are rendered in standard general base-10 encoding. Floats use decimal period and should be rendered with at least one decimal.

For strings and numbers that cannot fit a single page, a pagination is activated.

Boolean and null values are rendered as true, false and null respectively on a single page.

Array and map is rendered in form VAL1,VAL2,... and KEY1:VAL1,KEY1:VAL1,... respectively. For security, the items of the map must be sorted lexicographically by KEY. KEY and VAL can be of any supported type. If it is a map or array it is rendered as {DATA} or [DATA] respectively to avoid disambiguation. Otherwise, it is just DATA.

If the content of an array or a map cannot fit a single page, no pagination is introduced. Instead, the content is trimmed, ellipsis is appended at the end and the screen becomes confirmable. If the user double-clicks it, a subscreen for item n of an array or a map is shown. There is one subscreen for each item of the array or a map of size N titled Data n.1, Data n.2, ..., Data n.N which renders the item n as DATA for an array item or DATA:DATA for a map item:

|   Data 1.1 (1/1) > | < Data 1.2 (1/1) | < Data 1.3 (1/1) | ... | <          |
| <DATA> | <DATA> | <DATA> | | BACK |
| | | | | |

The recursive approach described above allows user to browse through a complete tree of data stracture (typically a request name along with the arguments) by using ⬅️ and ➡️ buttons, visit a child by double-clicking and returning to a parent node by confirming the BACK screen.

The maximum string length, the length of the array, the depth of a map must have reasonable limits on the hardware wallet. If that limit is exceeded, the hardware wallet displays an error on the initial screen. Then, if the user still wants to sign such a transaction, they need to enable blind signing.

The following UI is shown when blind-signing a non-encrypted transaction due to too complex function parameters.

|  Review Contract > | < BLIND > | < Instance ID (1/1) > | <   Amount    > | <     Fee     > | <  Network  > | <  ParaTime > | <            > | <             |
| Instantiation | SIGNING! | <INSTANCE ID> | <SYM> <AMOUNT> | <SYM> <FEE> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | |

Calling smart contract

The hardware wallet should show details of the runtime transaction to the user, when this is possible. We propose the following UI for the contracts.Call method:

| Review Contract > | < Instance ID > | < Amount (1/1) > | < Data (1/1) > | ... | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <              |
| Call | <INSTANCE ID> | <AMOUNT...> | <DATA> | ... | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | ... | | | | | | |

The Data screen behavior is the same as for contracts.Instantiate transaction.

Upgrading smart contracts

We propose the following UI for the contracts.Upgrade method:

|  Review Contract > | < Instance ID (1/1) > | < Amount (1/1) > | < New Code ID (1/1) > | < Data (1/1) > | ... | < ParaTime > | <     Fee     > | < Gas limit > | < Network > | < ParaTime > | <             > | <               |
| Upgrade | <INSTANCE ID> | <AMOUNT...> | <CODE_ID> | <DATA> | | <RUNTIME> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | | | | | |

The Data screen behavior is the same as for the contract instantiate transaction.

Example

To upload, instantiate and call the hello world example running on Testnet Cipher the user first signs the contract upload transaction with a file-based ed25519 keypair. The user obtains the Code ID 3 for the uploaded contract.

Next, the user instantiates the contract and obtains the Instance ID 2.

|  Review Contract > | < Code ID > | <  Amount  > | <      Data      > | <    Fee    > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <               |
| Instantiation | 3 | ROSE 0.0 | {instantiate:{init | ROSE 0.0 | 1348 | Mainnet | Cipher | APPROVE | REJECT |
| (ParaTime) | | | ial_counter:42}} | | | | | | | |

Finally, they perform a call to say_hello function on a smart contract passing the {"who":"me"} object as a function argument.

| Review Contract > | < Instance ID > | <  Amount  > | <      Data      > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <             > | <              |
| Call | 2 | ROSE 0.0 | {say_hello:{who:me | ROSE 0.0 | 1283 | Mainnet | Cipher | APPROVE | REJECT |
| (ParaTime) | | | }} | | | | | | |

For a complete example, the user can provide a more complex object:

{
"who": {
"username": "alice",
"client_secret": "e5868ebb4445fc2ad9f949956c1cb9ddefa0d421",
"last_logins": [1646835046, 1615299046, 1583763046, 1552140646],
"redirect": null
}
}

In this case the hardware wallet renders the following UI.

| Review Contract > | <  Instance ID  > | <   Amount  > | <      Data      > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime  > | <            > | <              |
| Call | 2 | ROSE 0.0 | {say_hello:{who:{u | ROSE 0.15 | 1283 | Mainnet | Cipher | APPROVE | REJECT |
| (ParaTime) | | | sername:alice,cli… | | | | | | |

V V

| Data 1 > | < |
| say_hello:{who:{us | BACK |
| ername:alice,clie… | |

V V

| Data 1.1 > | < |
| who:{username:alic | BACK |
| e,client_secret:[… | |

V V

| Data 1.1.1 > | < Data 1.1.2 (1/2) > | < Data 1.1.2 (2/2) > | < Data 1.1.3 > | < Data 1.1.4 > | < |
| username:alice | client_secret:e5868e | 1cb9ddefa0d421 | last_logins:[1646835 | redirect:null | BACK |
| | bb4445fc2ad9f949956c | | 046,1615299046,1583… | | |

V V

| Data 1.1.3.1 > | < Data 1.1.3.2 > | < Data 1.1.3.3 > | < Data 1.1.3.4 | < |
| 1646835046 | 1615299046 | 1583763046 | 1552140646 | BACK |
| | | | | |

Signing EVM runtime transactions

Creating smart contract

evm.Create method will not be managed by the hardware wallet because the size of the EVM byte code may easily exceed the wallet's encrypted memory size.

Calling smart contract

In contrast to contracts.Call, evm.Call method requires contract ABI and support for RLP decoding in order to extract argument names from tx.call.body.data. This is outside of the scope of this ADR and the blind signing, explicitly allowed by the user, is performed.

We propose the following UI:

| Review Contract > | < BLIND > | < Tx hash (1/1) > | < Address (1/1) > | <   Amount    > | <     Fee     > | < Gas limit > | <  Network  > | <  ParaTime > | <            > | <             |
| Call | SIGNING! | <TX_HASH> | <ADDRESS> | <SYM> <AMOUNT> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | | | |

TX_HASH is a hex representation of sha256 checksum of tx.call.body.data field.

ADDRESS is a hex-encoded address of the smart contract.

Signing encrypted runtime transactions

Encrypted transactions (tx.call.format == 1) contain call data inside the envelope's data field encrypted with Deoxys-II ephemeral key and X25519 key derivation.

The hardware wallet is not expected to implement any of these decryption schemes, neither it is safe to share the ephemeral key with anyone. Instead, the user should enable blind signing and the hardware wallet should show the hash of the encrypted call data, the public key and the nonce:

| Review Encrypted > | < BLIND > | < Tx hash (1/1) > | < Public key (1/1) > | <  Nonce (1/1) > | <    Fee    > | < Gas limit > | <  Network  > | <  ParaTime > | <             > | <             |
| Transaction | SIGNING! | <TX_HASH> | <PUBLIC_KEY> | <NONCE> | <SYM> <FEE> | <GAS LIMIT> | <NETWORK> | <RUNTIME> | APPROVE | REJECT |
| (ParaTime) | | | | | | | | | | |

TX_HASH is a hex representation of sha256 checksum of tx.call.body.data field.

PUBLIC_KEY is a hex representation of the 32-byte tx.call.body.pk field.

NONCE is a hex representation of the 15-byte tx.call.body.nonce field.

Since the transaction stored inside the tx.call.body.data field is encrypted, there is also no way to discriminate between the transactions, for example contracts.Call, contracts.Upgrade or evm.Call.

Consequences

Positive

Users will have a similar experience for signing runtime transactions on any wallet implementing this ADR.

Negative

For some transactions, user will need to trust the client application and use blind signing.

Neutral

Consideration of roothash.SubmitMsg transactions

This ADR does not propose a UI for generic runtime calls (roothash.SubmitMsg, see ADR 11). The proposed design in this ADR assumes a new release of the hardware wallet app each time a new runtime transaction type is introduced.

Signing contract uploads on hardware wallets

In the future perhaps, if only the merkle root hash of the Wasm contract would be contained in the transaction, signing such a contract could be feasible. See how Ethereum 2.x contract deployment is done using this approach.

Consideration of adding From screen

None of the proposed UIs and the existing implementation of signing the consensus transactions on Ledger show who is a signer of the transaction. The signer's from address can be extracted from tx.ai.si[0].address_spec.signature.<SIGNATURE TYPE> for oasis native address and if the signer wants to show the Ethereum address, Meta.orig_from should be populated and the hardware wallet should verify it before showing the tx.

References