-
Notifications
You must be signed in to change notification settings - Fork 104
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
Experimental offchain storage pt 1 #1630
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.
Some other changes I forgot to mention in the description!
Reducer, | ||
} from './lib/mina/zkapp.js'; | ||
export { SmartContract, method, declareMethods } from './lib/mina/zkapp.js'; | ||
export { Reducer } from './lib/mina/actions/reducer.js'; |
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.
moved Reducer
code to a separate file, zkapp.ts
is unreadable enough without it
src/lib/mina/actions/reducer.ts
Outdated
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.
no new code here, just moved from zkapp.ts
function State<A>(defaultValue?: A): State<A> { | ||
return createState<A>(defaultValue); |
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.
State()
now accepts passing in a default value, which is used to initialize it by init()
!
this is very handy when there's no other reason to override init()
, like in our offchain state example
// for all explicitly declared states, set them to their default value | ||
let stateKeys = getLayout(this.constructor as typeof SmartContract).keys(); | ||
for (let key of stateKeys) { | ||
let state = this[key as keyof this] as InternalStateType<any> | undefined; | ||
if (state !== undefined && state.defaultValue !== undefined) { | ||
state.set(state.defaultValue); | ||
} | ||
} |
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.
new logic in init()
that uses default values for all state fields that have one
/** | ||
* Iterate through the list in a fixed number of steps any apply a given callback on each element. | ||
* | ||
* Proves that the iteration traverses the entire list. | ||
* Once past the last element, dummy elements will be passed to the callback. | ||
* | ||
* Note: There are no guarantees about the contents of dummy elements, so the callback is expected | ||
* to handle the `isDummy` flag separately. | ||
*/ | ||
forEach( | ||
length: number, | ||
callback: (element: T, isDummy: Bool, i: number) => void |
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.
API for iterating a MerkleList without having to explicitly instantiate an iterator
this is safer than .next()
since it does assertAtEnd()
for you
src/lib/provable/merkle-tree.ts
Outdated
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 fixes two pain points I always had when dealing with MerkleTrees:
- can't clone them
- can't just get a leaf, need to figure out how to use
getNode()
src/lib/provable/option.ts
Outdated
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 nice new Option
type!
provable, | ||
provablePure, |
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 always find re-exports confusing, removed them here
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.
using Unconstrained
as part of a MerkleList
Struct
element (ex: MerkleLeaf
in this PR) used to fail in a very mysterious way, because the Merkle list attempted to use .empty()
and Struct
deeply walked the Unconstrained
without finding anything.
These changes add a clear error message, and an easier way to declare the empty type alongside an Unconstrained
type
@@ -18,14 +18,13 @@ const doProofs = true; | |||
const beforeGenesis = UInt64.from(Date.now()); | |||
|
|||
class SimpleZkapp extends SmartContract { | |||
@state(Field) x = State<Field>(); | |||
@state(Field) x = State(initialState); | |||
|
|||
events = { update: Field, payout: UInt64, payoutReceiver: PublicKey }; | |||
|
|||
@method | |||
async init() { |
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.
We should be able to remove this method entirely now, I think?
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.
no because this example uses the fact that it's a @method
and affects provedState
} | ||
|
||
// base proof | ||
console.time('batch 0'); |
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.
note to remove these before we merge
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.
MVP design of an offchain state implementation.
update()
and make the Merkle updates more efficient before releaseOffchainState
Design
There are currently two types of offchain state:
OffchainState.Field
(a single field of state) andOffchainState.Map
(a key-value map).field.get()
andfield.set(value)
in a contractmap.get(key)
andmap.set(key, value)
To use offchain state, a smart contract developer must
OffchainState
offchainState.compile()
andoffchainState.setContractInstance()
in the setup phasesetContractInstance()
requires the contract to have that state field.settle()
method and call it periodically to settle stateget()
after it was settledsettle()
implementation is trivial using the toolsOffchainState
providescreateSettlementProof()
outside the contract first, which is also simple from the user point of viewSome design decisions:
Caveats / Future work
While writing the example, I realized that the pure
set()
API, which instructs offchain settlement to blindly overwrite a current value with the new one, is a major footgun and probably only useful for specific scenarios (e.g. each user of a contract is empowered to manage their own slice of state and overwrite it without taking into account old state. example: registration as a voter), but fails as a solution for the majority of use cases.Q: Should we rename
set()
tooverwrite()
to be more honest about its limitations?An extension that can be made easily is to add an
update()
method which not only takes the new state, but also the old state to replace. It only succeeds if the old state is correct, i.e. it acts exactly the same as a precondition for onchain state.update()
can be implemented such that it fails atomically on an entire account update. E.g., you make two state updates, and both fail if one of the old states is invalid.transfer()
method, for example. The method which dispatches the transfer will only dispatch valid pairs of to/from balance changes. If one of them is outdated, both fail, so there's no way to cause an invalid balance change.With
update()
, what we arrive at is a clean extension of the onchain state API to offchain state of arbitrary size. It completely solves the size problem. However, it doesn't fully solve the concurrency problem:One idea to improve shared state handling is to introduce user-defined (or predefined) commutative actions. For example, an action could say "add this number to the state" instead of "change the state to this value".
OffchainState.Field
/OffchainState.Map
declarationsThe final design should probably be more similar to protokit's runtime modules: A client/server like model where arbitrary computation is run in an offchain reducer to go from the previous to the next set of states, in one atomic update.