diff --git a/README.md b/README.md index 07d95754..687f32b4 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,10 @@ enum SupernodeEventType { SIGNATURE_VERIFIED = 7; RQID_GENERATED = 8; RQID_VERIFIED = 9; - ARTEFACTS_STORED = 10; - ACTION_FINALIZED = 11; - ARTEFACTS_DOWNLOADED = 12; + FINALIZE_SIMULATED = 10; + ARTEFACTS_STORED = 11; + ACTION_FINALIZED = 12; + ARTEFACTS_DOWNLOADED = 13; } ``` diff --git a/gen/supernode/action/cascade/service.pb.go b/gen/supernode/action/cascade/service.pb.go index 42458794..9d97b6f2 100644 --- a/gen/supernode/action/cascade/service.pb.go +++ b/gen/supernode/action/cascade/service.pb.go @@ -7,10 +7,11 @@ package cascade import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -33,9 +34,11 @@ const ( SupernodeEventType_SIGNATURE_VERIFIED SupernodeEventType = 7 SupernodeEventType_RQID_GENERATED SupernodeEventType = 8 SupernodeEventType_RQID_VERIFIED SupernodeEventType = 9 - SupernodeEventType_ARTEFACTS_STORED SupernodeEventType = 10 - SupernodeEventType_ACTION_FINALIZED SupernodeEventType = 11 - SupernodeEventType_ARTEFACTS_DOWNLOADED SupernodeEventType = 12 + // Emitted by supernode when finalize transaction simulation passes + SupernodeEventType_FINALIZE_SIMULATED SupernodeEventType = 10 + SupernodeEventType_ARTEFACTS_STORED SupernodeEventType = 11 + SupernodeEventType_ACTION_FINALIZED SupernodeEventType = 12 + SupernodeEventType_ARTEFACTS_DOWNLOADED SupernodeEventType = 13 ) // Enum value maps for SupernodeEventType. @@ -51,9 +54,10 @@ var ( 7: "SIGNATURE_VERIFIED", 8: "RQID_GENERATED", 9: "RQID_VERIFIED", - 10: "ARTEFACTS_STORED", - 11: "ACTION_FINALIZED", - 12: "ARTEFACTS_DOWNLOADED", + 10: "FINALIZE_SIMULATED", + 11: "ARTEFACTS_STORED", + 12: "ACTION_FINALIZED", + 13: "ARTEFACTS_DOWNLOADED", } SupernodeEventType_value = map[string]int32{ "UNKNOWN": 0, @@ -66,9 +70,10 @@ var ( "SIGNATURE_VERIFIED": 7, "RQID_GENERATED": 8, "RQID_VERIFIED": 9, - "ARTEFACTS_STORED": 10, - "ACTION_FINALIZED": 11, - "ARTEFACTS_DOWNLOADED": 12, + "FINALIZE_SIMULATED": 10, + "ARTEFACTS_STORED": 11, + "ACTION_FINALIZED": 12, + "ARTEFACTS_DOWNLOADED": 13, } ) @@ -569,7 +574,7 @@ var file_supernode_action_cascade_service_proto_rawDesc = []byte{ 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x53, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, 0x64, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0xb6, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0xce, 0x02, 0x0a, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, 0x64, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x54, @@ -585,26 +590,27 @@ var file_supernode_action_cascade_service_proto_rawDesc = []byte{ 0x56, 0x45, 0x52, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x51, 0x49, 0x44, 0x5f, 0x47, 0x45, 0x4e, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x10, 0x08, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x51, 0x49, 0x44, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x09, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x52, 0x54, 0x45, 0x46, 0x41, 0x43, 0x54, 0x53, 0x5f, 0x53, - 0x54, 0x4f, 0x52, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x43, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x0b, 0x12, 0x18, 0x0a, - 0x14, 0x41, 0x52, 0x54, 0x45, 0x46, 0x41, 0x43, 0x54, 0x53, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, - 0x4f, 0x41, 0x44, 0x45, 0x44, 0x10, 0x0c, 0x32, 0x98, 0x01, 0x0a, 0x0e, 0x43, 0x61, 0x73, 0x63, - 0x61, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, - 0x41, 0x0a, 0x08, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x18, 0x2e, 0x63, 0x61, - 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, - 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x30, 0x01, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x4c, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, - 0x73, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x73, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2f, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x09, 0x12, 0x16, 0x0a, 0x12, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x5f, 0x53, 0x49, + 0x4d, 0x55, 0x4c, 0x41, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x52, 0x54, + 0x45, 0x46, 0x41, 0x43, 0x54, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x45, 0x44, 0x10, 0x0b, 0x12, + 0x14, 0x0a, 0x10, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, + 0x5a, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x52, 0x54, 0x45, 0x46, 0x41, 0x43, + 0x54, 0x53, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x45, 0x44, 0x10, 0x0d, 0x32, + 0x98, 0x01, 0x0a, 0x0e, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x18, + 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, + 0x64, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x08, 0x44, 0x6f, 0x77, 0x6e, 0x6c, + 0x6f, 0x61, 0x64, 0x12, 0x18, 0x2e, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, + 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4c, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, 0x64, + 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x75, 0x70, 0x65, 0x72, 0x6e, 0x6f, + 0x64, 0x65, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/lumera/Readme.md b/pkg/lumera/Readme.md index c1ef31fa..4c33b34f 100644 --- a/pkg/lumera/Readme.md +++ b/pkg/lumera/Readme.md @@ -1,4 +1,4 @@ -## Lumera Client (Slim Guide) +## Lumera Client A minimal guide to the Lumera client @@ -47,7 +47,7 @@ Send actions (ActionMsg) RequestAction: ```go -resp, err := cli.ActionMsg().RequesAction( +resp, err := cli.ActionMsg().RequestAction( ctx, "CASCADE", metadataJSON, // stringified JSON @@ -73,5 +73,4 @@ Validation rules (built-in) Notes -- Method name is currently `RequesAction` (typo kept for compatibility). - Tx uses simulation + adjustment + padding before sign/broadcast. diff --git a/pkg/lumera/modules/action_msg/action_msg_mock.go b/pkg/lumera/modules/action_msg/action_msg_mock.go index fbdce84e..c0d74bea 100644 --- a/pkg/lumera/modules/action_msg/action_msg_mock.go +++ b/pkg/lumera/modules/action_msg/action_msg_mock.go @@ -56,17 +56,32 @@ func (mr *MockModuleMockRecorder) FinalizeCascadeAction(ctx, actionId, rqIdsIds return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).FinalizeCascadeAction), ctx, actionId, rqIdsIds) } -// RequesAction mocks base method. -func (m *MockModule) RequesAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*tx.BroadcastTxResponse, error) { +// RequestAction mocks base method. +func (m *MockModule) RequestAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*tx.BroadcastTxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequesAction", ctx, actionType, metadata, price, expirationTime) + ret := m.ctrl.Call(m, "RequestAction", ctx, actionType, metadata, price, expirationTime) ret0, _ := ret[0].(*tx.BroadcastTxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } -// RequesAction indicates an expected call of RequesAction. -func (mr *MockModuleMockRecorder) RequesAction(ctx, actionType, metadata, price, expirationTime any) *gomock.Call { +// RequestAction indicates an expected call of RequestAction. +func (mr *MockModuleMockRecorder) RequestAction(ctx, actionType, metadata, price, expirationTime any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequesAction", reflect.TypeOf((*MockModule)(nil).RequesAction), ctx, actionType, metadata, price, expirationTime) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestAction", reflect.TypeOf((*MockModule)(nil).RequestAction), ctx, actionType, metadata, price, expirationTime) +} + +// SimulateFinalizeCascadeAction mocks base method. +func (m *MockModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*tx.SimulateResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SimulateFinalizeCascadeAction", ctx, actionId, rqIdsIds) + ret0, _ := ret[0].(*tx.SimulateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SimulateFinalizeCascadeAction indicates an expected call of SimulateFinalizeCascadeAction. +func (mr *MockModuleMockRecorder) SimulateFinalizeCascadeAction(ctx, actionId, rqIdsIds any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SimulateFinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).SimulateFinalizeCascadeAction), ctx, actionId, rqIdsIds) } diff --git a/pkg/lumera/modules/action_msg/impl.go b/pkg/lumera/modules/action_msg/impl.go index 33b6ac50..b2329e21 100644 --- a/pkg/lumera/modules/action_msg/impl.go +++ b/pkg/lumera/modules/action_msg/impl.go @@ -44,7 +44,7 @@ func newModule(conn *grpc.ClientConn, authmodule auth.Module, txmodule txmod.Mod }, nil } -func (m *module) RequesAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*sdktx.BroadcastTxResponse, error) { +func (m *module) RequestAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*sdktx.BroadcastTxResponse, error) { if err := validateRequestActionParams(actionType, metadata, price, expirationTime); err != nil { return nil, err } @@ -74,3 +74,31 @@ func (m *module) SetTxHelperConfig(config *txmod.TxHelperConfig) { func (m *module) GetTxHelper() *txmod.TxHelper { return m.txHelper } + +// SimulateFinalizeCascadeAction builds the finalize message and performs a simulation +// without broadcasting the transaction. This is useful to ensure the transaction +// would pass ante/ValidateBasic before doing irreversible work. +func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error) { + if err := validateFinalizeActionParams(actionId, rqIdsIds); err != nil { + return nil, err + } + + // Gather account info and creator address + accountInfo, err := m.txHelper.GetAccountInfo(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get account info: %w", err) + } + creator, err := m.txHelper.GetCreatorAddress() + if err != nil { + return nil, fmt.Errorf("failed to get creator address: %w", err) + } + + // Build the finalize message + msg, err := createFinalizeActionMessage(creator, actionId, rqIdsIds) + if err != nil { + return nil, err + } + + // Run simulation using tx helper + return m.txHelper.Simulate(ctx, []types.Msg{msg}, accountInfo) +} diff --git a/pkg/lumera/modules/action_msg/interface.go b/pkg/lumera/modules/action_msg/interface.go index 3d073e61..f45af851 100644 --- a/pkg/lumera/modules/action_msg/interface.go +++ b/pkg/lumera/modules/action_msg/interface.go @@ -13,8 +13,10 @@ import ( type Module interface { // FinalizeCascadeAction finalizes a CASCADE action with the given parameters - RequesAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*sdktx.BroadcastTxResponse, error) + RequestAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*sdktx.BroadcastTxResponse, error) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.BroadcastTxResponse, error) + // SimulateFinalizeCascadeAction simulates the finalize action (no broadcast) + SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error) } func NewModule(conn *grpc.ClientConn, authmod auth.Module, txmodule tx.Module, kr keyring.Keyring, keyName string, chainID string) (Module, error) { diff --git a/pkg/lumera/modules/tx/helper.go b/pkg/lumera/modules/tx/helper.go index 9218c542..1da9cbb0 100644 --- a/pkg/lumera/modules/tx/helper.go +++ b/pkg/lumera/modules/tx/helper.go @@ -135,46 +135,52 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount, // UpdateConfig allows updating the transaction configuration func (h *TxHelper) UpdateConfig(config *TxHelperConfig) { - // Merge provided fields with existing config to avoid zeroing defaults - if h.config == nil { - h.config = &TxConfig{} - } - - // ChainID - if config.ChainID != "" { - h.config.ChainID = config.ChainID - } - // Keyring - if config.Keyring != nil { - h.config.Keyring = config.Keyring - } - // KeyName - if config.KeyName != "" { - h.config.KeyName = config.KeyName - } - // GasLimit - if config.GasLimit != 0 { - h.config.GasLimit = config.GasLimit - } - // GasAdjustment - if config.GasAdjustment != 0 { - h.config.GasAdjustment = config.GasAdjustment - } - // GasPadding - if config.GasPadding != 0 { - h.config.GasPadding = config.GasPadding - } - // FeeDenom - if config.FeeDenom != "" { - h.config.FeeDenom = config.FeeDenom - } - // GasPrice - if config.GasPrice != "" { - h.config.GasPrice = config.GasPrice - } + // Merge provided fields with existing config to avoid zeroing defaults + if h.config == nil { + h.config = &TxConfig{} + } + + // ChainID + if config.ChainID != "" { + h.config.ChainID = config.ChainID + } + // Keyring + if config.Keyring != nil { + h.config.Keyring = config.Keyring + } + // KeyName + if config.KeyName != "" { + h.config.KeyName = config.KeyName + } + // GasLimit + if config.GasLimit != 0 { + h.config.GasLimit = config.GasLimit + } + // GasAdjustment + if config.GasAdjustment != 0 { + h.config.GasAdjustment = config.GasAdjustment + } + // GasPadding + if config.GasPadding != 0 { + h.config.GasPadding = config.GasPadding + } + // FeeDenom + if config.FeeDenom != "" { + h.config.FeeDenom = config.FeeDenom + } + // GasPrice + if config.GasPrice != "" { + h.config.GasPrice = config.GasPrice + } } // GetConfig returns the current transaction configuration func (h *TxHelper) GetConfig() *TxConfig { return h.config } + +// Simulate runs an offline simulation for the provided messages using the +// configured tx settings and given account info. Useful for pre-flight checks. +func (h *TxHelper) Simulate(ctx context.Context, msgs []types.Msg, accountInfo *authtypes.BaseAccount) (*sdktx.SimulateResponse, error) { + return h.txmod.SimulateTransaction(ctx, msgs, accountInfo, h.config) +} diff --git a/pkg/testutil/lumera.go b/pkg/testutil/lumera.go index 6e7404ac..7f5b778c 100644 --- a/pkg/testutil/lumera.go +++ b/pkg/testutil/lumera.go @@ -123,16 +123,9 @@ func (m *MockActionModule) GetParams(ctx context.Context) (*types.QueryParamsRes type MockActionMsgModule struct{} // RequestAction mocks the behavior of requesting an action. -// Adjust the signature and return values as needed to match the actual interface. -func (m *MockActionMsgModule) RequestAction(ctx context.Context, req *types.MsgRequestAction) (*sdktx.BroadcastTxResponse, error) { - // Mock implementation returns success with empty result - return &sdktx.BroadcastTxResponse{}, nil -} - -// RequesAction is a stub to satisfy the action_msg.Module interface in case of typo in interface definition. -func (m *MockActionMsgModule) RequesAction(ctx context.Context, arg1, arg2, arg3, arg4 string) (*sdktx.BroadcastTxResponse, error) { - // Mock implementation returns success with empty result - return &sdktx.BroadcastTxResponse{}, nil +func (m *MockActionMsgModule) RequestAction(ctx context.Context, actionType, metadata, price, expirationTime string) (*sdktx.BroadcastTxResponse, error) { + // Mock implementation returns success with empty result + return &sdktx.BroadcastTxResponse{}, nil } // FinalizeCascadeAction implements the required method from action_msg.Module interface @@ -141,6 +134,12 @@ func (m *MockActionMsgModule) FinalizeCascadeAction(ctx context.Context, actionI return &sdktx.BroadcastTxResponse{}, nil } +// SimulateFinalizeCascadeAction mocks simulation of finalize action. +func (m *MockActionMsgModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, signatures []string) (*sdktx.SimulateResponse, error) { + // Mock implementation returns empty simulation response + return &sdktx.SimulateResponse{}, nil +} + // MockSupernodeModule implements the supernode.Module interface for testing type MockSupernodeModule struct { addresses []string diff --git a/proto/supernode/action/cascade/service.proto b/proto/supernode/action/cascade/service.proto index 71f9b6ac..166ec0a2 100644 --- a/proto/supernode/action/cascade/service.proto +++ b/proto/supernode/action/cascade/service.proto @@ -57,8 +57,8 @@ enum SupernodeEventType { SIGNATURE_VERIFIED = 7; RQID_GENERATED = 8; RQID_VERIFIED = 9; - ARTEFACTS_STORED = 10; - ACTION_FINALIZED = 11; - ARTEFACTS_DOWNLOADED = 12; + FINALIZE_SIMULATED = 10; + ARTEFACTS_STORED = 11; + ACTION_FINALIZED = 12; + ARTEFACTS_DOWNLOADED = 13; } - diff --git a/sdk/README.md b/sdk/README.md index c2a94f44..a026d162 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -379,11 +379,14 @@ The SDK provides an event system to monitor task progress through event subscrip - `SupernodeSignatureVerified`: Signature verification passed - `SupernodeRQIDGenerated`: RaptorQ ID generated - `SupernodeRQIDVerified`: RaptorQ ID verified +- `SupernodeFinalizeSimulated`: Finalize transaction simulated successfully (pre-storage) - `SupernodeArtefactsStored`: Artifacts stored successfully - `SupernodeActionFinalized`: Action processing finalized - `SupernodeArtefactsDownloaded`: Artifacts downloaded - `SupernodeUnknown`: Unknown supernode event +Note: For backward compatibility, older supernodes may emit the finalize simulation as an `RQID_VERIFIED` event with the message `"finalize action simulation passed"`. The SDK adapter maps this to `SupernodeFinalizeSimulated` automatically. + ### Event Data Keys Events may include additional data accessible through these keys: @@ -415,4 +418,3 @@ err := client.SubscribeToEvents(ctx, event.SDKTaskCompleted, func(ctx context.Co err := client.SubscribeToAllEvents(ctx, func(ctx context.Context, e event.Event) { fmt.Printf("Event: %s for task %s\n", e.Type, e.TaskID) }) - diff --git a/sdk/adapters/supernodeservice/adapter.go b/sdk/adapters/supernodeservice/adapter.go index daa52114..c6475326 100644 --- a/sdk/adapters/supernodeservice/adapter.go +++ b/sdk/adapters/supernodeservice/adapter.go @@ -6,6 +6,8 @@ import ( "io" "os" "path/filepath" + "regexp" + "strconv" "github.com/LumeraProtocol/supernode/v2/gen/supernode" "github.com/LumeraProtocol/supernode/v2/gen/supernode/action/cascade" @@ -40,15 +42,15 @@ func NewCascadeAdapter(ctx context.Context, conn *grpc.ClientConn, logger log.Lo // to balance throughput and memory usage func calculateOptimalChunkSize(fileSize int64) int { const ( - minChunkSize = 64 * 1024 // 64 KB minimum - maxChunkSize = 4 * 1024 * 1024 // 4 MB maximum for 1GB+ files - smallFileThreshold = 1024 * 1024 // 1 MB + minChunkSize = 64 * 1024 // 64 KB minimum + maxChunkSize = 4 * 1024 * 1024 // 4 MB maximum for 1GB+ files + smallFileThreshold = 1024 * 1024 // 1 MB mediumFileThreshold = 50 * 1024 * 1024 // 50 MB - largeFileThreshold = 500 * 1024 * 1024 // 500 MB + largeFileThreshold = 500 * 1024 * 1024 // 500 MB ) var chunkSize int - + switch { case fileSize <= smallFileThreshold: // For small files (up to 1MB), use 64KB chunks @@ -63,7 +65,7 @@ func calculateOptimalChunkSize(fileSize int64) int { // For very large files (500MB+), use 4MB chunks for optimal throughput chunkSize = maxChunkSize } - + // Ensure chunk size is within bounds if chunkSize < minChunkSize { chunkSize = minChunkSize @@ -71,7 +73,7 @@ func calculateOptimalChunkSize(fileSize int64) int { if chunkSize > maxChunkSize { chunkSize = maxChunkSize } - + return chunkSize } @@ -106,16 +108,16 @@ func (a *cascadeAdapter) CascadeSupernodeRegister(ctx context.Context, in *Casca // Validate file size before starting upload if totalBytes > maxFileSize { - a.logger.Error(ctx, "File exceeds maximum size limit", - "filePath", in.FilePath, - "fileSize", totalBytes, + a.logger.Error(ctx, "File exceeds maximum size limit", + "filePath", in.FilePath, + "fileSize", totalBytes, "maxSize", maxFileSize) return nil, fmt.Errorf("file size %d bytes exceeds maximum allowed size of 1GB", totalBytes) } // Define adaptive chunk size based on file size chunkSize := calculateOptimalChunkSize(totalBytes) - + a.logger.Debug(ctx, "Calculated optimal chunk size", "fileSize", totalBytes, "chunkSize", chunkSize) // Keep track of how much data we've processed @@ -194,13 +196,20 @@ func (a *cascadeAdapter) CascadeSupernodeRegister(ctx context.Context, in *Casca a.logger.Info(ctx, "Supernode progress update received", "event_type", resp.EventType, "message", resp.Message, "tx_hash", resp.TxHash, "task_id", in.TaskId, "action_id", in.ActionID) if in.EventLogger != nil { - in.EventLogger(ctx, toSdkEvent(resp.EventType), resp.Message, event.EventData{ + edata := event.EventData{ event.KeyEventType: resp.EventType, event.KeyMessage: resp.Message, event.KeyTxHash: resp.TxHash, event.KeyTaskID: in.TaskId, event.KeyActionID: in.ActionID, - }) + } + // Extract success rate if provided in message format: "... success_rate=NN.NN%" + if resp.EventType == cascade.SupernodeEventType_ARTEFACTS_STORED { + if rate, ok := parseSuccessRate(resp.Message); ok { + edata[event.KeySuccessRate] = rate + } + } + in.EventLogger(ctx, toSdkEventWithMessage(resp.EventType, resp.Message), resp.Message, edata) } // Optionally capture the final response @@ -347,11 +356,36 @@ func toSdkEvent(e cascade.SupernodeEventType) event.EventType { return event.SupernodeActionFinalized case cascade.SupernodeEventType_ARTEFACTS_DOWNLOADED: return event.SupernodeArtefactsDownloaded + case cascade.SupernodeEventType_FINALIZE_SIMULATED: + return event.SupernodeFinalizeSimulated default: return event.SupernodeUnknown } } +// toSdkEventWithMessage extends event mapping using message content for finer granularity +func toSdkEventWithMessage(e cascade.SupernodeEventType, msg string) event.EventType { + // Detect finalize simulation pass piggybacked on RQID_VERIFIED + if e == cascade.SupernodeEventType_RQID_VERIFIED && msg == "finalize action simulation passed" { + return event.SupernodeFinalizeSimulated + } + return toSdkEvent(e) +} + +var rateRe = regexp.MustCompile(`success_rate=([0-9]+(?:\.[0-9]+)?)%`) + +func parseSuccessRate(msg string) (float64, bool) { + m := rateRe.FindStringSubmatch(msg) + if len(m) != 2 { + return 0, false + } + f, err := strconv.ParseFloat(m[1], 64) + if err != nil { + return 0, false + } + return f, true +} + func toSdkSupernodeStatus(resp *supernode.StatusResponse) *SupernodeStatusresponse { result := &SupernodeStatusresponse{} result.Version = resp.Version @@ -384,7 +418,7 @@ func toSdkSupernodeStatus(resp *supernode.StatusResponse) *SupernodeStatusrespon UsagePercent: storage.UsagePercent, }) } - + // Copy hardware summary result.Resources.HardwareSummary = resp.Resources.HardwareSummary } diff --git a/sdk/adapters/supernodeservice/types_test.go b/sdk/adapters/supernodeservice/types_test.go index 6c6ca361..f7a9cf57 100644 --- a/sdk/adapters/supernodeservice/types_test.go +++ b/sdk/adapters/supernodeservice/types_test.go @@ -25,6 +25,7 @@ func TestTranslateSupernodeEvent(t *testing.T) { {"RQID_VERIFIED", cascade.SupernodeEventType_RQID_VERIFIED, event.SupernodeRQIDVerified}, {"ARTEFACTS_STORED", cascade.SupernodeEventType_ARTEFACTS_STORED, event.SupernodeArtefactsStored}, {"ACTION_FINALIZED", cascade.SupernodeEventType_ACTION_FINALIZED, event.SupernodeActionFinalized}, + {"FINALIZE_SIMULATED", cascade.SupernodeEventType_FINALIZE_SIMULATED, event.SupernodeFinalizeSimulated}, } for _, tt := range tests { diff --git a/sdk/event/keys.go b/sdk/event/keys.go index 728c4276..c0e24cc8 100644 --- a/sdk/event/keys.go +++ b/sdk/event/keys.go @@ -15,6 +15,7 @@ const ( KeyProgress EventDataKey = "progress" KeyEventType EventDataKey = "event_type" KeyOutputPath EventDataKey = "output_path" + KeySuccessRate EventDataKey = "success_rate" // Task specific keys KeyTaskID EventDataKey = "task_id" diff --git a/sdk/event/progress.go b/sdk/event/progress.go deleted file mode 100644 index b1b7a27e..00000000 --- a/sdk/event/progress.go +++ /dev/null @@ -1,94 +0,0 @@ -package event - -// ProgressInfo provides comprehensive progress information -type ProgressInfo struct { - Percentage int // Percentage complete (0-100) - Status TaskStatus // Current status (pending, active, completed, failed) - CurrentEvent EventType // The current event type - IsErrorState bool // Whether this is an error state - -} - -// TaskStatus represents the current status of a task -type TaskStatus string - -const ( - StatusPending TaskStatus = "PENDING" - StatusActive TaskStatus = "ACTIVE" - StatusCompleted TaskStatus = "COMPLETED" - StatusFailed TaskStatus = "FAILED" -) - -// eventProgressMap maps event types to their progress percentages -var eventProgressMap = map[EventType]int{ - //SDK - SDKTaskStarted: 0, - SDKSupernodesUnavailable: 5, - SDKSupernodesFound: 10, - SDKRegistrationAttempt: 12, - SDKRegistrationFailure: 12, - //Supernode - SupernodeActionRetrieved: 15, - SupernodeActionFeeVerified: 20, - SupernodeTopCheckPassed: 25, - SupernodeMetadataDecoded: 30, - SupernodeDataHashVerified: 35, - SupernodeInputEncoded: 40, - SupernodeSignatureVerified: 45, - SupernodeRQIDGenerated: 50, - SupernodeRQIDVerified: 55, - SupernodeArtefactsStored: 60, - SupernodeActionFinalized: 80, - // SDK - SDKTaskTxHashReceived: 97, - SDKRegistrationSuccessful: 99, - SDKTaskCompleted: 100, - SDKTaskFailed: 100, -} - -// GetProgressFromEvent calculates progress information from a single event -func GetProgressFromEvent(e EventType) ProgressInfo { - // Determine percentage - percentage := 0 - if p, exists := eventProgressMap[e]; exists { - percentage = p - } - - // Determine status - var status TaskStatus - isError := false - - switch e { - case SDKTaskStarted: - status = StatusActive - case SDKTaskCompleted: - status = StatusCompleted - case SDKTaskFailed: - status = StatusFailed - isError = true - default: - status = StatusActive - } - - return ProgressInfo{ - Percentage: percentage, - Status: status, - CurrentEvent: e, - IsErrorState: isError, - } -} - -// GetLatestProgress calculates progress info from the most recent event in a list -func GetLatestProgress(events []Event) ProgressInfo { - if len(events) == 0 { - return ProgressInfo{ - Percentage: 0, - Status: StatusPending, - CurrentEvent: "", - IsErrorState: false, - } - } - - // Return progress based on the latest event - return GetProgressFromEvent(events[len(events)-1].Type) -} diff --git a/sdk/event/types.go b/sdk/event/types.go index 14ae6006..dd040f1f 100644 --- a/sdk/event/types.go +++ b/sdk/event/types.go @@ -40,6 +40,7 @@ const ( SupernodeSignatureVerified EventType = "supernode:signature_verified" SupernodeRQIDGenerated EventType = "supernode:rqid_generated" SupernodeRQIDVerified EventType = "supernode:rqid_verified" + SupernodeFinalizeSimulated EventType = "supernode:finalize_simulated" SupernodeArtefactsStored EventType = "supernode:artefacts_stored" SupernodeActionFinalized EventType = "supernode:action_finalized" SupernodeArtefactsDownloaded EventType = "supernode:artefacts_downloaded" diff --git a/sdk/task/cache.go b/sdk/task/cache.go index 0e144253..625f38bd 100644 --- a/sdk/task/cache.go +++ b/sdk/task/cache.go @@ -7,7 +7,6 @@ import ( "time" "github.com/LumeraProtocol/supernode/v2/sdk/event" - eventspkg "github.com/LumeraProtocol/supernode/v2/sdk/event" "github.com/LumeraProtocol/supernode/v2/sdk/log" "github.com/dgraph-io/ristretto/v2" ) @@ -17,7 +16,7 @@ type TaskEntry struct { TaskID string ActionID string TaskType TaskType - Status eventspkg.TaskStatus + Status TaskStatus TxHash string Error error Events []event.Event @@ -32,6 +31,16 @@ type TaskCache struct { taskLocks sync.Map } +// TaskStatus represents the current status of a task +type TaskStatus string + +const ( + StatusPending TaskStatus = "PENDING" + StatusActive TaskStatus = "ACTIVE" + StatusCompleted TaskStatus = "COMPLETED" + StatusFailed TaskStatus = "FAILED" +) + func NewTaskCache(ctx context.Context, logger log.Logger) (*TaskCache, error) { if logger == nil { logger = log.NewNoopLogger() @@ -79,7 +88,7 @@ func (tc *TaskCache) Set(ctx context.Context, taskID string, task Task, taskType TaskID: taskID, ActionID: actionID, TaskType: taskType, - Status: eventspkg.StatusPending, + Status: StatusPending, Events: make([]event.Event, 0), CreatedAt: now, LastUpdatedAt: now, @@ -103,13 +112,8 @@ func (tc *TaskCache) Get(ctx context.Context, taskID string) (*TaskEntry, bool) return entry, found } -// GetProgress returns the current progress information for the task -func (t *TaskEntry) GetProgress() eventspkg.ProgressInfo { - return eventspkg.GetLatestProgress(t.Events) -} - // UpdateStatus updates the status of a task in the cache atomically -func (tc *TaskCache) UpdateStatus(ctx context.Context, taskID string, status eventspkg.TaskStatus, err error) bool { +func (tc *TaskCache) UpdateStatus(ctx context.Context, taskID string, status TaskStatus, err error) bool { mu := tc.getOrCreateMutex(taskID) mu.Lock() defer mu.Unlock() diff --git a/sdk/task/cascade.go b/sdk/task/cascade.go index 11577ac2..cb46ef6e 100644 --- a/sdk/task/cascade.go +++ b/sdk/task/cascade.go @@ -12,8 +12,8 @@ import ( ) const ( - registrationTimeout = 120 * time.Second // Timeout for registration requests - connectionTimeout = 10 * time.Second // Timeout for connection requests + registrationTimeout = 5 * time.Minute // Timeout for registration requests + connectionTimeout = 10 * time.Second // Timeout for connection requests ) type CascadeTask struct { @@ -95,7 +95,7 @@ func (t *CascadeTask) registerWithSupernodes(ctx context.Context, supernodes lum lastErr = err continue } - t.LogEvent(ctx, event.SDKRegistrationSuccessful, "successfully registratered with supernode", event.EventData{ + t.LogEvent(ctx, event.SDKRegistrationSuccessful, "successfully registered with supernode", event.EventData{ event.KeySupernode: sn.GrpcEndpoint, event.KeySupernodeAddress: sn.CosmosAddress, event.KeyIteration: idx + 1, diff --git a/sdk/task/manager.go b/sdk/task/manager.go index 2f8c4aa6..052088f3 100644 --- a/sdk/task/manager.go +++ b/sdk/task/manager.go @@ -8,7 +8,6 @@ import ( "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" "github.com/LumeraProtocol/supernode/v2/sdk/config" "github.com/LumeraProtocol/supernode/v2/sdk/event" - taskstatus "github.com/LumeraProtocol/supernode/v2/sdk/event" "github.com/LumeraProtocol/supernode/v2/sdk/log" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/google/uuid" @@ -94,7 +93,7 @@ func NewManagerWithLumeraClient(ctx context.Context, config config.Config, logge func (m *ManagerImpl) CreateCascadeTask(ctx context.Context, filePath string, actionID, signature string) (string, error) { // Create a detached context immediately to prevent HTTP request cancellation taskCtx, cancel := context.WithCancel(context.Background()) - + // First validate the action before creating the task action, err := m.validateAction(taskCtx, actionID) if err != nil { @@ -214,10 +213,10 @@ func (m *ManagerImpl) handleEvent(ctx context.Context, e event.Event) { switch e.Type { case event.SDKTaskStarted: m.logger.Info(ctx, "Task started", "taskID", e.TaskID, "taskType", e.TaskType) - m.taskCache.UpdateStatus(ctx, e.TaskID, taskstatus.StatusActive, nil) + m.taskCache.UpdateStatus(ctx, e.TaskID, StatusActive, nil) case event.SDKTaskCompleted: m.logger.Info(ctx, "Task completed", "taskID", e.TaskID, "taskType", e.TaskType) - m.taskCache.UpdateStatus(ctx, e.TaskID, taskstatus.StatusCompleted, nil) + m.taskCache.UpdateStatus(ctx, e.TaskID, StatusCompleted, nil) case event.SDKTaskFailed: var err error if errMsg, ok := e.Data[event.KeyError].(string); ok { @@ -226,7 +225,7 @@ func (m *ManagerImpl) handleEvent(ctx context.Context, e event.Event) { } else { m.logger.Error(ctx, "Task failed with unknown error", "taskID", e.TaskID, "taskType", e.TaskType) } - m.taskCache.UpdateStatus(ctx, e.TaskID, taskstatus.StatusFailed, err) + m.taskCache.UpdateStatus(ctx, e.TaskID, StatusFailed, err) case event.SDKTaskTxHashReceived: // Capture and store transaction hash from event if txHash, ok := e.Data[event.KeyTxHash].(string); ok && txHash != "" { @@ -261,7 +260,7 @@ func (m *ManagerImpl) Close(ctx context.Context) { func (m *ManagerImpl) CreateDownloadTask(ctx context.Context, actionID string, outputDir string, signature string) (string, error) { // Create a detached context immediately to prevent HTTP request cancellation taskCtx, cancel := context.WithCancel(context.Background()) - + // First validate the action before creating the task action, err := m.validateDownloadAction(taskCtx, actionID) if err != nil { diff --git a/supernode/services/cascade/adaptors/lumera.go b/supernode/services/cascade/adaptors/lumera.go index 93b93efa..f5e3b52e 100644 --- a/supernode/services/cascade/adaptors/lumera.go +++ b/supernode/services/cascade/adaptors/lumera.go @@ -20,6 +20,7 @@ type LumeraClient interface { // Action Module GetAction(ctx context.Context, actionID string) (*actiontypes.QueryGetActionResponse, error) FinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.BroadcastTxResponse, error) + SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.SimulateResponse, error) GetActionFee(ctx context.Context, dataSize string) (*actiontypes.QueryGetActionFeeResponse, error) // Auth Verify(ctx context.Context, creator string, file []byte, sigBytes []byte) error @@ -67,6 +68,10 @@ func (c *Client) FinalizeAction(ctx context.Context, actionID string, rqids []st return resp, nil } +func (c *Client) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.SimulateResponse, error) { + return c.lc.ActionMsg().SimulateFinalizeCascadeAction(ctx, actionID, rqids) +} + func (c *Client) GetTopSupernodes(ctx context.Context, height uint64) (*sntypes.QueryGetTopSuperNodesForBlockResponse, error) { return c.lc.SuperNode().GetTopSuperNodesForBlock(ctx, height) } diff --git a/supernode/services/cascade/adaptors/mocks/lumera_mock.go b/supernode/services/cascade/adaptors/mocks/lumera_mock.go index 84ead7ac..15a6c901 100644 --- a/supernode/services/cascade/adaptors/mocks/lumera_mock.go +++ b/supernode/services/cascade/adaptors/mocks/lumera_mock.go @@ -58,6 +58,21 @@ func (mr *MockLumeraClientMockRecorder) FinalizeAction(ctx, actionID, rqids any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeAction", reflect.TypeOf((*MockLumeraClient)(nil).FinalizeAction), ctx, actionID, rqids) } +// SimulateFinalizeAction mocks base method. +func (m *MockLumeraClient) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*tx.SimulateResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SimulateFinalizeAction", ctx, actionID, rqids) + ret0, _ := ret[0].(*tx.SimulateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SimulateFinalizeAction indicates an expected call of SimulateFinalizeAction. +func (mr *MockLumeraClientMockRecorder) SimulateFinalizeAction(ctx, actionID, rqids any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SimulateFinalizeAction", reflect.TypeOf((*MockLumeraClient)(nil).SimulateFinalizeAction), ctx, actionID, rqids) +} + // GetAction mocks base method. func (m *MockLumeraClient) GetAction(ctx context.Context, actionID string) (*types.QueryGetActionResponse, error) { m.ctrl.T.Helper() diff --git a/supernode/services/cascade/events.go b/supernode/services/cascade/events.go index 09d517be..d878fc25 100644 --- a/supernode/services/cascade/events.go +++ b/supernode/services/cascade/events.go @@ -13,7 +13,8 @@ const ( SupernodeEventTypeSignatureVerified SupernodeEventType = 7 SupernodeEventTypeRQIDsGenerated SupernodeEventType = 8 SupernodeEventTypeRqIDsVerified SupernodeEventType = 9 - SupernodeEventTypeArtefactsStored SupernodeEventType = 10 - SupernodeEventTypeActionFinalized SupernodeEventType = 11 - SupernodeEventTypeArtefactsDownloaded SupernodeEventType = 12 + SupernodeEventTypeFinalizeSimulated SupernodeEventType = 10 + SupernodeEventTypeArtefactsStored SupernodeEventType = 11 + SupernodeEventTypeActionFinalized SupernodeEventType = 12 + SupernodeEventTypeArtefactsDownloaded SupernodeEventType = 13 ) diff --git a/supernode/services/cascade/events_test.go b/supernode/services/cascade/events_test.go index ce6ff782..ddf98871 100644 --- a/supernode/services/cascade/events_test.go +++ b/supernode/services/cascade/events_test.go @@ -20,8 +20,9 @@ func TestSupernodeEventTypeValues(t *testing.T) { {"SignatureVerified", SupernodeEventTypeSignatureVerified, 7}, {"RQIDsGenerated", SupernodeEventTypeRQIDsGenerated, 8}, {"RqIDsVerified", SupernodeEventTypeRqIDsVerified, 9}, - {"ArtefactsStored", SupernodeEventTypeArtefactsStored, 10}, - {"ActionFinalized", SupernodeEventTypeActionFinalized, 11}, + {"FinalizeSimulated", SupernodeEventTypeFinalizeSimulated, 10}, + {"ArtefactsStored", SupernodeEventTypeArtefactsStored, 11}, + {"ActionFinalized", SupernodeEventTypeActionFinalized, 12}, } for _, tt := range tests { diff --git a/supernode/services/cascade/register.go b/supernode/services/cascade/register.go index 7ccb9fc3..b3b30346 100644 --- a/supernode/services/cascade/register.go +++ b/supernode/services/cascade/register.go @@ -144,7 +144,17 @@ func (task *CascadeRegistrationTask) Register( logtrace.Info(ctx, "rq-ids have been verified", fields) task.streamEvent(SupernodeEventTypeRqIDsVerified, "rq-ids have been verified", "", send) - /* 10. Persist artefacts -------------------------------------------------------- */ + /* 10. Simulate finalize to avoid storing artefacts if it would fail ---------- */ + if _, err := task.LumeraClient.SimulateFinalizeAction(ctx, action.ActionID, rqidResp.RQIDs); err != nil { + fields[logtrace.FieldError] = err.Error() + logtrace.Info(ctx, "finalize action simulation failed", fields) + return task.wrapErr(ctx, "finalize action simulation failed", err, fields) + } + logtrace.Info(ctx, "finalize action simulation passed", fields) + // Transmit as a standard event so SDK can propagate it (dedicated type) + task.streamEvent(SupernodeEventTypeFinalizeSimulated, "finalize action simulation passed", "", send) + + /* 11. Persist artefacts ------------------------------------------------------- */ if err := task.storeArtefacts(ctx, action.ActionID, rqidResp.RedundantMetadataFiles, encResp.SymbolsDir, fields); err != nil { return err } diff --git a/supernode/services/cascade/register_test.go b/supernode/services/cascade/register_test.go index 2c236c4e..109941c4 100644 --- a/supernode/services/cascade/register_test.go +++ b/supernode/services/cascade/register_test.go @@ -83,15 +83,20 @@ func TestCascadeRegistrationTask_Register(t *testing.T) { Return(nil). Times(2) - // 4. Finalize + // 4. Simulate finalize should pass + lc.EXPECT(). + SimulateFinalizeAction(gomock.Any(), "action123", gomock.Any()). + Return(&sdktx.SimulateResponse{}, nil) + + // 5. Finalize lc.EXPECT(). FinalizeAction(gomock.Any(), "action123", gomock.Any()). Return(&sdktx.BroadcastTxResponse{TxResponse: &sdk.TxResponse{TxHash: "tx123"}}, nil) - // 5. Params (if used in fee check) + // 6. Params (if used in fee check) lc.EXPECT().GetActionFee(gomock.Any(), "10").Return(&actiontypes.QueryGetActionFeeResponse{Amount: "1000"}, nil) - // 6. Encode input + // 7. Encode input codec.EXPECT(). EncodeInput(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(adaptors.EncodeResult{ @@ -99,13 +104,13 @@ func TestCascadeRegistrationTask_Register(t *testing.T) { Metadata: codecpkg.Layout{Blocks: []codecpkg.Block{{BlockID: 1, Hash: "abc"}}}, }, nil) - // 7. Store artefacts + // 8. Store artefacts p2p.EXPECT(). StoreArtefacts(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) }, expectedError: "", - expectedEvents: 11, + expectedEvents: 12, }, { name: "get-action fails", diff --git a/tests/system/e2e_cascade_test.go b/tests/system/e2e_cascade_test.go index 11368cf1..e457ccd0 100644 --- a/tests/system/e2e_cascade_test.go +++ b/tests/system/e2e_cascade_test.go @@ -345,7 +345,7 @@ func TestCascadeE2E(t *testing.T) { // "--gas-adjustment", "1.5", // ) - response, err := lumeraClinet.ActionMsg().RequesAction(ctx, actionType, metadata, price, expirationTime) + response, err := lumeraClinet.ActionMsg().RequestAction(ctx, actionType, metadata, price, expirationTime) txresp := response.TxResponse