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

Reuse Existing Compiled Code from Equivalent Program if Exists #67

Merged
merged 67 commits into from
Aug 28, 2023

Conversation

rauljordan
Copy link
Contributor

@rauljordan rauljordan commented May 6, 2023

One important optimization that can lead to massive gas savings pointed out by @rachel-bousfield is reusing compiled code that already exists in the state DB when a user is trying to compile a program they deploy. If they have deployed the same keccak program, for example, at address 0xA than one that already exists at another address, we can short circuit the compilation and reuse the existing compiled code from the other program address.

Here's the difference in speed for keccak

Time to compile: 220.795153ms // Newly compiled
Time to compile: 23.957493ms // Reusing other program address' equal compiled code

This PR also depends on stylus-geth's PR here: OffchainLabs/stylus-geth#17

This PR changes functions associated with WASM programs and the state DB to deal with codehashes internally instead of purely program addresses. This allows us to reuse compiled programs. Changes are also added to the recording DB to ensure we can reuse code in there as well.

We include a comprehensive test that checks several invariants we expect from these changes. Here's how our test works:

We deploy the same keccak WASM code to two different program addresses
We attempt to call them and expect a failure as they have not been compiled
We then:

  1. Make a multicall that: (a) tries to compile keccak A and succeeds, (b) revert right after
  2. We then try to call keccak A, but expect it to fail due to it not being compiled
  3. We then compile keccak A and try to call B

Here are some of the details of our changelog:

  • We change the machine versions mapping to operate based on codehash instead of program address. When we attempt to compile, this will succeed early if the same code has already been compiled in Stylus regardless of address
  • StateDB changes in stylus-geth are changed to use codehash based compiled wasm caching

Thanks to @rachel-bousfield for the fantastic guidance along the way.

@rauljordan rauljordan marked this pull request as draft May 6, 2023 01:04
@rauljordan rauljordan marked this pull request as ready for review May 6, 2023 01:04
@rauljordan rauljordan marked this pull request as draft May 6, 2023 01:06
@rauljordan rauljordan marked this pull request as ready for review May 6, 2023 01:07
Copy link
Contributor

@rachel-bousfield rachel-bousfield left a comment

Choose a reason for hiding this comment

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

Nice! Just a couple of tiny things

arbos/programs/programs.go Outdated Show resolved Hide resolved
arbos/programs/programs.go Show resolved Hide resolved
defer cleanup()

programAddress := deployWasm(t, ctx, auth, l2client, rustFile("fallible"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Separating the deploy function from the setup so that I can test reusing existing compilation at two different program addresses

@@ -929,6 +929,7 @@ func deployContract(
) common.Address {
deploy := deployContractInitCode(code, false)
basefee := GetBaseFee(t, client, ctx)
basefee.Mul(basefee, big.NewInt(2))
Copy link
Contributor

Choose a reason for hiding this comment

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

note: I don't understand why it's necessary specifically for this change.

TestProgramMemory fails about 50% of the time while trying to deploy the grow-120 contract with an error that PricePerGas is lower than baseFee. This is specifically for this test and contract (the contract cannot compile - but I don't understand how that's relevant)

Increasing baseFee like that does the trick, but there might be better solutions.

func (con ArbWasm) ProgramVersion(c ctx, _ mech, program addr) (uint16, error) {
return c.State.Programs().ProgramVersion(program)
func (con ArbWasm) ProgramVersion(c ctx, evm mech, program addr) (uint16, error) {
return c.State.Programs().ProgramVersion(evm.StateDB.GetCodeHash(program))
Copy link
Contributor

Choose a reason for hiding this comment

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

We should charge for this

Comment on lines +280 to +282
contractAddress: scope.Contract.Address(),
msgSender: scope.Contract.Caller(),
msgValue: common.BigToHash(scope.Contract.Value()),
Copy link
Contributor

Choose a reason for hiding this comment

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

contract is scope.Contract

Comment on lines 307 to 310
func (p Programs) getProgram(contract *vm.Contract) (Program, error) {
address := contract.Address()
if contract.CodeAddr != nil {
address = *contract.CodeAddr
}
return p.deserializeProgram(address)

return p.deserializeProgram(contract.CodeHash)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be collapsed

Comment on lines 47 to 49
func TestProgramKeccakArb(t *testing.T) {
keccakTest(t, false)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is duplicative of TestProgramArbitratorKeccak

Comment on lines 131 to 134
func TestProgramCompilationReuse(t *testing.T) {
t.Parallel()
testCompilationReuse(t, true)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Analogue is missing in stylus_test.go

Comment on lines +967 to +968
func setupProgramTest(t *testing.T, jit bool) (
context.Context, *arbnode.Node, *BlockchainTestInfo, *ethclient.Client, bind.TransactOpts, func(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of 1 or 0, we should probs take a list of filenames for generality

arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, l2client)
Require(t, err)

ensure(arbOwner.SetInkPrice(&auth, 1e2))
Copy link
Contributor

Choose a reason for hiding this comment

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

Was 1e2 intentional?

Copy link
Contributor

@rachel-bousfield rachel-bousfield left a comment

Choose a reason for hiding this comment

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

Nice work! I've added a few commits that are worth taking a look at that address most of the feedback. I decided not to change programs.go to minimize merge conflicts with

@rachel-bousfield rachel-bousfield merged commit 81e3ea0 into stylus Aug 28, 2023
7 checks passed
@rachel-bousfield rachel-bousfield deleted the reuse-compiled-code branch August 28, 2023 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants