diff --git a/tests/e2e/c/proposervm_epoch.go b/tests/e2e/c/proposervm_epoch.go new file mode 100644 index 000000000000..c49f1d493313 --- /dev/null +++ b/tests/e2e/c/proposervm_epoch.go @@ -0,0 +1,100 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package c + +import ( + "math/big" + "time" + + "github.com/ava-labs/libevm/core/types" + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/proposervm" +) + +var _ = e2e.DescribeCChain("[ProposerVM Epoch]", func() { + tc := e2e.NewTestContext() + require := require.New(tc) + + const txAmount = 10 * units.Avax // Arbitrary amount to send and transfer + + ginkgo.It("should advance the proposervm epoch according to the upgrade config epoch duration", func() { + // TODO: Skip this test if Granite is not activated + + env := e2e.GetEnv(tc) + var ( + senderKey = env.PreFundedKey + senderEthAddress = senderKey.EthAddress() + recipientKey = e2e.NewPrivateKey(tc) + recipientEthAddress = recipientKey.EthAddress() + ) + + tc.By("initializing a new eth client") + // Select a random node URI to use for both the eth client and + // the wallet to avoid having to verify that all nodes are at + // the same height before initializing the wallet. + nodeURI := env.GetRandomNodeURI() + ethClient := e2e.NewEthClient(tc, nodeURI) + + proposerClient := proposervm.NewClient(nodeURI.URI, "C") + + tc.By("issuing C-Chain transactions to advance the epoch", func() { + // Issue enough C-Chain transactions to observe the epoch advancing + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + // Issue enough txs to activate the proposervm form and ensure we advance the epoch (duration is 4s) + const numTxs = 7 + txCount := 0 + + initialEpochNumber, _, _, err := proposerClient.GetEpoch(tc.DefaultContext()) + require.NoError(err) + + for range ticker.C { + acceptedNonce, err := ethClient.AcceptedNonceAt(tc.DefaultContext(), senderEthAddress) + require.NoError(err) + gasPrice := e2e.SuggestGasPrice(tc, ethClient) + tx := types.NewTransaction( + acceptedNonce, + recipientEthAddress, + big.NewInt(int64(txAmount)), + e2e.DefaultGasLimit, + gasPrice, + nil, + ) + + // Sign transaction + cChainID, err := ethClient.ChainID(tc.DefaultContext()) + require.NoError(err) + signer := types.NewEIP155Signer(cChainID) + signedTx, err := types.SignTx(tx, signer, senderKey.ToECDSA()) + require.NoError(err) + + receipt := e2e.SendEthTransaction(tc, ethClient, signedTx) + require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + + epochNumber, epochStartTime, pChainHeight, err := proposerClient.GetEpoch(tc.DefaultContext()) + tc.Log().Debug( + "epoch", + zap.Uint64("Epoch Number:", epochNumber), + zap.Uint64("Epoch Start Time:", epochStartTime), + zap.Uint64("P-Chain Height:", pChainHeight), + ) + + txCount++ + if txCount >= numTxs { + require.Greater(epochNumber, initialEpochNumber, + "expected epoch number to advance after issuing %d transactions, but it did not", + numTxs, + ) + break + } + } + }) + }) +}) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 88acfda1688a..66c8df97729b 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/json" "testing" + "time" "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/require" @@ -50,6 +51,7 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { upgrades := upgrade.Default if flagVars.ActivateGranite() { upgrades.GraniteTime = upgrade.InitiallyActiveTime + upgrades.GraniteEpochDuration = 4 * time.Second } else { upgrades.GraniteTime = upgrade.UnscheduledActivationTime } diff --git a/upgrade/upgrade.go b/upgrade/upgrade.go index 9aa560b39859..9af98ec82a37 100644 --- a/upgrade/upgrade.go +++ b/upgrade/upgrade.go @@ -77,30 +77,30 @@ var ( EtnaTime: InitiallyActiveTime, FortunaTime: InitiallyActiveTime, GraniteTime: UnscheduledActivationTime, - GraniteEpochDuration: 30, + GraniteEpochDuration: 30 * time.Second, } ErrInvalidUpgradeTimes = errors.New("invalid upgrade configuration") ) type Config struct { - ApricotPhase1Time time.Time `json:"apricotPhase1Time"` - ApricotPhase2Time time.Time `json:"apricotPhase2Time"` - ApricotPhase3Time time.Time `json:"apricotPhase3Time"` - ApricotPhase4Time time.Time `json:"apricotPhase4Time"` - ApricotPhase4MinPChainHeight uint64 `json:"apricotPhase4MinPChainHeight"` - ApricotPhase5Time time.Time `json:"apricotPhase5Time"` - ApricotPhasePre6Time time.Time `json:"apricotPhasePre6Time"` - ApricotPhase6Time time.Time `json:"apricotPhase6Time"` - ApricotPhasePost6Time time.Time `json:"apricotPhasePost6Time"` - BanffTime time.Time `json:"banffTime"` - CortinaTime time.Time `json:"cortinaTime"` - CortinaXChainStopVertexID ids.ID `json:"cortinaXChainStopVertexID"` - DurangoTime time.Time `json:"durangoTime"` - EtnaTime time.Time `json:"etnaTime"` - FortunaTime time.Time `json:"fortunaTime"` - GraniteTime time.Time `json:"graniteTime"` - GraniteEpochDuration uint64 `json:"graniteEpochDuration"` + ApricotPhase1Time time.Time `json:"apricotPhase1Time"` + ApricotPhase2Time time.Time `json:"apricotPhase2Time"` + ApricotPhase3Time time.Time `json:"apricotPhase3Time"` + ApricotPhase4Time time.Time `json:"apricotPhase4Time"` + ApricotPhase4MinPChainHeight uint64 `json:"apricotPhase4MinPChainHeight"` + ApricotPhase5Time time.Time `json:"apricotPhase5Time"` + ApricotPhasePre6Time time.Time `json:"apricotPhasePre6Time"` + ApricotPhase6Time time.Time `json:"apricotPhase6Time"` + ApricotPhasePost6Time time.Time `json:"apricotPhasePost6Time"` + BanffTime time.Time `json:"banffTime"` + CortinaTime time.Time `json:"cortinaTime"` + CortinaXChainStopVertexID ids.ID `json:"cortinaXChainStopVertexID"` + DurangoTime time.Time `json:"durangoTime"` + EtnaTime time.Time `json:"etnaTime"` + FortunaTime time.Time `json:"fortunaTime"` + GraniteTime time.Time `json:"graniteTime"` + GraniteEpochDuration time.Duration `json:"graniteEpochDuration"` } func (c *Config) Validate() error { diff --git a/vms/proposervm/block.go b/vms/proposervm/block.go index 5bd69c4c50ff..39392da063fd 100644 --- a/vms/proposervm/block.go +++ b/vms/proposervm/block.go @@ -107,7 +107,7 @@ func (p *postForkCommonComponents) getPChainEpoch(ctx context.Context, parentID return height, 0, parentTimestamp, nil } - if parentTimestamp.After(epochStartTime.Add(time.Duration(p.vm.Upgrades.GraniteEpochDuration) * time.Second)) { + if parentTimestamp.After(epochStartTime.Add(p.vm.Upgrades.GraniteEpochDuration)) { // If the parent crossed the epoch boundary, then it sealed the previous epoch. The child // is the first block of the new epoch, so should use the parent's P-Chain height, increment // the epoch number, and set the epoch start time to the parent's timestamp. @@ -115,6 +115,10 @@ func (p *postForkCommonComponents) getPChainEpoch(ctx context.Context, parentID if err != nil { return 0, 0, time.Time{}, fmt.Errorf("failed to get P-Chain height: %w", err) } + p.vm.ctx.Log.Info("parent sealed epoch. advancing epoch", + zap.Uint64("height", height), + zap.Uint64("epoch", epoch+1), + ) return height, epoch + 1, parentTimestamp, nil } // Otherwise, the parent did not seal the previous epoch, so the child should use the parent's @@ -124,6 +128,10 @@ func (p *postForkCommonComponents) getPChainEpoch(ctx context.Context, parentID if err != nil { return 0, 0, time.Time{}, fmt.Errorf("failed to get P-Chain height: %w", err) } + p.vm.ctx.Log.Debug("parent did not seal epoch. using parent's epoch", + zap.Uint64("height", height), + zap.Uint64("epoch", epoch), + ) return height, epoch, epochStartTime, nil } diff --git a/vms/proposervm/block/block.go b/vms/proposervm/block/block.go index 61d902906bb9..348b96a1a21a 100644 --- a/vms/proposervm/block/block.go +++ b/vms/proposervm/block/block.go @@ -46,19 +46,19 @@ type SignedBlock interface { } type statelessUnsignedBlock struct { - ParentID ids.ID `serialize:"true"` - Timestamp int64 `serialize:"true"` - PChainHeight uint64 `serialize:"true"` - PChainEpochHeight uint64 `serialize:"true"` - EpochNumber uint64 `serialize:"true"` - EpochStartTime int64 `serialize:"true"` - Certificate []byte `serialize:"true"` - Block []byte `serialize:"true"` + ParentID ids.ID `serialize:"true" json:"parentID"` + Timestamp int64 `serialize:"true" json:"timestamp"` + PChainHeight uint64 `serialize:"true" json:"pChainHeight"` + PChainEpochHeight uint64 `serialize:"true" json:"pChainEpochHeight"` + EpochNumber uint64 `serialize:"true" json:"epochNumber"` + EpochStartTime int64 `serialize:"true" json:"epochStartTime"` + Certificate []byte `serialize:"true" json:"certificate"` + Block []byte `serialize:"true" json:"block"` } type statelessBlock struct { - StatelessBlock statelessUnsignedBlock `serialize:"true"` - Signature []byte `serialize:"true"` + StatelessBlock statelessUnsignedBlock `serialize:"true" json:"block"` + Signature []byte `serialize:"true" json:"signature"` id ids.ID timestamp time.Time diff --git a/vms/proposervm/client.go b/vms/proposervm/client.go new file mode 100644 index 000000000000..9f194fef3085 --- /dev/null +++ b/vms/proposervm/client.go @@ -0,0 +1,62 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package proposervm + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/rpc" +) + +var _ Client = (*client)(nil) + +type Client interface { + // GetProposedHeight returns the current height of this node's proposer VM. + GetProposedHeight(ctx context.Context, options ...rpc.Option) (uint64, error) + // GetProposerBlockWrapper returns the ProposerVM block wrapper + GetProposerBlockWrapper(ctx context.Context, proposerID ids.ID, options ...rpc.Option) ([]byte, error) + // GetEpoch returns the current epoch number, start time, and P-Chain height. + GetEpoch(ctx context.Context, options ...rpc.Option) (uint64, uint64, uint64, error) +} + +type client struct { + requester rpc.EndpointRequester +} + +// NewClient returns a Client for interacting with the ProposerVM API. +// The provided blockchainName should be the blockchainID or an alias (e.g. "P" for the P-Chain). +func NewClient(uri string, blockchainName string) Client { + return &client{ + requester: rpc.NewEndpointRequester(uri + fmt.Sprintf("/ext/bc/%s/proposervm", blockchainName)), + } +} + +func (c *client) GetProposedHeight(ctx context.Context, options ...rpc.Option) (uint64, error) { + res := &api.GetHeightResponse{} + err := c.requester.SendRequest(ctx, "proposervm.getProposedHeight", struct{}{}, res, options...) + return uint64(res.Height), err +} + +func (c *client) GetProposerBlockWrapper(ctx context.Context, proposerID ids.ID, options ...rpc.Option) ([]byte, error) { + res := &api.FormattedBlock{} + if err := c.requester.SendRequest(ctx, "proposervm.getProposerBlockWrapper", &GetProposerBlockArgs{ + ProposerID: proposerID, + Encoding: formatting.Hex, + }, res, options...); err != nil { + return nil, err + } + return formatting.Decode(res.Encoding, res.Block) +} + +func (c *client) GetEpoch(ctx context.Context, options ...rpc.Option) (uint64, uint64, uint64, error) { + res := &GetEpochResponse{} + if err := c.requester.SendRequest(ctx, "proposervm.getEpoch", struct{}{}, res, options...); err != nil { + return 0, 0, 0, err + } + return uint64(res.Number), uint64(res.StartTime), uint64(res.PChainHeight), nil +} diff --git a/vms/proposervm/service.go b/vms/proposervm/service.go new file mode 100644 index 000000000000..19ca7e5a47df --- /dev/null +++ b/vms/proposervm/service.go @@ -0,0 +1,106 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package proposervm + +import ( + "encoding/json" + "fmt" + "net/http" + + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + avajson "github.com/ava-labs/avalanchego/utils/json" +) + +type ProposerAPI struct { + vm *VM +} + +func (p *ProposerAPI) GetProposedHeight(_ *http.Request, _ *struct{}, reply *api.GetHeightResponse) error { + p.vm.ctx.Log.Debug("API called", + zap.String("service", "proposervm"), + zap.String("method", "getProposedHeight"), + ) + + reply.Height = avajson.Uint64(p.vm.lastAcceptedHeight) + return nil +} + +// GetProposerBlockArgs is the parameters supplied to the GetProposerBlockWrapper API +type GetProposerBlockArgs struct { + ProposerID ids.ID `json:"proposerID"` + Encoding formatting.Encoding `json:"encoding"` +} + +func (p *ProposerAPI) GetProposerBlockWrapper(r *http.Request, args *GetProposerBlockArgs, reply *api.GetBlockResponse) error { + p.vm.ctx.Log.Debug("API called", + zap.String("service", "proposervm"), + zap.String("method", "getProposerBlockWrapper"), + zap.String("proposerID", args.ProposerID.String()), + zap.String("encoding", args.Encoding.String()), + ) + + block, err := p.vm.GetBlock(r.Context(), args.ProposerID) + if err != nil { + return err + } + reply.Encoding = args.Encoding + + var result any + if args.Encoding == formatting.JSON { + result = block + } else { + result, err = formatting.Encode(args.Encoding, block.Bytes()) + if err != nil { + return fmt.Errorf("couldn't encode block %s as %s: %w", args.ProposerID, args.Encoding, err) + } + } + + reply.Block, err = json.Marshal(result) + return nil +} + +type GetEpochResponse struct { + Number avajson.Uint64 `json:"Number"` + StartTime avajson.Uint64 `json:"StartTime"` + PChainHeight avajson.Uint64 `json:"pChainHeight"` +} + +func (p *ProposerAPI) GetEpoch(r *http.Request, _ *struct{}, reply *GetEpochResponse) error { + p.vm.ctx.Log.Debug("API called", + zap.String("service", "proposervm"), + zap.String("method", "getEpoch"), + ) + + lastAccepted, err := p.vm.LastAccepted(r.Context()) + if err != nil { + return fmt.Errorf("couldn't get last accepted block ID: %w", err) + } + latestBlock, err := p.vm.getBlock(r.Context(), lastAccepted) + if err != nil { + return fmt.Errorf("couldn't get latest block: %w", err) + } + + epochNumber, err := latestBlock.epochNumber(r.Context()) + if err != nil { + return fmt.Errorf("couldn't get epoch number: %w", err) + } + epochStartTime, err := latestBlock.epochStartTime(r.Context()) + if err != nil { + return fmt.Errorf("couldn't get epoch start time: %w", err) + } + epochPChainHeight, err := latestBlock.pChainEpochHeight(r.Context()) + if err != nil { + return fmt.Errorf("couldn't get epoch P-Chain height: %w", err) + } + + reply.Number = avajson.Uint64(epochNumber) + reply.StartTime = avajson.Uint64(epochStartTime.Unix()) + reply.PChainHeight = avajson.Uint64(epochPChainHeight) + + return nil +} diff --git a/vms/proposervm/service.md b/vms/proposervm/service.md new file mode 100644 index 000000000000..bd32454fe78c --- /dev/null +++ b/vms/proposervm/service.md @@ -0,0 +1,178 @@ +The ProposerVM API allows clients to fetch information about a Snowman++ chain's ProposerVM. + +## Endpoint + +``` +/ext/bc/{blockchainID}/proposervm +``` + +## Format + +This API uses the `json 2.0` RPC format. + +## Methods + +### `proposervm.getProposedHeight` + +Returns this node's current proposer VM height. + +**Signature:** + +``` +proposervm.getProposedHeight() -> +{ + height: int, +} +``` + +**Example Call:** + +```sh +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "proposervm.getProposedHeight", + "params": {}, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P/proposervm +``` + +**Example Response:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "height": "56" + }, + "id": 1 +} +``` + +### `proposervm.getProposerBlockWrapper` + +Get a block's ProposerVM wrapper by its proposer ID. + +**Signature:** + +``` +proposervm.getProposerBlockWrapper({ + proposerID: string + encoding: string // optional +}) -> { + block: string, + encoding: string +} +``` + +**Request:** + +- `proposerID` is the proposer ID. It should be in cb58 format. +- `encoding` is the encoding format to use. Can be either `hex` or `json`. Defaults to `hex`. + +#### Hex Example + +**Example Call:** + +```sh +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "proposervm.getProposerBlockWrapper", + "params": { + "proposerID": "owJxcaDMaehbqoib8FRP7MuPdfGpSdXqD4hjkBtW4vcCJzr2Y", + "encoding": "hex" + }, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/C/proposervm +``` + +**Example Response:** + +```json +{ + "jsonrpc":"2.0", + "result":{ + "block":"0x000000000000018a9f604b26237c49e54667b80e24433512774dd6bb8cd99032e927ae141dcd0000000068597a6d00000000000000000000053b308205373082031f020900baf3b5c5c6d0d14a300d06092a864886f70d01010b0500307f310b3009060355040613025553310b300906035504080c024e59310f300d06035504070c064974686163613110300e060355040a0c074176616c616273310e300c060355040b0c054765636b6f310c300a06035504030c036176613122302006092a864886f70d01090116137374657068656e406176616c6162732e6f72673020170d3139303730323136313231395a180f33303139303731303136313231395a303a310b3009060355040613025553310b300906035504080c024e593110300e060355040a0c074176616c616273310c300a06035504030c0361766130820222300d06092a864886f70d01010105000382020f003082020a0282020100dd4e847ad276ba36e47d892014332ccf5c934c59541b2f24f9fe2642889dc107861630185fc6925626259770cfb39c382926ec8211e8790e9e9963715eee4e8786de85985a438f09e3a5099d904294834d06f8494f4e9fd4b26b0f2fe240b303ea0595a93014b776c5d036e9cc32d30696b7f94b497a5d7e32ee473f193c5882f79667d26d47f9b628d7abfb08e0047a89e2b3ea9c7077d2c0d83d983dd42cf5024f016f6c36235d3ccd056028ae9e22a34a5c927fe298b3499754e3ddb4c0cc580699df0c07219f17a2f54fefdd06d3ede2942367c6afe84e59321653cd3e55e381b7c6ba3f8b12f40421e7fe86f2ac2602a3b7610f5dc4df368aefc7dfe37e1866bf334af9ac45abcf022e161c0abf241ca4f4419b6c909c19e12fd1b990dea7e0e04cd0ab2067709894765283879670d801b7558216c41ceef7d415a267afc40fde917857fe01a24ef04cee4737403511811562e5f9dcd1a7aca55ef8583dfb130e8173d691436a76fa48f596d5dd9d12e7cebaa85dffbc166b8600ab29ce44fef31ec4145ef0a8b8c0aa8e355862f561947111ef1448a473eed5acf8f461a1bacd52ddfae38024df357ca38f628e17e2c66b3323d89773a31ad3217d5e0abc268b12bd5db8e925a8ee9c0af190f220252d5cb7549c96e462740f42b28808abe1535d0092a8d598221a2bc92d6ba54df8d489f08df68d75e408aa2718aac30203010001300d06092a864886f70d01010b050003820201009039dc03ede448c41bdb40751cfa6d1347311ad7b0427dc9356365b03774d95c319290f544d2eba303c48bdea8bcfc24cb191a88f54de045925ffad24f045fcfd36d4f126c9a1534b886dc5c0f14f1b99bf68c2a3df97854132bd15a2b539a8cb57acab79a4a2d5501de4e642fc02b1024085cb5503f851749c2630fb3507c43139423a06a48e0476bd0ac5c8cd5385bfc16a13dd4f640da7b37d2c29062bae76c7f7d6a97c5e568252d4a323f0c5fd9edc000287b6de11d938e62bef2b08b9264f27afce4a0c760f96a92bd1b77d12197cc6995a34174c3de46e7ea043973b6370b277c289a7aedf7981031e4a82c763750db084636e3e37cc89aeeeaf6ffc30fda69429bb6e9a44346604d80b640f463395e76d94458b75712f2a9ab06712cda1f0cf973bbce04e90e765a9c43321246f583c623751e400bfa1922402b8c72180e44080cc333e8a1af2b7d6b4b229397329c2e5466fff2b18d2c43e8be808af73e9cf9667ece4468e3edd342438c63ae9786deeb021972de57abf93ca0e9ae635d3f7658a0bf317058032d9940da32db4e2434a7af707ed1d9c4a4c484ddde7e4469c372e4c02d08d224ec9c1e2c287dacbacb501b7e8b89f8e2c3fe2d585f5877a69e003c0a81789fa790d29a748b16537a7d23e2e3741b635ff89786325aa839b2d674839d1ffa5b0e9a571a865a28a0ddefe6c0ef81657b9521efed5edc00000373f90370f90298a057b43873f59d6e0ff493909e8373502eaa828f0eb4d7dbe999b855f9b895d88ca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940100000000000000000000000000000000000000a0685dcd731e76c2e6a503df5759ab11b324f03c8f26090637ad4dbb26ad5522fba04efbaa764431eba396b9edba52f594a6660473432761931c98b49ef00467b87ea0d95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010c83e4e1c082a4108468597a6db8560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421843b9aca0080808080a00000000000000000000000000000000000000000000000000000000000000000f8d0f866108534630b8a00825208948db97c7cece249c2b98bdc0226cc4c2a57bf52fc0180820a95a07ef0c6d54c839648ea6cfe191c37efc5ba9d9115695e061870b601c1ce647e75a03e066459cb0853a8d67f7b777753515b2ce627553a4d9b51b06ad6bdf3102b8ef866118534630b8a00825208948db97c7cece249c2b98bdc0226cc4c2a57bf52fc0180820a96a03a1c18eea77d4f08a29e482442ca2f2beb5bc8568e2768470ebdb6ae05382424a06e205ef2e0273c481f2e28a60e504575dba0714608601880d87906a98de41bb3c0808000000200bfcc7483e2c539c2d925a1b4eed65e2cbfae39e284be82d77ee86c078d1a1d4f54d8c46c6695d6619a589137eabcd4a8e93c94394c4d83458b122598b9d69f49f0c9e61b746ad46d91710c3475c97595d17c3ef1aa8c7e846fd8a231bdb68687a208ee752cd64ecf86288bec610688708e34d54ecb15ea4178e1dab8a74109ae1514240a08cddae9dc8ec6db17cfeffc2d0814e908e57676700c6fa3a4bbe1c84e537fd7d2f28491bf9ce3c5e57308027c198a3547ad9a23d365359b0c709c260abf6a04cdc59bcb4a65006c66c9cc2becae49e9275a2f3517260762a7b4c646121583590a25de74a924a66a43d2b1a6f09adfb664e552fc0b1b260594df57795b334c125627e112606d784790f6916dcb610f1e10a424f0ef8244cde7d2f0631e025b28d54f8a24003f3792b420a1eeb62a8a21a27e9eb7e3278b7e14dfc784e736ce2fad220ea9c2d97e98b451fd3a212b6b863a779d55a80ead284e49e0556792663e38316e7a31ac6b1915e96a1dd50924c48ffbb7d46df363baa3c8184457411577aa793cb3a9adef73301ecec8f7561f60a6187f13ed34d2e1756a7959fd5b10a4f753d07dd90022de2a2ea401e2516cd917a2a48c030e2cc0b2dfaa5dec82af18f6772462a5a58a9d128772b45f782c8a1854564158ed75531cc02d2050170de95147a8eb2b8364fb83d74722bee68e1654cf314d629599e3795617ff75c569bd", + "encoding":"hex" + }, + "id":1 +} +``` + +#### JSON Example + +**Example Call:** + +```sh +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "proposervm.getProposerBlockWrapper", + "params": { + "proposerID": "owJxcaDMaehbqoib8FRP7MuPdfGpSdXqD4hjkBtW4vcCJzr2Y", + "encoding": "json" + }, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/C/proposervm +``` + +**Example Response:** + +```json +{ + "jsonrpc":"2.0", + "result":{ + "block":{ + "SignedBlock":{ + "block":{ + "parentID":"gNms4aTjhR3vLDsTLhNtuWdv6S6xpA6vLjPB53uWSYRxxi3b", + "timestamp":1750694509, + "pChainHeight":0, + "certificate":"MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFiczEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYTc3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMTlaGA8zMDE5MDcxMDE2MTIxOVowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFsYWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdToR60na6NuR9iSAUMyzPXJNMWVQbLyT5/iZCiJ3BB4YWMBhfxpJWJiWXcM+znDgpJuyCEeh5Dp6ZY3Fe7k6Hht6FmFpDjwnjpQmdkEKUg00G+ElPTp/UsmsPL+JAswPqBZWpMBS3dsXQNunMMtMGlrf5S0l6XX4y7kc/GTxYgveWZ9JtR/m2KNer+wjgBHqJ4rPqnHB30sDYPZg91Cz1Ak8Bb2w2I108zQVgKK6eIqNKXJJ/4pizSZdU4920wMxYBpnfDAchnxei9U/v3QbT7eKUI2fGr+hOWTIWU80+VeOBt8a6P4sS9AQh5/6G8qwmAqO3YQ9dxN82iu/H3+N+GGa/M0r5rEWrzwIuFhwKvyQcpPRBm2yQnBnhL9G5kN6n4OBM0KsgZ3CYlHZSg4eWcNgBt1WCFsQc7vfUFaJnr8QP3pF4V/4Bok7wTO5HN0A1EYEVYuX53NGnrKVe+Fg9+xMOgXPWkUNqdvpI9ZbV3Z0S5866qF3/vBZrhgCrKc5E/vMexBRe8Ki4wKqONVhi9WGUcRHvFEikc+7VrPj0YaG6zVLd+uOAJN81fKOPYo4X4sZrMyPYl3OjGtMhfV4KvCaLEr1duOklqO6cCvGQ8iAlLVy3VJyW5GJ0D0KyiAir4VNdAJKo1ZgiGivJLWulTfjUifCN9o115AiqJxiqwwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCQOdwD7eRIxBvbQHUc+m0TRzEa17BCfck1Y2WwN3TZXDGSkPVE0uujA8SL3qi8/CTLGRqI9U3gRZJf+tJPBF/P021PEmyaFTS4htxcDxTxuZv2jCo9+XhUEyvRWitTmoy1esq3mkotVQHeTmQvwCsQJAhctVA/hRdJwmMPs1B8QxOUI6BqSOBHa9CsXIzVOFv8FqE91PZA2ns30sKQYrrnbH99apfF5WglLUoyPwxf2e3AACh7beEdk45ivvKwi5Jk8nr85KDHYPlqkr0bd9Ehl8xplaNBdMPeRufqBDlztjcLJ3womnrt95gQMeSoLHY3UNsIRjbj43zImu7q9v/DD9ppQpu26aRDRmBNgLZA9GM5XnbZRFi3VxLyqasGcSzaHwz5c7vOBOkOdlqcQzISRvWDxiN1HkAL+hkiQCuMchgORAgMwzPooa8rfWtLIpOXMpwuVGb/8rGNLEPovoCK9z6c+WZ+zkRo4+3TQkOMY66Xht7rAhly3ler+Tyg6a5jXT92WKC/MXBYAy2ZQNoy204kNKevcH7R2cSkxITd3n5EacNy5MAtCNIk7JweLCh9rLrLUBt+i4n44sP+LVhfWHemngA8CoF4n6eQ0pp0ixZTen0j4uN0G2Nf+JeGMlqoObLWdIOdH/pbDppXGoZaKKDd7+bA74Fle5Uh7+1e3A==", + "block":"+QNw+QKYoFe0OHP1nW4P9JOQnoNzUC6qgo8OtNfb6Zm4Vfm4ldiMoB3MTejex116q4W1Z7bM1BrTEkUblIp0E/ChQv1A1JNHlAEAAAAAAAAAAAAAAAAAAAAAAAAAoGhdzXMedsLmpQPfV1mrEbMk8DyPJgkGN61NuyatVSL7oE77qnZEMeujlrntulL1lKZmBHNDJ2GTHJi0nvAEZ7h+oNlbZzgY+kk97sQU4B5hDZfuKHyUIcjv9BArFkfBoYTkuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMg+ThwIKkEIRoWXptuFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAKBW6B8XG8xVpv+DReaSwPhuW0jgG5lsrcABYi+142O0IYQ7msoAgICAgKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPjQ+GYQhTRjC4oAglIIlI25fHzs4knCuYvcAibMTCpXv1L8AYCCCpWgfvDG1UyDlkjqbP4ZHDfvxbqdkRVpXgYYcLYBwc5kfnWgPgZkWcsIU6jWf3t3d1NRWyzmJ1U6TZtRsGrWvfMQK474ZhGFNGMLigCCUgiUjbl8fOziScK5i9wCJsxMKle/UvwBgIIKlqA6HBjup31PCKKeSCRCyi8r61vIVo4naEcOvbauBTgkJKBuIF7y4Cc8SB8uKKYOUEV126BxRghgGIDYeQapjeQbs8CAgA=="}, + "signature":"v8x0g+LFOcLZJaG07tZeLL+uOeKEvoLXfuhsB40aHU9U2MRsZpXWYZpYkTfqvNSo6TyUOUxNg0WLEiWYudafSfDJ5ht0atRtkXEMNHXJdZXRfD7xqox+hG/YojG9toaHogjudSzWTs+GKIvsYQaIcI401U7LFepBeOHauKdBCa4VFCQKCM3a6dyOxtsXz+/8LQgU6QjldnZwDG+jpLvhyE5Tf9fS8oSRv5zjxeVzCAJ8GYo1R62aI9NlNZsMcJwmCr9qBM3Fm8tKZQBsZsnMK+yuSeknWi81FyYHYqe0xkYSFYNZCiXedKkkpmpD0rGm8JrftmTlUvwLGyYFlN9XeVszTBJWJ+ESYG14R5D2kW3LYQ8eEKQk8O+CRM3n0vBjHgJbKNVPiiQAPzeStCCh7rYqiiGifp634yeLfhTfx4TnNs4vrSIOqcLZfpi0Uf06IStrhjp3nVWoDq0oTkngVWeSZj44MW56MaxrGRXpah3VCSTEj/u31G3zY7qjyBhEV0EVd6p5PLOpre9zMB7OyPdWH2CmGH8T7TTS4XVqeVn9WxCk91PQfdkAIt4qLqQB4lFs2ReipIwDDizAst+qXeyCrxj2dyRipaWKnRKHcrRfeCyKGFRWQVjtdVMcwC0gUBcN6VFHqOsrg2T7g9dHIr7mjhZUzzFNYpWZ43lWF/8=" + } + }, + "encoding":"json" + }, + "id":1 +} +``` + +### `proposervm.getEpoch + +Returns the current epoch information. + +**Signature** + +``` +proposervm.getEpoch() -> +{ + number: int, + startTime: int, + pChainHeight: int +} +``` + +** Example Call:** + +```sh +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "proposervm.getEpoch", + "params": {}, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P/proposervm +``` + +**Example Response:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "number": "56", + "startTime":"1751918364", + "pChainHeight": "21857141" + }, + "id": 1 +} +``` \ No newline at end of file diff --git a/vms/proposervm/service_test.go b/vms/proposervm/service_test.go new file mode 100644 index 000000000000..c1db815a18ab --- /dev/null +++ b/vms/proposervm/service_test.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package proposervm + +import ( + "testing" + "time" +) + +func defaultAPI(t *testing.T) *ProposerAPI { + var ( + activationTime = time.Unix(0, 0) + durangoTime = activationTime + ) + _, _, vm, _ := initTestProposerVM(t, activationTime, durangoTime, 0) + return &ProposerAPI{ + vm: vm, + } +} + +func TestGetProposedHeight(t *testing.T) { + // require := require.New(t) + +} diff --git a/vms/proposervm/vm.go b/vms/proposervm/vm.go index cd97f4aefc3d..45e3de7230ae 100644 --- a/vms/proposervm/vm.go +++ b/vms/proposervm/vm.go @@ -7,8 +7,10 @@ import ( "context" "errors" "fmt" + "net/http" "time" + "github.com/gorilla/rpc/v2" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" @@ -24,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" @@ -251,6 +254,25 @@ func (vm *VM) Shutdown(ctx context.Context) error { return vm.ChainVM.Shutdown(ctx) } +// overriddes ChainVM.CreateHandlers to expose the proposervm API path +func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) { + handlers, err := vm.ChainVM.CreateHandlers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create inner VM handlers: %w", err) + } + + server := rpc.NewServer() + server.RegisterCodec(json.NewCodec(), "application/json") + server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") + err = server.RegisterService(&ProposerAPI{vm}, "proposervm") + if err != nil { + return nil, fmt.Errorf("failed to register proposervm service: %w", err) + } + handlers["/proposervm"] = server + + return handlers, nil +} + func (vm *VM) SetState(ctx context.Context, newState snow.State) error { if err := vm.ChainVM.SetState(ctx, newState); err != nil { return err