-
Notifications
You must be signed in to change notification settings - Fork 99
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
feat(test): test opcode programs in different scenarios #808
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give it any better name? The "opcode diff places" has no meaning.
it translates as opcode in different (logical) places |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few suggestions after a quick review. I haven't checked files in the ./scenarios/ scenarios/
folder, but will do on the re-review.
b1af590
to
ff2eeff
Compare
b28f2ef
to
f7de36c
Compare
5d3dd84
to
940cb3e
Compare
57c4bd1
to
7b44f08
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More comments.
I feel like the programs should be dynamic based on the fork, and the approach could be improved in the sense that the program could be a pytest.fixture
that takes the fork as input and then resolves to the actual bytecode.
See the comment in the invalid_opcodes.py
file regarding the list of opcodes being invalid, since that is going to be changed frequently depending on the fork, it's going to be a pain to maintain, but we have the tools in pytest to do the maintenance more automatic and seamless.
combinations_input: ScenarioGeneratorInput = ScenarioGeneratorInput( | ||
fork=fork, | ||
pre=pre, | ||
operation_code=operation, | ||
external_address=external_address, | ||
external_balance=external_balance, | ||
) | ||
|
||
call_combinations = ScenariosCallCombinations(combinations_input).generate() | ||
for combination in call_combinations: | ||
if not debug.scenario_name or combination.name == debug.scenario_name: | ||
scenarios_list.append(combination) | ||
|
||
call_combinations = scenarios_create_combinations(combinations_input) | ||
for combination in call_combinations: | ||
if not debug.scenario_name or combination.name == debug.scenario_name: | ||
scenarios_list.append(combination) | ||
|
||
revert_combinations = scenarios_revert_combinations(combinations_input) | ||
for combination in revert_combinations: | ||
if not debug.scenario_name or combination.name == debug.scenario_name: | ||
scenarios_list.append(combination) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having the inputs as a separate structure that gets used in three different places is not a great pattern.
It seems to me like we should have a single class that can generate all types of combinations:
combinations_generator: ScenarioGenerator = ScenarioGenerator(
fork=fork,
pre=pre,
operation_code=operation,
external_address=external_address,
external_balance=external_balance,
)
for combination in combinations_generator.generate_call_combinations():
if not debug.scenario_name or combination.name == debug.scenario_name:
scenarios_list.append(combination)
for combination in combinations_generator.generate_create_combinations():
if not debug.scenario_name or combination.name == debug.scenario_name:
scenarios_list.append(combination)
for combination in combinations_generator.generate_revert_combinations():
if not debug.scenario_name or combination.name == debug.scenario_name:
scenarios_list.append(combination)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here generate_X is dynamic.
so ideally we iterate over array of functions and call each function with environment conditions.
and when we want to add new scenario generator it adds a function into that list
root_contract_balance = 105 | ||
scenario_contract_balance = 107 | ||
sub_contract_balance = 111 | ||
program_selfbalance = 113 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This specific property, program_selfbalance
, might be better off as a property of the operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here I calculate manually how much the selfbalance is supposed to be in this context. and pass it as expected result to verifier. if program return different value while checking selfbalance there will be an error
this is equvivalent to post state defenition
program_suicide, | ||
], | ||
) | ||
def test_scenarios( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_scenarios( | |
@pytest.mark.execute(pytest.mark.skip(reason="gas usage")) | |
def test_scenarios( |
This test has to be skipped in execute mode because it uses 500 million gas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should not be that much. I need to correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can make a test per scenario. right now all scenarios combined in one test for each program.
that would be a lot of test files though, but easier for debug
gas_limit=500_000_000, | ||
gas_price=tx_gasprice, | ||
to=runner_contract, | ||
data=b"0x11223344", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is what you meant:
data=b"0x11223344", | |
data=b"\x11\x22\x33\x44", |
The current one creates a byte array with the ascii equivalent of the "0x11223344" string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a handy constructor like BYTES("0x11223344") ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bytes.fromhex()
, example here: #1067 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see here is confusion
bytes.fromhex("11223344"),
prefix 0x is not allowed, can mistake with dec
invalid_opcode_ranges = [ | ||
range(0x0C, 0x10), | ||
range(0x1E, 0x20), | ||
range(0x21, 0x30), | ||
range(0x4B, 0x50), | ||
range(0xA5, 0xF0), | ||
range(0xF6, 0xFA), | ||
range(0xFB, 0xFD), | ||
range(0xFE, 0xFF), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this should not be a static list, and rather should be derived from the fork properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes this can be built depending on fork
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, but the programs does not know about fork. I would have to refactor all programs to take fork as an argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm I can make this a predeployed contract in scenarios if you concern so much. but making programs a fixtures that accept fork is rather difficult design decision. programs are intended as byte sequences that are to be deployed in different scenarios and tested
can you explain this part a little. |
🗒️ Description
Conversion of opcode diff places tests by ori.
A test defines series of test scenarios that are run on each parametrized opcode sequence.
Then we check if the sequence worked as expected in a given scenario.
I think this is a powerful method to template test any new given opcode.
we already have pre defined scenarios. then we just add one more parameter with what we want to test, and it will be covered on all the cases automatically.
Cases can be like:
callcode->staticcall-> [opcode]
create2-> [opcode]
[opcode] -> revert
check it out. so we can define opcode programs and scenarios. then the test will put each opcode program in each scenario and verify that it's result is the same (perhaps result will be complex depending on context and fork)
the idea is so far to have a template test and then we can easily just add opcode programms and it will be run in all crazy combinations.
likce call delegate call suicide revert and so on
This is still WIP.
🔗 Related Issues
#184
✅ Checklist
mkdocs serve
locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.