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

Flat Storage #399

Merged
merged 29 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a651a7d
add flat storage nep draft
Sep 29, 2022
9004d02
add nep number
Sep 29, 2022
34ae994
add more conntent'
Oct 2, 2022
8fa49ee
add discussion for storage writes
Oct 2, 2022
9b73a71
Merge branch 'master' into flat_storage
bucanero Oct 3, 2022
3fa77c9
address comments
Oct 4, 2022
548e33d
add more sections
Oct 5, 2022
8515b1b
Merge branch 'flat_storage' of github.com:near/NEPs into flat_storage
Oct 5, 2022
181b65b
Update neps/nep-0399.md
mzhangmzz Oct 6, 2022
51e35ab
Update neps/nep-0399.md
Looogarithm Feb 23, 2023
79f324e
Update neps/nep-0399.md
Looogarithm Feb 23, 2023
dabaea3
Update neps/nep-0399.md
Looogarithm Feb 23, 2023
a9ccf71
Update neps/nep-0399.md
Looogarithm Feb 23, 2023
4ce8b60
rewrite summary+motivation
Looogarithm Feb 23, 2023
809095e
size estimations
Looogarithm Feb 24, 2023
32b878b
drawbacks
Looogarithm Feb 24, 2023
210318a
shard removals
Looogarithm Feb 24, 2023
d7c88f5
costs
Looogarithm Feb 24, 2023
b8dcf87
Merge branch 'master' into flat_storage
Looogarithm Feb 24, 2023
f2a22fc
nits
Looogarithm Feb 27, 2023
855492a
apply suggestions
Looogarithm Mar 3, 2023
db247a0
Merge branch 'master' into flat_storage
Looogarithm Mar 9, 2023
1ad54cd
feat: Added Changelog section
frol Mar 20, 2023
9222c43
Update neps/nep-0399.md
Looogarithm Mar 20, 2023
066290f
Update neps/nep-0399.md
Looogarithm Mar 20, 2023
da383fa
Update neps/nep-0399.md
Looogarithm Mar 20, 2023
c36593e
Update neps/nep-0399.md
Looogarithm Mar 20, 2023
a886279
protocol change
Looogarithm Mar 20, 2023
aeb999a
Merge branch 'flat_storage' of github.com:near/NEPs into flat_storage
Looogarithm Mar 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Changes to the protocol specification and standards are called NEAR Enhancement
|[0245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) | Multi Token Standard | @zcstarr @riqi @jriemann @marcos.sun | Review |
|[0297](https://github.com/near/NEPs/blob/master/neps/nep-0297.md) | Events Standard | @telezhnaya | Final |
|[0330](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) | Source Metadata | @BenKurrek | Review |
|[0399](https://github.com/near/NEPs/blob/master/neps/nep-0399.md) | Flat Storage | @AleksandrLogunov @MinZhang | Draft |



Expand Down
340 changes: 340 additions & 0 deletions neps/nep-0399.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
---
NEP: 0399
Title: Flat Storage
Author: Aleksandr Logunov <[email protected]> Min Zhang <[email protected]>
DiscussionsTo: https://github.com/nearprotocol/neps/pull/0399
Status: Draft
Type: Protocol Track
Category: Storage
Created: 07-Sep-2022
---

## Summary

Currently, the state of blockchain is stored in our storage in the format of persistent merkelized tries.
Although the trie structure is needed to compute state roots and prove the validity of states, it is expensive
to read from the trie structure because it requires a traversal from the trie root to the leaf that contains the key
value pair, which could mean 2 * key_length of disk access in the worst case.

In addition, we charge receipts by the number of trie nodes they touched (TTN cost),
which is both confusing to developers and unpredictable. This NEP proposes the idea of FlatStorage,
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved
which stores a flattened key/value pairs of the current state on disk. This way, any storage read requires at most
mzhangmzz marked this conversation as resolved.
Show resolved Hide resolved
2 disk reads. As a result, we can make storage reads faster, decrease the fees, and get rid of the TTN
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved
cost.

## Motivation

The motivation of this proposal is to increase performance of storage reads, reduce storage read costs and
simplifies how storage fees are charged by getting rid of TTN cost.
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved

## Rationale and alternatives
Copy link

Choose a reason for hiding this comment

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

I'm missing some concrete values here:

  • if we're looking at some selected larger contracts - how much do they pay for reads right now, and how much would they save once flat storage is there ?
  • what is the difference (if any) for average contract ?

Copy link
Member

@Longarithm Longarithm Feb 27, 2023

Choose a reason for hiding this comment

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

This requires some understanding from reader, mentioned in "Storage Reads" subsection.
But we don't expect big difference in costs, the main goal is to solve undercharging issue.
There is also no big difference for different contracts. I could mention caching and read_cached_trie_node but the document is already quite big.


Q: Why is this design the best in the space of possible designs?
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to add a discussion about the following here:

Flat storage conceptually makes a tradeoff between making reads cheaper and simpler to charge gas fees for by making writes more expensive because writes have to keep the flat state up-to-date as well. The assertion of this work is that this tradeoff is a good one to make.

For a read heavy workload, the above tradeoff should be good. But this work should also demonstrate that the tradeoff is acceptable for write heavy workloads.

Copy link
Member

@Longarithm Longarithm Feb 27, 2023

Choose a reason for hiding this comment

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

I think it is too early to mention in this section. Mentioned this in "Drawbacks" session together with other tradeoffs we make ("we have to keep it up-to-date, which results in 1 extra disk write for changed key, and 1 auxiliary disk write + removal for each FSD...")


A: There are other ideas for how to improve storage performance, such as using
other database instead of rocksdb, or changing the representation of states
mzhangmzz marked this conversation as resolved.
Show resolved Hide resolved
to achieve locality of data in the same account. Considering that these ideas
will likely require much more work than FlatStorage, FlatStorage is a good investment
of our effort to achieve better storage performances. In addition, the improvement
from FlatStorage can be combined with the improvement brought by these other ideas,
so the implementation of FlatStorage won't be rendered obsolete in the future.

Q: What other designs have been considered and what is the rationale for not choosing them?
Copy link

Choose a reason for hiding this comment

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

one other alternative would be to change the tree type. (for example by moving to something like AVL or B-trees) -

let's mention pros&cons of this vs flat storage.

Copy link
Member

Choose a reason for hiding this comment

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

Done, changed this subsection significantly by listing other ideas: getting rid of state root, increasing TTN.


A: Alternatively, we can still get rid of TTN cost by increasing the base fees for storage reads and writes. However,
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
this could require increasing the fees by quite a lot, which could end up breaking many contracts.
Q: What is the impact of not doing this?
mzhangmzz marked this conversation as resolved.
Show resolved Hide resolved

A: Storage reads will remain inefficiently implemented and cost more than it could be.
mzhangmzz marked this conversation as resolved.
Show resolved Hide resolved

Copy link

Choose a reason for hiding this comment

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

I'd move the 'drawbacks' section here.

And also mention the (rough) new storage costs after we have flat storage.

Copy link
Member

@Longarithm Longarithm Feb 27, 2023

Choose a reason for hiding this comment

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

Well the "drawbacks" section is in the end by NEP design, and it makes sense there - I counted 6 different drawbacks, most of them require prior understanding how everything works. Costs go to "Storage Reads" section.

## Specification
The key idea of FlatStorage is to store a direct mapping from trie keys to values on disk.
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
Here the values of this mapping can be either the value corresponding to the trie key itself,
or the value ref, a hash that points to the address of the value. If the value itself is stored,
only one disk read is needed to look up a value from flat storage, otherwise two disk reads if the value
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
ref is stored. We will discuss more in the following section for whether we use values or value refs.
For the purpose of high level discussion, it suffices to say that with FlatStorage,
at most two disk reads are needed to perform a storage read.

The simple design above won't work because there could be forks in the chain. In the following case, FlatStorage
must support key value lookups for states of the blocks on both forks.
```
Block B1 - Block B2 - ...
/
block A
\ Block C1 - Block C2 - ...
```

The handling of forks will be the main consideration of the following design. More specifically,
the design should satisfy the following requirements,
1) It should support concurrent block processing. Blocks on different forks are processed
concurrently in our client code, so the flat storage API must support that.
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
2) In case of long forks, block processing time should not be too much longer than the average case.
We don’t want this case to be exploitable. It is acceptable that block processing time is 200ms longer,
which may slow down block production, but probably won’t cause missing blocks and chunks.
It is not acceptable if block processing time is 10s, which may lead to more forks and instability in the network.
3) The design must be able to decrease storage access cost in all cases,
since we are going to change the storage read fees based on flat storage.
We can't conditionally enable FlatStorage for some blocks and disable it for other, because
the fees we charge must be consistent.

