0xPolygonMiden / miden-base

Core components of the Polygon Miden rollup
MIT License
73 stars 45 forks source link

Rework procedure authentication #934

Closed Fumuran closed 3 weeks ago

Fumuran commented 1 month ago

Packages versions

miden-base: 0.5.0 branch: next

Bug description

The problem appeared during the testing of Foreign Procedure Invocation.

Context:

~The native account have some "foreign" procedures, which could be used to get some information from the foreign account. Foreign account, on the other hand, potentially could have no procedures at all.~

Edit: my understanding of how FPI works was not correct. The foreign procedure is a part of the foreign account, so during the invocation of execute_foreign_procedure we pass there the hash of the procedure in the foreign account, not native.

To make a successful call to the foreign account it should be provided to the merkle store (and then later will be loaded to the memory).

The bug occurs on the stage of the procedure authentication (here).

The authenticate_procedure procedure works like this

What goes wrong

How can this be reproduced?

Call the foreign procedure which is provided in the native account against the foreign account where this procedure is not provided.

Relevant log output

called Result::unwrap() on an Err value: TransactionExecutorError(ExecuteTransactionProgramFailed(FailedAssertion { clk: RowIndex(3215), err_code: 131083, err_msg: Some("Account procedure is not part of the account code") }))
bobbinth commented 1 month ago

If I understood correctly, we have some incorrect behavior when native and foreign accounts have different interfaces (i.e., expose a different set of procedures).

I think the issue may be related to how we build ProcedureIndexMap in the transaction host. Specifically, right now we build it only for the native account (see here). So, when a foreign account requests an index for a procedure that it has, unless the native account has exactly the same set of procedures, the return info would be wrong and the foreign procedure will fail to authenticate.

Assuming this is the issue, the fix should be to make the ProcedureIndexMap more general and able to handle procedures for multiple accounts. The trickiest thing would be figuring out how to let transaction host know about the foreign accounts (right now this info lives only in the advice provider). One way is to modify TransactionArgs to include AccountHeaders (or maybe even just code roots) for all foreign accounts. But I'm not sure this is the best way yet.

cc @igamigo as this will also be relevant to how the client provides this info during transaction execution.

igamigo commented 1 month ago

I think I'm a bit confused. Originally when testing on the client as I hit this issue the problem was as follows:

called `Result::unwrap()` on an `Err` value: TransactionExecutorError(ExecuteTransactionProgramFailed(EventError("Account procedure with root 0x1ef1a9cc88bb5808d9eef908cc79c29ff84c065be7d037d5adf34ead18691810 is not in the advice provider")))
called `Result::unwrap()` on an `Err` value: TransactionExecutorError(ExecuteTransactionProgramFailed(FailedAssertion { clk: RowIndex(3215), err_code: 131083, err_msg: Some("Account procedure is not part of the account code") }))

EDIT: I force pushed to my branch and erased commit history. Hopefully the scenarios are understood without looking at the code but otherwise feel free to let me know and we can repro it.

I think this might have the same root cause (not familiar with ProcedureIndexMap), but since the errors are a bit different I thought I'd clarify what I was hitting just in case.

bobbinth commented 1 month ago

My hunch is that this is all due to the issue I described above - just different manifestations of it, and the errors seems to be consistent with it. The question though is how to fix it. One option (which I mentioned above), is to modify TransactionArgs to include headers of foreign accounts. It could look something like this:

pub struct TransactionArgs {
    tx_script: Option<TransactionScript>,
    foreign_accounts: Vec<AccountHeader>,
    note_args: BTreeMap<NoteId, Word>,
    advice_inputs: AdviceInputs,
}

Or if we wanted to keep extra data to the minimum, we could just track foreign account code commitments - but I'm not sure it is worth it.

Then, in the transaction host we'd build the correct map for all accounts and things should work (hopefully).

On the client side, we'd just need to add headers (which I think we already have anyway) to the args and things should work there too.

bobbinth commented 1 month ago

One other option is to introduce a ForeignAccountHeader struct. This could look something like this:

pub struct ForeignAccountHeader {
    account: AccountHeader,
    storage: AccountStorageHeader,
    code: AccountCodeHeader,
}

Where AccountCodeHeader would be a new struct that could look like so:

pub struct AccountCodeHeader {
    commitment: Digest,
    procedures: Vec<AccountProcedureInfo>,
}

Then, TransactionArgs could be:

pub struct TransactionArgs {
    tx_script: Option<TransactionScript>,
    foreign_accounts: Vec<ForeignAccountHeader>,
    note_args: BTreeMap<NoteId, Word>,
    advice_inputs: AdviceInputs,
}

This would mean that the client would just need to populate foreign_accounts with the data it gets from the node.

I think one remaining question is how we actually get account code loaded into the MastForestStore of the transaction executor. Maybe we need to add a separate method on the executor to load code - but I haven't thought through this yet.

bobbinth commented 3 weeks ago

Closed by #940.