protocol-substrate
                                
                                 protocol-substrate copied to clipboard
                                
                                    protocol-substrate copied to clipboard
                            
                            
                            
                        [SPEC] Proposals and their pallet flow
Types
- r_id (resource ID) - 32 byte string:
- Last 6 bytes reserved for chain id type (2 bytes chain type + 4 bytes chain identifier)
- Last adjacent 20 bytes are for ... (more on this in the next section).
 
More on r_id's
r_id's serve two purposes. Let's take the example of an anchor-update.
- There is a r_idto method name mapping calledResourceson thesignature-bridge.
- On the anchor-handlerpallet, there is a mapping calledAnchorListthat mapsr_id's totree_id's. This is how theupdatemethod knows which anchor to update. Therefore, ther_idshould contain some uniquely identifiable information about thetree_id(or thetree_iditself). Don't think putting just thetree_idwill be enough since it just an integer...may not be unique enough. Might want to do something like the world 'ANCHOR" concatenated with thetree_id.
Overarching Schema of Proposal Data
Proposal Data:
- r_id: ResourceId(32 bytes)
- zeroes:- [u8, 4](4 bytes)
- nonce: T::ProposalNonce(4 bytes)
- call(the length of this is proposal specific)
Terminology: We will call the (r_id, zeroes, nonce) as the proposal header. The proposal header is a total of 40 bytes. We add 4 bytes of zeroes padding to match the length of the EVM/Solidity proposal header. On the EVM/Solidity side, these 4 bytes are taken up by the functionSig.
See how execute_proposal works (including pseudocode) in the comment below.
List of Proposals
1. Set Wrapping Fee Proposal
Pallet Flow: bridge/signature-bridge -> token-wrapper-handler -> token-wrapper
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure: Call::TokenWrapperHandler(Call::execute_wrapping_fee_proposal { r_id, wrapping_fee_percent, into_pool_share_id })
- r_idis the- r_id
- wrapping_fee_percentis the new token wrapping fee
- into_pool_share_idis the- idof the pool share we are setting the wrapping fee for (recall we have wrapping fees per pool share)
2. Add Token to Pool Share Proposal
Pallet Flow: bridge/signature-bridge -> token-wrapper-handler -> token-wrapper -> asset-registry
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure: Call::TokenWrapperHandler(Call::execute_add_token_to_pool_share { r_id, name, asset_id })
- r_idis the- r_id
- nameis the name of the pool share
- asset_idis the- idof the asset being added to the pool share.
3. Remove Token from Pool Share Proposal
Pallet Flow: bridge/signature-bridge -> token-wrapper-handler -> token-wrapper -> asset-registry
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure: Call::TokenWrapperHandler(Call::execute_remove_token_from_pool_share { r_id, name, asset_id })
- r_idis the- r_id
- nameis the name of the pool share
- asset_idis the- idof the asset being removed from the pool share.
4. Anchor Create Proposal
Pallet Flow: bridge/signature-bridge -> anchor-handler -> anchor
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure: Call::AnchorHandler(Call::execute_anchor_create_proposal { deposit_size, src_chain_id, r_id, max_edges, tree_depth, asset })
- deposit_sizeis the denomination size of the anchor
- src_chain_idis the- idof the source chain
- r_idis- r_id
- max_edgesis the maximum number of anchors the created anchor can have an edge to
- tree_depthis the depth of the Merkle tree maintained by the anchor
- assetis the type of asset (- CurrencyIdOf) that can be deposited into the created anchor
5. Anchor Update Proposal
Pallet Flow: bridge/signature-bridge -> anchor-handler -> anchor
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure: Call::AnchorHandler(Call::execute_anchor_update_proposal { r_id, anchor_metadata: EdgeMetadata { src_chain_id, root, latest_leaf_index } })
- r_idis the- r_id
- src_chain_idis the- idof the source chain
- rootis the new root of the Merkle tree
- latest_leaf_indexis the index of the latest inserted leaf (corresponds to the updated- rootabove)
6. Register r_id Proposal
Pallet Flow: signature-bridge/bridge
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure:
7. Set fee recipient proposal
Pallet Flow: signature-bridge -> token-wrapper-handler -> token-wrapper
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure:
8. Rescue Tokens Proposal
Pallet Flow: signature-bridge -> token-wrapper-handler -> token-wrapper
Proposal Data: (r_id, zeroes, nonce, call)
Total Bytes: 40 + (size of call)
Call Structure:
Questions/Comments/Issues
- How many bytes is a call?
- How many bytes is a T::AssetId? It seems to just be an integer, so 4 bytes should be enough to cover the different asset types we will encounter.
Code examples would be useful here for parsing proposals and understanding how the verification will look from your perspective.
Another idea:
We can use your idea of signing the call and heavily simplify things in the following way.
The proposal data for all the proposals listed above is simply:
(r_id, nonce, call)
Total Bytes: 32 + 4 + (size of call) = 36 + (size of call)
The signature is the signature of this proposal data.
execute_proposal will then look like:
pub fn execute_proposal(
    origin: OriginFor<T>,
    src_id: T::ChainId,
    call: Box<<T as Config<I>>::Proposal>,
    proposal_data: Vec<u8>,
    signature: Vec<u8>,
) -> DispatchResultWithPostInfo {
    let _ = ensure_signed(origin)?;
    let r_id = parse_r_id_from_proposal_data(proposal_data);
    let nonce = parse_nonce_from_proposal_data(proposal_data);
    let call = parse_call_from_proposal_data(proposal_data); 
    
    ensure!(
        T::SignatureVerifier::verify(&Self::maintainer(), &proposal_data[..], &signature)
            .unwrap_or(false),
        Error::<T, I>::InvalidPermissions,
    );
    ensure!(Self::chain_whitelisted(src_id), Error::<T, I>::ChainNotWhitelisted);
    ensure!(Self::resource_exists(r_id), Error::<T, I>::ResourceDoesNotExist);
    // Ensure that the call and the r_id are consistent
    let call_method = parse_method_name_from_call(call);
    ensure!(call_method == Self::resources(r_id), Error::<T, I>::CallDoesNotMatchResourceId);
    // Ensure this chain id matches the r_id
    let execution_chain_id_type = parse_chain_id_type_from_r_id(r_id);
    ensure!(execution_chain_id_type == 
        T::ChainId::try_from(compute_chain_id_type(
        T::ChainIdentifier::get(),
        T::ChainType::get()
    )), Error<T,I>::IncorrectExecutionChainIdType);
    
 
    Self::finalize_execution(src_id, nonce, call)
}
The downside of this approach is it does not allow us to verify the data in the call against the proposal_data.
The only thing left to decide would be the structure of the r_id's.
I also mean we should sign the entire proposal data + call (as in the proposal data contains the encoded call as well)
Nonetheless, that looks like a much better starting implementation for these updates. I would continue with an implementation of this firstly and then we can begin adding the relevant proposal tests.
Related task list: https://github.com/webb-tools/protocol-substrate/issues/164