The mapping of key value pairs FlatStorage stored on disk matches the state at some block.
We call this block the head of flat storage, or the flat head. There are two natural options
for which block should be the flat head, the chain head, or the last final block. Although
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved
both options could work, the implementation we propose will use the last final block because
it is simpler.

To support key value lookups for other blocks that are not the flat head, FlatStorage will
also store key value changes(deltas) per block for these blocks.
We call these deltas FlatStorageDelta (FSD). Let’s say the flat storage head is at block h,
and we are applying transactions based on block h’. Since h is the last final block,
h is an ancestor of h'. To access the state at block h', we need FSDs of all blocks between h and h'.
Note that all these FSDs must be stored in memory, otherwise, the access of FSDs will trigger
more disk reads and we will have to set storage key read fee higher.

However, the consensus algorithm doesn’t provide any guarantees in the distance of blocks
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
that we need to process since it could be arbitrarily long for a block to be finalized.
To solve this problem, we make another proposal (TODO: attach link for the proposal) to
Copy link
Member

Choose a reason for hiding this comment

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

This TODO needs to be resolved (or removed) before accepting.

Copy link
Contributor Author

@mzhangmzz mzhangmzz Oct 6, 2022

Choose a reason for hiding this comment

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

I plan to write separate NEP for this. WIP.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose the additional NEP will cover this topic in detail. I am curious about the case where h or one its children has many outgoing branches vs. h has a single deep branch. Will the proposal below work just fine in both cases and ensure that we do not store too many FSD in memory?

