diff --git a/network/p2p/acp118/handler.go b/network/p2p/acp118/handler.go index 971f29af9b82..f16358a6db32 100644 --- a/network/p2p/acp118/handler.go +++ b/network/p2p/acp118/handler.go @@ -18,6 +18,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) +const HandlerID = p2p.SignatureRequestHandlerID + var _ p2p.Handler = (*Handler)(nil) // Verifier verifies that a warp message should be signed diff --git a/proto/pb/platformvm/platformvm.pb.go b/proto/pb/platformvm/platformvm.pb.go new file mode 100644 index 000000000000..452e02d077d0 --- /dev/null +++ b/proto/pb/platformvm/platformvm.pb.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc (unknown) +// source: platformvm/platformvm.proto + +package platformvm + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SubnetValidatorRegistrationJustification struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Preimage: + // + // *SubnetValidatorRegistrationJustification_ConvertSubnetTxData + // *SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage + Preimage isSubnetValidatorRegistrationJustification_Preimage `protobuf_oneof:"preimage"` +} + +func (x *SubnetValidatorRegistrationJustification) Reset() { + *x = SubnetValidatorRegistrationJustification{} + if protoimpl.UnsafeEnabled { + mi := &file_platformvm_platformvm_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubnetValidatorRegistrationJustification) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubnetValidatorRegistrationJustification) ProtoMessage() {} + +func (x *SubnetValidatorRegistrationJustification) ProtoReflect() protoreflect.Message { + mi := &file_platformvm_platformvm_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubnetValidatorRegistrationJustification.ProtoReflect.Descriptor instead. +func (*SubnetValidatorRegistrationJustification) Descriptor() ([]byte, []int) { + return file_platformvm_platformvm_proto_rawDescGZIP(), []int{0} +} + +func (m *SubnetValidatorRegistrationJustification) GetPreimage() isSubnetValidatorRegistrationJustification_Preimage { + if m != nil { + return m.Preimage + } + return nil +} + +func (x *SubnetValidatorRegistrationJustification) GetConvertSubnetTxData() *SubnetIDIndex { + if x, ok := x.GetPreimage().(*SubnetValidatorRegistrationJustification_ConvertSubnetTxData); ok { + return x.ConvertSubnetTxData + } + return nil +} + +func (x *SubnetValidatorRegistrationJustification) GetRegisterSubnetValidatorMessage() []byte { + if x, ok := x.GetPreimage().(*SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage); ok { + return x.RegisterSubnetValidatorMessage + } + return nil +} + +type isSubnetValidatorRegistrationJustification_Preimage interface { + isSubnetValidatorRegistrationJustification_Preimage() +} + +type SubnetValidatorRegistrationJustification_ConvertSubnetTxData struct { + // This should be set to obtain an attestation that a validator specified in + // a ConvertSubnetTx has been removed from the validator set. + ConvertSubnetTxData *SubnetIDIndex `protobuf:"bytes,1,opt,name=convert_subnet_tx_data,json=convertSubnetTxData,proto3,oneof"` +} + +type SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage struct { + // This should be set to a RegisterSubnetValidatorMessage to obtain an + // attestation that a validator is not currently registered and can never be + // registered. This can be because the validator was successfully added and + // then later removed, or because the validator was never added and the + // registration expired. + RegisterSubnetValidatorMessage []byte `protobuf:"bytes,2,opt,name=register_subnet_validator_message,json=registerSubnetValidatorMessage,proto3,oneof"` +} + +func (*SubnetValidatorRegistrationJustification_ConvertSubnetTxData) isSubnetValidatorRegistrationJustification_Preimage() { +} + +func (*SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage) isSubnetValidatorRegistrationJustification_Preimage() { +} + +type SubnetIDIndex struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SubnetId []byte `protobuf:"bytes,1,opt,name=subnet_id,json=subnetId,proto3" json:"subnet_id,omitempty"` + Index uint32 `protobuf:"varint,2,opt,name=index,proto3" json:"index,omitempty"` +} + +func (x *SubnetIDIndex) Reset() { + *x = SubnetIDIndex{} + if protoimpl.UnsafeEnabled { + mi := &file_platformvm_platformvm_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubnetIDIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubnetIDIndex) ProtoMessage() {} + +func (x *SubnetIDIndex) ProtoReflect() protoreflect.Message { + mi := &file_platformvm_platformvm_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubnetIDIndex.ProtoReflect.Descriptor instead. +func (*SubnetIDIndex) Descriptor() ([]byte, []int) { + return file_platformvm_platformvm_proto_rawDescGZIP(), []int{1} +} + +func (x *SubnetIDIndex) GetSubnetId() []byte { + if x != nil { + return x.SubnetId + } + return nil +} + +func (x *SubnetIDIndex) GetIndex() uint32 { + if x != nil { + return x.Index + } + return 0 +} + +var File_platformvm_platformvm_proto protoreflect.FileDescriptor + +var file_platformvm_platformvm_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x2f, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x22, 0xd5, 0x01, 0x0a, 0x28, 0x53, 0x75, + 0x62, 0x6e, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x75, 0x73, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72, + 0x74, 0x5f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x74, 0x78, 0x5f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, + 0x6d, 0x76, 0x6d, 0x2e, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x44, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x48, 0x00, 0x52, 0x13, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x53, 0x75, 0x62, 0x6e, + 0x65, 0x74, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x12, 0x4b, 0x0a, 0x21, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x1e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, + 0x75, 0x62, 0x6e, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x22, 0x42, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x44, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x76, 0x61, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x61, 0x76, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x68, 0x65, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, + 0x62, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_platformvm_platformvm_proto_rawDescOnce sync.Once + file_platformvm_platformvm_proto_rawDescData = file_platformvm_platformvm_proto_rawDesc +) + +func file_platformvm_platformvm_proto_rawDescGZIP() []byte { + file_platformvm_platformvm_proto_rawDescOnce.Do(func() { + file_platformvm_platformvm_proto_rawDescData = protoimpl.X.CompressGZIP(file_platformvm_platformvm_proto_rawDescData) + }) + return file_platformvm_platformvm_proto_rawDescData +} + +var file_platformvm_platformvm_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_platformvm_platformvm_proto_goTypes = []interface{}{ + (*SubnetValidatorRegistrationJustification)(nil), // 0: platformvm.SubnetValidatorRegistrationJustification + (*SubnetIDIndex)(nil), // 1: platformvm.SubnetIDIndex +} +var file_platformvm_platformvm_proto_depIdxs = []int32{ + 1, // 0: platformvm.SubnetValidatorRegistrationJustification.convert_subnet_tx_data:type_name -> platformvm.SubnetIDIndex + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_platformvm_platformvm_proto_init() } +func file_platformvm_platformvm_proto_init() { + if File_platformvm_platformvm_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_platformvm_platformvm_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubnetValidatorRegistrationJustification); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_platformvm_platformvm_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubnetIDIndex); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_platformvm_platformvm_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*SubnetValidatorRegistrationJustification_ConvertSubnetTxData)(nil), + (*SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_platformvm_platformvm_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_platformvm_platformvm_proto_goTypes, + DependencyIndexes: file_platformvm_platformvm_proto_depIdxs, + MessageInfos: file_platformvm_platformvm_proto_msgTypes, + }.Build() + File_platformvm_platformvm_proto = out.File + file_platformvm_platformvm_proto_rawDesc = nil + file_platformvm_platformvm_proto_goTypes = nil + file_platformvm_platformvm_proto_depIdxs = nil +} diff --git a/proto/platformvm/platformvm.proto b/proto/platformvm/platformvm.proto new file mode 100644 index 000000000000..f1688d16592c --- /dev/null +++ b/proto/platformvm/platformvm.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package platformvm; + +option go_package = "github.com/ava-labs/avalanchego/proto/pb/platformvm"; + +message SubnetValidatorRegistrationJustification { + oneof preimage { + // This should be set to obtain an attestation that a validator specified in + // a ConvertSubnetTx has been removed from the validator set. + SubnetIDIndex convert_subnet_tx_data = 1; + // This should be set to a RegisterSubnetValidatorMessage to obtain an + // attestation that a validator is not currently registered and can never be + // registered. This can be because the validator was successfully added and + // then later removed, or because the validator was never added and the + // registration expired. + bytes register_subnet_validator_message = 2; + } +} + +message SubnetIDIndex { + bytes subnet_id = 1; + uint32 index = 2; +} diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 38fff3dccd0c..74f129f3a58d 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/network/peer" "github.com/ava-labs/avalanchego/proto/pb/sdk" "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/tests" "github.com/ava-labs/avalanchego/tests/fixture/e2e" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils" @@ -41,6 +42,7 @@ import ( p2pmessage "github.com/ava-labs/avalanchego/message" p2psdk "github.com/ava-labs/avalanchego/network/p2p" p2ppb "github.com/ava-labs/avalanchego/proto/pb/p2p" + platformvmpb "github.com/ava-labs/avalanchego/proto/pb/platformvm" snowvalidators "github.com/ava-labs/avalanchego/snow/validators" platformvmvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" @@ -208,7 +210,7 @@ var _ = e2e.DescribePChain("[L1]", func() { address := []byte{} tc.By("issuing a ConvertSubnetTx", func() { - _, err := pWallet.IssueConvertSubnetTx( + tx, err := pWallet.IssueConvertSubnetTx( subnetID, chainID, address, @@ -223,6 +225,22 @@ var _ = e2e.DescribePChain("[L1]", func() { tc.WithDefaultContext(), ) require.NoError(err) + + tc.By("ensuring the genesis peer has accepted the tx at "+subnetGenesisNode.URI, func() { + var ( + client = platformvm.NewClient(subnetGenesisNode.URI) + txID = tx.ID() + ) + tc.Eventually( + func() bool { + _, err := client.GetTx(tc.DefaultContext(), txID) + return err == nil + }, + tests.DefaultTimeout, + e2e.DefaultPollingInterval, + "transaction not accepted", + ) + }) }) genesisValidationID := subnetID.Append(0) @@ -293,6 +311,36 @@ var _ = e2e.DescribePChain("[L1]", func() { sov, ) }) + + tc.By("fetching the subnet conversion attestation", func() { + unsignedSubnetConversion := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + nil, + must[*warpmessage.SubnetConversion](tc)(warpmessage.NewSubnetConversion( + expectedConversionID, + )).Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + registerSubnetValidatorRequest, err := wrapWarpSignatureRequest( + unsignedSubnetConversion, + subnetID[:], + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), registerSubnetValidatorRequest)) + }) + + tc.By("getting the signature response", func() { + signature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + require.True(bls.Verify(genesisNodePK, signature, unsignedSubnetConversion.Bytes())) + }) + }) }) advanceProposerVMPChainHeight := func() { @@ -371,12 +419,28 @@ var _ = e2e.DescribePChain("[L1]", func() { require.NoError(err) tc.By("issuing a RegisterSubnetValidatorTx", func() { - _, err := pWallet.IssueRegisterSubnetValidatorTx( + tx, err := pWallet.IssueRegisterSubnetValidatorTx( registerBalance, registerNodePoP.ProofOfPossession, registerSubnetValidator.Bytes(), ) require.NoError(err) + + tc.By("ensuring the genesis peer has accepted the tx at "+subnetGenesisNode.URI, func() { + var ( + client = platformvm.NewClient(subnetGenesisNode.URI) + txID = tx.ID() + ) + tc.Eventually( + func() bool { + _, err := client.GetTx(tc.DefaultContext(), txID) + return err == nil + }, + tests.DefaultTimeout, + e2e.DefaultPollingInterval, + "transaction not accepted", + ) + }) }) }) @@ -418,6 +482,37 @@ var _ = e2e.DescribePChain("[L1]", func() { sov, ) }) + + tc.By("fetching the validator registration attestation", func() { + unsignedSubnetValidatorRegistration := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + nil, + must[*warpmessage.SubnetValidatorRegistration](tc)(warpmessage.NewSubnetValidatorRegistration( + registerValidationID, + true, // registered + )).Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + subnetValidatorRegistrationRequest, err := wrapWarpSignatureRequest( + unsignedSubnetValidatorRegistration, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), subnetValidatorRegistrationRequest)) + }) + + tc.By("getting the signature response", func() { + signature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + require.True(bls.Verify(genesisNodePK, signature, unsignedSubnetValidatorRegistration.Bytes())) + }) + }) }) var nextNonce uint64 @@ -464,10 +559,26 @@ var _ = e2e.DescribePChain("[L1]", func() { require.NoError(err) tc.By("issuing a SetSubnetValidatorWeightTx", func() { - _, err := pWallet.IssueSetSubnetValidatorWeightTx( + tx, err := pWallet.IssueSetSubnetValidatorWeightTx( setSubnetValidatorWeight.Bytes(), ) require.NoError(err) + + tc.By("ensuring the genesis peer has accepted the tx at "+subnetGenesisNode.URI, func() { + var ( + client = platformvm.NewClient(subnetGenesisNode.URI) + txID = tx.ID() + ) + tc.Eventually( + func() bool { + _, err := client.GetTx(tc.DefaultContext(), txID) + return err == nil + }, + tests.DefaultTimeout, + e2e.DefaultPollingInterval, + "transaction not accepted", + ) + }) }) nextNonce++ @@ -515,6 +626,38 @@ var _ = e2e.DescribePChain("[L1]", func() { sov, ) }) + + tc.By("fetching the validator weight change attestation", func() { + unsignedSubnetValidatorWeight := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + nil, + must[*warpmessage.SubnetValidatorWeight](tc)(warpmessage.NewSubnetValidatorWeight( + registerValidationID, + nextNonce-1, // Use the prior nonce + updatedWeight, + )).Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + subnetValidatorRegistrationRequest, err := wrapWarpSignatureRequest( + unsignedSubnetValidatorWeight, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), subnetValidatorRegistrationRequest)) + }) + + tc.By("getting the signature response", func() { + signature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + require.True(bls.Verify(genesisNodePK, signature, unsignedSubnetValidatorWeight.Bytes())) + }) + }) }) tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) @@ -533,6 +676,45 @@ var _ = e2e.DescribePChain("[L1]", func() { }, }) }) + + tc.By("fetching the validator removal attestation", func() { + unsignedSubnetValidatorRegistration := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + nil, + must[*warpmessage.SubnetValidatorRegistration](tc)(warpmessage.NewSubnetValidatorRegistration( + registerValidationID, + false, // removed + )).Bytes(), + )).Bytes(), + )) + + justification := platformvmpb.SubnetValidatorRegistrationJustification{ + Preimage: &platformvmpb.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorMessage.Bytes(), + }, + } + justificationBytes, err := proto.Marshal(&justification) + require.NoError(err) + + tc.By("sending the request to sign the warp message", func() { + subnetValidatorRegistrationRequest, err := wrapWarpSignatureRequest( + unsignedSubnetValidatorRegistration, + justificationBytes, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), subnetValidatorRegistrationRequest)) + }) + + tc.By("getting the signature response", func() { + signature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + require.True(bls.Verify(genesisNodePK, signature, unsignedSubnetValidatorRegistration.Bytes())) + }) + }) }) genesisPeerMessages.Close() diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index 194116273a0c..a00227309810 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -166,6 +166,9 @@ func newEnvironment(t *testing.T, f upgradetest.Fork) *environment { //nolint:un res.mempool, res.backend.Config.PartialSyncPrimaryNetwork, res.sender, + &res.ctx.Lock, + res.state, + res.ctx.WarpSigner, registerer, config.DefaultNetwork, ) diff --git a/vms/platformvm/network/network.go b/vms/platformvm/network/network.go index aa95cd67888c..08e6c95ca3b8 100644 --- a/vms/platformvm/network/network.go +++ b/vms/platformvm/network/network.go @@ -6,6 +6,7 @@ package network import ( "context" "errors" + "sync" "time" "github.com/prometheus/client_golang/prometheus" @@ -13,13 +14,16 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/network/p2p/gossip" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" ) var errMempoolDisabledWithPartialSync = errors.New("mempool is disabled partial syncing") @@ -48,6 +52,9 @@ func New( mempool mempool.Mempool, partialSyncPrimaryNetwork bool, appSender common.AppSender, + stateLock sync.Locker, + state state.Chain, + signer warp.Signer, registerer prometheus.Registerer, config config.Network, ) (*Network, error) { @@ -157,6 +164,17 @@ func New( return nil, err } + // We allow all peers to request warp messaging signatures + signatureRequestVerifier := signatureRequestVerifier{ + stateLock: stateLock, + state: state, + } + signatureRequestHandler := acp118.NewHandler(signatureRequestVerifier, signer) + + if err := p2pNetwork.AddHandler(acp118.HandlerID, signatureRequestHandler); err != nil { + return nil, err + } + return &Network{ Network: p2pNetwork, log: log, diff --git a/vms/platformvm/network/network_test.go b/vms/platformvm/network/network_test.go index 2dc6e2852585..d7cac489f5d9 100644 --- a/vms/platformvm/network/network_test.go +++ b/vms/platformvm/network/network_test.go @@ -176,6 +176,9 @@ func TestNetworkIssueTxFromRPC(t *testing.T) { tt.mempoolFunc(ctrl), tt.partialSyncPrimaryNetwork, tt.appSenderFunc(ctrl), + nil, + nil, + nil, prometheus.NewRegistry(), testConfig, ) diff --git a/vms/platformvm/network/warp.go b/vms/platformvm/network/warp.go new file mode 100644 index 000000000000..7935eb7b6d0b --- /dev/null +++ b/vms/platformvm/network/warp.go @@ -0,0 +1,356 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package network + +import ( + "context" + "fmt" + "math" + "sync" + + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" + "github.com/ava-labs/avalanchego/proto/pb/platformvm" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const ( + ErrFailedToParseWarpAddressedCall = iota + 1 + ErrWarpAddressedCallHasSourceAddress + ErrFailedToParseWarpAddressedCallPayload + ErrUnsupportedWarpAddressedCallPayloadType + + ErrFailedToParseJustification + ErrConversionDoesNotExist + ErrMismatchedConversionID + + ErrInvalidJustificationType + ErrFailedToParseSubnetID + ErrMismatchedValidationID + ErrValidationDoesNotExist + ErrValidationExists + ErrFailedToParseRegisterSubnetValidator + ErrValidationCouldBeRegistered + + ErrImpossibleNonce + ErrWrongNonce + ErrWrongWeight +) + +var _ acp118.Verifier = (*signatureRequestVerifier)(nil) + +type signatureRequestVerifier struct { + stateLock sync.Locker + state state.Chain +} + +func (s signatureRequestVerifier) Verify( + _ context.Context, + unsignedMessage *warp.UnsignedMessage, + justification []byte, +) *common.AppError { + msg, err := payload.ParseAddressedCall(unsignedMessage.Payload) + if err != nil { + return &common.AppError{ + Code: ErrFailedToParseWarpAddressedCall, + Message: "failed to parse warp addressed call: " + err.Error(), + } + } + if len(msg.SourceAddress) != 0 { + return &common.AppError{ + Code: ErrWarpAddressedCallHasSourceAddress, + Message: "source address should be empty", + } + } + + payloadIntf, err := message.Parse(msg.Payload) + if err != nil { + return &common.AppError{ + Code: ErrFailedToParseWarpAddressedCallPayload, + Message: "failed to parse warp addressed call payload: " + err.Error(), + } + } + + switch payload := payloadIntf.(type) { + case *message.SubnetConversion: + return s.verifySubnetConversion(payload, justification) + case *message.SubnetValidatorRegistration: + return s.verifySubnetValidatorRegistration(payload, justification) + case *message.SubnetValidatorWeight: + return s.verifySubnetValidatorWeight(payload) + default: + return &common.AppError{ + Code: ErrUnsupportedWarpAddressedCallPayloadType, + Message: fmt.Sprintf("unsupported warp addressed call payload type: %T", payloadIntf), + } + } +} + +func (s signatureRequestVerifier) verifySubnetConversion( + msg *message.SubnetConversion, + justification []byte, +) *common.AppError { + subnetID, err := ids.ToID(justification) + if err != nil { + return &common.AppError{ + Code: ErrFailedToParseJustification, + Message: "failed to parse justification: " + err.Error(), + } + } + + s.stateLock.Lock() + defer s.stateLock.Unlock() + + conversion, err := s.state.GetSubnetConversion(subnetID) + if err == database.ErrNotFound { + return &common.AppError{ + Code: ErrConversionDoesNotExist, + Message: fmt.Sprintf("subnet %q has not been converted", subnetID), + } + } + if err != nil { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to get subnet conversionID: " + err.Error(), + } + } + + if msg.ID != conversion.ConversionID { + return &common.AppError{ + Code: ErrMismatchedConversionID, + Message: fmt.Sprintf("provided conversionID %q != expected conversionID %q", msg.ID, conversion.ConversionID), + } + } + + return nil +} + +func (s signatureRequestVerifier) verifySubnetValidatorRegistration( + msg *message.SubnetValidatorRegistration, + justificationBytes []byte, +) *common.AppError { + if msg.Registered { + return s.verifySubnetValidatorRegistered(msg.ValidationID) + } + + var justification platformvm.SubnetValidatorRegistrationJustification + if err := proto.Unmarshal(justificationBytes, &justification); err != nil { + return &common.AppError{ + Code: ErrFailedToParseJustification, + Message: "failed to parse justification: " + err.Error(), + } + } + + switch preimage := justification.GetPreimage().(type) { + case *platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData: + return s.verifySubnetValidatorNotCurrentlyRegistered(msg.ValidationID, preimage.ConvertSubnetTxData) + case *platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage: + return s.verifySubnetValidatorCanNotValidate(msg.ValidationID, preimage.RegisterSubnetValidatorMessage) + default: + return &common.AppError{ + Code: ErrInvalidJustificationType, + Message: fmt.Sprintf("invalid justification type: %T", justification.Preimage), + } + } +} + +// verifySubnetValidatorRegistered verifies that the validationID is currently a +// validator. +func (s signatureRequestVerifier) verifySubnetValidatorRegistered( + validationID ids.ID, +) *common.AppError { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + // Verify that the validator exists + _, err := s.state.GetSubnetOnlyValidator(validationID) + if err == database.ErrNotFound { + return &common.AppError{ + Code: ErrValidationDoesNotExist, + Message: fmt.Sprintf("validation %q does not exist", validationID), + } + } + if err != nil { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to get subnet only validator: " + err.Error(), + } + } + return nil +} + +// verifySubnetValidatorNotCurrentlyRegistered verifies that the validationID +// could only correspond to a validator from a ConvertSubnetTx and that it is +// not currently a validator. +func (s signatureRequestVerifier) verifySubnetValidatorNotCurrentlyRegistered( + validationID ids.ID, + justification *platformvm.SubnetIDIndex, +) *common.AppError { + subnetID, err := ids.ToID(justification.GetSubnetId()) + if err != nil { + return &common.AppError{ + Code: ErrFailedToParseSubnetID, + Message: "failed to parse subnetID: " + err.Error(), + } + } + + justificationID := subnetID.Append(justification.GetIndex()) + if validationID != justificationID { + return &common.AppError{ + Code: ErrMismatchedValidationID, + Message: fmt.Sprintf("validationID %q != justificationID %q", validationID, justificationID), + } + } + + s.stateLock.Lock() + defer s.stateLock.Unlock() + + // Verify that the provided subnetID has been converted. + _, err = s.state.GetSubnetConversion(subnetID) + if err == database.ErrNotFound { + return &common.AppError{ + Code: ErrConversionDoesNotExist, + Message: fmt.Sprintf("subnet %q has not been converted", subnetID), + } + } + if err != nil { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to get subnet conversionID: " + err.Error(), + } + } + + // Verify that the validator is not in the current state + _, err = s.state.GetSubnetOnlyValidator(validationID) + if err == nil { + return &common.AppError{ + Code: ErrValidationExists, + Message: fmt.Sprintf("validation %q exists", validationID), + } + } + if err != database.ErrNotFound { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to lookup subnet only validator: " + err.Error(), + } + } + + // Either the validator was removed or it was never registered as part of + // the subnet conversion. + return nil +} + +// verifySubnetValidatorCanNotValidate verifies that the validationID is not +// currently and can never become a validator. +func (s signatureRequestVerifier) verifySubnetValidatorCanNotValidate( + validationID ids.ID, + justificationBytes []byte, +) *common.AppError { + justification, err := message.ParseRegisterSubnetValidator(justificationBytes) + if err != nil { + return &common.AppError{ + Code: ErrFailedToParseRegisterSubnetValidator, + Message: "failed to parse RegisterSubnetValidator justification: " + err.Error(), + } + } + + justificationID := justification.ValidationID() + if validationID != justificationID { + return &common.AppError{ + Code: ErrMismatchedValidationID, + Message: fmt.Sprintf("validationID %q != justificationID %q", validationID, justificationID), + } + } + + s.stateLock.Lock() + defer s.stateLock.Unlock() + + // Verify that the validator does not currently exist + _, err = s.state.GetSubnetOnlyValidator(validationID) + if err == nil { + return &common.AppError{ + Code: ErrValidationExists, + Message: fmt.Sprintf("validation %q exists", validationID), + } + } + if err != database.ErrNotFound { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to lookup subnet only validator: " + err.Error(), + } + } + + currentTimeUnix := uint64(s.state.GetTimestamp().Unix()) + if justification.Expiry <= currentTimeUnix { + return nil // The expiry time has passed + } + + // If the validation ID was successfully registered and then removed, it can + // never be re-used again even if its expiry has not yet passed. + hasExpiry, err := s.state.HasExpiry(state.ExpiryEntry{ + Timestamp: justification.Expiry, + ValidationID: validationID, + }) + if err != nil { + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to lookup expiry: " + err.Error(), + } + } + if !hasExpiry { + return &common.AppError{ + Code: ErrValidationCouldBeRegistered, + Message: fmt.Sprintf("validation %q can be registered until %d", validationID, justification.Expiry), + } + } + + return nil // The validator has been removed +} + +func (s signatureRequestVerifier) verifySubnetValidatorWeight( + msg *message.SubnetValidatorWeight, +) *common.AppError { + if msg.Nonce == math.MaxUint64 { + return &common.AppError{ + Code: ErrImpossibleNonce, + Message: "impossible nonce", + } + } + + s.stateLock.Lock() + defer s.stateLock.Unlock() + + sov, err := s.state.GetSubnetOnlyValidator(msg.ValidationID) + switch { + case err == database.ErrNotFound: + return &common.AppError{ + Code: ErrValidationDoesNotExist, + Message: fmt.Sprintf("validation %q does not exist", msg.ValidationID), + } + case err != nil: + return &common.AppError{ + Code: common.ErrUndefined.Code, + Message: "failed to get subnet only validator: " + err.Error(), + } + case msg.Nonce+1 != sov.MinNonce: + return &common.AppError{ + Code: ErrWrongNonce, + Message: fmt.Sprintf("provided nonce %d != expected nonce (%d - 1)", msg.Nonce, sov.MinNonce), + } + case msg.Weight != sov.Weight: + return &common.AppError{ + Code: ErrWrongWeight, + Message: fmt.Sprintf("provided weight %d != expected weight %d", msg.Weight, sov.Weight), + } + default: + return nil // The nonce and weight are correct + } +} diff --git a/vms/platformvm/network/warp_test.go b/vms/platformvm/network/warp_test.go new file mode 100644 index 000000000000..59eba4947356 --- /dev/null +++ b/vms/platformvm/network/warp_test.go @@ -0,0 +1,647 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package network + +import ( + "context" + "encoding/hex" + "math" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/proto/pb/platformvm" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +func TestSignatureRequestVerify(t *testing.T) { + tests := []struct { + name string + payload []byte + expectedErr *common.AppError + }{ + { + name: "failed to parse warp addressed call", + payload: nil, + expectedErr: &common.AppError{ + Code: ErrFailedToParseWarpAddressedCall, + Message: "failed to parse warp addressed call: couldn't unpack codec version", + }, + }, + { + name: "warp addressed call has source address", + payload: must[*payload.AddressedCall](t)(payload.NewAddressedCall( + []byte{1}, + nil, + )).Bytes(), + expectedErr: &common.AppError{ + Code: ErrWarpAddressedCallHasSourceAddress, + Message: "source address should be empty", + }, + }, + { + name: "failed to parse warp addressed call payload", + payload: must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + nil, + )).Bytes(), + expectedErr: &common.AppError{ + Code: ErrFailedToParseWarpAddressedCallPayload, + Message: "failed to parse warp addressed call payload: couldn't unpack codec version", + }, + }, + { + name: "unsupported warp addressed call payload type", + payload: must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + ids.GenerateTestID(), + ids.GenerateTestNodeID(), + [bls.PublicKeyLen]byte{}, + rand.Uint64(), + message.PChainOwner{}, + message.PChainOwner{}, + rand.Uint64(), + )).Bytes(), + )).Bytes(), + expectedErr: &common.AppError{ + Code: ErrUnsupportedWarpAddressedCallPayloadType, + Message: "unsupported warp addressed call payload type: *message.RegisterSubnetValidator", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := signatureRequestVerifier{} + err := s.Verify( + context.Background(), + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + constants.UnitTestID, + constants.PlatformChainID, + test.payload, + )), + nil, + ) + require.Equal(t, test.expectedErr, err) + }) + } +} + +func TestSignatureRequestVerifySubnetConversion(t *testing.T) { + var ( + subnetID = ids.GenerateTestID() + conversion = state.SubnetConversion{ + ConversionID: ids.ID{1, 2, 3, 4, 5, 6, 7, 8}, + ChainID: ids.GenerateTestID(), + Addr: utils.RandomBytes(20), + } + state = statetest.New(t, statetest.Config{}) + s = signatureRequestVerifier{ + stateLock: &sync.Mutex{}, + state: state, + } + ) + + state.SetSubnetConversion(subnetID, conversion) + + tests := []struct { + name string + subnetID []byte + conversionID ids.ID + expectedErr *common.AppError + }{ + { + name: "failed to parse justification", + subnetID: nil, + expectedErr: &common.AppError{ + Code: ErrFailedToParseJustification, + Message: "failed to parse justification: invalid hash length: expected 32 bytes but got 0", + }, + }, + { + name: "conversion does not exist", + subnetID: ids.Empty[:], + expectedErr: &common.AppError{ + Code: ErrConversionDoesNotExist, + Message: `subnet "11111111111111111111111111111111LpoYY" has not been converted`, + }, + }, + { + name: "mismatched conversionID", + subnetID: subnetID[:], + expectedErr: &common.AppError{ + Code: ErrMismatchedConversionID, + Message: `provided conversionID "11111111111111111111111111111111LpoYY" != expected conversionID "SkB92YpWm4Jdy1AQvv4wMsUNbcoYBVZRqKkdz5yByq1bfdik"`, + }, + }, + { + name: "valid", + subnetID: subnetID[:], + conversionID: conversion.ConversionID, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := s.Verify( + context.Background(), + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + constants.UnitTestID, + constants.PlatformChainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + must[*message.SubnetConversion](t)(message.NewSubnetConversion( + test.conversionID, + )).Bytes(), + )).Bytes(), + )), + test.subnetID, + ) + require.Equal(t, test.expectedErr, err) + }) + } +} + +func TestSignatureRequestVerifySubnetValidatorRegistrationRegistered(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + var ( + sov = state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(sk)), + Weight: 1, + } + state = statetest.New(t, statetest.Config{}) + s = signatureRequestVerifier{ + stateLock: &sync.Mutex{}, + state: state, + } + ) + + require.NoError(t, state.PutSubnetOnlyValidator(sov)) + + tests := []struct { + name string + validationID ids.ID + expectedErr *common.AppError + }{ + { + name: "validation does not exist", + expectedErr: &common.AppError{ + Code: ErrValidationDoesNotExist, + Message: `validation "11111111111111111111111111111111LpoYY" does not exist`, + }, + }, + { + name: "validation exists", + validationID: sov.ValidationID, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := s.Verify( + context.Background(), + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + constants.UnitTestID, + constants.PlatformChainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + must[*message.SubnetValidatorRegistration](t)(message.NewSubnetValidatorRegistration( + test.validationID, + true, + )).Bytes(), + )).Bytes(), + )), + nil, + ) + require.Equal(t, test.expectedErr, err) + }) + } +} + +func TestSignatureRequestVerifySubnetValidatorRegistrationNotRegistered(t *testing.T) { + skBytes, err := hex.DecodeString("36a33c536d283dfa599d0a70839c67ded6c954e346c5e8e5b4794e2299907887") + require.NoError(t, err) + + sk, err := bls.SecretKeyFromBytes(skBytes) + require.NoError(t, err) + + var ( + convertedSubnetID = ids.ID{3} + unconvertedSubnetID = ids.ID{7} + nodeID0 = ids.NodeID{4} + nodeID1 = ids.NodeID{5} + nodeID2 = ids.NodeID{6} + nodeID3 = ids.NodeID{6} + pk = bls.PublicFromSecretKey(sk) + expiry = genesistest.DefaultValidatorStartTimeUnix + 1 + weight uint64 = 1 + ) + + registerSubnetValidatorToRegister, err := message.NewRegisterSubnetValidator( + convertedSubnetID, + nodeID0, + [bls.PublicKeyLen]byte(bls.PublicKeyToCompressedBytes(pk)), + expiry, + message.PChainOwner{}, + message.PChainOwner{}, + weight, + ) + require.NoError(t, err) + + registerSubnetValidatorNotToRegister, err := message.NewRegisterSubnetValidator( + convertedSubnetID, + nodeID1, + [bls.PublicKeyLen]byte(bls.PublicKeyToCompressedBytes(pk)), + expiry, + message.PChainOwner{}, + message.PChainOwner{}, + weight, + ) + require.NoError(t, err) + + registerSubnetValidatorExpired, err := message.NewRegisterSubnetValidator( + convertedSubnetID, + nodeID2, + [bls.PublicKeyLen]byte(bls.PublicKeyToCompressedBytes(pk)), + genesistest.DefaultValidatorStartTimeUnix, + message.PChainOwner{}, + message.PChainOwner{}, + weight, + ) + require.NoError(t, err) + + registerSubnetValidatorToMarkExpired, err := message.NewRegisterSubnetValidator( + convertedSubnetID, + nodeID3, + [bls.PublicKeyLen]byte(bls.PublicKeyToCompressedBytes(pk)), + expiry, + message.PChainOwner{}, + message.PChainOwner{}, + weight, + ) + require.NoError(t, err) + + var ( + conversion = state.SubnetConversion{} + registerSubnetValidatorValidationID = registerSubnetValidatorToRegister.ValidationID() + registerSubnetValidatorSoV = state.SubnetOnlyValidator{ + ValidationID: registerSubnetValidatorValidationID, + SubnetID: registerSubnetValidatorToRegister.SubnetID, + NodeID: ids.NodeID(registerSubnetValidatorToRegister.NodeID), + PublicKey: bls.PublicKeyToUncompressedBytes(pk), + Weight: registerSubnetValidatorToRegister.Weight, + } + convertSubnetValidatorValidationID = registerSubnetValidatorToRegister.SubnetID.Append(0) + convertSubnetValidatorSoV = state.SubnetOnlyValidator{ + ValidationID: convertSubnetValidatorValidationID, + SubnetID: registerSubnetValidatorToRegister.SubnetID, + NodeID: ids.GenerateTestNodeID(), + PublicKey: bls.PublicKeyToUncompressedBytes(pk), + Weight: registerSubnetValidatorToRegister.Weight, + } + expiryEntry = state.ExpiryEntry{ + Timestamp: expiry, + ValidationID: registerSubnetValidatorToMarkExpired.ValidationID(), + } + + state = statetest.New(t, statetest.Config{}) + s = signatureRequestVerifier{ + stateLock: &sync.Mutex{}, + state: state, + } + ) + + state.SetSubnetConversion(convertedSubnetID, conversion) + require.NoError(t, state.PutSubnetOnlyValidator(registerSubnetValidatorSoV)) + require.NoError(t, state.PutSubnetOnlyValidator(convertSubnetValidatorSoV)) + state.PutExpiry(expiryEntry) + + tests := []struct { + name string + validationID ids.ID + justification []byte + expectedErr *common.AppError + }{ + { + name: "failed to parse justification", + justification: []byte("invalid"), + expectedErr: &common.AppError{ + Code: ErrFailedToParseJustification, + Message: "failed to parse justification: proto: cannot parse invalid wire-format data", + }, + }, + { + name: "failed to parse subnetID", + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{}, + }, + )), + expectedErr: &common.AppError{ + Code: ErrFailedToParseSubnetID, + Message: "failed to parse subnetID: invalid hash length: expected 32 bytes but got 0", + }, + }, + { + name: "mismatched convert subnet validationID", + validationID: registerSubnetValidatorNotToRegister.ValidationID(), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{ + ConvertSubnetTxData: &platformvm.SubnetIDIndex{ + SubnetId: convertedSubnetID[:], + Index: 0, + }, + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrMismatchedValidationID, + Message: `validationID "2SZuDErFdUGmrQNHuTaFobL6DewfJr4tEKrdcgPNVc7PXYejGD" != justificationID "8XSRE5pasJjRvghBXQyBzDPF91ywXm8AZWZ6jo4522tbVuynN"`, + }, + }, + { + name: "convert subnet validation exists", + validationID: convertSubnetValidatorValidationID, + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{ + ConvertSubnetTxData: &platformvm.SubnetIDIndex{ + SubnetId: convertedSubnetID[:], + Index: 0, + }, + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrValidationExists, + Message: `validation "8XSRE5pasJjRvghBXQyBzDPF91ywXm8AZWZ6jo4522tbVuynN" exists`, + }, + }, + { + name: "conversion does not exist", + validationID: unconvertedSubnetID.Append(0), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{ + ConvertSubnetTxData: &platformvm.SubnetIDIndex{ + SubnetId: unconvertedSubnetID[:], + Index: 0, + }, + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrConversionDoesNotExist, + Message: `subnet "45oj4CqFViNHUtBxJ55TZfqaVAXFwMRMj2XkHVqUYjJYoTaEM" has not been converted`, + }, + }, + { + name: "valid convert subnet data", + validationID: convertedSubnetID.Append(1), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{ + ConvertSubnetTxData: &platformvm.SubnetIDIndex{ + SubnetId: convertedSubnetID[:], + Index: 1, + }, + }, + }, + )), + }, + { + name: "failed to parse register subnet validator", + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{}, + }, + )), + expectedErr: &common.AppError{ + Code: ErrFailedToParseRegisterSubnetValidator, + Message: "failed to parse RegisterSubnetValidator justification: couldn't unpack codec version", + }, + }, + { + name: "mismatched registration validationID", + validationID: registerSubnetValidatorValidationID, + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorNotToRegister.Bytes(), + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrMismatchedValidationID, + Message: `validationID "2UB8nmhSCDbhBBzXkpvjZYAu37nC7spNGQAbkVSeWVvbT8RNqS" != justificationID "2SZuDErFdUGmrQNHuTaFobL6DewfJr4tEKrdcgPNVc7PXYejGD"`, + }, + }, + { + name: "registration validation exists", + validationID: registerSubnetValidatorValidationID, + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorToRegister.Bytes(), + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrValidationExists, + Message: `validation "2UB8nmhSCDbhBBzXkpvjZYAu37nC7spNGQAbkVSeWVvbT8RNqS" exists`, + }, + }, + { + name: "valid expired registration", + validationID: registerSubnetValidatorExpired.ValidationID(), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorExpired.Bytes(), + }, + }, + )), + }, + { + name: "validation could be registered", + validationID: registerSubnetValidatorNotToRegister.ValidationID(), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorNotToRegister.Bytes(), + }, + }, + )), + expectedErr: &common.AppError{ + Code: ErrValidationCouldBeRegistered, + Message: `validation "2SZuDErFdUGmrQNHuTaFobL6DewfJr4tEKrdcgPNVc7PXYejGD" can be registered until 1607144401`, + }, + }, + { + name: "validation removed", + validationID: registerSubnetValidatorToMarkExpired.ValidationID(), + justification: must[[]byte](t)(proto.Marshal( + &platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidatorToMarkExpired.Bytes(), + }, + }, + )), + }, + { + name: "invalid justification type", + expectedErr: &common.AppError{ + Code: ErrInvalidJustificationType, + Message: "invalid justification type: ", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := s.Verify( + context.Background(), + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + constants.UnitTestID, + constants.PlatformChainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + must[*message.SubnetValidatorRegistration](t)(message.NewSubnetValidatorRegistration( + test.validationID, + false, + )).Bytes(), + )).Bytes(), + )), + test.justification, + ) + if err != nil { + // Replace use non-breaking spaces (U+00a0) with regular spaces + // (U+0020). This is needed because the proto library + // intentionally makes the error message unstable. + err.Message = strings.ReplaceAll(err.Message, " ", " ") + } + require.Equal(t, test.expectedErr, err) + }) + } +} + +func TestSignatureRequestVerifySubnetValidatorWeight(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + weight = 100 + nonce = 10 + ) + var ( + sov = state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(sk)), + Weight: weight, + MinNonce: nonce + 1, + } + + state = statetest.New(t, statetest.Config{}) + s = signatureRequestVerifier{ + stateLock: &sync.Mutex{}, + state: state, + } + ) + + require.NoError(t, state.PutSubnetOnlyValidator(sov)) + + tests := []struct { + name string + validationID ids.ID + nonce uint64 + weight uint64 + expectedErr *common.AppError + }{ + { + name: "impossible nonce", + nonce: math.MaxUint64, + expectedErr: &common.AppError{ + Code: ErrImpossibleNonce, + Message: "impossible nonce", + }, + }, + { + name: "validation does not exist", + expectedErr: &common.AppError{ + Code: ErrValidationDoesNotExist, + Message: `validation "11111111111111111111111111111111LpoYY" does not exist`, + }, + }, + { + name: "wrong nonce", + validationID: sov.ValidationID, + expectedErr: &common.AppError{ + Code: ErrWrongNonce, + Message: "provided nonce 0 != expected nonce (11 - 1)", + }, + }, + { + name: "wrong weight", + validationID: sov.ValidationID, + nonce: nonce, + expectedErr: &common.AppError{ + Code: ErrWrongWeight, + Message: "provided weight 0 != expected weight 100", + }, + }, + { + name: "valid", + validationID: sov.ValidationID, + nonce: nonce, + weight: weight, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := s.Verify( + context.Background(), + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + constants.UnitTestID, + constants.PlatformChainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + nil, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + test.validationID, + test.nonce, + test.weight, + )).Bytes(), + )).Bytes(), + )), + nil, + ) + require.Equal(t, test.expectedErr, err) + }) + } +} + +func must[T any](t require.TestingT) func(T, error) T { + return func(val T, err error) T { + require.NoError(t, err) + return val + } +} diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index d2dfd34e47a8..58afa8498ddd 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -816,6 +816,7 @@ func (s *state) GetExpiryIterator() (iterator.Iterator[ExpiryEntry], error) { ), nil } +// HasExpiry allows for concurrent reads. func (s *state) HasExpiry(entry ExpiryEntry) (bool, error) { if has, modified := s.expiryDiff.modified[entry]; modified { return has, nil @@ -859,6 +860,7 @@ func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { return weight, nil } +// GetSubnetOnlyValidator allows for concurrent reads. func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { if sov, modified := s.sovDiff.modified[validationID]; modified { if sov.isDeleted() { @@ -1047,6 +1049,7 @@ func (s *state) SetSubnetOwner(subnetID ids.ID, owner fx.Owner) { s.subnetOwners[subnetID] = owner } +// GetSubnetConversion allows for concurrent reads. func (s *state) GetSubnetConversion(subnetID ids.ID) (SubnetConversion, error) { if c, ok := s.subnetConversions[subnetID]; ok { return c, nil @@ -1267,6 +1270,7 @@ func (s *state) GetEtnaHeight() (uint64, error) { return database.GetUInt64(s.singletonDB, EtnaHeightKey) } +// GetTimestamp allows for concurrent reads. func (s *state) GetTimestamp() time.Time { return s.timestamp } diff --git a/vms/platformvm/vm.go b/vms/platformvm/vm.go index 15ad3362d388..90540203e8d9 100644 --- a/vms/platformvm/vm.go +++ b/vms/platformvm/vm.go @@ -193,6 +193,9 @@ func (vm *VM) Initialize( mempool, txExecutorBackend.Config.PartialSyncPrimaryNetwork, appSender, + chainCtx.Lock.RLocker(), + vm.state, + chainCtx.WarpSigner, registerer, execConfig.Network, ) diff --git a/vms/platformvm/warp/message/payload.go b/vms/platformvm/warp/message/payload.go index 6903cc22001d..1da0f71aa9b5 100644 --- a/vms/platformvm/warp/message/payload.go +++ b/vms/platformvm/warp/message/payload.go @@ -43,7 +43,7 @@ func Parse(bytes []byte) (Payload, error) { return p, nil } -func initialize(p Payload) error { +func Initialize(p Payload) error { bytes, err := Codec.Marshal(CodecVersion, &p) if err != nil { return fmt.Errorf("couldn't marshal %T payload: %w", p, err) diff --git a/vms/platformvm/warp/message/register_subnet_validator.go b/vms/platformvm/warp/message/register_subnet_validator.go index cf0b1cbcd569..ba4eb3aab168 100644 --- a/vms/platformvm/warp/message/register_subnet_validator.go +++ b/vms/platformvm/warp/message/register_subnet_validator.go @@ -99,7 +99,7 @@ func NewRegisterSubnetValidator( DisableOwner: disableOwner, Weight: weight, } - return msg, initialize(msg) + return msg, Initialize(msg) } // ParseRegisterSubnetValidator parses bytes into an initialized diff --git a/vms/platformvm/warp/message/subnet_conversion.go b/vms/platformvm/warp/message/subnet_conversion.go index a93354fbe446..f1af8e78e5ab 100644 --- a/vms/platformvm/warp/message/subnet_conversion.go +++ b/vms/platformvm/warp/message/subnet_conversion.go @@ -50,7 +50,7 @@ func NewSubnetConversion(id ids.ID) (*SubnetConversion, error) { msg := &SubnetConversion{ ID: id, } - return msg, initialize(msg) + return msg, Initialize(msg) } // ParseSubnetConversion parses bytes into an initialized SubnetConversion. diff --git a/vms/platformvm/warp/message/subnet_validator_registration.go b/vms/platformvm/warp/message/subnet_validator_registration.go index 2ab46c88dc3b..f0e1919b1e24 100644 --- a/vms/platformvm/warp/message/subnet_validator_registration.go +++ b/vms/platformvm/warp/message/subnet_validator_registration.go @@ -34,7 +34,7 @@ func NewSubnetValidatorRegistration( ValidationID: validationID, Registered: registered, } - return msg, initialize(msg) + return msg, Initialize(msg) } // ParseSubnetValidatorRegistration parses bytes into an initialized diff --git a/vms/platformvm/warp/message/subnet_validator_weight.go b/vms/platformvm/warp/message/subnet_validator_weight.go index dcfa6c5a16c0..5b94889a8f03 100644 --- a/vms/platformvm/warp/message/subnet_validator_weight.go +++ b/vms/platformvm/warp/message/subnet_validator_weight.go @@ -46,7 +46,7 @@ func NewSubnetValidatorWeight( Nonce: nonce, Weight: weight, } - return msg, initialize(msg) + return msg, Initialize(msg) } // ParseSubnetValidatorWeight parses bytes into an initialized diff --git a/wallet/subnet/primary/examples/sign-subnet-conversion/main.go b/wallet/subnet/primary/examples/sign-subnet-conversion/main.go new file mode 100644 index 000000000000..1d3fdd7a47de --- /dev/null +++ b/wallet/subnet/primary/examples/sign-subnet-conversion/main.go @@ -0,0 +1,119 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + p2pmessage "github.com/ava-labs/avalanchego/message" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +func main() { + uri := primary.LocalAPIURI + subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") + conversionID := ids.FromStringOrPanic("28tfqwucuoH7oWxmVYDVQ2C1ehdYecF5mzwNmX2t1dTu1S5vHE") + infoClient := info.NewClient(uri) + networkID, err := infoClient.GetNetworkID(context.Background()) + if err != nil { + log.Fatalf("failed to fetch network ID: %s\n", err) + } + + subnetConversion, err := warpmessage.NewSubnetConversion(conversionID) + if err != nil { + log.Fatalf("failed to create SubnetConversion message: %s\n", err) + } + + addressedCall, err := payload.NewAddressedCall( + nil, + subnetConversion.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + p, err := peer.StartTestPeer( + context.Background(), + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ), + networkID, + router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { + log.Printf("received %s: %s", msg.Op(), msg.Message()) + }), + ) + if err != nil { + log.Fatalf("failed to start peer: %s\n", err) + } + + mesageBuilder, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + compression.TypeZstd, + time.Hour, + ) + if err != nil { + log.Fatalf("failed to create message builder: %s\n", err) + } + + appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ + Message: unsignedWarp.Bytes(), + Justification: subnetID[:], + }) + if err != nil { + log.Fatalf("failed to marshal SignatureRequest: %s\n", err) + } + + appRequest, err := mesageBuilder.AppRequest( + constants.PlatformChainID, + 0, + time.Hour, + p2p.PrefixMessage( + p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), + appRequestPayload, + ), + ) + if err != nil { + log.Fatalf("failed to create AppRequest: %s\n", err) + } + + p.Send(context.Background(), appRequest) + + time.Sleep(5 * time.Second) + + p.StartClose() + err = p.AwaitClosed(context.Background()) + if err != nil { + log.Fatalf("failed to close peer: %s\n", err) + } +} diff --git a/wallet/subnet/primary/examples/sign-subnet-validator-registration/main.go b/wallet/subnet/primary/examples/sign-subnet-validator-registration/main.go new file mode 100644 index 000000000000..71524515ab9e --- /dev/null +++ b/wallet/subnet/primary/examples/sign-subnet-validator-registration/main.go @@ -0,0 +1,120 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + p2pmessage "github.com/ava-labs/avalanchego/message" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +func main() { + uri := primary.LocalAPIURI + validationID := ids.FromStringOrPanic("2DWCCiYb7xRTRHeKybkLY5ygRhZ1CWhtHgLuUCJBxktRnUYdCT") + infoClient := info.NewClient(uri) + networkID, err := infoClient.GetNetworkID(context.Background()) + if err != nil { + log.Fatalf("failed to fetch network ID: %s\n", err) + } + + subnetValidatorRegistration, err := warpmessage.NewSubnetValidatorRegistration( + validationID, + true, + ) + if err != nil { + log.Fatalf("failed to create SubnetValidatorRegistration message: %s\n", err) + } + + addressedCall, err := payload.NewAddressedCall( + nil, + subnetValidatorRegistration.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + p, err := peer.StartTestPeer( + context.Background(), + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ), + networkID, + router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { + log.Printf("received %s: %s", msg.Op(), msg.Message()) + }), + ) + if err != nil { + log.Fatalf("failed to start peer: %s\n", err) + } + + messageBuilder, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + compression.TypeZstd, + time.Hour, + ) + if err != nil { + log.Fatalf("failed to create message builder: %s\n", err) + } + + appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ + Message: unsignedWarp.Bytes(), + }) + if err != nil { + log.Fatalf("failed to marshal SignatureRequest: %s\n", err) + } + + appRequest, err := messageBuilder.AppRequest( + constants.PlatformChainID, + 0, + time.Hour, + p2p.PrefixMessage( + p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), + appRequestPayload, + ), + ) + if err != nil { + log.Fatalf("failed to create AppRequest: %s\n", err) + } + + p.Send(context.Background(), appRequest) + + time.Sleep(5 * time.Second) + + p.StartClose() + err = p.AwaitClosed(context.Background()) + if err != nil { + log.Fatalf("failed to close peer: %s\n", err) + } +} diff --git a/wallet/subnet/primary/examples/sign-subnet-validator-removal-genesis/main.go b/wallet/subnet/primary/examples/sign-subnet-validator-removal-genesis/main.go new file mode 100644 index 000000000000..7b4eb57e0d69 --- /dev/null +++ b/wallet/subnet/primary/examples/sign-subnet-validator-removal-genesis/main.go @@ -0,0 +1,137 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/platformvm" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + p2pmessage "github.com/ava-labs/avalanchego/message" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +func main() { + uri := primary.LocalAPIURI + subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") + validationIndex := uint32(0) + infoClient := info.NewClient(uri) + networkID, err := infoClient.GetNetworkID(context.Background()) + if err != nil { + log.Fatalf("failed to fetch network ID: %s\n", err) + } + + validationID := subnetID.Append(validationIndex) + subnetValidatorRegistration, err := warpmessage.NewSubnetValidatorRegistration( + validationID, + false, + ) + if err != nil { + log.Fatalf("failed to create SubnetValidatorRegistration message: %s\n", err) + } + + addressedCall, err := payload.NewAddressedCall( + nil, + subnetValidatorRegistration.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + justification := platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_ConvertSubnetTxData{ + ConvertSubnetTxData: &platformvm.SubnetIDIndex{ + SubnetId: subnetID[:], + Index: validationIndex, + }, + }, + } + justificationBytes, err := proto.Marshal(&justification) + if err != nil { + log.Fatalf("failed to create justification: %s\n", err) + } + + p, err := peer.StartTestPeer( + context.Background(), + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ), + networkID, + router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { + log.Printf("received %s: %s", msg.Op(), msg.Message()) + }), + ) + if err != nil { + log.Fatalf("failed to start peer: %s\n", err) + } + + messageBuilder, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + compression.TypeZstd, + time.Hour, + ) + if err != nil { + log.Fatalf("failed to create message builder: %s\n", err) + } + + appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ + Message: unsignedWarp.Bytes(), + Justification: justificationBytes, + }) + if err != nil { + log.Fatalf("failed to marshal SignatureRequest: %s\n", err) + } + + appRequest, err := messageBuilder.AppRequest( + constants.PlatformChainID, + 0, + time.Hour, + p2p.PrefixMessage( + p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), + appRequestPayload, + ), + ) + if err != nil { + log.Fatalf("failed to create AppRequest: %s\n", err) + } + + p.Send(context.Background(), appRequest) + + time.Sleep(5 * time.Second) + + p.StartClose() + err = p.AwaitClosed(context.Background()) + if err != nil { + log.Fatalf("failed to close peer: %s\n", err) + } +} diff --git a/wallet/subnet/primary/examples/sign-subnet-validator-removal-registration/main.go b/wallet/subnet/primary/examples/sign-subnet-validator-removal-registration/main.go new file mode 100644 index 000000000000..aab951eadab6 --- /dev/null +++ b/wallet/subnet/primary/examples/sign-subnet-validator-removal-registration/main.go @@ -0,0 +1,207 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/json" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/platformvm" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + p2pmessage "github.com/ava-labs/avalanchego/message" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +var registerSubnetValidatorJSON = []byte(`{ + "subnetID": "2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof", + "nodeID": "0x550f3c8f2ebd89e6a69adca196bea38a1b4d65bc", + "blsPublicKey": [ + 178, + 119, + 51, + 152, + 247, + 239, + 52, + 16, + 89, + 246, + 6, + 11, + 76, + 81, + 114, + 139, + 141, + 251, + 127, + 202, + 205, + 177, + 62, + 75, + 152, + 207, + 170, + 120, + 86, + 213, + 226, + 226, + 104, + 135, + 245, + 231, + 226, + 223, + 64, + 19, + 242, + 246, + 227, + 12, + 223, + 23, + 193, + 219 + ], + "expiry": 1728331617, + "remainingBalanceOwner": { + "threshold": 0, + "addresses": null + }, + "disableOwner": { + "threshold": 0, + "addresses": null + }, + "weight": 1 +}`) + +func main() { + uri := primary.LocalAPIURI + infoClient := info.NewClient(uri) + networkID, err := infoClient.GetNetworkID(context.Background()) + if err != nil { + log.Fatalf("failed to fetch network ID: %s\n", err) + } + + var registerSubnetValidator warpmessage.RegisterSubnetValidator + err = json.Unmarshal(registerSubnetValidatorJSON, ®isterSubnetValidator) + if err != nil { + log.Fatalf("failed to unmarshal RegisterSubnetValidator message: %s\n", err) + } + err = warpmessage.Initialize(®isterSubnetValidator) + if err != nil { + log.Fatalf("failed to initialize RegisterSubnetValidator message: %s\n", err) + } + + validationID := registerSubnetValidator.ValidationID() + subnetValidatorRegistration, err := warpmessage.NewSubnetValidatorRegistration( + validationID, + false, + ) + if err != nil { + log.Fatalf("failed to create SubnetValidatorRegistration message: %s\n", err) + } + + addressedCall, err := payload.NewAddressedCall( + nil, + subnetValidatorRegistration.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + justification := platformvm.SubnetValidatorRegistrationJustification{ + Preimage: &platformvm.SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage{ + RegisterSubnetValidatorMessage: registerSubnetValidator.Bytes(), + }, + } + justificationBytes, err := proto.Marshal(&justification) + if err != nil { + log.Fatalf("failed to create justification: %s\n", err) + } + + p, err := peer.StartTestPeer( + context.Background(), + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ), + networkID, + router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { + log.Printf("received %s: %s", msg.Op(), msg.Message()) + }), + ) + if err != nil { + log.Fatalf("failed to start peer: %s\n", err) + } + + mesageBuilder, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + compression.TypeZstd, + time.Hour, + ) + if err != nil { + log.Fatalf("failed to create message builder: %s\n", err) + } + + appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ + Message: unsignedWarp.Bytes(), + Justification: justificationBytes, + }) + if err != nil { + log.Fatalf("failed to marshal SignatureRequest: %s\n", err) + } + + appRequest, err := mesageBuilder.AppRequest( + constants.PlatformChainID, + 0, + time.Hour, + p2p.PrefixMessage( + p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), + appRequestPayload, + ), + ) + if err != nil { + log.Fatalf("failed to create AppRequest: %s\n", err) + } + + p.Send(context.Background(), appRequest) + + time.Sleep(5 * time.Second) + + p.StartClose() + err = p.AwaitClosed(context.Background()) + if err != nil { + log.Fatalf("failed to close peer: %s\n", err) + } +} diff --git a/wallet/subnet/primary/examples/sign-subnet-validator-weight/main.go b/wallet/subnet/primary/examples/sign-subnet-validator-weight/main.go new file mode 100644 index 000000000000..26a8dd1cfcb3 --- /dev/null +++ b/wallet/subnet/primary/examples/sign-subnet-validator-weight/main.go @@ -0,0 +1,127 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/json" + "log" + "net/netip" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" + "github.com/ava-labs/avalanchego/utils/compression" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + p2pmessage "github.com/ava-labs/avalanchego/message" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +var subnetValidatorWeightJSON = []byte(`{ + "validationID": "2Y3ZZZXxpzm46geqVuqFXeSFVbeKihgrfeXRDaiF4ds6R2N8M5", + "nonce": 1, + "weight": 2 +}`) + +func main() { + uri := primary.LocalAPIURI + infoClient := info.NewClient(uri) + networkID, err := infoClient.GetNetworkID(context.Background()) + if err != nil { + log.Fatalf("failed to fetch network ID: %s\n", err) + } + + var subnetValidatorWeight warpmessage.SubnetValidatorWeight + err = json.Unmarshal(subnetValidatorWeightJSON, &subnetValidatorWeight) + if err != nil { + log.Fatalf("failed to unmarshal SubnetValidatorWeight message: %s\n", err) + } + err = warpmessage.Initialize(&subnetValidatorWeight) + if err != nil { + log.Fatalf("failed to initialize SubnetValidatorWeight message: %s\n", err) + } + + addressedCall, err := payload.NewAddressedCall( + nil, + subnetValidatorWeight.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + networkID, + constants.PlatformChainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + p, err := peer.StartTestPeer( + context.Background(), + netip.AddrPortFrom( + netip.AddrFrom4([4]byte{127, 0, 0, 1}), + 9651, + ), + networkID, + router.InboundHandlerFunc(func(_ context.Context, msg p2pmessage.InboundMessage) { + log.Printf("received %s: %s", msg.Op(), msg.Message()) + }), + ) + if err != nil { + log.Fatalf("failed to start peer: %s\n", err) + } + + mesageBuilder, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + compression.TypeZstd, + time.Hour, + ) + if err != nil { + log.Fatalf("failed to create message builder: %s\n", err) + } + + appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ + Message: unsignedWarp.Bytes(), + }) + if err != nil { + log.Fatalf("failed to marshal SignatureRequest: %s\n", err) + } + + appRequest, err := mesageBuilder.AppRequest( + constants.PlatformChainID, + 0, + time.Hour, + p2p.PrefixMessage( + p2p.ProtocolPrefix(p2p.SignatureRequestHandlerID), + appRequestPayload, + ), + ) + if err != nil { + log.Fatalf("failed to create AppRequest: %s\n", err) + } + + p.Send(context.Background(), appRequest) + + time.Sleep(5 * time.Second) + + p.StartClose() + err = p.AwaitClosed(context.Background()) + if err != nil { + log.Fatalf("failed to close peer: %s\n", err) + } +}