From 6e37edaf4af8e2a9ffe61caad3d601113eca3f08 Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Mon, 23 Jun 2025 13:35:16 -0500 Subject: [PATCH 1/6] expose proposervm API --- vms/proposervm/block/block.go | 14 ++-- vms/proposervm/service.go | 65 ++++++++++++++++ vms/proposervm/service.md | 138 ++++++++++++++++++++++++++++++++++ vms/proposervm/vm.go | 22 ++++++ 4 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 vms/proposervm/service.go create mode 100644 vms/proposervm/service.md diff --git a/vms/proposervm/block/block.go b/vms/proposervm/block/block.go index 68da910e1dbd..28581b516282 100644 --- a/vms/proposervm/block/block.go +++ b/vms/proposervm/block/block.go @@ -43,16 +43,16 @@ type SignedBlock interface { } type statelessUnsignedBlock struct { - ParentID ids.ID `serialize:"true"` - Timestamp int64 `serialize:"true"` - PChainHeight uint64 `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"` + 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/service.go b/vms/proposervm/service.go new file mode 100644 index 000000000000..0f194e3112d6 --- /dev/null +++ b/vms/proposervm/service.go @@ -0,0 +1,65 @@ +// 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 +} diff --git a/vms/proposervm/service.md b/vms/proposervm/service.md new file mode 100644 index 000000000000..9055bbec1127 --- /dev/null +++ b/vms/proposervm/service.md @@ -0,0 +1,138 @@ +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 +} +``` 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 From e250b8535810a62aeea4863879ef1bb40a6a489b Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Mon, 23 Jun 2025 14:52:04 -0500 Subject: [PATCH 2/6] lint --- vms/proposervm/service.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vms/proposervm/service.go b/vms/proposervm/service.go index 0f194e3112d6..5fe710f30c4b 100644 --- a/vms/proposervm/service.go +++ b/vms/proposervm/service.go @@ -1,4 +1,4 @@ -// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package proposervm @@ -13,6 +13,7 @@ import ( "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" ) @@ -61,5 +62,5 @@ func (p *ProposerAPI) GetProposerBlockWrapper(r *http.Request, args *GetProposer } reply.Block, err = json.Marshal(result) - return nil + return err } From 79613f17f14cac11432f1356821950455d048a54 Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Tue, 24 Jun 2025 09:15:26 -0500 Subject: [PATCH 3/6] lock vm state --- vms/proposervm/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vms/proposervm/service.go b/vms/proposervm/service.go index 5fe710f30c4b..b23bcd83e695 100644 --- a/vms/proposervm/service.go +++ b/vms/proposervm/service.go @@ -26,6 +26,8 @@ func (p *ProposerAPI) GetProposedHeight(_ *http.Request, _ *struct{}, reply *api zap.String("service", "proposervm"), zap.String("method", "getProposedHeight"), ) + p.vm.ctx.Lock.Lock() + defer p.vm.ctx.Lock.Unlock() reply.Height = avajson.Uint64(p.vm.lastAcceptedHeight) return nil @@ -44,6 +46,8 @@ func (p *ProposerAPI) GetProposerBlockWrapper(r *http.Request, args *GetProposer zap.String("proposerID", args.ProposerID.String()), zap.String("encoding", args.Encoding.String()), ) + p.vm.ctx.Lock.Lock() + defer p.vm.ctx.Lock.Unlock() block, err := p.vm.GetBlock(r.Context(), args.ProposerID) if err != nil { From 0e093dfc996cbf9997448cd550728df6caf8d76c Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Fri, 27 Jun 2025 09:34:50 -0500 Subject: [PATCH 4/6] rename json tag --- vms/proposervm/block/block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/proposervm/block/block.go b/vms/proposervm/block/block.go index 28581b516282..3f9839eb10b3 100644 --- a/vms/proposervm/block/block.go +++ b/vms/proposervm/block/block.go @@ -51,7 +51,7 @@ type statelessUnsignedBlock struct { } type statelessBlock struct { - StatelessBlock statelessUnsignedBlock `serialize:"true" json:"block"` + StatelessBlock statelessUnsignedBlock `serialize:"true" json:"statelessBlock"` Signature []byte `serialize:"true" json:"signature"` id ids.ID From 0875cb752c596fe372c742f7c70a22b1944715db Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Thu, 3 Jul 2025 14:47:41 -0500 Subject: [PATCH 5/6] proposervm api client --- vms/proposervm/client.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 vms/proposervm/client.go diff --git a/vms/proposervm/client.go b/vms/proposervm/client.go new file mode 100644 index 000000000000..8daacb67a7ca --- /dev/null +++ b/vms/proposervm/client.go @@ -0,0 +1,52 @@ +// 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) +} + +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("/%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.getBlock", &GetProposerBlockArgs{ + ProposerID: proposerID, + Encoding: formatting.Hex, + }, res, options...); err != nil { + return nil, err + } + return formatting.Decode(res.Encoding, res.Block) +} From 7ea6ee90425143bdb871ba0b0c52b7e90e916c8b Mon Sep 17 00:00:00 2001 From: cam-schultz Date: Thu, 3 Jul 2025 17:13:03 -0500 Subject: [PATCH 6/6] client fixes --- vms/proposervm/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/proposervm/client.go b/vms/proposervm/client.go index 8daacb67a7ca..9d957a3687c7 100644 --- a/vms/proposervm/client.go +++ b/vms/proposervm/client.go @@ -30,7 +30,7 @@ type client struct { // 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("/%s/proposervm", blockchainName)), + requester: rpc.NewEndpointRequester(uri + fmt.Sprintf("/ext/bc/%s/proposervm", blockchainName)), } } @@ -42,7 +42,7 @@ func (c *client) GetProposedHeight(ctx context.Context, options ...rpc.Option) ( 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.getBlock", &GetProposerBlockArgs{ + if err := c.requester.SendRequest(ctx, "proposervm.getProposerBlockWrapper", &GetProposerBlockArgs{ ProposerID: proposerID, Encoding: formatting.Hex, }, res, options...); err != nil {