set gas limit to zero for blocks with height larger than the latest final block’s height + X.
If the gas limit is set to zero for a block, it won't contain any transactions or receipts,
Copy link
Contributor

Choose a reason for hiding this comment

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

What will precisely happen in such case:

  1. Will flat storage store an empty FSD for blocks with no transactions?
  2. Or will it store no FSD at all?

If it is the second, then do you need any special handling to identify that you have already processed an empty block or not?

Copy link
Member

Choose a reason for hiding this comment

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

This is an open question, should be answered in NEP like #460.

and FlatStorage won't need to store the delta for this block.
With this change, FlatStorage only needs to store FSDs for blocks with height less than the latest
final block’s height + X. And since there can be at most one valid block per height,
FlatStorage only needs to store at most X FSDs in memory.

### FSD size estimation
To set the value of X, we need to see how many block deltas can fit in memory.
Copy link
Member

Choose a reason for hiding this comment

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

Is there any chance on abnormal conditions of the network (maybe during an attack), that this value of X is not large enough, and the client will return a different value after reading than the one expected?

As I understand, we are fetching this value from a non-finalized block, so there is a chance for this to happen, even if highly improbable. How would the system react in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question! This is actually the part that I left out about the proposal to set gas limit to zero for blocks with height larger than the latest final block’s height + X. If we implement that proposal, blocks that are further than X blocks away from the last final block can't include any new chunks, in other words, they are just empty blocks. This is a way to allow the blockchain to recover in case of some abnormal conditions. With that change, FlatStorage only need to store deltas for blocks from the last final block to last final block + X, thus at most X blocks. Any other block further than last final block + X will just have an empty delta.


