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

add simulateTransaction endpoint #1610

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

tsachiherman
Copy link
Contributor

@tsachiherman tsachiherman commented Sep 30, 2024

What's in this PR ?

This PR adds the simulateTransaction endpoint to the VM API.

What are the changes in this PR ?

  • The Recorder struct from the vmwithcontracts example was moved into the core of the hyper-sdk.
  • Added simulateTransaction endpoint to the API server/client

What is left and still need to be implemented in a subsequent PR ?

The existing support of Simulate in the vmwithcontracts remained as is. Removing it while taking advantage of the above would require some additional work that wasn't captured in this PR.

@tsachiherman tsachiherman marked this pull request as ready for review September 30, 2024 20:33
Comment on lines 241 to 242
// 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.
Copy link
Collaborator

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).

Copy link
Collaborator

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?

Copy link
Contributor Author

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)
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 266 to 270
func (j *JSONRPCServer) SimulateTransaction(
req *http.Request,
args *SimulateTransactionArgs,
reply *SimulateTransactionReply,
) error {
Copy link
Collaborator

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.

Copy link
Contributor Author

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 {
Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

Comment on lines 240 to 247
type SimulateTransactionArgs struct {
Tx []byte `json:"tx"`
}

type SimulatedAction struct {
Output []byte `json:"output_b64"`
Error *string `json:"error"`
}
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 249 to 256
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"`
}
Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 274 to 276
if reply == nil {
return errors.New("SimulateTransaction was called with a nil reply object")
}
Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 240 to 251
type SimulatedActionResponse struct {
Output []byte
Error error
}

type SimulateTransactionResponse struct {
SimulatedActions []SimulatedActionResponse
ReadKeys []string
WriteKeys []string
AllocateKeys []string
BlockHeight uint64
}
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@tsachiherman tsachiherman marked this pull request as draft October 1, 2024 19:57
@tsachiherman tsachiherman marked this pull request as ready for review October 2, 2024 13:30
@@ -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 {
Copy link
Collaborator

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:

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?

Copy link
Contributor Author

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
Copy link
Collaborator

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

Comment on lines +149 to +154

// Marshal encodes a type as bytes.
func (r *Result) Marshal(p *codec.Packer) {
p.PackBytes(r.Value)
p.PackUint64(r.ConsumedFuel)
}
Copy link
Collaborator

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),
instead of adding explicit marshal functions here

Comment on lines +262 to +270
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))
}
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +264 to +266
if reply == nil {
return errors.New("SimulateAction was called with a nil reply object")
}
Copy link
Collaborator

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

Copy link
Contributor Author

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
Copy link
Collaborator

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

Copy link
Contributor Author

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
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +277 to +279
if len(actions) != 1 {
return fmt.Errorf("simulateAction expects a single action, %d found", len(actions))
}
Copy link
Collaborator

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?

Comment on lines +303 to +305
for key, permission := range recorder.GetStateKeys() {
reply.StateKeys = append(reply.StateKeys, SimulateStateKey{HexKey: hex.EncodeToString([]byte(key)), Permissions: byte(permission)})
}
Copy link
Collaborator

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() {
Copy link
Collaborator

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?

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.

Create an API that enables you to simulate execution of an action and see what state keys it touches.
2 participants