diff --git a/database/pchain_entities.go b/database/pchain_entities.go index 88683e3..2a09cf2 100644 --- a/database/pchain_entities.go +++ b/database/pchain_entities.go @@ -25,6 +25,7 @@ type PChainTx struct { Bytes []byte `gorm:"type:mediumblob"` FeePercentage uint32 // Fee percentage (in case of add validator transaction) ChainTime *time.Time // Chain time, time of the previous advance time transaction + BlockTime *time.Time `gorm:"index"` // Block time, non-null from Banff block activation on (Avalanche 1.9.0) } type PChainTxInput struct { diff --git a/database/types.go b/database/types.go index bc385a3..49acf51 100644 --- a/database/types.go +++ b/database/types.go @@ -14,16 +14,20 @@ const ( type PChainTxType string const ( - PChainRewardValidatorTx PChainTxType = "REWARD_TX" - PChainAddDelegatorTx PChainTxType = "ADD_DELEGATOR_TX" - PChainAddValidatorTx PChainTxType = "ADD_VALIDATOR_TX" - PChainImportTx PChainTxType = "IMPORT_TX" - PChainExportTx PChainTxType = "EXPORT_TX" - PChainAdvanceTimeTx PChainTxType = "ADVANCE_TIME_TX" - PChainCreateChainTx PChainTxType = "CREATE_CHAIN_TX" - PChainCreateSubnetTx PChainTxType = "CREATE_SUBNET_TX" - PChainAddSubnetValidatorTx PChainTxType = "ADD_SUBNET_VALIDATOR_TX" - PChainUnknownTx PChainTxType = "UNKNOWN_TX" + PChainRewardValidatorTx PChainTxType = "REWARD_TX" + PChainAddDelegatorTx PChainTxType = "ADD_DELEGATOR_TX" + PChainAddValidatorTx PChainTxType = "ADD_VALIDATOR_TX" + PChainImportTx PChainTxType = "IMPORT_TX" + PChainExportTx PChainTxType = "EXPORT_TX" + PChainAdvanceTimeTx PChainTxType = "ADVANCE_TIME_TX" + PChainCreateChainTx PChainTxType = "CREATE_CHAIN_TX" + PChainCreateSubnetTx PChainTxType = "CREATE_SUBNET_TX" + PChainAddSubnetValidatorTx PChainTxType = "ADD_SUBNET_VALIDATOR_TX" + PChainRemoveSubnetValidatorTx PChainTxType = "REMOVE_SUBNET_VALIDATOR_TX" + PChainTransformSubnetTx PChainTxType = "TRANSFORM_SUBNET_TX" + PChainAddPermissionlessValidatorTx PChainTxType = "ADD_PERMISSIONLESS_VALIDATOR_TX" + PChainAddPermissionlessDelegatorTx PChainTxType = "ADD_PERMISSIONLESS_DELEGATOR_TX" + PChainUnknownTx PChainTxType = "UNKNOWN_TX" ) type PChainBlockType string diff --git a/go.mod b/go.mod index f23ff3e..57157ac 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module flare-indexer -go 1.22 +go 1.21 require ( github.com/BurntSushi/toml v1.2.1 diff --git a/indexer/context/context.go b/indexer/context/context.go index 52cccc1..21d5fea 100644 --- a/indexer/context/context.go +++ b/indexer/context/context.go @@ -16,6 +16,8 @@ type IndexerContext interface { } type IndexerFlags struct { + Version bool + ConfigFileName string // Set start epoch for voting cronjob to this value, overrides config and database value, @@ -33,8 +35,7 @@ type indexerContext struct { flags *IndexerFlags } -func BuildContext() (IndexerContext, error) { - flags := parseIndexerFlags() +func BuildContext(flags *IndexerFlags) (IndexerContext, error) { cfg, err := config.BuildConfig(flags.ConfigFileName) if err != nil { return nil, err @@ -59,13 +60,15 @@ func (c *indexerContext) DB() *gorm.DB { return c.db } func (c *indexerContext) Flags() *IndexerFlags { return c.flags } -func parseIndexerFlags() *IndexerFlags { +func ParseIndexerFlags() *IndexerFlags { cfgFlag := flag.String("config", globalConfig.CONFIG_FILE, "Configuration file (toml format)") + versionFlag := flag.Bool("version", false, "Print version information and exit") resetVotingFlag := flag.Int64("reset-voting", 0, "Set start epoch for voting cronjob to this value, overrides config and database value, valid values are > 0") resetMirrorFlag := flag.Int64("reset-mirroring", 0, "Set start epoch for mirroring cronjob to this value, overrides config and database value, valid values are > 0") flag.Parse() return &IndexerFlags{ + Version: *versionFlag, ConfigFileName: *cfgFlag, ResetVotingCronjob: *resetVotingFlag, ResetMirrorCronjob: *resetMirrorFlag, diff --git a/indexer/main/indexer.go b/indexer/main/indexer.go index 59f83af..d47c342 100644 --- a/indexer/main/indexer.go +++ b/indexer/main/indexer.go @@ -17,7 +17,14 @@ import ( ) func main() { - ctx, err := context.BuildContext() + flags := context.ParseIndexerFlags() + + if flags.Version { + fmt.Printf("Flare P-chain indexer version %s\n", shared.ApplicationVersion) + return + } + + ctx, err := context.BuildContext(flags) if err != nil { fmt.Printf("%v\n", err) return diff --git a/indexer/pchain/batch_indexer.go b/indexer/pchain/batch_indexer.go index a4d44a5..11079d6 100644 --- a/indexer/pchain/batch_indexer.go +++ b/indexer/pchain/batch_indexer.go @@ -15,7 +15,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/proposervm/block" mapset "github.com/deckarep/golang-set/v2" "gorm.io/gorm" ) @@ -91,11 +90,7 @@ func (xi *txBatchIndexer) Reset(containerLen int) (err error) { } func (xi *txBatchIndexer) AddContainer(index uint64, container indexer.Container) error { - blk, err := block.Parse(container.Bytes) - if err != nil { - return err - } - innerBlk, err := blocks.Parse(blocks.GenesisCodec, blk.Block()) + innerBlk, err := chain.ParsePChainBlock(container.Bytes) if err != nil { return err } @@ -103,14 +98,29 @@ func (xi *txBatchIndexer) AddContainer(index uint64, container indexer.Container switch innerBlkType := innerBlk.(type) { case *blocks.ApricotProposalBlock: tx := innerBlkType.Tx - err = xi.addTx(&container, database.PChainProposalBlock, innerBlk.Height(), tx) + err = xi.addTx(&container, database.PChainProposalBlock, innerBlk.Height(), 0, tx) case *blocks.ApricotCommitBlock: - xi.addEmptyTx(&container, database.PChainCommitBlock, innerBlk.Height()) + xi.addEmptyTx(&container, database.PChainCommitBlock, innerBlk.Height(), 0) case *blocks.ApricotAbortBlock: - xi.addEmptyTx(&container, database.PChainAbortBlock, innerBlk.Height()) + xi.addEmptyTx(&container, database.PChainAbortBlock, innerBlk.Height(), 0) case *blocks.ApricotStandardBlock: for _, tx := range innerBlkType.Txs() { - err = xi.addTx(&container, database.PChainStandardBlock, innerBlk.Height(), tx) + err = xi.addTx(&container, database.PChainStandardBlock, innerBlk.Height(), 0, tx) + if err != nil { + break + } + } + // Banff blocks were introduced in Avalanche 1.9.0 + case *blocks.BanffProposalBlock: + tx := innerBlkType.Tx + err = xi.addTx(&container, database.PChainProposalBlock, innerBlk.Height(), innerBlkType.Time, tx) + case *blocks.BanffCommitBlock: + xi.addEmptyTx(&container, database.PChainCommitBlock, innerBlk.Height(), innerBlkType.Time) + case *blocks.BanffAbortBlock: + xi.addEmptyTx(&container, database.PChainAbortBlock, innerBlk.Height(), innerBlkType.Time) + case *blocks.BanffStandardBlock: + for _, tx := range innerBlkType.Txs() { + err = xi.addTx(&container, database.PChainStandardBlock, innerBlk.Height(), innerBlkType.Time, tx) if err != nil { break } @@ -125,7 +135,7 @@ func (xi *txBatchIndexer) ProcessBatch() error { return xi.inOutIndexer.ProcessBatch() } -func (xi *txBatchIndexer) addTx(container *indexer.Container, blockType database.PChainBlockType, height uint64, tx *txs.Tx) error { +func (xi *txBatchIndexer) addTx(container *indexer.Container, blockType database.PChainBlockType, height uint64, blockTime uint64, tx *txs.Tx) error { txID := tx.ID().String() dbTx := &database.PChainTx{} dbTx.TxID = &txID @@ -135,6 +145,10 @@ func (xi *txBatchIndexer) addTx(container *indexer.Container, blockType database dbTx.Timestamp = chain.TimestampToTime(container.Timestamp) dbTx.Bytes = container.Bytes dbTx.ChainTime = xi.chainTime + if blockTime != 0 { + time := time.Unix(int64(blockTime), 0) + dbTx.BlockTime = &time + } var err error = nil switch unsignedTx := tx.Unsigned.(type) { @@ -156,6 +170,13 @@ func (xi *txBatchIndexer) addTx(container *indexer.Container, blockType database err = xi.updateGeneralBaseTx(dbTx, database.PChainCreateChainTx, &unsignedTx.BaseTx) case *txs.CreateSubnetTx: err = xi.updateGeneralBaseTx(dbTx, database.PChainCreateSubnetTx, &unsignedTx.BaseTx) + case *txs.RemoveSubnetValidatorTx: + err = xi.updateGeneralBaseTx(dbTx, database.PChainRemoveSubnetValidatorTx, &unsignedTx.BaseTx) + case *txs.TransformSubnetTx: + err = xi.updateGeneralBaseTx(dbTx, database.PChainTransformSubnetTx, &unsignedTx.BaseTx) + // We leave out the following transaction types as they are rejected by Flare nodes + // - AddPermissionlessValidatorTx + // - AddPermissionlessDelegatorTx default: err = fmt.Errorf("p-chain transaction %v with type %T in block %d is not indexed", dbTx.TxID, unsignedTx, height) } @@ -165,7 +186,7 @@ func (xi *txBatchIndexer) addTx(container *indexer.Container, blockType database return xi.addAddresses(tx) } -func (xi *txBatchIndexer) addEmptyTx(container *indexer.Container, blockType database.PChainBlockType, height uint64) { +func (xi *txBatchIndexer) addEmptyTx(container *indexer.Container, blockType database.PChainBlockType, height uint64, blockTime uint64) { dbTx := &database.PChainTx{} dbTx.BlockID = container.ID.String() dbTx.BlockType = blockType @@ -173,6 +194,10 @@ func (xi *txBatchIndexer) addEmptyTx(container *indexer.Container, blockType dat dbTx.Timestamp = chain.TimestampToTime(container.Timestamp) dbTx.Bytes = container.Bytes dbTx.TxID = nil + if blockTime != 0 { + time := time.Unix(int64(blockTime), 0) + dbTx.BlockTime = &time + } xi.newTxs = append(xi.newTxs, dbTx) } diff --git a/indexer/pchain/migrations.go b/indexer/pchain/migrations.go index ce36fb6..e5c11aa 100644 --- a/indexer/pchain/migrations.go +++ b/indexer/pchain/migrations.go @@ -11,6 +11,7 @@ import ( func init() { migrations.Container.Add("2023-02-10-00-00", "Create initial state for P-Chain transactions", createPChainTxState) migrations.Container.Add("2024-01-24-00-00", "Update transaction input type", updateTxInputType) + migrations.Container.Add("2024-11-07-00-00", "Alter type column size in p_chain_txes table", alterPChainTxType) } func createPChainTxState(db *gorm.DB) error { @@ -29,3 +30,7 @@ func updateTxInputType(db *gorm.DB) error { } return db.Model(&database.PChainTxInput{}).Where("type IS NULL").Update("type", database.DefaultInput).Error } + +func alterPChainTxType(db *gorm.DB) error { + return db.Exec("ALTER TABLE p_chain_txes CHANGE COLUMN type type VARCHAR(40)").Error +} diff --git a/indexer/shared/constants.go b/indexer/shared/constants.go new file mode 100644 index 0000000..477cf20 --- /dev/null +++ b/indexer/shared/constants.go @@ -0,0 +1,5 @@ +package shared + +const ( + ApplicationVersion = "2.0.0" +) diff --git a/indexer/shared/metrics.go b/indexer/shared/metrics.go index 3c5b1c1..aab3f0c 100644 --- a/indexer/shared/metrics.go +++ b/indexer/shared/metrics.go @@ -51,6 +51,7 @@ func InitMetricsServer(cfg *config.MetricsConfig) { r.Path("/metrics").Handler(promhttp.Handler()) r.Path("/health").HandlerFunc(healthHandler) + r.Path("/version").HandlerFunc(versionHandler) srv := &http.Server{ Addr: cfg.PrometheusAddress, @@ -70,6 +71,14 @@ func healthHandler(w http.ResponseWriter, r *http.Request) { } } +func versionHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + _, err := w.Write([]byte(ApplicationVersion)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } +} + func writeHealthResponse(w http.ResponseWriter) (err error) { ok, err := getHealthStatus() if err != nil { diff --git a/services/api/pchain.go b/services/api/pchain.go index 67760f9..e054d3e 100644 --- a/services/api/pchain.go +++ b/services/api/pchain.go @@ -16,6 +16,7 @@ type ApiPChainTxBase struct { Weight uint64 `json:"weight"` FeePercentage uint32 `json:"feePercentage"` ChainTime *time.Time `json:"chainTime"` + BlockTime *time.Time `json:"blockTime"` } type ApiPChainTx struct { @@ -55,6 +56,7 @@ func newApiPChainTxBase(tx *database.PChainTx) ApiPChainTxBase { Weight: tx.Weight, ChainTime: tx.ChainTime, FeePercentage: tx.FeePercentage, + BlockTime: tx.BlockTime, } } diff --git a/services/main/services.go b/services/main/services.go index 53e30ca..999a34d 100644 --- a/services/main/services.go +++ b/services/main/services.go @@ -41,7 +41,7 @@ func main() { } muxRouter := mux.NewRouter() - router := utils.NewSwaggerRouter(muxRouter, "Flare P-Chain Indexer", "0.1.0") + router := utils.NewSwaggerRouter(muxRouter, "Flare P-Chain Indexer", "2.0.0") routes.AddTransferRoutes(router, ctx) routes.AddStakerRoutes(router, ctx) routes.AddUTXORoutes(router, ctx) diff --git a/utils/chain/p_chain_rpc_client.go b/utils/chain/p_chain_rpc_client.go index 34fa576..5e2ae8a 100644 --- a/utils/chain/p_chain_rpc_client.go +++ b/utils/chain/p_chain_rpc_client.go @@ -15,7 +15,7 @@ import ( ) const ( - RequestTimeout = 10 * time.Second + ClientRequestTimeout = 10 * time.Second ) // Copy-paste from @@ -49,13 +49,15 @@ func (c *AvalancheRPCClient) GetRewardUTXOs(id ids.ID) (*GetRewardUTXOsReply, er TxID: id, Encoding: formatting.Hex, } - reply := &GetRewardUTXOsReply{} - ctx, cancelCtx := context.WithTimeout(context.Background(), RequestTimeout) + ctx, cancelCtx := context.WithTimeout(context.Background(), ClientRequestTimeout) defer cancelCtx() + response, err := c.client.Call(ctx, "platform.getRewardUTXOs", params) if err != nil { return nil, err } + + reply := &GetRewardUTXOsReply{} err = response.GetObject(reply) if err != nil { return nil, err @@ -68,12 +70,15 @@ func (c *AvalancheRPCClient) GetTx(id ids.ID) (*api.GetTxReply, error) { TxID: id, Encoding: formatting.Hex, } - reply := &api.GetTxReply{} - ctx := context.Background() + ctx, cancelCtx := context.WithTimeout(context.Background(), ClientRequestTimeout) + defer cancelCtx() + response, err := c.client.Call(ctx, "platform.getTx", params) if err != nil { return nil, err } + + reply := &api.GetTxReply{} err = response.GetObject(reply) if err != nil { return nil, err diff --git a/utils/chain/p_chain_tx.go b/utils/chain/p_chain_tx.go index eeb64aa..678210c 100644 --- a/utils/chain/p_chain_tx.go +++ b/utils/chain/p_chain_tx.go @@ -16,18 +16,34 @@ var ( ErrInvalidCredentialType = errors.New("invalid credential type") ) +// If block.Parse fails, try to parse as a "pre-fork" block +func ParsePChainBlock(blockBytes []byte) (blocks.Block, error) { + blk, err := block.Parse(blockBytes) + var innerBlk blocks.Block + if err == nil { + innerBlk, err = blocks.Parse(blocks.GenesisCodec, blk.Block()) + if err != nil { + return nil, errors.Wrap(err, "failed to parse inner block") + } + } else { + // try to parse as as a "pre-fork" block + innerBlk, err = blocks.Parse(blocks.Codec, blockBytes) + if err != nil { + return nil, errors.Wrap(err, "failed to parse block") + } + } + return innerBlk, nil +} + // For a given block (byte array) return a list of public keys for // signatures of inputs of the transaction in this block // Block must be of type "ApricotProposalBlock" func PublicKeysFromPChainBlock(blockBytes []byte) ([][]crypto.PublicKey, error) { - blk, err := block.Parse(blockBytes) + innerBlk, err := ParsePChainBlock(blockBytes) if err != nil { - return nil, errors.Wrap(err, "failed to parse block") - } - innerBlk, err := blocks.Parse(blocks.GenesisCodec, blk.Block()) - if err != nil { - return nil, errors.Wrap(err, "failed to parse inner block") + return nil, err } + if propBlk, ok := innerBlk.(*blocks.ApricotProposalBlock); ok { return PublicKeysFromPChainTx(propBlk.Tx) } else {