We can estimate FSD size using protocol fees.
Assume that flat state stores a mapping from keys to value refs.
Maximal key length is ~2 KiB which is the limit of contract data key size.
During wasm execution, we pay `wasm_storage_write_base` = 64 Ggas per call and
`wasm_storage_write_key_byte` = 70 Mgas per key byte.
In the extreme case it means that we pay `(64_000 / 2 KiB + 70) Mgas ~= 102 Mgas` per byte.
Then the total size of keys changed in a block is at most
`block_gas_limit / gas_per_byte * num_shards = (1300 Tgas / 102 Mgas) * 4 ~= 50 MiB`.

To estimate the sizes of value refs, there will be at most
`block_gas_limit / wasm_storage_write_base * num_shards
= 1300 Tgas / 64 Ggas * 4 = 80K` changed entries in a block.
Since one value ref takes 40 bytes, limit of total size of changed value refs in a block
is then 3.2 MiB.

To sum it up, we will have < 54 MiB for one block, and ~1.1 GiB for 20 blocks.
Copy link
Contributor

Choose a reason for hiding this comment

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

  • How do we ensure that FSDs are kept in memory? Will we use a memory only data struct for them like hash maps?
  • Do we know how many memory accesses we need to check if a worst case FSD of 54 MiB has a given key or not?

Copy link
Contributor Author

@mzhangmzz mzhangmzz Oct 4, 2022

Choose a reason for hiding this comment

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

  • Yes. They will be stored in FlatStateDelta. They are also persisted on disk, but we don't read from disk during block processing. We only read from disk for this column when node first starts and FlatStorage loads deltas for all blocks after the flat head into memory.
  • Most of the times 2 because in most case there is no fork, and the chain head should be at last final block + 2. Worst case, it can be up to he number of blocks between the last final block to this block. If we implement the idea of setting gas limit to zero for blocks that are further than X blocks from the final block, then the number of in memory hashmap lookup will be X.


Note that if we store a value instead of value ref, size of FSDs can potentially be much larger.
Because value limit is 4 MiB, we can’t apply previous argument about base cost.
Since `wasm_storage_write_value_byte` = 31 Mgas, one FSD size can be estimated as
`(1300 Tgas / min(storage_write_value_byte, storage_write_key_byte) * num_shards)`, or ~170 MiB,
which is 3 times higher.

The advantage of storing values instead of value refs is that it saves one disk read if the key has been
modified in the recent blocks. It may be beneficial if we get many transactions or receipts touching the same
trie keys in consecutive blocks, but it is hard to estimate the value of such benefits without more data.
Since storing values will cost much more memory than value refs, we will likely choose to store value refs
in FSDs and set X to a value between 10 and 20.

### Storage Writes
Currently, storage writes are charged based on the number of touched trie nodes (TTN cost), because updating the leaf trie
node which stores the value to the trie key requires updating all trie nodes on the path leading to the leaf node.
All writes are committed at once in one db transaction at the end of block processing, outside of runtime after
all receipts in a block are executed. However, at the time of execution, runtime needs to calculate the cost,
which means it needs to know how many trie nodes the write affects, so runtime will issue a read for every write
to calculate the TTN cost for the write. Such reads cannot be replaced by a read in FlatStorage because FlatStorage does
not provide the path to the trie node.

There are multiple proposals on how storage writes can work with FlatStorage.
- Keep it the same. The cost of writes remain the same. Note that this can increase the cost for writes in
some cases, for example, if a contract first read from a key and then writes to the same key in the same chunk.
Without FlatStorage, the key will be cached in the chunk cache after the read, so the write will cost less.
With FlatStorage, the read will go through FlatStorage, the write will not find the key in the chunk cache and
it will cost more.
- Remove the TTN cost from storage write fees. Currently, there are two ideas in this direction.
- Charge based on maximum depth of a contract’s state, instead of per-touch-trie node.
- Charge based on key length only.

