diff --git a/Makefile b/Makefile index 28b8279b0..95f0d9a69 100644 --- a/Makefile +++ b/Makefile @@ -124,7 +124,7 @@ env: @echo export CARTESI_BLOCKCHAIN_WS_ENDPOINT="ws://localhost:8545" @echo export CARTESI_BLOCKCHAIN_ID="31337" @echo export CARTESI_CONTRACTS_INPUT_BOX_ADDRESS="0x1b51e2992A2755Ba4D6F7094032DF91991a0Cfac" - @echo export CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS="0x5E96408CFE423b01dADeD3bc867E6013135990cc" + @echo export CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS="0xA1c705E93597837981062316FB447b95eB2F6B3d" @echo export CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS="0x26E758238CB6eC5aB70ce0dd52aF2d7b82e1972E" @echo export CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS="0x010D3CbB4223F5bCc7b7B03cEE59f3aAea8eDb8A" @echo export CARTESI_CONTRACTS_DAVE_APP_FACTORY_ADDRESS="0xfC2DBC639b5FB9AfE66A8696eC14EaD9FbFBC404" diff --git a/internal/claimer/blockchain.go b/internal/claimer/blockchain.go deleted file mode 100644 index 9d4cb71a5..000000000 --- a/internal/claimer/blockchain.go +++ /dev/null @@ -1,302 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package claimer - -import ( - "context" - "errors" - "fmt" - "log/slog" - "math/big" - - "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/model" - "github.com/cartesi/rollups-node/pkg/contracts/iconsensus" - "github.com/cartesi/rollups-node/pkg/ethutil" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" -) - -type iclaimerBlockchain interface { - findClaimSubmittedEventAndSucc( - ctx context.Context, - application *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, - ) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimSubmitted, - *iconsensus.IConsensusClaimSubmitted, - error, - ) - - submitClaimToBlockchain( - ic *iconsensus.IConsensus, - application *model.Application, - epoch *model.Epoch, - ) (common.Hash, error) - - pollTransaction( - ctx context.Context, - txHash common.Hash, - endBlock *big.Int, - ) (bool, *types.Receipt, error) - - findClaimAcceptedEventAndSucc( - ctx context.Context, - application *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, - ) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimAccepted, - *iconsensus.IConsensusClaimAccepted, - error, - ) - - getDefaultBlockNumber(ctx context.Context) (*big.Int, error) - - getConsensusAddress( - ctx context.Context, - app *model.Application, - ) (common.Address, error) -} - -type claimerBlockchain struct { - client *ethclient.Client - txOpts *bind.TransactOpts - logger *slog.Logger - defaultBlock config.DefaultBlock -} - -func (cb *claimerBlockchain) submitClaimToBlockchain( - ic *iconsensus.IConsensus, - application *model.Application, - epoch *model.Epoch, -) (common.Hash, error) { - txHash := common.Hash{} - if cb.txOpts == nil { - return txHash, fmt.Errorf("txOpts is required for claim submission") - } - lastBlockNumber := new(big.Int).SetUint64(epoch.LastBlock) - tx, err := ic.SubmitClaim(cb.txOpts, application.IApplicationAddress, - lastBlockNumber, *epoch.OutputsMerkleRoot) - if err != nil { - cb.logger.Error("submitClaimToBlockchain:failed", - "appContractAddress", application.IApplicationAddress, - "claimHash", *epoch.OutputsMerkleRoot, - "last_block", epoch.LastBlock, - "error", err) - } else { - txHash = tx.Hash() - cb.logger.Debug("submitClaimToBlockchain:success", - "appContractAddress", application.IApplicationAddress, - "claimHash", *epoch.OutputsMerkleRoot, - "last_block", epoch.LastBlock, - "TxHash", txHash) - } - return txHash, err -} - -type eventIterator interface { - Next() bool - Close() error - Error() error -} - -func newOracle( - nr func(*bind.CallOpts) (*big.Int, error), -) func(ctx context.Context, block uint64) (*big.Int, error) { - return func(ctx context.Context, block uint64) (*big.Int, error) { - return nr(&bind.CallOpts{ - Context: ctx, - BlockNumber: new(big.Int).SetUint64(block), - }) - } -} - -func newOnHit[IT eventIterator]( - ctx context.Context, - address common.Address, - filter func(*bind.FilterOpts, []common.Address, []common.Address) (IT, error), - onEvent func(IT), -) func(block uint64) error { - return func(block uint64) error { - filterOpts := &bind.FilterOpts{ - Context: ctx, - Start: block, - End: &block, - } - it, err := filter(filterOpts, nil, []common.Address{address}) - if err != nil { - return err - } - defer it.Close() - for it.Next() { - onEvent(it) - } - return it.Error() - } -} - -// scan the event stream for a claimSubmitted event that matches claim. -// return this event and its successor -func (cb *claimerBlockchain) findClaimSubmittedEventAndSucc( - ctx context.Context, - application *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimSubmitted, - *iconsensus.IConsensusClaimSubmitted, - error, -) { - ic, err := iconsensus.NewIConsensus(application.IConsensusAddress, cb.client) - if err != nil { - return nil, nil, nil, err - } - oracle := newOracle(ic.GetNumberOfSubmittedClaims) - events := []*iconsensus.IConsensusClaimSubmitted{} - onHit := newOnHit(ctx, application.IApplicationAddress, ic.FilterClaimSubmitted, - func(it *iconsensus.IConsensusClaimSubmittedIterator) { - event := it.Event - if (len(events) > 0) || claimSubmittedEventMatches(application, epoch, event) { - events = append(events, event) - } - }, - ) - - numSubmittedClaims, err := oracle(ctx, epoch.LastBlock) - if err != nil { - return nil, nil, nil, err - } - _, err = ethutil.FindTransitions(ctx, fromBlock, toBlock, numSubmittedClaims, oracle, onHit) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to walk ClaimSubmitted transitions: %w", err) - } - - if len(events) == 0 { - return ic, nil, nil, nil - } else if len(events) == 1 { - return ic, events[0], nil, nil - } else { - return ic, events[0], events[1], nil - } -} - -// scan the event stream for a claimAccepted event that matches claim. -// return this event and its successor -func (cb *claimerBlockchain) findClaimAcceptedEventAndSucc( - ctx context.Context, - application *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimAccepted, - *iconsensus.IConsensusClaimAccepted, - error, -) { - ic, err := iconsensus.NewIConsensus(application.IConsensusAddress, cb.client) - if err != nil { - return nil, nil, nil, err - } - - oracle := newOracle(ic.GetNumberOfAcceptedClaims) - events := []*iconsensus.IConsensusClaimAccepted{} - filter := func( - opts *bind.FilterOpts, - _ []common.Address, - appContract []common.Address, - ) (*iconsensus.IConsensusClaimAcceptedIterator, error) { - return ic.FilterClaimAccepted(opts, appContract) - } - onHit := newOnHit(ctx, application.IApplicationAddress, filter, - func(it *iconsensus.IConsensusClaimAcceptedIterator) { - event := it.Event - if (len(events) > 0) || claimAcceptedEventMatches(application, epoch, event) { - events = append(events, event) - } - }, - ) - - numAcceptedClaims, err := oracle(ctx, epoch.LastBlock) - if err != nil { - return nil, nil, nil, err - } - _, err = ethutil.FindTransitions(ctx, fromBlock, toBlock, numAcceptedClaims, oracle, onHit) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to walk ClaimAccepted transitions: %w", err) - } - - if len(events) == 0 { - return ic, nil, nil, nil - } else if len(events) == 1 { - return ic, events[0], nil, nil - } else { - return ic, events[0], events[1], nil - } -} - -func (cb *claimerBlockchain) getConsensusAddress( - ctx context.Context, - app *model.Application, -) (common.Address, error) { - return ethutil.GetConsensus(ctx, cb.client, app.IApplicationAddress) -} - -// poll a transaction for its receipt -func (cb *claimerBlockchain) pollTransaction( - ctx context.Context, - txHash common.Hash, - endBlockNumber *big.Int, -) (bool, *types.Receipt, error) { - receipt, err := cb.client.TransactionReceipt(ctx, txHash) - if err != nil { - // TransactionReceipt returns ethereum.NotFound while the transaction is still pending. - // Treat this as "not ready yet" rather than a hard failure so that callers keep - // the claim in-flight and avoid duplicate submissions. - if errors.Is(err, ethereum.NotFound) { - return false, nil, nil - } - return false, nil, err - } - - // receipt must be committed before use. Return false until it is. - return receipt.BlockNumber.Cmp(endBlockNumber) <= 0, receipt, nil -} - -/* Retrieve the block number for the configured commitment level in ethereum terms, - * that is: `latest`, `safe`, `finalized`, etc. Which may be many blocks behind. */ -func (cb *claimerBlockchain) getDefaultBlockNumber(ctx context.Context) (*big.Int, error) { - var nr int64 - switch cb.defaultBlock { - case model.DefaultBlock_Pending: - nr = rpc.PendingBlockNumber.Int64() - case model.DefaultBlock_Latest: - nr = rpc.LatestBlockNumber.Int64() - case model.DefaultBlock_Finalized: - nr = rpc.FinalizedBlockNumber.Int64() - case model.DefaultBlock_Safe: - nr = rpc.SafeBlockNumber.Int64() - default: - return nil, fmt.Errorf("default block '%v' not supported", cb.defaultBlock) - } - - hdr, err := cb.client.HeaderByNumber(ctx, big.NewInt(nr)) - if err != nil { - return nil, err - } - return hdr.Number, nil -} diff --git a/internal/claimer/claimer.go b/internal/claimer/claimer.go index 9cc05fd4e..c085a7f15 100644 --- a/internal/claimer/claimer.go +++ b/internal/claimer/claimer.go @@ -39,551 +39,581 @@ package claimer import ( "context" + "errors" "fmt" + "log/slog" "math/big" - "time" "github.com/cartesi/rollups-node/internal/appstatus" - "github.com/cartesi/rollups-node/internal/model" + . "github.com/cartesi/rollups-node/internal/model" + "github.com/cartesi/rollups-node/internal/repository" "github.com/cartesi/rollups-node/pkg/contracts/iconsensus" + "github.com/cartesi/rollups-node/pkg/ethutil" + //"github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" ) -var ( - ErrClaimMismatch = fmt.Errorf("constraints failed for epoch claim and its successor.") - ErrEventMismatch = fmt.Errorf("epoch claim does not match its corresponding event.") - ErrMissingEvent = fmt.Errorf("epoch claim does not have a corresponding event.") -) +type iRepository interface { + repository.ApplicationRepository + repository.EpochRepository + repository.NodeConfigRepository +} -type iclaimerRepository interface { - // key is model.Application.ID - SelectSubmittedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, - ) +// iBlockchain is the minimal client interface the claimer needs. +// It embeds bind.ContractBackend (required by the consensus contract binding) +// and adds TransactionReceipt for monitoring in-flight claim transactions. +// HeaderByNumber is already part of bind.ContractBackend via ContractTransactor. +type iBlockchain interface { + bind.ContractBackend + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) +} - // key is model.Application.ID - SelectAcceptedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, - ) +// iConsensus is a testable façade over the generated *iconsensus.IConsensus, +// using slice-based filter methods to avoid exposing iterator internals. +type iConsensus interface { + GetNumberOfSubmittedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) + GetNumberOfAcceptedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) + FilterClaimSubmitted(opts *bind.FilterOpts, submitter []common.Address, appContract []common.Address) ([]*iconsensus.IConsensusClaimSubmitted, error) + FilterClaimAccepted(opts *bind.FilterOpts, appContract []common.Address) ([]*iconsensus.IConsensusClaimAccepted, error) + SubmitClaim(opts *bind.TransactOpts, appContract common.Address, lastProcessedBlockNumber *big.Int, outputsMerkleRoot [32]byte) (*types.Transaction, error) +} - UpdateEpochWithSubmittedClaim( - ctx context.Context, - applicationID int64, - index uint64, - transactionHash common.Hash, - ) error - - UpdateEpochWithAcceptedClaim( - ctx context.Context, - applicationID int64, - index uint64, - ) error - - UpdateApplicationState( - ctx context.Context, - appID int64, - state model.ApplicationState, - reason *string, - ) error - - SaveNodeConfigRaw(ctx context.Context, key string, rawJSON []byte) error - LoadNodeConfigRaw(ctx context.Context, key string) (rawJSON []byte, createdAt, updatedAt time.Time, err error) +// consensusAdapter wraps the generated *iconsensus.IConsensus and implements +// iConsensus by draining iterators into slices. +type consensusAdapter struct { + ic *iconsensus.IConsensus } -func hashToHex(h *common.Hash) string { - if h == nil { - return "" +func newConsensus(addr common.Address, client iBlockchain) (iConsensus, error) { + ic, err := iconsensus.NewIConsensus(addr, client) + if err != nil { + return nil, err } - return h.Hex() + return &consensusAdapter{ic: ic}, nil } -// claims in flight are those that have been submitted but are waiting for a -// transaction confirmation. When confirmed, we update their status on the -// database. The epoch is now "submitted" and no longer "computed". -func (s *Service) checkClaimsInFlight( - computedEpochs map[int64]*model.Epoch, - apps map[int64]*model.Application, - endBlock *big.Int, -) error { - // check claims in flight. NOTE: map mutation + iteration is safe in Go - for key, txHash := range s.claimsInFlight { - ready, receipt, err := s.blockchain.pollTransaction(s.Context, txHash, endBlock) - if err != nil { - s.Logger.Warn("Claim submission failed, retrying.", - "txHash", txHash, - "err", err, - ) - delete(s.claimsInFlight, key) - continue - } - if !ready { - continue - } - if receipt.Status == 0 { - s.Logger.Warn("Claim submission reverted, retrying.", - "txHash", txHash, - "err", err, - ) - delete(s.claimsInFlight, key) - continue - } - if computedEpoch, ok := computedEpochs[key]; ok { - err = s.repository.UpdateEpochWithSubmittedClaim( - s.Context, - computedEpoch.ApplicationID, - computedEpoch.Index, - receipt.TxHash, - ) - - // NOTE: there is no point in trying the other applications on a database error - // so we just return and try again later (next tick) - if err != nil { - return err - } +func (a *consensusAdapter) GetNumberOfSubmittedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { + return a.ic.GetNumberOfSubmittedClaims(opts, appContract) +} - // we expect apps[key] to always exist, - // but guard its use behind `if` to ensure there is no panic if we are wrong. - appAddress := common.Address{} - if app, ok := apps[key]; ok { - appAddress = app.IApplicationAddress - } - s.Logger.Info("Claim submitted", - "app", appAddress, - "receipt_block_number", receipt.BlockNumber, - "claim_hash", hashToHex(computedEpoch.OutputsMerkleRoot), - "last_block", computedEpoch.LastBlock, - "tx", txHash) - - // epoch is no longer "computed" and is now "submitted". - // Processing will happen on the next tick iteration. - delete(computedEpochs, key) - } else { - s.Logger.Warn("unexpected, claim in flight is not a computed epoch.", - "id", key, - "tx", receipt.TxHash) - } - delete(s.claimsInFlight, key) - } - return nil +func (a *consensusAdapter) GetNumberOfAcceptedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { + return a.ic.GetNumberOfAcceptedClaims(opts, appContract) } -func (s *Service) findClaimSubmittedEventAndSucc( - ctx context.Context, - app *model.Application, - prevEpoch *model.Epoch, - currEpoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimSubmitted, - *iconsensus.IConsensusClaimSubmitted, - error, -) { - err := checkEpochSequenceConstraint(prevEpoch, currEpoch) +func (a *consensusAdapter) FilterClaimSubmitted(opts *bind.FilterOpts, submitter []common.Address, appContract []common.Address) ([]*iconsensus.IConsensusClaimSubmitted, error) { + it, err := a.ic.FilterClaimSubmitted(opts, submitter, appContract) if err != nil { - err = s.setApplicationInoperable( - s.Context, - app, - "%v. epoch: %v (%v).", - err, - prevEpoch.Index, - prevEpoch.VirtualIndex, - ) - return nil, nil, nil, err + return nil, err } + defer it.Close() + var events []*iconsensus.IConsensusClaimSubmitted + for it.Next() { + events = append(events, it.Event) + } + return events, it.Error() +} - ic, prevClaimSubmissionEvent, currClaimSubmissionEvent, err := - s.blockchain.findClaimSubmittedEventAndSucc(ctx, app, prevEpoch, fromBlock, toBlock) +func (a *consensusAdapter) FilterClaimAccepted(opts *bind.FilterOpts, appContract []common.Address) ([]*iconsensus.IConsensusClaimAccepted, error) { + it, err := a.ic.FilterClaimAccepted(opts, appContract) if err != nil { - return nil, nil, nil, err + return nil, err } - - if prevClaimSubmissionEvent == nil { - err = s.setApplicationInoperable( - s.Context, - app, - "application has an invalid epoch: %v (%v). No claim submission event to match.", - prevEpoch.Index, - prevEpoch.VirtualIndex, - ) - return nil, nil, nil, err + defer it.Close() + var events []*iconsensus.IConsensusClaimAccepted + for it.Next() { + events = append(events, it.Event) } + return events, it.Error() +} - if !claimSubmittedEventMatches(app, prevEpoch, prevClaimSubmissionEvent) { - err = s.setApplicationInoperable( - s.Context, - app, - "application has an invalid epoch: %v (%v), missing claim submitted event (%v).", - prevEpoch.Index, - prevEpoch.VirtualIndex, - prevClaimSubmissionEvent.Raw.TxHash, - ) - return nil, nil, nil, err - } - return ic, prevClaimSubmissionEvent, currClaimSubmissionEvent, nil +func (a *consensusAdapter) SubmitClaim(opts *bind.TransactOpts, appContract common.Address, lastProcessedBlockNumber *big.Int, outputsMerkleRoot [32]byte) (*types.Transaction, error) { + return a.ic.SubmitClaim(opts, appContract, lastProcessedBlockNumber, outputsMerkleRoot) } -// transition epoch claims from computed to submitted. -func (s *Service) submitClaimsAndUpdateDatabase( - acceptedOrSubmittedEpochs map[int64]*model.Epoch, - computedEpochs map[int64]*model.Epoch, - apps map[int64]*model.Application, - defaultBlockNumber *big.Int, +func update( + ctx context.Context, + logger *slog.Logger, + txOpts *bind.TransactOpts, + repo iRepository, + client iBlockchain, + endBlock uint64, ) []error { - err := s.checkClaimsInFlight(computedEpochs, apps, defaultBlockNumber) + var errs []error + apps, _, err := repo.ListApplications( + ctx, + repository.ApplicationFilter{ + State: Pointer(ApplicationState_Enabled), + }, + repository.Pagination{}, + true, + ) if err != nil { return []error{err} } - errs := []error{} - // check computed epochs. NOTE: map mutation + iteration is safe in Go - for key, currEpoch := range computedEpochs { - var ic *iconsensus.IConsensus - var currEvent *iconsensus.IConsensusClaimSubmitted - - if _, isClaimInFlight := s.claimsInFlight[key]; isClaimInFlight { + for _, app := range apps { + if app.IsDaveConsensus() { + logger.Debug("incompatible consensus", "application", app.Name) continue } - app := apps[key] // guaranteed to exist because of the query and database constraints - prevEpoch, prevEpochExists := acceptedOrSubmittedEpochs[key] - - // check address for changes - if err := s.checkConsensusForAddressChange(app); err != nil { - delete(computedEpochs, key) + logger.Debug("processing claims", "application", app.Name) + if err := updateApplication(ctx, logger, txOpts, repo, client, app, endBlock); err != nil { errs = append(errs, err) continue } - if prevEpochExists { - ic, _, currEvent, err = s.findClaimSubmittedEventAndSucc( - s.Context, app, prevEpoch, currEpoch, prevEpoch.LastBlock+1, defaultBlockNumber.Uint64(), - ) - } else { - ic, currEvent, _, err = s.blockchain.findClaimSubmittedEventAndSucc( - s.Context, app, currEpoch, currEpoch.LastBlock+1, defaultBlockNumber.Uint64(), - ) - } - if err != nil { - delete(computedEpochs, key) - errs = append(errs, err) - continue - } - - if currEvent != nil { - s.Logger.Debug("Found ClaimSubmitted Event", - "app", currEvent.AppContract, - "claim_hash", fmt.Sprintf("%x", currEvent.OutputsMerkleRoot), - "last_block", currEvent.LastProcessedBlockNumber.Uint64(), - ) - if !claimSubmittedEventMatches(app, currEpoch, currEvent) { - err = s.setApplicationInoperable( - s.Context, - app, - "computed claim does not match event. computed_claim=%v, current_event=%v", - currEpoch, currEvent, - ) - delete(computedEpochs, key) - errs = append(errs, err) - continue - } - s.Logger.Debug("Updating claim status to submitted", - "app", app.IApplicationAddress, - "claim_hash", hashToHex(currEpoch.OutputsMerkleRoot), - "last_block", currEpoch.LastBlock, - ) - txHash := currEvent.Raw.TxHash - err = s.repository.UpdateEpochWithSubmittedClaim( - s.Context, - currEpoch.ApplicationID, - currEpoch.Index, - txHash, - ) - if err != nil { - delete(computedEpochs, key) - errs = append(errs, err) - continue - } - delete(s.claimsInFlight, key) - s.Logger.Info("Claim previously submitted", - "app", app.IApplicationAddress, - "event_block_number", currEvent.Raw.BlockNumber, - "claim_hash", hashToHex(currEpoch.OutputsMerkleRoot), - "last_block", currEpoch.LastBlock, - ) - } else { - if s.submissionEnabled { - if prevEpoch != nil && prevEpoch.Status != model.EpochStatus_ClaimAccepted { - s.Logger.Debug("Waiting previous claim to be accepted before submitting new one. Previous:", - "app", app.IApplicationAddress, - "claim_hash", hashToHex(prevEpoch.OutputsMerkleRoot), - "last_block", prevEpoch.LastBlock, - ) - continue - } - s.Logger.Debug("Submitting claim to blockchain", - "app", app.IApplicationAddress, - "claim_hash", hashToHex(currEpoch.OutputsMerkleRoot), - "last_block", currEpoch.LastBlock, - ) - txHash, err := s.blockchain.submitClaimToBlockchain(ic, app, currEpoch) - if err != nil { - delete(computedEpochs, key) - errs = append(errs, err) - continue - } - s.claimsInFlight[key] = txHash - } - } } return errs } -func (s *Service) findClaimAcceptedEventAndSucc( +func updateApplication( ctx context.Context, - app *model.Application, - prevEpoch *model.Epoch, - currEpoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimAccepted, - *iconsensus.IConsensusClaimAccepted, - error, -) { - err := checkEpochSequenceConstraint(prevEpoch, currEpoch) + logger *slog.Logger, + txOpts *bind.TransactOpts, + ir iRepository, + client iBlockchain, + app *Application, + endBlock uint64, +) error { + epochs, _, err := ir.ListEpochs( + ctx, + app.Name, + repository.EpochFilter{ + Status: []EpochStatus{ + EpochStatus_ClaimComputed, + EpochStatus_ClaimSubmitted, + }, + }, + repository.Pagination{}, + false, + ) if err != nil { - err = s.setApplicationInoperable( - ctx, - app, - "%v. epoch: %v (%v).", - err, - prevEpoch.Index, - prevEpoch.VirtualIndex, - ) - return nil, nil, nil, err + return err } - ic, prevClaimAcceptanceEvent, currClaimAcceptanceEvent, err := - s.blockchain.findClaimAcceptedEventAndSucc(ctx, app, prevEpoch, fromBlock, toBlock) + // nothing to do + if len(epochs) == 0 { + return nil + } + + ic, err := newConsensus(app.IConsensusAddress, client) if err != nil { - return nil, nil, nil, err + return err } - if prevClaimAcceptanceEvent == nil { - err = s.setApplicationInoperable( - ctx, - app, - "application has an invalid epoch: %v (%v), missing claim acceptance event.", - prevEpoch.Index, - prevEpoch.VirtualIndex, - ) - return nil, nil, nil, err - } - if !claimAcceptedEventMatches(app, prevEpoch, prevClaimAcceptanceEvent) { - err = s.setApplicationInoperable( - ctx, - app, - "application has an invalid epoch: %v (%v). event does not match: %v", - prevEpoch.Index, - prevEpoch.VirtualIndex, - prevClaimAcceptanceEvent.Raw.TxHash, - ) - return nil, nil, nil, err + if err := updateClaimSubmitted(ctx, logger, app, epochs, ir, ic, endBlock); err != nil { + return err } - return ic, prevClaimAcceptanceEvent, currClaimAcceptanceEvent, nil + + if err := updateClaimAccepted(ctx, logger, app, epochs, ir, ic, endBlock); err != nil { + return err + } + + if epochs[0].Status == EpochStatus_ClaimComputed { + return trySubmitClaim(ctx, logger, app, epochs[0], ir, ic, client, txOpts, endBlock) + } + + return nil } -// transition claims from submitted to accepted -func (s *Service) acceptClaimsAndUpdateDatabase( - acceptedEpochs map[int64]*model.Epoch, - submittedEpochs map[int64]*model.Epoch, - apps map[int64]*model.Application, - defaultBlockNumber *big.Int, -) []error { - errs := []error{} - var err error - - // check submitted epochs. NOTE: map mutation + iteration is safe in Go - for key, currEpoch := range submittedEpochs { - var currEvent *iconsensus.IConsensusClaimAccepted - - app := apps[key] - prevEpoch, prevEpochExists := acceptedEpochs[key] - // check address for changes - if err := s.checkConsensusForAddressChange(app); err != nil { - delete(submittedEpochs, key) - errs = append(errs, err) - continue +// update epochs from claim accepted events. +// ClaimAccepted events are exactly 1 - 1 with epochs +func updateClaimAccepted( + ctx context.Context, + logger *slog.Logger, + app *Application, + epochs []*Epoch, + ir iRepository, + ic iConsensus, + endBlock uint64, +) error { + startBlock := max(app.LastAcceptedClaimCheckBlock, epochs[0].LastBlock) + 1 + base, events, err := collectClaimAcceptedEvents(ctx, logger, app, ic, startBlock, endBlock) + if err != nil { + return err + } + + for i, ev := range events { + // there are more accepted events than computed + submitted + // epochs, that means we are probably catching up an + // application with some history. We'll try again later after + // the validator produces more computed epochs. + if i >= len(epochs) { + endBlock = ev.Raw.BlockNumber - 1 + break } - if prevEpochExists { - _, _, currEvent, err = s.findClaimAcceptedEventAndSucc( - s.Context, app, prevEpoch, currEpoch, prevEpoch.LastBlock+1, defaultBlockNumber.Uint64(), + expectedVirtualIndex := base + uint64(i) + if epochs[i].VirtualIndex != expectedVirtualIndex { + return appstatus.SetInoperablef( + ctx, + logger, + ir, + app, + "claim accepted event sequence mismatch: expected virtual index %d, got %d (base=%d, offset=%d)", + expectedVirtualIndex, + epochs[i].VirtualIndex, + base, + i, ) - } else { - _, currEvent, _, err = s.blockchain.findClaimAcceptedEventAndSucc( - s.Context, app, currEpoch, currEpoch.LastBlock+1, defaultBlockNumber.Uint64(), + } + if err := checkClaimAcceptedEvent(app, epochs[i], ev); err != nil { + return appstatus.SetInoperablef( + ctx, + logger, + ir, + app, + "claim accepted event validation failed: %v", + err, ) } - if err != nil { - delete(submittedEpochs, key) - errs = append(errs, err) - continue + + txHash := ev.Raw.TxHash + epochs[i].ClaimAcceptedTransactionHash = &txHash + if err := ir.UpdateEpochClaimAcceptedTransactionHash(ctx, app.Name, epochs[i]); err != nil { + endBlock = ev.Raw.BlockNumber - 1 + break } - if currEvent != nil { - s.Logger.Debug("Found ClaimAccepted Event", - "app", currEvent.AppContract, - "claim_hash", fmt.Sprintf("%x", currEvent.OutputsMerkleRoot), - "last_block", currEvent.LastProcessedBlockNumber.Uint64(), + epochs[i].Status = EpochStatus_ClaimAccepted + if err := ir.UpdateEpochStatus(ctx, app.Name, epochs[i]); err != nil { + endBlock = ev.Raw.BlockNumber - 1 + break + } + logger.Info("Claim accepted (confirmed)", + "application", app.Name, + "epoch_index", epochs[i].Index, + "outputs_merkle_root", *epochs[i].OutputsMerkleRoot, + "tx_hash", ev.Raw.TxHash, + "block_number", ev.Raw.BlockNumber, + ) + } + + err = ir.UpdateEventLastCheckBlock( + ctx, + []int64{app.ID}, + MonitoredEvent_ClaimAccepted, + endBlock, + ) + if err != nil { + return err + } + return nil +} + +// update epochs from claim submitted events. +// ClaimSubmitted events are exactly 1 - 1 with epochs (for authority) +func updateClaimSubmitted( + ctx context.Context, + logger *slog.Logger, + app *Application, + epochs []*Epoch, + ir iRepository, + ic iConsensus, + endBlock uint64, +) error { + startBlock := max(app.LastSubmittedClaimCheckBlock, epochs[0].LastBlock) + 1 + base, events, err := collectClaimSubmittedEvents(ctx, logger, app, ic, startBlock, endBlock) + if err != nil { + return err + } + for i, ev := range events { + // there are more submitted events than computed epochs, that + // means we are probably catching up an application with some + // history. We'll try again later after the validator produces + // more computed epochs. + if i >= len(epochs) { + endBlock = ev.Raw.BlockNumber - 1 + break + } + + expectedVirtualIndex := base + uint64(i) + if epochs[i].VirtualIndex != expectedVirtualIndex { + return appstatus.SetInoperablef(ctx, logger, ir, app, + "claim submitted event sequence mismatch: expected virtual index %d, got %d (base=%d, offset=%d)", + expectedVirtualIndex, + epochs[i].VirtualIndex, + base, + i, ) - if !claimAcceptedEventMatches(app, currEpoch, currEvent) { - s.Logger.Error("event mismatch", - "claim", currEpoch, - "event", currEvent, - "err", ErrEventMismatch, - ) - err := s.setApplicationInoperable( - s.Context, - app, - "event mismatch for epoch %v, event tx_hash: %v", - currEpoch.Index, - currEvent.Raw.TxHash, - ) - delete(submittedEpochs, key) - errs = append(errs, err) - continue - } - s.Logger.Debug("Updating claim status to accepted", - "app", app.IApplicationAddress, - "claim_hash", hashToHex(currEpoch.OutputsMerkleRoot), - "last_block", currEpoch.LastBlock, + } + if err := checkClaimSubmittedEvent(app, epochs[i], ev); err != nil { + return appstatus.SetInoperablef(ctx, logger, ir, app, + "claim submitted event validation failed: %v", + err, ) - txHash := currEvent.Raw.TxHash - err = s.repository.UpdateEpochWithAcceptedClaim(s.Context, currEpoch.ApplicationID, currEpoch.Index) + } + + txHash := ev.Raw.TxHash + epochs[i].ClaimSubmittedTransactionHash = &txHash + if err := ir.UpdateEpochClaimSubmittedTransactionHash(ctx, app.Name, epochs[i]); err != nil { + endBlock = ev.Raw.BlockNumber - 1 + break + } + + // epochs may have advanced its state during this tick. + // make sure it makes sense to update this one + if epochs[i].Status == EpochStatus_ClaimComputed { + epochs[i].Status = EpochStatus_ClaimSubmitted + err = ir.UpdateEpochStatus(ctx, app.Name, epochs[i]) if err != nil { - delete(submittedEpochs, key) - errs = append(errs, err) - continue + endBlock = ev.Raw.BlockNumber - 1 + break } - s.Logger.Info("Claim accepted", - "app", currEvent.AppContract, - "event_block_number", currEvent.Raw.BlockNumber, - "claim_hash", fmt.Sprintf("%x", currEvent.OutputsMerkleRoot), - "last_block", currEvent.LastProcessedBlockNumber.Uint64(), - "tx", txHash, - ) } } - return errs + + err = ir.UpdateEventLastCheckBlock( + ctx, + []int64{app.ID}, + MonitoredEvent_ClaimSubmitted, + endBlock, + ) + if err != nil { + return err + } + return nil } -func (s *Service) setApplicationInoperable( +func collectClaimSubmittedEvents( ctx context.Context, - app *model.Application, - reasonFmt string, - args ...any, -) error { - return appstatus.SetInoperablef(ctx, s.Logger, s.repository, app, reasonFmt, args...) + logger *slog.Logger, + app *Application, + ic iConsensus, + startBlock uint64, + endBlock uint64, +) (uint64, []*iconsensus.IConsensusClaimSubmitted, error) { + oracle := func(ctx context.Context, block uint64) (*big.Int, error) { + return ic.GetNumberOfSubmittedClaims(&bind.CallOpts{ + Context: ctx, + BlockNumber: new(big.Int).SetUint64(block), + }, app.IApplicationAddress) + } + prevValue, err := oracle(ctx, startBlock-1) + if err != nil { + return 0, nil, err + } + + var events []*iconsensus.IConsensusClaimSubmitted + _, err = ethutil.FindTransitions(ctx, startBlock, + endBlock, prevValue, oracle, func(block uint64) error { + evs, err := ic.FilterClaimSubmitted(&bind.FilterOpts{ + Context: ctx, + Start: block, + End: &block, + }, nil, []common.Address{app.IApplicationAddress}) + if err != nil { + return err + } + events = append(events, evs...) + return nil + }, + ) + if err != nil { + return 0, nil, err + } + return prevValue.Uint64(), events, nil } -func (s *Service) checkConsensusForAddressChange( - app *model.Application, -) error { - newConsensusAddress, err := s.blockchain.getConsensusAddress(s.Context, app) +func collectClaimAcceptedEvents( + ctx context.Context, + logger *slog.Logger, + app *Application, + ic iConsensus, + startBlock uint64, + endBlock uint64, +) (uint64, []*iconsensus.IConsensusClaimAccepted, error) { + oracle := func(ctx context.Context, block uint64) (*big.Int, error) { + return ic.GetNumberOfAcceptedClaims(&bind.CallOpts{ + Context: ctx, + BlockNumber: new(big.Int).SetUint64(block), + }, app.IApplicationAddress) + } + prevValue, err := oracle(ctx, startBlock-1) if err != nil { - return err + return 0, nil, err } - if app.IConsensusAddress != newConsensusAddress { - err = s.setApplicationInoperable( - s.Context, - app, - "consensus change detected. application: %v.", - app.IApplicationAddress, - ) - return err + + var events []*iconsensus.IConsensusClaimAccepted + _, err = ethutil.FindTransitions(ctx, startBlock, + endBlock, prevValue, oracle, func(block uint64) error { + evs, err := ic.FilterClaimAccepted(&bind.FilterOpts{ + Context: ctx, + Start: block, + End: &block, + }, []common.Address{app.IApplicationAddress}) + if err != nil { + return err + } + events = append(events, evs...) + return nil + }, + ) + if err != nil { + return 0, nil, err } - return nil + return prevValue.Uint64(), events, nil } -func checkEpochConstraint(epoch *model.Epoch) error { - if epoch.FirstBlock > epoch.LastBlock { - return fmt.Errorf("unexpected epoch state. first_block: %v > last_block: %v", - epoch.FirstBlock, epoch.LastBlock) - } +func trySubmitClaim( + ctx context.Context, + logger *slog.Logger, + app *Application, + epoch *Epoch, + ir iRepository, + ic iConsensus, + client iBlockchain, + txOpts *bind.TransactOpts, + endBlock uint64, +) error { + // claim submitted but not confirmed, try to confirm it + if epoch.ClaimSubmittedTransactionHash != nil { + receipt, err := client.TransactionReceipt(ctx, *epoch.ClaimSubmittedTransactionHash) + if errors.Is(err, ethereum.NotFound) { + goto submit // yes: something went wrong with the one we had, resubmit + } + if err != nil { + return err + } - mustHaveOutputsMerkleRoot := epoch.Status == model.EpochStatus_ClaimSubmitted || - epoch.Status == model.EpochStatus_ClaimAccepted || - epoch.Status == model.EpochStatus_ClaimComputed - if mustHaveOutputsMerkleRoot { - if epoch.OutputsMerkleRoot == nil { - return fmt.Errorf("unexpected epoch state. missing outputs_merkle_root.") + if receipt.BlockNumber.Cmp(new(big.Int).SetUint64(endBlock)) > 0 { + return nil // no: its too early, wait for receipt to be committed } - } - mustHaveClaimTransactionHash := epoch.Status == model.EpochStatus_ClaimSubmitted || - epoch.Status == model.EpochStatus_ClaimAccepted - if mustHaveClaimTransactionHash { - if epoch.ClaimTransactionHash == nil { - return fmt.Errorf("unexpected epoch state. missing claim_transaction_hash.") + if receipt.Status == 1 { + epoch.Status = EpochStatus_ClaimSubmitted + logger.Info("Claim submitted (confirmed)", + "application", app.Name, + "epoch_index", epoch.Index, + "outputs_merkle_root", *epoch.OutputsMerkleRoot, + "tx_hash", receipt.TxHash, + "block_number", receipt.BlockNumber, + ) + return ir.UpdateEpochStatus(ctx, app.Name, epoch) + } else { + // transaction reverted + // what do we do? } } - return nil + +submit: + if txOpts == nil { + logger.Debug("Claim NOT submitted (disabled)", + "application", app.Name, + "epoch_index", epoch.Index, + "outputs_merkle_root", *epoch.OutputsMerkleRoot, + ) + return nil + } + tx, err := ic.SubmitClaim( + txOpts, + app.IApplicationAddress, + new(big.Int).SetUint64(epoch.LastBlock), + *epoch.OutputsMerkleRoot, + ) + if err != nil { + return err + } + + txHash := tx.Hash() + logger.Info("Claim submitted (unconfirmed)", + "application", app.Name, + "epoch_index", epoch.Index, + "outputs_merkle_root", *epoch.OutputsMerkleRoot, + "tx_hash", txHash, + ) + + epoch.ClaimSubmittedTransactionHash = &txHash + return ir.UpdateEpochClaimSubmittedTransactionHash(ctx, app.Name, epoch) } -func checkEpochSequenceConstraint(prevEpoch *model.Epoch, currEpoch *model.Epoch) error { - var err error +/* Retrieve the block number for the configured commitment level in ethereum terms, + * that is: `latest`, `safe`, `finalized`, etc. Which may be many blocks behind. */ +func getDefaultBlockNumber( + ctx context.Context, + client iBlockchain, + defaultBlock DefaultBlock, +) (uint64, error) { + var nr int64 + switch defaultBlock { + case DefaultBlock_Pending: + nr = rpc.PendingBlockNumber.Int64() + case DefaultBlock_Latest: + nr = rpc.LatestBlockNumber.Int64() + case DefaultBlock_Finalized: + nr = rpc.FinalizedBlockNumber.Int64() + case DefaultBlock_Safe: + nr = rpc.SafeBlockNumber.Int64() + default: + return 0, fmt.Errorf("default block '%v' not supported", defaultBlock) + } - err = checkEpochConstraint(currEpoch) + hdr, err := client.HeaderByNumber(ctx, big.NewInt(nr)) if err != nil { - return fmt.Errorf("%w on current epoch.", err) + return 0, err } - err = checkEpochConstraint(prevEpoch) - if err != nil { - return fmt.Errorf("%w on previous epoch.", err) + return hdr.Number.Uint64(), nil +} + +func checkClaimAcceptedEvent(application *Application, epoch *Epoch, event *iconsensus.IConsensusClaimAccepted) error { + if application == nil { + return fmt.Errorf("missing the application (nil)") + } + if epoch == nil { + return fmt.Errorf("missing the epoch (nil)") + } + if event == nil { + return fmt.Errorf("missing the event (nil)") } - if prevEpoch.LastBlock > currEpoch.LastBlock { - return fmt.Errorf("unexpected epochs sequence on field last_block: previous(%v) > current(%v)", prevEpoch.LastBlock, currEpoch.LastBlock) + if application.IApplicationAddress != event.AppContract { + return fmt.Errorf("application contract mismatch: %v != %v", + application.IApplicationAddress, event.AppContract) + } + if epoch.OutputsMerkleRoot == nil { + return fmt.Errorf("epoch is missing outputs merkle root (nil)") } - if prevEpoch.FirstBlock > currEpoch.FirstBlock { - return fmt.Errorf("unexpected epochs sequence on field first_block: previous(%v) > current(%v)", prevEpoch.FirstBlock, currEpoch.FirstBlock) + if *epoch.OutputsMerkleRoot != event.OutputsMerkleRoot { + return fmt.Errorf("outputs merkle root mismatch: %v != %v", + *epoch.OutputsMerkleRoot, common.Hash(event.OutputsMerkleRoot)) } - if prevEpoch.Index > currEpoch.Index { - return fmt.Errorf("unexpected epochs sequence on field index: previous(%v) > current(%v)", prevEpoch.Index, currEpoch.Index) + if nr := event.LastProcessedBlockNumber.Uint64(); epoch.LastBlock != nr { + return fmt.Errorf("outputs merkle root mismatch: %v != %v", + epoch.LastBlock, nr) } return nil } -func claimSubmittedEventMatches(application *model.Application, epoch *model.Epoch, event *iconsensus.IConsensusClaimSubmitted) bool { - if application == nil || epoch == nil || event == nil { - return false +func checkClaimSubmittedEvent(application *Application, epoch *Epoch, event *iconsensus.IConsensusClaimSubmitted) error { + if application == nil { + return fmt.Errorf("missing the application (nil)") } - return application.IApplicationAddress == event.AppContract && - epoch.OutputsMerkleRoot != nil && - *epoch.OutputsMerkleRoot == event.OutputsMerkleRoot && - epoch.LastBlock == event.LastProcessedBlockNumber.Uint64() -} - -func claimAcceptedEventMatches(application *model.Application, epoch *model.Epoch, event *iconsensus.IConsensusClaimAccepted) bool { - if application == nil || epoch == nil || event == nil { - return false + if epoch == nil { + return fmt.Errorf("missing the epoch (nil)") + } + if event == nil { + return fmt.Errorf("missing the event (nil)") } - return application.IApplicationAddress == event.AppContract && - epoch.OutputsMerkleRoot != nil && - *epoch.OutputsMerkleRoot == event.OutputsMerkleRoot && - epoch.LastBlock == event.LastProcessedBlockNumber.Uint64() -} -func (s *Service) String() string { - return s.Name + if application.IApplicationAddress != event.AppContract { + return fmt.Errorf("application contract mismatch: %v != %v", + application.IApplicationAddress, event.AppContract) + } + if epoch.OutputsMerkleRoot == nil { + return fmt.Errorf("epoch is missing outputs merkle root (nil)") + } + if *epoch.OutputsMerkleRoot != event.OutputsMerkleRoot { + return fmt.Errorf("outputs merkle root mismatch: %v != %v", + *epoch.OutputsMerkleRoot, common.Hash(event.OutputsMerkleRoot)) + } + if nr := event.LastProcessedBlockNumber.Uint64(); epoch.LastBlock != nr { + return fmt.Errorf("outputs merkle root mismatch: %v != %v", + epoch.LastBlock, nr) + } + return nil } diff --git a/internal/claimer/claimer_test.go b/internal/claimer/claimer_test.go index a8714d918..33a06edd5 100644 --- a/internal/claimer/claimer_test.go +++ b/internal/claimer/claimer_test.go @@ -5,7 +5,7 @@ package claimer import ( "context" - "fmt" + "errors" "log/slog" "math/big" "os" @@ -13,885 +13,1145 @@ import ( "time" "github.com/cartesi/rollups-node/internal/model" + "github.com/cartesi/rollups-node/internal/repository" + "github.com/cartesi/rollups-node/internal/repository/repotest" "github.com/cartesi/rollups-node/pkg/contracts/iconsensus" - "github.com/cartesi/rollups-node/pkg/service" "github.com/lmittmann/tint" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" + "github.com/ethereum/go-ethereum/rpc" ) -type claimerRepositoryMock struct { +// //////////////////////////////////////////////////////////////////////////// +// Mocks +// //////////////////////////////////////////////////////////////////////////// + +// repositoryMock implements iRepository via testify/mock. +type repositoryMock struct { mock.Mock } -func (m *claimerRepositoryMock) SelectSubmittedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, -) { - args := m.Called(ctx) - return args.Get(0).(map[int64]*model.Epoch), - args.Get(1).(map[int64]*model.Epoch), - args.Get(2).(map[int64]*model.Application), - args.Error(3) -} - -func (m *claimerRepositoryMock) SelectAcceptedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, -) { - args := m.Called(ctx) - return args.Get(0).(map[int64]*model.Epoch), - args.Get(1).(map[int64]*model.Epoch), - args.Get(2).(map[int64]*model.Application), - args.Error(3) -} -func (m *claimerRepositoryMock) UpdateEpochWithSubmittedClaim( - ctx context.Context, - appid int64, - index uint64, - txHash common.Hash, -) error { - args := m.Called(ctx, appid, index, txHash) +// ApplicationRepository methods + +func (m *repositoryMock) CreateApplication(ctx context.Context, app *model.Application, withExecutionParameters bool) (int64, error) { + args := m.Called(ctx, app, withExecutionParameters) + return args.Get(0).(int64), args.Error(1) +} + +func (m *repositoryMock) GetApplication(ctx context.Context, nameOrAddress string) (*model.Application, error) { + args := m.Called(ctx, nameOrAddress) + return args.Get(0).(*model.Application), args.Error(1) +} + +func (m *repositoryMock) GetProcessedInputCount(ctx context.Context, nameOrAddress string) (uint64, error) { + args := m.Called(ctx, nameOrAddress) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *repositoryMock) UpdateApplication(ctx context.Context, app *model.Application) error { + args := m.Called(ctx, app) return args.Error(0) } -func (m *claimerRepositoryMock) UpdateApplicationState( - ctx context.Context, - appID int64, - state model.ApplicationState, - reason *string, -) error { +func (m *repositoryMock) UpdateApplicationState(ctx context.Context, appID int64, state model.ApplicationState, reason *string) error { args := m.Called(ctx, appID, state, reason) return args.Error(0) } -func (m *claimerRepositoryMock) UpdateEpochWithAcceptedClaim( - ctx context.Context, - appid int64, - index uint64, -) error { - args := m.Called(ctx, appid, index) +func (m *repositoryMock) DeleteApplication(ctx context.Context, id int64) error { + args := m.Called(ctx, id) return args.Error(0) } -func (m *claimerRepositoryMock) SaveNodeConfigRaw( - ctx context.Context, - key string, - rawJSON []byte, -) error { +func (m *repositoryMock) ListApplications(ctx context.Context, f repository.ApplicationFilter, p repository.Pagination, descending bool) ([]*model.Application, uint64, error) { + args := m.Called(ctx, f, p, descending) + return args.Get(0).([]*model.Application), args.Get(1).(uint64), args.Error(2) +} + +func (m *repositoryMock) GetExecutionParameters(ctx context.Context, applicationID int64) (*model.ExecutionParameters, error) { + args := m.Called(ctx, applicationID) + return args.Get(0).(*model.ExecutionParameters), args.Error(1) +} + +func (m *repositoryMock) UpdateExecutionParameters(ctx context.Context, ep *model.ExecutionParameters) error { + args := m.Called(ctx, ep) + return args.Error(0) +} + +func (m *repositoryMock) GetEventLastCheckBlock(ctx context.Context, appID int64, event model.MonitoredEvent) (uint64, error) { + args := m.Called(ctx, appID, event) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *repositoryMock) UpdateEventLastCheckBlock(ctx context.Context, appIDs []int64, event model.MonitoredEvent, blockNumber uint64) error { + args := m.Called(ctx, appIDs, event, blockNumber) + return args.Error(0) +} + +func (m *repositoryMock) GetLastSnapshot(ctx context.Context, nameOrAddress string) (*model.Input, error) { + args := m.Called(ctx, nameOrAddress) + return args.Get(0).(*model.Input), args.Error(1) +} + +// EpochRepository methods + +func (m *repositoryMock) CreateEpochsAndInputs(ctx context.Context, nameOrAddress string, epochInputMap map[*model.Epoch][]*model.Input, blockNumber uint64) error { + args := m.Called(ctx, nameOrAddress, epochInputMap, blockNumber) + return args.Error(0) +} + +func (m *repositoryMock) GetEpoch(ctx context.Context, nameOrAddress string, index uint64) (*model.Epoch, error) { + args := m.Called(ctx, nameOrAddress, index) + return args.Get(0).(*model.Epoch), args.Error(1) +} + +func (m *repositoryMock) GetLastAcceptedEpochIndex(ctx context.Context, nameOrAddress string) (uint64, error) { + args := m.Called(ctx, nameOrAddress) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *repositoryMock) GetLastNonOpenEpoch(ctx context.Context, nameOrAddress string) (*model.Epoch, error) { + args := m.Called(ctx, nameOrAddress) + return args.Get(0).(*model.Epoch), args.Error(1) +} + +func (m *repositoryMock) GetEpochByVirtualIndex(ctx context.Context, nameOrAddress string, index uint64) (*model.Epoch, error) { + args := m.Called(ctx, nameOrAddress, index) + return args.Get(0).(*model.Epoch), args.Error(1) +} + +func (m *repositoryMock) UpdateEpochClaimTransactionHash(ctx context.Context, nameOrAddress string, e *model.Epoch) error { + args := m.Called(ctx, nameOrAddress, e) + return args.Error(0) +} + +func (m *repositoryMock) UpdateEpochClaimSubmittedTransactionHash(ctx context.Context, nameOrAddress string, e *model.Epoch) error { + args := m.Called(ctx, nameOrAddress, e) + return args.Error(0) +} + +func (m *repositoryMock) UpdateEpochClaimAcceptedTransactionHash(ctx context.Context, nameOrAddress string, e *model.Epoch) error { + args := m.Called(ctx, nameOrAddress, e) + return args.Error(0) +} + +func (m *repositoryMock) UpdateEpochStatus(ctx context.Context, nameOrAddress string, e *model.Epoch) error { + args := m.Called(ctx, nameOrAddress, e) + return args.Error(0) +} + +func (m *repositoryMock) UpdateEpochInputsProcessed(ctx context.Context, nameOrAddress string, epochIndex uint64) error { + args := m.Called(ctx, nameOrAddress, epochIndex) + return args.Error(0) +} + +func (m *repositoryMock) UpdateEpochOutputsProof(ctx context.Context, appID int64, epochIndex uint64, proof *model.OutputsProof) error { + args := m.Called(ctx, appID, epochIndex, proof) + return args.Error(0) +} + +func (m *repositoryMock) RepeatPreviousEpochOutputsProof(ctx context.Context, appID int64, epochIndex uint64) error { + args := m.Called(ctx, appID, epochIndex) + return args.Error(0) +} + +func (m *repositoryMock) ListEpochs(ctx context.Context, nameOrAddress string, f repository.EpochFilter, p repository.Pagination, descending bool) ([]*model.Epoch, uint64, error) { + args := m.Called(ctx, nameOrAddress, f, p, descending) + return args.Get(0).([]*model.Epoch), args.Get(1).(uint64), args.Error(2) +} + +// NodeConfigRepository methods + +func (m *repositoryMock) SaveNodeConfigRaw(ctx context.Context, key string, rawJSON []byte) error { args := m.Called(ctx, key, rawJSON) return args.Error(0) } -func (m *claimerRepositoryMock) LoadNodeConfigRaw(ctx context.Context, key string) ( - rawJSON []byte, - createdAt, updatedAt time.Time, - err error, -) { +func (m *repositoryMock) LoadNodeConfigRaw(ctx context.Context, key string) ([]byte, time.Time, time.Time, error) { args := m.Called(ctx, key) return args.Get(0).([]byte), args.Get(1).(time.Time), args.Get(2).(time.Time), args.Error(3) } -type claimerBlockchainMock struct { +// blockchainMock implements iBlockchain via testify/mock. +// Only HeaderByNumber and TransactionReceipt are exercised by claimer logic; +// the ContractBackend methods are present to satisfy the interface but panic +// on unexpected calls. +type blockchainMock struct { mock.Mock } -func (m *claimerBlockchainMock) findClaimSubmittedEventAndSucc( - ctx context.Context, - app *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimSubmitted, - *iconsensus.IConsensusClaimSubmitted, - error, -) { - args := m.Called(ctx, app, epoch, fromBlock, toBlock) - return args.Get(0).(*iconsensus.IConsensus), - args.Get(1).(*iconsensus.IConsensusClaimSubmitted), - args.Get(2).(*iconsensus.IConsensusClaimSubmitted), - args.Error(3) -} - -func (m *claimerBlockchainMock) findClaimAcceptedEventAndSucc( - ctx context.Context, - app *model.Application, - epoch *model.Epoch, - fromBlock uint64, - toBlock uint64, -) ( - *iconsensus.IConsensus, - *iconsensus.IConsensusClaimAccepted, - *iconsensus.IConsensusClaimAccepted, - error, -) { - args := m.Called(ctx, app, epoch, fromBlock, toBlock) - return args.Get(0).(*iconsensus.IConsensus), - args.Get(1).(*iconsensus.IConsensusClaimAccepted), - args.Get(2).(*iconsensus.IConsensusClaimAccepted), - args.Error(3) -} - -func (m *claimerBlockchainMock) submitClaimToBlockchain( - instance *iconsensus.IConsensus, - app *model.Application, - epoch *model.Epoch, -) (common.Hash, error) { - args := m.Called(instance, app, epoch) - return args.Get(0).(common.Hash), args.Error(1) -} -func (m *claimerBlockchainMock) pollTransaction( - ctx context.Context, - txHash common.Hash, - endBlock *big.Int, -) (bool, *types.Receipt, error) { - args := m.Called(ctx, txHash, endBlock) - return args.Bool(0), - args.Get(1).(*types.Receipt), - args.Error(2) -} -func (m *claimerBlockchainMock) getDefaultBlockNumber(ctx context.Context) (*big.Int, error) { - args := m.Called(ctx) - return args.Get(0).(*big.Int), - args.Error(1) -} - -func (m *claimerBlockchainMock) getConsensusAddress( - ctx context.Context, - app *model.Application, -) (common.Address, error) { - args := m.Called(ctx, app) - return args.Get(0).(common.Address), - args.Error(1) +func (m *blockchainMock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + args := m.Called(ctx, number) + return args.Get(0).(*types.Header), args.Error(1) } -func newServiceMock() (*Service, *claimerRepositoryMock, *claimerBlockchainMock) { - opts := &tint.Options{ - Level: slog.LevelDebug, - AddSource: true, - // RFC3339 with milliseconds and without timezone - TimeFormat: "2006-01-02T15:04:05.000", - } - handler := tint.NewHandler(os.Stdout, opts) - repository := &claimerRepositoryMock{} - blockchain := &claimerBlockchainMock{} +func (m *blockchainMock) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + args := m.Called(ctx, txHash) + return args.Get(0).(*types.Receipt), args.Error(1) +} - claimer := &Service{ - Service: service.Service{ - Logger: slog.New(handler), - }, - submissionEnabled: true, - claimsInFlight: map[int64]common.Hash{}, - repository: repository, - blockchain: blockchain, - } - return claimer, repository, blockchain +func (m *blockchainMock) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + panic("unexpected call to CodeAt") +} +func (m *blockchainMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + panic("unexpected call to CallContract") +} +func (m *blockchainMock) PendingCodeAt(ctx context.Context, contract common.Address) ([]byte, error) { + panic("unexpected call to PendingCodeAt") +} +func (m *blockchainMock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + panic("unexpected call to PendingNonceAt") +} +func (m *blockchainMock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + panic("unexpected call to SuggestGasPrice") +} +func (m *blockchainMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + panic("unexpected call to SuggestGasTipCap") +} +func (m *blockchainMock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + panic("unexpected call to EstimateGas") +} +func (m *blockchainMock) SendTransaction(ctx context.Context, tx *types.Transaction) error { + panic("unexpected call to SendTransaction") +} +func (m *blockchainMock) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { + panic("unexpected call to FilterLogs") +} +func (m *blockchainMock) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + panic("unexpected call to SubscribeFilterLogs") } -func makeApplication(id int64) *model.Application { - return &model.Application{ - ID: id, - IApplicationAddress: common.HexToAddress("0x01"), - IConsensusAddress: common.HexToAddress("0x01"), - IInputBoxAddress: common.HexToAddress("0x02"), - } +// consensusMock implements iConsensus via testify/mock. +type consensusMock struct { + mock.Mock } -func makeEpoch(id int64, status model.EpochStatus, i uint64) *model.Epoch { - hash := common.HexToHash("0x01") - tx := common.HexToHash("0x02") - epoch := &model.Epoch{ - ApplicationID: id, - Index: i, - FirstBlock: i * 10, - LastBlock: i*10 + 9, - Status: status, - ClaimTransactionHash: &tx, - OutputsMerkleRoot: &hash, - } - return epoch +func (m *consensusMock) GetNumberOfSubmittedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { + args := m.Called(opts, appContract) + return args.Get(0).(*big.Int), args.Error(1) } -func makeAcceptedEpoch(app *model.Application, i uint64) *model.Epoch { - return makeEpoch(app.ID, model.EpochStatus_ClaimAccepted, i) +func (m *consensusMock) GetNumberOfAcceptedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { + args := m.Called(opts, appContract) + return args.Get(0).(*big.Int), args.Error(1) } -func makeSubmittedEpoch(app *model.Application, i uint64) *model.Epoch { - return makeEpoch(app.ID, model.EpochStatus_ClaimSubmitted, i) +func (m *consensusMock) FilterClaimSubmitted(opts *bind.FilterOpts, submitter []common.Address, appContract []common.Address) ([]*iconsensus.IConsensusClaimSubmitted, error) { + args := m.Called(opts, submitter, appContract) + return args.Get(0).([]*iconsensus.IConsensusClaimSubmitted), args.Error(1) } -func makeComputedEpoch(app *model.Application, i uint64) *model.Epoch { - return makeEpoch(app.ID, model.EpochStatus_ClaimComputed, i) +func (m *consensusMock) FilterClaimAccepted(opts *bind.FilterOpts, appContract []common.Address) ([]*iconsensus.IConsensusClaimAccepted, error) { + args := m.Called(opts, appContract) + return args.Get(0).([]*iconsensus.IConsensusClaimAccepted), args.Error(1) } -func makeEpochMap(epochs ...*model.Epoch) map[int64]*model.Epoch { - result := map[int64]*model.Epoch{} - for _, epoch := range epochs { - result[epoch.ApplicationID] = epoch - } - return result + +func (m *consensusMock) SubmitClaim(opts *bind.TransactOpts, appContract common.Address, lastProcessedBlockNumber *big.Int, outputsMerkleRoot [32]byte) (*types.Transaction, error) { + args := m.Called(opts, appContract, lastProcessedBlockNumber, outputsMerkleRoot) + return args.Get(0).(*types.Transaction), args.Error(1) } -func makeApplicationMap(apps ...*model.Application) map[int64]*model.Application { - result := map[int64]*model.Application{} - for _, app := range apps { - result[app.ID] = app + +// //////////////////////////////////////////////////////////////////////////// +// Helpers +// //////////////////////////////////////////////////////////////////////////// + +func newLogger() *slog.Logger { + opts := &tint.Options{ + Level: slog.LevelDebug, + AddSource: true, + TimeFormat: "2006-01-02T15:04:05.000", } - return result + return slog.New(tint.NewHandler(os.Stdout, opts)) +} + +var ( + merkleRoot = common.HexToHash("0xDEAD") + txHashA = common.HexToHash("0xAAAA") + txHashB = common.HexToHash("0xBBBB") +) + +func makeApp() *model.Application { + return repotest.NewApplicationBuilder().Build() +} + +func makeDaveApp() *model.Application { + return repotest.NewApplicationBuilder().WithConsensus(model.Consensus_PRT).Build() +} + +// makeComputedEpoch returns a ClaimComputed epoch with blocks [index*10, index*10+9] +// and VirtualIndex == Index (the common case). +func makeComputedEpoch(appID int64, index uint64) *model.Epoch { + e := repotest.NewEpochBuilder(appID). + WithIndex(index). + WithStatus(model.EpochStatus_ClaimComputed). + WithBlocks(index*10, index*10+9). + WithClaimHash(merkleRoot). + Build() + e.VirtualIndex = index + return e +} + +// makeSubmittedEpoch returns a ClaimSubmitted epoch with blocks [index*10, index*10+9] +// and VirtualIndex == Index. +func makeSubmittedEpoch(appID int64, index uint64) *model.Epoch { + e := repotest.NewEpochBuilder(appID). + WithIndex(index). + WithStatus(model.EpochStatus_ClaimSubmitted). + WithBlocks(index*10, index*10+9). + WithClaimHash(merkleRoot). + Build() + e.VirtualIndex = index + return e } -func makeSubmittedEvent(app *model.Application, epoch *model.Epoch) *iconsensus.IConsensusClaimSubmitted { +func makeSubmittedEvent(app *model.Application, epoch *model.Epoch, txHash common.Hash) *iconsensus.IConsensusClaimSubmitted { return &iconsensus.IConsensusClaimSubmitted{ - LastProcessedBlockNumber: new(big.Int).SetUint64(epoch.LastBlock), AppContract: app.IApplicationAddress, + LastProcessedBlockNumber: new(big.Int).SetUint64(epoch.LastBlock), OutputsMerkleRoot: *epoch.OutputsMerkleRoot, Raw: types.Log{ - TxHash: common.HexToHash(epoch.ClaimTransactionHash.Hex()), + TxHash: txHash, BlockNumber: epoch.LastBlock + 5, }, } } -func makeAcceptedEvent(app *model.Application, epoch *model.Epoch) *iconsensus.IConsensusClaimAccepted { +func makeAcceptedEvent(app *model.Application, epoch *model.Epoch, txHash common.Hash) *iconsensus.IConsensusClaimAccepted { return &iconsensus.IConsensusClaimAccepted{ - LastProcessedBlockNumber: new(big.Int).SetUint64(epoch.LastBlock), AppContract: app.IApplicationAddress, + LastProcessedBlockNumber: new(big.Int).SetUint64(epoch.LastBlock), OutputsMerkleRoot: *epoch.OutputsMerkleRoot, Raw: types.Log{ - TxHash: common.HexToHash(epoch.ClaimTransactionHash.Hex()), + TxHash: txHash, BlockNumber: epoch.LastBlock + 5, }, } } -// ////////////////////////////////////////////////////////////////////////////// -// Success -// ////////////////////////////////////////////////////////////////////////////// -func TestDoNothing(t *testing.T) { - m, r, _ := newServiceMock() - defer r.AssertExpectations(t) +// makeTx creates a minimal signed transaction whose Hash() is deterministic +// for a given nonce value. +func makeTx(nonce uint64) *types.Transaction { + return types.NewTx(&types.LegacyTx{Nonce: nonce}) +} - prevEpochs := makeEpochMap() - currEpochs := makeEpochMap() +// epochFilter returns the EpochFilter the claimer passes to ListEpochs. +func epochFilter() repository.EpochFilter { + return repository.EpochFilter{ + Status: []model.EpochStatus{ + model.EpochStatus_ClaimComputed, + model.EpochStatus_ClaimSubmitted, + }, + } +} - errs := m.submitClaimsAndUpdateDatabase(prevEpochs, currEpochs, makeApplicationMap(), big.NewInt(0)) - assert.Equal(t, len(errs), 0) +// appFilter returns the ApplicationFilter the claimer passes to ListApplications. +func appFilter() repository.ApplicationFilter { + return repository.ApplicationFilter{ + State: model.Pointer(model.ApplicationState_Enabled), + } } -func TestSubmitFirstClaim(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) - defer b.AssertExpectations(t) +// //////////////////////////////////////////////////////////////////////////// +// checkClaimSubmittedEvent +// //////////////////////////////////////////////////////////////////////////// - endBlock := big.NewInt(40) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - var prevEvent *iconsensus.IConsensusClaimSubmitted = nil - var currEvent *iconsensus.IConsensusClaimSubmitted = nil +func TestCheckSubmitted_Valid(t *testing.T) { + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + ev := makeSubmittedEvent(app, epoch, txHashA) + assert.NoError(t, checkClaimSubmittedEvent(app, epoch, ev)) +} - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - b.On("submitClaimToBlockchain", mock.Anything, app, currEpoch). - Return(common.HexToHash("0x10"), nil).Once() +func TestCheckSubmitted_AddressMismatch(t *testing.T) { + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + ev := makeSubmittedEvent(app, epoch, txHashA) + ev.AppContract = common.HexToAddress("0xDEAD") + assert.Error(t, checkClaimSubmittedEvent(app, epoch, ev)) +} - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 1) +func TestCheckSubmitted_MerkleRootMismatch(t *testing.T) { + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + ev := makeSubmittedEvent(app, epoch, txHashA) + ev.OutputsMerkleRoot = common.HexToHash("0xBAD") + assert.Error(t, checkClaimSubmittedEvent(app, epoch, ev)) } -func TestSubmitClaimWithAntecessor(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) - defer b.AssertExpectations(t) +func TestCheckSubmitted_BlockMismatch(t *testing.T) { + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + ev := makeSubmittedEvent(app, epoch, txHashA) + ev.LastProcessedBlockNumber = new(big.Int).SetUint64(epoch.LastBlock + 1) + assert.Error(t, checkClaimSubmittedEvent(app, epoch, ev)) +} - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - var currEvent *iconsensus.IConsensusClaimSubmitted = nil - prevEvent := makeSubmittedEvent(app, prevEpoch) +// //////////////////////////////////////////////////////////////////////////// +// checkClaimAcceptedEvent +// //////////////////////////////////////////////////////////////////////////// - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - b.On("submitClaimToBlockchain", mock.Anything, app, currEpoch). - Return(common.HexToHash("0x10"), nil).Once() +func TestCheckAccepted_Valid(t *testing.T) { + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + ev := makeAcceptedEvent(app, epoch, txHashA) + assert.NoError(t, checkClaimAcceptedEvent(app, epoch, ev)) +} - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 1) +func TestCheckAccepted_AddressMismatch(t *testing.T) { + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + ev := makeAcceptedEvent(app, epoch, txHashA) + ev.AppContract = common.HexToAddress("0xDEAD") + assert.Error(t, checkClaimAcceptedEvent(app, epoch, ev)) } -func TestSkipSubmitFirstClaim(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) - defer b.AssertExpectations(t) +func TestCheckAccepted_MerkleRootMismatch(t *testing.T) { + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + ev := makeAcceptedEvent(app, epoch, txHashA) + ev.OutputsMerkleRoot = common.HexToHash("0xBAD") + assert.Error(t, checkClaimAcceptedEvent(app, epoch, ev)) +} - m.submissionEnabled = false - endBlock := big.NewInt(40) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - var prevEvent *iconsensus.IConsensusClaimSubmitted = nil - var currEvent *iconsensus.IConsensusClaimSubmitted = nil +func TestCheckAccepted_BlockMismatch(t *testing.T) { + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + ev := makeAcceptedEvent(app, epoch, txHashA) + ev.LastProcessedBlockNumber = new(big.Int).SetUint64(epoch.LastBlock + 1) + assert.Error(t, checkClaimAcceptedEvent(app, epoch, ev)) +} + +// //////////////////////////////////////////////////////////////////////////// +// getDefaultBlockNumber +// //////////////////////////////////////////////////////////////////////////// - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() +func TestGetDefaultBlock_Finalized(t *testing.T) { + b := &blockchainMock{} + defer b.AssertExpectations(t) + b.On("HeaderByNumber", mock.Anything, big.NewInt(rpc.FinalizedBlockNumber.Int64())). + Return(&types.Header{Number: big.NewInt(42)}, nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 0) + nr, err := getDefaultBlockNumber(context.Background(), b, model.DefaultBlock_Finalized) + assert.NoError(t, err) + assert.Equal(t, uint64(42), nr) } -func TestSkipSubmitClaimWithAntecessor(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) +func TestGetDefaultBlock_Latest(t *testing.T) { + b := &blockchainMock{} defer b.AssertExpectations(t) + b.On("HeaderByNumber", mock.Anything, big.NewInt(rpc.LatestBlockNumber.Int64())). + Return(&types.Header{Number: big.NewInt(100)}, nil).Once() - m.submissionEnabled = false - endBlock := big.NewInt(40) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - prevEvent := makeSubmittedEvent(app, prevEpoch) - var currEvent *iconsensus.IConsensusClaimSubmitted = nil + nr, err := getDefaultBlockNumber(context.Background(), b, model.DefaultBlock_Latest) + assert.NoError(t, err) + assert.Equal(t, uint64(100), nr) +} - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() +func TestGetDefaultBlock_Pending(t *testing.T) { + b := &blockchainMock{} + defer b.AssertExpectations(t) + b.On("HeaderByNumber", mock.Anything, big.NewInt(rpc.PendingBlockNumber.Int64())). + Return(&types.Header{Number: big.NewInt(101)}, nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 0) + nr, err := getDefaultBlockNumber(context.Background(), b, model.DefaultBlock_Pending) + assert.NoError(t, err) + assert.Equal(t, uint64(101), nr) } -func TestInFlightCompleted(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) +func TestGetDefaultBlock_Safe(t *testing.T) { + b := &blockchainMock{} defer b.AssertExpectations(t) + b.On("HeaderByNumber", mock.Anything, big.NewInt(rpc.SafeBlockNumber.Int64())). + Return(&types.Header{Number: big.NewInt(99)}, nil).Once() - txHash := common.HexToHash("0x10") - endBlock := big.NewInt(100) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - currEpoch.ClaimTransactionHash = &txHash + nr, err := getDefaultBlockNumber(context.Background(), b, model.DefaultBlock_Safe) + assert.NoError(t, err) + assert.Equal(t, uint64(99), nr) +} - m.claimsInFlight[app.ID] = *currEpoch.ClaimTransactionHash +func TestGetDefaultBlock_Invalid(t *testing.T) { + b := &blockchainMock{} + _, err := getDefaultBlockNumber(context.Background(), b, model.DefaultBlock("INVALID")) + assert.Error(t, err) +} - b.On("pollTransaction", mock.Anything, txHash, endBlock). - Return(true, &types.Receipt{ - ContractAddress: app.IApplicationAddress, - TxHash: txHash, - BlockNumber: new(big.Int).SetUint64(currEpoch.LastBlock + 1), - Status: 1, - }, nil).Once() - r.On("UpdateEpochWithSubmittedClaim", mock.Anything, app.ID, currEpoch.Index, txHash). - Return(nil).Once() +// //////////////////////////////////////////////////////////////////////////// +// trySubmitClaim +// //////////////////////////////////////////////////////////////////////////// - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 0) -} +// No previous in-flight tx, submission enabled → SubmitClaim called, hash persisted. +func TestTrySubmit_NilTxHash_Submits(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + // epoch.ClaimSubmittedTransactionHash is nil + + tx := makeTx(1) + txOpts := &bind.TransactOpts{} -func TestInFlightReverted(t *testing.T) { - m, r, b := newServiceMock() + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) + b := &blockchainMock{} + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("SubmitClaim", txOpts, app.IApplicationAddress, + new(big.Int).SetUint64(epoch.LastBlock), [32]byte(*epoch.OutputsMerkleRoot)). + Return(tx, nil).Once() + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() - txHash := common.HexToHash("0x10") - endBlock := big.NewInt(100) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - currEpoch.ClaimTransactionHash = &txHash + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, txOpts, 100) + assert.NoError(t, err) + assert.Equal(t, tx.Hash(), *epoch.ClaimSubmittedTransactionHash) + r.AssertExpectations(t) +} - var prevEvent *iconsensus.IConsensusClaimSubmitted = nil - var currEvent *iconsensus.IConsensusClaimSubmitted = nil +// No previous in-flight tx, submission disabled (txOpts == nil) → silent no-op. +func TestTrySubmit_NilTxHash_Disabled(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + + r := &repositoryMock{} + b := &blockchainMock{} + ic := &consensusMock{} + + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, nil, 100) + assert.NoError(t, err) + assert.Nil(t, epoch.ClaimSubmittedTransactionHash) + ic.AssertNotCalled(t, "SubmitClaim") +} - m.claimsInFlight[app.ID] = *currEpoch.ClaimTransactionHash +// In-flight tx found, receipt block > endBlock → wait, return nil. +func TestTrySubmit_InFlight_TooEarly(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epoch.ClaimSubmittedTransactionHash = &txHashA + endBlock := uint64(50) - b.On("pollTransaction", mock.Anything, txHash, endBlock). - Return(true, &types.Receipt{ - ContractAddress: app.IApplicationAddress, - TxHash: txHash, - BlockNumber: new(big.Int).SetUint64(currEpoch.LastBlock + 1), - Status: 0, + b := &blockchainMock{} + defer b.AssertExpectations(t) + r := &repositoryMock{} + ic := &consensusMock{} + + b.On("TransactionReceipt", ctx, txHashA). + Return(&types.Receipt{ + Status: 1, + BlockNumber: big.NewInt(int64(endBlock + 1)), // beyond endBlock }, nil).Once() - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - b.On("submitClaimToBlockchain", mock.Anything, app, currEpoch). - Return(common.HexToHash("0x10"), nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 1) + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, &bind.TransactOpts{}, endBlock) + assert.NoError(t, err) + r.AssertNotCalled(t, "UpdateEpochStatus") } -func TestUpdateFirstClaim(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) +// In-flight tx confirmed (Status=1, block <= endBlock) → UpdateEpochStatus called. +func TestTrySubmit_InFlight_Confirmed(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epoch.ClaimSubmittedTransactionHash = &txHashA + endBlock := uint64(100) + + b := &blockchainMock{} defer b.AssertExpectations(t) + r := &repositoryMock{} + defer r.AssertExpectations(t) + ic := &consensusMock{} - endBlock := big.NewInt(40) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - var prevEvent *iconsensus.IConsensusClaimSubmitted = nil - currEvent := makeSubmittedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, currEvent, prevEvent, nil).Once() - r.On("UpdateEpochWithSubmittedClaim", mock.Anything, app.ID, currEpoch.Index, currEvent.Raw.TxHash). + b.On("TransactionReceipt", ctx, txHashA). + Return(&types.Receipt{ + Status: 1, + TxHash: txHashA, + BlockNumber: big.NewInt(50), + }, nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 0) + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, &bind.TransactOpts{}, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimSubmitted, epoch.Status) + ic.AssertNotCalled(t, "SubmitClaim") } -func TestUpdateClaimWithAntecessor(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) +// In-flight tx reverted (Status=0) → falls through to submit label → resubmits. +func TestTrySubmit_InFlight_Reverted_Resubmits(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epoch.ClaimSubmittedTransactionHash = &txHashA + endBlock := uint64(100) + txOpts := &bind.TransactOpts{} + newTx := makeTx(2) + + b := &blockchainMock{} defer b.AssertExpectations(t) + r := &repositoryMock{} + defer r.AssertExpectations(t) + ic := &consensusMock{} + defer ic.AssertExpectations(t) - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - prevEvent := makeSubmittedEvent(app, prevEpoch) - currEvent := makeSubmittedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - r.On("UpdateEpochWithSubmittedClaim", mock.Anything, app.ID, currEpoch.Index, currEvent.Raw.TxHash). + b.On("TransactionReceipt", ctx, txHashA). + Return(&types.Receipt{ + Status: 0, + BlockNumber: big.NewInt(50), + }, nil).Once() + ic.On("SubmitClaim", txOpts, app.IApplicationAddress, + new(big.Int).SetUint64(epoch.LastBlock), [32]byte(*epoch.OutputsMerkleRoot)). + Return(newTx, nil).Once() + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(m.claimsInFlight), 0) + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, txOpts, endBlock) + assert.NoError(t, err) + assert.Equal(t, newTx.Hash(), *epoch.ClaimSubmittedTransactionHash) } -func TestAcceptFirstClaim(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) +// In-flight tx not found (ethereum.NotFound) → falls through to submit label → resubmits. +func TestTrySubmit_InFlight_NotFound_Resubmits(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epoch.ClaimSubmittedTransactionHash = &txHashA + endBlock := uint64(100) + txOpts := &bind.TransactOpts{} + newTx := makeTx(3) + + b := &blockchainMock{} defer b.AssertExpectations(t) + r := &repositoryMock{} + defer r.AssertExpectations(t) + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + b.On("TransactionReceipt", ctx, txHashA). + Return((*types.Receipt)(nil), ethereum.NotFound).Once() + ic.On("SubmitClaim", txOpts, app.IApplicationAddress, + new(big.Int).SetUint64(epoch.LastBlock), [32]byte(*epoch.OutputsMerkleRoot)). + Return(newTx, nil).Once() + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() - endBlock := big.NewInt(100) - app := makeApplication(0) - currEpoch := makeSubmittedEpoch(app, 3) - var prevEvent *iconsensus.IConsensusClaimAccepted = nil - currEvent := makeAcceptedEvent(app, currEpoch) - - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) + err := trySubmitClaim(ctx, logger, app, epoch, r, ic, b, txOpts, endBlock) + assert.NoError(t, err) + assert.Equal(t, newTx.Hash(), *epoch.ClaimSubmittedTransactionHash) } -func TestAcceptClaimWithAntecessor(t *testing.T) { - m, r, b := newServiceMock() +// //////////////////////////////////////////////////////////////////////////// +// updateClaimSubmitted +// //////////////////////////////////////////////////////////////////////////// + +// collectClaimSubmittedEvents calls GetNumberOfSubmittedClaims twice (startBlock-1 +// and startBlock as part of FindTransitions). When the count does not change, +// no FilterClaimSubmitted is called and only UpdateEventLastCheckBlock is persisted. +func TestUpdateSubmitted_NoEvents(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + // startBlock = max(0, epoch.LastBlock) + 1 = epoch.LastBlock + 1 = 10 + // FindTransitions calls oracle at startBlock-1, startBlock, and endBlock (3 total) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeSubmittedEpoch(app, 3) - prevEvent := makeAcceptedEvent(app, prevEpoch) - currEvent := makeAcceptedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - r.On("UpdateEpochWithAcceptedClaim", mock.Anything, app.ID, currEpoch.Index). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + count := big.NewInt(0) + // oracle called at startBlock-1, startBlock, and endBlock (FindTransitions) + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(count, nil).Times(3) + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimSubmitted, endBlock). Return(nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + // epoch status should be unchanged + assert.Equal(t, model.EpochStatus_ClaimComputed, epoch.Status) } -// ////////////////////////////////////////////////////////////////////////////// -// Failure -// ////////////////////////////////////////////////////////////////////////////// - -func TestClaimInFlightMissingFromCurrClaims(t *testing.T) { - m, r, b := newServiceMock() +// One submitted event matches the first epoch (VirtualIndex=0, base=0). +// Epoch status advances from CLAIM_COMPUTED to CLAIM_SUBMITTED. +func TestUpdateSubmitted_FirstClaim(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + // VirtualIndex == 0 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeSubmittedEvent(app, epoch, txHashA) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - reqHash := common.HexToHash("0x01") - receipt := new(types.Receipt) - - app := makeApplication(0) - m.claimsInFlight[app.ID] = reqHash - - b.On("pollTransaction", mock.Anything, reqHash, endBlock). - Return(true, receipt, nil).Once() + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + // At startBlock-1: count=0 (base), at startBlock: count=1 (transition found), + // at endBlock: count=1 (no further transitions) + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() // prevValue at startBlock-1 + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() // startBlock value in FindTransitions + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() // endBlock value — startValue==endValue, no binary search + ic.On("FilterClaimSubmitted", mock.Anything, ([]common.Address)(nil), + []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimSubmitted{ev}, nil).Once() + + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimSubmitted, endBlock). + Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 0) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimSubmitted, epoch.Status) + assert.Equal(t, txHashA, *epoch.ClaimSubmittedTransactionHash) } -// submit again after pollTransaction failure -func TestSubmitFailedClaim(t *testing.T) { - m, r, b := newServiceMock() +// One submitted event matches second epoch (VirtualIndex=1, base=1). +func TestUpdateSubmitted_WithAntecessor(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 1) + epoch.VirtualIndex = 1 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeSubmittedEvent(app, epoch, txHashA) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - expectedErr := fmt.Errorf("not found") - endBlock := big.NewInt(100) - reqHash := common.HexToHash("0x01") - var nilReceipt *types.Receipt - - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - prevEvent := makeSubmittedEvent(app, prevEpoch) - var currEvent *iconsensus.IConsensusClaimSubmitted = nil - - m.claimsInFlight[app.ID] = reqHash - - b.On("pollTransaction", mock.Anything, reqHash, endBlock). - Return(false, nilReceipt, expectedErr).Once() - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - b.On("submitClaimToBlockchain", mock.Anything, app, currEpoch). - Return(common.HexToHash("0x10"), nil).Once() + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + // prevValue = 1 (already 1 claim submitted before our range) + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() // startBlock-1 + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil).Once() // startBlock in FindTransitions → transition found + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil).Once() // endBlock — startValue==endValue, no binary search + ic.On("FilterClaimSubmitted", mock.Anything, ([]common.Address)(nil), + []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimSubmitted{ev}, nil).Once() + + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimSubmitted, endBlock). + Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 0, len(errs)) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimSubmitted, epoch.Status) } -// !claimSubmittedMatche(prevClaim, prevEvent) -func TestSubmitClaimWithAntecessorMismatch(t *testing.T) { - m, r, b := newServiceMock() +// Event sequence mismatch: epoch.VirtualIndex != base+i → application marked inoperable. +func TestUpdateSubmitted_VirtualIndexMismatch(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epoch.VirtualIndex = 5 // mismatched: base=0, expected=0, got=5 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeSubmittedEvent(app, epoch, txHashA) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - - // event has an incorrect LastProcessedBlockNumber field. - prevEvent := &iconsensus.IConsensusClaimSubmitted{ - LastProcessedBlockNumber: new(big.Int).SetUint64(prevEpoch.LastBlock + 1), - AppContract: app.IApplicationAddress, - OutputsMerkleRoot: *prevEpoch.OutputsMerkleRoot, - } - var currEvent *iconsensus.IConsensusClaimSubmitted = nil - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil). - Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil). - Once() - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("FilterClaimSubmitted", mock.Anything, ([]common.Address)(nil), + []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimSubmitted{ev}, nil).Once() + + r.On("UpdateApplicationState", ctx, app.ID, model.ApplicationState_Inoperable, mock.Anything). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.Error(t, err) } -// !claimMatchesEvent(currClaim, currEvent) -func TestSubmitClaimWithEventMismatch(t *testing.T) { - m, r, b := newServiceMock() +// Event field mismatch (wrong OutputsMerkleRoot) → application marked inoperable. +func TestUpdateSubmitted_EventMismatch(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeSubmittedEvent(app, epoch, txHashA) + ev.OutputsMerkleRoot = common.HexToHash("0xBAD") // wrong root + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(40) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - prevEvent := makeSubmittedEvent(app, prevEpoch) - wrongEvent := makeSubmittedEvent(app, makeComputedEpoch(app, 2)) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, wrongEvent, nil) - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("FilterClaimSubmitted", mock.Anything, ([]common.Address)(nil), + []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimSubmitted{ev}, nil).Once() + + r.On("UpdateApplicationState", ctx, app.ID, model.ApplicationState_Inoperable, mock.Anything). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.Error(t, err) } -// !checkClaimsConstraint(prevClaim, currClaim) // epoch pair has its blocks out of order -func TestSubmitClaimWithAntecessorOutOfOrder(t *testing.T) { - m, r, b := newServiceMock() +// More events than epochs: only the available epoch is processed; endBlock is +// capped to the surplus event's block minus 1 before UpdateEventLastCheckBlock. +func TestUpdateSubmitted_MoreEventsThanEpochs(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeComputedEpoch(app.ID, 0) + epochs := []*model.Epoch{epoch} // only 1 epoch + endBlock := uint64(200) + + ev0 := makeSubmittedEvent(app, epoch, txHashA) + // ev1 is a second event for which there is no epoch + epoch1 := makeComputedEpoch(app.ID, 1) + ev1 := makeSubmittedEvent(app, epoch1, txHashB) + ev1.Raw.BlockNumber = 150 + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - app := makeApplication(0) - prevEpoch := makeSubmittedEpoch(app, 2) - currEpoch := makeComputedEpoch(app, 1) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + // Two transitions: count goes 0→1 at one block, 1→2 at another. + // Simplify by returning both events from a single FilterClaimSubmitted call + // on a single transition block (count jumps by 2). + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() // prevValue + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil).Once() // endBlock value + ic.On("GetNumberOfSubmittedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil) // mid-point queries all resolve to 2 as well + ic.On("FilterClaimSubmitted", mock.Anything, ([]common.Address)(nil), + []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimSubmitted{ev0, ev1}, nil).Once() + + r.On("UpdateEpochClaimSubmittedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). + Return(nil).Once() + // endBlock is capped to ev1.Raw.BlockNumber - 1 = 149 + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimSubmitted, uint64(149)). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), big.NewInt(0)) - assert.Equal(t, 1, len(errs)) + err := updateClaimSubmitted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimSubmitted, epoch.Status) } -func TestErrSubmittedMissingEvent(t *testing.T) { - m, r, b := newServiceMock() +// //////////////////////////////////////////////////////////////////////////// +// updateClaimAccepted +// //////////////////////////////////////////////////////////////////////////// + +// updateClaimAccepted must use GetNumberOfAcceptedClaims (not GetNumberOfSubmittedClaims). +// When the count does not change, no FilterClaimAccepted is called. +func TestUpdateAccepted_NoEvents(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeComputedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 2) - var prevEvent *iconsensus.IConsensusClaimSubmitted = nil - currEvent := makeSubmittedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimSubmittedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + count := big.NewInt(0) + // oracle must call GetNumberOfAcceptedClaims — NOT GetNumberOfSubmittedClaims + // FindTransitions calls oracle at startBlock-1, startBlock, and endBlock (3 total) + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(count, nil).Times(3) + // GetNumberOfSubmittedClaims must NOT be called (would indicate the bug is present) + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimAccepted, endBlock). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimAccepted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + ic.AssertNotCalled(t, "GetNumberOfSubmittedClaims") } -func TestConsensusAddressChangedOnSubmittedClaims(t *testing.T) { - m, r, b := newServiceMock() +// One accepted event matches the first epoch. +func TestUpdateAccepted_FirstClaim(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + epoch.VirtualIndex = 0 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeAcceptedEvent(app, epoch, txHashA) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - wrongConsensusAddress := app.IConsensusAddress - wrongConsensusAddress[0]++ - - b.On("getConsensusAddress", mock.Anything, app). - Return(wrongConsensusAddress, nil). - Once() - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("FilterClaimAccepted", mock.Anything, []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimAccepted{ev}, nil).Once() + + r.On("UpdateEpochClaimAcceptedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimAccepted, endBlock). Return(nil).Once() - errs := m.submitClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 1) + err := updateClaimAccepted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimAccepted, epoch.Status) + assert.Equal(t, txHashA, *epoch.ClaimAcceptedTransactionHash) } -//////////////////////////////////////////////////////////////////////////////// - -func TestFindClaimAcceptedEventAndSuccFailure0(t *testing.T) { - m, r, b := newServiceMock() +// VirtualIndex mismatch on accepted event → application marked inoperable. +func TestUpdateAccepted_VirtualIndexMismatch(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + epoch.VirtualIndex = 7 // base=0, expected=0, got=7 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeAcceptedEvent(app, epoch, txHashA) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - expectedErr := fmt.Errorf("not found") - endBlock := big.NewInt(100) - - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 2) - var prevEvent *iconsensus.IConsensusClaimAccepted = nil - currEvent := makeAcceptedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, currEpoch, currEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, expectedErr).Once() + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("FilterClaimAccepted", mock.Anything, []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimAccepted{ev}, nil).Once() + + r.On("UpdateApplicationState", ctx, app.ID, model.ApplicationState_Inoperable, mock.Anything). + Return(nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimAccepted(ctx, logger, app, epochs, r, ic, endBlock) + assert.Error(t, err) } -func TestFindClaimAcceptedEventAndSuccFailure1(t *testing.T) { - m, r, b := newServiceMock() +// Event field mismatch (wrong OutputsMerkleRoot) on accepted event → inoperable. +func TestUpdateAccepted_EventMismatch(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + epoch.VirtualIndex = 0 + epochs := []*model.Epoch{epoch} + endBlock := uint64(100) + ev := makeAcceptedEvent(app, epoch, txHashA) + ev.OutputsMerkleRoot = common.HexToHash("0xBAD") + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - expectedErr := fmt.Errorf("not found") - endBlock := big.NewInt(100) - - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 2) - prevEvent := makeAcceptedEvent(app, prevEpoch) - currEvent := makeAcceptedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, expectedErr).Once() + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(1), nil).Once() + ic.On("FilterClaimAccepted", mock.Anything, []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimAccepted{ev}, nil).Once() + + r.On("UpdateApplicationState", ctx, app.ID, model.ApplicationState_Inoperable, mock.Anything). + Return(nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimAccepted(ctx, logger, app, epochs, r, ic, endBlock) + assert.Error(t, err) } -// !claimAcceptedMatch(prevClaim, prevEvent) -func TestAcceptClaimWithAntecessorMismatch(t *testing.T) { - m, r, b := newServiceMock() +// More accepted events than epochs → only available epochs processed, endBlock capped. +func TestUpdateAccepted_MoreEventsThanEpochs(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + epoch := makeSubmittedEpoch(app.ID, 0) + epoch.VirtualIndex = 0 + epochs := []*model.Epoch{epoch} + endBlock := uint64(200) + + ev0 := makeAcceptedEvent(app, epoch, txHashA) + epoch1 := makeSubmittedEpoch(app.ID, 1) + ev1 := makeAcceptedEvent(app, epoch1, txHashB) + ev1.Raw.BlockNumber = 150 + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) - - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 3) - - prevEvent := &iconsensus.IConsensusClaimAccepted{ - LastProcessedBlockNumber: new(big.Int).SetUint64(prevEpoch.LastBlock + 1), - AppContract: app.IApplicationAddress, - OutputsMerkleRoot: *prevEpoch.OutputsMerkleRoot, - } - var currEvent *iconsensus.IConsensusClaimAccepted = nil - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil) - r.On("UpdateApplicationState", mock.Anything, mock.Anything, model.ApplicationState_Inoperable, mock.Anything). + ic := &consensusMock{} + defer ic.AssertExpectations(t) + + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(0), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil).Once() + ic.On("GetNumberOfAcceptedClaims", mock.Anything, app.IApplicationAddress). + Return(big.NewInt(2), nil) + ic.On("FilterClaimAccepted", mock.Anything, []common.Address{app.IApplicationAddress}). + Return([]*iconsensus.IConsensusClaimAccepted{ev0, ev1}, nil).Once() + + r.On("UpdateEpochClaimAcceptedTransactionHash", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEpochStatus", ctx, app.Name, epoch). + Return(nil).Once() + r.On("UpdateEventLastCheckBlock", ctx, []int64{app.ID}, model.MonitoredEvent_ClaimAccepted, uint64(149)). Return(nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateClaimAccepted(ctx, logger, app, epochs, r, ic, endBlock) + assert.NoError(t, err) + assert.Equal(t, model.EpochStatus_ClaimAccepted, epoch.Status) } -// !claimAcceptedMatch(currClaim, currEvent) -func TestAcceptClaimWithEventMismatch(t *testing.T) { - m, r, b := newServiceMock() +// //////////////////////////////////////////////////////////////////////////// +// updateApplication +// //////////////////////////////////////////////////////////////////////////// + +// When ListEpochs returns no epochs, updateApplication returns nil immediately +// and never calls newConsensus (and thus never calls iBlockchain RPC methods). +func TestUpdateApp_NoEpochs(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + endBlock := uint64(100) + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) + b := &blockchainMock{} - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeAcceptedEpoch(app, 1) - wrongEpoch := makeComputedEpoch(app, 2) - currEpoch := makeComputedEpoch(app, 3) - wrongEvent := makeAcceptedEvent(app, wrongEpoch) - prevEvent := makeAcceptedEvent(app, prevEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, wrongEvent, nil) - r.On("UpdateApplicationState", mock.Anything, mock.Anything, model.ApplicationState_Inoperable, mock.Anything). - Return(nil).Once() + r.On("ListEpochs", ctx, app.Name, epochFilter(), repository.Pagination{}, false). + Return([]*model.Epoch{}, uint64(0), nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + err := updateApplication(ctx, logger, nil, r, b, app, endBlock) + assert.NoError(t, err) } -// !checkClaimsConstraint(prevClaim, currClaim) -func TestAcceptClaimWithAntecessorOutOfOrder(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) - defer b.AssertExpectations(t) +// //////////////////////////////////////////////////////////////////////////// +// update +// //////////////////////////////////////////////////////////////////////////// - app := makeApplication(0) - wrongEpoch := makeComputedEpoch(app, 2) - currEpoch := makeComputedEpoch(app, 1) +// When ListApplications returns no apps, update returns an empty error slice. +func TestUpdate_NoApps(t *testing.T) { + ctx := context.Background() + logger := newLogger() + + r := &repositoryMock{} + defer r.AssertExpectations(t) + b := &blockchainMock{} - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - r.On("UpdateApplicationState", mock.Anything, mock.Anything, model.ApplicationState_Inoperable, mock.Anything). - Return(nil). - Once() + r.On("ListApplications", ctx, appFilter(), repository.Pagination{}, true). + Return([]*model.Application{}, uint64(0), nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(wrongEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), big.NewInt(0)) - assert.Equal(t, 1, len(errs)) + errs := update(ctx, logger, nil, r, b, 100) + assert.Empty(t, errs) } -func TestErrAcceptedMissingEvent(t *testing.T) { - m, r, b := newServiceMock() +// Dave consensus (PRT) apps are skipped without error. +func TestUpdate_DaveConsensusSkipped(t *testing.T) { + ctx := context.Background() + logger := newLogger() + daveApp := makeDaveApp() + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) + b := &blockchainMock{} - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeComputedEpoch(app, 1) - currEpoch := makeComputedEpoch(app, 2) - var prevEvent *iconsensus.IConsensusClaimAccepted = nil - currEvent := makeAcceptedEvent(app, currEpoch) - - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - r.On("UpdateApplicationState", mock.Anything, mock.Anything, model.ApplicationState_Inoperable, mock.Anything). - Return(nil).Once() + r.On("ListApplications", ctx, appFilter(), repository.Pagination{}, true). + Return([]*model.Application{daveApp}, uint64(1), nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + errs := update(ctx, logger, nil, r, b, 100) + assert.Empty(t, errs) + // ListEpochs must not be called for a PRT app + r.AssertNotCalled(t, "ListEpochs") } -func TestUpdateEpochWithAcceptedClaimFailed(t *testing.T) { - m, r, b := newServiceMock() - defer r.AssertExpectations(t) - defer b.AssertExpectations(t) +// An enabled non-PRT app with no epochs is processed without error. +func TestUpdate_EnabledApp_NoEpochs(t *testing.T) { + ctx := context.Background() + logger := newLogger() + app := makeApp() + endBlock := uint64(100) - expectedErr := fmt.Errorf("not found") - - endBlock := big.NewInt(100) - app := makeApplication(0) - prevEpoch := makeSubmittedEpoch(app, 1) - currEpoch := makeSubmittedEpoch(app, 2) - prevEvent := makeAcceptedEvent(app, prevEpoch) - currEvent := makeAcceptedEvent(app, currEpoch) + r := &repositoryMock{} + defer r.AssertExpectations(t) + b := &blockchainMock{} - b.On("getConsensusAddress", mock.Anything, app). - Return(app.IConsensusAddress, nil).Once() - b.On("findClaimAcceptedEventAndSucc", mock.Anything, app, prevEpoch, prevEpoch.LastBlock+1, endBlock.Uint64()). - Return(&iconsensus.IConsensus{}, prevEvent, currEvent, nil).Once() - r.On("UpdateEpochWithAcceptedClaim", mock.Anything, app.ID, currEpoch.Index). - Return(expectedErr).Once() + r.On("ListApplications", ctx, appFilter(), repository.Pagination{}, true). + Return([]*model.Application{app}, uint64(1), nil).Once() + r.On("ListEpochs", ctx, app.Name, epochFilter(), repository.Pagination{}, false). + Return([]*model.Epoch{}, uint64(0), nil).Once() - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(prevEpoch), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, 1, len(errs)) + errs := update(ctx, logger, nil, r, b, endBlock) + assert.Empty(t, errs) } -func TestConsensusAddressChangedOnAcceptedClaims(t *testing.T) { - m, r, b := newServiceMock() +// ListApplications error is propagated as the single returned error. +func TestUpdate_ListApplicationsError(t *testing.T) { + ctx := context.Background() + logger := newLogger() + expectedErr := errors.New("db failure") + + r := &repositoryMock{} defer r.AssertExpectations(t) - defer b.AssertExpectations(t) + b := &blockchainMock{} + + r.On("ListApplications", ctx, appFilter(), repository.Pagination{}, true). + Return([]*model.Application(nil), uint64(0), expectedErr).Once() - endBlock := big.NewInt(100) - app := makeApplication(0) - currEpoch := makeComputedEpoch(app, 3) - wrongConsensusAddress := app.IConsensusAddress - wrongConsensusAddress[0]++ - - b.On("getConsensusAddress", mock.Anything, app). - Return(wrongConsensusAddress, nil). - Once() - r.On("UpdateApplicationState", mock.Anything, int64(0), model.ApplicationState_Inoperable, mock.Anything). - Return(nil). - Once() - - errs := m.acceptClaimsAndUpdateDatabase(makeEpochMap(), makeEpochMap(currEpoch), makeApplicationMap(app), endBlock) - assert.Equal(t, len(errs), 1) + errs := update(ctx, logger, nil, r, b, 100) + assert.Len(t, errs, 1) + assert.ErrorIs(t, errs[0], expectedErr) } diff --git a/internal/claimer/service.go b/internal/claimer/service.go index 46cf6fce6..99d006d16 100644 --- a/internal/claimer/service.go +++ b/internal/claimer/service.go @@ -16,7 +16,6 @@ import ( "github.com/cartesi/rollups-node/pkg/service" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) @@ -32,15 +31,11 @@ type CreateInfo struct { type Service struct { service.Service - repository iclaimerRepository - blockchain iclaimerBlockchain - - // submitted claims waiting for confirmation from the blockchain. - // only accessed from tick, so no need for a lock - // contains: application ID -> transaction hash, with a maximum of one - // key per application due to the epoch advancement logic. - claimsInFlight map[int64]common.Hash + repository iRepository + blockchain iBlockchain submissionEnabled bool + txOpts *bind.TransactOpts + defaultBlock model.DefaultBlock } const ClaimerConfigKey = "claimer" @@ -93,7 +88,7 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) { chainId.Uint64(), nodeConfig.ChainID) } s.submissionEnabled = nodeConfig.ClaimSubmissionEnabled - s.claimsInFlight = map[int64]common.Hash{} + s.defaultBlock = nodeConfig.DefaultBlock var txOpts *bind.TransactOpts = nil if s.submissionEnabled { @@ -104,12 +99,8 @@ func Create(ctx context.Context, c *CreateInfo) (*Service, error) { } s.repository = c.Repository - s.blockchain = &claimerBlockchain{ - logger: s.Logger, - client: c.EthConn, - txOpts: txOpts, - defaultBlock: c.Config.BlockchainDefaultBlock, - } + s.blockchain = c.EthConn + s.txOpts = txOpts return s, nil } @@ -132,49 +123,17 @@ func (s *Service) Stop(bool) []error { // NOTE: tick is not re-entrant! func (s *Service) Tick() []error { - errs := []error{} - - // gather epochs pairs with open claims, either: - // - computed but not yet submitted - acceptedOrSubmittedEpochs, computedEpochs, computedApps, errSubmitted := s.repository.SelectSubmittedClaimPairsPerApp(s.Context) - if errSubmitted != nil { - errs = append(errs, errSubmitted) - return errs - } - - // - submitted but not yet accepted. - acceptedEpochs, submittedEpochs, submittedApps, errAccepted := s.repository.SelectAcceptedClaimPairsPerApp(s.Context) - if errAccepted != nil { - errs = append(errs, errAccepted) - return errs - } - - s.Logger.Debug("Processing claims for epochs", - "computed", len(computedEpochs), - "submitted", len(submittedEpochs), - ) - - // return early if there is nothing to do - if len(computedEpochs) == 0 && len(submittedEpochs) == 0 { - return nil - } - - // we have claims to check. Get the latest/safe/finalized, etc. block - defaultBlockNumber, err := s.blockchain.getDefaultBlockNumber(s.Context) + nr, err := getDefaultBlockNumber(s.Context, s.blockchain, s.defaultBlock) if err != nil { - errs = append(errs, err) - return errs + return []error{err} } - - errs = append(errs, s.submitClaimsAndUpdateDatabase(acceptedOrSubmittedEpochs, computedEpochs, computedApps, defaultBlockNumber)...) - errs = append(errs, s.acceptClaimsAndUpdateDatabase(acceptedEpochs, submittedEpochs, submittedApps, defaultBlockNumber)...) - return errs + return update(s.Context, s.Logger, s.txOpts, s.repository, s.blockchain, nr) } func setupPersistentConfig( ctx context.Context, logger *slog.Logger, - repo iclaimerRepository, + repo iRepository, c *config.ClaimerConfig, ) (*PersistentConfig, error) { config, err := repository.LoadNodeConfig[PersistentConfig](ctx, repo, ClaimerConfigKey) diff --git a/internal/model/models.go b/internal/model/models.go index bb20e3cfb..6b3d600ee 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -17,27 +17,29 @@ import ( ) type Application struct { - ID int64 `sql:"primary_key" json:"-"` - Name string `json:"name"` - IApplicationAddress common.Address `json:"iapplication_address"` - IConsensusAddress common.Address `json:"iconsensus_address"` - IInputBoxAddress common.Address `json:"iinputbox_address"` - TemplateHash common.Hash `json:"template_hash"` - TemplateURI string `json:"-"` - EpochLength uint64 `json:"epoch_length"` - DataAvailability []byte `json:"data_availability"` - ConsensusType Consensus `json:"consensus_type"` - State ApplicationState `json:"state"` - Reason *string `json:"reason"` - IInputBoxBlock uint64 `json:"iinputbox_block"` - LastEpochCheckBlock uint64 `json:"last_epoch_check_block"` - LastInputCheckBlock uint64 `json:"last_input_check_block"` - LastOutputCheckBlock uint64 `json:"last_output_check_block"` - LastTournamentCheckBlock uint64 `json:"last_tournament_check_block"` - ProcessedInputs uint64 `json:"processed_inputs"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ExecutionParameters ExecutionParameters `json:"execution_parameters"` + ID int64 `sql:"primary_key" json:"-"` + Name string `json:"name"` + IApplicationAddress common.Address `json:"iapplication_address"` + IConsensusAddress common.Address `json:"iconsensus_address"` + IInputBoxAddress common.Address `json:"iinputbox_address"` + TemplateHash common.Hash `json:"template_hash"` + TemplateURI string `json:"-"` + EpochLength uint64 `json:"epoch_length"` + DataAvailability []byte `json:"data_availability"` + ConsensusType Consensus `json:"consensus_type"` + State ApplicationState `json:"state"` + Reason *string `json:"reason"` + IInputBoxBlock uint64 `json:"iinputbox_block"` + LastEpochCheckBlock uint64 `json:"last_epoch_check_block"` + LastInputCheckBlock uint64 `json:"last_input_check_block"` + LastOutputCheckBlock uint64 `json:"last_output_check_block"` + LastTournamentCheckBlock uint64 `json:"last_tournament_check_block"` + LastSubmittedClaimCheckBlock uint64 `json:"last_submitted_claim_check_block"` + LastAcceptedClaimCheckBlock uint64 `json:"last_accepted_claim_check_block"` + ProcessedInputs uint64 `json:"processed_inputs"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ExecutionParameters ExecutionParameters `json:"execution_parameters"` } // HasDataAvailabilitySelector checks if the application's DataAvailability @@ -581,23 +583,25 @@ func ParseHexDuration(s string) (time.Duration, error) { } type Epoch struct { - ApplicationID int64 `sql:"primary_key" json:"-"` - Index uint64 `sql:"primary_key" json:"index"` - FirstBlock uint64 `json:"first_block"` - LastBlock uint64 `json:"last_block"` - InputIndexLowerBound uint64 `json:"input_index_lower_bound"` - InputIndexUpperBound uint64 `json:"input_index_upper_bound"` - MachineHash *common.Hash `json:"machine_hash"` - OutputsMerkleRoot *common.Hash `json:"claim_hash"` - OutputsMerkleProof []common.Hash `json:"outputs_merkle_proof,omitempty"` - ClaimTransactionHash *common.Hash `json:"claim_transaction_hash"` - Commitment *common.Hash `json:"commitment"` - CommitmentProof []common.Hash `json:"commitment_proof,omitempty"` - TournamentAddress *common.Address `json:"tournament_address"` - Status EpochStatus `json:"status"` - VirtualIndex uint64 `json:"virtual_index"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ApplicationID int64 `sql:"primary_key" json:"-"` + Index uint64 `sql:"primary_key" json:"index"` + FirstBlock uint64 `json:"first_block"` + LastBlock uint64 `json:"last_block"` + InputIndexLowerBound uint64 `json:"input_index_lower_bound"` + InputIndexUpperBound uint64 `json:"input_index_upper_bound"` + MachineHash *common.Hash `json:"machine_hash"` + OutputsMerkleRoot *common.Hash `json:"claim_hash"` + OutputsMerkleProof []common.Hash `json:"outputs_merkle_proof,omitempty"` + ClaimSubmittedTransactionHash *common.Hash `json:"claim_submitted_transaction_hash"` + ClaimAcceptedTransactionHash *common.Hash `json:"claim_accepted_transaction_hash"` + ClaimTransactionHash *common.Hash `json:"claim_transaction_hash"` + Commitment *common.Hash `json:"commitment"` + CommitmentProof []common.Hash `json:"commitment_proof,omitempty"` + TournamentAddress *common.Address `json:"tournament_address"` + Status EpochStatus `json:"status"` + VirtualIndex uint64 `json:"virtual_index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (e *Epoch) MarshalJSON() ([]byte, error) { diff --git a/internal/repository/postgres/application.go b/internal/repository/postgres/application.go index b18efd6d8..7c4eeabea 100644 --- a/internal/repository/postgres/application.go +++ b/internal/repository/postgres/application.go @@ -41,6 +41,8 @@ func (r *PostgresRepository) CreateApplication( table.Application.LastInputCheckBlock, table.Application.LastOutputCheckBlock, table.Application.LastTournamentCheckBlock, + table.Application.LastSubmittedClaimCheckBlock, + table.Application.LastAcceptedClaimCheckBlock, table.Application.ProcessedInputs, ). VALUES( @@ -59,6 +61,8 @@ func (r *PostgresRepository) CreateApplication( app.LastInputCheckBlock, app.LastOutputCheckBlock, app.LastTournamentCheckBlock, + app.LastSubmittedClaimCheckBlock, + app.LastAcceptedClaimCheckBlock, app.ProcessedInputs, ). RETURNING(table.Application.ID) @@ -159,6 +163,8 @@ func (r *PostgresRepository) GetApplication( table.Application.LastInputCheckBlock, table.Application.LastOutputCheckBlock, table.Application.LastTournamentCheckBlock, + table.Application.LastSubmittedClaimCheckBlock, + table.Application.LastAcceptedClaimCheckBlock, table.Application.ProcessedInputs, table.Application.CreatedAt, table.Application.UpdatedAt, @@ -209,6 +215,8 @@ func (r *PostgresRepository) GetApplication( &app.LastInputCheckBlock, &app.LastOutputCheckBlock, &app.LastTournamentCheckBlock, + &app.LastSubmittedClaimCheckBlock, + &app.LastAcceptedClaimCheckBlock, &app.ProcessedInputs, &app.CreatedAt, &app.UpdatedAt, @@ -286,6 +294,8 @@ func (r *PostgresRepository) UpdateApplication( table.Application.LastInputCheckBlock, table.Application.LastOutputCheckBlock, table.Application.LastTournamentCheckBlock, + table.Application.LastSubmittedClaimCheckBlock, + table.Application.LastAcceptedClaimCheckBlock, table.Application.ProcessedInputs, ). SET( @@ -305,6 +315,8 @@ func (r *PostgresRepository) UpdateApplication( app.LastInputCheckBlock, app.LastOutputCheckBlock, app.LastTournamentCheckBlock, + app.LastSubmittedClaimCheckBlock, + app.LastAcceptedClaimCheckBlock, app.ProcessedInputs, ). WHERE(table.Application.ID.EQ(postgres.Int(app.ID))) @@ -356,9 +368,9 @@ func getColumnForEvent(event model.MonitoredEvent) (postgres.ColumnFloat, error) case model.MonitoredEvent_NewInnerTournament: return table.Application.LastTournamentCheckBlock, nil case model.MonitoredEvent_ClaimSubmitted: - fallthrough + return table.Application.LastSubmittedClaimCheckBlock, nil case model.MonitoredEvent_ClaimAccepted: - fallthrough + return table.Application.LastAcceptedClaimCheckBlock, nil default: return nil, fmt.Errorf("invalid monitored event type: %v", event) } @@ -530,6 +542,8 @@ func (r *PostgresRepository) ListApplications( table.Application.LastInputCheckBlock, table.Application.LastOutputCheckBlock, table.Application.LastTournamentCheckBlock, + table.Application.LastSubmittedClaimCheckBlock, + table.Application.LastAcceptedClaimCheckBlock, table.Application.ProcessedInputs, table.Application.CreatedAt, table.Application.UpdatedAt, @@ -618,6 +632,8 @@ func (r *PostgresRepository) ListApplications( &app.LastInputCheckBlock, &app.LastOutputCheckBlock, &app.LastTournamentCheckBlock, + &app.LastSubmittedClaimCheckBlock, + &app.LastAcceptedClaimCheckBlock, &app.ProcessedInputs, &app.CreatedAt, &app.UpdatedAt, diff --git a/internal/repository/postgres/claimer.go b/internal/repository/postgres/claimer.go deleted file mode 100644 index cd13526ee..000000000 --- a/internal/repository/postgres/claimer.go +++ /dev/null @@ -1,342 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package postgres - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/go-jet/jet/v2/postgres" - "github.com/jackc/pgx/v5" - - "github.com/cartesi/rollups-node/internal/model" - "github.com/cartesi/rollups-node/internal/repository" - "github.com/cartesi/rollups-node/internal/repository/postgres/db/rollupsdb/public/enum" - "github.com/cartesi/rollups-node/internal/repository/postgres/db/rollupsdb/public/table" -) - -// Retrieve the claim of each application with the smallest index. -// The query may return either 0 or 1 entries per application. -func (r *PostgresRepository) selectOldestClaimPerApp( - ctx context.Context, - tx pgx.Tx, - epochStatus model.EpochStatus, -) ( - map[int64]*model.Epoch, - map[int64]*model.Application, - error, -) { - if (epochStatus != model.EpochStatus_ClaimSubmitted) && (epochStatus != model.EpochStatus_ClaimComputed) { - return nil, nil, fmt.Errorf("invalid epoch status: %v", epochStatus) - } - - // NOTE(mpolitzer): DISTINCT ON is a postgres extension. To implement - // this in SQLite there is an alternative using GROUP BY and HAVING - // clauses instead. - stmt := table.Epoch.SELECT( - table.Epoch.ApplicationID, - table.Epoch.Index, - table.Epoch.FirstBlock, - table.Epoch.LastBlock, - table.Epoch.OutputsMerkleRoot, - table.Epoch.ClaimTransactionHash, - table.Epoch.Status, - table.Epoch.VirtualIndex, - table.Epoch.CreatedAt, - table.Epoch.UpdatedAt, - - table.Application.ID, - table.Application.Name, - table.Application.IapplicationAddress, - table.Application.IconsensusAddress, - table.Application.IinputboxAddress, - table.Application.TemplateHash, - table.Application.TemplateURI, - table.Application.EpochLength, - table.Application.DataAvailability, - table.Application.State, - table.Application.Reason, - table.Application.IinputboxBlock, - table.Application.LastInputCheckBlock, - table.Application.LastOutputCheckBlock, - table.Application.ProcessedInputs, - table.Application.CreatedAt, - table.Application.UpdatedAt, - ). - DISTINCT(table.Epoch.ApplicationID). - FROM( - table.Epoch. - INNER_JOIN( - table.Application, - table.Epoch.ApplicationID.EQ(table.Application.ID), - ), - ). - WHERE( - table.Epoch.Status.EQ(postgres.NewEnumValue(epochStatus.String())). - AND(table.Application.State.EQ(enum.ApplicationState.Enabled)). - AND(table.Application.ConsensusType.NOT_EQ(enum.Consensus.Prt)), - ). - ORDER_BY( - table.Epoch.ApplicationID, - table.Epoch.Index.ASC(), - ) - - sqlStr, args := stmt.Sql() - rows, err := tx.Query(ctx, sqlStr, args...) - if err != nil { - return nil, nil, err - } - defer rows.Close() - - epochs := map[int64]*model.Epoch{} - applications := map[int64]*model.Application{} - for rows.Next() { - var application model.Application - var epoch model.Epoch - err := rows.Scan( - &epoch.ApplicationID, - &epoch.Index, - &epoch.FirstBlock, - &epoch.LastBlock, - &epoch.OutputsMerkleRoot, - &epoch.ClaimTransactionHash, - &epoch.Status, - &epoch.VirtualIndex, - &epoch.CreatedAt, - &epoch.UpdatedAt, - - &application.ID, - &application.Name, - &application.IApplicationAddress, - &application.IConsensusAddress, - &application.IInputBoxAddress, - &application.TemplateHash, - &application.TemplateURI, - &application.EpochLength, - &application.DataAvailability, - &application.State, - &application.Reason, - &application.IInputBoxBlock, - &application.LastInputCheckBlock, - &application.LastOutputCheckBlock, - &application.ProcessedInputs, - &application.CreatedAt, - &application.UpdatedAt, - ) - if err != nil { - return nil, nil, err - } - epochs[application.ID] = &epoch - applications[application.ID] = &application - } - if err := rows.Err(); err != nil { - return nil, nil, err - } - return epochs, applications, nil -} - -// Retrieve the newest accepted claim of each application -func (r *PostgresRepository) selectNewestAcceptedClaimPerApp( - ctx context.Context, - tx pgx.Tx, - includeSubmitted bool, -) ( - map[int64]*model.Epoch, - error, -) { - expr := table.Epoch.Status.EQ(postgres.NewEnumValue(model.EpochStatus_ClaimAccepted.String())) - if includeSubmitted { - expr = expr.OR(table.Epoch.Status.EQ(postgres.NewEnumValue(model.EpochStatus_ClaimSubmitted.String()))) - } - - // NOTE(mpolitzer): DISTINCT ON is a postgres extension. To implement - // this in SQLite there is an alternative using GROUP BY and HAVING - // clauses instead. - stmt := table.Epoch.SELECT( - table.Epoch.ApplicationID, - table.Epoch.Index, - table.Epoch.FirstBlock, - table.Epoch.LastBlock, - table.Epoch.OutputsMerkleRoot, - table.Epoch.ClaimTransactionHash, - table.Epoch.Status, - table.Epoch.VirtualIndex, - table.Epoch.CreatedAt, - table.Epoch.UpdatedAt, - ). - DISTINCT(table.Epoch.ApplicationID). - FROM( - table.Epoch. - INNER_JOIN( - table.Application, - table.Epoch.ApplicationID.EQ(table.Application.ID), - ), - ). - WHERE( - expr.AND(table.Application.State.EQ(enum.ApplicationState.Enabled)). - AND(table.Application.ConsensusType.NOT_EQ(enum.Consensus.Prt)), - ). - ORDER_BY( - table.Epoch.ApplicationID, - table.Epoch.Index.DESC(), - ) - - sqlStr, args := stmt.Sql() - rows, err := tx.Query(ctx, sqlStr, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - epochs := map[int64]*model.Epoch{} - for rows.Next() { - var epoch model.Epoch - err := rows.Scan( - &epoch.ApplicationID, - &epoch.Index, - &epoch.FirstBlock, - &epoch.LastBlock, - &epoch.OutputsMerkleRoot, - &epoch.ClaimTransactionHash, - &epoch.Status, - &epoch.VirtualIndex, - &epoch.CreatedAt, - &epoch.UpdatedAt, - ) - if err != nil { - return nil, err - } - epochs[epoch.ApplicationID] = &epoch - } - if err := rows.Err(); err != nil { - return nil, err - } - return epochs, nil -} - -func (r *PostgresRepository) SelectSubmittedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, -) { - tx, err := r.db.BeginTx(ctx, pgx.TxOptions{ - IsoLevel: pgx.RepeatableRead, - AccessMode: pgx.ReadOnly, - }) - if err != nil { - return nil, nil, nil, err - } - // Read-only tx: rollback releases the snapshot, equivalent to commit. - defer tx.Rollback(ctx) //nolint:errcheck - - computed, applications, err := r.selectOldestClaimPerApp(ctx, tx, model.EpochStatus_ClaimComputed) - if err != nil { - return nil, nil, nil, err - } - - acceptedOrSubmitted, err := r.selectNewestAcceptedClaimPerApp(ctx, tx, true) - if err != nil { - return nil, nil, nil, err - } - - return acceptedOrSubmitted, computed, applications, err -} - -func (r *PostgresRepository) SelectAcceptedClaimPairsPerApp(ctx context.Context) ( - map[int64]*model.Epoch, - map[int64]*model.Epoch, - map[int64]*model.Application, - error, -) { - tx, err := r.db.BeginTx(ctx, pgx.TxOptions{ - IsoLevel: pgx.RepeatableRead, - AccessMode: pgx.ReadOnly, - }) - if err != nil { - return nil, nil, nil, err - } - // Read-only tx: rollback releases the snapshot, equivalent to commit. - defer tx.Rollback(ctx) //nolint:errcheck - - submitted, applications, err := r.selectOldestClaimPerApp(ctx, tx, model.EpochStatus_ClaimSubmitted) - if err != nil { - return nil, nil, nil, err - } - - accepted, err := r.selectNewestAcceptedClaimPerApp(ctx, tx, false) - if err != nil { - return nil, nil, nil, err - } - - return accepted, submitted, applications, err -} - -func (r *PostgresRepository) UpdateEpochWithSubmittedClaim( - ctx context.Context, - applicationID int64, - index uint64, - transactionHash common.Hash, -) error { - updStmt := table.Epoch. - UPDATE( - table.Epoch.ClaimTransactionHash, - table.Epoch.Status, - ). - SET( - transactionHash, - postgres.NewEnumValue(model.EpochStatus_ClaimSubmitted.String()), - ). - FROM( - table.Application, - ). - WHERE( - table.Epoch.ApplicationID.EQ(postgres.Int64(applicationID)). - AND(table.Epoch.Index.EQ(uint64Expr(index))). - AND(table.Epoch.Status.EQ(postgres.NewEnumValue(model.EpochStatus_ClaimComputed.String()))), - ) - - sqlStr, args := updStmt.Sql() - cmd, err := r.db.Exec(ctx, sqlStr, args...) - if err != nil { - return err - } - if cmd.RowsAffected() == 0 { - return repository.ErrNoUpdate - } - return nil -} - -func (r *PostgresRepository) UpdateEpochWithAcceptedClaim( - ctx context.Context, - applicationID int64, - index uint64, -) error { - updStmt := table.Epoch. - UPDATE( - table.Epoch.Status, - ). - SET( - postgres.NewEnumValue(model.EpochStatus_ClaimAccepted.String()), - ). - FROM( - table.Application, - ). - WHERE( - table.Epoch.ApplicationID.EQ(postgres.Int64(applicationID)). - AND(table.Epoch.Index.EQ(uint64Expr(index))). - AND(table.Epoch.Status.EQ(postgres.NewEnumValue(model.EpochStatus_ClaimSubmitted.String()))), - ) - - sqlStr, args := updStmt.Sql() - cmd, err := r.db.Exec(ctx, sqlStr, args...) - if err != nil { - return err - } - if cmd.RowsAffected() == 0 { - return repository.ErrNoUpdate - } - return nil -} diff --git a/internal/repository/postgres/db/rollupsdb/public/table/application.go b/internal/repository/postgres/db/rollupsdb/public/table/application.go index 8a855c89c..d58b66807 100644 --- a/internal/repository/postgres/db/rollupsdb/public/table/application.go +++ b/internal/repository/postgres/db/rollupsdb/public/table/application.go @@ -17,26 +17,28 @@ type applicationTable struct { postgres.Table // Columns - ID postgres.ColumnInteger - Name postgres.ColumnString - IapplicationAddress postgres.ColumnBytea - IconsensusAddress postgres.ColumnBytea - IinputboxAddress postgres.ColumnBytea - IinputboxBlock postgres.ColumnFloat - TemplateHash postgres.ColumnBytea - TemplateURI postgres.ColumnString - EpochLength postgres.ColumnFloat - DataAvailability postgres.ColumnBytea - ConsensusType postgres.ColumnString - State postgres.ColumnString - Reason postgres.ColumnString - LastEpochCheckBlock postgres.ColumnFloat - LastInputCheckBlock postgres.ColumnFloat - LastOutputCheckBlock postgres.ColumnFloat - LastTournamentCheckBlock postgres.ColumnFloat - ProcessedInputs postgres.ColumnFloat - CreatedAt postgres.ColumnTimestampz - UpdatedAt postgres.ColumnTimestampz + ID postgres.ColumnInteger + Name postgres.ColumnString + IapplicationAddress postgres.ColumnBytea + IconsensusAddress postgres.ColumnBytea + IinputboxAddress postgres.ColumnBytea + IinputboxBlock postgres.ColumnFloat + TemplateHash postgres.ColumnBytea + TemplateURI postgres.ColumnString + EpochLength postgres.ColumnFloat + DataAvailability postgres.ColumnBytea + ConsensusType postgres.ColumnString + State postgres.ColumnString + Reason postgres.ColumnString + LastEpochCheckBlock postgres.ColumnFloat + LastInputCheckBlock postgres.ColumnFloat + LastOutputCheckBlock postgres.ColumnFloat + LastSubmittedClaimCheckBlock postgres.ColumnFloat + LastAcceptedClaimCheckBlock postgres.ColumnFloat + LastTournamentCheckBlock postgres.ColumnFloat + ProcessedInputs postgres.ColumnFloat + CreatedAt postgres.ColumnTimestampz + UpdatedAt postgres.ColumnTimestampz AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -78,55 +80,59 @@ func newApplicationTable(schemaName, tableName, alias string) *ApplicationTable func newApplicationTableImpl(schemaName, tableName, alias string) applicationTable { var ( - IDColumn = postgres.IntegerColumn("id") - NameColumn = postgres.StringColumn("name") - IapplicationAddressColumn = postgres.ByteaColumn("iapplication_address") - IconsensusAddressColumn = postgres.ByteaColumn("iconsensus_address") - IinputboxAddressColumn = postgres.ByteaColumn("iinputbox_address") - IinputboxBlockColumn = postgres.FloatColumn("iinputbox_block") - TemplateHashColumn = postgres.ByteaColumn("template_hash") - TemplateURIColumn = postgres.StringColumn("template_uri") - EpochLengthColumn = postgres.FloatColumn("epoch_length") - DataAvailabilityColumn = postgres.ByteaColumn("data_availability") - ConsensusTypeColumn = postgres.StringColumn("consensus_type") - StateColumn = postgres.StringColumn("state") - ReasonColumn = postgres.StringColumn("reason") - LastEpochCheckBlockColumn = postgres.FloatColumn("last_epoch_check_block") - LastInputCheckBlockColumn = postgres.FloatColumn("last_input_check_block") - LastOutputCheckBlockColumn = postgres.FloatColumn("last_output_check_block") - LastTournamentCheckBlockColumn = postgres.FloatColumn("last_tournament_check_block") - ProcessedInputsColumn = postgres.FloatColumn("processed_inputs") - CreatedAtColumn = postgres.TimestampzColumn("created_at") - UpdatedAtColumn = postgres.TimestampzColumn("updated_at") - allColumns = postgres.ColumnList{IDColumn, NameColumn, IapplicationAddressColumn, IconsensusAddressColumn, IinputboxAddressColumn, IinputboxBlockColumn, TemplateHashColumn, TemplateURIColumn, EpochLengthColumn, DataAvailabilityColumn, ConsensusTypeColumn, StateColumn, ReasonColumn, LastEpochCheckBlockColumn, LastInputCheckBlockColumn, LastOutputCheckBlockColumn, LastTournamentCheckBlockColumn, ProcessedInputsColumn, CreatedAtColumn, UpdatedAtColumn} - mutableColumns = postgres.ColumnList{NameColumn, IapplicationAddressColumn, IconsensusAddressColumn, IinputboxAddressColumn, IinputboxBlockColumn, TemplateHashColumn, TemplateURIColumn, EpochLengthColumn, DataAvailabilityColumn, ConsensusTypeColumn, StateColumn, ReasonColumn, LastEpochCheckBlockColumn, LastInputCheckBlockColumn, LastOutputCheckBlockColumn, LastTournamentCheckBlockColumn, ProcessedInputsColumn, CreatedAtColumn, UpdatedAtColumn} - defaultColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn} + IDColumn = postgres.IntegerColumn("id") + NameColumn = postgres.StringColumn("name") + IapplicationAddressColumn = postgres.ByteaColumn("iapplication_address") + IconsensusAddressColumn = postgres.ByteaColumn("iconsensus_address") + IinputboxAddressColumn = postgres.ByteaColumn("iinputbox_address") + IinputboxBlockColumn = postgres.FloatColumn("iinputbox_block") + TemplateHashColumn = postgres.ByteaColumn("template_hash") + TemplateURIColumn = postgres.StringColumn("template_uri") + EpochLengthColumn = postgres.FloatColumn("epoch_length") + DataAvailabilityColumn = postgres.ByteaColumn("data_availability") + ConsensusTypeColumn = postgres.StringColumn("consensus_type") + StateColumn = postgres.StringColumn("state") + ReasonColumn = postgres.StringColumn("reason") + LastEpochCheckBlockColumn = postgres.FloatColumn("last_epoch_check_block") + LastInputCheckBlockColumn = postgres.FloatColumn("last_input_check_block") + LastOutputCheckBlockColumn = postgres.FloatColumn("last_output_check_block") + LastSubmittedClaimCheckBlockColumn = postgres.FloatColumn("last_submitted_claim_check_block") + LastAcceptedClaimCheckBlockColumn = postgres.FloatColumn("last_accepted_claim_check_block") + LastTournamentCheckBlockColumn = postgres.FloatColumn("last_tournament_check_block") + ProcessedInputsColumn = postgres.FloatColumn("processed_inputs") + CreatedAtColumn = postgres.TimestampzColumn("created_at") + UpdatedAtColumn = postgres.TimestampzColumn("updated_at") + allColumns = postgres.ColumnList{IDColumn, NameColumn, IapplicationAddressColumn, IconsensusAddressColumn, IinputboxAddressColumn, IinputboxBlockColumn, TemplateHashColumn, TemplateURIColumn, EpochLengthColumn, DataAvailabilityColumn, ConsensusTypeColumn, StateColumn, ReasonColumn, LastEpochCheckBlockColumn, LastInputCheckBlockColumn, LastOutputCheckBlockColumn, LastSubmittedClaimCheckBlockColumn, LastAcceptedClaimCheckBlockColumn, LastTournamentCheckBlockColumn, ProcessedInputsColumn, CreatedAtColumn, UpdatedAtColumn} + mutableColumns = postgres.ColumnList{NameColumn, IapplicationAddressColumn, IconsensusAddressColumn, IinputboxAddressColumn, IinputboxBlockColumn, TemplateHashColumn, TemplateURIColumn, EpochLengthColumn, DataAvailabilityColumn, ConsensusTypeColumn, StateColumn, ReasonColumn, LastEpochCheckBlockColumn, LastInputCheckBlockColumn, LastOutputCheckBlockColumn, LastSubmittedClaimCheckBlockColumn, LastAcceptedClaimCheckBlockColumn, LastTournamentCheckBlockColumn, ProcessedInputsColumn, CreatedAtColumn, UpdatedAtColumn} + defaultColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn} ) return applicationTable{ Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), //Columns - ID: IDColumn, - Name: NameColumn, - IapplicationAddress: IapplicationAddressColumn, - IconsensusAddress: IconsensusAddressColumn, - IinputboxAddress: IinputboxAddressColumn, - IinputboxBlock: IinputboxBlockColumn, - TemplateHash: TemplateHashColumn, - TemplateURI: TemplateURIColumn, - EpochLength: EpochLengthColumn, - DataAvailability: DataAvailabilityColumn, - ConsensusType: ConsensusTypeColumn, - State: StateColumn, - Reason: ReasonColumn, - LastEpochCheckBlock: LastEpochCheckBlockColumn, - LastInputCheckBlock: LastInputCheckBlockColumn, - LastOutputCheckBlock: LastOutputCheckBlockColumn, - LastTournamentCheckBlock: LastTournamentCheckBlockColumn, - ProcessedInputs: ProcessedInputsColumn, - CreatedAt: CreatedAtColumn, - UpdatedAt: UpdatedAtColumn, + ID: IDColumn, + Name: NameColumn, + IapplicationAddress: IapplicationAddressColumn, + IconsensusAddress: IconsensusAddressColumn, + IinputboxAddress: IinputboxAddressColumn, + IinputboxBlock: IinputboxBlockColumn, + TemplateHash: TemplateHashColumn, + TemplateURI: TemplateURIColumn, + EpochLength: EpochLengthColumn, + DataAvailability: DataAvailabilityColumn, + ConsensusType: ConsensusTypeColumn, + State: StateColumn, + Reason: ReasonColumn, + LastEpochCheckBlock: LastEpochCheckBlockColumn, + LastInputCheckBlock: LastInputCheckBlockColumn, + LastOutputCheckBlock: LastOutputCheckBlockColumn, + LastSubmittedClaimCheckBlock: LastSubmittedClaimCheckBlockColumn, + LastAcceptedClaimCheckBlock: LastAcceptedClaimCheckBlockColumn, + LastTournamentCheckBlock: LastTournamentCheckBlockColumn, + ProcessedInputs: ProcessedInputsColumn, + CreatedAt: CreatedAtColumn, + UpdatedAt: UpdatedAtColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/internal/repository/postgres/db/rollupsdb/public/table/epoch.go b/internal/repository/postgres/db/rollupsdb/public/table/epoch.go index 091e788f6..f76536748 100644 --- a/internal/repository/postgres/db/rollupsdb/public/table/epoch.go +++ b/internal/repository/postgres/db/rollupsdb/public/table/epoch.go @@ -17,23 +17,25 @@ type epochTable struct { postgres.Table // Columns - ApplicationID postgres.ColumnInteger - Index postgres.ColumnFloat - FirstBlock postgres.ColumnFloat - LastBlock postgres.ColumnFloat - InputIndexLowerBound postgres.ColumnFloat - InputIndexUpperBound postgres.ColumnFloat - MachineHash postgres.ColumnBytea - OutputsMerkleRoot postgres.ColumnBytea - OutputsMerkleProof postgres.ColumnByteaArray - Commitment postgres.ColumnBytea - CommitmentProof postgres.ColumnByteaArray - TournamentAddress postgres.ColumnBytea - ClaimTransactionHash postgres.ColumnBytea - Status postgres.ColumnString - VirtualIndex postgres.ColumnFloat - CreatedAt postgres.ColumnTimestampz - UpdatedAt postgres.ColumnTimestampz + ApplicationID postgres.ColumnInteger + Index postgres.ColumnFloat + FirstBlock postgres.ColumnFloat + LastBlock postgres.ColumnFloat + InputIndexLowerBound postgres.ColumnFloat + InputIndexUpperBound postgres.ColumnFloat + MachineHash postgres.ColumnBytea + OutputsMerkleRoot postgres.ColumnBytea + OutputsMerkleProof postgres.ColumnByteaArray + Commitment postgres.ColumnBytea + CommitmentProof postgres.ColumnByteaArray + TournamentAddress postgres.ColumnBytea + ClaimSubmittedTransactionHash postgres.ColumnBytea + ClaimAcceptedTransactionHash postgres.ColumnBytea + ClaimTransactionHash postgres.ColumnBytea + Status postgres.ColumnString + VirtualIndex postgres.ColumnFloat + CreatedAt postgres.ColumnTimestampz + UpdatedAt postgres.ColumnTimestampz AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -75,49 +77,53 @@ func newEpochTable(schemaName, tableName, alias string) *EpochTable { func newEpochTableImpl(schemaName, tableName, alias string) epochTable { var ( - ApplicationIDColumn = postgres.IntegerColumn("application_id") - IndexColumn = postgres.FloatColumn("index") - FirstBlockColumn = postgres.FloatColumn("first_block") - LastBlockColumn = postgres.FloatColumn("last_block") - InputIndexLowerBoundColumn = postgres.FloatColumn("input_index_lower_bound") - InputIndexUpperBoundColumn = postgres.FloatColumn("input_index_upper_bound") - MachineHashColumn = postgres.ByteaColumn("machine_hash") - OutputsMerkleRootColumn = postgres.ByteaColumn("outputs_merkle_root") - OutputsMerkleProofColumn = postgres.ByteaArrayColumn("outputs_merkle_proof") - CommitmentColumn = postgres.ByteaColumn("commitment") - CommitmentProofColumn = postgres.ByteaArrayColumn("commitment_proof") - TournamentAddressColumn = postgres.ByteaColumn("tournament_address") - ClaimTransactionHashColumn = postgres.ByteaColumn("claim_transaction_hash") - StatusColumn = postgres.StringColumn("status") - VirtualIndexColumn = postgres.FloatColumn("virtual_index") - CreatedAtColumn = postgres.TimestampzColumn("created_at") - UpdatedAtColumn = postgres.TimestampzColumn("updated_at") - allColumns = postgres.ColumnList{ApplicationIDColumn, IndexColumn, FirstBlockColumn, LastBlockColumn, InputIndexLowerBoundColumn, InputIndexUpperBoundColumn, MachineHashColumn, OutputsMerkleRootColumn, OutputsMerkleProofColumn, CommitmentColumn, CommitmentProofColumn, TournamentAddressColumn, ClaimTransactionHashColumn, StatusColumn, VirtualIndexColumn, CreatedAtColumn, UpdatedAtColumn} - mutableColumns = postgres.ColumnList{FirstBlockColumn, LastBlockColumn, InputIndexLowerBoundColumn, InputIndexUpperBoundColumn, MachineHashColumn, OutputsMerkleRootColumn, OutputsMerkleProofColumn, CommitmentColumn, CommitmentProofColumn, TournamentAddressColumn, ClaimTransactionHashColumn, StatusColumn, VirtualIndexColumn, CreatedAtColumn, UpdatedAtColumn} - defaultColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn} + ApplicationIDColumn = postgres.IntegerColumn("application_id") + IndexColumn = postgres.FloatColumn("index") + FirstBlockColumn = postgres.FloatColumn("first_block") + LastBlockColumn = postgres.FloatColumn("last_block") + InputIndexLowerBoundColumn = postgres.FloatColumn("input_index_lower_bound") + InputIndexUpperBoundColumn = postgres.FloatColumn("input_index_upper_bound") + MachineHashColumn = postgres.ByteaColumn("machine_hash") + OutputsMerkleRootColumn = postgres.ByteaColumn("outputs_merkle_root") + OutputsMerkleProofColumn = postgres.ByteaArrayColumn("outputs_merkle_proof") + CommitmentColumn = postgres.ByteaColumn("commitment") + CommitmentProofColumn = postgres.ByteaArrayColumn("commitment_proof") + TournamentAddressColumn = postgres.ByteaColumn("tournament_address") + ClaimSubmittedTransactionHashColumn = postgres.ByteaColumn("claim_submitted_transaction_hash") + ClaimAcceptedTransactionHashColumn = postgres.ByteaColumn("claim_accepted_transaction_hash") + ClaimTransactionHashColumn = postgres.ByteaColumn("claim_transaction_hash") + StatusColumn = postgres.StringColumn("status") + VirtualIndexColumn = postgres.FloatColumn("virtual_index") + CreatedAtColumn = postgres.TimestampzColumn("created_at") + UpdatedAtColumn = postgres.TimestampzColumn("updated_at") + allColumns = postgres.ColumnList{ApplicationIDColumn, IndexColumn, FirstBlockColumn, LastBlockColumn, InputIndexLowerBoundColumn, InputIndexUpperBoundColumn, MachineHashColumn, OutputsMerkleRootColumn, OutputsMerkleProofColumn, CommitmentColumn, CommitmentProofColumn, TournamentAddressColumn, ClaimSubmittedTransactionHashColumn, ClaimAcceptedTransactionHashColumn, ClaimTransactionHashColumn, StatusColumn, VirtualIndexColumn, CreatedAtColumn, UpdatedAtColumn} + mutableColumns = postgres.ColumnList{FirstBlockColumn, LastBlockColumn, InputIndexLowerBoundColumn, InputIndexUpperBoundColumn, MachineHashColumn, OutputsMerkleRootColumn, OutputsMerkleProofColumn, CommitmentColumn, CommitmentProofColumn, TournamentAddressColumn, ClaimSubmittedTransactionHashColumn, ClaimAcceptedTransactionHashColumn, ClaimTransactionHashColumn, StatusColumn, VirtualIndexColumn, CreatedAtColumn, UpdatedAtColumn} + defaultColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn} ) return epochTable{ Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), //Columns - ApplicationID: ApplicationIDColumn, - Index: IndexColumn, - FirstBlock: FirstBlockColumn, - LastBlock: LastBlockColumn, - InputIndexLowerBound: InputIndexLowerBoundColumn, - InputIndexUpperBound: InputIndexUpperBoundColumn, - MachineHash: MachineHashColumn, - OutputsMerkleRoot: OutputsMerkleRootColumn, - OutputsMerkleProof: OutputsMerkleProofColumn, - Commitment: CommitmentColumn, - CommitmentProof: CommitmentProofColumn, - TournamentAddress: TournamentAddressColumn, - ClaimTransactionHash: ClaimTransactionHashColumn, - Status: StatusColumn, - VirtualIndex: VirtualIndexColumn, - CreatedAt: CreatedAtColumn, - UpdatedAt: UpdatedAtColumn, + ApplicationID: ApplicationIDColumn, + Index: IndexColumn, + FirstBlock: FirstBlockColumn, + LastBlock: LastBlockColumn, + InputIndexLowerBound: InputIndexLowerBoundColumn, + InputIndexUpperBound: InputIndexUpperBoundColumn, + MachineHash: MachineHashColumn, + OutputsMerkleRoot: OutputsMerkleRootColumn, + OutputsMerkleProof: OutputsMerkleProofColumn, + Commitment: CommitmentColumn, + CommitmentProof: CommitmentProofColumn, + TournamentAddress: TournamentAddressColumn, + ClaimSubmittedTransactionHash: ClaimSubmittedTransactionHashColumn, + ClaimAcceptedTransactionHash: ClaimAcceptedTransactionHashColumn, + ClaimTransactionHash: ClaimTransactionHashColumn, + Status: StatusColumn, + VirtualIndex: VirtualIndexColumn, + CreatedAt: CreatedAtColumn, + UpdatedAt: UpdatedAtColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/internal/repository/postgres/epoch.go b/internal/repository/postgres/epoch.go index 8098c3455..21443aac7 100644 --- a/internal/repository/postgres/epoch.go +++ b/internal/repository/postgres/epoch.go @@ -507,6 +507,76 @@ func (r *PostgresRepository) UpdateEpochClaimTransactionHash( return nil } +func (r *PostgresRepository) UpdateEpochClaimSubmittedTransactionHash( + ctx context.Context, + nameOrAddress string, + e *model.Epoch, +) error { + + whereClause := getWhereClauseFromNameOrAddress(nameOrAddress) + + updStmt := table.Epoch. + UPDATE( + table.Epoch.ClaimSubmittedTransactionHash, + ). + SET( + e.ClaimSubmittedTransactionHash, + ). + FROM( + table.Application, + ). + WHERE( + whereClause. + AND(table.Epoch.ApplicationID.EQ(table.Application.ID)). + AND(table.Epoch.Index.EQ(uint64Expr(e.Index))), + ) + + sqlStr, args := updStmt.Sql() + cmd, err := r.db.Exec(ctx, sqlStr, args...) + if err != nil { + return err + } + if cmd.RowsAffected() == 0 { + return repository.ErrNotFound + } + return nil +} + +func (r *PostgresRepository) UpdateEpochClaimAcceptedTransactionHash( + ctx context.Context, + nameOrAddress string, + e *model.Epoch, +) error { + + whereClause := getWhereClauseFromNameOrAddress(nameOrAddress) + + updStmt := table.Epoch. + UPDATE( + table.Epoch.ClaimAcceptedTransactionHash, + ). + SET( + e.ClaimAcceptedTransactionHash, + ). + FROM( + table.Application, + ). + WHERE( + whereClause. + AND(table.Epoch.ApplicationID.EQ(table.Application.ID)). + AND(table.Epoch.Index.EQ(uint64Expr(e.Index))), + ) + + sqlStr, args := updStmt.Sql() + cmd, err := r.db.Exec(ctx, sqlStr, args...) + if err != nil { + return err + } + if cmd.RowsAffected() == 0 { + return repository.ErrNotFound + } + return nil +} + func (r *PostgresRepository) UpdateEpochOutputsProof(ctx context.Context, appID int64, epochIndex uint64, proof *model.OutputsProof) error { tx, err := r.db.Begin(ctx) if err != nil { @@ -657,6 +727,8 @@ func (r *PostgresRepository) ListEpochs( table.Epoch.OutputsMerkleRoot, table.Epoch.Commitment, table.Epoch.ClaimTransactionHash, + table.Epoch.ClaimSubmittedTransactionHash, + table.Epoch.ClaimAcceptedTransactionHash, table.Epoch.TournamentAddress, table.Epoch.Status, table.Epoch.VirtualIndex, @@ -722,6 +794,8 @@ func (r *PostgresRepository) ListEpochs( &ep.OutputsMerkleRoot, &ep.Commitment, &ep.ClaimTransactionHash, + &ep.ClaimSubmittedTransactionHash, + &ep.ClaimAcceptedTransactionHash, &ep.TournamentAddress, &ep.Status, &ep.VirtualIndex, diff --git a/internal/repository/postgres/schema/migrations/000001_create_initial_schema.up.sql b/internal/repository/postgres/schema/migrations/000001_create_initial_schema.up.sql index 6956e6354..e20e6abf9 100644 --- a/internal/repository/postgres/schema/migrations/000001_create_initial_schema.up.sql +++ b/internal/repository/postgres/schema/migrations/000001_create_initial_schema.up.sql @@ -87,8 +87,10 @@ CREATE TABLE "application" "last_epoch_check_block" uint64 NOT NULL, "last_input_check_block" uint64 NOT NULL, "last_output_check_block" uint64 NOT NULL, + "last_submitted_claim_check_block" uint64 NOT NULL, + "last_accepted_claim_check_block" uint64 NOT NULL, "last_tournament_check_block" uint64 NOT NULL, - "processed_inputs" uint64 NOT NULL, + "processed_inputs" uint64 NOT NULL DEFAULT 0, "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT "reason_required_for_failure_states" CHECK (NOT ("state" IN ('FAILED', 'INOPERABLE') AND ("reason" IS NULL OR LENGTH("reason") = 0))), @@ -164,6 +166,8 @@ CREATE TABLE "epoch" "commitment" hash, "commitment_proof" BYTEA[], "tournament_address" ethereum_address, + "claim_submitted_transaction_hash" hash, + "claim_accepted_transaction_hash" hash, "claim_transaction_hash" hash, "status" "EpochStatus" NOT NULL, "virtual_index" uint64 NOT NULL, diff --git a/internal/repository/repository.go b/internal/repository/repository.go index e479f8818..4d98bd5fc 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -109,6 +109,8 @@ type EpochRepository interface { GetEpochByVirtualIndex(ctx context.Context, nameOrAddress string, index uint64) (*Epoch, error) UpdateEpochClaimTransactionHash(ctx context.Context, nameOrAddress string, e *Epoch) error + UpdateEpochClaimSubmittedTransactionHash(ctx context.Context, nameOrAddress string, e *Epoch) error + UpdateEpochClaimAcceptedTransactionHash(ctx context.Context, nameOrAddress string, e *Epoch) error UpdateEpochStatus(ctx context.Context, nameOrAddress string, e *Epoch) error UpdateEpochInputsProcessed(ctx context.Context, nameOrAddress string, epochIndex uint64) error UpdateEpochOutputsProof(ctx context.Context, appID int64, epochIndex uint64, proof *OutputsProof) error @@ -183,33 +185,6 @@ type NodeConfigRepository interface { LoadNodeConfigRaw(ctx context.Context, key string) (rawJSON []byte, createdAt, updatedAt time.Time, err error) } -// TODO: migrate ClaimRow -> Application + Epoch and use the other interfaces -type ClaimerRepository interface { - SelectSubmittedClaimPairsPerApp(ctx context.Context) ( - map[int64]*Epoch, - map[int64]*Epoch, - map[int64]*Application, - error, - ) - SelectAcceptedClaimPairsPerApp(ctx context.Context) ( - map[int64]*Epoch, - map[int64]*Epoch, - map[int64]*Application, - error, - ) - UpdateEpochWithSubmittedClaim( - ctx context.Context, - applicationID int64, - index uint64, - transactionHash common.Hash, - ) error - UpdateEpochWithAcceptedClaim( - ctx context.Context, - applicationID int64, - index uint64, - ) error -} - type Repository interface { ApplicationRepository EpochRepository @@ -223,7 +198,6 @@ type Repository interface { MatchAdvancedRepository BulkOperationsRepository NodeConfigRepository - ClaimerRepository Close() } diff --git a/internal/repository/repotest/claimer_test_cases.go b/internal/repository/repotest/claimer_test_cases.go index cb108a639..6cdc22b9b 100644 --- a/internal/repository/repotest/claimer_test_cases.go +++ b/internal/repository/repotest/claimer_test_cases.go @@ -3,13 +3,8 @@ package repotest -import ( - "context" - - . "github.com/cartesi/rollups-node/internal/model" - "github.com/ethereum/go-ethereum/common" -) - +// ClaimerSuite contains repository test cases for claimer-related queries. +// TODO: add test cases. type ClaimerSuite struct { BaseSuite } @@ -17,459 +12,3 @@ type ClaimerSuite struct { func NewClaimerSuite(factory RepositoryFactory) *ClaimerSuite { return &ClaimerSuite{BaseSuite: BaseSuite{factory: factory}} } - -// createAppWithClaimComputedEpoch creates an app with one epoch at ClaimComputed status. -func (s *ClaimerSuite) createAppWithClaimComputedEpoch() *Application { - s.T().Helper() - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimComputed) - - return app -} - -func (s *ClaimerSuite) TestSelectSubmittedClaimPairsPerApp() { - s.Run("EmptyWhenNoClaimComputed", func() { - NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - // Returns: (acceptedOrSubmitted, computed, applications, error) - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(computed) - s.Empty(apps) - }) - - s.Run("ReturnsPairWhenClaimComputed", func() { - app := s.createAppWithClaimComputedEpoch() - - // Returns: (acceptedOrSubmitted, computed, applications, error) - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.NotEmpty(computed) - s.NotEmpty(apps) - s.Contains(computed, app.ID) - s.Contains(apps, app.ID) - }) - - s.Run("MultipleAppsReturnsSeparateEntries", func() { - app1 := s.createAppWithClaimComputedEpoch() - app2 := s.createAppWithClaimComputedEpoch() - - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Len(computed, 2) - s.Len(apps, 2) - s.Contains(computed, app1.ID) - s.Contains(computed, app2.ID) - s.Contains(apps, app1.ID) - s.Contains(apps, app2.ID) - }) - - s.Run("IncludesAcceptedOrSubmittedForMultipleApps", func() { - // Create two apps, each with a submitted epoch. - // SelectSubmittedClaimPairsPerApp returns acceptedOrSubmitted - // via selectNewestAcceptedClaimPerApp(includeSubmitted=true). - app1 := s.createAppWithClaimComputedEpoch() - app2 := s.createAppWithClaimComputedEpoch() - - // Move both to ClaimSubmitted - txHash1 := UniqueHash() - err := s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app1.ID, 0, txHash1) - s.Require().NoError(err) - - txHash2 := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app2.ID, 0, txHash2) - s.Require().NoError(err) - - acceptedOrSubmitted, _, _, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Len(acceptedOrSubmitted, 2) - s.Contains(acceptedOrSubmitted, app1.ID) - s.Contains(acceptedOrSubmitted, app2.ID) - }) - - // Regression guard: verify map keys are actual application IDs - // and that each epoch is stored under the correct key. - s.Run("MultiAppMapKeysMatchEpochApplicationIDs", func() { - app1 := s.createAppWithClaimComputedEpoch() - app2 := s.createAppWithClaimComputedEpoch() - - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - - for appID, epoch := range computed { - s.NotEqual(int64(0), appID, "map key must not be zero") - s.Equal(appID, epoch.ApplicationID, - "epoch stored under wrong key") - } - for appID, app := range apps { - s.NotEqual(int64(0), appID, "map key must not be zero") - s.Equal(appID, app.ID, - "application stored under wrong key") - } - - // Verify specific app data integrity - s.Equal(app1.IApplicationAddress, apps[app1.ID].IApplicationAddress) - s.Equal(app2.IApplicationAddress, apps[app2.ID].IApplicationAddress) - }) - - s.Run("ExcludesPRTApps", func() { - app := NewApplicationBuilder(). - WithConsensus(Consensus_PRT). - Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimComputed) - - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(computed) - s.Empty(apps) - }) - - s.Run("ExcludesDisabledApps", func() { - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimComputed) - - reason := "test disabled" - err = s.Repo.UpdateApplicationState( - s.Ctx, app.ID, ApplicationState_Disabled, &reason) - s.Require().NoError(err) - - _, computed, apps, err := s.Repo.SelectSubmittedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(computed) - s.Empty(apps) - }) - - s.Run("ContextCancellation", func() { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - _, _, _, err := s.Repo.SelectSubmittedClaimPairsPerApp(ctx) - s.Require().Error(err) - }) -} - -func (s *ClaimerSuite) TestSelectAcceptedClaimPairsPerApp() { - s.Run("EmptyWhenNoClaimSubmitted", func() { - NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - accepted, submitted, apps, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(accepted) - s.Empty(submitted) - s.Empty(apps) - }) - - s.Run("ReturnsPairWhenClaimAccepted", func() { - app := s.createAppWithClaimComputedEpoch() - - // Move to ClaimSubmitted - txHash := UniqueHash() - err := s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - - // Move to ClaimAccepted - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - - accepted, _, _, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Len(accepted, 1) - s.Contains(accepted, app.ID) - }) - - s.Run("MultipleAppsReturnsSeparateEntries", func() { - app1 := s.createAppWithClaimComputedEpoch() - app2 := s.createAppWithClaimComputedEpoch() - - // Move both through submitted -> accepted - for _, app := range []*Application{app1, app2} { - txHash := UniqueHash() - err := s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - } - - accepted, _, _, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Len(accepted, 2) - s.Contains(accepted, app1.ID) - s.Contains(accepted, app2.ID) - }) - - // Regression guard: verify accepted map keys match the actual - // epoch.ApplicationID, not a zero-value from an unscanned field. - s.Run("MultiAppMapKeysMatchEpochApplicationIDs", func() { - app1 := s.createAppWithClaimComputedEpoch() - app2 := s.createAppWithClaimComputedEpoch() - - for _, app := range []*Application{app1, app2} { - txHash := UniqueHash() - err := s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - } - - accepted, submitted, apps, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - - for appID, epoch := range accepted { - s.NotEqual(int64(0), appID, "accepted map key must not be zero") - s.Equal(appID, epoch.ApplicationID, - "accepted epoch stored under wrong key") - } - for appID, epoch := range submitted { - s.NotEqual(int64(0), appID, "submitted map key must not be zero") - s.Equal(appID, epoch.ApplicationID, - "submitted epoch stored under wrong key") - } - for appID, app := range apps { - s.NotEqual(int64(0), appID, "apps map key must not be zero") - s.Equal(appID, app.ID, - "application stored under wrong key") - } - - s.Contains(accepted, app1.ID) - s.Contains(accepted, app2.ID) - }) - - s.Run("ExcludesPRTApps", func() { - app := NewApplicationBuilder(). - WithConsensus(Consensus_PRT). - Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimComputed) - - txHash := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - - accepted, submitted, apps, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(accepted) - s.Empty(submitted) - s.Empty(apps) - }) - - s.Run("ExcludesDisabledApps", func() { - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimComputed) - - txHash := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - - reason := "test disabled" - err = s.Repo.UpdateApplicationState( - s.Ctx, app.ID, ApplicationState_Disabled, &reason) - s.Require().NoError(err) - - accepted, submitted, apps, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - s.Empty(accepted) - s.Empty(submitted) - s.Empty(apps) - }) - - s.Run("ReturnsSubmittedMapWithBothEpochStates", func() { - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - // Create two epochs with inputs - epoch0 := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input0 := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - epoch1 := NewEpochBuilder(app.ID). - WithIndex(1).WithStatus(EpochStatus_Closed). - WithBlocks(10, 19).WithInputBounds(1, 1).Build() - input1 := NewInputBuilder().WithIndex(1).WithBlockNumber(15).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch0: {input0}, epoch1: {input1}}, 20) - s.Require().NoError(err) - - // Move epoch 0 to ClaimAccepted - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch0, EpochStatus_ClaimComputed) - - txHash0 := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash0) - s.Require().NoError(err) - - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - - // Move epoch 1 to ClaimSubmitted - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch1, EpochStatus_ClaimComputed) - - txHash1 := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 1, txHash1) - s.Require().NoError(err) - - accepted, submitted, apps, err := s.Repo.SelectAcceptedClaimPairsPerApp(s.Ctx) - s.Require().NoError(err) - - // accepted contains epoch 0 (newest accepted) - s.Len(accepted, 1) - s.Contains(accepted, app.ID) - s.Equal(uint64(0), accepted[app.ID].Index) - s.Equal(EpochStatus_ClaimAccepted, accepted[app.ID].Status) - - // submitted contains epoch 1 (oldest submitted) - s.Len(submitted, 1) - s.Contains(submitted, app.ID) - s.Equal(uint64(1), submitted[app.ID].Index) - s.Equal(EpochStatus_ClaimSubmitted, submitted[app.ID].Status) - - // apps contains the application - s.Len(apps, 1) - s.Contains(apps, app.ID) - s.Equal(app.IApplicationAddress, apps[app.ID].IApplicationAddress) - }) - - s.Run("ContextCancellation", func() { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - _, _, _, err := s.Repo.SelectAcceptedClaimPairsPerApp(ctx) - s.Require().Error(err) - }) -} - -func (s *ClaimerSuite) TestUpdateEpochWithSubmittedClaim() { - s.Run("SetsClaimSubmitted", func() { - app := s.createAppWithClaimComputedEpoch() - - txHash := common.HexToHash("0xdeadbeef") - err := s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().NoError(err) - - got, err := s.Repo.GetEpoch(s.Ctx, app.IApplicationAddress.String(), 0) - s.Require().NoError(err) - s.Equal(EpochStatus_ClaimSubmitted, got.Status) - s.Require().NotNil(got.ClaimTransactionHash) - s.Equal(txHash, *got.ClaimTransactionHash) - }) - - s.Run("ErrorWhenEpochNotClaimComputed", func() { - // Create an app with an epoch still in Closed status (not ClaimComputed) - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - txHash := UniqueHash() - err = s.Repo.UpdateEpochWithSubmittedClaim(s.Ctx, app.ID, 0, txHash) - s.Require().Error(err) - }) -} - -func (s *ClaimerSuite) TestUpdateEpochWithAcceptedClaim() { - s.Run("SetsClaimAccepted", func() { - app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) - - epoch := NewEpochBuilder(app.ID). - WithIndex(0).WithStatus(EpochStatus_Closed). - WithBlocks(0, 9).WithInputBounds(0, 0).Build() - input := NewInputBuilder().WithIndex(0).WithBlockNumber(5).Build() - - err := s.Repo.CreateEpochsAndInputs( - s.Ctx, app.IApplicationAddress.String(), - map[*Epoch][]*Input{epoch: {input}}, 10) - s.Require().NoError(err) - - AdvanceEpochStatus(s.Ctx, s.T(), s.Repo, - app.IApplicationAddress.String(), epoch, EpochStatus_ClaimSubmitted) - - err = s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().NoError(err) - - got, err := s.Repo.GetEpoch(s.Ctx, app.IApplicationAddress.String(), 0) - s.Require().NoError(err) - s.Equal(EpochStatus_ClaimAccepted, got.Status) - }) - - s.Run("ErrorWhenEpochNotClaimSubmitted", func() { - // Create an app with an epoch in ClaimComputed status (not ClaimSubmitted) - app := s.createAppWithClaimComputedEpoch() - - err := s.Repo.UpdateEpochWithAcceptedClaim(s.Ctx, app.ID, 0) - s.Require().Error(err) - }) -} diff --git a/pkg/contracts/iconsensus/iconsensus.go b/pkg/contracts/iconsensus/iconsensus.go index d0071cdd2..5f38ad9f0 100644 --- a/pkg/contracts/iconsensus/iconsensus.go +++ b/pkg/contracts/iconsensus/iconsensus.go @@ -31,7 +31,7 @@ var ( // IConsensusMetaData contains all meta data concerning the IConsensus contract. var IConsensusMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"getEpochLength\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNumberOfAcceptedClaims\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNumberOfSubmittedClaims\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isOutputsMerkleRootValid\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"submitClaim\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"supportsInterface\",\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"ClaimAccepted\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ClaimSubmitted\",\"inputs\":[{\"name\":\"submitter\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"appContract\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"NotEpochFinalBlock\",\"inputs\":[{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"epochLength\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"NotFirstClaim\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"NotPastBlock\",\"inputs\":[{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"currentBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]", + ABI: "[{\"type\":\"function\",\"name\":\"getEpochLength\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNumberOfAcceptedClaims\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNumberOfSubmittedClaims\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isOutputsMerkleRootValid\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"submitClaim\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"supportsInterface\",\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"ClaimAccepted\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ClaimSubmitted\",\"inputs\":[{\"name\":\"submitter\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"appContract\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"outputsMerkleRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"NotEpochFinalBlock\",\"inputs\":[{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"epochLength\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"NotFirstClaim\",\"inputs\":[{\"name\":\"appContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"NotPastBlock\",\"inputs\":[{\"name\":\"lastProcessedBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"currentBlockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]", } // IConsensusABI is the input ABI used to generate the binding from. @@ -211,12 +211,12 @@ func (_IConsensus *IConsensusCallerSession) GetEpochLength() (*big.Int, error) { return _IConsensus.Contract.GetEpochLength(&_IConsensus.CallOpts) } -// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0xd574f4d7. +// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0x80a80953. // -// Solidity: function getNumberOfAcceptedClaims() view returns(uint256) -func (_IConsensus *IConsensusCaller) GetNumberOfAcceptedClaims(opts *bind.CallOpts) (*big.Int, error) { +// Solidity: function getNumberOfAcceptedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusCaller) GetNumberOfAcceptedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { var out []interface{} - err := _IConsensus.contract.Call(opts, &out, "getNumberOfAcceptedClaims") + err := _IConsensus.contract.Call(opts, &out, "getNumberOfAcceptedClaims", appContract) if err != nil { return *new(*big.Int), err @@ -228,26 +228,26 @@ func (_IConsensus *IConsensusCaller) GetNumberOfAcceptedClaims(opts *bind.CallOp } -// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0xd574f4d7. +// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0x80a80953. // -// Solidity: function getNumberOfAcceptedClaims() view returns(uint256) -func (_IConsensus *IConsensusSession) GetNumberOfAcceptedClaims() (*big.Int, error) { - return _IConsensus.Contract.GetNumberOfAcceptedClaims(&_IConsensus.CallOpts) +// Solidity: function getNumberOfAcceptedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusSession) GetNumberOfAcceptedClaims(appContract common.Address) (*big.Int, error) { + return _IConsensus.Contract.GetNumberOfAcceptedClaims(&_IConsensus.CallOpts, appContract) } -// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0xd574f4d7. +// GetNumberOfAcceptedClaims is a free data retrieval call binding the contract method 0x80a80953. // -// Solidity: function getNumberOfAcceptedClaims() view returns(uint256) -func (_IConsensus *IConsensusCallerSession) GetNumberOfAcceptedClaims() (*big.Int, error) { - return _IConsensus.Contract.GetNumberOfAcceptedClaims(&_IConsensus.CallOpts) +// Solidity: function getNumberOfAcceptedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusCallerSession) GetNumberOfAcceptedClaims(appContract common.Address) (*big.Int, error) { + return _IConsensus.Contract.GetNumberOfAcceptedClaims(&_IConsensus.CallOpts, appContract) } -// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0xee5e0faa. +// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0x43aacc77. // -// Solidity: function getNumberOfSubmittedClaims() view returns(uint256) -func (_IConsensus *IConsensusCaller) GetNumberOfSubmittedClaims(opts *bind.CallOpts) (*big.Int, error) { +// Solidity: function getNumberOfSubmittedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusCaller) GetNumberOfSubmittedClaims(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) { var out []interface{} - err := _IConsensus.contract.Call(opts, &out, "getNumberOfSubmittedClaims") + err := _IConsensus.contract.Call(opts, &out, "getNumberOfSubmittedClaims", appContract) if err != nil { return *new(*big.Int), err @@ -259,18 +259,18 @@ func (_IConsensus *IConsensusCaller) GetNumberOfSubmittedClaims(opts *bind.CallO } -// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0xee5e0faa. +// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0x43aacc77. // -// Solidity: function getNumberOfSubmittedClaims() view returns(uint256) -func (_IConsensus *IConsensusSession) GetNumberOfSubmittedClaims() (*big.Int, error) { - return _IConsensus.Contract.GetNumberOfSubmittedClaims(&_IConsensus.CallOpts) +// Solidity: function getNumberOfSubmittedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusSession) GetNumberOfSubmittedClaims(appContract common.Address) (*big.Int, error) { + return _IConsensus.Contract.GetNumberOfSubmittedClaims(&_IConsensus.CallOpts, appContract) } -// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0xee5e0faa. +// GetNumberOfSubmittedClaims is a free data retrieval call binding the contract method 0x43aacc77. // -// Solidity: function getNumberOfSubmittedClaims() view returns(uint256) -func (_IConsensus *IConsensusCallerSession) GetNumberOfSubmittedClaims() (*big.Int, error) { - return _IConsensus.Contract.GetNumberOfSubmittedClaims(&_IConsensus.CallOpts) +// Solidity: function getNumberOfSubmittedClaims(address appContract) view returns(uint256) +func (_IConsensus *IConsensusCallerSession) GetNumberOfSubmittedClaims(appContract common.Address) (*big.Int, error) { + return _IConsensus.Contract.GetNumberOfSubmittedClaims(&_IConsensus.CallOpts, appContract) } // IsOutputsMerkleRootValid is a free data retrieval call binding the contract method 0xe5cc8664.