Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dispatch Lockdrop account - Precompile #1142

Merged
merged 22 commits into from
Feb 7, 2024
Merged

Conversation

PierreOssun
Copy link
Member

@PierreOssun PierreOssun commented Jan 18, 2024

Pull Request Summary

Added a precompile dispatch-lockdrop that expose dispatch_lockdrop_call function in order to dispatch calls on behalf of SS58 AccountId of the lockdrop account (in the same way it was derived in eth-sig).

The precompile is only accessible from EOA and proceed to some checks before disptaching the call.

It used it's own CallFilter (allow the same call as Dispatch)

@PierreOssun PierreOssun marked this pull request as draft January 18, 2024 10:44
@PierreOssun PierreOssun added the runtime This PR/Issue is related to the topic “runtime”. label Jan 18, 2024
@PierreOssun PierreOssun changed the title Claim Lockdrop account - Precompile Dispatch Lockdrop account - Precompile Jan 23, 2024
@PierreOssun PierreOssun marked this pull request as ready for review January 26, 2024 12:52
Copy link
Member

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went over the implementation and the approach seems good to me 👍
Left some comments.

Will do a more detailed review once others chip in.

Copy link
Member

@ashutoshvarma ashutoshvarma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After going through the code I realised few things,

  • Instead of taking the whole signature (signed message) as input and extract the pub key inside the precompile, we should just take a pub key as input and just add validation of keccak_256(pub_key)[..20] = evm_caller.
  • That way we don't have to build EIP signature payload, verify signature and extract the secp256k1 key inside the precompile code which we can't properly benchmark.
  • The UI can handle this very easily in one line using existing eth libraries like ethers.js - recoverPublicKey (do note that UI already has the logic to produce payload and signature before hand, so it's like preventing double work also)

Copy link
Member

@ashutoshvarma ashutoshvarma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! except for some minor comments

I couldn't find .sol interface file for the precompile and overlooked in my previous review. The original dispatch precompile take only one input (encoded call) and without a method selector (fallback routine) so it was fine for it, but for our precompile IMO it is needed as we take more than one input and has explicit function selector.(dispatch_lockdrop_call(bytes,bytes))

Comment on lines 99 to 101
return Err(PrecompileFailure::Error {
exit_status: ExitError::Other(message.into()),
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Err(PrecompileFailure::Error {
exit_status: ExitError::Other(message.into()),
});
return Err(revert("caller does not match the public key");

Comment on lines 141 to 145
Err(PrecompileFailure::Error {
exit_status: ExitError::Other(
format!("dispatch execution failed: {}", <&'static str>::from(e)).into(),
),
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use revert() here as well to keep it consistent

let caller: H160 = handle.context().caller.into();
let input: Vec<u8> = call.into();

// 1. Decode the call
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how expensive all these operations are (ignoring the call dispatch), but I'd suggest to add some static cost at the beginning, that's always charged. Sort of a deterrent to prevent spam attack and continued use of this precompile.

Correct me if I'm wrong, but we just want users to get their funds out and continue using "normal" ways of interacting with the chain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How gas is handled:

  1. Check if call can be made (not exceed gas limit)
  2. Charge external cost: call weight + hash weight
  3. If call is dispatched:
  • Charge actual cost of the runtime call
  • Refund the difference between the actual cost and the estimated cost (not refunding the hash cost)
    So user only pays for the actual weight of the dispatched calls + overhead (2 hashing instructions)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I understand where we're misunderstanding each other.

I see two problems:

  1. Double charging
  2. Incorrect charging

Double charging

The first one, double charging, happens because as you outlined your steps:
2. Charge external cost
3.1. Charge actual cost
3.2. Refund the initial charged weight

After the 2nd step, you have double charged.
It's true that you refund it later, but at that point the total charged amount might have exceeded what would otherwise be good enough good limit if it weren't for temporary double charging.

Incorrect charging

Later on, when you call:

let actual_weight = post_info.actual_weight.unwrap_or(info.weight);
let cost = Runtime::GasWeightMapping::weight_to_gas(actual_weight);
handle.record_cost(cost)?;

you will actually ignore the PoV part of the weight.

fn weight_to_gas(weight: Weight) -> u64 {
    weight.div(T::WeightPerGas::get().ref_time()).ref_time()
}

This is the approach used by try_dispatch:

    pub fn try_dispatch<Call>(
        handle: &mut impl PrecompileHandle,
        origin: <Runtime::RuntimeCall as Dispatchable>::RuntimeOrigin,
        call: Call,
    ) -> Result<PostDispatchInfo, TryDispatchError>
    where
        Runtime::RuntimeCall: From<Call>,
    {
        let call = Runtime::RuntimeCall::from(call);
        let dispatch_info = call.get_dispatch_info();

        Self::record_weight_v2_cost(handle, dispatch_info.weight).map_err(TryDispatchError::Evm)?;

        // Dispatch call.
        // It may be possible to not record gas cost if the call returns Pays::No.
        // However while Substrate handle checking weight while not making the sender pay for it,
        // the EVM doesn't. It seems this safer to always record the costs to avoid unmetered
        // computations.
        let post_dispatch_info = using_precompile_handle(handle, || call.dispatch(origin))
            .map_err(|e| TryDispatchError::Substrate(e.error))?;

        Self::refund_weight_v2_cost(
            handle,
            dispatch_info.weight,
            post_dispatch_info.actual_weight,
        )
        .map_err(TryDispatchError::Evm)?;

        Ok(post_dispatch_info)
    }
}

Suggestion

  1. At the very beginning of the call you charge the hardcoded/fixed value to cover for all the decoding, hashing, etc. This is purely computation (ref_time, gas), no PoV is involved.
  2. When you get to the part when it's time to dispatch the call, use the approach from try_dispatch, which records both weight components.

Copy link
Member

@ashutoshvarma ashutoshvarma Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Double Charging: I don't see any purpose for that record_cost call, it can be removed completely as it doesn't really make sense to double charge ref_time.
  2. Incorrect charging: We should probably charge a hefty fee from users using this precompile, as Dino said we just want users to get their funds out and continue using "normal" ways of interacting with the chain,. Charging a big fee will surely discourage people using it for other purposes.

Copy link
Member

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked the changes, and they look good to me!
Left just one comment about charged ref time.

Suggestion - add an unit test or two to make sure expected weight is charged.
Given how much discussions we had about it, it would be good to have a UT ensure it's behaving as we now expect 🙂


// Derive the account id from the public key
let origin = Self::get_account_id_from_pubkey(pubkey.as_bytes())
.ok_or(revert("could not derive AccountId from pubkey"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to cover this case in unit tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a tests for it. Using the same pubkey & AccountId as from old pallet eth-sig

Copy link

github-actions bot commented Feb 5, 2024

Code Coverage

Package Line Rate Branch Rate Health
primitives/src 62% 0%
precompiles/sr25519/src 64% 0%
pallets/inflation/src 86% 0%
pallets/xc-asset-config/src 64% 0%
pallets/dapps-staking/src/pallet 86% 0%
precompiles/unified-accounts/src 100% 0%
precompiles/xcm/src 72% 0%
pallets/dapps-staking/src 90% 0%
precompiles/assets-erc20/src 81% 0%
pallets/dapp-staking-v3/src 88% 0%
chain-extensions/types/xvm/src 0% 0%
precompiles/dapps-staking/src 94% 0%
pallets/xvm/src 51% 0%
pallets/dapp-staking-v3/src/benchmarking 98% 0%
precompiles/dapp-staking-v3/src/test 0% 0%
pallets/astar-xcm-benchmarks/src 89% 0%
precompiles/xvm/src 74% 0%
chain-extensions/types/assets/src 0% 0%
chain-extensions/unified-accounts/src 0% 0%
pallets/astar-xcm-benchmarks/src/generic 100% 0%
pallets/astar-xcm-benchmarks/src/fungible 100% 0%
chain-extensions/pallet-assets/src 56% 0%
pallets/dapp-staking-v3/src/test 0% 0%
pallets/dapp-staking-migration/src 49% 0%
pallets/dynamic-evm-base-fee/src 92% 0%
primitives/src/xcm 66% 0%
pallets/block-rewards-hybrid/src 91% 0%
pallets/unified-accounts/src 84% 0%
precompiles/dispatch-lockdrop/src 86% 0%
pallets/ethereum-checked/src 75% 0%
precompiles/substrate-ecdsa/src 74% 0%
chain-extensions/xvm/src 0% 0%
precompiles/dapp-staking-v3/src 90% 0%
pallets/static-price-provider/src 58% 0%
pallets/collator-selection/src 90% 0%
chain-extensions/types/unified-accounts/src 0% 0%
pallets/dapp-staking-v3/rpc/runtime-api/src 0% 0%
Summary 79% (4400 / 5563) 0% (0 / 0)

Minimum allowed line rate is 50%

Copy link
Member

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did another check, LGTM! 👍

Copy link
Member

@ashutoshvarma ashutoshvarma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@PierreOssun PierreOssun merged commit 4b76262 into master Feb 7, 2024
8 checks passed
@PierreOssun PierreOssun deleted the feat/evm-claim-au branch February 7, 2024 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
runtime This PR/Issue is related to the topic “runtime”.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants