Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 79 additions & 26 deletions docs-main/appdev/deep-dives/external-signing-hashing-algorithm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The hashing algorithm is tied to the protocol version of the synchronizer used t
|------------------|---------------------------|
| v34 | V2 |
| v35 | V2, V3 |
| dev | V2, V3, V4 |

### Transaction Nodes

Expand All @@ -60,11 +61,21 @@ message Node {
}
```

## V4

### General approach

V4 follows the same general hashing approach as V3. The V3 section below defines the common encoding used by V4 unless a V4-specific difference is stated.

### Changes from V3

- Exercise nodes with version dev include `external_call_results` in the prepared transaction hash. This binds the recorded external-call result payloads shown in prepared transaction review to the external party's signature.

## V3

### General approach

The hash of the `PreparedTransaction` is computed by encoding every protobuf field of the messages to byte arrays, and feeding those encoded values into a `SHA-256` hash builder. The rest of this section details how to deterministically encode every proto message into a byte array. Sometimes during the process, partially encoded results are hashed with SHA-256, and the resulting hash value serves as the encoding in messages further up. This is explicit when necessary.
The hash of the `PreparedTransaction` is computed by encoding the fields specified by this section to byte arrays, and feeding those encoded values into a `SHA-256` hash builder. The rest of this section details how to deterministically encode every proto message into a byte array. Sometimes during the process, partially encoded results are hashed with SHA-256, and the resulting hash value serves as the encoding in messages further up. This is explicit when necessary.

Big Endian notation is used for numeric values. Furthermore, protobuf numeric values are encoded according to their Java type representation. Refer to the official protobuf documentation for more information about protobuf to Java type mappings: [https://protobuf.dev/programming-guides/proto3/#scalar](https://protobuf.dev/programming-guides/proto3/#scalar) In particular:

Expand All @@ -89,7 +100,7 @@ Additionally, this is the java library used under the hood in Canton to serializ
> - These effectively replace a fixed ledger time with time bounds, allowing Daml Models to make assertions based on time without restricting the signing window as was required with a fixed set ledger time.

<Warning>
V3 introduces support for contract keys. Usage of contract keys in externally signed transactions requires usage of V3. Contract keys will not work on V2. Also note that V3 is only supported on protocol version 35.
V3 introduces support for contract keys. Usage of contract keys in externally signed transactions requires usage of V3. Contract keys will not work on V2. Also note that V3 is supported from protocol version 35 onwards.
</Warning>

### Notation and Utility Functions
Expand Down Expand Up @@ -566,34 +577,76 @@ fn encode_node(create):

#### Exercise

```
fn encode_node(exercise):
0x01 || # Node encoding version
encode(exercise.lf_version) || # Node LF version
0x01 || # Exercise node tag
encode_seed(find_seed(node.node_id).get) ||
encode(exercise.contract_id) ||
encode(exercise.package_name) ||
encode(exercise.template_id) ||
encode(exercise.signatories) ||
encode(exercise.stakeholders) ||
encode(exercise.acting_parties) ||
encode(exercise.interface_id) ||
encode(exercise.choice_id) ||
encode(exercise.chosen_value) ||
encode(exercise.consuming) ||
encode(exercise.exercise_result) ||
encode(exercise.choice_observers) ||
encode(exercise.children)
```
<Tabs>
<Tab title="V4">

```
fn encode_node(exercise):
encode(exercise.lf_version) || # Node LF version
0x01 || # Exercise node tag
encode_seed(find_seed(node.node_id).get) ||
encode(exercise.contract_id) ||
encode(exercise.package_name) ||
encode(exercise.template_id) ||
encode(exercise.signatories) ||
encode(exercise.stakeholders) ||
encode(exercise.acting_parties) ||
encode(exercise.interface_id) ||
encode(exercise.choice_id) ||
encode(exercise.chosen_value) ||
encode(exercise.consuming) ||
encode(exercise.exercise_result) ||
encode(exercise.choice_observers) ||
encode(exercise.by_key) ||
encode(exercise.key) ||
encode(exercise.external_call_results) || # new in V4
encode(exercise.children)
```

</Tab>
<Tab title="V3">

```
fn encode_node(exercise):
encode(exercise.lf_version) || # Node LF version
0x01 || # Exercise node tag
encode_seed(find_seed(node.node_id).get) ||
encode(exercise.contract_id) ||
encode(exercise.package_name) ||
encode(exercise.template_id) ||
encode(exercise.signatories) ||
encode(exercise.stakeholders) ||
encode(exercise.acting_parties) ||
encode(exercise.interface_id) ||
encode(exercise.choice_id) ||
encode(exercise.chosen_value) ||
encode(exercise.consuming) ||
encode(exercise.exercise_result) ||
encode(exercise.choice_observers) ||
encode(exercise.by_key) ||
encode(exercise.key) ||
encode(exercise.children)
```

</Tab>
</Tabs>

<Warning>
For Exercise nodes, the node seed **MUST** be defined. Therefore it is encoded as a **non** optional field, as noted via the `.get` in `find_seed(node.node_id).get`. If the seed of an exercise node cannot be found in the list of `node_seeds`, encoding must be stopped and it should be reported as a bug.
</Warning>

<Note>
The last encoded value of the exercise node is its `children` field. This recursively traverses the transaction tree.
</Note>
#### External call result

When V4 encodes an `external_call_results` element, the element is encoded as follows:

```
fn encode_external_call_result(result):
return encode(result.extension_id) ||
encode(result.function_id) ||
encode(result.config) ||
encode(result.input) ||
encode(result.output)
```

#### Fetch

Expand Down Expand Up @@ -682,7 +735,7 @@ Finally, compute the hash that needs to be signed to commit to the ledger change
```
fn encode(prepared_transaction):
0x00000030 || # Hash purpose
0x03 || # Hashing Scheme Version
hashing_scheme_version_byte || # e.g. 0x03 for V3, 0x04 for V4
hash(transaction) ||
hash(metadata)
```
Expand Down