Both of the above ideas would allow us to remove writes from the critical path of block execution. However,
Longarithm marked this conversation as resolved.
Show resolved Hide resolved
it is unclear at this point what the new cost would look like and whether further optimizations are needed
to bring down the cost for writes in the new cost model.

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 where to add this so just picking a random point. How do you propose to handle the case where a node is not tracking all shards. I imagine that you would want to garbage collect state from shards that you are no longer tracking. Will state for each shard be stored in a separate column?

Copy link
Member

Choose a reason for hiding this comment

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

Good question. There is no need to store state in separate column. For now we can afford naive removals, I added more details to "Drawbacks" section.

### Migration Plan
// TODO

## Reference Implementation
FlatStorage will implement the following structs.

`FlatStateDelta`: a HashMap that contains state changes introduced in a block. They can be applied
on top the state at flat head to compute state at another block.

`FlatState`: provides an interface to get value or value references from flat storage. It
will be part of `Trie`, and all trie reads will be directed to the FlatState object.
A `FlatState` object is based on a block `block_hash`, and it provides key value lookups
on the state after the block `block_hash` is applied.

`ShardFlatStates`: provides an interface to construct `FlatState` for each shard.

`FlatStorageState`: stores information about the state of the flat storage itself,
for example, all block deltas that are stored in flat storage and the flat
storage head. `FlatState` can access `FlatStorageState` to get the list of
deltas it needs to apply on top of state of current flat head in order to
compute state of a target block.

It may be noted that in this implementation, a separate `FlatState` and `FlatStorageState`
will be created for each shard. The reason is that there are two modes of block processing,
normal block processing and block catchups.
Since they are performed on different ranges of blocks, flat storage need to be able to support
different range of blocks on different shards. Therefore, we separate the flat storage objects
used for different shards.

### DB columns
`DBCol::FlatState` stores a mapping from trie keys to the value corresponding to the trie keys,
based on the state of the block at flat storage head.
- *Rows*: trie key (`Vec<u8>`)
- *Column type*: `ValueOrValueRef`

`DBCol::FlatStateDeltas` stores a mapping from `(shard_id, block_hash)` to the `FlatStateDelta` that stores
state changes introduced in the given shard of the given block.
- *Rows*: `{ shard_id, block_hash }`
- *Column type*: `FlatStateDelta`
Note that `FlatStateDelta`s needed are stored in memory, so during block processing this column won't be used
at all. This column is only used to load deltas into memory at `FlatStorageState` initialization time when node starts.

`DBCol::FlatStateHead` stores the flat head at different shards.
- *Rows*: `shard_id`
- *Column type*: `CryptoHash`
Similarly, flat head is also stored in `FlatStorageState` in memory, so this column is only used to initialize
`FlatStorageState` when node starts.

### `FlatStateDelta`
`FlatStateDelta` stores a mapping from trie keys to value refs. If the value is `None`, it means the key is deleted
in the block.
```rust
pub struct FlatStateDelta(HashMap<Vec<u8>, Option<ValueRef>>);
```

```rust
pub fn from_state_changes(changes: &[RawStateChangesWithTrieKey]) -> FlatStateDelta
```
Converts raw state changes to flat state delta. The raw state changes will be returned as part of the result of
`Runtime::apply_transactions`. They will be converted to `FlatStateDelta` to be added
to `FlatStorageState` during `Chain::post_processblock`.

### ```FlatState```
```FlatState``` will be created for a shard `shard_id` and a block `block_hash`, and it can perform
key value lookup for the state of shard `shard_id` after block `block_hash` is applied.
```rust
pub struct FlatState {
/// Used to access flat state stored at the head of flat storage.
store: Store,
/// The block for which key-value pairs of its state will be retrieved. The flat state
/// will reflect the state AFTER the block is applied.
block_hash: CryptoHash,
/// In-memory cache for the key value pairs stored on disk.
#[allow(unused)]
cache: FlatStateCache,
/// Stores the state of the flat storage
#[allow(unused)]
flat_storage_state: FlatStorageState,
}
```

