Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Simulate a chain of actions #1635

Merged
merged 3 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 13 additions & 9 deletions api/jsonrpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,21 +194,25 @@ func (cli *JSONRPCClient) GetABI(ctx context.Context) (abi.ABI, error) {
return resp.ABI, err
}

func (cli *JSONRPCClient) Execute(ctx context.Context, actor codec.Address, action chain.Action) ([]byte, error) {
actionBytes, err := chain.MarshalTyped(action)
if err != nil {
return nil, fmt.Errorf("failed to marshal action: %w", err)
func (cli *JSONRPCClient) Execute(ctx context.Context, actor codec.Address, actions []chain.Action) ([][]byte, error) {
actionsMarshaled := make([][]byte, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: can we pre-allocate where we already know the length of the slice? Same comment throughout

for _, action := range actions {
actionBytes, err := chain.MarshalTyped(action)
if err != nil {
return nil, fmt.Errorf("failed to marshal action: %w", err)
}
actionsMarshaled = append(actionsMarshaled, actionBytes)
}

args := &ExecuteActionArgs{
Actor: actor,
Action: actionBytes,
Actor: actor,
Actions: actionsMarshaled,
}

resp := new(ExecuteActionReply)
err = cli.requester.SendRequest(
err := cli.requester.SendRequest(
ctx,
"executeAction",
"execute",
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 rename to executeActions ?

args,
resp,
)
Expand All @@ -219,7 +223,7 @@ func (cli *JSONRPCClient) Execute(ctx context.Context, actor codec.Address, acti
return nil, fmt.Errorf("failed to execute action: %s", resp.Error)
}

return resp.Output, nil
return resp.Outputs, nil
}

func Wait(ctx context.Context, interval time.Duration, check func(ctx context.Context) (bool, error)) error {
Expand Down
98 changes: 53 additions & 45 deletions api/jsonrpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,13 @@ func (j *JSONRPCServer) GetABI(_ *http.Request, _ *GetABIArgs, reply *GetABIRepl
}

type ExecuteActionArgs struct {
Actor codec.Address `json:"actor"`
Action []byte `json:"action"`
Actor codec.Address `json:"actor"`
Actions [][]byte `json:"actions"`
}

type ExecuteActionReply struct {
Output []byte `json:"output"`
Error string `json:"error"`
Outputs [][]byte `json:"outputs"`
Error string `json:"error"`
}

func (j *JSONRPCServer) Execute(
Expand All @@ -175,58 +175,66 @@ func (j *JSONRPCServer) Execute(
defer span.End()

actionRegistry := j.vm.ActionRegistry()
action, err := (*actionRegistry).Unmarshal(codec.NewReader(args.Action, len(args.Action)))
if err != nil {
return fmt.Errorf("failed to unmashal action: %w", err)
actions := make([]chain.Action, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

same comment on pre-allocating where we already know the length of args.Actions

for _, action := range args.Actions {
action, err := (*actionRegistry).Unmarshal(codec.NewReader(action, len(action)))
if err != nil {
return fmt.Errorf("failed to unmashal action: %w", err)
}
actions = append(actions, action)
}

now := time.Now().UnixMilli()

// Get expected state keys
stateKeysWithPermissions := action.StateKeys(args.Actor)
storage := make(map[string][]byte)
ts := tstate.New(1)

// flatten the map to a slice of keys
storageKeysToRead := make([][]byte, 0)
for key := range stateKeysWithPermissions {
storageKeysToRead = append(storageKeysToRead, []byte(key))
}
for _, action := range actions {
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
// Get expected state keys
stateKeysWithPermissions := action.StateKeys(args.Actor)

storage := make(map[string][]byte)
values, errs := j.vm.ReadState(ctx, storageKeysToRead)
for _, err := range errs {
if err != nil && !errors.Is(err, database.ErrNotFound) {
return fmt.Errorf("failed to read state: %w", err)
// flatten the map to a slice of keys
storageKeysToRead := make([][]byte, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

same comment on pre-allocating where we already know the size of stateKeysWithPermissions

for key := range stateKeysWithPermissions {
storageKeysToRead = append(storageKeysToRead, []byte(key))
}
}
for i, value := range values {
if value == nil {
continue

values, errs := j.vm.ReadState(ctx, storageKeysToRead)
for _, err := range errs {
if err != nil && !errors.Is(err, database.ErrNotFound) {
return fmt.Errorf("failed to read state: %w", err)
}
}
for i, value := range values {
if value == nil {
continue
}
storage[string(storageKeysToRead[i])] = value
}
storage[string(storageKeysToRead[i])] = value
}

ts := tstate.New(1)
tsv := ts.NewView(stateKeysWithPermissions, storage)

output, err := action.Execute(
ctx,
j.vm.Rules(now),
tsv,
now,
args.Actor,
ids.Empty,
)
if err != nil {
reply.Error = fmt.Sprintf("failed to execute action: %s", err)
return nil
}
tsv := ts.NewView(stateKeysWithPermissions, storage)

output, err := action.Execute(
ctx,
j.vm.Rules(now),
tsv,
now,
args.Actor,
ids.Empty,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This may cause issues for actions that rely on the actionID providing a unique ID.

Could we prevent this by using CreateActionID(ids.Empty, actionIndex) ? This should work because it will never conflict with a transaction that was actually issued onchain (there should never be a transaction with a zero value hash) and each action in the simulate API will get a unique actionIndex value. I think this should give us the best we can do here ie. on-chain execution results may differ with the actual actionID, but the actionID passed in to the simulation will always be unique.

)
if err != nil {
reply.Error = fmt.Sprintf("failed to execute action: %s", err)
return nil
}

encodedOutput, err := chain.MarshalTyped(output)
if err != nil {
return fmt.Errorf("failed to marshal output: %w", err)
}
tsv.Commit()

reply.Output = encodedOutput
encodedOutput, err := chain.MarshalTyped(output)
if err != nil {
return fmt.Errorf("failed to marshal output: %w", err)
}

reply.Outputs = append(reply.Outputs, encodedOutput)
}
return nil
}
Loading