Is there a convenience function for state access list creation from transaction data?

The Transaction Validity Rules in the specs describe the data fields which comprise a transaction’s state access list. For example, it states that a write-destroy access list is comprised of the UTXO ID for each Coin and Contract input, or message ID for each Message input.

Is there a convenience function in the Rust SDK that parses a transaction and returns the state access list that follows the rules defined by the specification? Thanks!

Pinging Rust SDK / client team for you internally.

1 Like

On the SDK side, the state-access list is implied through the inputs/outputs of a transaction, there is no explicit representation of an access list.
Could you provide some more details on your use-case? Maybe there is a way we can better support it.

The context is that I’m writing a function that groups together transactions based on this implied state access list.

I ran into an issue working with the SDK and a local provider. I’m trying to simulate a case where two transactions in the same block update the same contract storage variable. When I use a ScriptTransactionBuilder to create a tx that calls a contract function that writes to contract storage, the utxo_id in the tx’s inputs are zeroed (UtxoId { tx_id: 0000000000000000000000000000000000000000000000000000000000000000, output_index: 0 })

However, when these txs are sent to the provider, and a block is produced, and I retrieve the txs from that block, the utxo_ids are no longer zeros - the UtxoId.tx_id values are nonzero. It seems that the local node somehow modifies the transaction input UTXOs after they are received by the provider.

I’ll follow up with sample code that demonstrates the problem.

That is unusual. ScriptTransactionBuilder may at one point use temporary utxos with a zero id to enable gas estimation, but this should at no point be visible on your end. I’ll look into the sample code once you share it.

As for the access list, you would need to filter transaction inputs for Input::Contract and look at the utxo_id field.

Something like this should do the trick, provided you’re only interested in the accessed contracts:

let contracts_accessed: Vec<UtxoId> = tx.inputs().iter().filter_map(|input| {
        match input {
            Input::Contract(c) => Some(c.utxo_id),
            _ => None,
        }
    }).collect();
1 Like

Here’s the code: GitHub - weijiekoh/fuel_tx_inputs

