Witness Validation
Key Definitions
Witness: A witness is a piece of data that allows you to verify the authenticity of the transaction. There are many different witness types, depending on what the transaction requires, such as vkey witnesses, bootstrap witnesses, and script witnesses.
Vkey Witness: A vkey witness is a specific witness type. It is used to verify that the transaction has the authority to consume a UTxO(s) locked at the assosciated pkh address. It includes the signature and the vkey (instead of the key hash, which is encoded in the address).
Bootstrap Witness: A bootstrap witness is a specific witness type. It is used to verify that the transaction has the authority to consume a UTxO(s) locked at the associated Byron-era address. It includes the assosciated public key, the signature, the 32 byte chain code, and a set of attributes encoded in the address.
TODO: script witnesses
Process
Witness validation is the process of validating that all witnesses are:
- Valid signatures given the tranasaction hash and the provided public key.
- Present if required by the transaction. There cannot be any missing witnesses.
- TODO: script witness validation
The process of witness validation is described in section 12.2 of the Cardano Ledger Specification, in the UTXOW inference rules.
TODO: provide links to the relevent sections of code in Amaru and Haskell node
“Land Mine” #1: Bootstrap Witnesses
Context
After Shelley introduced the bootstrap_witness field to the transaction_witness_set. One may reasonably assume that if a Byron-era address is present, the assosciated witness would be found in the bootstrap_witness field. However, it is possible to construct a valid transaction such that it includes a Byron-era address, but the bootstrap_witness field is an empty list.
Solution
A node must consider the set of bootstrap root hashes and vkey hashes together when validating that there are no missing required witnesses, instead of isolating the handling of bootstrap addresses and BootstrapWitnesses from shelley-era addresses and VkeyWitnesses.
Example: 0c22edee0ffd7c8f32d2fe4da1f144e9ef78dfb51e1678d5198493a83d6cf8ec
Consider the following transaction on Preprod. In JSON, it looks like this:
{
"id": "0c22edee0ffd7c8f32d2fe4da1f144e9ef78dfb51e1678d5198493a83d6cf8ec",
"spends": "inputs",
"inputs": [
{
"transaction": {
"id": "4a0f0fd2ea2e91b34065e3085448b211afdcf72f9db0b2d74d1f99246e16c860"
},
"index": 1
},
{
"transaction": {
"id": "9157ee358b91c319a2e9dd087fe612d1c3d72d34fa4104bec13c8d37fd40b854"
},
"index": 1
}
],
"outputs": [
{
"address": "FHnt4NL7yPXtiYgxWx33wH6JXA9cYxzGAgVG1iMmaX9muBogARkHTRkUox4g4aR",
"value": {
"ada": {
"lovelace": 4832251
}
}
},
{
"address": "addr_test1vpfnhjud440uspylt4pewj7uy8tr0adh84sjqgmnq09xssca7lf4g",
"value": {
"ada": {
"lovelace": 5000000
}
}
}
],
"fee": {
"ada": {
"lovelace": 167749
}
},
"validityInterval": {},
"treasury": {},
"signatories": [
{
"key": "b1ef2a278ebe7cfd563c30f1bb642fb6b5616e040792527e6cd58f119895d657",
"signature": "4110259fb4433f462512d6fa69958070f9e365831962744e7e2e7a2f6a721a707ec4f213ff736fcc195490254dc9d22e0fe0552ae1781b965fc4d05bd5ebb304"
}
],
"cbor": "84a300828258204a0f0fd2ea2e91b34065e3085448b211afdcf72f9db0b2d74d1f99246e16c860018258209157ee358b91c319a2e9dd087fe612d1c3d72d34fa4104bec13c8d37fd40b85401018282582e82d818582483581c533bcb8dad5fc8049f5d43974bdc21d637f5b73d6120237303ca6843a1024101001a63bbc5a61a0049bbfb82581d60533bcb8dad5fc8049f5d43974bdc21d637f5b73d6120237303ca68431a004c4b40021a00028f45a10081825820b1ef2a278ebe7cfd563c30f1bb642fb6b5616e040792527e6cd58f119895d65758404110259fb4433f462512d6fa69958070f9e365831962744e7e2e7a2f6a721a707ec4f213ff736fcc195490254dc9d22e0fe0552ae1781b965fc4d05bd5ebb304f5f6"
}
Notably, there is only one signature, and it is a VkeyWitness. Examining the logic that collects required key hashes (getShelleyWitsVkeyNeededNoGov), the reason becomes apparent:
inputAuthors :: Set (KeyHash 'Witness)
inputAuthors = foldr' accum Set.empty (txBody ^. spendableInputsTxBodyF)
where
accum txin !ans =
case txinLookup txin utxo' of
Just txOut ->
case txOut ^. addrTxOutL of
Addr _ (KeyHashObj pay) _ -> Set.insert (asWitness pay) ans
AddrBootstrap bootAddr ->
Set.insert (asWitness (bootstrapKeyHash bootAddr)) ans
_ -> ans
Nothing -> ans
The Haskell node is inserting hashes from both AddrBootstrap and Addr into the same set, since they are both just hash digests of the same size. The hash digest inserted in the case of an AddrBootstrap is the bytes that are found at the first field of the BYRON_ADDRESS_PAYLOAD. In theory, this is the digest of the blake2b_224(sha3_256(BYRON_ADDRESS_ROOT)) and that should never be the same digest as a Addr vkey hash digest, so there would be no complexity introduced here. However one could, and clearly did, manually construct a Byron-era address where the bytes that are suppose to be the digest of the BYRON_ADDRESS_ROOT are instead the digest of a vkey. In that case, a vkey witness present with the assosciated public key would satifsy the required witness, even though that requirement came from an Byron-era bootstrap address.
tip
The provided solution already handles this case, but it’s worth noting that this could, theoretically, also apply the other direction. If one were to construct a shelley era address where the payment part is the digest of some BYRON_ADDRESS_ROOT instead of a verification key, they could have a BootstrapWitness present instead of a VkeyWitness.
Author’s note: I only just thought of this case while writing up this information and haven’t verified that it’s true. But, off the top of my head, there is no reason this wouldn’t be the case.