Transaction Signing
In a transparent blockchain, a signer can inspect the transaction to be signed to validate the contents are what the signer expects. However, in a shielded blockchain, the contents of the transaction are opaque. Ideally, using a private blockchain would enable a user to sign a transaction while also understanding what they are signing.
To avoid the blind signing problem, in the Penumbra protocol we allow the user
to review a description of the transaction - the TransactionPlan
- prior to
signing. The TransactionPlan
contains a declarative description of all details of the proposed transaction, including a plan of each action in a transparent
form, the fee specified, the chain ID, and so on. From this plan, we authorize the and build the transaction. This has the additional advantage of allowing the signer to authorize the
transaction while the computationally-intensive Zero-Knowledge
Proofs (ZKPs) are optimistically generated as part of the transaction build process.
The signing process first takes a TransactionPlan
and SpendKey
and returns
the AuthorizationData
, essentially a bundle of signatures over
the effect hash, which can be computed directly from the plan data. You can
read more about the details of the effect hash computation below.
The building process takes the TransactionPlan
, generates the proofs, and constructs a fully-
formed Transaction
. This process is internally partitioned into three steps:
- Each
Action
is individually built based to its specification in theTransactionPlan
. - The pre-built actions to collectively used to construct a transaction with placeholder dummy signatures, that can be filled in once the
signatures from the
AuthorizationData
are ready1. This intermediate state of the transaction without the full authorizing data is referred to as the “Unauthenticated Transaction”. - Slot the
AuthorizationData
to replace the placeholder signatures to assemble the finalTransaction
.
The Penumbra protocol was designed to only require the custodian, e.g. the hardware wallet environment, to do signing, as the generation of ZKPs can be done without access to signing keys, requiring only witness data and viewing keys.
A figure showing how these pieces fit together is shown below:
╔════════════════════════╗
║ Authorization ║
║ ║
║┌──────────────────────┐║
║│ Spend authorization │║
║│ key │║ ┌───────────────────┐
║└──────────────────────┘║ │ │
║ ║───▶│ AuthorizationData │──┐
║ ║ │ │ │
║┌──────────────────────┐║ └───────────────────┘ │
║│ EffectHash │║ │
║└──────────────────────┘║ │
║ ║ │
║ ║ │
╚════════════▲═══════════╝ │
│ │
│ │ ┌───────────┐
┌───────────┴───────────┐ │ │ │
│ │ └─────────┬────▶│Transaction│
│ TransactionPlan │ | │ │
│ │ │ └───────────┘
└───────────┬───────────┘ │
│ │
│ │
│ │
╔═══════════▼════════════╗ │
║ Proving ║ │
║ ║ │
║┌──────────────────────┐║ │
║│ WitnessData │║ │
║└──────────────────────┘║ ┌──────────────────────────────┐ │
║ ║ │ │ │
║ ╠──▶│ Unauthenticated Transaction ├─┘
║┌──────────────────────┐║ │ │
║│ Full viewing key │║ └──────────────────────────────┘
║└──────────────────────┘║
║ ║
║ ║
║ ║
╚════════════════════════╝
Transactions are signed used the decaf377-rdsa
construction. As described briefly in that section, there are two signature domains used in Penumbra: SpendAuth
signatures and Binding
signatures.
SpendAuth
Signatures
SpendAuth
signatures are included on each Spend
and DelegatorVote
action
(see Multi-Asset Shielded Pool and Governance
for more details on Spend
and DelegatorVote
actions respectively).
The SpendAuth
signatures are created using a randomized signing key and the corresponding randomized verification key provided on the action. The purpose of the randomization is to prevent linkage of verification keys across actions.
The SpendAuth
signature is computed using the decaf377-rdsa
Sign
algorithm
where the message to be signed is the effect hash of the entire transaction
(described below), and the decaf377-rdsa
domain is SpendAuth
.
Effect Hash
The effect hash is computed over the effecting data of the transaction, which following the terminology used in Zcash2:
“Effecting data” is any data within a transaction that contributes to the effects of applying the transaction to the global state (results in previously-spendable coins or notes becoming spent, creates newly-spendable coins or notes, causes the root of a commitment tree to change, etc.).
The data that is not effecting data is authorizing data:
“Authorizing data” is the rest of the data within a transaction. It does not contribute to the effects of the transaction on global state, but allows those effects to take place. This data can be changed arbitrarily without resulting in a different transaction (but the changes may alter whether the transaction is allowed to be applied or not).
For example, the nullifier on a Spend
is effecting data, whereas the
proofs or signatures associated with the Spend
are authorizing data.
In Penumbra, the effect hash of each transaction is computed using the BLAKE2b-512 hash function. The effect hash is derived from the proto-encoding of the action - in cases where the effecting data and authorizing data are the same, or the body of the action - in cases where the effecting data and authorizing data are different. Each proto has a unique string associated with it called its Type URL, which is included in the inputs to BLAKE2b-512. Type URLs are variable length, so a fixed-length field (8 bytes) is first included in the hash to denote the length of the Type URL field.
Summarizing the above, the effect hash for each action is computed as:
effect_hash = BLAKE2b-512(len(type_url) || type_url || proto_encode(proto))
where type_url
is the bytes of the variable-length Type URL, len(type_url)
is the length of the Type URL encoded as 8
bytes in little-endian byte order, proto
represents the proto used to represent
the effecting data, and proto_encode
represents encoding the proto message as
a vector of bytes. In Rust, the Type URL is found by calling type_url()
on the protobuf
message.
All transaction data field effect hashes, such as the Fee
, MemoCiphertext
, and TransactionParameters
, as well as the per-action effect hashes, are computed using this method.
Transaction Effect Hash
To compute the effect hash of the entire transaction, we combine the hashes of the individual fields in the transaction body. First we include the fixed-sized effect hashes of the per-transaction data fields: the transaction parameters eh(tx_params)
, fee eh(fee)
, (optional) detection data eh(detection_data)
, and (optional) memo eh(memo)
which are derived as described above. Then, we include the number of actions and the fixed-size effect hash of each action a_0
through a_j
. Combining all fields:
effect_hash = BLAKE2b-512(len(type_url) || type_url || eh(tx_params) || eh(fee) || eh(memo) || eh(detection_data) || j || eh(a_0) || ... || eh(a_j))
where the type_url
is the variable-length Type URL of the transaction body message, and len(type_url)
is the length of that string encoded as 8 bytes in little-endian byte order.
Test vectors for the effect hash computation for 100 randomly generated TransactionPlan
s are available here. You can also use a tool in that same repository to re-generate those test vectors or generate additional random test vectors via:
cargo test -- --ignored --test generate_transaction_signing_test_vectors
Binding
Signature
The Binding
signature is computed once on the transaction level.
It is a signature over a hash of the authorizing data of that transaction, called the auth hash. The auth hash of each transaction is computed using the BLAKE2b-512 hash function over the proto-encoding of the entire TransactionBody
.
The Binding
signature is computed using the decaf377-rdsa
Sign
algorithm
where the message to be signed is the auth hash as described above, and the
decaf377-rdsa
domain is Binding
. The binding signing key is computed using the random blinding factors for each balance commitment.
At this final stage we also generate the last signature: the binding signature, which can only be added
once the rest of the transaction is ready since it is computed over the proto-encoded TransactionBody
.
https://github.com/zcash/zips/issues/651