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

feat(store): prevent onchain state and indexer state from diverging #1581

Merged
merged 38 commits into from
Sep 23, 2023

Conversation

alvrs
Copy link
Member

@alvrs alvrs commented Sep 22, 2023

Fixes #1449
Fixes #1137
Fixes #1523

Details on the change in the changeset.

More context
  • setRecord static data, dynamic data lengths, dynamic datas
    • The event includes the entire data that is being written to storage (static, packed lengths, dynamic data)
    • For static data the onchain way of handling it is the same as offchain
    • The provided field layout is used for separating the provided data blob.
      • If the numDynamicFields value in fieldLayout is invalid (e.g. 0), the indexer would override the dynamic field data, while it would stay its previous value onchain.
        • we could solve this by either loading fieldLayout from storage instead of accepting it as a passed in parameter
        • or by emitting fieldLayout as part of the event, so indexers can imitate the onchain behavior
    • We “validate the input” onchain, but can actually remove that check, because all we’re testing is
      • provided static data matches the field layout static data length (doesn’t matter, if data is too long or too short it’s handled the same onchain and offchain)
      • check that the encoded total length matches the dynamic data length
        • i’m not sure if we currently do this, but it would be possible to append 0 to the dynamic data blob to mirror the onchain behavior. In this case we don’t need the onchain check
  • spliceStaticData static data
    • onchain deleteCount is always data.length, so we should imitate that behavior offchain
    • no need to include deleteCount in the event or function signature
  • deleteRecord static data, dynamic data lengths
    • the user provided field layout determines how much of the static data is actually deleted
      • to solve this, either emit the field layout as part of the event and handle the same way as onchain, or load the field layout from storage
    • the user provided field layout determines whether or not we set the dynamic lengths to 0 (= deleting the dynamic data)
      • also, since we don’t actually set the dynamic data to 0, we get invalid data onchain in getFieldSlice after deleting the data
        • this can be solved by adding a bound check for reading slices of fields
        • alternatively we could make it more explicit that users have to check the length before reading from an index, but that would be more expensive (if they decide to do it)
  • spliceDynamicData dynamic data length, dynamic data ✅
    • start depends on user provided field, previous encoded lengths, and startWithin field
      • we load previousEncodedLengths from storage, so can’t be tampered with
      • startWithinField is what we use onchain, and we use the provided field index to find the right storage slot
    • we make sure onchain that deleteCount is at the end of the field to guarantee the indexer doesn’t cut into another field or shifts data at the end of the field
    • the updated encoded length is emitted with the event and stored onchain

@changeset-bot
Copy link

changeset-bot bot commented Sep 22, 2023

🦋 Changeset detected

Latest commit: 7893115

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 29 packages
Name Type
@latticexyz/block-logs-stream Major
@latticexyz/cli Major
@latticexyz/common Major
@latticexyz/dev-tools Major
@latticexyz/store-sync Major
@latticexyz/store Major
@latticexyz/world Major
create-mud Major
@latticexyz/store-indexer Major
@latticexyz/config Major
@latticexyz/protocol-parser Major
@latticexyz/react Major
@latticexyz/abi-ts Major
@latticexyz/ecs-browser Major
@latticexyz/faucet Major
@latticexyz/gas-report Major
@latticexyz/network Major
@latticexyz/noise Major
@latticexyz/phaserx Major
@latticexyz/recs Major
@latticexyz/schema-type Major
@latticexyz/services Major
@latticexyz/solecs Major
solhint-config-mud Major
solhint-plugin-mud Major
@latticexyz/std-client Major
@latticexyz/std-contracts Major
@latticexyz/store-cache Major
@latticexyz/utils Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

"gasUsed": 103978
"gasUsed": 104908
Copy link
Member Author

Choose a reason for hiding this comment

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

gas only increased around 1k for set record here (i expected 2k for the additional sload, but seems like some of it was offset by less memory being passed around between functions and the optimized getFieldLayout)