```FlatState``` will provide the following interface.
```rust
pub fn get_ref(
&self,
key: &[u8],
) -> Result<Option<ValueOrValueRef>, StorageError>
```
Returns the value or value reference corresponding to the given `key`
for the state that this `FlatState` object represents, i.e., the state that after
block `self.block_hash` is applied.

`FlatState` will be stored as a field in `Tries`.

###```ShardFlatStates```
`ShardFlatStates` will be stored as part of `ShardTries`. Similar to how `ShardTries` is used to
construct new `Trie` objects given a state root and a shard id, `ShardFlatStates` is used to construct
a new `FlatState` object given a block hash and a shard id.

```rust
pub fn new_flat_state_for_shard(
&self,
shard_id: ShardId,
block_hash: Option<CryptoHash>,
) -> FlatState
```
Creates a new `FlatState` to be used for performing key value lookups on the state of shard `shard_id`
after block `block_hash` is applied.

```rust
pub fn get_flat_storage_state_for_shard(
&self,
shard_id: ShardId,
) -> Result<FlatStorageState, FlatStorageError>
```
Returns the `FlatStorageState` for the shard `shard_id`. This function is needed because even though
`FlatStorageState` is part of `Runtime`, `Chain` also needs access to `FlatStorageState` to update flat head.
We will also create a function with the same in `Runtime` that calls this function to provide `Chain` to access
to `FlatStorageState`.

###```FlatStorageState```
`FlatStorageState` is created per shard. It provides information to which blocks the flat storage
on the given shard currently supports and what block deltas need to be applied on top the stored
flat state on disk to get the state of the target block.

```rust
fn get_deltas_between_blocks(
&self,
target_block_hash: &CryptoHash,
) -> Result<Vec<Arc<FlatStateDelta>>, FlatStorageError>
```
Returns the list of deltas between blocks `target_block_hash`(inclusive) and flat head(exclusive),
Returns an error if `target_block_hash` is not a direct descendent of the current flat head.
This function will be used in `FlatState::get_ref`.

```rust
fn update_flat_head(&self, new_head: &CryptoHash) -> Result<(), FlatStorageError>
```
Updates the head of the flat storage, including updating the flat head in memory and on disk,
update the flat state on disk to reflect the state at the new head, and gc the `FlatStateDelta`s that
are no longer needed from memory and from disk.

```rust
fn add_delta(
&self,
block_hash: &CryptoHash,
delta: FlatStateDelta,
) -> Result<StoreUpdate, FlatStorageError>
```
Adds `delta` to `FlatStorageState`, returns a `StoreUpdate` object that includes

#### Thread Safety
We should note that the implementation of `FlatStorageState` must be thread safe because it can
be concurrently accessed by multiple threads. A node can process multiple blocks at the same time
if they are on different forks. Therefore, `FlatStorageState` will be guarded by a `RwLock` so its
access can be shared safely.

```rust
pub struct FlatStorageState(Arc<RwLock<FlatStorageStateInner>>);
```

## Drawbacks (Optional)

Why should we *not* do this?

Copy link

Choose a reason for hiding this comment

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

there is also a higher risk of database corruption -- as with TRIEs the rows were only appended (or deleted) - while with flat storage, we do read-modify-write

Copy link
Member

@Longarithm Longarithm Feb 27, 2023

Choose a reason for hiding this comment

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

Fair, added paragraph for that - together with replayability becoming more challenging.

## Unresolved Issues

As we discussed in Section Specification, there are still unanswered questions around how the new cost model for storage
writes would look like and how the current storage can be upgraded to enabled FlatStorage. We expect to finalize
the migration plan before this NEP gets merged, but we might need more time to collect data and measurement around
storage write costs, which can be only be collected after FlatStorage is partially implemented.

Another big unanswered question is how FlatStorage would work when challenges are enabled. We consider that to be out of
the scope of this NEP because the details of how challenges will be implemented are not clear yet.

## Future possibilities

## Copyright
[copyright]: #copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).