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

[language] introduce InternalGasUnits and fix gas check in the adapter #7448

Merged
merged 1 commit into from
Feb 10, 2021

Conversation

bob-wilson
Copy link
Contributor

This fixes a bug (#7238) where a transaction incorrectly fails to validate in the adapter when the transaction's max_gas_amount is less than the min_transaction_gas_units gas constant. The bug is that the comparison does not take into account the gas_unit_scaling_factor gas constant. To help avoid similar bugs in the future, this introduces a new InternalGasUnits type that is distinct from the existing GasUnits type. Both are newtype wrappers around u64 so BCS-serialized values should be interchangeable between these two types (e.g., in the on-chain gas cost table).

Motivation

Fixes #7238

Have you read the Contributing Guidelines on pull requests?

Yes

Test Plan

Updated tests

Related PRs

#7239

Copy link
Contributor

@tzakian tzakian left a comment

Choose a reason for hiding this comment

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

Thanks for fixing this! The changes look good to me except for the two places in the tests.

@@ -301,11 +301,11 @@ fn verify_simple_payment() {
.script(Script::new(
p2p_script.clone(),
vec![account_config::xus_tag()],
vec![TransactionArgument::U64(42); MAX_TRANSACTION_SIZE_IN_BYTES as usize],
vec![TransactionArgument::U8(42); MAX_TRANSACTION_SIZE_IN_BYTES as usize],
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if I quite follow this change. Is this to make sure that max transaction size is getting checked before the bytecode verifier is being run?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing complicated like that. I should have left a comment in the PR description to explain. This code is just padding out the size of the transaction to trigger the EXCEEDED_MAX_TRANSACTION_SIZE error. If you want to increase the size by MAX_TRANSACTION_SIZE_IN_BYTES, then you should add that many bytes. Adding a vector of that many U64 values adds 8x more space than necessary.

p2p_script.clone(),
vec![account_config::xus_tag()],
args.clone(),
vec![TransactionArgument::U8(42); extra_txn_bytes as usize],
Copy link
Contributor

Choose a reason for hiding this comment

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

A bit puzzled here as well about the switch to U8 from the U64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above -- this is just padding out the size of the transaction by extra_txn_bytes

Comment on lines 257 to 261
let mut extra_txn_bytes = 0;
if gas_constants.gas_unit_scaling_factor > gas_constants.min_transaction_gas_units.get() {
extra_txn_bytes = gas_constants.large_transaction_cutoff.get()
+ (gas_constants.gas_unit_scaling_factor / gas_constants.intrinsic_gas_per_byte.get());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit (personal preference, so ignore if you prefer the current way):

Suggested change
let mut extra_txn_bytes = 0;
if gas_constants.gas_unit_scaling_factor > gas_constants.min_transaction_gas_units.get() {
extra_txn_bytes = gas_constants.large_transaction_cutoff.get()
+ (gas_constants.gas_unit_scaling_factor / gas_constants.intrinsic_gas_per_byte.get());
}
let extra_txn_bytes = if gas_constants.gas_unit_scaling_factor > gas_constants.min_transaction_gas_units.get() {
extra_txn_bytes = gas_constants.large_transaction_cutoff.get()
+ (gas_constants.gas_unit_scaling_factor / gas_constants.intrinsic_gas_per_byte.get());
} else {
0
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had considered that, but my preference is not at all strong. I don't mind changing it.

@bob-wilson
Copy link
Contributor Author

/land

@bors-libra bors-libra moved this from In Review to Queued in bors Feb 4, 2021
@bors-libra bors-libra moved this from Queued to Testing in bors Feb 4, 2021
bors-libra pushed a commit that referenced this pull request Feb 4, 2021
This fixes a bug (#7238) where a transaction incorrectly fails to validate
in the adapter when the transaction's max_gas_amount is less than the
min_transaction_gas_units gas constant. The bug is that the comparison does
not take into account the gas_unit_scaling_factor gas constant. To help
avoid similar bugs in the future, this introduces a new InternalGasUnits
type that is distinct from the existing GasUnits type. Both are newtype
wrappers around u64 so BCS-serialized values should be interchangeable
between these two types (e.g., in the on-chain gas cost table).

Closes: #7448
@bors-libra
Copy link
Contributor

💔 Test Failed - ci-test-success

@bors-libra bors-libra moved this from Testing to In Review in bors Feb 4, 2021
@github-actions
Copy link

github-actions bot commented Feb 4, 2021

Cluster Test Result

Compatibility test results for land_fdff1f0e ==> land_ea4b44ca (PR)
1. All instances running land_fdff1f0e, generating some traffic on network
2. First validator land_fdff1f0e ==> land_ea4b44ca, to validate storage
3. First batch validators (14) land_fdff1f0e ==> land_ea4b44ca, to test consensus
4. Second batch validators (15) land_fdff1f0e ==> land_ea4b44ca, to upgrade rest of the validators
5. All full nodes (30) land_fdff1f0e ==> land_ea4b44ca, to finish the network upgrade
all up : 933 TPS, 4881 ms latency, 5600 ms p99 latency, no expired txns
Logs: http://kibana.ct-0-k8s-testnet.aws.hlw3truzy4ls.com/app/kibana#/discover?_g=(time:(from:'2021-02-04T19:40:47Z',to:'2021-02-04T20:04:10Z'))
Dashboard: http://grafana.ct-0-k8s-testnet.aws.hlw3truzy4ls.com/d/performance/performance?from=1612467648000&to=1612469050000
Validator 1 logs: http://kibana.ct-0-k8s-testnet.aws.hlw3truzy4ls.com/app/kibana#/discover?_g=(time:(from:'2021-02-04T19:40:47Z',to:'2021-02-04T20:04:10Z'))&_a=(columns:!(log),query:(language:kuery,query:'kubernetes.pod_name:"val-1"'),sort:!(!('@timestamp',desc)))

Repro cmd:

./scripts/cti --tag land_fdff1f0e --cluster-test-tag land_ea4b44ca -E RUST_LOG=debug -E BATCH_SIZE=15 -E UPDATE_TO_TAG=land_ea4b44ca --report report.json --suite land_blocking_compat

🎉 Land-blocking cluster test passed! 👌

@bob-wilson
Copy link
Contributor Author

Sigh. The ir-testsuite/tests/testing_infra/gas_submitted_too_low.mvir test is failing. I can't wait to get rid of this technical debt from the IR tests! The history of this test is instructive. It was originally set up to check for an account with an insufficient balance to cover the transaction's max gas amount, and that is what the comment in the test still says. But, way back in June 2019, it was changed to expect a failure with MaxGasUnitsBelowMinTransactionGasUnits instead of InsufficientBalanceForTransactionFee. IMO that was a mistake -- the test was at the time not specifying the max gas field in the transaction, and the change in question changed the max gas to default to the account balance. That dropped the max gas below the minimum gas fee and completely changed what the test was checking. The test continued failing when we introduced gas scaling only because of the bug being fixed here. Since we've already got coverage for this in the verify_txn test, I'm just going to delete this redundant and obsolete IR test. @vgao1996 are you OK with that?

Copy link
Contributor

@vgao1996 vgao1996 left a comment

Choose a reason for hiding this comment

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

@bob-wilson yeah deleting the test sounds good to me and thanks for the accurate write up on the history of this test!

Copy link
Contributor

@vgao1996 vgao1996 left a comment

Choose a reason for hiding this comment

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

Actually wait a sec, there may be additional issues we need to address. I ran the following tests with your changes applied and the results look a little bit weird

Test 1: balance (0) < actual gas usage (9) < max gas specified (200)

//! account: Alice, 0

// this sets up Alice's account with very low balance, which is not sufficient to cover the gas cost

//! sender: Alice
//! max-gas: 200
main() {
    let x: u64;
    let y: u64;

    x = 3;
    y = 5;

    assert(move(x) + move(y) == 8, 42);
    return;
}

// check: MAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS

Output:

Output(TransactionOutput(TransactionOutput { write_set: WriteSet(WriteSetMut { write_set: [(AccessPath { address: a4a46d1b1421502568a4a6ac326d7250, path: 01000000000000000000000000000000010b4469656d4163636f756e740b4469656d4163636f756e7400 }, Value(20975741f6b37b591141e96e0c60a57ce8a4a46d1b1421502568a4a6ac326d725001a4a46d1b1421502568a4a6ac326d725001a4a46d1b1421502568a4a6ac326d72500000000000000000180000000000000000a4a46d1b1421502568a4a6ac326d72500000000000000000180000000000000000a4a46d1b1421502568a4a6ac326d72500100000000000000))] }), events: [], gas_used: 9, status: Keep(EXECUTED) }))

Shouldn't it be OUT_OF_GAS or ?

Test 2: max-gas < actual usage (9)

//! account: Alice, 0

// this sets up Alice's account with very low balance, which is not sufficient to cover the gas cost

//! sender: Alice
//! max-gas: 5
main() {
    let x: u64;
    let y: u64;

    x = 3;
    y = 5;

    assert(move(x) + move(y) == 8, 42);
    return;
}

// check: MAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS

Output:

thread 'run_test::a.mvir' panicked at 'Unexpected offsets. major_status: OUT_OF_GASsub_status: Nonelocation: Undefinedoffsets: []', language/vm/src/errors.rs:69:17
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
   1: std::panicking::begin_panic_fmt
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:437
   2: vm::errors::VMError::into_vm_status
             at /Users/vgao/libra/language/vm/src/errors.rs:69
   3: diem_vm::diem_vm::charge_global_write_gas_usage::{{closure}}
             at /Users/vgao/libra/language/diem-vm/src/diem_vm.rs:540
   4: core::result::Result<T,E>::map_err
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:595
   5: diem_vm::diem_vm::charge_global_write_gas_usage
             at /Users/vgao/libra/language/diem-vm/src/diem_vm.rs:538
   6: diem_vm::diem_transaction_executor::DiemVM::execute_script
             at /Users/vgao/libra/language/diem-vm/src/diem_transaction_executor.rs:187
   7: diem_vm::diem_transaction_executor::DiemVM::execute_user_transaction
             at /Users/vgao/libra/language/diem-vm/src/diem_transaction_executor.rs:284
   8: diem_vm::diem_transaction_executor::DiemVM::execute_block_impl
             at /Users/vgao/libra/language/diem-vm/src/diem_transaction_executor.rs:656
   9: diem_vm::diem_transaction_executor::DiemVM::execute_block_and_keep_vm_status
             at /Users/vgao/libra/language/diem-vm/src/diem_transaction_executor.rs:721
  10: language_e2e_tests::executor::FakeExecutor::execute_block_and_keep_vm_status
             at /Users/vgao/libra/language/testing-infra/e2e-tests/src/executor.rs:226
  11: functional_tests::evaluator::run_transaction
             at /Users/vgao/libra/language/testing-infra/functional-tests/src/evaluator.rs:419
  12: functional_tests::evaluator::eval_transaction
             at /Users/vgao/libra/language/testing-infra/functional-tests/src/evaluator.rs:626
  13: functional_tests::evaluator::eval
             at /Users/vgao/libra/language/testing-infra/functional-tests/src/evaluator.rs:752
  14: functional_tests::testsuite::functional_tests
             at /Users/vgao/libra/language/testing-infra/functional-tests/src/testsuite.rs:326
  15: testsuite::run_test
             at ./tests/testsuite.rs:64
  16: datatest_stable::runner::Requirements::expand::{{closure}}::{{closure}}
             at /Users/vgao/libra/common/datatest-stable/src/runner.rs:323
  17: <alloc::boxed::Box<F> as core::ops::function::Fn<A>>::call
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1056
  18: datatest_stable::runner::run_test::{{closure}}::{{closure}}
             at /Users/vgao/libra/common/datatest-stable/src/runner.rs:172
  19: core::ops::function::FnOnce::call_once
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:227
  20: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:308
  21: std::panicking::try::do_call
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:381
  22: ___rust_try
  23: std::panicking::try
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:345
  24: std::panic::catch_unwind
             at /Users/vgao/.rustup/toolchains/1.48.0-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:382
  25: datatest_stable::runner::run_test::{{closure}}
             at /Users/vgao/libra/common/datatest-stable/src/runner.rs:172

Not sure if these are real bugs in the VM or just things not handled properly by the testing infra yet. Will study them more. It's also weird that some other tests (e.g. out_of_gas_in_nested_module.move) that throws OUT_OF_GAS error do pass.

@bors-libra
Copy link
Contributor

❗ Invalid command

Bors help and documentation

Bors is a Github App used to manage merging of PRs in order to ensure that CI is always green. It does so by maintaining a Merge Queue. Once a PR reaches the head of the Merge Queue it is rebased on top of the latest version of the PR's base-branch (generally master) and then triggers CI. If CI comes back green the PR is then merged into the base-branch. Regardless of the outcome, the next PR is the queue is then processed.

General

  • This project's Merge Queue can be found here.
  • This project requires PRs to be Reviewed and Approved before they can be queued for merging.
  • Before PR's can be merged they must be configured with "Allow edits from maintainers" enabled. This is needed for Bors to be able to update PR's in-place so that Github can properly recognize and mark them as "merged" once they've been merged into the upstream branch.

Commands

Bors actions can be triggered by posting a comment which includes a line of the form /<action>.

Command Action Description
Land land, merge attempt to land or merge a PR
Canary canary, try canary a PR by performing all checks without merging
Cancel cancel, stop stop an in-progress land
Cherry Pick cherry-pick <target> cherry-pick a PR into <target> branch
Priority priority set the priority level for a PR (high, normal, low)
Help help, h show this help message

Options

Options for Pull Requests are configured through the application of labels.

          Option           Description
label: bors-high-priority Indicates that the PR is high-priority. When queued the PR will be placed at the head of the merge queue.
label: bors-low-priority Indicates that the PR is low-priority. When queued the PR will be placed at the back of the merge queue.
label: bors-squash Before merging the PR will be squashed down to a single commit, only retaining the commit message of the first commit in the PR.

@vgao1996
Copy link
Contributor

vgao1996 commented Feb 9, 2021

Looks like my first example is due to gas-price defaulting to 0 in functional tests. Definitely a footgun.

The second example is caused by

impl VMError {
    pub fn into_vm_status(self) -> VMStatus {
        let VMError {
            major_status,
            sub_status,
            location,
            mut offsets,
            ..
        } = self;
        match (major_status, sub_status, location) {
            (StatusCode::EXECUTED, sub_status, _) => {
                debug_assert!(sub_status.is_none());
                VMStatus::Executed
            }
            (StatusCode::ABORTED, Some(code), Location::Script) => {
                VMStatus::MoveAbort(vm_status::AbortLocation::Script, code)
            }
            (StatusCode::ABORTED, Some(code), Location::Module(id)) => {
                VMStatus::MoveAbort(vm_status::AbortLocation::Module(id), code)
            }

            (StatusCode::ABORTED, sub_status, location) => {
                debug_assert!(
                    false,
                    "Expected a code and module/script location with ABORTED, but got {:?} and {}",
                    sub_status, location
                );
                VMStatus::Error(StatusCode::ABORTED)
            }

            // TODO Errors for OUT_OF_GAS do not always have index set
            (major_status, sub_status, location)
                if major_status.status_type() == StatusType::Execution =>
            {
                debug_assert!(
                    offsets.len() == 1,
                    "Unexpected offsets. major_status: {:?}\
                    sub_status: {:?}\
                    location: {:?}\
                    offsets: {:#?}",
                    major_status,
                    sub_status,
                    location,
                    offsets
                );
                let abort_location = match location {
                    Location::Script => vm_status::AbortLocation::Script,
                    Location::Module(id) => vm_status::AbortLocation::Module(id),
                    Location::Undefined => {
                        return VMStatus::Error(major_status);
                    }
                };
                let (function, code_offset) = match offsets.pop() {
                    None => {
                        return VMStatus::Error(major_status);
                    }
                    Some((fdef_idx, code_offset)) => (fdef_idx.0, code_offset),
                };
                VMStatus::ExecutionFailure {
                    status_code: major_status,
                    location: abort_location,
                    function,
                    code_offset,
                }
            }

            (major_status, _, _) => VMStatus::Error(major_status),
        }
    }

Seems like it's exactly what the TODO says we fail to address: OUT_OF_GAS do not always have index set

@bob-wilson
Copy link
Contributor Author

Thanks for investigating those. Maybe file a GitHub issue for problem 2? It sounds like this change is still OK then, right?

@vgao1996
Copy link
Contributor

vgao1996 commented Feb 9, 2021

Yeah I'll file an issue for problem 2. Let me see if I can reproduce this on master (with modified code).

There might be another issue this PR may have to address: rounding when converting from internal units to external units.
The latest failing test (functional_testsuite::diem/prologue/fail_if_max_gas_below_minimum.move) is a good example. According to our current gas constants, the minimum transaction fee is 600 internal units, and when converted to external units it is divided by 1000 and rounded down to 0. Is it problematic?

@bob-wilson
Copy link
Contributor Author

There might be another issue this PR may have to address: rounding when converting from internal units to external units.
The latest failing test (functional_testsuite::diem/prologue/fail_if_max_gas_below_minimum.move) is a good example. According to our current gas constants, the minimum transaction fee is 600 internal units, and when converted to external units it is divided by 1000 and rounded down to 0. Is it problematic?

I don't think we need to round, but even if we wanted to, it can't be changed without breaking compatibility, so it would need to be done under control of some versioned flag. This PR does not affect how much gas is charged -- it just fixes the inconsistency in the validation check.

I believe we definitely should increase the minimum transaction fee above the scaling factor, though (but not as part of this PR -- that has to be done by modifying the on-chain gas schedule).

@bob-wilson
Copy link
Contributor Author

I don't know why I didn't see that fail_if_max_gas_below_minimum.move failure earlier. I don't see a good way to fix the test until we adjust the gas schedule, and since we've already got good coverage for this error elsewhere, I propose to delete that test.

Copy link
Contributor

@vgao1996 vgao1996 left a comment

Choose a reason for hiding this comment

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

Agreed that rounding cannot be changed without breaking compatibility. Let's worry about this later.

I'm fine with deleting the test or disabling it and leaving a TODO reminding us to re-enable it in the future.

@bob-wilson
Copy link
Contributor Author

/land

@bors-libra bors-libra moved this from In Review to Queued in bors Feb 10, 2021
bors-libra pushed a commit that referenced this pull request Feb 10, 2021
This fixes a bug (#7238) where a transaction incorrectly fails to validate
in the adapter when the transaction's max_gas_amount is less than the
min_transaction_gas_units gas constant. The bug is that the comparison does
not take into account the gas_unit_scaling_factor gas constant. To help
avoid similar bugs in the future, this introduces a new InternalGasUnits
type that is distinct from the existing GasUnits type. Both are newtype
wrappers around u64 so BCS-serialized values should be interchangeable
between these two types (e.g., in the on-chain gas cost table).

Closes: #7448
@bors-libra bors-libra moved this from Queued to Testing in bors Feb 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug] account can't send any txn if its balance is less than 600
5 participants