Skip to content

Commit a9ac03f

Browse files
committed
feat(jsonrpc) add methods to get the count of executed and pending outputs
Methods added: - cartesi_getExecutedOutputCount - cartesi_getPendingExecutableOutputCount Each method: - Accepts an application parameter. - Returns {"data":"0x..."}. - Explicitly checks application existence before querying the aggregate. - Returns -32002 (application not found) for unknown applications. - Is registered in the dispatch table. Also updated the OpenRPC specification with the requested descriptions and added tests distinguishing unknown applications from existing applications with zero outputs.
1 parent 76c211c commit a9ac03f

3 files changed

Lines changed: 269 additions & 28 deletions

File tree

internal/jsonrpc/jsonrpc-discover.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,70 @@
471471
}
472472
]
473473
},
474+
{
475+
"name": "cartesi_getExecutedOutputCount",
476+
"summary": "Retrieve the number of executed outputs for the application",
477+
"description": "Returns a monotone change signal: an unchanged value means no new executions; not a resume cursor.",
478+
"params": [
479+
{
480+
"name": "application",
481+
"description": "The application's name or hex encoded address.",
482+
"schema": {
483+
"$ref": "#/components/schemas/NameOrAddress"
484+
},
485+
"required": true
486+
}
487+
],
488+
"result": {
489+
"name": "result",
490+
"schema": {
491+
"$ref": "#/components/schemas/ProcessedInputCountResult"
492+
}
493+
},
494+
"errors": [
495+
{
496+
"$ref": "#/components/errors/InvalidParams"
497+
},
498+
{
499+
"$ref": "#/components/errors/ApplicationNotFound"
500+
},
501+
{
502+
"$ref": "#/components/errors/InternalError"
503+
}
504+
]
505+
},
506+
{
507+
"name": "cartesi_getPendingExecutableOutputCount",
508+
"summary": "Retrieve the number of pending executable outputs for the application",
509+
"description": "Returns a non-monotone gauge (grows with new vouchers, shrinks with executions): do not use for change detection — poll the executed count instead.",
510+
"params": [
511+
{
512+
"name": "application",
513+
"description": "The application's name or hex encoded address.",
514+
"schema": {
515+
"$ref": "#/components/schemas/NameOrAddress"
516+
},
517+
"required": true
518+
}
519+
],
520+
"result": {
521+
"name": "result",
522+
"schema": {
523+
"$ref": "#/components/schemas/ProcessedInputCountResult"
524+
}
525+
},
526+
"errors": [
527+
{
528+
"$ref": "#/components/errors/InvalidParams"
529+
},
530+
{
531+
"$ref": "#/components/errors/ApplicationNotFound"
532+
},
533+
{
534+
"$ref": "#/components/errors/InternalError"
535+
}
536+
]
537+
},
474538
{
475539
"name": "cartesi_listOutputs",
476540
"summary": "Retrieve a List of Outputs",

internal/jsonrpc/jsonrpc.go

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,33 +57,35 @@ type rpcHandler = func(*Service, *http.Request, RPCRequest) (any, error)
5757
type dispatchTable = map[string]rpcHandler
5858

5959
var jsonrpcHandlers = dispatchTable{
60-
"rpc.discover": handleDiscover,
61-
"cartesi_listApplications": handleListApplications,
62-
"cartesi_getApplication": handleGetApplication,
63-
"cartesi_listEpochs": handleListEpochs,
64-
"cartesi_getEpoch": handleGetEpoch,
65-
"cartesi_getEpochByVirtualIndex": handleGetEpochByVirtualIndex,
66-
"cartesi_getLastAcceptedEpochIndex": handleGetLastAcceptedEpochIndex,
67-
"cartesi_listInputs": handleListInputs,
68-
"cartesi_getInput": handleGetInput,
69-
"cartesi_getProcessedInputCount": handleGetProcessedInputCount,
70-
"cartesi_listOutputs": handleListOutputs,
71-
"cartesi_getOutput": handleGetOutput,
72-
"cartesi_listReports": handleListReports,
73-
"cartesi_getReport": handleGetReport,
74-
"cartesi_listWithdrawals": handleListWithdrawals,
75-
"cartesi_getWithdrawal": handleGetWithdrawal,
76-
"cartesi_listTournaments": handleListTournaments,
77-
"cartesi_getTournament": handleGetTournament,
78-
"cartesi_listCommitments": handleListCommitments,
79-
"cartesi_getCommitment": handleGetCommitment,
80-
"cartesi_listMatches": handleListMatches,
81-
"cartesi_getMatch": handleGetMatch,
82-
"cartesi_listMatchAdvances": handleListMatchAdvances,
83-
"cartesi_getMatchAdvanced": handleGetMatchAdvanced,
84-
"cartesi_getNodeInfo": handleGetNodeInfo,
85-
"cartesi_getChainId": handleGetChainID,
86-
"cartesi_getNodeVersion": handleGetNodeVersion,
60+
"rpc.discover": handleDiscover,
61+
"cartesi_listApplications": handleListApplications,
62+
"cartesi_getApplication": handleGetApplication,
63+
"cartesi_listEpochs": handleListEpochs,
64+
"cartesi_getEpoch": handleGetEpoch,
65+
"cartesi_getEpochByVirtualIndex": handleGetEpochByVirtualIndex,
66+
"cartesi_getLastAcceptedEpochIndex": handleGetLastAcceptedEpochIndex,
67+
"cartesi_listInputs": handleListInputs,
68+
"cartesi_getInput": handleGetInput,
69+
"cartesi_getProcessedInputCount": handleGetProcessedInputCount,
70+
"cartesi_getExecutedOutputCount": handleGetExecutedOutputCount,
71+
"cartesi_getPendingExecutableOutputCount": handleGetPendingExecutableOutputCount,
72+
"cartesi_listOutputs": handleListOutputs,
73+
"cartesi_getOutput": handleGetOutput,
74+
"cartesi_listReports": handleListReports,
75+
"cartesi_getReport": handleGetReport,
76+
"cartesi_listWithdrawals": handleListWithdrawals,
77+
"cartesi_getWithdrawal": handleGetWithdrawal,
78+
"cartesi_listTournaments": handleListTournaments,
79+
"cartesi_getTournament": handleGetTournament,
80+
"cartesi_listCommitments": handleListCommitments,
81+
"cartesi_getCommitment": handleGetCommitment,
82+
"cartesi_listMatches": handleListMatches,
83+
"cartesi_getMatch": handleGetMatch,
84+
"cartesi_listMatchAdvances": handleListMatchAdvances,
85+
"cartesi_getMatchAdvanced": handleGetMatchAdvanced,
86+
"cartesi_getNodeInfo": handleGetNodeInfo,
87+
"cartesi_getChainId": handleGetChainID,
88+
"cartesi_getNodeVersion": handleGetNodeVersion,
8789
}
8890

8991
// -----------------------------------------------------------------------------
@@ -621,6 +623,52 @@ func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) (
621623
return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", processedInputs)}, nil
622624
}
623625

