-
Notifications
You must be signed in to change notification settings - Fork 105
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
add simulateTransaction endpoint #1610
base: main
Are you sure you want to change the base?
Conversation
chain/dependencies.go
Outdated
// If any keys are touched during [Execute] while running in [OnchainTransactionExecution] mode that | ||
// are not specified in [StateKeys], the transaction will revert and the max fee will be charged. |
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.
Hmm this comment was actually not true before. If a state key is read that was not specified, it's up to the action to decide whether to propagate it (usually should).
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.
Could we revert this if we're able to remove OnchainTransactionExecution
mode?
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.
done.
@@ -101,7 +101,7 @@ func (j *JSONRPCServer) simulate(ctx context.Context, t actions.Call, actor code | |||
if err != nil { | |||
return nil, 0, err | |||
} | |||
recorder := storage.NewRecorder(currentState) | |||
recorder := state.NewRecorder(currentState) |
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 we remove this from vmwithcontracts/
since it can use the general purpose API after this PR?
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.
done
api/jsonrpc/server.go
Outdated
func (j *JSONRPCServer) SimulateTransaction( | ||
req *http.Request, | ||
args *SimulateTransactionArgs, | ||
reply *SimulateTransactionReply, | ||
) error { |
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.
Could we start with simulating a single action and providing the additional arguments as needed (actor) ?
In our case, transactions include additional data that we don't need to require from the user. For example, with this API you can only simulate an action from a given address if you can generate a signature from that address.
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.
done
genesis/rules.go
Outdated
@@ -152,6 +152,10 @@ func (*Rules) FetchCustom(string) (any, bool) { | |||
return nil, false | |||
} | |||
|
|||
func (*Rules) GetTransactionExecutionMode() chain.TransactionExecutionMode { |
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 reason that we need to add this mode to Rules
rather than just tracking what state keys were touched by executing an action?
If we just wrap the current view produced by the VM and run Execute
against it, I don't think that we should need to add key tracking within vmwithcontracts
at all and can remove this extra function on Rules
.
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.
done.
api/jsonrpc/server.go
Outdated
type SimulateTransactionArgs struct { | ||
Tx []byte `json:"tx"` | ||
} | ||
|
||
type SimulatedAction struct { | ||
Output []byte `json:"output_b64"` | ||
Error *string `json:"error"` | ||
} |
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.
Could we use codec.Bytes
in API responses when passing []byte
values to provide a reasonable JSON output?
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.
done
api/jsonrpc/server.go
Outdated
type SimulateTransactionReply struct { | ||
SimulatedActions []SimulatedAction `json:"simulatedActions"` | ||
ReadKeys []string `json:"readKeys"` | ||
WriteKeys []string `json:"writeKeys"` | ||
AllocateKeys []string `json:"allocKeys"` | ||
BlockHeight uint64 `json:"blockheight"` | ||
Error string `json:"error"` | ||
} |
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.
Could we reduce the state key fields here down to just state.Keys
and add support for JSON marshal/unmarshal directly to this type?
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'd prefer to remove BlockHeight
and Error
(in favor of returning the error directly and pushing to the client to handle it) unless there's a clear need to include them in the response.
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.
done
api/jsonrpc/server.go
Outdated
if reply == nil { | ||
return errors.New("SimulateTransaction was called with a nil reply object") | ||
} |
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 should be provided by the server, so we can remove this check and follow the style in the rest of the APIs of not checking if the reply value is nil.
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.
done
api/jsonrpc/client.go
Outdated
type SimulatedActionResponse struct { | ||
Output []byte | ||
Error error | ||
} | ||
|
||
type SimulateTransactionResponse struct { | ||
SimulatedActions []SimulatedActionResponse | ||
ReadKeys []string | ||
WriteKeys []string | ||
AllocateKeys []string | ||
BlockHeight uint64 | ||
} |
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 we re-use the types specified in server.go
as we do for the existing server/client implementations?
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.
done
…ersdk into tsachi/simulate_transaction
@@ -50,10 +48,10 @@ func (r *Recorder) GetValue(ctx context.Context, key []byte) (value []byte, err | |||
return r.State.GetValue(ctx, key) | |||
} | |||
|
|||
func (r *Recorder) GetStateKeys() state.Keys { | |||
result := state.Keys{} | |||
func (r *Recorder) GetStateKeys() Keys { |
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.
Ik this is just moving code, but we should make sure it's bug free since we're moving it out of examples.
During onchain execution, we use tstate
and want the recorder to exactly replicate the permissions required to execute an action on top of tstate.TStateView
.
tstate.TStateView
will return an error if we EVER try to perform a read/write/allocate where we do not have permission.
The current Recorder
overwrites the changedValue
for a given key between insert/remove operations and then records the summation of these events at the end. This means that if the action inserts and then removes a key, it will not record it as a write operation even though during onchain execution, the first write will fail here:
hypersdk/state/tstate/tstate_view.go
Line 199 in 47849d8
if !ts.checkScope(ctx, key, state.Write) { |
Could we update the recorder to make sure that it correctly handles this case and add unit tests for the happy path of tracking read/write/allocate + regression test for operations that may overwrite each other?
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.
the above isn't the correct example, but you're correct in saying that it won't perform correctly in the way it's currently implemented.
return err | ||
} | ||
|
||
rtx := codec.NewReader(encodedActionResults, actions.MaxResultSizeLimit) // will likely be much smaller than this |
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.
nit: we can set the max size to the size of the buffer when reading since we'll never read more than the length of the bytes we're reading
|
||
// Marshal encodes a type as bytes. | ||
func (r *Result) Marshal(p *codec.Packer) { | ||
p.PackBytes(r.Value) | ||
p.PackUint64(r.ConsumedFuel) | ||
} |
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 use the output parser
OutputParser.Register(&actions.Result{}, nil), |
stateKeys := state.Keys{} | ||
for _, entry := range resp.StateKeys { | ||
hexBytes, err := hex.DecodeString(entry.HexKey) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
stateKeys.Add(string(hexBytes), state.Permissions(entry.Permissions)) | ||
} |
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.
Rather than adding special handling here - could we implement MarshalJSON
and UnmarshalJSON
on state.Keys
so that we can use the type directly in the response?
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.
done
if reply == nil { | ||
return errors.New("SimulateAction was called with a nil reply object") | ||
} |
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.
Could we remove this check? This is populated by the server, so there should be no need to check for a nil value 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.
done
} | ||
|
||
actionRegistry := j.vm.ActionRegistry() | ||
rtx := codec.NewReader(args.Action, consts.NetworkSizeLimit) // will likely be much smaller than this |
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.
nit: let's use len(args.Action)
since we'll never read longer than the actual bytes provided
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.
done
} | ||
|
||
actionRegistry := j.vm.ActionRegistry() | ||
rtx := codec.NewReader(args.Action, consts.NetworkSizeLimit) // will likely be much smaller than this |
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 rtx
for read transaction? Could we rename this since it's reading in action bytes now?
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.
done
if len(actions) != 1 { | ||
return fmt.Errorf("simulateAction expects a single action, %d found", len(actions)) | ||
} |
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.
Could we remove this check and support handling multiple actions?
for key, permission := range recorder.GetStateKeys() { | ||
reply.StateKeys = append(reply.StateKeys, SimulateStateKey{HexKey: hex.EncodeToString([]byte(key)), Permissions: byte(permission)}) | ||
} |
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.
To support multiple actions, could we return a StateKeys
output per action?
return err | ||
} | ||
} | ||
for key, permission := range recorder.GetStateKeys() { |
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 we also remove the special reply handling by implementing JSON marshalling on the state.Keys
type directly?
What's in this PR ?
This PR adds the simulateTransaction endpoint to the VM API.
What are the changes in this PR ?
What is left and still need to be implemented in a subsequent PR ?
The existing support of
Simulate
in thevmwithcontracts
remained as is. Removing it while taking advantage of the above would require some additional work that wasn't captured in this PR.