@alvrs alvrs changed the title feat(store): WIP prevent onchain state and indexer state from diverging feat(store): prevent onchain state and indexer state from diverging Sep 23, 2023
```

- All methods that are only valid for dynamic fields (`pushToField`, `popFromField`, `updateInField`, `getFieldSlice`)
have been renamed to make this more explicit (`pushToDynamicField`, `popFromDynamicField`, `updateInDynamicField`, `getDynamicFieldSlice`).
Copy link
Member

@holic holic Sep 23, 2023

Choose a reason for hiding this comment

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

should we make updateInDynamicField more like a spliceDynamicField function?

Copy link
Member Author

Choose a reason for hiding this comment

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

there is a spliceDynamicField function too! updateInDynamicField was there before but now basically just calls spliceDynamicField with the correct deleteCount value and does a range check. In fact the range check is not even necessary, since _spliceDynamicField does a range check already. Could argue updateInDynamicField not really necessary.

Copy link
Member

Choose a reason for hiding this comment

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

yeah I am leaning towards us removing, the name was always confusing to me

- `IStore` now exposes `setStaticField` and `setDynamicField` to save gas by avoiding the dynamic inference of whether the field is static or dynamic.

- The `getDynamicFieldSlice` method no longer accepts reading outside the bounds of the dynamic field.
This is to avoid returning invalid data, as the data of a dynamic field is not deleted when the record is deleted, but only its length is set to zero.
Copy link
Member

@holic holic Sep 23, 2023

Choose a reason for hiding this comment

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

I'm curious about the gas difference between actually deleting and setting length to zero - isn't there a gas return when freeing storage slots?

Copy link
Member Author

@alvrs alvrs Sep 23, 2023

Choose a reason for hiding this comment

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

I thought so too and actually implemented the actual removal today. Surprisingly setting it to 0 was a lot more expensive, and the cost grew linearly. Eventually I realized that you still have to provide the entire gas for the transaction to complete, and at the end of the tx receive up to 20% refund. So in the end it's almost always cheaper for us to not touch the dynamic data at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

See 004fe9c and the following 3 commits

Copy link
Member

Choose a reason for hiding this comment

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

sorry ethereum for the state bloat we're about to create

Copy link
Member Author

Choose a reason for hiding this comment

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

that's what ethereum gets for being so stingy with the refund

@@ -8,7 +8,7 @@ When a record or a single field is edited, or when a record is deleted, Store em

```solidity
event Store_SetRecord(bytes32 indexed tableId, bytes32[] keyTuple, bytes staticData, bytes32 encodedLengths, bytes dynamicData),
event Store_SpliceStaticData(bytes32 indexed tableId, bytes32[] keyTuple, uint48 start, uint40 deleteCount, bytes data),
event Store_SpliceStaticData(bytes32 indexed tableId, bytes32[] keyTuple, uint48 start, bytes data),
Copy link
Member

Choose a reason for hiding this comment

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

sorry lermchair

@@ -0,0 +1,6 @@
import { Hex } from "viem";

export function getByteLength(data: Hex): number {
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

oh nice, i figured there must exist something for a super general util like this one

},
{
"file": "test/WorldDynamicUpdate.t.sol",
"test": "testPopFromField",
"test": "testPopFromDyanmicField",
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
"test": "testPopFromDyanmicField",
"test": "testPopFromDynamicField",

},
{
"file": "test/WorldDynamicUpdate.t.sol",
"test": "testPopFromField",
"test": "testPopFromDyanmicField",
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
"test": "testPopFromDyanmicField",
"test": "testPopFromDynamicField",

@alvrs alvrs marked this pull request as ready for review September 23, 2023 21:38
@alvrs alvrs requested a review from dk1a as a code owner September 23, 2023 21:38
@alvrs alvrs merged commit cea754d into main Sep 23, 2023
@alvrs alvrs deleted the alvrs/store-polish branch September 23, 2023 21:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants