diff --git a/Makefile b/Makefile index 8b704b4de..a467c6b63 100644 --- a/Makefile +++ b/Makefile @@ -616,8 +616,14 @@ acc_initialize_pubkeys: ## Make sure the account keeper has public keys for all .PHONY: acc_initialize_pubkeys_warn_message acc_initialize_pubkeys_warn_message: ## Print a warning message about the need to run `make acc_initialize_pubkeys` - @printf "!!!!!!!!! YOU MUST RUN THE FOLLOWING COMMAND ONCE FOR E2E TESTS TO WORK AFTER THE NETWORK HAS STARTED !!!!!!!!!\n"\ - "\t\tmake acc_initialize_pubkeys\n" + @echo "+----------------------------------------------------------------------------------+" + @echo "| |" + @echo "| IMPORTANT: Please run the following command once to initialize E2E tests |" + @echo "| after the network has started: |" + @echo "| make acc_initialize_pubkeys |" + @echo "| |" + @echo "+----------------------------------------------------------------------------------+" + ############## ### Claims ### diff --git a/api/poktroll/service/relay.pulsar.go b/api/poktroll/service/relay.pulsar.go index 58e673ebe..320fb76a4 100644 --- a/api/poktroll/service/relay.pulsar.go +++ b/api/poktroll/service/relay.pulsar.go @@ -4,6 +4,7 @@ package service import ( fmt "fmt" runtime "github.com/cosmos/cosmos-proto/runtime" + _ "github.com/cosmos/gogoproto/gogoproto" session "github.com/pokt-network/poktroll/api/poktroll/session" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoiface "google.golang.org/protobuf/runtime/protoiface" @@ -2781,56 +2782,58 @@ var file_poktroll_service_relay_proto_rawDesc = []byte{ 0x0a, 0x1c, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x1a, 0x1e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x6c, 0x0a, 0x05, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x30, 0x0a, 0x03, 0x72, 0x65, 0x71, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x03, 0x72, 0x65, 0x71, 0x12, 0x31, 0x0a, 0x03, 0x72, - 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x03, 0x72, 0x65, 0x73, 0x22, 0x7c, - 0x0a, 0x14, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, + 0x1a, 0x14, 0x67, 0x6f, 0x67, 0x6f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x67, 0x6f, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6c, 0x0a, 0x05, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x12, + 0x30, 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, + 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x03, 0x72, 0x65, + 0x71, 0x12, 0x31, 0x0a, 0x03, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, + 0x03, 0x72, 0x65, 0x73, 0x22, 0x7c, 0x0a, 0x14, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x0e, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x22, 0x6a, 0x0a, 0x0c, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x40, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x04, 0xc8, 0xde, 0x1f, 0x00, 0x52, 0x04, + 0x6d, 0x65, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x6c, + 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x41, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, + 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x04, 0xc8, 0xde, 0x1f, 0x00, 0x52, 0x04, 0x6d, 0x65, + 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x8e, 0x01, 0x0a, + 0x15, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x0e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, - 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1c, - 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x64, 0x0a, 0x0c, - 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6b, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x22, 0x66, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, - 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x8e, 0x01, 0x0a, 0x15, 0x52, - 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x0e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, - 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0d, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x2d, 0x0a, 0x12, - 0x73, 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, 0x6c, 0x69, - 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0xa6, 0x01, 0x0a, 0x14, - 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x42, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x21, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0xa2, 0x02, 0x03, 0x50, 0x53, 0x58, 0xaa, 0x02, 0x10, 0x50, 0x6f, - 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0xca, 0x02, - 0x10, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0xe2, 0x02, 0x1c, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x11, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x3a, 0x3a, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x2d, + 0x0a, 0x12, 0x73, 0x75, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, + 0x6c, 0x69, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0xa6, 0x01, + 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x21, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, + 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0xa2, 0x02, 0x03, 0x50, 0x53, 0x58, 0xaa, 0x02, 0x10, + 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0xca, 0x02, 0x10, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0xe2, 0x02, 0x1c, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x5c, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x11, 0x50, 0x6f, 0x6b, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x3a, 0x3a, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/e2e/tests/tokenomics.feature b/e2e/tests/tokenomics.feature index 5362550b0..09e6cd83b 100644 --- a/e2e/tests/tokenomics.feature +++ b/e2e/tests/tokenomics.feature @@ -6,7 +6,7 @@ Feature: Tokenomics Namespaces And an account exists for "supplier1" And an account exists for "app1" When the supplier "supplier1" has serviced a session with "20" relays for service "svc1" for application "app1" - And the user should wait for "5" seconds + # And the user should wait for "5" seconds # TODO_UPNEXT(@Olshansk, #359): Expand on the two expectations below after integrating the tokenomics module # into the supplier module. # Then the account balance of "supplier1" should be "1000" uPOKT "more" than before diff --git a/go.mod b/go.mod index b844f0cb9..61f9b82a4 100644 --- a/go.mod +++ b/go.mod @@ -277,3 +277,4 @@ require ( ) // replace github.com/cosmos/cosmos-sdk => github.com/rollkit/cosmos-sdk v0.50.1-rollkit-v0.11.19-no-fraud-proofs +replace github.com/pokt-network/smt => github.com/pokt-network/smt v0.9.3-0.20240321060129-e3dbbbd9f97d diff --git a/go.sum b/go.sum index f6f3a1e70..fa93e23d4 100644 --- a/go.sum +++ b/go.sum @@ -978,8 +978,8 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pokt-network/smt v0.9.2 h1:h/GnFm1F6mNBbF1hopr+9+y7nr173SU55NX7NxTVU0Y= -github.com/pokt-network/smt v0.9.2/go.mod h1:S4Ho4OPkK2v2vUCHNtA49XDjqUC/OFYpBbynRVYmxvA= +github.com/pokt-network/smt v0.9.3-0.20240321060129-e3dbbbd9f97d h1:6x7LiRWV+mHugWbJlGaYSWESEV+by8hGIbXb3/bWXOg= +github.com/pokt-network/smt v0.9.3-0.20240321060129-e3dbbbd9f97d/go.mod h1:S4Ho4OPkK2v2vUCHNtA49XDjqUC/OFYpBbynRVYmxvA= github.com/pokt-network/smt/kvstore/badger v0.0.0-20240109205447-868237978c0b h1:TjfgV3vgW0zW47Br/OgUXD4M8iyR74EYanbFfN4ed8o= github.com/pokt-network/smt/kvstore/badger v0.0.0-20240109205447-868237978c0b/go.mod h1:GbzcG5ebj8twKmBL1VzdPM4NS44okwYXBfQaVXT+6yU= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= diff --git a/pkg/client/interface.go b/pkg/client/interface.go index da18a8790..52b50e052 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -19,8 +19,8 @@ import ( comettypes "github.com/cometbft/cometbft/rpc/core/types" cosmosclient "github.com/cosmos/cosmos-sdk/client" cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" cosmostypes "github.com/cosmos/cosmos-sdk/types" - accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/pokt-network/smt" "github.com/pokt-network/poktroll/pkg/either" @@ -231,7 +231,10 @@ type SupplierClientOption func(SupplierClient) // on-chain account information type AccountQueryClient interface { // GetAccount queries the chain for the details of the account provided - GetAccount(ctx context.Context, address string) (accounttypes.AccountI, error) + GetAccount(ctx context.Context, address string) (cosmostypes.AccountI, error) + + // GetPubKeyFromAddress returns the public key of the given address. + GetPubKeyFromAddress(ctx context.Context, address string) (cryptotypes.PubKey, error) } // ApplicationQueryClient defines an interface that enables the querying of the diff --git a/pkg/client/query/accquerier.go b/pkg/client/query/accquerier.go index c61ec9760..7e84bdbde 100644 --- a/pkg/client/query/accquerier.go +++ b/pkg/client/query/accquerier.go @@ -4,6 +4,8 @@ import ( "context" "cosmossdk.io/depinject" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" grpc "github.com/cosmos/gogoproto/grpc" @@ -18,6 +20,10 @@ var _ client.AccountQueryClient = (*accQuerier)(nil) type accQuerier struct { clientConn grpc.ClientConn accountQuerier accounttypes.QueryClient + + // accountCache is a cache of accounts that have already been queried. + // TODO_TECHDEBT: Add a size limit to the cache and consider an LRU cache. + accountCache map[string]types.AccountI } // NewAccountQuerier returns a new instance of a client.AccountQueryClient by @@ -26,7 +32,7 @@ type accQuerier struct { // Required dependencies: // - clientCtx func NewAccountQuerier(deps depinject.Config) (client.AccountQueryClient, error) { - aq := &accQuerier{} + aq := &accQuerier{accountCache: make(map[string]types.AccountI)} if err := depinject.Inject( deps, @@ -44,15 +50,41 @@ func NewAccountQuerier(deps depinject.Config) (client.AccountQueryClient, error) func (aq *accQuerier) GetAccount( ctx context.Context, address string, -) (accounttypes.AccountI, error) { +) (types.AccountI, error) { + if foundAccount, isAccountFound := aq.accountCache[address]; isAccountFound { + return foundAccount, nil + } + + // Query the blockchain for the account record req := &accounttypes.QueryAccountRequest{Address: address} res, err := aq.accountQuerier.Account(ctx, req) if err != nil { return nil, ErrQueryAccountNotFound.Wrapf("address: %s [%v]", address, err) } - var acc accounttypes.AccountI - if err = queryCodec.UnpackAny(res.Account, &acc); err != nil { + + // Unpack and cache the account object + var fetchedAccount types.AccountI + if err = queryCodec.UnpackAny(res.Account, &fetchedAccount); err != nil { return nil, ErrQueryUnableToDeserializeAccount.Wrapf("address: %s [%v]", address, err) } - return acc, nil + aq.accountCache[address] = fetchedAccount + + return fetchedAccount, nil +} + +// GetPubKeyFromAddress returns the public key of the given address. +// It uses the accountQuerier to get the account and then returns its public key. +func (aq *accQuerier) GetPubKeyFromAddress(ctx context.Context, address string) (cryptotypes.PubKey, error) { + acc, err := aq.GetAccount(ctx, address) + if err != nil { + return nil, err + } + + // If the account's public key is nil, then return an error. + pubKey := acc.GetPubKey() + if pubKey == nil { + return nil, ErrQueryPubKeyNotFound + } + + return pubKey, nil } diff --git a/pkg/client/query/errors.go b/pkg/client/query/errors.go index 7d28cff84..352e56ce3 100644 --- a/pkg/client/query/errors.go +++ b/pkg/client/query/errors.go @@ -7,4 +7,5 @@ var ( ErrQueryAccountNotFound = sdkerrors.Register(codespace, 1, "account not found") ErrQueryUnableToDeserializeAccount = sdkerrors.Register(codespace, 2, "unable to deserialize account") ErrQueryRetrieveSession = sdkerrors.Register(codespace, 3, "error while trying to retrieve a session") + ErrQueryPubKeyNotFound = sdkerrors.Register(codespace, 4, "account pub key not found") ) diff --git a/pkg/client/supplier/client_test.go b/pkg/client/supplier/client_test.go index 03fc27567..a72542ce4 100644 --- a/pkg/client/supplier/client_test.go +++ b/pkg/client/supplier/client_test.go @@ -172,8 +172,11 @@ func TestSupplierClient_SubmitProof(t *testing.T) { kvStore, err := badger.NewKVStore("") require.NoError(t, err) + // Generating an ephemeral tree & spec just so we can submit + // a proof of the right size. tree := smt.NewSparseMerkleSumTrie(kvStore, sha256.New()) - proof, err := tree.ProveClosest([]byte{1}) + emptyPath := make([]byte, tree.PathHasherSize()) + proof, err := tree.ProveClosest(emptyPath) require.NoError(t, err) go func() { diff --git a/pkg/client/tx/client_test.go b/pkg/client/tx/client_test.go index 50849c6b6..d5aefbbfe 100644 --- a/pkg/client/tx/client_test.go +++ b/pkg/client/tx/client_test.go @@ -2,6 +2,7 @@ package tx_test import ( "context" + "crypto/sha256" "sync" "testing" "time" @@ -14,6 +15,7 @@ import ( cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" + "github.com/pokt-network/smt" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/pkg/client" @@ -401,8 +403,11 @@ func TestTxClient_SignAndBroadcast_Timeout(t *testing.T) { err, errCh := eitherErr.SyncOrAsyncError() require.NoError(t, err) + spec := smt.NoPrehashSpec(sha256.New(), true) + emptyBlockHash := make([]byte, spec.PathHasherSize()) + for i := 0; i < tx.DefaultCommitTimeoutHeightOffset; i++ { - blocksPublishCh <- testblock.NewAnyTimesBlock(t, []byte{}, int64(i+1)) + blocksPublishCh <- testblock.NewAnyTimesBlock(t, emptyBlockHash, int64(i+1)) } // Assert that we receive the expected error type & message. diff --git a/pkg/crypto/interface.go b/pkg/crypto/interface.go index 4298a9b6a..a2e35931d 100644 --- a/pkg/crypto/interface.go +++ b/pkg/crypto/interface.go @@ -4,7 +4,10 @@ package crypto import ( "context" - "github.com/noot/ring-go" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + ring "github.com/noot/ring-go" + + "github.com/pokt-network/poktroll/x/service/types" ) // RingCache is used to store rings used for signing and verifying relay requests. @@ -12,19 +15,38 @@ import ( // the addresses of the gateways the application is delegated to, and converting // them into their corresponding public key points on the secp256k1 curve. type RingCache interface { + RingClient + + // GetCachedAddresses returns the addresses of the applications that are + // currently cached in the ring cache. + GetCachedAddresses() []string // Start starts the ring cache, it takes a cancellable context and, in a // separate goroutine, listens for on-chain delegation events and invalidates // the cache if the redelegation event's AppAddress is stored in the cache. Start(ctx context.Context) - // GetCachedAddresses returns the addresses of the applications that are - // currently cached in the ring cache. - GetCachedAddresses() []string - // GetRingForAddress returns the ring for the given application address if - // it exists. If it does not exist in the cache, it follows a lazy approach - // of querying the on-chain state and creating it just-in-time, caching for - // future retrievals. - GetRingForAddress(ctx context.Context, appAddress string) (*ring.Ring, error) // Stop stops the ring cache by unsubscribing from on-chain delegation events. // And clears the cache, so that it no longer contains any rings, Stop() } + +// RingClient is used to construct rings by querying the application module for +// the addresses of the gateways the application delegated to, and converting +// them into their corresponding public key points on the secp256k1 curve. +type RingClient interface { + // GetRingForAddress returns the ring for the given application address if + // it exists. + GetRingForAddress(ctx context.Context, appAddress string) (*ring.Ring, error) + + // VerifyRelayRequestSignature verifies the relay request signature against + // the ring for the application address in the relay request. + VerifyRelayRequestSignature(ctx context.Context, relayRequest *types.RelayRequest) error +} + +// PubKeyClient is used to get the public key given an address. +// On-chain and off-chain implementations should take care of retrieving the +// address' account and returning its public key. +type PubKeyClient interface { + // GetPubKeyFromAddress returns the public key of the given account address + // if it exists. + GetPubKeyFromAddress(ctx context.Context, address string) (cryptotypes.PubKey, error) +} diff --git a/pkg/crypto/rings/cache.go b/pkg/crypto/rings/cache.go index e1a21ed50..4817832d4 100644 --- a/pkg/crypto/rings/cache.go +++ b/pkg/crypto/rings/cache.go @@ -2,19 +2,16 @@ package rings import ( "context" - "fmt" "sync" "cosmossdk.io/depinject" - ring_secp256k1 "github.com/athanorlabs/go-dleq/secp256k1" - ringtypes "github.com/athanorlabs/go-dleq/types" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - "github.com/noot/ring-go" + ring "github.com/noot/ring-go" "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/pkg/crypto" "github.com/pokt-network/poktroll/pkg/observable/channel" "github.com/pokt-network/poktroll/pkg/polylog" + "github.com/pokt-network/poktroll/x/service/types" ) var _ crypto.RingCache = (*ringCache)(nil) @@ -23,23 +20,17 @@ type ringCache struct { // logger is the logger for the ring cache. logger polylog.Logger - // ringPointsCache maintains a map of application addresses to the points - // on the secp256k1 curve that correspond to the public keys of the gateways - // the application is delegated to. These are used to build the app's ring. - ringPointsCache map[string][]ringtypes.Point - ringPointsMu *sync.RWMutex + // ringsByAddr maintains a map from app addresses to the ring composed of + // the public keys of both the application and its delegated gateways. + ringsByAddr map[string]*ring.Ring + ringsByAddrMu *sync.RWMutex // delegationClient is used to listen for on-chain delegation events and - // invalidate cache entries for rings that have been updated on chain. + // invalidate entries in ringsByAddr if an associated updated has been made. delegationClient client.DelegationClient - // applicationQuerier is the querier for the application module, and is - // used to get the addresses of the gateways an application is delegated to. - applicationQuerier client.ApplicationQueryClient - - // accountQuerier is the querier for the account module, and is used to get - // the public keys of the application and its delegated gateways. - accountQuerier client.AccountQueryClient + // ringClient is used to retrieve cached rings and verify relay requests. + ringClient crypto.RingClient } // NewRingCache returns a new RingCache instance. It requires a depinject.Config @@ -50,10 +41,10 @@ type ringCache struct { // - client.DelegationClient // - client.ApplicationQueryClient // - client.AccountQueryClient -func NewRingCache(deps depinject.Config) (crypto.RingCache, error) { +func NewRingCache(deps depinject.Config) (_ crypto.RingCache, err error) { rc := &ringCache{ - ringPointsCache: make(map[string][]ringtypes.Point), - ringPointsMu: &sync.RWMutex{}, + ringsByAddr: make(map[string]*ring.Ring), + ringsByAddrMu: &sync.RWMutex{}, } // Supply the account and application queriers to the RingCache. @@ -61,32 +52,37 @@ func NewRingCache(deps depinject.Config) (crypto.RingCache, error) { deps, &rc.logger, &rc.delegationClient, - &rc.applicationQuerier, - &rc.accountQuerier, ); err != nil { return nil, err } + // Construct and assign underlying ring client. + rc.ringClient, err = NewRingClient(deps) + if err != nil { + return nil, err + } + return rc, nil } // Start starts the ring cache by subscribing to on-chain redelegation events. func (rc *ringCache) Start(ctx context.Context) { rc.logger.Info().Msg("starting ring cache") - // Listen for redelegation events and invalidate the cache if the - // redelegation event's address is stored in the cache. + // Stop the ringCache when the context is cancelled. go func() { select { case <-ctx.Done(): - // Stop the ring cache if the context is cancelled. rc.Stop() } }() + // Listen for redelegation events and invalidate the cache if it contains an + // address corresponding to the redelegation event's. go rc.goInvalidateCache(ctx) } // goInvalidateCache listens for redelegation events and invalidates the -// cache if the app address in the redelegation event is stored in the cache. +// cache if the ring corresponding to the app address in the redelegation event +// exists in the cache. // This function is intended to be run in a goroutine. func (rc *ringCache) goInvalidateCache(ctx context.Context) { // Get the latest redelegation replay observable. @@ -96,26 +92,27 @@ func (rc *ringCache) goInvalidateCache(ctx context.Context) { channel.ForEach[client.Redelegation]( ctx, redelegationObs, func(ctx context.Context, redelegation client.Redelegation) { - // Lock the cache for writing. - rc.ringPointsMu.Lock() - defer rc.ringPointsMu.Unlock() + // Lock ringsByAddr for writing. + rc.ringsByAddrMu.Lock() + defer rc.ringsByAddrMu.Unlock() // Check if the redelegation event's app address is in the cache. - if _, ok := rc.ringPointsCache[redelegation.GetAppAddress()]; ok { + if _, ok := rc.ringsByAddr[redelegation.GetAppAddress()]; ok { rc.logger.Debug(). Str("app_address", redelegation.GetAppAddress()). - Msg("redelegation event received; invalidating cache entry") - // Invalidate the cache entry. - delete(rc.ringPointsCache, redelegation.GetAppAddress()) + Msg("redelegation event received; invalidating ringsByAddr entry") + // Invalidate the ringsByAddr entry. + delete(rc.ringsByAddr, redelegation.GetAppAddress()) } }) } -// Stop stops the ring cache by unsubscribing from on-chain redelegation events. +// Stop stops the ring cache by unsubscribing from on-chain redelegation events +// and clears any existing entries. func (rc *ringCache) Stop() { // Clear the cache. - rc.ringPointsMu.Lock() - rc.ringPointsCache = make(map[string][]ringtypes.Point) - rc.ringPointsMu.Unlock() + rc.ringsByAddrMu.Lock() + rc.ringsByAddr = make(map[string]*ring.Ring) + rc.ringsByAddrMu.Unlock() // Close the delegation client. rc.delegationClient.Close() } @@ -123,13 +120,14 @@ func (rc *ringCache) Stop() { // GetCachedAddresses returns the addresses of the applications that are // currently cached in the ring cache. func (rc *ringCache) GetCachedAddresses() []string { - rc.ringPointsMu.RLock() - defer rc.ringPointsMu.RUnlock() - keys := make([]string, 0, len(rc.ringPointsCache)) - for k := range rc.ringPointsCache { - keys = append(keys, k) + rc.ringsByAddrMu.RLock() + defer rc.ringsByAddrMu.RUnlock() + + appAddresses := make([]string, 0, len(rc.ringsByAddr)) + for appAddr := range rc.ringsByAddr { + appAddresses = append(appAddresses, appAddr) } - return keys + return appAddresses } // GetRingForAddress returns the ring for the address provided. If it does not @@ -139,140 +137,41 @@ func (rc *ringCache) GetCachedAddresses() []string { func (rc *ringCache) GetRingForAddress( ctx context.Context, appAddress string, -) (*ring.Ring, error) { - var ( - ring *ring.Ring - err error - ) +) (ring *ring.Ring, err error) { + rc.ringsByAddrMu.Lock() + defer rc.ringsByAddrMu.Unlock() - // Lock the cache for reading. - rc.ringPointsMu.RLock() // Check if the ring is in the cache. - points, ok := rc.ringPointsCache[appAddress] - // Unlock the cache in case it was not cached. - rc.ringPointsMu.RUnlock() + ring, ok := rc.ringsByAddr[appAddress] - if !ok { - // If the ring is not in the cache, get it from the application module. - rc.logger.Debug(). - Str("app_address", appAddress). - Msg("ring cache miss; fetching from application module") - ring, err = rc.getRingForAppAddress(ctx, appAddress) - } else { - // If the ring is in the cache, create it from the points. + // Use the existing ring if it's cached. + if ok { rc.logger.Debug(). Str("app_address", appAddress). - Msg("ring cache hit; creating from points") - ring, err = newRingFromPoints(points) - } - if err != nil { - return nil, err - } - - // Return the ring. - return ring, nil -} + Msg("ring cache hit; using cached ring") -// getRingForAppAddress returns the RingSinger used to sign relays. It does so by fetching -// the latest information from the application module and creating the correct ring. -// This method also caches the ring's public keys for future use. -func (rc *ringCache) getRingForAppAddress( - ctx context.Context, - appAddress string, -) (*ring.Ring, error) { - points, err := rc.getDelegatedPubKeysForAddress(ctx, appAddress) - if err != nil { - return nil, err + return ring, nil } - // Cache the ring's points for future use + + // If the ring is not in the cache, get it from the ring client. rc.logger.Debug(). Str("app_address", appAddress). - Msg("updating ring cache for app") - rc.ringPointsMu.Lock() - defer rc.ringPointsMu.Unlock() - rc.ringPointsCache[appAddress] = points - return newRingFromPoints(points) -} - -// newRingFromPoints creates a new ring from points on the secp256k1 curve -func newRingFromPoints(points []ringtypes.Point) (*ring.Ring, error) { - return ring.NewFixedKeyRingFromPublicKeys(ring_secp256k1.NewCurve(), points) -} - -// getDelegatedPubKeysForAddress returns the ring used to sign a message for -// the given application address, by querying the application module for it's -// delegated pubkeys and converting them to points on the secp256k1 curve in -// order to create the ring. -func (rc *ringCache) getDelegatedPubKeysForAddress( - ctx context.Context, - appAddress string, -) ([]ringtypes.Point, error) { - rc.ringPointsMu.Lock() - defer rc.ringPointsMu.Unlock() + Msg("ring cache miss; fetching from application module") - // Get the application's on chain state. - app, err := rc.applicationQuerier.GetApplication(ctx, appAddress) + ring, err = rc.ringClient.GetRingForAddress(ctx, appAddress) if err != nil { return nil, err } + rc.ringsByAddr[appAddress] = ring - // Create a slice of addresses for the ring. - ringAddresses := make([]string, 0) - ringAddresses = append(ringAddresses, appAddress) // app address is index 0 - if len(app.DelegateeGatewayAddresses) == 0 { - // add app address twice to make the ring size of mininmum 2 - // TODO_HACK: We are adding the appAddress twice because a ring - // signature requires AT LEAST two pubKeys. When the Application has - // not delegated to any gateways, we add the application's own address - // twice. This is a HACK and should be investigated as to what is the - // best approach to take in this situation. - ringAddresses = append(ringAddresses, appAddress) - } else { - // add the delegatee gateway addresses - ringAddresses = append(ringAddresses, app.DelegateeGatewayAddresses...) - } - - // Get the points on the secp256k1 curve for the addresses. - points, err := rc.addressesToPoints(ctx, ringAddresses) - if err != nil { - return nil, err - } - - // Return the public key points on the secp256k1 curve. - return points, nil + return ring, nil } -// addressesToPoints converts a slice of addresses to a slice of points on the -// secp256k1 curve, by querying the account module for the public key for each -// address and converting them to the corresponding points on the secp256k1 curve -func (rc *ringCache) addressesToPoints( +// VerifyRelayRequestSignature verifies the relay request signature against the +// ring for the application address in the relay request. +func (rc *ringCache) VerifyRelayRequestSignature( ctx context.Context, - addresses []string, -) ([]ringtypes.Point, error) { - curve := ring_secp256k1.NewCurve() - points := make([]ringtypes.Point, len(addresses)) - rc.logger.Debug(). - // TODO_TECHDEBT: implement and use `polylog.Event#Strs([]string)` instead of formatting here. - Str("addresses", fmt.Sprintf("%v", addresses)). - Msg("converting addresses to points") - for i, addr := range addresses { - // Retrieve the account from the auth module - acc, err := rc.accountQuerier.GetAccount(ctx, addr) - if err != nil { - return nil, err - } - key := acc.GetPubKey() - // Check if the key is a secp256k1 public key - if _, ok := key.(*secp256k1.PubKey); !ok { - return nil, ErrRingsNotSecp256k1Curve.Wrapf("got %T", key) - } - // Convert the public key to the point on the secp256k1 curve - point, err := curve.DecodeToPoint(key.Bytes()) - if err != nil { - return nil, err - } - // Insert the point into the slice of points - points[i] = point - } - return points, nil + relayRequest *types.RelayRequest, +) error { + return rc.ringClient.VerifyRelayRequestSignature(ctx, relayRequest) } diff --git a/pkg/crypto/rings/client.go b/pkg/crypto/rings/client.go new file mode 100644 index 000000000..fd1106327 --- /dev/null +++ b/pkg/crypto/rings/client.go @@ -0,0 +1,196 @@ +package rings + +import ( + "context" + "fmt" + + "cosmossdk.io/depinject" + ring_secp256k1 "github.com/athanorlabs/go-dleq/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + ring "github.com/noot/ring-go" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/crypto" + "github.com/pokt-network/poktroll/pkg/polylog" + "github.com/pokt-network/poktroll/x/service/types" +) + +var _ crypto.RingClient = (*ringClient)(nil) + +// ringClient is an implementation of the RingClient interface that uses the +// client.ApplicationQueryClient to get application's delegation information +// needed to construct the ring for signing relay requests. +type ringClient struct { + logger polylog.Logger + + // applicationQuerier is the querier for the application module, and is + // used to get the gateways an application is delegated to. + applicationQuerier client.ApplicationQueryClient + + // accountQuerier is used to fetch accounts for a given an account address. + accountQuerier client.AccountQueryClient +} + +// NewRingClient returns a new ring client constructed from the given dependencies. +// It returns an error if the required dependencies are not supplied. +// +// Required dependencies: +// - polylog.Logger +// - client.ApplicationQueryClient +// - client.AccountQueryClient +func NewRingClient(deps depinject.Config) (_ crypto.RingClient, err error) { + rc := new(ringClient) + + if err := depinject.Inject( + deps, + &rc.logger, + &rc.accountQuerier, + &rc.applicationQuerier, + ); err != nil { + return nil, err + } + + return rc, nil +} + +// GetRingForAddress returns the ring for the address provided. +// The ring is created by querying for the application's and its delegated +// gateways public keys. These keys are converted to secp256k1 curve points +// before forming the ring. +func (rc *ringClient) GetRingForAddress( + ctx context.Context, + appAddress string, +) (*ring.Ring, error) { + pubKeys, err := rc.getDelegatedPubKeysForAddress(ctx, appAddress) + if err != nil { + return nil, err + } + + // Get the points on the secp256k1 curve for the public keys in the ring. + points, err := pointsFromPublicKeys(pubKeys...) + if err != nil { + return nil, err + } + + // Return the ring the constructed from the points retrieved above. + return newRingFromPoints(points) +} + +// VerifyRelayRequestSignature verifies the signature of the relay request +// provided against the corresponding ring for the application address in +// the same request. +func (rc *ringClient) VerifyRelayRequestSignature( + ctx context.Context, + relayRequest *types.RelayRequest, +) error { + relayRequestMeta := relayRequest.GetMeta() + + sessionHeader := relayRequestMeta.GetSessionHeader() + if err := sessionHeader.ValidateBasic(); err != nil { + return ErrRingClientInvalidRelayRequest.Wrapf("invalid session header: %v", err) + } + + rc.logger.Debug(). + Fields(map[string]any{ + "session_id": sessionHeader.GetSessionId(), + "application_address": sessionHeader.GetApplicationAddress(), + "service_id": sessionHeader.GetService().GetId(), + }). + Msg("verifying relay request signature") + + // Extract the relay request's ring signature. + signature := relayRequestMeta.GetSignature() + if signature == nil { + return ErrRingClientInvalidRelayRequest.Wrap("missing signature from relay request") + } + + // Deserialize the request signature bytes back into a ring signature. + relayRequestRingSig := new(ring.RingSig) + if err := relayRequestRingSig.Deserialize(ring_secp256k1.NewCurve(), signature); err != nil { + return ErrRingClientInvalidRelayRequestSignature.Wrapf( + "error deserializing ring signature: %s", err, + ) + } + + // Get the ring for the application address of the relay request. + appAddress := sessionHeader.GetApplicationAddress() + expectedAppRing, err := rc.GetRingForAddress(ctx, appAddress) + if err != nil { + return ErrRingClientInvalidRelayRequest.Wrapf( + "error getting ring for application address %s: %v", appAddress, err, + ) + } + + // Compare the expected ring signature against the one provided in the relay request. + if !relayRequestRingSig.Ring().Equals(expectedAppRing) { + return ErrRingClientInvalidRelayRequestSignature.Wrapf( + "ring signature in the relay request does not match the expected one for the app %s", appAddress, + ) + } + + // Get and hash the signable bytes of the relay request. + requestSignableBz, err := relayRequest.GetSignableBytesHash() + if err != nil { + return ErrRingClientInvalidRelayRequest.Wrapf("error getting relay request signable bytes: %v", err) + } + + // Verify the relay request's signature. + if valid := relayRequestRingSig.Verify(requestSignableBz); !valid { + return ErrRingClientInvalidRelayRequestSignature.Wrapf("invalid relay request signature or bytes") + } + + return nil +} + +// getDelegatedPubKeysForAddress returns the gateway public keys an application +// delegated the ability to sign relay requests on its behalf. +func (rc *ringClient) getDelegatedPubKeysForAddress( + ctx context.Context, + appAddress string, +) ([]cryptotypes.PubKey, error) { + // Get the application's on chain state. + app, err := rc.applicationQuerier.GetApplication(ctx, appAddress) + if err != nil { + return nil, err + } + + // Create a slice of addresses for the ring. + ringAddresses := make([]string, 0) + ringAddresses = append(ringAddresses, appAddress) // app address is index 0 + if len(app.DelegateeGatewayAddresses) == 0 { + // add app address twice to make the ring size of minimum 2 + // TODO_IMPROVE: The appAddress is added twice because a ring signature + // requires AT LEAST two pubKeys. If the Application has not delegated + // to any gateways, the app's own address needs to be used twice to + // create a ring. This is not a huge issue but an improvement should + // be investigated in the future. + ringAddresses = append(ringAddresses, appAddress) + } else { + // add the delegatee gateway addresses + ringAddresses = append(ringAddresses, app.DelegateeGatewayAddresses...) + } + + rc.logger.Debug(). + // TODO_TECHDEBT: implement and use `polylog.Event#Strs([]string)` + Str("addresses", fmt.Sprintf("%v", ringAddresses)). + Msg("converting addresses to points") + + return rc.addressesToPubKeys(ctx, ringAddresses) +} + +// addressesToPubKeys queries for and returns the public keys for the addresses +// provided. +func (rc *ringClient) addressesToPubKeys( + ctx context.Context, + addresses []string, +) ([]cryptotypes.PubKey, error) { + pubKeys := make([]cryptotypes.PubKey, len(addresses)) + for i, addr := range addresses { + acc, err := rc.accountQuerier.GetPubKeyFromAddress(ctx, addr) + if err != nil { + return nil, err + } + pubKeys[i] = acc + } + return pubKeys, nil +} diff --git a/pkg/crypto/rings/errors.go b/pkg/crypto/rings/errors.go index cd85b519b..2c51776bd 100644 --- a/pkg/crypto/rings/errors.go +++ b/pkg/crypto/rings/errors.go @@ -1,8 +1,13 @@ package rings -import sdkerrors "cosmossdk.io/errors" +import ( + sdkerrors "cosmossdk.io/errors" +) var ( - codespace = "rings" - ErrRingsNotSecp256k1Curve = sdkerrors.Register(codespace, 1, "key is not a secp256k1 public key") + codespace = "rings" + ErrRingsNotSecp256k1Curve = sdkerrors.Register(codespace, 1, "key is not a secp256k1 public key") + ErrRingClientEmptyRelayRequestSignature = sdkerrors.Register(codespace, 2, "empty relay request signature") + ErrRingClientInvalidRelayRequest = sdkerrors.Register(codespace, 3, "invalid relay request") + ErrRingClientInvalidRelayRequestSignature = sdkerrors.Register(codespace, 4, "invalid relay request signature") ) diff --git a/pkg/crypto/rings/ring.go b/pkg/crypto/rings/ring.go new file mode 100644 index 000000000..13fd4269b --- /dev/null +++ b/pkg/crypto/rings/ring.go @@ -0,0 +1,40 @@ +package rings + +import ( + ring_secp256k1 "github.com/athanorlabs/go-dleq/secp256k1" + ringtypes "github.com/athanorlabs/go-dleq/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + ring "github.com/noot/ring-go" +) + +// newRingFromPoints creates a new ring from points (i.e. public keys) on the secp256k1 curve +func newRingFromPoints(points []ringtypes.Point) (*ring.Ring, error) { + return ring.NewFixedKeyRingFromPublicKeys(ring_secp256k1.NewCurve(), points) +} + +// pointsFromPublicKeys returns the secp256k1 points for the given public keys. +// It returns an error if any of the keys is not on the secp256k1 curve. +func pointsFromPublicKeys( + publicKeys ...cryptotypes.PubKey, +) (points []ringtypes.Point, err error) { + curve := ring_secp256k1.NewCurve() + + for _, key := range publicKeys { + // Check if the key is a secp256k1 public key + if _, ok := key.(*secp256k1.PubKey); !ok { + return nil, ErrRingsNotSecp256k1Curve.Wrapf("got %T", key) + } + + // Convert the public key to the point on the secp256k1 curve + point, err := curve.DecodeToPoint(key.Bytes()) + if err != nil { + return nil, err + } + + // Insert the point into the slice of points + points = append(points, point) + } + + return points, nil +} diff --git a/pkg/relayer/proxy/errors.go b/pkg/relayer/proxy/errors.go index 5a4d81af7..74e259502 100644 --- a/pkg/relayer/proxy/errors.go +++ b/pkg/relayer/proxy/errors.go @@ -1,18 +1,18 @@ package proxy -import sdkerrors "cosmossdk.io/errors" +import ( + sdkerrors "cosmossdk.io/errors" +) var ( codespace = "relayer_proxy" - ErrRelayerProxyUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported relayer proxy rpc type") - ErrRelayerProxyInvalidRelayRequestSignature = sdkerrors.Register(codespace, 2, "invalid relay request signature") - ErrRelayerProxyInvalidSession = sdkerrors.Register(codespace, 3, "invalid session in relayer request") - ErrRelayerProxyInvalidSupplier = sdkerrors.Register(codespace, 4, "invalid relayer proxy supplier") - ErrRelayerProxyUndefinedSigningKeyName = sdkerrors.Register(codespace, 5, "undefined relayer proxy signing key name") - ErrRelayerProxyUndefinedProxiedServicesEndpoints = sdkerrors.Register(codespace, 6, "undefined proxied services endpoints for relayer proxy") - ErrRelayerProxyInvalidRelayRequest = sdkerrors.Register(codespace, 7, "invalid relay request") - ErrRelayerProxyInvalidRelayResponse = sdkerrors.Register(codespace, 8, "invalid relay response") - ErrRelayerProxyEmptyRelayRequestSignature = sdkerrors.Register(codespace, 9, "empty relay response signature") - ErrRelayerProxyServiceEndpointNotHandled = sdkerrors.Register(codespace, 10, "service endpoint not handled by relayer proxy") - ErrRelayerProxyUnsupportedTransportType = sdkerrors.Register(codespace, 11, "unsupported proxy transport type") + ErrRelayerProxyUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported rpc type") + ErrRelayerProxyInvalidSession = sdkerrors.Register(codespace, 2, "invalid session in relayer request") + ErrRelayerProxyInvalidSupplier = sdkerrors.Register(codespace, 3, "supplier does not belong to session") + ErrRelayerProxyUndefinedSigningKeyName = sdkerrors.Register(codespace, 4, "supplier signing key name is undefined") + ErrRelayerProxyUndefinedProxiedServicesEndpoints = sdkerrors.Register(codespace, 5, "undefined proxied services endpoints for relayer proxy") + ErrRelayerProxyInvalidRelayRequest = sdkerrors.Register(codespace, 6, "invalid relay request") + ErrRelayerProxyInvalidRelayResponse = sdkerrors.Register(codespace, 7, "invalid relay response") + ErrRelayerProxyServiceEndpointNotHandled = sdkerrors.Register(codespace, 8, "service endpoint not handled by relayer proxy") + ErrRelayerProxyUnsupportedTransportType = sdkerrors.Register(codespace, 9, "unsupported proxy transport type") ) diff --git a/pkg/relayer/proxy/proxy_test.go b/pkg/relayer/proxy/proxy_test.go index 5ddba5d5f..b223f726e 100644 --- a/pkg/relayer/proxy/proxy_test.go +++ b/pkg/relayer/proxy/proxy_test.go @@ -17,7 +17,6 @@ import ( "github.com/pokt-network/poktroll/pkg/relayer/config" "github.com/pokt-network/poktroll/pkg/relayer/proxy" "github.com/pokt-network/poktroll/testutil/testproxy" - servicetypes "github.com/pokt-network/poktroll/x/service/types" sessionkeeper "github.com/pokt-network/poktroll/x/session/keeper" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) @@ -385,15 +384,6 @@ func TestRelayerProxy_Relays(t *testing.T) { expectedErrCode: 0, expectedErrMsg: "cannot unmarshal request payload", }, - { - desc: "Missing session meta from relay request", - - relayerProxyBehavior: defaultRelayerProxyBehavior, - inputScenario: sendRequestWithMissingMeta, - - expectedErrCode: -32000, - expectedErrMsg: "missing meta from relay request", - }, { desc: "Missing signature from relay request", @@ -419,7 +409,7 @@ func TestRelayerProxy_Relays(t *testing.T) { inputScenario: sendRequestWithMissingSessionHeaderApplicationAddress, expectedErrCode: -32000, - expectedErrMsg: "missing application address from relay request", + expectedErrMsg: "invalid session header: invalid application address", }, { desc: "Non staked application address", @@ -437,7 +427,7 @@ func TestRelayerProxy_Relays(t *testing.T) { inputScenario: sendRequestWithRingSignatureMismatch, expectedErrCode: -32000, - expectedErrMsg: "ring signature does not match ring for application address", + expectedErrMsg: "ring signature in the relay request does not match the expected one for the app", }, { desc: "Session mismatch", @@ -471,7 +461,7 @@ func TestRelayerProxy_Relays(t *testing.T) { inputScenario: sendRequestWithSignatureForDifferentPayload, expectedErrCode: -32000, - expectedErrMsg: "invalid ring signature", + expectedErrMsg: "invalid relay request signature or bytes", }, { desc: "Successful relay", @@ -572,19 +562,6 @@ func sendRequestWithUnparsableBody( return testproxy.GetRelayResponseError(t, res) } -func sendRequestWithMissingMeta( - t *testing.T, - test *testproxy.TestBehavior, -) (errorCode int32, errorMessage string) { - - req := &servicetypes.RelayRequest{ - // RelayRequest is missing Metadata - Payload: testproxy.PrepareJsonRPCRequestPayload(), - } - - return testproxy.MarshalAndSend(test, proxiedServices, defaultProxyServer, defaultService, req) -} - func sendRequestWithMissingSignature( t *testing.T, test *testproxy.TestBehavior, diff --git a/pkg/relayer/proxy/relay_builders.go b/pkg/relayer/proxy/relay_builders.go index c6e9c9a77..7abba5c2e 100644 --- a/pkg/relayer/proxy/relay_builders.go +++ b/pkg/relayer/proxy/relay_builders.go @@ -34,7 +34,7 @@ func (sync *synchronousRPCServer) newRelayResponse( sessionHeader *sessiontypes.SessionHeader, ) (*types.RelayResponse, error) { relayResponse := &types.RelayResponse{ - Meta: &types.RelayResponseMetadata{SessionHeader: sessionHeader}, + Meta: types.RelayResponseMetadata{SessionHeader: sessionHeader}, } responseBz, err := io.ReadAll(response.Body) diff --git a/pkg/relayer/proxy/relay_verifier.go b/pkg/relayer/proxy/relay_verifier.go index 2030ec239..98cdd65cd 100644 --- a/pkg/relayer/proxy/relay_verifier.go +++ b/pkg/relayer/proxy/relay_verifier.go @@ -3,115 +3,67 @@ package proxy import ( "context" - ring_secp256k1 "github.com/athanorlabs/go-dleq/secp256k1" - ring "github.com/noot/ring-go" - sessiontypes "github.com/pokt-network/poktroll/pkg/relayer/session" "github.com/pokt-network/poktroll/x/service/types" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) -// VerifyRelayRequest is a shared method used by RelayServers to check the relay request signature and session validity. +// VerifyRelayRequest is a shared method used by RelayServers to check the relay +// request signature and session validity. func (rp *relayerProxy) VerifyRelayRequest( ctx context.Context, relayRequest *types.RelayRequest, - service *sharedtypes.Service, + supplierService *sharedtypes.Service, ) error { - rp.logger.Debug(). - Fields(map[string]any{ - "session_id": relayRequest.Meta.SessionHeader.SessionId, - "application_address": relayRequest.Meta.SessionHeader.ApplicationAddress, - "service_id": relayRequest.Meta.SessionHeader.Service.Id, - }). - Msg("verifying relay request signature") - - // extract the relay request's ring signature - if relayRequest.Meta == nil { - return ErrRelayerProxyEmptyRelayRequestSignature.Wrapf( - "request payload: %s", relayRequest.Payload, - ) - } - signature := relayRequest.Meta.Signature - if signature == nil { - return ErrRelayerProxyInvalidRelayRequest.Wrapf( - "missing signature from relay request: %v", relayRequest, - ) - } - - ringSig := new(ring.RingSig) - if err := ringSig.Deserialize(ring_secp256k1.NewCurve(), signature); err != nil { - return ErrRelayerProxyInvalidRelayRequestSignature.Wrapf( - "error deserializing ring signature: %v", err, - ) - } - - if relayRequest.Meta.SessionHeader.ApplicationAddress == "" { - return ErrRelayerProxyInvalidRelayRequest.Wrap( - "missing application address from relay request", - ) - } - - // get the ring for the application address of the relay request - appAddress := relayRequest.Meta.SessionHeader.ApplicationAddress - appRing, err := rp.ringCache.GetRingForAddress(ctx, appAddress) - if err != nil { - return ErrRelayerProxyInvalidRelayRequest.Wrapf( - "error getting ring for application address %s: %v", appAddress, err, - ) - } - - // verify the ring signature against the ring - if !ringSig.Ring().Equals(appRing) { - return ErrRelayerProxyInvalidRelayRequestSignature.Wrapf( - "ring signature does not match ring for application address %s", appAddress, - ) + // Verify the relayRequest metadata, signature, session header and other + // basic validation. + if err := rp.ringCache.VerifyRelayRequestSignature(ctx, relayRequest); err != nil { + return err } - // get and hash the signable bytes of the relay request - requestSignableBz, err := relayRequest.GetSignableBytesHash() - if err != nil { - return ErrRelayerProxyInvalidRelayRequest.Wrapf("error getting signable bytes: %v", err) - } + // Extract the session header for usage below. + // ringCache.VerifyRelayRequestSignature already verified the header's validaity. + sessionHeader := relayRequest.GetMeta().SessionHeader - // verify the relay request's signature - if valid := ringSig.Verify(requestSignableBz); !valid { - return ErrRelayerProxyInvalidRelayRequestSignature.Wrapf( - "invalid ring signature", - ) - } + // Application address is used to verify the relayRequest signature. + // It is guaranteed to be present in the relayRequest since the signature + // has already been verified. + appAddress := sessionHeader.GetApplicationAddress() - // Query for the current session to check if relayRequest sessionId matches the current session. rp.logger.Debug(). Fields(map[string]any{ - "session_id": relayRequest.Meta.SessionHeader.SessionId, - "application_address": relayRequest.Meta.SessionHeader.ApplicationAddress, - "service_id": relayRequest.Meta.SessionHeader.Service.Id, + "session_id": sessionHeader.GetSessionId(), + "application_address": appAddress, + "service_id": sessionHeader.GetService().GetId(), }). Msg("verifying relay request session") + // Get the block height at which the relayRequest should be processed. sessionBlockHeight, err := rp.getTargetSessionBlockHeight(ctx, relayRequest) if err != nil { return err } + // Query for the current session to check if relayRequest sessionId matches the current session. session, err := rp.sessionQuerier.GetSession( ctx, appAddress, - service.Id, + supplierService.Id, sessionBlockHeight, ) if err != nil { return err } + // Session validity can be checked via a basic ID comparison due to the reasons below. + // // Since the retrieved sessionId was in terms of: // - the current block height and sessionGracePeriod (which are not provided by the relayRequest) // - serviceId (which is not provided by the relayRequest) // - applicationAddress (which is used to to verify the relayRequest signature) - // we can reduce the session validity check to checking if the retrieved session's sessionId - // matches the relayRequest sessionId. - // TODO_INVESTIGATE: Revisit the assumptions above at some point in the future, but good enough for now. - if session.SessionId != relayRequest.Meta.SessionHeader.SessionId { + // + // TODO_BLOCKER: Revisit the assumptions above but good enough for now. + if session.SessionId != sessionHeader.GetSessionId() { return ErrRelayerProxyInvalidSession.Wrapf( "session mismatch, expecting: %+v, got: %+v", session.Header, @@ -141,11 +93,13 @@ func (rp *relayerProxy) getTargetSessionBlockHeight( currentBlockHeight := rp.blockClient.LastNBlocks(ctx, 1)[0].Height() sessionEndblockHeight := relayRequest.Meta.SessionHeader.GetSessionEndBlockHeight() - // Check if the `RelayRequest`'s session has expired. + // Check if the RelayRequest's session has expired. if sessionEndblockHeight < currentBlockHeight { // Do not process the `RelayRequest` if the session has expired and the current // block height is outside the session's grace period. if sessiontypes.IsWithinGracePeriod(sessionEndblockHeight, currentBlockHeight) { + // The RelayRequest's session has expired but is still within the + // grace period so process it as if the session is still active. return sessionEndblockHeight, nil } @@ -156,5 +110,6 @@ func (rp *relayerProxy) getTargetSessionBlockHeight( ) } + // The RelayRequest's session is active so return the current block height. return currentBlockHeight, nil } diff --git a/pkg/relayer/proxy/synchronous.go b/pkg/relayer/proxy/synchronous.go index 174e896f2..ad0870a52 100644 --- a/pkg/relayer/proxy/synchronous.go +++ b/pkg/relayer/proxy/synchronous.go @@ -175,15 +175,6 @@ func (sync *synchronousRPCServer) ServeHTTP(writer http.ResponseWriter, request return } - if relayRequest.Meta == nil { - err = ErrRelayerProxyInvalidRelayRequest.Wrapf( - "missing meta from relay request: %v", relayRequest, - ) - sync.replyWithError(ctx, relayRequest.Payload, writer, sync.proxyConfig.ProxyName, supplierService.Id, err) - sync.logger.Warn().Err(err).Msg("relay request metadata is nil which could be a result of failed unmashaling") - return - } - // Relay the request to the proxied service and build the response that will be sent back to the client. relay, err := sync.serveHTTP(ctx, serviceUrl, supplierService, request, relayRequest) if err != nil { diff --git a/pkg/relayer/session/session.go b/pkg/relayer/session/session.go index dc46e644e..d356ee44b 100644 --- a/pkg/relayer/session/session.go +++ b/pkg/relayer/session/session.go @@ -256,7 +256,7 @@ func (rs *relayerSessionsManager) mapAddMinedRelayToSessionTree( // ensure the session tree exists for this relay // TODO_CONSIDERATION: if we get the session header from the response, there // is no possibility that we forgot to hydrate it (i.e. blindly trust the client). - sessionHeader := relay.GetReq().GetMeta().GetSessionHeader() + sessionHeader := relay.GetReq().GetMeta().SessionHeader smst, err := rs.ensureSessionTree(sessionHeader) if err != nil { // TODO_IMPROVE: log additional info? diff --git a/pkg/relayer/session/session_test.go b/pkg/relayer/session/session_test.go index 64d3dcef1..7387702b1 100644 --- a/pkg/relayer/session/session_test.go +++ b/pkg/relayer/session/session_test.go @@ -2,10 +2,12 @@ package session_test import ( "context" + "crypto/sha256" "testing" "time" "cosmossdk.io/depinject" + "github.com/pokt-network/smt" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/pkg/client" @@ -25,14 +27,16 @@ func TestRelayerSessionsManager_Start(t *testing.T) { sessionStartHeight = 1 sessionEndHeight = 2 ) + var ( - zeroByteSlice = []byte{0} - _, ctx = testpolylog.NewLoggerWithCtx(context.Background(), polyzero.DebugLevel) + _, ctx = testpolylog.NewLoggerWithCtx(context.Background(), polyzero.DebugLevel) + spec = smt.NoPrehashSpec(sha256.New(), true) + emptyBlockHash = make([]byte, spec.PathHasherSize()) ) // Set up dependencies. blocksObs, blockPublishCh := channel.NewReplayObservable[client.Block](ctx, 1) - blockClient := testblock.NewAnyTimesCommittedBlocksSequenceBlockClient(t, blocksObs) + blockClient := testblock.NewAnyTimesCommittedBlocksSequenceBlockClient(t, emptyBlockHash, blocksObs) supplierClient := testsupplier.NewOneTimeClaimProofSupplierClient(ctx, t) deps := depinject.Supply(blockClient, supplierClient) @@ -60,7 +64,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { time.Sleep(10 * time.Millisecond) // Publish a block to the blockPublishCh to simulate non-actionable blocks. - noopBlock := testblock.NewAnyTimesBlock(t, zeroByteSlice, sessionStartHeight) + noopBlock := testblock.NewAnyTimesBlock(t, emptyBlockHash, sessionStartHeight) blockPublishCh <- noopBlock // Calculate the session grace period end block height to emit that block height @@ -70,7 +74,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { // Publish a block to the blockPublishCh to trigger claim creation for the session. // TODO_TECHDEBT: assumes claiming at sessionGracePeriodEndBlockHeight is valid. // This will likely change in future work. - triggerClaimBlock := testblock.NewAnyTimesBlock(t, zeroByteSlice, sessionGracePeriodEndBlockHeight) + triggerClaimBlock := testblock.NewAnyTimesBlock(t, emptyBlockHash, sessionGracePeriodEndBlockHeight) blockPublishCh <- triggerClaimBlock // TODO_IMPROVE: ensure correctness of persisted session trees here. @@ -78,7 +82,7 @@ func TestRelayerSessionsManager_Start(t *testing.T) { // Publish a block to the blockPublishCh to trigger proof submission for the session. // TODO_TECHDEBT: assumes proving at sessionGracePeriodEndBlockHeight + 1 is valid. // This will likely change in future work. - triggerProofBlock := testblock.NewAnyTimesBlock(t, zeroByteSlice, sessionGracePeriodEndBlockHeight+1) + triggerProofBlock := testblock.NewAnyTimesBlock(t, emptyBlockHash, sessionGracePeriodEndBlockHeight+1) blockPublishCh <- triggerProofBlock // Wait a tick to allow the relayer sessions manager to process asynchronously. diff --git a/pkg/sdk/deps_builder.go b/pkg/sdk/deps_builder.go index cfd0ff4b3..64d18dc64 100644 --- a/pkg/sdk/deps_builder.go +++ b/pkg/sdk/deps_builder.go @@ -54,7 +54,7 @@ func (sdk *poktrollSDK) buildDeps( } deps = depinject.Configs(deps, depinject.Supply(grpcClient)) - // Create and supply the account querier + // Create the account querier and add it to the required dependencies. accountQuerier, err := query.NewAccountQuerier(deps) if err != nil { return nil, err diff --git a/pkg/sdk/relay_verifier.go b/pkg/sdk/relay_verifier.go deleted file mode 100644 index 6b487a9a6..000000000 --- a/pkg/sdk/relay_verifier.go +++ /dev/null @@ -1,82 +0,0 @@ -package sdk - -import ( - "context" - - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - - "github.com/pokt-network/poktroll/pkg/polylog" - "github.com/pokt-network/poktroll/x/service/types" -) - -// verifyResponse verifies the relay response signature. -func (sdk *poktrollSDK) verifyResponse( - ctx context.Context, - supplierAddress string, - relayResponse *types.RelayResponse, -) error { - logger := polylog.Ctx(context.Background()) - - // Get the supplier's public key. - supplierPubKey, err := sdk.getSupplierPubKeyFromAddress(ctx, supplierAddress) - if err != nil { - return err - } - - // Extract the supplier's signature - if relayResponse.Meta == nil { - return ErrSDKEmptyRelayResponseSignature.Wrapf( - "response payload: %s", relayResponse.Payload, - ) - } - supplierSignature := relayResponse.Meta.SupplierSignature - - // Get the relay response signable bytes and hash them. - responseSignableBz, err := relayResponse.GetSignableBytesHash() - if err != nil { - return err - } - - logger.Debug(). - Str("supplier", supplierAddress). - Str("application", relayResponse.Meta.SessionHeader.ApplicationAddress). - Str("service", relayResponse.Meta.SessionHeader.Service.Id). - Int64("end_height", relayResponse.Meta.SessionHeader.SessionEndBlockHeight). - Msg("About to verify relay response signature.") - - // Verify the relay response signature. - if !supplierPubKey.VerifySignature(responseSignableBz[:], supplierSignature) { - return ErrSDKInvalidRelayResponseSignature - } - - return nil -} - -// getSupplierPubKeyFromAddress gets the supplier's public key from the cache or -// queries if it is not found. The public key is then cached before being returned. -func (sdk *poktrollSDK) getSupplierPubKeyFromAddress( - ctx context.Context, - supplierAddress string, -) (cryptotypes.PubKey, error) { - supplierPubKey, ok := sdk.supplierAccountCache[supplierAddress] - if ok { - return supplierPubKey, nil - } - - // Query for the supplier account to get the application's public key - // to verify the relay request signature. - acc, err := sdk.accountQuerier.GetAccount(ctx, supplierAddress) - if err != nil { - return nil, err - } - - fetchedPubKey := acc.GetPubKey() - if fetchedPubKey == nil { - return nil, ErrSDKEmptySupplierPubKey - } - - // Cache the retrieved public key. - sdk.supplierAccountCache[supplierAddress] = fetchedPubKey - - return fetchedPubKey, nil -} diff --git a/pkg/sdk/sdk.go b/pkg/sdk/sdk.go index c2c836e6b..5cc4d1458 100644 --- a/pkg/sdk/sdk.go +++ b/pkg/sdk/sdk.go @@ -53,21 +53,17 @@ type poktrollSDK struct { // for a specific session serviceSessionSuppliers map[string]map[string]*SessionSuppliers - // accountQuerier is the querier for the account module. - // It is used to get the the supplier's public key to verify the relay response signature. - accountQuerier client.AccountQueryClient - // applicationQuerier is the querier for the application module. // It is used to query a specific application or all applications applicationQuerier client.ApplicationQueryClient + // accountQuerier is the querier for the account module. + // It retrieves on-chain accounts provided the address. + accountQuerier client.AccountQueryClient + // blockClient is the client for the block module. // It is used to get the current block height to query for the current session. blockClient client.BlockClient - - // accountCache is a cache of the supplier accounts that has been queried - // TODO_TECHDEBT: Add a size limit to the cache. - supplierAccountCache map[string]cryptotypes.PubKey } // NewPOKTRollSDK creates a new POKTRollSDK instance with the given configuration. @@ -75,7 +71,6 @@ func NewPOKTRollSDK(ctx context.Context, config *POKTRollSDKConfig) (POKTRollSDK sdk := &poktrollSDK{ config: config, serviceSessionSuppliers: make(map[string]map[string]*SessionSuppliers), - supplierAccountCache: make(map[string]cryptotypes.PubKey), } var err error diff --git a/pkg/sdk/send_relay.go b/pkg/sdk/send_relay.go index bb6ece483..447418048 100644 --- a/pkg/sdk/send_relay.go +++ b/pkg/sdk/send_relay.go @@ -19,21 +19,21 @@ func init() { // SendRelay sends a relay request to the given supplier's endpoint. // It signs the request, relays it to the supplier and verifies the response signature. -// It takes an http.Request as an argument and uses its method and headers to create -// the relay request. +// The relay request is created by adding method headers to the provided http.Request. func (sdk *poktrollSDK) SendRelay( ctx context.Context, supplierEndpoint *SingleSupplierEndpoint, request *http.Request, ) (response *types.RelayResponse, err error) { + // Retrieve the request's payload. payloadBz, err := io.ReadAll(request.Body) if err != nil { return nil, ErrSDKHandleRelay.Wrapf("reading request body: %s", err) } - // Create the relay request. + // Prepare the relay request. relayRequest := &types.RelayRequest{ - Meta: &types.RelayRequestMetadata{ + Meta: types.RelayRequestMetadata{ SessionHeader: supplierEndpoint.Header, Signature: nil, // signature added below }, @@ -48,12 +48,13 @@ func (sdk *poktrollSDK) SendRelay( } signer := signer.NewRingSigner(appRing, sdk.signingKey) - // Hash and sign the request's signable bytes. + // Hash request's signable bytes. signableBz, err := relayRequest.GetSignableBytesHash() if err != nil { return nil, ErrSDKHandleRelay.Wrapf("error getting signable bytes: %s", err) } + // Sign the relay request. requestSig, err := signer.Sign(signableBz) if err != nil { return nil, ErrSDKHandleRelay.Wrapf("error signing relay: %s", err) @@ -66,10 +67,6 @@ func (sdk *poktrollSDK) SendRelay( return nil, ErrSDKHandleRelay.Wrapf("error marshaling relay request: %s", err) } relayRequestReader := io.NopCloser(bytes.NewReader(relayRequestBz)) - var relayReq types.RelayRequest - if err := relayReq.Unmarshal(relayRequestBz); err != nil { - return nil, ErrSDKHandleRelay.Wrapf("error unmarshaling relay request: %s", err) - } // Create the HTTP request to send the request to the relayer. // All the RPC protocols to be supported (JSONRPC, Rest, Websockets, gRPC, etc) @@ -84,6 +81,7 @@ func (sdk *poktrollSDK) SendRelay( sdk.logger.Debug(). Str("supplier_url", supplierEndpoint.Url.String()). Msg("sending relay request") + relayHTTPResponse, err := http.DefaultClient.Do(relayHTTPRequest) if err != nil { return nil, ErrSDKHandleRelay.Wrapf("error sending relay request: %s", err) @@ -95,19 +93,37 @@ func (sdk *poktrollSDK) SendRelay( return nil, ErrSDKHandleRelay.Wrapf("error reading relay response body: %s", err) } - // Unmarshal the response bytes into a RelayResponse. + // Unmarshal the response bytes into a RelayResponse and validate it. relayResponse := &types.RelayResponse{} if err := relayResponse.Unmarshal(relayResponseBz); err != nil { return nil, ErrSDKHandleRelay.Wrapf("error unmarshaling relay response: %s", err) } + if err := relayResponse.ValidateBasic(); err != nil { + return nil, ErrSDKHandleRelay.Wrapf("%s", err) + } + + // relayResponse.ValidateBasic validates Meta and SessionHeader, so + // we can safely use the session header. + sessionHeader := relayResponse.GetMeta().SessionHeader + + // Get the supplier's public key. + supplierPubKey, err := sdk.accountQuerier.GetPubKeyFromAddress(ctx, supplierEndpoint.SupplierAddress) + if err != nil { + return nil, ErrSDKHandleRelay.Wrapf("error getting supplier public key: %v", err) + } - // Verify the response signature. We use the supplier address that we got from - // the getRelayerUrl function since this is the address we are expecting to sign the response. - // TODO_TECHDEBT: if the RelayResponse is an internal error response, we should not verify the signature - // as in some relayer early failures, it may not be signed by the supplier. - // TODO_IMPROVE: Add more logging & telemetry so we can get visibility and signal into - // failed responses. - if err := sdk.verifyResponse(ctx, supplierEndpoint.SupplierAddress, relayResponse); err != nil { + sdk.logger.Debug(). + Str("supplier", supplierEndpoint.SupplierAddress). + Str("application", sessionHeader.GetApplicationAddress()). + Str("service", sessionHeader.GetService().GetId()). + Int64("end_height", sessionHeader.GetSessionEndBlockHeight()). + Msg("About to verify relay response signature.") + + // Verify the relay response's supplier signature. + // TODO_TECHDEBT: if the RelayResponse has an internal error response, we + // SHOULD NOT verify the signature, and return an error early. + // TODO_IMPROVE: Increase logging & telemetry get visibility into failed responses. + if err := relayResponse.VerifySupplierSignature(supplierPubKey); err != nil { return nil, ErrSDKVerifyResponseSignature.Wrapf("%s", err) } diff --git a/proto/poktroll/service/relay.proto b/proto/poktroll/service/relay.proto index 9b94a4d91..8dc0bdab9 100644 --- a/proto/poktroll/service/relay.proto +++ b/proto/poktroll/service/relay.proto @@ -3,6 +3,8 @@ package poktroll.service; option go_package = "github.com/pokt-network/poktroll/x/service/types"; +import "gogoproto/gogo.proto"; + import "poktroll/session/session.proto"; // Relay contains both the RelayRequest (signed by the Application) and the RelayResponse (signed by the Supplier). @@ -24,7 +26,7 @@ message RelayRequestMetadata { // RelayRequest holds the request details for a relay. message RelayRequest { - RelayRequestMetadata meta = 1; + RelayRequestMetadata meta = 1 [(gogoproto.nullable) = false]; // payload is the serialized payload for the request. // The payload is passed directly to the service and as such can be any // format that the service supports: JSON-RPC, REST, gRPC, etc. @@ -33,7 +35,7 @@ message RelayRequest { // RelayResponse contains the response details for a RelayRequest. message RelayResponse { - RelayResponseMetadata meta = 1; + RelayResponseMetadata meta = 1 [(gogoproto.nullable) = false]; // payload is the serialized payload for the response. // The payload is passed directly from the service and as such can be any // format the the service responds with: JSON-RPC, REST, gRPC, etc. diff --git a/testutil/keeper/proof.go b/testutil/keeper/proof.go index 0189ccd62..4ca4dd76b 100644 --- a/testutil/keeper/proof.go +++ b/testutil/keeper/proof.go @@ -80,6 +80,8 @@ func ProofKeeper( log.NewNopLogger(), authority.String(), mockSessionKeeper, + nil, + nil, ) ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger()) diff --git a/testutil/testclient/testblock/client.go b/testutil/testclient/testblock/client.go index 5948cb9db..0724de904 100644 --- a/testutil/testclient/testblock/client.go +++ b/testutil/testclient/testblock/client.go @@ -36,12 +36,13 @@ func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { // and when that call is made, it returns the given EventsObservable[Block]. func NewAnyTimesCommittedBlocksSequenceBlockClient( t *testing.T, + blockHash []byte, blocksObs observable.Observable[client.Block], ) *mockclient.MockBlockClient { t.Helper() // Create a mock for the block client which expects the LastNBlocks method to be called any number of times. - blockClientMock := NewAnyTimeLastNBlocksBlockClient(t, nil, 0) + blockClientMock := NewAnyTimeLastNBlocksBlockClient(t, blockHash, 0) // Set up the mock expectation for the CommittedBlocksSequence method. When // the method is called, it returns a new replay observable that publishes @@ -91,14 +92,14 @@ func NewOneTimeCommittedBlocksSequenceBlockClient( // method is called, it returns a mock Block with the provided hash and height. func NewAnyTimeLastNBlocksBlockClient( t *testing.T, - hash []byte, - height int64, + blockHash []byte, + blockHeight int64, ) *mockclient.MockBlockClient { t.Helper() ctrl := gomock.NewController(t) // Create a mock block that returns the provided hash and height. - blockMock := NewAnyTimesBlock(t, hash, height) + blockMock := NewAnyTimesBlock(t, blockHash, blockHeight) // Create a mock block client that expects calls to LastNBlocks method and // returns the mock block. blockClientMock := mockclient.NewMockBlockClient(ctrl) @@ -110,14 +111,18 @@ func NewAnyTimeLastNBlocksBlockClient( // NewAnyTimesBlock creates a mock Block that expects calls to Height and Hash // methods any number of times. When the methods are called, they return the // provided height and hash respectively. -func NewAnyTimesBlock(t *testing.T, hash []byte, height int64) *mockclient.MockBlock { +func NewAnyTimesBlock( + t *testing.T, + blockHash []byte, + blockHeight int64, +) *mockclient.MockBlock { t.Helper() ctrl := gomock.NewController(t) // Create a mock block that returns the provided hash and height AnyTimes. blockMock := mockclient.NewMockBlock(ctrl) - blockMock.EXPECT().Height().Return(height).AnyTimes() - blockMock.EXPECT().Hash().Return(hash).AnyTimes() + blockMock.EXPECT().Height().Return(blockHeight).AnyTimes() + blockMock.EXPECT().Hash().Return(blockHash).AnyTimes() return blockMock } diff --git a/testutil/testclient/testqueryclients/accquerier.go b/testutil/testclient/testqueryclients/accquerier.go index 6924dc963..cd645be68 100644 --- a/testutil/testclient/testqueryclients/accquerier.go +++ b/testutil/testclient/testqueryclients/accquerier.go @@ -6,9 +6,11 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/golang/mock/gomock" + "github.com/pokt-network/poktroll/pkg/client/query" "github.com/pokt-network/poktroll/testutil/mockclient" ) @@ -32,12 +34,12 @@ func NewTestAccountQueryClient( ) *mockclient.MockAccountQueryClient { ctrl := gomock.NewController(t) - accoutQuerier := mockclient.NewMockAccountQueryClient(ctrl) - accoutQuerier.EXPECT().GetAccount(gomock.Any(), gomock.Any()). + accountQuerier := mockclient.NewMockAccountQueryClient(ctrl) + accountQuerier.EXPECT().GetAccount(gomock.Any(), gomock.Any()). DoAndReturn(func( _ context.Context, address string, - ) (account accounttypes.AccountI, err error) { + ) (account types.AccountI, err error) { anyPk := (*codectypes.Any)(nil) if pk, ok := addressAccountMap[address]; ok { anyPk, err = codectypes.NewAnyWithValue(pk) @@ -52,7 +54,20 @@ func NewTestAccountQueryClient( }). AnyTimes() - return accoutQuerier + accountQuerier.EXPECT().GetPubKeyFromAddress(gomock.Any(), gomock.Any()). + DoAndReturn(func( + _ context.Context, + address string, + ) (pk cryptotypes.PubKey, err error) { + pk, ok := addressAccountMap[address] + if !ok { + return nil, query.ErrQueryAccountNotFound + } + return pk, nil + }). + AnyTimes() + + return accountQuerier } // addAddressToAccountMap adds the given address to the addressAccountMap diff --git a/testutil/testproxy/relayerproxy.go b/testutil/testproxy/relayerproxy.go index ba1101abf..64d21ee38 100644 --- a/testutil/testproxy/relayerproxy.go +++ b/testutil/testproxy/relayerproxy.go @@ -393,7 +393,7 @@ func GenerateRelayRequest( sessionId, _ := sessionkeeper.GetSessionId(appAddress, serviceId, blockHashBz, blockHeight) return &servicetypes.RelayRequest{ - Meta: &servicetypes.RelayRequestMetadata{ + Meta: servicetypes.RelayRequestMetadata{ SessionHeader: &sessiontypes.SessionHeader{ ApplicationAddress: appAddress, SessionId: string(sessionId[:]), diff --git a/testutil/testrelayer/relays.go b/testutil/testrelayer/relays.go index 53ddbadc2..db415b468 100644 --- a/testutil/testrelayer/relays.go +++ b/testutil/testrelayer/relays.go @@ -20,7 +20,7 @@ func NewMinedRelay( ) *relayer.MinedRelay { relay := servicetypes.Relay{ Req: &servicetypes.RelayRequest{ - Meta: &servicetypes.RelayRequestMetadata{ + Meta: servicetypes.RelayRequestMetadata{ SessionHeader: &sessiontypes.SessionHeader{ SessionStartBlockHeight: sessionStartHeight, SessionEndBlockHeight: sessionEndHeight, diff --git a/x/proof/keeper/keeper.go b/x/proof/keeper/keeper.go index 09e5d465a..4a6c69268 100644 --- a/x/proof/keeper/keeper.go +++ b/x/proof/keeper/keeper.go @@ -1,13 +1,20 @@ package keeper import ( + "context" "fmt" "cosmossdk.io/core/store" + "cosmossdk.io/depinject" "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/crypto" + "github.com/pokt-network/poktroll/pkg/crypto/rings" + "github.com/pokt-network/poktroll/pkg/polylog" + _ "github.com/pokt-network/poktroll/pkg/polylog/polyzero" "github.com/pokt-network/poktroll/x/proof/types" ) @@ -21,7 +28,11 @@ type ( // should be the x/gov module account. authority string - sessionKeeper types.SessionKeeper + sessionKeeper types.SessionKeeper + applicationKeeper types.ApplicationKeeper + + ringClient crypto.RingClient + accountQuerier client.AccountQueryClient } ) @@ -32,18 +43,50 @@ func NewKeeper( authority string, sessionKeeper types.SessionKeeper, + applicationKeeper types.ApplicationKeeper, + accountKeeper types.AccountKeeper, ) Keeper { if _, err := sdk.AccAddressFromBech32(authority); err != nil { panic(fmt.Sprintf("invalid authority address: %s", authority)) } + // TODO_TECHDEBT: Use cosmos-sdk based polylog implementation once available. Also remove polyzero import. + polylogger := polylog.Ctx(context.Background()) + applicationQuerier := types.NewAppKeeperQueryClient(applicationKeeper) + accountQuerier := types.NewAccountKeeperQueryClient(accountKeeper) + + // RingKeeperClient holds the logic of verifying RelayRequests ring signatures + // for both on-chain and off-chain actors. + // + // ApplicationQueriers & AccountQuerier are compatible with the environment + // they're used in and may or may not make an actual network request. + // + // When used in an on-chain context, the ProofKeeper supplies AppKeeperQueryClient + // and AccountKeeperQueryClient that are thin wrappers around the Application and + // Account keepers respectively to satisfy the RingClient needs. + // + // TODO_IMPROVE_CONSIDERATION: Make ring signature verification a stateless + // function and get rid of the RingClient and its dependencies by moving + // application ring retrieval to the application keeper, and making it + // retrievable using the application query client for off-chain actors. Signature + // verification code will still be shared across off/on chain environments. + ringKeeperClientDeps := depinject.Supply(polylogger, applicationQuerier, accountQuerier) + ringKeeperClient, err := rings.NewRingClient(ringKeeperClientDeps) + if err != nil { + panic(err) + } + return Keeper{ cdc: cdc, storeService: storeService, authority: authority, logger: logger, - sessionKeeper: sessionKeeper, + sessionKeeper: sessionKeeper, + applicationKeeper: applicationKeeper, + + ringClient: ringKeeperClient, + accountQuerier: accountQuerier, } } diff --git a/x/proof/keeper/msg_server_submit_proof.go b/x/proof/keeper/msg_server_submit_proof.go index 442098270..6ce05d024 100644 --- a/x/proof/keeper/msg_server_submit_proof.go +++ b/x/proof/keeper/msg_server_submit_proof.go @@ -1,22 +1,49 @@ package keeper import ( + "bytes" "context" + "crypto/sha256" + "github.com/pokt-network/smt" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/pokt-network/poktroll/pkg/relayer/protocol" "github.com/pokt-network/poktroll/x/proof/types" + servicetypes "github.com/pokt-network/poktroll/x/service/types" + sessionkeeper "github.com/pokt-network/poktroll/x/session/keeper" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) +const ( + // relayMinDifficultyBits is the minimum difficulty that a relay must have + // to be reward (i.e. volume) applicable. + // TODO_BLOCKER: relayMinDifficultyBits should be a governance-based parameter + relayMinDifficultyBits = 0 +) + +// SMT specification used for the proof verification. +var spec *smt.TrieSpec + +func init() { + // Use a spec that does not prehash values in the smst. This returns a nil + // value hasher for the proof verification in order to to avoid hashing the + // value twice. + spec = smt.NoPrehashSpec(sha256.New(), true) +} + +// SubmitProof is the server handler to submit and store a proof on-chain. +// A proof that's stored on-chain is what leads to rewards (i.e. inflation) +// downstream, making the series of checks a critical part of the protocol. +// TODO_BLOCKER: Prevent proof upserts after the tokenomics module has processes the respective session. func (k msgServer) SubmitProof(ctx context.Context, msg *types.MsgSubmitProof) (*types.MsgSubmitProofResponse, error) { - // TODO_BLOCKER: Prevent Proof upserts after the tokenomics module has processes the respective session. - // TODO_BLOCKER: Validate the signature on the Proof message corresponds to the supplier before Upserting. logger := k.Logger().With("method", "SubmitProof") - logger.Debug("submitting proof") + logger.Debug("about to start submitting proof") /* - TODO_INCOMPLETE: Handling the message + TODO_DOCUMENT(@bryanchriswhite): Document these steps in proof + verification, link to the doc for reference and delete the comments. ## Actions (error if anything fails) 1. Retrieve a fully hydrated `session` from on-chain store using `msg` metadata @@ -43,28 +70,125 @@ func (k msgServer) SubmitProof(ctx context.Context, msg *types.MsgSubmitProof) ( 3. verify(claim.Root, proof.ClosestProof); verify the closest proof is correct */ + // Basic validation of the SubmitProof message. if err := msg.ValidateBasic(); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + // Retrieve the supplier's public key. + supplierAddr := msg.GetSupplierAddress() + supplierPubKey, err := k.accountQuerier.GetPubKeyFromAddress(ctx, supplierAddr) + if err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + + // Validate the session header. if _, err := k.queryAndValidateSessionHeader( ctx, msg.GetSessionHeader(), - msg.GetSupplierAddress(), + supplierAddr, ); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } - // Construct and insert proof after all validation. - proof := types.Proof{ - SupplierAddress: msg.GetSupplierAddress(), - SessionHeader: msg.GetSessionHeader(), - ClosestMerkleProof: msg.Proof, + // Unmarshal the closest merkle proof from the message. + sparseMerkleClosestProof := &smt.SparseMerkleClosestProof{} + if err := sparseMerkleClosestProof.Unmarshal(msg.GetProof()); err != nil { + return nil, types.ErrProofInvalidProof.Wrapf( + "failed to unmarshal closest merkle proof: %s", + err, + ) + } + + // Get the relay request and response from the proof.GetClosestMerkleProof. + relayBz := sparseMerkleClosestProof.GetValueHash(spec) + relay := &servicetypes.Relay{} + if err := k.cdc.Unmarshal(relayBz, relay); err != nil { + return nil, types.ErrProofInvalidRelay.Wrapf( + "failed to unmarshal relay: %s", + err, + ) + } + + logger = logger. + With( + "session_id", msg.GetSessionHeader().GetSessionId(), + "session_end_height", msg.GetSessionHeader().GetSessionEndBlockHeight(), + "supplier", supplierAddr, + ) + + // Basic validation of the relay request. + relayReq := relay.GetReq() + if err := relayReq.ValidateBasic(); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully validated relay request") + + // Basic validation of the relay response. + relayRes := relay.GetRes() + if err := relayRes.ValidateBasic(); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) } + logger.Debug("successfully validated relay response") - if err := k.queryAndValidateClaimForProof(ctx, &proof); err != nil { + // Verify that the relay request session header matches the proof session header. + if err := compareSessionHeaders(msg.GetSessionHeader(), relayReq.Meta.GetSessionHeader()); err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } + logger.Debug("successfully compared relay request session header") + + // Verify that the relay response session header matches the proof session header. + if err := compareSessionHeaders(msg.GetSessionHeader(), relayRes.Meta.GetSessionHeader()); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully compared relay response session header") + + // Verify the relay request's signature. + // TODO_TECHDEBT(@h5law): Fetch the correct ring for the session this relay is from. + if err := k.ringClient.VerifyRelayRequestSignature(ctx, relayReq); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully verified relay request signature") + + // Verify the relay response's signature. + if err := relayRes.VerifySupplierSignature(supplierPubKey); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully verified relay response signature") + + // Verify the relay difficulty is above the minimum required to earn rewards. + if err := validateMiningDifficulty(relayBz); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully validated relay mining difficulty") + + // Validate that path the proof is submitted for matches the expected one + // based on the pseudo-random on-chain data associated with the header. + if err := k.validateClosestPath(ctx, sparseMerkleClosestProof, msg.GetSessionHeader()); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully validated proof path") + + // Retrieve the corresponding claim for the proof submitted so it can be + // used in the proof validation below. + claim, err := k.queryAndValidateClaimForProof(ctx, msg) + if err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully retrieved and validated claim") + + // Verify the proof's closest merkle proof. + if err := verifyClosestProof(sparseMerkleClosestProof, claim.GetRootHash()); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + logger.Debug("successfully verified closest merkle proof") + + // Construct and insert proof after all validation. + proof := types.Proof{ + SupplierAddress: supplierAddr, + SessionHeader: msg.GetSessionHeader(), + ClosestMerkleProof: msg.GetProof(), + } // TODO_BLOCKER: check if this proof already exists and return an appropriate error // in any case where the supplier should no longer be able to update the given proof. @@ -72,35 +196,36 @@ func (k msgServer) SubmitProof(ctx context.Context, msg *types.MsgSubmitProof) ( // TODO_UPNEXT(@Olshansk, #359): Call `tokenomics.SettleSessionAccounting()` here - logger. - With( - "session_id", proof.GetSessionHeader().GetSessionId(), - "session_end_height", proof.GetSessionHeader().GetSessionEndBlockHeight(), - "supplier", proof.GetSupplierAddress(), - ). - Debug("created proof") + logger.Debug("successfully submitted proof") return &types.MsgSubmitProofResponse{}, nil } -// queryAndValidateClaimForProof ensures that a claim corresponding to the given proof's -// session exists & has a matching supplier address and session header. -func (k msgServer) queryAndValidateClaimForProof(ctx context.Context, proof *types.Proof) error { - sessionId := proof.GetSessionHeader().GetSessionId() +// queryAndValidateClaimForProof ensures that a claim corresponding to the given +// proof's session exists & has a matching supplier address and session header, +// it then returns the corresponding claim if the validation is successful. +func (k msgServer) queryAndValidateClaimForProof( + ctx context.Context, + msg *types.MsgSubmitProof, +) (*types.Claim, error) { + sessionId := msg.GetSessionHeader().GetSessionId() // NB: no need to assert the testSessionId or supplier address as it is retrieved // by respective values of the given proof. I.e., if the claim exists, then these // values are guaranteed to match. - foundClaim, found := k.GetClaim(ctx, sessionId, proof.GetSupplierAddress()) + foundClaim, found := k.GetClaim(ctx, sessionId, msg.GetSupplierAddress()) if !found { - return types.ErrProofClaimNotFound.Wrapf("no claim found for session ID %q and supplier %q", sessionId, proof.GetSupplierAddress()) + return nil, types.ErrProofClaimNotFound.Wrapf( + "no claim found for session ID %q and supplier %q", + sessionId, msg.GetSupplierAddress(), + ) } claimSessionHeader := foundClaim.GetSessionHeader() - proofSessionHeader := proof.GetSessionHeader() + proofSessionHeader := msg.GetSessionHeader() // Ensure session start heights match. if claimSessionHeader.GetSessionStartBlockHeight() != proofSessionHeader.GetSessionStartBlockHeight() { - return types.ErrProofInvalidSessionStartHeight.Wrapf( + return nil, types.ErrProofInvalidSessionStartHeight.Wrapf( "claim session start height %d does not match proof session start height %d", claimSessionHeader.GetSessionStartBlockHeight(), proofSessionHeader.GetSessionStartBlockHeight(), @@ -109,7 +234,7 @@ func (k msgServer) queryAndValidateClaimForProof(ctx context.Context, proof *typ // Ensure session end heights match. if claimSessionHeader.GetSessionEndBlockHeight() != proofSessionHeader.GetSessionEndBlockHeight() { - return types.ErrProofInvalidSessionEndHeight.Wrapf( + return nil, types.ErrProofInvalidSessionEndHeight.Wrapf( "claim session end height %d does not match proof session end height %d", claimSessionHeader.GetSessionEndBlockHeight(), proofSessionHeader.GetSessionEndBlockHeight(), @@ -118,7 +243,7 @@ func (k msgServer) queryAndValidateClaimForProof(ctx context.Context, proof *typ // Ensure application addresses match. if claimSessionHeader.GetApplicationAddress() != proofSessionHeader.GetApplicationAddress() { - return types.ErrProofInvalidAddress.Wrapf( + return nil, types.ErrProofInvalidAddress.Wrapf( "claim application address %q does not match proof application address %q", claimSessionHeader.GetApplicationAddress(), proofSessionHeader.GetApplicationAddress(), @@ -127,12 +252,165 @@ func (k msgServer) queryAndValidateClaimForProof(ctx context.Context, proof *typ // Ensure service IDs match. if claimSessionHeader.GetService().GetId() != proofSessionHeader.GetService().GetId() { - return types.ErrProofInvalidService.Wrapf( + return nil, types.ErrProofInvalidService.Wrapf( "claim service ID %q does not match proof service ID %q", claimSessionHeader.GetService().GetId(), proofSessionHeader.GetService().GetId(), ) } + return &foundClaim, nil +} + +// compareSessionHeaders compares a session header against an expected session header. +// This is necessary to validate the proof's session header against both the relay +// request and response's session headers. +func compareSessionHeaders(expectedSessionHeader, sessionHeader *sessiontypes.SessionHeader) error { + // Compare the Application address. + if sessionHeader.GetApplicationAddress() != expectedSessionHeader.GetApplicationAddress() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders application addresses mismatch expect: %q, got: %q", + expectedSessionHeader.GetApplicationAddress(), + sessionHeader.GetApplicationAddress(), + ) + } + + // Compare the Service IDs. + if sessionHeader.GetService().GetId() != expectedSessionHeader.GetService().GetId() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders service IDs mismatch expect: %q, got: %q", + expectedSessionHeader.GetService().GetId(), + sessionHeader.GetService().GetId(), + ) + } + + // Compare the Service names. + if sessionHeader.GetService().GetName() != expectedSessionHeader.GetService().GetName() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders service names mismatch expect: %q, got: %q", + expectedSessionHeader.GetService().GetName(), + sessionHeader.GetService().GetName(), + ) + } + + // Compare the Session start block heights. + if sessionHeader.GetSessionStartBlockHeight() != expectedSessionHeader.GetSessionStartBlockHeight() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders session start heights mismatch expect: %d, got: %d", + expectedSessionHeader.GetSessionStartBlockHeight(), + sessionHeader.GetSessionStartBlockHeight(), + ) + } + + // Compare the Session end block heights. + if sessionHeader.GetSessionEndBlockHeight() != expectedSessionHeader.GetSessionEndBlockHeight() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders session end heights mismatch expect: %d, got: %d", + expectedSessionHeader.GetSessionEndBlockHeight(), + sessionHeader.GetSessionEndBlockHeight(), + ) + } + + // Compare the Session IDs. + if sessionHeader.GetSessionId() != expectedSessionHeader.GetSessionId() { + return types.ErrProofInvalidRelay.Wrapf( + "sessionHeaders session IDs mismatch expect: %q, got: %q", + expectedSessionHeader.GetSessionId(), + sessionHeader.GetSessionId(), + ) + } + + return nil +} + +// verifyClosestProof verifies the the correctness of the ClosestMerkleProof +// against the root hash committed to when creating the claim. +func verifyClosestProof( + proof *smt.SparseMerkleClosestProof, + claimRootHash []byte, +) error { + valid, err := smt.VerifyClosestProof(proof, claimRootHash, spec) + if err != nil { + return err + } + + if !valid { + return types.ErrProofInvalidProof.Wrap("invalid closest merkle proof") + } + + return nil +} + +// validateMiningDifficulty ensures that the relay's mining difficulty meets the +// required minimum threshold. +// TODO_TECHDEBT: Factor out the relay mining difficulty validation into a shared +// function that can be used by both the proof and the miner packages. +func validateMiningDifficulty(relayBz []byte) error { + hasher := sha256.New() + hasher.Write(relayBz) + relayHash := hasher.Sum(nil) + + difficultyBits, err := protocol.CountDifficultyBits(relayHash) + if err != nil { + return types.ErrProofInvalidRelay.Wrapf( + "error counting difficulty bits: %s", + err, + ) + } + + // TODO: Devise a test that tries to attack the network and ensure that there + // is sufficient telemetry. + if difficultyBits < relayMinDifficultyBits { + return types.ErrProofInvalidRelay.Wrapf( + "relay difficulty %d is less than the required difficulty %d", + difficultyBits, + relayMinDifficultyBits, + ) + } + + return nil +} + +// validateClosestPath ensures that the proof's path matches the expected path. +// Since the proof path needs to be pseudo-randomly selected AFTER the session +// ends, the seed for this is the block hash at the height when the proof window +// opens. +func (k msgServer) validateClosestPath( + ctx context.Context, + proof *smt.SparseMerkleClosestProof, + sessionHeader *sessiontypes.SessionHeader, +) error { + // The RelayMiner has to wait until the createClaimWindowStartHeight and the + // submitProofWindowStartHeight windows are open to create the claim and + // submit the proof respectively. + // These windows are calculated as (SessionEndBlockHeight + GracePeriodBlockCount). + // + // For reference, see relayerSessionsManager.waitForEarliest{CreateClaim,SubmitProof}Height(). + // + // The RelayMiner has to wait this long to ensure that late relays (i.e. + // submitted during SessionNumber=(N+1) but created during SessionNumber=N) are + // still included as part of SessionNumber=N. + // + // Since smt.ProveClosest is defined in terms of submitProofWindowStartHeight, + // this block's hash needs to be used for validation too. + // + // TODO_TECHDEBT(#409): Reference the session rollover documentation here. + // TODO_BLOCKER: Update `blockHeight` to be the value of when the `ProofWindow` + // opens once the variable is added. + sessionEndBlockHeightWithGracePeriod := sessionHeader.GetSessionEndBlockHeight() + + sessionkeeper.GetSessionGracePeriodBlockCount() + blockHash := k.sessionKeeper.GetBlockHash(ctx, sessionEndBlockHeightWithGracePeriod) + + // TODO_BLOCKER(@Olshansk, @red-0ne, @h5law): The seed of the path should be + // `ConcatAndHash(blockHash, '.', sessionId)` to prevent all proofs needing to use the same path. + // See the conversation in the following thread for more details: https://github.com/pokt-network/poktroll/pull/406#discussion_r1520790083 + if !bytes.Equal(proof.Path, blockHash) { + return types.ErrProofInvalidProof.Wrapf( + "proof path %x does not match block hash %x", + proof.Path, + blockHash, + ) + } + return nil } diff --git a/x/proof/module/module.go b/x/proof/module/module.go index 8eb8cacde..07ba82954 100644 --- a/x/proof/module/module.go +++ b/x/proof/module/module.go @@ -97,20 +97,17 @@ type AppModule struct { keeper keeper.Keeper accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper } func NewAppModule( cdc codec.Codec, keeper keeper.Keeper, accountKeeper types.AccountKeeper, - bankKeeper types.BankKeeper, ) AppModule { return AppModule{ AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, accountKeeper: accountKeeper, - bankKeeper: bankKeeper, } } @@ -177,9 +174,9 @@ type ModuleInputs struct { Config *modulev1.Module Logger log.Logger - AccountKeeper types.AccountKeeper - BankKeeper types.BankKeeper - SessionKeeper types.SessionKeeper + SessionKeeper types.SessionKeeper + ApplicationKeeper types.ApplicationKeeper + AccountKeeper types.AccountKeeper } type ModuleOutputs struct { @@ -201,12 +198,13 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { in.Logger, authority.String(), in.SessionKeeper, + in.ApplicationKeeper, + in.AccountKeeper, ) m := NewAppModule( in.Cdc, k, in.AccountKeeper, - in.BankKeeper, ) return ModuleOutputs{ProofKeeper: k, Module: m} diff --git a/x/proof/module/simulation.go b/x/proof/module/simulation.go index 0458e0998..0f10c54b1 100644 --- a/x/proof/module/simulation.go +++ b/x/proof/module/simulation.go @@ -67,7 +67,7 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) operations = append(operations, simulation.NewWeightedOperation( weightMsgCreateClaim, - proofsimulation.SimulateMsgCreateClaim(am.accountKeeper, am.bankKeeper, am.keeper), + proofsimulation.SimulateMsgCreateClaim(am.accountKeeper, am.keeper), )) var weightMsgSubmitProof int @@ -78,7 +78,7 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) operations = append(operations, simulation.NewWeightedOperation( weightMsgSubmitProof, - proofsimulation.SimulateMsgSubmitProof(am.accountKeeper, am.bankKeeper, am.keeper), + proofsimulation.SimulateMsgSubmitProof(am.accountKeeper, am.keeper), )) // this line is used by starport scaffolding # simapp/module/operation @@ -93,7 +93,7 @@ func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.Wei opWeightMsgCreateClaim, defaultWeightMsgCreateClaim, func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { - proofsimulation.SimulateMsgCreateClaim(am.accountKeeper, am.bankKeeper, am.keeper) + proofsimulation.SimulateMsgCreateClaim(am.accountKeeper, am.keeper) return nil }, ), @@ -101,7 +101,7 @@ func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.Wei opWeightMsgSubmitProof, defaultWeightMsgSubmitProof, func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { - proofsimulation.SimulateMsgSubmitProof(am.accountKeeper, am.bankKeeper, am.keeper) + proofsimulation.SimulateMsgSubmitProof(am.accountKeeper, am.keeper) return nil }, ), diff --git a/x/proof/simulation/create_claim.go b/x/proof/simulation/create_claim.go index 4abeddc02..08b6b7ddb 100644 --- a/x/proof/simulation/create_claim.go +++ b/x/proof/simulation/create_claim.go @@ -13,7 +13,6 @@ import ( func SimulateMsgCreateClaim( ak types.AccountKeeper, - bk types.BankKeeper, k keeper.Keeper, ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, diff --git a/x/proof/simulation/submit_proof.go b/x/proof/simulation/submit_proof.go index 4b0fa83c6..0b6bfeacb 100644 --- a/x/proof/simulation/submit_proof.go +++ b/x/proof/simulation/submit_proof.go @@ -13,7 +13,6 @@ import ( func SimulateMsgSubmitProof( ak types.AccountKeeper, - bk types.BankKeeper, k keeper.Keeper, ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, diff --git a/x/proof/types/account_query_client.go b/x/proof/types/account_query_client.go new file mode 100644 index 000000000..9d89b44d7 --- /dev/null +++ b/x/proof/types/account_query_client.go @@ -0,0 +1,73 @@ +package types + +import ( + "context" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" + + "github.com/pokt-network/poktroll/pkg/client" +) + +var _ client.AccountQueryClient = (*AccountKeeperQueryClient)(nil) + +// AccountKeeperQueryClient is a thin wrapper around the AccountKeeper. +// It does not rely on the QueryClient, and therefore does not make any +// network requests as in the off-chain implementation. +type AccountKeeperQueryClient struct { + keeper AccountKeeper +} + +// NewAccountKeeperQueryClient returns a new AccountQueryClient that is backed +// by an AccountKeeper instance. +// It is used by the PubKeyClient to get the public key that corresponds to the +// provided address. +// It should be injected into the PubKeyClient when initialized from within the a keeper. +func NewAccountKeeperQueryClient(accountKeeper AccountKeeper) client.AccountQueryClient { + return &AccountKeeperQueryClient{keeper: accountKeeper} +} + +// GetAccount returns the account associated with the provided address. +func (accountQueryClient *AccountKeeperQueryClient) GetAccount( + ctx context.Context, + addr string, +) (account types.AccountI, err error) { + addrBz, err := types.AccAddressFromBech32(addr) + if err != nil { + return nil, err + } + + // keeper.GetAccount panics if the account is not found. + // Capture the panic and return an error if one occurs. + defer func() { + if r := recover(); r != nil { + err = ErrProofPubKeyNotFound + account = nil + } + }() + + // Retrieve an account from the account keeper. + account = accountQueryClient.keeper.GetAccount(ctx, addrBz) + + return account, err +} + +// GetPubKeyFromAddress returns the public key of the given address. +// It uses the accountQuerier to get the account and then returns its public key. +func (accountQueryClient *AccountKeeperQueryClient) GetPubKeyFromAddress( + ctx context.Context, + address string, +) (cryptotypes.PubKey, error) { + acc, err := accountQueryClient.GetAccount(ctx, address) + if err != nil { + return nil, err + } + + // If the account's public key is nil, then return an error. + pubKey := acc.GetPubKey() + if pubKey == nil { + return nil, ErrProofPubKeyNotFound + } + + return pubKey, nil +} diff --git a/x/proof/types/application_query_client.go b/x/proof/types/application_query_client.go new file mode 100644 index 000000000..1cd887314 --- /dev/null +++ b/x/proof/types/application_query_client.go @@ -0,0 +1,44 @@ +package types + +import ( + "context" + + "github.com/pokt-network/poktroll/pkg/client" + apptypes "github.com/pokt-network/poktroll/x/application/types" +) + +var _ client.ApplicationQueryClient = (*AppKeeperQueryClient)(nil) + +// AppKeeperQueryClient is a thin wrapper around the AccountKeeper. +// It does not rely on the QueryClient, and therefore does not make any +// network requests as in the off-chain implementation. +type AppKeeperQueryClient struct { + keeper ApplicationKeeper +} + +// NewAppKeeperQueryClient returns a new ApplicationQueryClient that is backed +// by an ApplicationKeeper instance. +// It is used by the RingClient to get the gateway address that an application +// has delegated its signing power to. +// It should be injected into the RingClient when initialized from within the a keeper. +func NewAppKeeperQueryClient(appKeeper ApplicationKeeper) client.ApplicationQueryClient { + return &AppKeeperQueryClient{keeper: appKeeper} +} + +// GetApplication returns the application corresponding to the given address. +func (appQueryClient *AppKeeperQueryClient) GetApplication( + ctx context.Context, + appAddr string, +) (apptypes.Application, error) { + foundApp, appFound := appQueryClient.keeper.GetApplication(ctx, appAddr) + if !appFound { + return apptypes.Application{}, ErrProofApplicationNotFound + } + + return foundApp, nil +} + +// GetAllApplications returns all the applications in the application store. +func (appQueryClient *AppKeeperQueryClient) GetAllApplications(ctx context.Context) ([]apptypes.Application, error) { + return appQueryClient.keeper.GetAllApplications(ctx), nil +} diff --git a/x/proof/types/errors.go b/x/proof/types/errors.go index 6f686ed68..414bf467a 100644 --- a/x/proof/types/errors.go +++ b/x/proof/types/errors.go @@ -18,7 +18,10 @@ var ( ErrProofClaimNotFound = sdkerrors.Register(ModuleName, 1109, "claim not found") ErrProofProofNotFound = sdkerrors.Register(ModuleName, 1110, "proof not found") ErrProofInvalidProof = sdkerrors.Register(ModuleName, 1111, "invalid proof") - //ErrProofUnauthorized = sdkerrors.Register(ModuleName, 1112, "unauthorized supplier signer") - //ErrProofInvalidServiceConfig = sdkerrors.Register(ModuleName, 1113, "invalid service config") - //ErrProofInvalidClosestMerkleProof = sdkerrors.Register(ModuleName, 1114, "invalid closest merkle proof") + ErrProofInvalidRelay = sdkerrors.Register(ModuleName, 1112, "invalid relay") + ErrProofInvalidRelayRequest = sdkerrors.Register(ModuleName, 1113, "invalid relay request") + ErrProofInvalidRelayResponse = sdkerrors.Register(ModuleName, 1114, "invalid relay response") + ErrProofNotSecp256k1Curve = sdkerrors.Register(ModuleName, 1115, "not secp256k1 curve") + ErrProofApplicationNotFound = sdkerrors.Register(ModuleName, 1116, "application not found") + ErrProofPubKeyNotFound = sdkerrors.Register(ModuleName, 1117, "public key not found") ) diff --git a/x/proof/types/expected_keepers.go b/x/proof/types/expected_keepers.go index a9d8eaf71..ef1555452 100644 --- a/x/proof/types/expected_keepers.go +++ b/x/proof/types/expected_keepers.go @@ -7,21 +7,29 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + apptypes "github.com/pokt-network/poktroll/x/application/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) type SessionKeeper interface { GetSession(context.Context, *sessiontypes.QueryGetSessionRequest) (*sessiontypes.QueryGetSessionResponse, error) + GetBlockHash(ctx context.Context, height int64) []byte } // AccountKeeper defines the expected interface for the Account module. type AccountKeeper interface { - GetAccount(context.Context, sdk.AccAddress) sdk.AccountI // only used for simulation - // Methods imported from account should be defined here + GetAccount(context.Context, sdk.AccAddress) sdk.AccountI } // BankKeeper defines the expected interface for the Bank module. type BankKeeper interface { SpendableCoins(context.Context, sdk.AccAddress) sdk.Coins - // Methods imported from bank should be defined here + DelegateCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + UndelegateCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +// ApplicationKeeper defines the expected application keeper to retrieve applications +type ApplicationKeeper interface { + GetApplication(ctx context.Context, address string) (app apptypes.Application, found bool) + GetAllApplications(ctx context.Context) []apptypes.Application } diff --git a/x/service/types/errors.go b/x/service/types/errors.go index cd4ea82b6..d94a48fc8 100644 --- a/x/service/types/errors.go +++ b/x/service/types/errors.go @@ -6,14 +6,16 @@ import sdkerrors "cosmossdk.io/errors" // x/service module sentinel errors var ( - ErrServiceInvalidSigner = sdkerrors.Register(ModuleName, 1100, "expected gov account as only signer for proposal message") - ErrServiceDuplicateIndex = sdkerrors.Register(ModuleName, 1101, "duplicate index when adding a new service") - ErrServiceInvalidAddress = sdkerrors.Register(ModuleName, 1102, "invalid address when adding a new service") - ErrServiceMissingID = sdkerrors.Register(ModuleName, 1103, "missing service ID") - ErrServiceMissingName = sdkerrors.Register(ModuleName, 1104, "missing service name") - ErrServiceAlreadyExists = sdkerrors.Register(ModuleName, 1105, "service already exists") - ErrServiceInvalidServiceFee = sdkerrors.Register(ModuleName, 1106, "invalid service fee") - ErrServiceAccountNotFound = sdkerrors.Register(ModuleName, 1107, "account not found") - ErrServiceNotEnoughFunds = sdkerrors.Register(ModuleName, 1108, "not enough funds to add service") - ErrServiceFailedToDeductFee = sdkerrors.Register(ModuleName, 1109, "failed to deduct fee") + ErrServiceInvalidSigner = sdkerrors.Register(ModuleName, 1100, "expected gov account as only signer for proposal message") + ErrServiceDuplicateIndex = sdkerrors.Register(ModuleName, 1101, "duplicate index when adding a new service") + ErrServiceInvalidAddress = sdkerrors.Register(ModuleName, 1102, "invalid address when adding a new service") + ErrServiceMissingID = sdkerrors.Register(ModuleName, 1103, "missing service ID") + ErrServiceMissingName = sdkerrors.Register(ModuleName, 1104, "missing service name") + ErrServiceAlreadyExists = sdkerrors.Register(ModuleName, 1105, "service already exists") + ErrServiceInvalidServiceFee = sdkerrors.Register(ModuleName, 1106, "invalid service fee") + ErrServiceAccountNotFound = sdkerrors.Register(ModuleName, 1107, "account not found") + ErrServiceNotEnoughFunds = sdkerrors.Register(ModuleName, 1108, "not enough funds to add service") + ErrServiceFailedToDeductFee = sdkerrors.Register(ModuleName, 1109, "failed to deduct fee") + ErrServiceInvalidRelayResponse = sdkerrors.Register(ModuleName, 1110, "invalid relay response") + ErrServiceInvalidRelayRequest = sdkerrors.Register(ModuleName, 1111, "invalid relay request") ) diff --git a/x/service/types/relay.go b/x/service/types/relay.go index 7815fc1ab..cbc45161c 100644 --- a/x/service/types/relay.go +++ b/x/service/types/relay.go @@ -1,57 +1,101 @@ package types -import "crypto/sha256" +import ( + "crypto/sha256" -// getSignableBytes returns the bytes resulting from marshaling the relay request -// A value receiver is used to avoid overwriting any pre-existing signature -func (req RelayRequest) getSignableBytes() ([]byte, error) { - // set signature to nil - req.Meta.Signature = nil - - return req.Marshal() -} + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) // GetSignableBytesHash returns the hash of the signable bytes of the relay request // Hashing the marshaled request message guarantees that the signable bytes are // always of a constant and expected length. -func (req *RelayRequest) GetSignableBytesHash() ([32]byte, error) { - requestBz, err := req.getSignableBytes() +func (req RelayRequest) GetSignableBytesHash() ([32]byte, error) { + // req and req.Meta are not pointers, so we can set the signature to nil + // in order to generate the signable bytes hash without the need restore it. + req.Meta.Signature = nil + requestBz, err := req.Marshal() if err != nil { return [32]byte{}, err } - // return the marshaled request hash to guarantee that the signable bytes are - // always of a constant and expected length + // return the marshaled request hash to guarantee that the signable bytes + // are always of a constant and expected length return sha256.Sum256(requestBz), nil } -// getSignableBytes returns the bytes resulting from marshaling the relay response -// A value receiver is used to avoid overwriting any pre-existing signature -func (res RelayResponse) getSignableBytes() ([]byte, error) { - // set signature to nil - res.Meta.SupplierSignature = nil +// ValidateBasic performs basic validation of the RelayResponse Meta, SessionHeader +// and Signature fields. +// TODO_TEST: Add tests for RelayRequest validation +func (req *RelayRequest) ValidateBasic() error { + meta := req.GetMeta() + if meta.GetSessionHeader() == nil { + return ErrServiceInvalidRelayRequest.Wrap("missing session header") + } - return res.Marshal() + if err := meta.GetSessionHeader().ValidateBasic(); err != nil { + return ErrServiceInvalidRelayRequest.Wrapf("invalid session header: %s", err) + } + + if len(meta.GetSignature()) == 0 { + return ErrServiceInvalidRelayRequest.Wrap("missing application signature") + } + + return nil } // GetSignableBytesHash returns the hash of the signable bytes of the relay response // Hashing the marshaled response message guarantees that the signable bytes are // always of a constant and expected length. -func (res *RelayResponse) GetSignableBytesHash() ([32]byte, error) { - responseBz, err := res.getSignableBytes() +func (res RelayResponse) GetSignableBytesHash() ([32]byte, error) { + // res and res.Meta are not pointers, so we can set the signature to nil + // in order to generate the signable bytes hash without the need restore it. + res.Meta.SupplierSignature = nil + responseBz, err := res.Marshal() if err != nil { return [32]byte{}, err } - // return the marshaled response hash to guarantee that the signable bytes are - // always of a constant and expected length + // return the marshaled response hash to guarantee that the signable bytes + // are always of a constant and expected length return sha256.Sum256(responseBz), nil } +// ValidateBasic performs basic validation of the RelayResponse Meta, SessionHeader +// and SupplierSignature fields. +// TODO_TEST: Add tests for RelayResponse validation func (res *RelayResponse) ValidateBasic() error { // TODO_FUTURE: if a client gets a response with an invalid/incomplete // SessionHeader, consider sending an on-chain challenge, lowering their // QoS, or other future work. + meta := res.GetMeta() + if meta.GetSessionHeader() == nil { + return ErrServiceInvalidRelayResponse.Wrap("missing meta") + } + + if err := meta.GetSessionHeader().ValidateBasic(); err != nil { + return ErrServiceInvalidRelayResponse.Wrapf("invalid session header: %v", err) + } + + if len(meta.GetSupplierSignature()) == 0 { + return ErrServiceInvalidRelayResponse.Wrap("missing supplier signature") + } + + return nil +} + +// VerifySupplierSignature ensures the signature provided by the supplier is +// valid according to their relay response. +func (res *RelayResponse) VerifySupplierSignature(supplierPubKey cryptotypes.PubKey) error { + // Get the signable bytes hash of the response. + signableBz, err := res.GetSignableBytesHash() + if err != nil { + return err + } + + if ok := supplierPubKey.VerifySignature(signableBz[:], res.GetMeta().SupplierSignature); !ok { + return ErrServiceInvalidRelayResponse.Wrap("invalid signature") + } + return nil }