626+
func handleGetExecutedOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) {
627+
var params api.GetApplicationParams
628+
if err := UnmarshalParams(req.Params, &params); err != nil {
629+
s.Logger.Debug("Invalid parameters", "err", err)
630+
return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters")
631+
}
632+
633+
if err := validateNameOrAddress(params.Application); err != nil {
634+
return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err))
635+
}
636+
if err := s.applicationAbsentOrError(r, params.Application); err != nil {
637+
return nil, err
638+
}
639+
640+
count, err := s.repository.GetNumberOfExecutedOutputs(r.Context(), params.Application)
641+
if err != nil {
642+
s.Logger.Error("Unable to retrieve executed output count from repository", "err", err)
643+
return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error")
644+
}
645+
646+
return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", count)}, nil
647+
}
648+
649+
func handleGetPendingExecutableOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) {
650+
var params api.GetApplicationParams
651+
if err := UnmarshalParams(req.Params, &params); err != nil {
652+
s.Logger.Debug("Invalid parameters", "err", err)
653+
return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters")
654+
}
655+
656+
if err := validateNameOrAddress(params.Application); err != nil {
657+
return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err))
658+
}
659+
if err := s.applicationAbsentOrError(r, params.Application); err != nil {
660+
return nil, err
661+
}
662+
663+
count, err := s.repository.GetNumberOfPendingExecutableOutputs(r.Context(), params.Application)
664+
if err != nil {
665+
s.Logger.Error("Unable to retrieve pending executable output count from repository", "err", err)
666+
return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error")
667+
}
668+
669+
return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", count)}, nil
670+
}
671+
624672
func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) {
625673
var params api.ListOutputsParams
626674
if err := UnmarshalParams(req.Params, &params); err != nil {

internal/jsonrpc/jsonrpc_test.go

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,9 +911,138 @@ func TestMethod(t *testing.T) {
911911
assert.Equal(t, uint64(0), uint64(resp.Result.Data))
912912
})
913913

914-
// TODO: test with inputs (use createTestEpochWithInput)
914+
t.Run("processedInputs", func(t *testing.T) {
915+
testHistogram.inc(method)
916+
s := newTestService(t, t.Name())
917+
ctx := context.Background()
918+
919+
app := uint64(1)
920+
appID := s.newTestApplication(ctx, t, app)
921+
epoch := repotest.NewEpochBuilder(appID).
922+
WithIndex(0).
923+
WithStatus(model.EpochStatus_ClaimAccepted).
924+
Build()
925+
inputs := []*model.Input{
926+
repotest.NewInputBuilder().WithIndex(0).WithRawData(emptyInput()).Build(),
927+
repotest.NewInputBuilder().WithIndex(1).WithRawData(emptyInput()).Build(),
928+
}
929+
err := s.repository.CreateEpochsAndInputs(
930+
ctx,
931+
numberToName(app),
932+
map[*model.Epoch][]*model.Input{epoch: inputs},
933+
10,
934+
)
935+
require.NoError(t, err)
936+
s.advanceInput(ctx, t, appID, 0, 0, nil, nil)
937+
s.advanceInput(ctx, t, appID, 0, 1, nil, nil)
938+
939+
body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{
940+
"jsonrpc": "2.0",
941+
"method": "cartesi_getProcessedInputCount",
942+
"params": { "application": "%s" },
943+
"id": 0
944+
}`, numberToName(app)))
945+
946+
resp := testRPCResponse[hex64]{}
947+
require.NoError(t, json.Unmarshal(body, &resp))
948+
assert.Nil(t, resp.Error)
949+
assert.Equal(t, uint64(2), uint64(resp.Result.Data))
950+
})
915951
})
916952

953+
for _, methodName := range []string{
954+
"cartesi_getExecutedOutputCount",
955+
"cartesi_getPendingExecutableOutputCount",
956+
} {
957+
t.Run(methodName, func(t *testing.T) {
958+
method := getName(t.Name())
959+
960+
t.Run("absentApplication", func(t *testing.T) {
961+
testHistogram.inc(method)
962+
s := newTestService(t, t.Name())
963+
964+
body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{
965+
"jsonrpc": "2.0",
966+
"method": "%s",
967+
"params": { "application": "%s" },
968+
"id": 0
969+
}`, method, numberToName(1)))
970+
971+
resp := testRPCResponse[hex64]{}
972+
require.NoError(t, json.Unmarshal(body, &resp))
973+
assert.Equal(t, JSONRPC_APPLICATION_NOT_FOUND, resp.Error.Code)
974+
assert.Equal(t, "Application not found", resp.Error.Message)
975+
})
976+
977+
t.Run("existingApplicationWithNoOutputs", func(t *testing.T) {
978+
testHistogram.inc(method)
979+
s := newTestService(t, t.Name())
980+
app := uint64(1)
981+
s.newTestApplication(context.Background(), t, app)
982+
983+
body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{
984+
"jsonrpc": "2.0",
985+
"method": "%s",
986+
"params": { "application": "%s" },
987+
"id": 0
988+
}`, method, numberToName(app)))
989+
990+
resp := testRPCResponse[hex64]{}
991+
require.NoError(t, json.Unmarshal(body, &resp))
992+
assert.Nil(t, resp.Error)
993+
assert.Equal(t, uint64(0), uint64(resp.Result.Data))
994+
})
995+
996+
t.Run("outputsPresent", func(t *testing.T) {
997+
testHistogram.inc(method)
998+
s := newTestService(t, t.Name())
999+
ctx := context.Background()
1000+
1001+
app := uint64(1)
1002+
appID := s.newTestApplication(ctx, t, app)
1003+
epoch := repotest.NewEpochBuilder(appID).
1004+
WithIndex(0).
1005+
WithStatus(model.EpochStatus_ClaimAccepted).
1006+
Build()
1007+
input := repotest.NewInputBuilder().
1008+
WithIndex(0).
1009+
WithRawData(emptyInput()).
1010+
Build()
1011+
s.createTestEpochWithInput(ctx, t, numberToName(app), epoch, input)
1012+
s.advanceInput(ctx, t, appID, 0, 0, [][]byte{
1013+
emptyVoucher(),
1014+
{0x10, 0x32, 0x1e, 0x8b},
1015+
{0xc2, 0x58, 0xd6, 0xe5},
1016+
}, nil)
1017+
1018+
txHash := common.HexToHash("0x1")
1019+
err := s.repository.UpdateOutputsExecution(
1020+
ctx,
1021+
numberToName(app),
1022+
[]*model.Output{{
1023+
InputEpochApplicationID: appID,
1024+
Index: 0,
1025+
ExecutionTransactionHash: &txHash,
1026+
}},
1027+
10,
1028+
)
1029+
require.NoError(t, err)
1030+
1031+
body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{
1032+
"jsonrpc": "2.0",
1033+
"method": "%s",
1034+
"params": { "application": "%s" },
1035+
"id": 0
1036+
}`, method, numberToName(app)))
1037+
1038+
resp := testRPCResponse[hex64]{}
1039+
require.NoError(t, json.Unmarshal(body, &resp))
1040+
assert.Nil(t, resp.Error)
1041+
assert.Equal(t, uint64(1), uint64(resp.Result.Data))
1042+
})
1043+
})
1044+
}
1045+
9171046
////////////////////////////////////////////////////////////////////////
9181047
// getReport
9191048
////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)