It uses the call_handler.transaction_builder()` to construct a transaction, and prints its inputs. It then submits the transaction to the provider. Next, it fetches the transaction from the provider, and prints its inputs.

The output shows that the utxo_id values of the initially printed tx are all 0.

tx inputs: [Contract(Contract { utxo_id: UtxoId { tx_id: 0000000000000000000000000000000000000000000000000000000000000000, output_index: 0 }, balance_root: 0000000000000000000000000000000000000000000000000000000000000000, state_root: 0000000000000000000000000000000000000000000000000000000000000000, tx_pointer: TxPointer { block_height: 00000000, tx_index: 0 }, contract_id: 7f4aa1caeb68a5c768b0a56085e38f3b6c0db67346f0504af5141c35978e5ce1 }), CoinSigned(Coin { utxo_id: UtxoId { tx_id: 876623495f0f2d32a0d6303adc9aac61f1b1a026609635d88c0da4e2ea88aeb2, output_index: 1 }, owner: 09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db, amount: 100, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, tx_pointer: TxPointer { block_height: 00000000, tx_index: 0 }, witness_index: 0, predicate_gas_used: Empty, predicate: Empty, predicate_data: Empty })]

fetched tx inputs: [Contract(Contract { utxo_id: UtxoId { tx_id: 876623495f0f2d32a0d6303adc9aac61f1b1a026609635d88c0da4e2ea88aeb2, output_index: 0 }, balance_root: 0000000000000000000000000000000000000000000000000000000000000000, state_root: dab5d0b9b43773eab24ef7b9413d36f849c9627fd37882a52927685e8e1c3163, tx_pointer: TxPointer { block_height: 00000001, tx_index: 0 }, contract_id: 7f4aa1caeb68a5c768b0a56085e38f3b6c0db67346f0504af5141c35978e5ce1 }), CoinSigned(Coin { utxo_id: UtxoId { tx_id: 876623495f0f2d32a0d6303adc9aac61f1b1a026609635d88c0da4e2ea88aeb2, output_index: 1 }, owner: 09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db, amount: 100, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, tx_pointer: TxPointer { block_height: 00000001, tx_index: 0 }, witness_index: 0, predicate_gas_used: Empty, predicate: Empty, predicate_data: Empty })]

Sorry for the confusion.

A contracts UTXO changes with every transaction. According to the the specs, the tx_id and output_index are to be set to zero so that the node will modify these fields with the right values once the tx get’s executed.
The only field that needs to be supplied is the contracts permanent contract_id.

To correct the snippet from above, you would need to filter for the contracts id field:

let contracts_accessed: Vec<UtxoId> = tx.inputs().iter().filter_map(|input| {
        match input {
            Input::Contract(c) => Some(c.contract_id),
            _ => None,
        }
    }).collect();

Let me know if you have further questions.

1 Like

Thanks for the explanation @mujkica!

To further clarify, is there ever a case where two separate transactions have inputs with the same utxo._idtx_id? For instance, will a node ever see such an occurrence whether the transactions are in the same block or in different blocks:

tx0: { ... inputs: [Contract{contract_id: "0001", uxto_id: {tx_id: "abcd...", output_index: 0}}, ...]}

tx1: { ... inputs: [Contract{contract_id: "0001", uxto_id: {tx_id: "abcd...", output_index: 1}}, ...]}

The specs (Transaction Validity Rules - Fuel Specifications) seem to imply that the UTXO ID for Contract inputs comprises a write-destroy access list. If, however, the UTXO ID (or rather, the utxo_id.tx_id) is unique among transactions’ inputs, then one way to group transactions by their state access is via their inputs’ contract_id field - is that correct? If so, what is the correct way to interpret the specs?

A couple further questions:

  1. Is it possible for a contract function to have nested calls to other contract functions, leading to state accesses of multiple contracts? e.g. a transaction that updates the state of both contract A and contract B? In such a case, do the IDs of both contract A and contract B show up in the transaction’s inputs field (inputs: [Contract{contract_id: "...a"}, Contract{contract_id: "...b"})?

  2. Is there any way to determine if two transactions that update the same contract will update non-overlapping storage slots, and if so, does that mean that these transactions can be executed in parallel?

Thanks again for your patience and for answering my questions!

For any contract that a transaction interacts with, it needs to have the corresponding InputContract in its inputs, and an OutputContract in its outputs. For InputContract we specify the contract_id and the node will determine and populate the utxo_id during execution. OutputContract has an inputIndex field which points to the corresponding InputContract. After transaction execution, the old utxo_id is invalid and the new one consists of the executed transactions id and the index of OutputContract in the outputs.

For your examples this means that there can be no such two transactions in different or the same block since the execution of the first transaction would change the tx_id part of the utxo_id.

The specs (Transaction Validity Rules - Fuel Specifications) seem to imply that the UTXO ID for Contract inputs comprises a write-destroy access list. If, however, the UTXO ID (or rather, the utxo_id.tx_id ) is unique among transactions’ inputs, then one way to group transactions by their state access is via their inputs’ contract_id field - is that correct? If so, what is the correct way to interpret the specs?

Yes, that is correct. I am not exactly sure why the decision was made to use the utxo_id instead of just relying on contract_id. Maybe someone from the client can provide more insight into this.

Answering your other questions:

  1. Yes, this is possible and the inputs must be included for the transaction to be valid. However, in that case, the SDK can’t tell what other calls will be invoked during execution. It is your responsibility to provide that information to the SDK, check:
    Calling other contracts - The Fuel Rust SDK
    Alternatively, you can ask the SDK to try and determine the right contract inputs, check:
    Transaction dependency estimation - The Fuel Rust SDK

  2. To my knowledge, no. Storage slots keys are not part of the access list. The state of the contract is represented through a single state root. Even if non-overlapping storage slots are updated, it would cause a change of the same state root.