diff --git a/Dockerfile b/Dockerfile index bfe2945..d839fc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /app COPY . . ENV GOPROXY=https://goproxy.io,direct -RUN cd server && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.Version=${VERSION}' -X 'main.Commit=${COMMIT}' -X 'main.Date=${DATE}}'" -o ../bin/arkd cmd/arkd/main.go +RUN cd server && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.Version=${VERSION}' -X 'main.Commit=${COMMIT}' -X 'main.Date=${DATE}}'" -o ../bin/arkd ./cmd/arkd RUN cd client && CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.Version=${VERSION}}' -X 'main.Commit=${COMMIT}' -X 'main.Date=${DATE}}'" -o ../bin/ark . # Second image, running the arkd executable diff --git a/client/covenant/onboard.go b/client/covenant/onboard.go index 6627ed1..da19060 100644 --- a/client/covenant/onboard.go +++ b/client/covenant/onboard.go @@ -15,11 +15,9 @@ import ( const minRelayFee = 30 func (c *covenantLiquidCLI) Onboard(ctx *cli.Context) error { - isTrusted := ctx.Bool("trusted") - amount := ctx.Uint64("amount") - if !isTrusted && amount <= 0 { + if amount <= 0 { return fmt.Errorf("missing amount flag (--amount)") } @@ -39,19 +37,6 @@ func (c *covenantLiquidCLI) Onboard(ctx *cli.Context) error { } defer cancel() - if isTrusted { - resp, err := client.TrustedOnboarding(ctx.Context, &arkv1.TrustedOnboardingRequest{ - UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()), - }) - if err != nil { - return err - } - - return utils.PrintJSON(map[string]interface{}{ - "onboard_address": resp.Address, - }) - } - aspPubkey, err := utils.GetAspPublicKey(ctx) if err != nil { return err diff --git a/client/covenantless/onboard.go b/client/covenantless/onboard.go index 6824e85..a92bb97 100644 --- a/client/covenantless/onboard.go +++ b/client/covenantless/onboard.go @@ -17,11 +17,9 @@ import ( ) func (c *clArkBitcoinCLI) Onboard(ctx *cli.Context) error { - isTrusted := ctx.Bool("trusted") - amount := ctx.Uint64("amount") - if !isTrusted && amount <= 0 { + if amount <= 0 { return fmt.Errorf("missing amount flag (--amount)") } @@ -41,19 +39,6 @@ func (c *clArkBitcoinCLI) Onboard(ctx *cli.Context) error { } defer cancel() - if isTrusted { - resp, err := client.TrustedOnboarding(ctx.Context, &arkv1.TrustedOnboardingRequest{ - UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()), - }) - if err != nil { - return err - } - - return utils.PrintJSON(map[string]interface{}{ - "onboard_address": resp.Address, - }) - } - aspPubkey, err := utils.GetAspPublicKey(ctx) if err != nil { return err diff --git a/client/flags/flags.go b/client/flags/flags.go index a6117bc..811d83f 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -25,10 +25,6 @@ var ( Name: "amount", Usage: "amount to onboard in sats", } - TrustedOnboardFlag = cli.BoolFlag{ - Name: "trusted", - Usage: "trusted onboard", - } ExpiryDetailsFlag = cli.BoolFlag{ Name: "compute-expiry-details", Usage: "compute client-side the VTXOs expiry time", diff --git a/client/go.mod b/client/go.mod index 5419319..526fe9a 100644 --- a/client/go.mod +++ b/client/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark-cli -go 1.22.2 +go 1.22.4 replace github.com/ark-network/ark/common => ../common @@ -14,7 +14,7 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.9 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 - github.com/urfave/cli/v2 v2.27.2 + github.com/urfave/cli/v2 v2.27.3 golang.org/x/crypto v0.25.0 golang.org/x/term v0.22.0 ) @@ -31,7 +31,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/vulpemventures/go-elements v0.5.4 - github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/client/go.sum b/client/go.sum index 55e6c28..459c5de 100644 --- a/client/go.sum +++ b/client/go.sum @@ -84,16 +84,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= +github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 h1:CTcw80hz/Sw8hqlKX5ZYvBUF5gAHSHwdjXxRf/cjDcI= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:GXBJykxW2kUcktGdsgyay7uwwWvkljASfljNcT0mbh8= github.com/vulpemventures/go-elements v0.5.4 h1:l94xoa9aYPPWiOB7Pmi08rKYvdk/n/sQIbLkQfEAASc= github.com/vulpemventures/go-elements v0.5.4/go.mod h1:Tvhb+rZWv3lxoI5CdK03J3V+e2QVr/7UAnCYILxFSq4= github.com/vulpemventures/go-secp256k1-zkp v1.1.6 h1:BmsrmXRLUibwa75Qkk8yELjpzCzlAjYFGLiLiOdq7Xo= github.com/vulpemventures/go-secp256k1-zkp v1.1.6/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/client/main.go b/client/main.go index 084b324..1b82d76 100644 --- a/client/main.go +++ b/client/main.go @@ -86,7 +86,7 @@ var ( } return cli.Onboard(ctx) }, - Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.TrustedOnboardFlag, &flags.PasswordFlag}, + Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.PasswordFlag}, } sendCommand = cli.Command{ diff --git a/docker-compose.clark.regtest.yml b/docker-compose.clark.regtest.yml index f6a7baf..ea7577a 100644 --- a/docker-compose.clark.regtest.yml +++ b/docker-compose.clark.regtest.yml @@ -14,6 +14,8 @@ services: - ARK_MIN_RELAY_FEE=200 - ARK_NEUTRINO_PEER=bitcoin:18444 - ARK_ESPLORA_URL=http://chopsticks:3000 + - ARK_NO_TLS=true + - ARK_NO_MACAROONS=true ports: - "6000:6000" diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml index 01396de..5c0c615 100644 --- a/docker-compose.regtest.yml +++ b/docker-compose.regtest.yml @@ -31,6 +31,8 @@ services: - ARK_DB_TYPE=sqlite - ARK_TX_BUILDER_TYPE=covenant - ARK_PORT=8080 + - ARK_NO_TLS=true + - ARK_NO_MACAROONS=true ports: - "8080:8080" diff --git a/pkg/client-sdk/client/rest/service/arkservice/ark_service/ark_service_client.go b/pkg/client-sdk/client/rest/service/arkservice/ark_service/ark_service_client.go index 054f0c1..b153acd 100644 --- a/pkg/client-sdk/client/rest/service/arkservice/ark_service/ark_service_client.go +++ b/pkg/client-sdk/client/rest/service/arkservice/ark_service/ark_service_client.go @@ -74,8 +74,6 @@ type ClientService interface { ArkServiceRegisterPayment(params *ArkServiceRegisterPaymentParams, opts ...ClientOption) (*ArkServiceRegisterPaymentOK, error) - ArkServiceTrustedOnboarding(params *ArkServiceTrustedOnboardingParams, opts ...ClientOption) (*ArkServiceTrustedOnboardingOK, error) - SetTransport(transport runtime.ClientTransport) } @@ -449,43 +447,6 @@ func (a *Client) ArkServiceRegisterPayment(params *ArkServiceRegisterPaymentPara return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) } -/* -ArkServiceTrustedOnboarding ark service trusted onboarding API -*/ -func (a *Client) ArkServiceTrustedOnboarding(params *ArkServiceTrustedOnboardingParams, opts ...ClientOption) (*ArkServiceTrustedOnboardingOK, error) { - // TODO: Validate the params before sending - if params == nil { - params = NewArkServiceTrustedOnboardingParams() - } - op := &runtime.ClientOperation{ - ID: "ArkService_TrustedOnboarding", - Method: "POST", - PathPattern: "/v1/onboard/address", - ProducesMediaTypes: []string{"application/json"}, - ConsumesMediaTypes: []string{"application/json"}, - Schemes: []string{"http"}, - Params: params, - Reader: &ArkServiceTrustedOnboardingReader{formats: a.formats}, - Context: params.Context, - Client: params.HTTPClient, - } - for _, opt := range opts { - opt(op) - } - - result, err := a.transport.Submit(op) - if err != nil { - return nil, err - } - success, ok := result.(*ArkServiceTrustedOnboardingOK) - if ok { - return success, nil - } - // unexpected success response - unexpectedSuccess := result.(*ArkServiceTrustedOnboardingDefault) - return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) -} - // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/pkg/client-sdk/example/covenant/wasm/go.mod b/pkg/client-sdk/example/covenant/wasm/go.mod index d07636c..8ce2efa 100644 --- a/pkg/client-sdk/example/covenant/wasm/go.mod +++ b/pkg/client-sdk/example/covenant/wasm/go.mod @@ -1,6 +1,6 @@ module ark/pkg/client-sdk/example -go 1.22.2 +go 1.22.4 replace github.com/ark-network/ark => ./../../../../../server diff --git a/pkg/client-sdk/example/covenantless/wasm/go.mod b/pkg/client-sdk/example/covenantless/wasm/go.mod index d07636c..8ce2efa 100644 --- a/pkg/client-sdk/example/covenantless/wasm/go.mod +++ b/pkg/client-sdk/example/covenantless/wasm/go.mod @@ -1,6 +1,6 @@ module ark/pkg/client-sdk/example -go 1.22.2 +go 1.22.4 replace github.com/ark-network/ark => ./../../../../../server diff --git a/pkg/client-sdk/go.mod b/pkg/client-sdk/go.mod index 08dc58d..00291d4 100644 --- a/pkg/client-sdk/go.mod +++ b/pkg/client-sdk/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/pkg/client-sdk -go 1.22.2 +go 1.22.4 replace github.com/ark-network/ark/common => ./../../common diff --git a/server/Makefile b/server/Makefile index 73cbedb..be23e60 100755 --- a/server/Makefile +++ b/server/Makefile @@ -38,12 +38,15 @@ run: clean export ARK_LOG_LEVEL=5; \ export ARK_NETWORK=liquidregtest; \ export ARK_PORT=8080; \ + export ARK_NO_TLS=true; \ + export ARK_NO_MACAROONS=true; \ go run ./cmd/arkd ## test: runs unit and component tests test: @echo "Running unit tests..." - @find . -name go.mod -execdir go test -v -count=1 -race ./internal/... \; + @go test -v -count=1 -race ./internal/... + @find ./pkg -name go.mod -execdir go test -v -count=1 -race ./... \; ## vet: code analysis vet: diff --git a/server/api-spec/openapi/swagger/ark/v1/service.swagger.json b/server/api-spec/openapi/swagger/ark/v1/service.swagger.json index 3904c55..df2f846 100644 --- a/server/api-spec/openapi/swagger/ark/v1/service.swagger.json +++ b/server/api-spec/openapi/swagger/ark/v1/service.swagger.json @@ -101,38 +101,6 @@ ] } }, - "/v1/onboard/address": { - "post": { - "operationId": "ArkService_TrustedOnboarding", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/v1TrustedOnboardingResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1TrustedOnboardingRequest" - } - } - ], - "tags": [ - "ArkService" - ] - } - }, "/v1/payment/claim": { "post": { "operationId": "ArkService_ClaimPayment", @@ -704,22 +672,6 @@ } } }, - "v1TrustedOnboardingRequest": { - "type": "object", - "properties": { - "userPubkey": { - "type": "string" - } - } - }, - "v1TrustedOnboardingResponse": { - "type": "object", - "properties": { - "address": { - "type": "string" - } - } - }, "v1Vtxo": { "type": "object", "properties": { diff --git a/server/api-spec/openapi/swagger/ark/v1/wallet.swagger.json b/server/api-spec/openapi/swagger/ark/v1/wallet.swagger.json index a86bd97..3127415 100644 --- a/server/api-spec/openapi/swagger/ark/v1/wallet.swagger.json +++ b/server/api-spec/openapi/swagger/ark/v1/wallet.swagger.json @@ -5,6 +5,9 @@ "version": "version not set" }, "tags": [ + { + "name": "WalletInitializerService" + }, { "name": "WalletService" } @@ -62,7 +65,7 @@ }, "/v1/admin/wallet/create": { "post": { - "operationId": "WalletService_Create", + "operationId": "WalletInitializerService_Create", "responses": { "200": { "description": "A successful response.", @@ -88,7 +91,7 @@ } ], "tags": [ - "WalletService" + "WalletInitializerService" ] } }, @@ -126,7 +129,7 @@ }, "/v1/admin/wallet/restore": { "post": { - "operationId": "WalletService_Restore", + "operationId": "WalletInitializerService_Restore", "responses": { "200": { "description": "A successful response.", @@ -152,13 +155,13 @@ } ], "tags": [ - "WalletService" + "WalletInitializerService" ] } }, "/v1/admin/wallet/seed": { "get": { - "operationId": "WalletService_GenSeed", + "operationId": "WalletInitializerService_GenSeed", "responses": { "200": { "description": "A successful response.", @@ -174,13 +177,13 @@ } }, "tags": [ - "WalletService" + "WalletInitializerService" ] } }, "/v1/admin/wallet/status": { "get": { - "operationId": "WalletService_GetStatus", + "operationId": "WalletInitializerService_GetStatus", "responses": { "200": { "description": "A successful response.", @@ -196,13 +199,13 @@ } }, "tags": [ - "WalletService" + "WalletInitializerService" ] } }, "/v1/admin/wallet/unlock": { "post": { - "operationId": "WalletService_Unlock", + "operationId": "WalletInitializerService_Unlock", "responses": { "200": { "description": "A successful response.", @@ -228,7 +231,7 @@ } ], "tags": [ - "WalletService" + "WalletInitializerService" ] } } @@ -347,6 +350,10 @@ }, "password": { "type": "string" + }, + "gapLimit": { + "type": "string", + "format": "uint64" } } }, diff --git a/server/api-spec/protobuf/ark/v1/service.proto b/server/api-spec/protobuf/ark/v1/service.proto index 293d03f..a1406d2 100755 --- a/server/api-spec/protobuf/ark/v1/service.proto +++ b/server/api-spec/protobuf/ark/v1/service.proto @@ -60,12 +60,6 @@ service ArkService { body: "*" }; } - rpc TrustedOnboarding(TrustedOnboardingRequest) returns (TrustedOnboardingResponse) { - option (google.api.http) = { - post: "/v1/onboard/address" - body: "*" - }; - } } message RegisterPaymentRequest { @@ -148,14 +142,6 @@ message OnboardRequest { message OnboardResponse { } -message TrustedOnboardingRequest { - string user_pubkey = 1; -} - -message TrustedOnboardingResponse { - string address = 1; -} - // EVENT TYPES message RoundFinalizationEvent { diff --git a/server/api-spec/protobuf/ark/v1/wallet.proto b/server/api-spec/protobuf/ark/v1/wallet.proto index 56d6e6b..d5eff80 100644 --- a/server/api-spec/protobuf/ark/v1/wallet.proto +++ b/server/api-spec/protobuf/ark/v1/wallet.proto @@ -4,7 +4,7 @@ package ark.v1; import "google/api/annotations.proto"; -service WalletService { +service WalletInitializerService { rpc GenSeed(GenSeedRequest) returns (GenSeedResponse) { option (google.api.http) = { get: "/v1/admin/wallet/seed" @@ -28,17 +28,20 @@ service WalletService { body: "*" }; } + rpc GetStatus(GetStatusRequest) returns (GetStatusResponse) { + option (google.api.http) = { + get: "/v1/admin/wallet/status" + }; + } +} + +service WalletService { rpc Lock(LockRequest) returns (LockResponse) { option (google.api.http) = { post: "/v1/admin/wallet/lock" body: "*" }; } - rpc GetStatus(GetStatusRequest) returns (GetStatusResponse) { - option (google.api.http) = { - get: "/v1/admin/wallet/status" - }; - } rpc DeriveAddress(DeriveAddressRequest) returns (DeriveAddressResponse) { option (google.api.http) = { get: "/v1/admin/wallet/address" @@ -65,6 +68,7 @@ message CreateResponse {} message RestoreRequest { string seed = 1; string password = 2; + uint64 gap_limit = 3; } message RestoreResponse {} diff --git a/server/api-spec/protobuf/gen/ark/v1/admin.pb.gw.go b/server/api-spec/protobuf/gen/ark/v1/admin.pb.gw.go index 6647354..521a687 100644 --- a/server/api-spec/protobuf/gen/ark/v1/admin.pb.gw.go +++ b/server/api-spec/protobuf/gen/ark/v1/admin.pb.gw.go @@ -131,6 +131,7 @@ func local_request_AdminService_GetRounds_0(ctx context.Context, marshaler runti // UnaryRPC :call AdminServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAdminServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. func RegisterAdminServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AdminServiceServer) error { mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -246,7 +247,7 @@ func RegisterAdminServiceHandler(ctx context.Context, mux *runtime.ServeMux, con // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AdminServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AdminServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "AdminServiceClient" to call the correct interceptors. +// "AdminServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterAdminServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AdminServiceClient) error { mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { diff --git a/server/api-spec/protobuf/gen/ark/v1/service.pb.go b/server/api-spec/protobuf/gen/ark/v1/service.pb.go index fe8d0ab..6fdebd3 100644 --- a/server/api-spec/protobuf/gen/ark/v1/service.pb.go +++ b/server/api-spec/protobuf/gen/ark/v1/service.pb.go @@ -1104,100 +1104,6 @@ func (*OnboardResponse) Descriptor() ([]byte, []int) { return file_ark_v1_service_proto_rawDescGZIP(), []int{19} } -type TrustedOnboardingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - UserPubkey string `protobuf:"bytes,1,opt,name=user_pubkey,json=userPubkey,proto3" json:"user_pubkey,omitempty"` -} - -func (x *TrustedOnboardingRequest) Reset() { - *x = TrustedOnboardingRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrustedOnboardingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrustedOnboardingRequest) ProtoMessage() {} - -func (x *TrustedOnboardingRequest) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrustedOnboardingRequest.ProtoReflect.Descriptor instead. -func (*TrustedOnboardingRequest) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{20} -} - -func (x *TrustedOnboardingRequest) GetUserPubkey() string { - if x != nil { - return x.UserPubkey - } - return "" -} - -type TrustedOnboardingResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` -} - -func (x *TrustedOnboardingResponse) Reset() { - *x = TrustedOnboardingResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrustedOnboardingResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrustedOnboardingResponse) ProtoMessage() {} - -func (x *TrustedOnboardingResponse) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrustedOnboardingResponse.ProtoReflect.Descriptor instead. -func (*TrustedOnboardingResponse) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{21} -} - -func (x *TrustedOnboardingResponse) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - type RoundFinalizationEvent struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1213,7 +1119,7 @@ type RoundFinalizationEvent struct { func (x *RoundFinalizationEvent) Reset() { *x = RoundFinalizationEvent{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[22] + mi := &file_ark_v1_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1226,7 +1132,7 @@ func (x *RoundFinalizationEvent) String() string { func (*RoundFinalizationEvent) ProtoMessage() {} func (x *RoundFinalizationEvent) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[22] + mi := &file_ark_v1_service_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1239,7 +1145,7 @@ func (x *RoundFinalizationEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use RoundFinalizationEvent.ProtoReflect.Descriptor instead. func (*RoundFinalizationEvent) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{22} + return file_ark_v1_service_proto_rawDescGZIP(), []int{20} } func (x *RoundFinalizationEvent) GetId() string { @@ -1289,7 +1195,7 @@ type RoundFinalizedEvent struct { func (x *RoundFinalizedEvent) Reset() { *x = RoundFinalizedEvent{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[23] + mi := &file_ark_v1_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1302,7 +1208,7 @@ func (x *RoundFinalizedEvent) String() string { func (*RoundFinalizedEvent) ProtoMessage() {} func (x *RoundFinalizedEvent) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[23] + mi := &file_ark_v1_service_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1315,7 +1221,7 @@ func (x *RoundFinalizedEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use RoundFinalizedEvent.ProtoReflect.Descriptor instead. func (*RoundFinalizedEvent) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{23} + return file_ark_v1_service_proto_rawDescGZIP(), []int{21} } func (x *RoundFinalizedEvent) GetId() string { @@ -1344,7 +1250,7 @@ type RoundFailed struct { func (x *RoundFailed) Reset() { *x = RoundFailed{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[24] + mi := &file_ark_v1_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1357,7 +1263,7 @@ func (x *RoundFailed) String() string { func (*RoundFailed) ProtoMessage() {} func (x *RoundFailed) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[24] + mi := &file_ark_v1_service_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1370,7 +1276,7 @@ func (x *RoundFailed) ProtoReflect() protoreflect.Message { // Deprecated: Use RoundFailed.ProtoReflect.Descriptor instead. func (*RoundFailed) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{24} + return file_ark_v1_service_proto_rawDescGZIP(), []int{22} } func (x *RoundFailed) GetId() string { @@ -1405,7 +1311,7 @@ type Round struct { func (x *Round) Reset() { *x = Round{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[25] + mi := &file_ark_v1_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1418,7 +1324,7 @@ func (x *Round) String() string { func (*Round) ProtoMessage() {} func (x *Round) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[25] + mi := &file_ark_v1_service_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1431,7 +1337,7 @@ func (x *Round) ProtoReflect() protoreflect.Message { // Deprecated: Use Round.ProtoReflect.Descriptor instead. func (*Round) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{25} + return file_ark_v1_service_proto_rawDescGZIP(), []int{23} } func (x *Round) GetId() string { @@ -1502,7 +1408,7 @@ type Input struct { func (x *Input) Reset() { *x = Input{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[26] + mi := &file_ark_v1_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1515,7 +1421,7 @@ func (x *Input) String() string { func (*Input) ProtoMessage() {} func (x *Input) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[26] + mi := &file_ark_v1_service_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1528,7 +1434,7 @@ func (x *Input) ProtoReflect() protoreflect.Message { // Deprecated: Use Input.ProtoReflect.Descriptor instead. func (*Input) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{26} + return file_ark_v1_service_proto_rawDescGZIP(), []int{24} } func (x *Input) GetTxid() string { @@ -1559,7 +1465,7 @@ type Output struct { func (x *Output) Reset() { *x = Output{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[27] + mi := &file_ark_v1_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1572,7 +1478,7 @@ func (x *Output) String() string { func (*Output) ProtoMessage() {} func (x *Output) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[27] + mi := &file_ark_v1_service_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1585,7 +1491,7 @@ func (x *Output) ProtoReflect() protoreflect.Message { // Deprecated: Use Output.ProtoReflect.Descriptor instead. func (*Output) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{27} + return file_ark_v1_service_proto_rawDescGZIP(), []int{25} } func (x *Output) GetAddress() string { @@ -1613,7 +1519,7 @@ type Tree struct { func (x *Tree) Reset() { *x = Tree{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[28] + mi := &file_ark_v1_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1626,7 +1532,7 @@ func (x *Tree) String() string { func (*Tree) ProtoMessage() {} func (x *Tree) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[28] + mi := &file_ark_v1_service_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1639,7 +1545,7 @@ func (x *Tree) ProtoReflect() protoreflect.Message { // Deprecated: Use Tree.ProtoReflect.Descriptor instead. func (*Tree) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{28} + return file_ark_v1_service_proto_rawDescGZIP(), []int{26} } func (x *Tree) GetLevels() []*TreeLevel { @@ -1660,7 +1566,7 @@ type TreeLevel struct { func (x *TreeLevel) Reset() { *x = TreeLevel{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[29] + mi := &file_ark_v1_service_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1673,7 +1579,7 @@ func (x *TreeLevel) String() string { func (*TreeLevel) ProtoMessage() {} func (x *TreeLevel) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[29] + mi := &file_ark_v1_service_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1686,7 +1592,7 @@ func (x *TreeLevel) ProtoReflect() protoreflect.Message { // Deprecated: Use TreeLevel.ProtoReflect.Descriptor instead. func (*TreeLevel) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{29} + return file_ark_v1_service_proto_rawDescGZIP(), []int{27} } func (x *TreeLevel) GetNodes() []*Node { @@ -1709,7 +1615,7 @@ type Node struct { func (x *Node) Reset() { *x = Node{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[30] + mi := &file_ark_v1_service_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1722,7 +1628,7 @@ func (x *Node) String() string { func (*Node) ProtoMessage() {} func (x *Node) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[30] + mi := &file_ark_v1_service_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1735,7 +1641,7 @@ func (x *Node) ProtoReflect() protoreflect.Message { // Deprecated: Use Node.ProtoReflect.Descriptor instead. func (*Node) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{30} + return file_ark_v1_service_proto_rawDescGZIP(), []int{28} } func (x *Node) GetTxid() string { @@ -1776,7 +1682,7 @@ type Vtxo struct { func (x *Vtxo) Reset() { *x = Vtxo{} if protoimpl.UnsafeEnabled { - mi := &file_ark_v1_service_proto_msgTypes[31] + mi := &file_ark_v1_service_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1789,7 +1695,7 @@ func (x *Vtxo) String() string { func (*Vtxo) ProtoMessage() {} func (x *Vtxo) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_service_proto_msgTypes[31] + mi := &file_ark_v1_service_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1802,7 +1708,7 @@ func (x *Vtxo) ProtoReflect() protoreflect.Message { // Deprecated: Use Vtxo.ProtoReflect.Descriptor instead. func (*Vtxo) Descriptor() ([]byte, []int) { - return file_ark_v1_service_proto_rawDescGZIP(), []int{31} + return file_ark_v1_service_proto_rawDescGZIP(), []int{29} } func (x *Vtxo) GetOutpoint() *Input { @@ -1955,170 +1861,155 @@ var file_ark_v1_service_proto_rawDesc = []byte{ 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x11, 0x0a, 0x0f, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x18, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, - 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, - 0x22, 0x35, 0x0a, 0x19, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xb9, 0x01, 0x0a, 0x16, 0x52, 0x6f, 0x75, 0x6e, - 0x64, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x66, - 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x5f, 0x74, 0x78, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x35, 0x0a, 0x0f, - 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, - 0x72, 0x65, 0x65, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x72, 0x65, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x13, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x69, 0x6e, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6f, - 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x69, 0x64, 0x22, 0x35, 0x0a, 0x0b, 0x52, 0x6f, 0x75, 0x6e, 0x64, - 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xfa, - 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, - 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x65, 0x6e, 0x64, - 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x12, 0x35, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, - 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x65, 0x65, - 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, - 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x5f, 0x74, 0x78, 0x73, 0x18, - 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, - 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x12, 0x28, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x12, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x53, - 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x22, 0x2f, 0x0a, 0x05, 0x49, - 0x6e, 0x70, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x6f, 0x75, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x76, 0x6f, 0x75, 0x74, 0x22, 0x3a, 0x0a, 0x06, - 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x31, 0x0a, 0x04, 0x54, 0x72, 0x65, 0x65, - 0x12, 0x29, 0x0a, 0x06, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x73, 0x22, 0x2f, 0x0a, 0x09, 0x54, - 0x72, 0x65, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, 0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22, 0x4b, 0x0a, 0x04, - 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x54, 0x78, 0x69, 0x64, 0x22, 0xde, 0x01, 0x0a, 0x04, 0x56, 0x74, - 0x78, 0x6f, 0x12, 0x29, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, - 0x70, 0x75, 0x74, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2a, 0x0a, - 0x08, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, - 0x08, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x70, 0x65, - 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x73, 0x70, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x73, 0x70, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x65, 0x70, 0x74, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x65, 0x70, 0x74, 0x2a, 0x98, 0x01, 0x0a, 0x0a, 0x52, - 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, - 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, - 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x52, 0x41, 0x54, 0x49, - 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, - 0x41, 0x47, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, - 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, - 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, - 0x12, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x41, 0x49, - 0x4c, 0x45, 0x44, 0x10, 0x04, 0x32, 0xd2, 0x08, 0x0a, 0x0a, 0x41, 0x72, 0x6b, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, - 0x3a, 0x01, 0x2a, 0x22, 0x14, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x67, 0x0a, 0x0c, 0x43, 0x6c, 0x61, - 0x69, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x3a, 0x01, 0x2a, 0x22, - 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6c, 0x61, - 0x69, 0x6d, 0x12, 0x73, 0x0a, 0x0f, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, - 0x2a, 0x22, 0x14, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x66, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x6f, - 0x75, 0x6e, 0x64, 0x12, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, - 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x7b, 0x74, 0x78, 0x69, 0x64, 0x7d, - 0x12, 0x64, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, - 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, - 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, - 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, - 0x79, 0x49, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x69, - 0x64, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x65, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, - 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x12, 0x50, 0x0a, - 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x13, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x50, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, - 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x69, - 0x6e, 0x67, 0x2f, 0x7b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, - 0x5d, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x18, 0x2e, 0x61, - 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x76, - 0x74, 0x78, 0x6f, 0x73, 0x2f, 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7d, 0x12, 0x4c, - 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0a, 0x12, 0x08, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x52, 0x0a, 0x07, - 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, - 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, - 0x12, 0x78, 0x0a, 0x11, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x20, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, - 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, - 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x6e, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x92, 0x01, 0x0a, 0x0a, 0x63, - 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, 0x2d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x73, 0x70, 0x65, 0x63, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x72, 0x6b, 0x2f, - 0x76, 0x31, 0x3b, 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, - 0x06, 0x41, 0x72, 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, - 0xe2, 0x02, 0x12, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x72, 0x6b, 0x3a, 0x3a, 0x56, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x73, 0x65, 0x22, 0xb9, 0x01, 0x0a, 0x16, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x69, 0x6e, + 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, + 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x6f, 0x72, 0x66, 0x65, + 0x69, 0x74, 0x5f, 0x74, 0x78, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x6f, + 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x35, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x67, + 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x52, + 0x0e, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, + 0x42, 0x0a, 0x13, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, + 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6f, 0x6c, 0x54, + 0x78, 0x69, 0x64, 0x22, 0x35, 0x0a, 0x0b, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xfa, 0x01, 0x0a, 0x05, 0x52, + 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x12, 0x35, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, + 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x52, 0x0e, 0x63, 0x6f, + 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, 0x12, 0x1f, 0x0a, 0x0b, + 0x66, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x5f, 0x74, 0x78, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x66, 0x65, 0x69, 0x74, 0x54, 0x78, 0x73, 0x12, 0x1e, 0x0a, + 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x28, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x22, 0x2f, 0x0a, 0x05, 0x49, 0x6e, 0x70, 0x75, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x78, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x04, 0x76, 0x6f, 0x75, 0x74, 0x22, 0x3a, 0x0a, 0x06, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x31, 0x0a, 0x04, 0x54, 0x72, 0x65, 0x65, 0x12, 0x29, 0x0a, 0x06, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, + 0x06, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x73, 0x22, 0x2f, 0x0a, 0x09, 0x54, 0x72, 0x65, 0x65, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, 0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22, 0x4b, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x78, 0x69, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x74, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x74, + 0x78, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x54, 0x78, 0x69, 0x64, 0x22, 0xde, 0x01, 0x0a, 0x04, 0x56, 0x74, 0x78, 0x6f, 0x12, 0x29, + 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, + 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x08, 0x72, 0x65, 0x63, + 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x72, + 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x08, 0x72, 0x65, 0x63, + 0x65, 0x69, 0x76, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, + 0x6f, 0x6f, 0x6c, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x6f, 0x6f, 0x6c, 0x54, 0x78, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x65, 0x6e, + 0x74, 0x5f, 0x62, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x65, 0x6e, + 0x74, 0x42, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x5f, 0x61, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x65, 0x70, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x05, 0x73, 0x77, 0x65, 0x70, 0x74, 0x2a, 0x98, 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x75, 0x6e, 0x64, + 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, + 0x54, 0x41, 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, + 0x45, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, + 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, + 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x19, + 0x0a, 0x15, 0x52, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x49, + 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x4f, 0x55, + 0x4e, 0x44, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, + 0x04, 0x32, 0xd8, 0x07, 0x0a, 0x0a, 0x41, 0x72, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x73, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, + 0x14, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x67, 0x0a, 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6c, 0x61, 0x69, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x3a, 0x01, 0x2a, 0x22, 0x11, 0x2f, 0x76, 0x31, + 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x73, + 0x0a, 0x0f, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14, 0x2f, + 0x76, 0x31, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x12, + 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, + 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x7b, 0x74, 0x78, 0x69, 0x64, 0x7d, 0x12, 0x64, 0x0a, 0x0c, + 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, + 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x79, 0x49, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, + 0x11, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x69, 0x64, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x12, 0x65, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, + 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x04, 0x50, 0x69, 0x6e, + 0x67, 0x12, 0x13, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x7b, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x5d, 0x0a, 0x09, 0x4c, + 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x56, 0x74, 0x78, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x74, 0x78, 0x6f, 0x73, + 0x2f, 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x7d, 0x12, 0x4c, 0x0a, 0x07, 0x47, 0x65, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, + 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, + 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x52, 0x0a, 0x07, 0x4f, 0x6e, 0x62, 0x6f, + 0x61, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x62, + 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, + 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, + 0x0b, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x42, 0x92, 0x01, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, 0x2d, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x73, 0x70, 0x65, 0x63, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x72, + 0x6b, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, + 0xaa, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x5c, + 0x56, 0x31, 0xe2, 0x02, 0x12, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x72, 0x6b, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2134,61 +2025,59 @@ func file_ark_v1_service_proto_rawDescGZIP() []byte { } var file_ark_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_ark_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_ark_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_ark_v1_service_proto_goTypes = []interface{}{ - (RoundStage)(0), // 0: ark.v1.RoundStage - (*RegisterPaymentRequest)(nil), // 1: ark.v1.RegisterPaymentRequest - (*RegisterPaymentResponse)(nil), // 2: ark.v1.RegisterPaymentResponse - (*ClaimPaymentRequest)(nil), // 3: ark.v1.ClaimPaymentRequest - (*ClaimPaymentResponse)(nil), // 4: ark.v1.ClaimPaymentResponse - (*FinalizePaymentRequest)(nil), // 5: ark.v1.FinalizePaymentRequest - (*FinalizePaymentResponse)(nil), // 6: ark.v1.FinalizePaymentResponse - (*GetRoundRequest)(nil), // 7: ark.v1.GetRoundRequest - (*GetRoundResponse)(nil), // 8: ark.v1.GetRoundResponse - (*GetRoundByIdRequest)(nil), // 9: ark.v1.GetRoundByIdRequest - (*GetRoundByIdResponse)(nil), // 10: ark.v1.GetRoundByIdResponse - (*GetEventStreamRequest)(nil), // 11: ark.v1.GetEventStreamRequest - (*GetEventStreamResponse)(nil), // 12: ark.v1.GetEventStreamResponse - (*PingRequest)(nil), // 13: ark.v1.PingRequest - (*PingResponse)(nil), // 14: ark.v1.PingResponse - (*ListVtxosRequest)(nil), // 15: ark.v1.ListVtxosRequest - (*ListVtxosResponse)(nil), // 16: ark.v1.ListVtxosResponse - (*GetInfoRequest)(nil), // 17: ark.v1.GetInfoRequest - (*GetInfoResponse)(nil), // 18: ark.v1.GetInfoResponse - (*OnboardRequest)(nil), // 19: ark.v1.OnboardRequest - (*OnboardResponse)(nil), // 20: ark.v1.OnboardResponse - (*TrustedOnboardingRequest)(nil), // 21: ark.v1.TrustedOnboardingRequest - (*TrustedOnboardingResponse)(nil), // 22: ark.v1.TrustedOnboardingResponse - (*RoundFinalizationEvent)(nil), // 23: ark.v1.RoundFinalizationEvent - (*RoundFinalizedEvent)(nil), // 24: ark.v1.RoundFinalizedEvent - (*RoundFailed)(nil), // 25: ark.v1.RoundFailed - (*Round)(nil), // 26: ark.v1.Round - (*Input)(nil), // 27: ark.v1.Input - (*Output)(nil), // 28: ark.v1.Output - (*Tree)(nil), // 29: ark.v1.Tree - (*TreeLevel)(nil), // 30: ark.v1.TreeLevel - (*Node)(nil), // 31: ark.v1.Node - (*Vtxo)(nil), // 32: ark.v1.Vtxo + (RoundStage)(0), // 0: ark.v1.RoundStage + (*RegisterPaymentRequest)(nil), // 1: ark.v1.RegisterPaymentRequest + (*RegisterPaymentResponse)(nil), // 2: ark.v1.RegisterPaymentResponse + (*ClaimPaymentRequest)(nil), // 3: ark.v1.ClaimPaymentRequest + (*ClaimPaymentResponse)(nil), // 4: ark.v1.ClaimPaymentResponse + (*FinalizePaymentRequest)(nil), // 5: ark.v1.FinalizePaymentRequest + (*FinalizePaymentResponse)(nil), // 6: ark.v1.FinalizePaymentResponse + (*GetRoundRequest)(nil), // 7: ark.v1.GetRoundRequest + (*GetRoundResponse)(nil), // 8: ark.v1.GetRoundResponse + (*GetRoundByIdRequest)(nil), // 9: ark.v1.GetRoundByIdRequest + (*GetRoundByIdResponse)(nil), // 10: ark.v1.GetRoundByIdResponse + (*GetEventStreamRequest)(nil), // 11: ark.v1.GetEventStreamRequest + (*GetEventStreamResponse)(nil), // 12: ark.v1.GetEventStreamResponse + (*PingRequest)(nil), // 13: ark.v1.PingRequest + (*PingResponse)(nil), // 14: ark.v1.PingResponse + (*ListVtxosRequest)(nil), // 15: ark.v1.ListVtxosRequest + (*ListVtxosResponse)(nil), // 16: ark.v1.ListVtxosResponse + (*GetInfoRequest)(nil), // 17: ark.v1.GetInfoRequest + (*GetInfoResponse)(nil), // 18: ark.v1.GetInfoResponse + (*OnboardRequest)(nil), // 19: ark.v1.OnboardRequest + (*OnboardResponse)(nil), // 20: ark.v1.OnboardResponse + (*RoundFinalizationEvent)(nil), // 21: ark.v1.RoundFinalizationEvent + (*RoundFinalizedEvent)(nil), // 22: ark.v1.RoundFinalizedEvent + (*RoundFailed)(nil), // 23: ark.v1.RoundFailed + (*Round)(nil), // 24: ark.v1.Round + (*Input)(nil), // 25: ark.v1.Input + (*Output)(nil), // 26: ark.v1.Output + (*Tree)(nil), // 27: ark.v1.Tree + (*TreeLevel)(nil), // 28: ark.v1.TreeLevel + (*Node)(nil), // 29: ark.v1.Node + (*Vtxo)(nil), // 30: ark.v1.Vtxo } var file_ark_v1_service_proto_depIdxs = []int32{ - 27, // 0: ark.v1.RegisterPaymentRequest.inputs:type_name -> ark.v1.Input - 28, // 1: ark.v1.ClaimPaymentRequest.outputs:type_name -> ark.v1.Output - 26, // 2: ark.v1.GetRoundResponse.round:type_name -> ark.v1.Round - 26, // 3: ark.v1.GetRoundByIdResponse.round:type_name -> ark.v1.Round - 23, // 4: ark.v1.GetEventStreamResponse.round_finalization:type_name -> ark.v1.RoundFinalizationEvent - 24, // 5: ark.v1.GetEventStreamResponse.round_finalized:type_name -> ark.v1.RoundFinalizedEvent - 25, // 6: ark.v1.GetEventStreamResponse.round_failed:type_name -> ark.v1.RoundFailed - 23, // 7: ark.v1.PingResponse.event:type_name -> ark.v1.RoundFinalizationEvent - 32, // 8: ark.v1.ListVtxosResponse.spendable_vtxos:type_name -> ark.v1.Vtxo - 32, // 9: ark.v1.ListVtxosResponse.spent_vtxos:type_name -> ark.v1.Vtxo - 29, // 10: ark.v1.OnboardRequest.congestion_tree:type_name -> ark.v1.Tree - 29, // 11: ark.v1.RoundFinalizationEvent.congestion_tree:type_name -> ark.v1.Tree - 29, // 12: ark.v1.Round.congestion_tree:type_name -> ark.v1.Tree + 25, // 0: ark.v1.RegisterPaymentRequest.inputs:type_name -> ark.v1.Input + 26, // 1: ark.v1.ClaimPaymentRequest.outputs:type_name -> ark.v1.Output + 24, // 2: ark.v1.GetRoundResponse.round:type_name -> ark.v1.Round + 24, // 3: ark.v1.GetRoundByIdResponse.round:type_name -> ark.v1.Round + 21, // 4: ark.v1.GetEventStreamResponse.round_finalization:type_name -> ark.v1.RoundFinalizationEvent + 22, // 5: ark.v1.GetEventStreamResponse.round_finalized:type_name -> ark.v1.RoundFinalizedEvent + 23, // 6: ark.v1.GetEventStreamResponse.round_failed:type_name -> ark.v1.RoundFailed + 21, // 7: ark.v1.PingResponse.event:type_name -> ark.v1.RoundFinalizationEvent + 30, // 8: ark.v1.ListVtxosResponse.spendable_vtxos:type_name -> ark.v1.Vtxo + 30, // 9: ark.v1.ListVtxosResponse.spent_vtxos:type_name -> ark.v1.Vtxo + 27, // 10: ark.v1.OnboardRequest.congestion_tree:type_name -> ark.v1.Tree + 27, // 11: ark.v1.RoundFinalizationEvent.congestion_tree:type_name -> ark.v1.Tree + 27, // 12: ark.v1.Round.congestion_tree:type_name -> ark.v1.Tree 0, // 13: ark.v1.Round.stage:type_name -> ark.v1.RoundStage - 30, // 14: ark.v1.Tree.levels:type_name -> ark.v1.TreeLevel - 31, // 15: ark.v1.TreeLevel.nodes:type_name -> ark.v1.Node - 27, // 16: ark.v1.Vtxo.outpoint:type_name -> ark.v1.Input - 28, // 17: ark.v1.Vtxo.receiver:type_name -> ark.v1.Output + 28, // 14: ark.v1.Tree.levels:type_name -> ark.v1.TreeLevel + 29, // 15: ark.v1.TreeLevel.nodes:type_name -> ark.v1.Node + 25, // 16: ark.v1.Vtxo.outpoint:type_name -> ark.v1.Input + 26, // 17: ark.v1.Vtxo.receiver:type_name -> ark.v1.Output 1, // 18: ark.v1.ArkService.RegisterPayment:input_type -> ark.v1.RegisterPaymentRequest 3, // 19: ark.v1.ArkService.ClaimPayment:input_type -> ark.v1.ClaimPaymentRequest 5, // 20: ark.v1.ArkService.FinalizePayment:input_type -> ark.v1.FinalizePaymentRequest @@ -2199,20 +2088,18 @@ var file_ark_v1_service_proto_depIdxs = []int32{ 15, // 25: ark.v1.ArkService.ListVtxos:input_type -> ark.v1.ListVtxosRequest 17, // 26: ark.v1.ArkService.GetInfo:input_type -> ark.v1.GetInfoRequest 19, // 27: ark.v1.ArkService.Onboard:input_type -> ark.v1.OnboardRequest - 21, // 28: ark.v1.ArkService.TrustedOnboarding:input_type -> ark.v1.TrustedOnboardingRequest - 2, // 29: ark.v1.ArkService.RegisterPayment:output_type -> ark.v1.RegisterPaymentResponse - 4, // 30: ark.v1.ArkService.ClaimPayment:output_type -> ark.v1.ClaimPaymentResponse - 6, // 31: ark.v1.ArkService.FinalizePayment:output_type -> ark.v1.FinalizePaymentResponse - 8, // 32: ark.v1.ArkService.GetRound:output_type -> ark.v1.GetRoundResponse - 10, // 33: ark.v1.ArkService.GetRoundById:output_type -> ark.v1.GetRoundByIdResponse - 12, // 34: ark.v1.ArkService.GetEventStream:output_type -> ark.v1.GetEventStreamResponse - 14, // 35: ark.v1.ArkService.Ping:output_type -> ark.v1.PingResponse - 16, // 36: ark.v1.ArkService.ListVtxos:output_type -> ark.v1.ListVtxosResponse - 18, // 37: ark.v1.ArkService.GetInfo:output_type -> ark.v1.GetInfoResponse - 20, // 38: ark.v1.ArkService.Onboard:output_type -> ark.v1.OnboardResponse - 22, // 39: ark.v1.ArkService.TrustedOnboarding:output_type -> ark.v1.TrustedOnboardingResponse - 29, // [29:40] is the sub-list for method output_type - 18, // [18:29] is the sub-list for method input_type + 2, // 28: ark.v1.ArkService.RegisterPayment:output_type -> ark.v1.RegisterPaymentResponse + 4, // 29: ark.v1.ArkService.ClaimPayment:output_type -> ark.v1.ClaimPaymentResponse + 6, // 30: ark.v1.ArkService.FinalizePayment:output_type -> ark.v1.FinalizePaymentResponse + 8, // 31: ark.v1.ArkService.GetRound:output_type -> ark.v1.GetRoundResponse + 10, // 32: ark.v1.ArkService.GetRoundById:output_type -> ark.v1.GetRoundByIdResponse + 12, // 33: ark.v1.ArkService.GetEventStream:output_type -> ark.v1.GetEventStreamResponse + 14, // 34: ark.v1.ArkService.Ping:output_type -> ark.v1.PingResponse + 16, // 35: ark.v1.ArkService.ListVtxos:output_type -> ark.v1.ListVtxosResponse + 18, // 36: ark.v1.ArkService.GetInfo:output_type -> ark.v1.GetInfoResponse + 20, // 37: ark.v1.ArkService.Onboard:output_type -> ark.v1.OnboardResponse + 28, // [28:38] is the sub-list for method output_type + 18, // [18:28] is the sub-list for method input_type 18, // [18:18] is the sub-list for extension type_name 18, // [18:18] is the sub-list for extension extendee 0, // [0:18] is the sub-list for field type_name @@ -2465,30 +2352,6 @@ func file_ark_v1_service_proto_init() { } } file_ark_v1_service_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustedOnboardingRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ark_v1_service_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustedOnboardingResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ark_v1_service_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RoundFinalizationEvent); i { case 0: return &v.state @@ -2500,7 +2363,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RoundFinalizedEvent); i { case 0: return &v.state @@ -2512,7 +2375,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RoundFailed); i { case 0: return &v.state @@ -2524,7 +2387,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Round); i { case 0: return &v.state @@ -2536,7 +2399,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Input); i { case 0: return &v.state @@ -2548,7 +2411,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Output); i { case 0: return &v.state @@ -2560,7 +2423,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Tree); i { case 0: return &v.state @@ -2572,7 +2435,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TreeLevel); i { case 0: return &v.state @@ -2584,7 +2447,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Node); i { case 0: return &v.state @@ -2596,7 +2459,7 @@ func file_ark_v1_service_proto_init() { return nil } } - file_ark_v1_service_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + file_ark_v1_service_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Vtxo); i { case 0: return &v.state @@ -2620,7 +2483,7 @@ func file_ark_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_ark_v1_service_proto_rawDesc, NumEnums: 1, - NumMessages: 32, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/server/api-spec/protobuf/gen/ark/v1/service.pb.gw.go b/server/api-spec/protobuf/gen/ark/v1/service.pb.gw.go index b95a7c5..a36bd07 100644 --- a/server/api-spec/protobuf/gen/ark/v1/service.pb.gw.go +++ b/server/api-spec/protobuf/gen/ark/v1/service.pb.gw.go @@ -378,36 +378,11 @@ func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.M } -func request_ArkService_TrustedOnboarding_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq TrustedOnboardingRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.TrustedOnboarding(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_ArkService_TrustedOnboarding_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq TrustedOnboardingRequest - var metadata runtime.ServerMetadata - - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.TrustedOnboarding(ctx, &protoReq) - return msg, metadata, err - -} - // RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux". // UnaryRPC :call ArkServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterArkServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ArkServiceServer) error { mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -642,31 +617,6 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) - mux.Handle("POST", pattern_ArkService_TrustedOnboarding_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/TrustedOnboarding", runtime.WithHTTPPathPattern("/v1/onboard/address")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_ArkService_TrustedOnboarding_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ArkService_TrustedOnboarding_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - return nil } @@ -705,7 +655,7 @@ func RegisterArkServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ArkServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ArkServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "ArkServiceClient" to call the correct interceptors. +// "ArkServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ArkServiceClient) error { mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -928,28 +878,6 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) - mux.Handle("POST", pattern_ArkService_TrustedOnboarding_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/TrustedOnboarding", runtime.WithHTTPPathPattern("/v1/onboard/address")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_ArkService_TrustedOnboarding_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_ArkService_TrustedOnboarding_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - return nil } @@ -973,8 +901,6 @@ var ( pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, "")) pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, "")) - - pattern_ArkService_TrustedOnboarding_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "onboard", "address"}, "")) ) var ( @@ -997,6 +923,4 @@ var ( forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage - - forward_ArkService_TrustedOnboarding_0 = runtime.ForwardResponseMessage ) diff --git a/server/api-spec/protobuf/gen/ark/v1/service_grpc.pb.go b/server/api-spec/protobuf/gen/ark/v1/service_grpc.pb.go index 8a1abfa..5770baa 100644 --- a/server/api-spec/protobuf/gen/ark/v1/service_grpc.pb.go +++ b/server/api-spec/protobuf/gen/ark/v1/service_grpc.pb.go @@ -29,7 +29,6 @@ type ArkServiceClient interface { ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) - TrustedOnboarding(ctx context.Context, in *TrustedOnboardingRequest, opts ...grpc.CallOption) (*TrustedOnboardingResponse, error) } type arkServiceClient struct { @@ -153,15 +152,6 @@ func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts return out, nil } -func (c *arkServiceClient) TrustedOnboarding(ctx context.Context, in *TrustedOnboardingRequest, opts ...grpc.CallOption) (*TrustedOnboardingResponse, error) { - out := new(TrustedOnboardingResponse) - err := c.cc.Invoke(ctx, "/ark.v1.ArkService/TrustedOnboarding", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - // ArkServiceServer is the server API for ArkService service. // All implementations should embed UnimplementedArkServiceServer // for forward compatibility @@ -177,7 +167,6 @@ type ArkServiceServer interface { ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) - TrustedOnboarding(context.Context, *TrustedOnboardingRequest) (*TrustedOnboardingResponse, error) } // UnimplementedArkServiceServer should be embedded to have forward compatible implementations. @@ -214,9 +203,6 @@ func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) ( func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented") } -func (UnimplementedArkServiceServer) TrustedOnboarding(context.Context, *TrustedOnboardingRequest) (*TrustedOnboardingResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method TrustedOnboarding not implemented") -} // UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ArkServiceServer will @@ -412,24 +398,6 @@ func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } -func _ArkService_TrustedOnboarding_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(TrustedOnboardingRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ArkServiceServer).TrustedOnboarding(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.ArkService/TrustedOnboarding", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ArkServiceServer).TrustedOnboarding(ctx, req.(*TrustedOnboardingRequest)) - } - return interceptor(ctx, in, info, handler) -} - // ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -473,10 +441,6 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Onboard", Handler: _ArkService_Onboard_Handler, }, - { - MethodName: "TrustedOnboarding", - Handler: _ArkService_TrustedOnboarding_Handler, - }, }, Streams: []grpc.StreamDesc{ { diff --git a/server/api-spec/protobuf/gen/ark/v1/wallet.pb.go b/server/api-spec/protobuf/gen/ark/v1/wallet.pb.go index a5b6337..fefd7ce 100644 --- a/server/api-spec/protobuf/gen/ark/v1/wallet.pb.go +++ b/server/api-spec/protobuf/gen/ark/v1/wallet.pb.go @@ -206,6 +206,7 @@ type RestoreRequest struct { Seed string `protobuf:"bytes,1,opt,name=seed,proto3" json:"seed,omitempty"` Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + GapLimit uint64 `protobuf:"varint,3,opt,name=gap_limit,json=gapLimit,proto3" json:"gap_limit,omitempty"` } func (x *RestoreRequest) Reset() { @@ -254,6 +255,13 @@ func (x *RestoreRequest) GetPassword() string { return "" } +func (x *RestoreRequest) GetGapLimit() uint64 { + if x != nil { + return x.GapLimit + } + return 0 +} + type RestoreResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -811,106 +819,109 @@ var file_ark_v1_wallet_proto_rawDesc = []byte{ 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x65, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5d, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x65, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x0d, - 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x55, 0x6e, 0x6c, - 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x0a, 0x0b, 0x4c, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x70, + 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x67, 0x61, + 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x0d, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x0e, 0x0a, 0x0c, 0x4c, 0x6f, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x69, 0x0a, 0x11, 0x47, 0x65, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, - 0x79, 0x6e, 0x63, 0x65, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, 0x0a, - 0x15, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x22, 0x13, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x07, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, - 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, - 0x0c, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x52, 0x0b, 0x6d, 0x61, 0x69, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x3e, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x5f, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, - 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x11, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x32, 0x94, 0x06, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x59, 0x0a, 0x07, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x12, 0x16, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x0a, 0x0b, 0x4c, 0x6f, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x22, 0x0e, 0x0a, 0x0c, 0x4c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x69, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, + 0x6e, 0x63, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x79, 0x6e, 0x63, + 0x65, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, 0x0a, 0x15, 0x44, 0x65, + 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x13, 0x0a, + 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x07, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x6c, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x0c, 0x6d, 0x61, + 0x69, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x52, 0x0b, 0x6d, 0x61, 0x69, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3e, + 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x72, 0x6b, + 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x11, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0xf3, + 0x03, 0x0a, 0x18, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x59, 0x0a, 0x07, 0x47, + 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, - 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x65, 0x64, 0x12, 0x5b, - 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, - 0x01, 0x2a, 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x5f, 0x0a, 0x07, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, + 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2f, 0x73, 0x65, 0x65, 0x64, 0x12, 0x5b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x12, 0x15, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x01, 0x2a, 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x12, 0x5f, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x3a, - 0x01, 0x2a, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x5b, 0x0a, 0x06, - 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x15, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, - 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x01, 0x2a, - 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x53, 0x0a, 0x04, 0x4c, 0x6f, 0x63, - 0x6b, 0x12, 0x13, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x63, 0x6b, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, - 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x61, - 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x72, - 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, - 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x6e, 0x0a, 0x0d, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x1c, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x72, 0x69, - 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, - 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x65, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, - 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x72, 0x6b, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, - 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x2f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x42, 0x91, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, - 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, 0x2d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x61, - 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x73, 0x70, 0x65, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x76, 0x31, 0x3b, - 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x41, 0x72, - 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, - 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x72, 0x6b, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x3a, 0x01, 0x2a, 0x22, 0x18, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x72, 0x65, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x5b, 0x0a, 0x06, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x15, + 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x55, + 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x3a, 0x01, 0x2a, 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, + 0x6b, 0x12, 0x61, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, + 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x32, 0xbb, 0x02, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x53, 0x0a, 0x04, 0x4c, 0x6f, 0x63, 0x6b, 0x12, 0x13, + 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x6e, 0x0a, 0x0d, 0x44, + 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1c, 0x2e, 0x61, + 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x72, 0x6b, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1a, 0x12, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x65, 0x0a, 0x0a, 0x47, + 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x72, 0x6b, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x62, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x42, 0x91, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x6b, 0x2e, 0x76, + 0x31, 0x42, 0x0b, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6b, + 0x2d, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x61, 0x70, 0x69, + 0x2d, 0x73, 0x70, 0x65, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, + 0x65, 0x6e, 0x2f, 0x61, 0x72, 0x6b, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x72, 0x6b, 0x76, 0x31, 0xa2, + 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x41, 0x72, 0x6b, 0x2e, 0x56, 0x31, 0xca, 0x02, + 0x06, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, 0x41, 0x72, 0x6b, 0x5c, 0x56, 0x31, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, + 0x72, 0x6b, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -948,20 +959,20 @@ var file_ark_v1_wallet_proto_goTypes = []interface{}{ var file_ark_v1_wallet_proto_depIdxs = []int32{ 15, // 0: ark.v1.GetBalanceResponse.main_account:type_name -> ark.v1.Balance 15, // 1: ark.v1.GetBalanceResponse.connectors_account:type_name -> ark.v1.Balance - 0, // 2: ark.v1.WalletService.GenSeed:input_type -> ark.v1.GenSeedRequest - 2, // 3: ark.v1.WalletService.Create:input_type -> ark.v1.CreateRequest - 4, // 4: ark.v1.WalletService.Restore:input_type -> ark.v1.RestoreRequest - 6, // 5: ark.v1.WalletService.Unlock:input_type -> ark.v1.UnlockRequest - 8, // 6: ark.v1.WalletService.Lock:input_type -> ark.v1.LockRequest - 10, // 7: ark.v1.WalletService.GetStatus:input_type -> ark.v1.GetStatusRequest + 0, // 2: ark.v1.WalletInitializerService.GenSeed:input_type -> ark.v1.GenSeedRequest + 2, // 3: ark.v1.WalletInitializerService.Create:input_type -> ark.v1.CreateRequest + 4, // 4: ark.v1.WalletInitializerService.Restore:input_type -> ark.v1.RestoreRequest + 6, // 5: ark.v1.WalletInitializerService.Unlock:input_type -> ark.v1.UnlockRequest + 10, // 6: ark.v1.WalletInitializerService.GetStatus:input_type -> ark.v1.GetStatusRequest + 8, // 7: ark.v1.WalletService.Lock:input_type -> ark.v1.LockRequest 12, // 8: ark.v1.WalletService.DeriveAddress:input_type -> ark.v1.DeriveAddressRequest 14, // 9: ark.v1.WalletService.GetBalance:input_type -> ark.v1.GetBalanceRequest - 1, // 10: ark.v1.WalletService.GenSeed:output_type -> ark.v1.GenSeedResponse - 3, // 11: ark.v1.WalletService.Create:output_type -> ark.v1.CreateResponse - 5, // 12: ark.v1.WalletService.Restore:output_type -> ark.v1.RestoreResponse - 7, // 13: ark.v1.WalletService.Unlock:output_type -> ark.v1.UnlockResponse - 9, // 14: ark.v1.WalletService.Lock:output_type -> ark.v1.LockResponse - 11, // 15: ark.v1.WalletService.GetStatus:output_type -> ark.v1.GetStatusResponse + 1, // 10: ark.v1.WalletInitializerService.GenSeed:output_type -> ark.v1.GenSeedResponse + 3, // 11: ark.v1.WalletInitializerService.Create:output_type -> ark.v1.CreateResponse + 5, // 12: ark.v1.WalletInitializerService.Restore:output_type -> ark.v1.RestoreResponse + 7, // 13: ark.v1.WalletInitializerService.Unlock:output_type -> ark.v1.UnlockResponse + 11, // 14: ark.v1.WalletInitializerService.GetStatus:output_type -> ark.v1.GetStatusResponse + 9, // 15: ark.v1.WalletService.Lock:output_type -> ark.v1.LockResponse 13, // 16: ark.v1.WalletService.DeriveAddress:output_type -> ark.v1.DeriveAddressResponse 16, // 17: ark.v1.WalletService.GetBalance:output_type -> ark.v1.GetBalanceResponse 10, // [10:18] is the sub-list for method output_type @@ -1190,7 +1201,7 @@ func file_ark_v1_wallet_proto_init() { NumEnums: 0, NumMessages: 17, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_ark_v1_wallet_proto_goTypes, DependencyIndexes: file_ark_v1_wallet_proto_depIdxs, diff --git a/server/api-spec/protobuf/gen/ark/v1/wallet.pb.gw.go b/server/api-spec/protobuf/gen/ark/v1/wallet.pb.gw.go index dfc1884..2329357 100644 --- a/server/api-spec/protobuf/gen/ark/v1/wallet.pb.gw.go +++ b/server/api-spec/protobuf/gen/ark/v1/wallet.pb.gw.go @@ -31,7 +31,7 @@ var _ = runtime.String var _ = utilities.NewDoubleArray var _ = metadata.Join -func request_WalletService_GenSeed_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func request_WalletInitializerService_GenSeed_0(ctx context.Context, marshaler runtime.Marshaler, client WalletInitializerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GenSeedRequest var metadata runtime.ServerMetadata @@ -40,7 +40,7 @@ func request_WalletService_GenSeed_0(ctx context.Context, marshaler runtime.Mars } -func local_request_WalletService_GenSeed_0(ctx context.Context, marshaler runtime.Marshaler, server WalletServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func local_request_WalletInitializerService_GenSeed_0(ctx context.Context, marshaler runtime.Marshaler, server WalletInitializerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GenSeedRequest var metadata runtime.ServerMetadata @@ -49,7 +49,7 @@ func local_request_WalletService_GenSeed_0(ctx context.Context, marshaler runtim } -func request_WalletService_Create_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func request_WalletInitializerService_Create_0(ctx context.Context, marshaler runtime.Marshaler, client WalletInitializerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateRequest var metadata runtime.ServerMetadata @@ -62,7 +62,7 @@ func request_WalletService_Create_0(ctx context.Context, marshaler runtime.Marsh } -func local_request_WalletService_Create_0(ctx context.Context, marshaler runtime.Marshaler, server WalletServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func local_request_WalletInitializerService_Create_0(ctx context.Context, marshaler runtime.Marshaler, server WalletInitializerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateRequest var metadata runtime.ServerMetadata @@ -75,7 +75,7 @@ func local_request_WalletService_Create_0(ctx context.Context, marshaler runtime } -func request_WalletService_Restore_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func request_WalletInitializerService_Restore_0(ctx context.Context, marshaler runtime.Marshaler, client WalletInitializerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq RestoreRequest var metadata runtime.ServerMetadata @@ -88,7 +88,7 @@ func request_WalletService_Restore_0(ctx context.Context, marshaler runtime.Mars } -func local_request_WalletService_Restore_0(ctx context.Context, marshaler runtime.Marshaler, server WalletServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func local_request_WalletInitializerService_Restore_0(ctx context.Context, marshaler runtime.Marshaler, server WalletInitializerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq RestoreRequest var metadata runtime.ServerMetadata @@ -101,7 +101,7 @@ func local_request_WalletService_Restore_0(ctx context.Context, marshaler runtim } -func request_WalletService_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func request_WalletInitializerService_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, client WalletInitializerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UnlockRequest var metadata runtime.ServerMetadata @@ -114,7 +114,7 @@ func request_WalletService_Unlock_0(ctx context.Context, marshaler runtime.Marsh } -func local_request_WalletService_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, server WalletServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { +func local_request_WalletInitializerService_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, server WalletInitializerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UnlockRequest var metadata runtime.ServerMetadata @@ -127,6 +127,24 @@ func local_request_WalletService_Unlock_0(ctx context.Context, marshaler runtime } +func request_WalletInitializerService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, client WalletInitializerServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetStatusRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WalletInitializerService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, server WalletInitializerServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetStatusRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetStatus(ctx, &protoReq) + return msg, metadata, err + +} + func request_WalletService_Lock_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LockRequest var metadata runtime.ServerMetadata @@ -153,24 +171,6 @@ func local_request_WalletService_Lock_0(ctx context.Context, marshaler runtime.M } -func request_WalletService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetStatusRequest - var metadata runtime.ServerMetadata - - msg, err := client.GetStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_WalletService_GetStatus_0(ctx context.Context, marshaler runtime.Marshaler, server WalletServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetStatusRequest - var metadata runtime.ServerMetadata - - msg, err := server.GetStatus(ctx, &protoReq) - return msg, metadata, err - -} - func request_WalletService_DeriveAddress_0(ctx context.Context, marshaler runtime.Marshaler, client WalletServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeriveAddressRequest var metadata runtime.ServerMetadata @@ -207,112 +207,148 @@ func local_request_WalletService_GetBalance_0(ctx context.Context, marshaler run } +// RegisterWalletInitializerServiceHandlerServer registers the http handlers for service WalletInitializerService to "mux". +// UnaryRPC :call WalletInitializerServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletInitializerServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterWalletInitializerServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletInitializerServiceServer) error { + + mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletInitializerService/GenSeed", runtime.WithHTTPPathPattern("/v1/admin/wallet/seed")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletInitializerService_GenSeed_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_GenSeed_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Create", runtime.WithHTTPPathPattern("/v1/admin/wallet/create")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletInitializerService_Create_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Create_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Restore_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Restore", runtime.WithHTTPPathPattern("/v1/admin/wallet/restore")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletInitializerService_Restore_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Restore_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Unlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Unlock", runtime.WithHTTPPathPattern("/v1/admin/wallet/unlock")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletInitializerService_Unlock_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Unlock_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_WalletInitializerService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletInitializerService/GetStatus", runtime.WithHTTPPathPattern("/v1/admin/wallet/status")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletInitializerService_GetStatus_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_GetStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterWalletServiceHandlerServer registers the http handlers for service WalletService to "mux". // UnaryRPC :call WalletServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. func RegisterWalletServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletServiceServer) error { - mux.Handle("GET", pattern_WalletService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletService/GenSeed", runtime.WithHTTPPathPattern("/v1/admin/wallet/seed")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WalletService_GenSeed_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_GenSeed_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletService/Create", runtime.WithHTTPPathPattern("/v1/admin/wallet/create")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WalletService_Create_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Create_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Restore_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletService/Restore", runtime.WithHTTPPathPattern("/v1/admin/wallet/restore")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WalletService_Restore_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Restore_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Unlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletService/Unlock", runtime.WithHTTPPathPattern("/v1/admin/wallet/unlock")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WalletService_Unlock_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Unlock_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -338,31 +374,6 @@ func RegisterWalletServiceHandlerServer(ctx context.Context, mux *runtime.ServeM }) - mux.Handle("GET", pattern_WalletService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.WalletService/GetStatus", runtime.WithHTTPPathPattern("/v1/admin/wallet/status")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_WalletService_GetStatus_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_GetStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("GET", pattern_WalletService_DeriveAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -416,6 +427,181 @@ func RegisterWalletServiceHandlerServer(ctx context.Context, mux *runtime.ServeM return nil } +// RegisterWalletInitializerServiceHandlerFromEndpoint is same as RegisterWalletInitializerServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterWalletInitializerServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterWalletInitializerServiceHandler(ctx, mux, conn) +} + +// RegisterWalletInitializerServiceHandler registers the http handlers for service WalletInitializerService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterWalletInitializerServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterWalletInitializerServiceHandlerClient(ctx, mux, NewWalletInitializerServiceClient(conn)) +} + +// RegisterWalletInitializerServiceHandlerClient registers the http handlers for service WalletInitializerService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletInitializerServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletInitializerServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "WalletInitializerServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterWalletInitializerServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletInitializerServiceClient) error { + + mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletInitializerService/GenSeed", runtime.WithHTTPPathPattern("/v1/admin/wallet/seed")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletInitializerService_GenSeed_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_GenSeed_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Create", runtime.WithHTTPPathPattern("/v1/admin/wallet/create")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletInitializerService_Create_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Create_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Restore_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Restore", runtime.WithHTTPPathPattern("/v1/admin/wallet/restore")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletInitializerService_Restore_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Restore_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_WalletInitializerService_Unlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletInitializerService/Unlock", runtime.WithHTTPPathPattern("/v1/admin/wallet/unlock")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletInitializerService_Unlock_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_Unlock_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_WalletInitializerService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletInitializerService/GetStatus", runtime.WithHTTPPathPattern("/v1/admin/wallet/status")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletInitializerService_GetStatus_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletInitializerService_GetStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_WalletInitializerService_GenSeed_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "seed"}, "")) + + pattern_WalletInitializerService_Create_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "create"}, "")) + + pattern_WalletInitializerService_Restore_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "restore"}, "")) + + pattern_WalletInitializerService_Unlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "unlock"}, "")) + + pattern_WalletInitializerService_GetStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "status"}, "")) +) + +var ( + forward_WalletInitializerService_GenSeed_0 = runtime.ForwardResponseMessage + + forward_WalletInitializerService_Create_0 = runtime.ForwardResponseMessage + + forward_WalletInitializerService_Restore_0 = runtime.ForwardResponseMessage + + forward_WalletInitializerService_Unlock_0 = runtime.ForwardResponseMessage + + forward_WalletInitializerService_GetStatus_0 = runtime.ForwardResponseMessage +) + // RegisterWalletServiceHandlerFromEndpoint is same as RegisterWalletServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterWalletServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -451,97 +637,9 @@ func RegisterWalletServiceHandler(ctx context.Context, mux *runtime.ServeMux, co // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "WalletServiceClient" to call the correct interceptors. +// "WalletServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterWalletServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletServiceClient) error { - mux.Handle("GET", pattern_WalletService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletService/GenSeed", runtime.WithHTTPPathPattern("/v1/admin/wallet/seed")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WalletService_GenSeed_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_GenSeed_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletService/Create", runtime.WithHTTPPathPattern("/v1/admin/wallet/create")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WalletService_Create_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Create_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Restore_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletService/Restore", runtime.WithHTTPPathPattern("/v1/admin/wallet/restore")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WalletService_Restore_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Restore_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_WalletService_Unlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletService/Unlock", runtime.WithHTTPPathPattern("/v1/admin/wallet/unlock")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WalletService_Unlock_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_Unlock_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -564,28 +662,6 @@ func RegisterWalletServiceHandlerClient(ctx context.Context, mux *runtime.ServeM }) - mux.Handle("GET", pattern_WalletService_GetStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.WalletService/GetStatus", runtime.WithHTTPPathPattern("/v1/admin/wallet/status")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_WalletService_GetStatus_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_WalletService_GetStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("GET", pattern_WalletService_DeriveAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -634,36 +710,16 @@ func RegisterWalletServiceHandlerClient(ctx context.Context, mux *runtime.ServeM } var ( - pattern_WalletService_GenSeed_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "seed"}, "")) - - pattern_WalletService_Create_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "create"}, "")) - - pattern_WalletService_Restore_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "restore"}, "")) - - pattern_WalletService_Unlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "unlock"}, "")) - pattern_WalletService_Lock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "lock"}, "")) - pattern_WalletService_GetStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "status"}, "")) - pattern_WalletService_DeriveAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "address"}, "")) pattern_WalletService_GetBalance_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "admin", "wallet", "balance"}, "")) ) var ( - forward_WalletService_GenSeed_0 = runtime.ForwardResponseMessage - - forward_WalletService_Create_0 = runtime.ForwardResponseMessage - - forward_WalletService_Restore_0 = runtime.ForwardResponseMessage - - forward_WalletService_Unlock_0 = runtime.ForwardResponseMessage - forward_WalletService_Lock_0 = runtime.ForwardResponseMessage - forward_WalletService_GetStatus_0 = runtime.ForwardResponseMessage - forward_WalletService_DeriveAddress_0 = runtime.ForwardResponseMessage forward_WalletService_GetBalance_0 = runtime.ForwardResponseMessage diff --git a/server/api-spec/protobuf/gen/ark/v1/wallet_grpc.pb.go b/server/api-spec/protobuf/gen/ark/v1/wallet_grpc.pb.go index feeebc4..ed05381 100644 --- a/server/api-spec/protobuf/gen/ark/v1/wallet_grpc.pb.go +++ b/server/api-spec/protobuf/gen/ark/v1/wallet_grpc.pb.go @@ -14,16 +14,239 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 -// WalletServiceClient is the client API for WalletService service. +// WalletInitializerServiceClient is the client API for WalletInitializerService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type WalletServiceClient interface { +type WalletInitializerServiceClient interface { GenSeed(ctx context.Context, in *GenSeedRequest, opts ...grpc.CallOption) (*GenSeedResponse, error) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) Restore(ctx context.Context, in *RestoreRequest, opts ...grpc.CallOption) (*RestoreResponse, error) Unlock(ctx context.Context, in *UnlockRequest, opts ...grpc.CallOption) (*UnlockResponse, error) - Lock(ctx context.Context, in *LockRequest, opts ...grpc.CallOption) (*LockResponse, error) GetStatus(ctx context.Context, in *GetStatusRequest, opts ...grpc.CallOption) (*GetStatusResponse, error) +} + +type walletInitializerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWalletInitializerServiceClient(cc grpc.ClientConnInterface) WalletInitializerServiceClient { + return &walletInitializerServiceClient{cc} +} + +func (c *walletInitializerServiceClient) GenSeed(ctx context.Context, in *GenSeedRequest, opts ...grpc.CallOption) (*GenSeedResponse, error) { + out := new(GenSeedResponse) + err := c.cc.Invoke(ctx, "/ark.v1.WalletInitializerService/GenSeed", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletInitializerServiceClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) { + out := new(CreateResponse) + err := c.cc.Invoke(ctx, "/ark.v1.WalletInitializerService/Create", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletInitializerServiceClient) Restore(ctx context.Context, in *RestoreRequest, opts ...grpc.CallOption) (*RestoreResponse, error) { + out := new(RestoreResponse) + err := c.cc.Invoke(ctx, "/ark.v1.WalletInitializerService/Restore", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletInitializerServiceClient) Unlock(ctx context.Context, in *UnlockRequest, opts ...grpc.CallOption) (*UnlockResponse, error) { + out := new(UnlockResponse) + err := c.cc.Invoke(ctx, "/ark.v1.WalletInitializerService/Unlock", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletInitializerServiceClient) GetStatus(ctx context.Context, in *GetStatusRequest, opts ...grpc.CallOption) (*GetStatusResponse, error) { + out := new(GetStatusResponse) + err := c.cc.Invoke(ctx, "/ark.v1.WalletInitializerService/GetStatus", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WalletInitializerServiceServer is the server API for WalletInitializerService service. +// All implementations should embed UnimplementedWalletInitializerServiceServer +// for forward compatibility +type WalletInitializerServiceServer interface { + GenSeed(context.Context, *GenSeedRequest) (*GenSeedResponse, error) + Create(context.Context, *CreateRequest) (*CreateResponse, error) + Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) + Unlock(context.Context, *UnlockRequest) (*UnlockResponse, error) + GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) +} + +// UnimplementedWalletInitializerServiceServer should be embedded to have forward compatible implementations. +type UnimplementedWalletInitializerServiceServer struct { +} + +func (UnimplementedWalletInitializerServiceServer) GenSeed(context.Context, *GenSeedRequest) (*GenSeedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GenSeed not implemented") +} +func (UnimplementedWalletInitializerServiceServer) Create(context.Context, *CreateRequest) (*CreateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedWalletInitializerServiceServer) Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented") +} +func (UnimplementedWalletInitializerServiceServer) Unlock(context.Context, *UnlockRequest) (*UnlockResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Unlock not implemented") +} +func (UnimplementedWalletInitializerServiceServer) GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented") +} + +// UnsafeWalletInitializerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WalletInitializerServiceServer will +// result in compilation errors. +type UnsafeWalletInitializerServiceServer interface { + mustEmbedUnimplementedWalletInitializerServiceServer() +} + +func RegisterWalletInitializerServiceServer(s grpc.ServiceRegistrar, srv WalletInitializerServiceServer) { + s.RegisterService(&WalletInitializerService_ServiceDesc, srv) +} + +func _WalletInitializerService_GenSeed_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenSeedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletInitializerServiceServer).GenSeed(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ark.v1.WalletInitializerService/GenSeed", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletInitializerServiceServer).GenSeed(ctx, req.(*GenSeedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WalletInitializerService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletInitializerServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ark.v1.WalletInitializerService/Create", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletInitializerServiceServer).Create(ctx, req.(*CreateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WalletInitializerService_Restore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestoreRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletInitializerServiceServer).Restore(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ark.v1.WalletInitializerService/Restore", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletInitializerServiceServer).Restore(ctx, req.(*RestoreRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WalletInitializerService_Unlock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnlockRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletInitializerServiceServer).Unlock(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ark.v1.WalletInitializerService/Unlock", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletInitializerServiceServer).Unlock(ctx, req.(*UnlockRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WalletInitializerService_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletInitializerServiceServer).GetStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ark.v1.WalletInitializerService/GetStatus", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletInitializerServiceServer).GetStatus(ctx, req.(*GetStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WalletInitializerService_ServiceDesc is the grpc.ServiceDesc for WalletInitializerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WalletInitializerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ark.v1.WalletInitializerService", + HandlerType: (*WalletInitializerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GenSeed", + Handler: _WalletInitializerService_GenSeed_Handler, + }, + { + MethodName: "Create", + Handler: _WalletInitializerService_Create_Handler, + }, + { + MethodName: "Restore", + Handler: _WalletInitializerService_Restore_Handler, + }, + { + MethodName: "Unlock", + Handler: _WalletInitializerService_Unlock_Handler, + }, + { + MethodName: "GetStatus", + Handler: _WalletInitializerService_GetStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ark/v1/wallet.proto", +} + +// WalletServiceClient is the client API for WalletService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WalletServiceClient interface { + Lock(ctx context.Context, in *LockRequest, opts ...grpc.CallOption) (*LockResponse, error) DeriveAddress(ctx context.Context, in *DeriveAddressRequest, opts ...grpc.CallOption) (*DeriveAddressResponse, error) GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error) } @@ -36,42 +259,6 @@ func NewWalletServiceClient(cc grpc.ClientConnInterface) WalletServiceClient { return &walletServiceClient{cc} } -func (c *walletServiceClient) GenSeed(ctx context.Context, in *GenSeedRequest, opts ...grpc.CallOption) (*GenSeedResponse, error) { - out := new(GenSeedResponse) - err := c.cc.Invoke(ctx, "/ark.v1.WalletService/GenSeed", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *walletServiceClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) { - out := new(CreateResponse) - err := c.cc.Invoke(ctx, "/ark.v1.WalletService/Create", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *walletServiceClient) Restore(ctx context.Context, in *RestoreRequest, opts ...grpc.CallOption) (*RestoreResponse, error) { - out := new(RestoreResponse) - err := c.cc.Invoke(ctx, "/ark.v1.WalletService/Restore", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *walletServiceClient) Unlock(ctx context.Context, in *UnlockRequest, opts ...grpc.CallOption) (*UnlockResponse, error) { - out := new(UnlockResponse) - err := c.cc.Invoke(ctx, "/ark.v1.WalletService/Unlock", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *walletServiceClient) Lock(ctx context.Context, in *LockRequest, opts ...grpc.CallOption) (*LockResponse, error) { out := new(LockResponse) err := c.cc.Invoke(ctx, "/ark.v1.WalletService/Lock", in, out, opts...) @@ -81,15 +268,6 @@ func (c *walletServiceClient) Lock(ctx context.Context, in *LockRequest, opts .. return out, nil } -func (c *walletServiceClient) GetStatus(ctx context.Context, in *GetStatusRequest, opts ...grpc.CallOption) (*GetStatusResponse, error) { - out := new(GetStatusResponse) - err := c.cc.Invoke(ctx, "/ark.v1.WalletService/GetStatus", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *walletServiceClient) DeriveAddress(ctx context.Context, in *DeriveAddressRequest, opts ...grpc.CallOption) (*DeriveAddressResponse, error) { out := new(DeriveAddressResponse) err := c.cc.Invoke(ctx, "/ark.v1.WalletService/DeriveAddress", in, out, opts...) @@ -112,12 +290,7 @@ func (c *walletServiceClient) GetBalance(ctx context.Context, in *GetBalanceRequ // All implementations should embed UnimplementedWalletServiceServer // for forward compatibility type WalletServiceServer interface { - GenSeed(context.Context, *GenSeedRequest) (*GenSeedResponse, error) - Create(context.Context, *CreateRequest) (*CreateResponse, error) - Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) - Unlock(context.Context, *UnlockRequest) (*UnlockResponse, error) Lock(context.Context, *LockRequest) (*LockResponse, error) - GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) DeriveAddress(context.Context, *DeriveAddressRequest) (*DeriveAddressResponse, error) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) } @@ -126,24 +299,9 @@ type WalletServiceServer interface { type UnimplementedWalletServiceServer struct { } -func (UnimplementedWalletServiceServer) GenSeed(context.Context, *GenSeedRequest) (*GenSeedResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GenSeed not implemented") -} -func (UnimplementedWalletServiceServer) Create(context.Context, *CreateRequest) (*CreateResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") -} -func (UnimplementedWalletServiceServer) Restore(context.Context, *RestoreRequest) (*RestoreResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented") -} -func (UnimplementedWalletServiceServer) Unlock(context.Context, *UnlockRequest) (*UnlockResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Unlock not implemented") -} func (UnimplementedWalletServiceServer) Lock(context.Context, *LockRequest) (*LockResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Lock not implemented") } -func (UnimplementedWalletServiceServer) GetStatus(context.Context, *GetStatusRequest) (*GetStatusResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented") -} func (UnimplementedWalletServiceServer) DeriveAddress(context.Context, *DeriveAddressRequest) (*DeriveAddressResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeriveAddress not implemented") } @@ -162,78 +320,6 @@ func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServe s.RegisterService(&WalletService_ServiceDesc, srv) } -func _WalletService_GenSeed_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GenSeedRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).GenSeed(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.WalletService/GenSeed", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).GenSeed(ctx, req.(*GenSeedRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _WalletService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CreateRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).Create(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.WalletService/Create", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).Create(ctx, req.(*CreateRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _WalletService_Restore_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RestoreRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).Restore(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.WalletService/Restore", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).Restore(ctx, req.(*RestoreRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _WalletService_Unlock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UnlockRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).Unlock(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.WalletService/Unlock", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).Unlock(ctx, req.(*UnlockRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _WalletService_Lock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LockRequest) if err := dec(in); err != nil { @@ -252,24 +338,6 @@ func _WalletService_Lock_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } -func _WalletService_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetStatusRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WalletServiceServer).GetStatus(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/ark.v1.WalletService/GetStatus", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WalletServiceServer).GetStatus(ctx, req.(*GetStatusRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _WalletService_DeriveAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeriveAddressRequest) if err := dec(in); err != nil { @@ -313,30 +381,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "ark.v1.WalletService", HandlerType: (*WalletServiceServer)(nil), Methods: []grpc.MethodDesc{ - { - MethodName: "GenSeed", - Handler: _WalletService_GenSeed_Handler, - }, - { - MethodName: "Create", - Handler: _WalletService_Create_Handler, - }, - { - MethodName: "Restore", - Handler: _WalletService_Restore_Handler, - }, - { - MethodName: "Unlock", - Handler: _WalletService_Unlock_Handler, - }, { MethodName: "Lock", Handler: _WalletService_Lock_Handler, }, - { - MethodName: "GetStatus", - Handler: _WalletService_GetStatus_Handler, - }, { MethodName: "DeriveAddress", Handler: _WalletService_DeriveAddress_Handler, diff --git a/server/cmd/arkd/commands.go b/server/cmd/arkd/commands.go new file mode 100644 index 0000000..9c5c1e3 --- /dev/null +++ b/server/cmd/arkd/commands.go @@ -0,0 +1,447 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/urfave/cli/v2" + "gopkg.in/macaroon.v2" +) + +// flags +var ( + passwordFlag = &cli.StringFlag{ + Name: "password", + Usage: "wallet password", + Required: true, + } + mnemonicFlag = &cli.StringFlag{ + Name: "mnemonic", + Usage: "mnemonic from which restore the wallet", + } + gapLimitFlag = &cli.Uint64Flag{ + Name: "addr-gap-limit", + Usage: "address gap limit for wallet restoration", + Value: 100, + } +) + +// commands +var ( + walletCmd = &cli.Command{ + Name: "wallet", + Usage: "Manage the Ark Server wallet", + Subcommands: append( + cli.Commands{}, + walletStatusCmd, + walletCreateOrRestoreCmd, + walletUnlockCmd, + walletAddressCmd, + walletBalanceCmd, + ), + } + walletStatusCmd = &cli.Command{ + Name: "status", + Usage: "Get info about the status of the wallet", + Action: walletStatusAction, + } + walletCreateOrRestoreCmd = &cli.Command{ + Name: "create", + Usage: "Create or restore the wallet", + Action: walletCreateOrRestoreAction, + Flags: []cli.Flag{passwordFlag, mnemonicFlag, gapLimitFlag}, + } + walletUnlockCmd = &cli.Command{ + Name: "unlock", + Usage: "Unlock the wallet", + Action: walletUnlockAction, + Flags: []cli.Flag{passwordFlag}, + } + walletAddressCmd = &cli.Command{ + Name: "address", + Usage: "Generate a receiving address", + Action: walletAddressAction, + } + walletBalanceCmd = &cli.Command{ + Name: "balance", + Usage: "Get the wallet balance", + Action: walletBalanceAction, + } +) + +func walletStatusAction(ctx *cli.Context) error { + baseURL := ctx.String("url") + tlsCertPath := ctx.String("tls-cert-path") + if strings.Contains(baseURL, "http://") { + tlsCertPath = "" + } + + url := fmt.Sprintf("%s/v1/admin/wallet/status", baseURL) + status, err := getStatus(url, tlsCertPath) + if err != nil { + return err + } + + fmt.Println(status) + return nil +} + +func walletCreateOrRestoreAction(ctx *cli.Context) error { + baseURL := ctx.String("url") + password := ctx.String("password") + mnemonic := ctx.String("mnemonic") + gapLimit := ctx.Uint64("addr-gap-limit") + tlsCertPath := ctx.String("tls-cert-path") + if strings.Contains(baseURL, "http://") { + tlsCertPath = "" + } + + if len(mnemonic) > 0 { + url := fmt.Sprintf("%s/v1/admin/wallet/restore", baseURL) + body := fmt.Sprintf( + `{"seed": "%s", "password": "%s", "gap_limit": %d}`, + mnemonic, password, gapLimit, + ) + if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil { + return err + } + + fmt.Println("wallet restored") + return nil + } + + url := fmt.Sprintf("%s/v1/admin/wallet/seed", baseURL) + seed, err := get[string](url, "seed", "", tlsCertPath) + if err != nil { + return err + } + + url = fmt.Sprintf("%s/v1/admin/wallet/create", baseURL) + body := fmt.Sprintf( + `{"seed": "%s", "password": "%s"}`, seed, password, + ) + if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil { + return err + } + + fmt.Println(seed) + return nil +} + +func walletUnlockAction(ctx *cli.Context) error { + baseURL := ctx.String("url") + password := ctx.String("password") + url := fmt.Sprintf("%s/v1/admin/wallet/unlock", baseURL) + body := fmt.Sprintf(`{"password": "%s"}`, password) + tlsCertPath := ctx.String("tls-cert-path") + if strings.Contains(baseURL, "http://") { + tlsCertPath = "" + } + + if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil { + return err + } + + fmt.Println("wallet unlocked") + return nil +} + +func walletAddressAction(ctx *cli.Context) error { + baseURL := ctx.String("url") + var macaroon string + if !ctx.Bool("no-macaroon") { + macaroonPath := ctx.String("macaroon-path") + mac, err := getMacaroon(macaroonPath) + if err != nil { + return err + } + macaroon = mac + } + tlsCertPath := ctx.String("tls-cert-path") + if strings.Contains(baseURL, "http://") { + tlsCertPath = "" + } + + url := fmt.Sprintf("%s/v1/admin/wallet/address", baseURL) + addr, err := get[string](url, "address", macaroon, tlsCertPath) + if err != nil { + return err + } + + fmt.Println(addr) + return nil +} + +func walletBalanceAction(ctx *cli.Context) error { + baseURL := ctx.String("url") + var macaroon string + if !ctx.Bool("no-macaroon") { + macaroonPath := ctx.String("macaroon-path") + mac, err := getMacaroon(macaroonPath) + if err != nil { + return err + } + macaroon = mac + } + tlsCertPath := ctx.String("tls-cert-path") + if strings.Contains(baseURL, "http://") { + tlsCertPath = "" + } + + url := fmt.Sprintf("%s/v1/admin/wallet/balance", baseURL) + balance, err := getBalance(url, macaroon, tlsCertPath) + if err != nil { + return err + } + + fmt.Println(balance) + return nil +} + +func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error) { + tlsConfig, err := getTLSConfig(tlsCert) + if err != nil { + return + } + req, err := http.NewRequest("POST", url, strings.NewReader(body)) + if err != nil { + return + } + req.Header.Add("Content-Type", "application/json") + if len(macaroon) > 0 { + req.Header.Add("X-Macaroon", macaroon) + } + client := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + + resp, err := client.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return + } + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf(string(buf)) + return + } + if key == "" { + return + } + res := make(map[string]T) + if err = json.Unmarshal(buf, &res); err != nil { + return + } + + result = res[key] + return +} + +func get[T any](url, key, macaroon, tlsCert string) (result T, err error) { + tlsConfig, err := getTLSConfig(tlsCert) + if err != nil { + return + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + req.Header.Add("Content-Type", "application/json") + if len(macaroon) > 0 { + req.Header.Add("X-Macaroon", macaroon) + } + + client := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + resp, err := client.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return + } + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf(string(buf)) + return + } + + res := make(map[string]T) + if err = json.Unmarshal(buf, &res); err != nil { + return + } + + result = res[key] + return +} + +type accountBalance struct { + Available string `json:"available"` + Locked string `json:"locked"` +} + +func (b accountBalance) String() string { + return fmt.Sprintf(" available: %s\n locked: %s", b.Available, b.Locked) +} + +type balance struct { + MainAccount accountBalance `json:"mainAccount"` + ConnectorsAccount accountBalance `json:"connectorsAccount"` +} + +func (b balance) String() string { + return fmt.Sprintf( + "main account\n%s\nconnectors account\n%s", + b.MainAccount, b.ConnectorsAccount, + ) +} + +func getBalance(url, macaroon, tlsCert string) (*balance, error) { + tlsConfig, err := getTLSConfig(tlsCert) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + if len(macaroon) > 0 { + req.Header.Add("X-Macaroon", macaroon) + } + client := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf(string(buf)) + return nil, err + } + + result := &balance{} + if err := json.Unmarshal(buf, result); err != nil { + return nil, err + } + return result, nil +} + +type status struct { + Initialized bool `json:"initialized"` + Unlocked bool `json:"unlocked"` + Synced bool `json:"synced"` +} + +func (s status) String() string { + return fmt.Sprintf( + "initialized: %t\nunlocked: %t\nsynced: %t", + s.Initialized, s.Unlocked, s.Synced, + ) +} + +func getStatus(url, tlsCert string) (*status, error) { + tlsConfig, err := getTLSConfig(tlsCert) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf(string(buf)) + return nil, err + } + + result := &status{} + if err := json.Unmarshal(buf, result); err != nil { + return nil, err + } + return result, nil +} + +func getMacaroon(path string) (string, error) { + macBytes, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read macaroon %s: %s", path, err) + } + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(macBytes); err != nil { + return "", fmt.Errorf("failed to parse macaroon %s: %s", path, err) + } + + return hex.EncodeToString(macBytes), nil +} + +func getTLSConfig(path string) (*tls.Config, error) { + if len(path) <= 0 { + return nil, nil + } + var buf []byte + buf, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM(buf); !ok { + return nil, fmt.Errorf("failed to parse tls cert") + } + + return &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: caCertPool, + }, nil +} diff --git a/server/cmd/arkd/main.go b/server/cmd/arkd/main.go index ba1799b..319859e 100755 --- a/server/cmd/arkd/main.go +++ b/server/cmd/arkd/main.go @@ -1,14 +1,18 @@ package main import ( + "fmt" "os" "os/signal" + "path/filepath" "syscall" + "github.com/ark-network/ark/common" appconfig "github.com/ark-network/ark/internal/app-config" "github.com/ark-network/ark/internal/config" grpcservice "github.com/ark-network/ark/internal/interface/grpc" log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) //nolint:all @@ -18,19 +22,45 @@ var ( date = "unknown" ) -func main() { +// flags +var ( + urlFlag = &cli.StringFlag{ + Name: "url", + Usage: "the url where to reach ark server", + Value: fmt.Sprintf("http://localhost:%d", config.DefaultPort), + } + noMacaroonFlag = &cli.BoolFlag{ + Name: "no-macaroon", + Usage: "don't use macaroon auth", + Value: false, + } + macaroonFlag = &cli.StringFlag{ + Name: "macaroon-path", + Usage: "the path where to find the admin macaroon file", + Value: filepath.Join(common.AppDataDir("arkd", false), "macaroons", "admin.macaroon"), + } + tlsCertFlag = &cli.StringFlag{ + Name: "tls-cert-path", + Usage: "the path where to find the TLS certificate", + Value: filepath.Join(common.AppDataDir("arkd", false), "tls", "cert.pem"), + } +) + +func mainAction(_ *cli.Context) error { cfg, err := config.LoadConfig() if err != nil { - log.WithError(err).Fatal("invalid config") + return fmt.Errorf("invalid config: %s", err) } log.SetLevel(log.Level(cfg.LogLevel)) svcConfig := grpcservice.Config{ - Port: cfg.Port, - NoTLS: cfg.NoTLS, - AuthUser: cfg.AuthUser, - AuthPass: cfg.AuthPass, + Datadir: cfg.Datadir, + Port: cfg.Port, + NoTLS: cfg.NoTLS, + NoMacaroons: cfg.NoMacaroons, + TLSExtraIPs: cfg.TLSExtraIPs, + TLSExtraDomains: cfg.TLSExtraDomains, } appConfig := &appconfig.Config{ @@ -53,14 +83,14 @@ func main() { } svc, err := grpcservice.NewService(svcConfig, appConfig) if err != nil { - log.Fatal(err) + return err } log.RegisterExitHandler(svc.Stop) log.Info("starting service...") if err := svc.Start(); err != nil { - log.Fatal(err) + return err } sigChan := make(chan os.Signal, 1) @@ -69,4 +99,20 @@ func main() { log.Info("shutting down service...") log.Exit(0) + + return nil +} + +func main() { + app := cli.NewApp() + app.Version = version + app.Name = "Arkd CLI" + app.Usage = "arkd command line interface" + app.Commands = append(app.Commands, walletCmd) + app.Action = mainAction + app.Flags = append(app.Flags, urlFlag, noMacaroonFlag, macaroonFlag, tlsCertFlag) + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } } diff --git a/server/go.mod b/server/go.mod index 768a9a3..f5ae0c5 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,11 +1,17 @@ module github.com/ark-network/ark -go 1.22.2 +go 1.22.4 replace github.com/ark-network/ark/common => ../common +replace github.com/ark-network/tools/macaroons => ./pkg/macaroons + +replace github.com/ark-network/tools/kvdb => ./pkg/kvdb + require ( github.com/ark-network/ark/common v0.0.0 + github.com/ark-network/tools/kvdb v0.0.0-00010101000000-000000000000 + github.com/ark-network/tools/macaroons v0.0.0-00010101000000-000000000000 github.com/btcsuite/btcwallet/walletdb v1.4.2 github.com/btcsuite/btcwallet/wtxmgr v1.5.3 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 @@ -21,11 +27,14 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/timshannon/badgerhold/v4 v4.0.3 + github.com/urfave/cli/v2 v2.27.3 github.com/vulpemventures/go-bip39 v1.0.2 github.com/vulpemventures/go-elements v0.5.4 google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 + gopkg.in/macaroon-bakery.v2 v2.3.0 + gopkg.in/macaroon.v2 v2.1.0 modernc.org/sqlite v1.29.10 ) @@ -47,10 +56,11 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/decred/dcrd/lru v1.1.3 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v27.1.1+incompatible // indirect @@ -61,10 +71,12 @@ require ( github.com/go-errors/errors v1.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect - github.com/google/btree v1.0.1 // indirect + github.com/google/btree v1.1.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect @@ -92,8 +104,8 @@ require ( github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn v1.1.0 // indirect - github.com/lightningnetwork/lnd/healthcheck v1.2.4 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.8 // indirect + github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect + github.com/lightningnetwork/lnd/kvdb v1.4.10 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.2 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect @@ -117,6 +129,8 @@ require ( github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect @@ -126,22 +140,23 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect - go.etcd.io/bbolt v1.3.7 // indirect - go.etcd.io/etcd/api/v3 v3.5.12 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect - go.etcd.io/etcd/client/v2 v2.305.12 // indirect - go.etcd.io/etcd/client/v3 v3.5.12 // indirect - go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect - go.etcd.io/etcd/raft/v3 v3.5.7 // indirect - go.etcd.io/etcd/server/v3 v3.5.7 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.etcd.io/etcd/api/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/v2 v2.305.15 // indirect + go.etcd.io/etcd/client/v3 v3.5.15 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/raft/v3 v3.5.15 // indirect + go.etcd.io/etcd/server/v3 v3.5.15 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.0.1 // indirect + go.opentelemetry.io/otel/sdk v1.20.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v0.9.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect @@ -149,6 +164,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/server/go.sum b/server/go.sum index a903d14..81dc63f 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,15 +1,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= @@ -82,12 +79,9 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -97,16 +91,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -119,6 +107,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -162,22 +152,21 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0= github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= +github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= @@ -194,14 +183,17 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= +github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -231,20 +223,20 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.9+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -252,7 +244,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -348,6 +339,11 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= +github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= +github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl2uZhryo= +github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= +github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -376,6 +372,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -393,10 +390,10 @@ github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsD github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= github.com/lightningnetwork/lnd/fn v1.1.0 h1:W1p/bUXMgAh5YlmawdQYaNgmLaLMT77BilepzWOSZ2A= github.com/lightningnetwork/lnd/fn v1.1.0/go.mod h1:P027+0CyELd92H9gnReUkGGAqbFA1HwjHWdfaDFD51U= -github.com/lightningnetwork/lnd/healthcheck v1.2.4 h1:lLPLac+p/TllByxGSlkCwkJlkddqMP5UCoawCj3mgFQ= -github.com/lightningnetwork/lnd/healthcheck v1.2.4/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= -github.com/lightningnetwork/lnd/kvdb v1.4.8 h1:xH0a5Vi1yrcZ5BEeF2ba3vlKBRxrL9uYXlWTjOjbNTY= -github.com/lightningnetwork/lnd/kvdb v1.4.8/go.mod h1:J2diNABOoII9UrMnxXS5w7vZwP7CA1CStrl8MnIrb3A= +github.com/lightningnetwork/lnd/healthcheck v1.2.5 h1:aTJy5xeBpcWgRtW/PGBDe+LMQEmNm/HQewlQx2jt7OA= +github.com/lightningnetwork/lnd/healthcheck v1.2.5/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= +github.com/lightningnetwork/lnd/kvdb v1.4.10 h1:vK89IVv1oVH9ubQWU+EmoCQFeVRaC8kfmOrqHbY5zoY= +github.com/lightningnetwork/lnd/kvdb v1.4.10/go.mod h1:J2diNABOoII9UrMnxXS5w7vZwP7CA1CStrl8MnIrb3A= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/sqldb v1.0.2 h1:PfuYzScYMD9/QonKo/QvgsbXfTnH5DfldIimkfdW4Bk= @@ -503,16 +500,20 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -576,6 +577,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1 github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= +github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 h1:CTcw80hz/Sw8hqlKX5ZYvBUF5gAHSHwdjXxRf/cjDcI= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:GXBJykxW2kUcktGdsgyay7uwwWvkljASfljNcT0mbh8= github.com/vulpemventures/go-bip32 v0.0.0-20200624192635-867c159da4d7 h1:X7DtNv+YWy76kELMZB/xVkIJ7YNp2vpgMFVsDcQA40U= @@ -597,27 +600,29 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= -go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= -go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A= -go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= -go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI= -go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E= -go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg= -go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= -go.etcd.io/etcd/pkg/v3 v3.5.7 h1:obOzeVwerFwZ9trMWapU/VjDcYUJb5OfgC1zqEGWO/0= -go.etcd.io/etcd/pkg/v3 v3.5.7/go.mod h1:kcOfWt3Ov9zgYdOiJ/o1Y9zFfLhQjylTgL4Lru8opRo= -go.etcd.io/etcd/raft/v3 v3.5.7 h1:aN79qxLmV3SvIq84aNTliYGmjwsW6NqJSnqmI1HLJKc= -go.etcd.io/etcd/raft/v3 v3.5.7/go.mod h1:TflkAb/8Uy6JFBxcRaH2Fr6Slm9mCPVdI2efzxY96yU= -go.etcd.io/etcd/server/v3 v3.5.7 h1:BTBD8IJUV7YFgsczZMHhMTS67XuA4KpRquL0MFOJGRk= -go.etcd.io/etcd/server/v3 v3.5.7/go.mod h1:gxBgT84issUVBRpZ3XkW1T55NjOb4vZZRI4wVvNhf4A= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v2 v2.305.15 h1:VG2xbf8Vz1KJh65Ar2V5eDmfkp1bpzkSEHlhJM3usp8= +go.etcd.io/etcd/client/v2 v2.305.15/go.mod h1:Ad5dRjPVb/n5yXgAWQ/hXzuXXkBk0Y658ocuXYaUU48= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.etcd.io/etcd/pkg/v3 v3.5.15 h1:/Iu6Sr3iYaAjy++8sIDoZW9/EfhcwLZwd4FOZX2mMOU= +go.etcd.io/etcd/pkg/v3 v3.5.15/go.mod h1:e3Acf298sPFmTCGTrnGvkClEw9RYIyPtNzi1XM8rets= +go.etcd.io/etcd/raft/v3 v3.5.15 h1:jOA2HJF7zb3wy8H/pL13e8geWqkEa/kUs0waUggZC0I= +go.etcd.io/etcd/raft/v3 v3.5.15/go.mod h1:k3r7P4seEiUcgxOPLp+mloJWV3Q4QLPGNvy/OgC8OtM= +go.etcd.io/etcd/server/v3 v3.5.15 h1:x35jrWnZgsRwMsFsUJIUdT1bvzIz1B+29HjMfRYVN/E= +go.etcd.io/etcd/server/v3 v3.5.15/go.mod h1:l9jX9oa/iuArjqz0RNX/TDbc70dLXxRZo/nmPucrpFo= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= @@ -625,23 +630,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.0.1/go.mod h1:OPEOD4jIT2SlZPMmwT6FqZz2C0ZNdQqiWcoK6M0SNFU= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 h1:ofMbch7i29qIUf7VtF+r0HRF6ac0SBaPSziSsKp7wkk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1/go.mod h1:Kv8liBeVNFkkkbilbgWRpV+wWuu+H5xdOT6HAgd30iw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 h1:CFMFNoz+CGprjFAFy+RJFrfEe4GBia3RRm2a4fREvCA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1/go.mod h1:xOvWoTOrQjxjW61xtOmD/WKGRYb/P4NzRo3bs65U6Rk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.0.1 h1:wXxFEWGo7XfXupPwVJvTBOaPBC9FEg0wB8hMNrKk+cA= -go.opentelemetry.io/otel/sdk v1.0.1/go.mod h1:HrdXne+BiwsOHYYkBE5ysIcv2bvdZstxzmCQhxTcZkI= -go.opentelemetry.io/otel/trace v1.0.1/go.mod h1:5g4i4fKLaX2BQpSBsxw8YYcgKpMMSW3x7ZTuYBr3sUk= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.9.0 h1:C0g6TWmQYvjKRnljRULLWUVJGy8Uvu0NEL/5frY2/t4= -go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -652,8 +654,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -669,9 +671,11 @@ go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -699,6 +703,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -730,8 +735,6 @@ golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -774,7 +777,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -809,6 +811,7 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -856,9 +859,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -872,7 +872,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= @@ -882,11 +881,21 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= +gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= +gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic= +gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc= +gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= +gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 2e05458..894e159 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -11,6 +11,7 @@ import ( ) type Config struct { + Datadir string WalletAddr string RoundInterval int64 Port uint32 @@ -22,15 +23,16 @@ type Config struct { TxBuilderType string BlockchainScannerType string NoTLS bool + NoMacaroons bool Network common.Network LogLevel int MinRelayFee uint64 RoundLifetime int64 UnilateralExitDelay int64 - AuthUser string - AuthPass string EsploraURL string NeutrinoPeer string + TLSExtraIPs []string + TLSExtraDomains []string } var ( @@ -44,20 +46,21 @@ var ( SchedulerType = "SCHEDULER_TYPE" TxBuilderType = "TX_BUILDER_TYPE" BlockchainScannerType = "BC_SCANNER_TYPE" - Insecure = "INSECURE" LogLevel = "LOG_LEVEL" Network = "NETWORK" MinRelayFee = "MIN_RELAY_FEE" RoundLifetime = "ROUND_LIFETIME" UnilateralExitDelay = "UNILATERAL_EXIT_DELAY" - AuthUser = "AUTH_USER" - AuthPass = "AUTH_PASS" EsploraURL = "ESPLORA_URL" NeutrinoPeer = "NEUTRINO_PEER" + NoMacaroons = "NO_MACAROONS" + NoTLS = "NO_TLS" + TLSExtraIP = "TLS_EXTRA_IP" + TLSExtraDomain = "TLS_EXTRA_DOMAIN" defaultDatadir = common.AppDataDir("arkd", false) defaultRoundInterval = 5 - defaultPort = 6000 + DefaultPort = 6000 defaultWalletAddr = "localhost:18000" defaultDbType = "sqlite" defaultDbMigrationPath = "file://internal/infrastructure/db/sqlite/migration" @@ -65,14 +68,13 @@ var ( defaultSchedulerType = "gocron" defaultTxBuilderType = "covenant" defaultBlockchainScannerType = "ocean" - defaultInsecure = true defaultNetwork = "liquid" defaultLogLevel = 4 defaultMinRelayFee = 30 // 0.1 sat/vbyte on Liquid defaultRoundLifetime = 604672 defaultUnilateralExitDelay = 1024 - defaultAuthUser = "admin" - defaultAuthPass = "admin" + defaultNoMacaroons = false + defaultNoTLS = false ) func LoadConfig() (*Config, error) { @@ -80,10 +82,10 @@ func LoadConfig() (*Config, error) { viper.AutomaticEnv() viper.SetDefault(Datadir, defaultDatadir) - viper.SetDefault(Port, defaultPort) + viper.SetDefault(Port, DefaultPort) viper.SetDefault(DbType, defaultDbType) viper.SetDefault(DbMigrationPath, defaultDbMigrationPath) - viper.SetDefault(Insecure, defaultInsecure) + viper.SetDefault(NoTLS, defaultNoTLS) viper.SetDefault(LogLevel, defaultLogLevel) viper.SetDefault(Network, defaultNetwork) viper.SetDefault(WalletAddr, defaultWalletAddr) @@ -95,8 +97,7 @@ func LoadConfig() (*Config, error) { viper.SetDefault(TxBuilderType, defaultTxBuilderType) viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay) viper.SetDefault(BlockchainScannerType, defaultBlockchainScannerType) - viper.SetDefault(AuthUser, defaultAuthUser) - viper.SetDefault(AuthPass, defaultAuthPass) + viper.SetDefault(NoMacaroons, defaultNoMacaroons) net, err := getNetwork() if err != nil { @@ -108,6 +109,7 @@ func LoadConfig() (*Config, error) { } return &Config{ + Datadir: viper.GetString(Datadir), WalletAddr: viper.GetString(WalletAddr), RoundInterval: viper.GetInt64(RoundInterval), Port: viper.GetUint32(Port), @@ -117,17 +119,18 @@ func LoadConfig() (*Config, error) { SchedulerType: viper.GetString(SchedulerType), TxBuilderType: viper.GetString(TxBuilderType), BlockchainScannerType: viper.GetString(BlockchainScannerType), - NoTLS: viper.GetBool(Insecure), + NoTLS: viper.GetBool(NoTLS), DbDir: filepath.Join(viper.GetString(Datadir), "db"), LogLevel: viper.GetInt(LogLevel), Network: net, MinRelayFee: viper.GetUint64(MinRelayFee), RoundLifetime: viper.GetInt64(RoundLifetime), UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay), - AuthUser: viper.GetString(AuthUser), - AuthPass: viper.GetString(AuthPass), EsploraURL: viper.GetString(EsploraURL), NeutrinoPeer: viper.GetString(NeutrinoPeer), + NoMacaroons: viper.GetBool(NoMacaroons), + TLSExtraIPs: viper.GetStringSlice(TLSExtraIP), + TLSExtraDomains: viper.GetStringSlice(TLSExtraDomain), }, nil } diff --git a/server/internal/core/application/covenant.go b/server/internal/core/application/covenant.go index e8323ce..6828615 100644 --- a/server/internal/core/application/covenant.go +++ b/server/internal/core/application/covenant.go @@ -17,8 +17,6 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/decred/dcrd/dcrec/secp256k1/v4" log "github.com/sirupsen/logrus" - "github.com/vulpemventures/go-elements/network" - "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" ) @@ -42,9 +40,7 @@ type covenantService struct { eventsCh chan domain.RoundEvent onboardingCh chan onboarding - trustedOnboardingScriptLock *sync.Mutex - trustedOnboardingScripts map[string]*secp256k1.PublicKey - currentRound *domain.Round + currentRound *domain.Round } func NewCovenantService( @@ -70,8 +66,7 @@ func NewCovenantService( network, pubkey, roundLifetime, roundInterval, unilateralExitDelay, minRelayFee, walletSvc, repoManager, builder, scanner, sweeper, - paymentRequests, forfeitTxs, eventsCh, onboardingCh, - &sync.Mutex{}, make(map[string]*secp256k1.PublicKey), nil, + paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, } repoManager.RegisterEventsHandler( func(round *domain.Round) { @@ -236,57 +231,15 @@ func (s *covenantService) Onboard( log.Debugf("broadcasted boarding tx %s", txid) - sharedOutputScript := hex.EncodeToString(extracted.Outputs[0].Script) - if _, ok := s.trustedOnboardingScripts[sharedOutputScript]; !ok { - s.onboardingCh <- onboarding{ - tx: boardingTx, - congestionTree: congestionTree, - userPubkey: userPubkey, - } + s.onboardingCh <- onboarding{ + tx: boardingTx, + congestionTree: congestionTree, + userPubkey: userPubkey, } return nil } -func (s *covenantService) TrustedOnboarding( - ctx context.Context, userPubKey *secp256k1.PublicKey, -) (string, error) { - congestionTreeLeaf := tree.Receiver{ - Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()), - } - - _, sharedOutputScript, _, err := tree.CraftCongestionTree( - s.onchainNework().AssetID, s.pubkey, []tree.Receiver{congestionTreeLeaf}, - s.minRelayFee, s.roundLifetime, s.unilateralExitDelay, - ) - if err != nil { - return "", err - } - - pay, err := payment.FromScript(sharedOutputScript, s.onchainNework(), nil) - if err != nil { - return "", err - } - - address, err := pay.TaprootAddress() - if err != nil { - return "", err - } - - s.trustedOnboardingScriptLock.Lock() - - script := hex.EncodeToString(sharedOutputScript) - s.trustedOnboardingScripts[script] = userPubKey - - s.trustedOnboardingScriptLock.Unlock() - - if err := s.scanner.WatchScripts(ctx, []string{script}); err != nil { - return "", err - } - - return address, nil -} - func (s *covenantService) start() { s.startRound() } @@ -493,59 +446,7 @@ func (s *covenantService) listenToScannerNotifications() { vtxosRepo := s.repoManager.Vtxos() roundRepo := s.repoManager.Rounds() - for script, v := range vtxoKeys { - //onboarding - if userPubkey, ok := s.trustedOnboardingScripts[script]; ok { - congestionTreeLeaf := tree.Receiver{ - Pubkey: hex.EncodeToString(userPubkey.SerializeCompressed()), - Amount: v.Value - s.minRelayFee, - } - - treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree( - s.onchainNework().AssetID, s.pubkey, []tree.Receiver{congestionTreeLeaf}, - s.minRelayFee, s.roundLifetime, s.unilateralExitDelay, - ) - if err != nil { - log.WithError(err).Warn("failed to craft onboarding congestion tree") - return - } - - congestionTree, err := treeFactoryFn( - psetv2.InputArgs{ - Txid: v.Txid, - TxIndex: v.VOut, - }, - ) - if err != nil { - log.WithError(err).Warn("failed to build onboarding congestion tree") - return - } - - if sharedOutputAmount != v.Value { - log.Errorf("shared output amount mismatch, expected %d, got %d", sharedOutputAmount, v.Value) - return - } - - precomputedScript, _ := hex.DecodeString(script) - - if !bytes.Equal(sharedOutputScript, precomputedScript) { - log.Errorf("shared output script mismatch, expected %x, got %x", sharedOutputScript, precomputedScript) - return - } - - pubkey := hex.EncodeToString(userPubkey.SerializeCompressed()) - payments := getPaymentsFromOnboardingLiquid(congestionTree, pubkey) - round := domain.NewFinalizedRound( - dustAmount, pubkey, v.Txid, "", congestionTree, payments, - ) - if err := s.saveEvents(ctx, round.Id, round.Events()); err != nil { - log.WithError(err).Warn("failed to store new round events") - return - } - - return - } - + for _, v := range vtxoKeys { // redeem vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey}) if err != nil { @@ -931,19 +832,6 @@ func (s *covenantService) saveEvents( return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) } -func (s *covenantService) onchainNework() *network.Network { - switch s.network.Name { - case common.Liquid.Name: - return &network.Liquid - case common.LiquidRegTest.Name: - return &network.Regtest - case common.LiquidTestNet.Name: - return &network.Testnet - default: - return &network.Liquid - } -} - func getPaymentsFromOnboardingLiquid( congestionTree tree.CongestionTree, userKey string, ) []domain.Payment { diff --git a/server/internal/core/application/covenantless.go b/server/internal/core/application/covenantless.go index 3d74e7d..06b87af 100644 --- a/server/internal/core/application/covenantless.go +++ b/server/internal/core/application/covenantless.go @@ -243,12 +243,6 @@ func (s *covenantlessService) Onboard( return nil } -func (s *covenantlessService) TrustedOnboarding( - ctx context.Context, userPubKey *secp256k1.PublicKey, -) (string, error) { - return "", fmt.Errorf("not implemented") -} - func (s *covenantlessService) start() { s.startRound() } diff --git a/server/internal/core/application/types.go b/server/internal/core/application/types.go index 0f6f6de..77407f8 100644 --- a/server/internal/core/application/types.go +++ b/server/internal/core/application/types.go @@ -34,7 +34,6 @@ type Service interface { ctx context.Context, boardingTx string, congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey, ) error - TrustedOnboarding(ctx context.Context, userPubKey *secp256k1.PublicKey) (string, error) } type ServiceInfo struct { diff --git a/server/internal/infrastructure/db/service_test.go b/server/internal/infrastructure/db/service_test.go index b7598dd..f068dad 100644 --- a/server/internal/infrastructure/db/service_test.go +++ b/server/internal/infrastructure/db/service_test.go @@ -111,6 +111,7 @@ func TestService(t *testing.T) { testRoundRepository(t, svc) testVtxoRepository(t, svc) + time.Sleep(5 * time.Second) svc.Close() }) } diff --git a/server/internal/infrastructure/wallet/btc-embedded/wallet.go b/server/internal/infrastructure/wallet/btc-embedded/wallet.go index a02a3ef..edd5eda 100644 --- a/server/internal/infrastructure/wallet/btc-embedded/wallet.go +++ b/server/internal/infrastructure/wallet/btc-embedded/wallet.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/hex" - "errors" "fmt" "strings" "sync" @@ -89,12 +88,10 @@ type service struct { func WithNeutrino(initialPeer string) WalletOption { return func(s *service) error { if s.cfg.Network.Name == common.BitcoinRegTest.Name && len(initialPeer) == 0 { - return errors.New("initial neutrino peer required for regtest network, set NEUTRINO_PEER env var") + return fmt.Errorf("initial neutrino peer required for regtest network, set NEUTRINO_PEER env var") } - db, err := walletdb.Create( - "bdb", s.cfg.Datadir+"/neutrino.db", true, 60*time.Second, - ) + db, err := createOrOpenWalletDB(s.cfg.Datadir + "/neutrino.db") if err != nil { return err } @@ -235,7 +232,7 @@ func (s *service) DeriveAddresses(ctx context.Context, num int) ([]string, error } if len(addresses) == 0 { - return nil, errors.New("no addresses derived") + return nil, fmt.Errorf("no addresses derived") } return addresses, nil @@ -376,7 +373,7 @@ func (s *service) SignTransaction(ctx context.Context, partialTx string, extract if extractRawTx { // verify that all inputs are signed if len(signedInputs) != len(ptx.Inputs) { - return "", errors.New("not all inputs are signed, unable to finalize the psbt") + return "", fmt.Errorf("not all inputs are signed, unable to finalize the psbt") } if err := psbt.MaybeFinalizeAll(ptx); err != nil { @@ -602,6 +599,7 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { if len(password) <= 0 { return fmt.Errorf("missing password") } + pwd := []byte(password) seed := bip39.NewSeed(mnemonic, password) opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute) @@ -618,10 +616,12 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { ChainSource: s.chainSource, } blockCache := blockcache.NewBlockCache(20 * 1024 * 1024) + wallet, err := btcwallet.New(config, blockCache) if err != nil { return fmt.Errorf("failed to setup wallet loader: %s", err) } + if err := wallet.Start(); err != nil { return fmt.Errorf("failed to start wallet: %s", err) } @@ -639,6 +639,13 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { } log.Debugf("chain synced") + if addrGap > 0 { + // TODO: fix rescan + if err := wallet.InternalWallet().Rescan(nil, nil); err != nil { + return err + } + } + wallet.InternalWallet().Lock() s.wallet = wallet return nil @@ -707,7 +714,7 @@ func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { managedAddr, ok := addrInfos.(waddrmgr.ManagedPubKeyAddress) if !ok { - return errors.New("failed to cast address to managed pubkey address") + return fmt.Errorf("failed to cast address to managed pubkey address") } s.aspTaprootAddr = managedAddr @@ -737,7 +744,7 @@ func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { managedPubkeyAddr, ok := infos.(waddrmgr.ManagedPubKeyAddress) if !ok { - return errors.New("failed to cast address to managed pubkey address") + return fmt.Errorf("failed to cast address to managed pubkey address") } s.aspTaprootAddr = managedPubkeyAddr @@ -767,7 +774,7 @@ func (s *service) deriveNextAddress(account accountName) (btcutil.Address, error func withChainSource(chainSource chain.Interface) WalletOption { return func(s *service) error { if s.chainSource != nil { - return errors.New("chain source already set") + return fmt.Errorf("chain source already set") } s.chainSource = chainSource @@ -778,17 +785,27 @@ func withChainSource(chainSource chain.Interface) WalletOption { func withScanner(chainSource chain.Interface) WalletOption { return func(s *service) error { if s.scanner != nil { - return errors.New("scanner already set") + return fmt.Errorf("scanner already set") } if err := chainSource.Start(); err != nil { return fmt.Errorf("failed to start scanner: %s", err) } - s.scanner = chainSource return nil } } +func createOrOpenWalletDB(path string) (walletdb.DB, error) { + db, err := walletdb.Open("bdb", path, true, 60*time.Second) + if err == nil { + return db, nil + } + if err != walletdb.ErrDbDoesNotExist { + return nil, err + } + return walletdb.Create("bdb", path, true, 60*time.Second) +} + // status implements ports.WalletStatus interface type status struct { initialized bool diff --git a/server/internal/interface/grpc/config.go b/server/internal/interface/grpc/config.go index 85818ff..8132d7d 100644 --- a/server/internal/interface/grpc/config.go +++ b/server/internal/interface/grpc/config.go @@ -1,27 +1,25 @@ package grpcservice import ( + "crypto/rand" "crypto/tls" "fmt" "net" + "path/filepath" + + "golang.org/x/net/http2" ) type Config struct { - Port uint32 - NoTLS bool - AuthUser string - AuthPass string + Datadir string + Port uint32 + NoTLS bool + NoMacaroons bool + TLSExtraIPs []string + TLSExtraDomains []string } func (c Config) Validate() error { - if len(c.AuthUser) == 0 { - return fmt.Errorf("missing auth user") - } - - if len(c.AuthPass) == 0 { - return fmt.Errorf("missing auth password") - } - lis, err := net.Listen("tcp", c.address()) if err != nil { return fmt.Errorf("invalid port: %s", err) @@ -29,7 +27,40 @@ func (c Config) Validate() error { defer lis.Close() if !c.NoTLS { - return fmt.Errorf("tls termination not supported yet") + tlsDir := c.tlsDatadir() + tlsKeyExists := pathExists(filepath.Join(tlsDir, tlsKeyFile)) + tlsCertExists := pathExists(filepath.Join(tlsDir, tlsCertFile)) + if !tlsKeyExists && tlsCertExists { + return fmt.Errorf( + "found %s file but %s is missing. Please delete %s to make the "+ + "daemon recreating both files in path %s", + tlsCertFile, tlsKeyFile, tlsCertFile, tlsDir, + ) + } + + if len(c.TLSExtraIPs) > 0 { + for _, ip := range c.TLSExtraIPs { + if net.ParseIP(ip) == nil { + return fmt.Errorf("invalid operator extra ip %s", ip) + } + } + } + } + + if !c.NoMacaroons { + macDir := c.macaroonsDatadir() + adminMacExists := pathExists(filepath.Join(macDir, adminMacaroonFile)) + roMacExists := pathExists(filepath.Join(macDir, roMacaroonFile)) + walletMacExists := pathExists(filepath.Join(macDir, walletMacaroonFile)) + managerMacExists := pathExists(filepath.Join(macDir, managerMacaroonFile)) + + if adminMacExists != roMacExists || + adminMacExists != walletMacExists || + adminMacExists != managerMacExists { + return fmt.Errorf( + "all macaroons must be either existing or not in path %s", macDir, + ) + } } return nil } @@ -46,6 +77,52 @@ func (c Config) gatewayAddress() string { return fmt.Sprintf("localhost:%d", c.Port) } -func (c Config) tlsConfig() *tls.Config { - return nil +func (c Config) macaroonsDatadir() string { + return filepath.Join(c.Datadir, macaroonsFolder) +} + +func (c Config) tlsDatadir() string { + return filepath.Join(c.Datadir, tlsFolder) +} + +func (c Config) tlsKey() string { + if c.NoTLS { + return "" + } + return filepath.Join(c.tlsDatadir(), tlsKeyFile) +} + +func (c Config) tlsCert() string { + if c.NoTLS { + return "" + } + return filepath.Join(c.tlsDatadir(), tlsCertFile) +} + +func (c Config) tlsConfig() (*tls.Config, error) { + if c.NoTLS { + return nil, nil + } + + if c.tlsKey() == "" || c.tlsCert() == "" { + return nil, fmt.Errorf("tls_key and tls_cert both needs to be provided") + } + + certificate, err := tls.LoadX509KeyPair(c.tlsCert(), c.tlsKey()) + if err != nil { + return nil, err + } + + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + NextProtos: []string{"http/1.1", http2.NextProtoTLS, "h2-14"}, // h2-14 is just for compatibility. will be eventually removed. + Certificates: []tls.Certificate{certificate}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + } + config.Rand = rand.Reader + + return config, nil } diff --git a/server/internal/interface/grpc/handlers/arkservice.go b/server/internal/interface/grpc/handlers/arkservice.go index 0d57089..670b836 100644 --- a/server/internal/interface/grpc/handlers/arkservice.go +++ b/server/internal/interface/grpc/handlers/arkservice.go @@ -40,31 +40,6 @@ func NewHandler(service application.Service) arkv1.ArkServiceServer { return h } -func (h *handler) TrustedOnboarding(ctx context.Context, req *arkv1.TrustedOnboardingRequest) (*arkv1.TrustedOnboardingResponse, error) { - if req.GetUserPubkey() == "" { - return nil, status.Error(codes.InvalidArgument, "missing user pubkey") - } - - pubKey, err := hex.DecodeString(req.GetUserPubkey()) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid user pubkey") - } - - decodedPubKey, err := secp256k1.ParsePubKey(pubKey) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid user pubkey") - } - - address, err := h.svc.TrustedOnboarding(ctx, decodedPubKey) - if err != nil { - return nil, err - } - - return &arkv1.TrustedOnboardingResponse{ - Address: address, - }, nil -} - func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) { if req.GetUserPubkey() == "" { return nil, status.Error(codes.InvalidArgument, "missing user pubkey") diff --git a/server/internal/interface/grpc/handlers/walletservice.go b/server/internal/interface/grpc/handlers/walletservice.go index 31046e4..7518591 100644 --- a/server/internal/interface/grpc/handlers/walletservice.go +++ b/server/internal/interface/grpc/handlers/walletservice.go @@ -8,16 +8,19 @@ import ( "github.com/ark-network/ark/internal/core/ports" ) -type walletHandler struct { +type walletInitHandler struct { walletService ports.WalletService - onUnlock func() + onInit func(password string) + onUnlock func(password string) } -func NewWalletHandler(walletService ports.WalletService, onUnlock func()) arkv1.WalletServiceServer { - return &walletHandler{walletService, onUnlock} +func NewWalletInitializerHandler( + walletService ports.WalletService, onInit, onUnlock func(string), +) arkv1.WalletInitializerServiceServer { + return &walletInitHandler{walletService, onInit, onUnlock} } -func (a *walletHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (*arkv1.GenSeedResponse, error) { +func (a *walletInitHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (*arkv1.GenSeedResponse, error) { seed, err := a.walletService.GenSeed(ctx) if err != nil { return nil, err @@ -26,7 +29,7 @@ func (a *walletHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (* return &arkv1.GenSeedResponse{Seed: seed}, nil } -func (a *walletHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (*arkv1.CreateResponse, error) { +func (a *walletInitHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (*arkv1.CreateResponse, error) { if len(req.GetSeed()) <= 0 { return nil, fmt.Errorf("missing wallet seed") } @@ -40,10 +43,12 @@ func (a *walletHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (* return nil, err } + go a.onInit(req.GetPassword()) + return &arkv1.CreateResponse{}, nil } -func (a *walletHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest) (*arkv1.RestoreResponse, error) { +func (a *walletInitHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest) (*arkv1.RestoreResponse, error) { if len(req.GetSeed()) <= 0 { return nil, fmt.Errorf("missing wallet seed") } @@ -57,10 +62,12 @@ func (a *walletHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest) return nil, err } + go a.onInit(req.GetPassword()) + return &arkv1.RestoreResponse{}, nil } -func (a *walletHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (*arkv1.UnlockResponse, error) { +func (a *walletInitHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (*arkv1.UnlockResponse, error) { if len(req.GetPassword()) <= 0 { return nil, fmt.Errorf("missing wallet password") } @@ -68,11 +75,32 @@ func (a *walletHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (* return nil, err } - go a.onUnlock() + go a.onUnlock(req.GetPassword()) return &arkv1.UnlockResponse{}, nil } +func (a *walletInitHandler) GetStatus(ctx context.Context, _ *arkv1.GetStatusRequest) (*arkv1.GetStatusResponse, error) { + status, err := a.walletService.Status(ctx) + if err != nil { + return nil, err + } + + return &arkv1.GetStatusResponse{ + Initialized: status.IsInitialized(), + Unlocked: status.IsUnlocked(), + Synced: status.IsSynced(), + }, nil +} + +type walletHandler struct { + walletService ports.WalletService +} + +func NewWalletHandler(walletService ports.WalletService) arkv1.WalletServiceServer { + return &walletHandler{walletService} +} + func (a *walletHandler) Lock(ctx context.Context, req *arkv1.LockRequest) (*arkv1.LockResponse, error) { if len(req.GetPassword()) <= 0 { return nil, fmt.Errorf("missing wallet password") @@ -84,19 +112,6 @@ func (a *walletHandler) Lock(ctx context.Context, req *arkv1.LockRequest) (*arkv return &arkv1.LockResponse{}, nil } -func (a *walletHandler) GetStatus(ctx context.Context, _ *arkv1.GetStatusRequest) (*arkv1.GetStatusResponse, error) { - status, err := a.walletService.Status(ctx) - if err != nil { - return nil, err - } - - return &arkv1.GetStatusResponse{ - Initialized: status.IsInitialized(), - Unlocked: status.IsUnlocked(), - Synced: status.IsSynced(), - }, nil -} - func (a *walletHandler) DeriveAddress(ctx context.Context, _ *arkv1.DeriveAddressRequest) (*arkv1.DeriveAddressResponse, error) { addr, err := a.walletService.DeriveAddresses(ctx, 1) if err != nil { diff --git a/server/internal/interface/grpc/interceptors/auth.go b/server/internal/interface/grpc/interceptors/auth.go index 2236dc2..3454601 100644 --- a/server/internal/interface/grpc/interceptors/auth.go +++ b/server/internal/interface/grpc/interceptors/auth.go @@ -2,41 +2,70 @@ package interceptors import ( "context" - "encoding/base64" "fmt" - "strings" - arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" - grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "github.com/ark-network/ark/internal/interface/grpc/permissions" + "github.com/ark-network/tools/macaroons" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) -func unaryAuthenticator(user, pass string) grpc.UnaryServerInterceptor { - adminToken := fmt.Sprintf("%s:%s", user, pass) - adminTokenEncoded := base64.StdEncoding.EncodeToString([]byte(adminToken)) - +func unaryMacaroonAuthHandler( + macaroonSvc *macaroons.Service, +) grpc.UnaryServerInterceptor { return func( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (interface{}, error) { - // whitelist the ArkService - if strings.Contains(info.FullMethod, arkv1.ArkService_ServiceDesc.ServiceName) { - return handler(ctx, req) - } - - token, err := grpc_auth.AuthFromMD(ctx, "basic") - if err != nil { - return nil, status.Errorf(codes.Unauthenticated, "no basic header found: %v", err) - } - - if token != adminTokenEncoded { - return nil, status.Errorf(codes.Unauthenticated, "invalid auth credentials: %v", err) + if err := checkMacaroon(ctx, info.FullMethod, macaroonSvc); err != nil { + return nil, err } return handler(ctx, req) } } + +func streamMacaroonAuthHandler( + macaroonSvc *macaroons.Service, +) grpc.StreamServerInterceptor { + return func( + srv interface{}, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, + ) error { + if err := checkMacaroon(ss.Context(), info.FullMethod, macaroonSvc); err != nil { + return err + } + + return handler(srv, ss) + } +} + +func checkMacaroon( + ctx context.Context, fullMethod string, svc *macaroons.Service, +) error { + if svc == nil { + return nil + } + // Check whether the method is whitelisted, if so we'll allow it regardless + // of macaroons. + if _, ok := permissions.Whitelist()[fullMethod]; ok { + return nil + } + + uriPermissions, ok := permissions.AllPermissionsByMethod()[fullMethod] + if !ok { + return fmt.Errorf("%s: unknown permissions required for method", fullMethod) + } + + // Find out if there is an external validator registered for + // this method. Fall back to the internal one if there isn't. + validator, ok := svc.ExternalValidators[fullMethod] + if !ok { + validator = svc + } + // Now that we know what validator to use, let it do its work. + return validator.ValidateMacaroon(ctx, uriPermissions, fullMethod) +} diff --git a/server/internal/interface/grpc/interceptors/interceptor.go b/server/internal/interface/grpc/interceptors/interceptor.go index 8c5b413..530c351 100644 --- a/server/internal/interface/grpc/interceptors/interceptor.go +++ b/server/internal/interface/grpc/interceptors/interceptor.go @@ -1,19 +1,23 @@ package interceptors import ( + "github.com/ark-network/tools/macaroons" middleware "github.com/grpc-ecosystem/go-grpc-middleware" "google.golang.org/grpc" ) // UnaryInterceptor returns the unary interceptor -func UnaryInterceptor(user, pass string) grpc.ServerOption { +func UnaryInterceptor(svc *macaroons.Service) grpc.ServerOption { return grpc.UnaryInterceptor(middleware.ChainUnaryServer( - unaryAuthenticator(user, pass), unaryLogger, + unaryMacaroonAuthHandler(svc), )) } // StreamInterceptor returns the stream interceptor with a logrus log -func StreamInterceptor() grpc.ServerOption { - return grpc.StreamInterceptor(middleware.ChainStreamServer(streamLogger)) +func StreamInterceptor(svc *macaroons.Service) grpc.ServerOption { + return grpc.StreamInterceptor(middleware.ChainStreamServer( + streamLogger, + streamMacaroonAuthHandler(svc), + )) } diff --git a/server/internal/interface/grpc/macaroons.go b/server/internal/interface/grpc/macaroons.go new file mode 100644 index 0000000..15008d7 --- /dev/null +++ b/server/internal/interface/grpc/macaroons.go @@ -0,0 +1,82 @@ +package grpcservice + +import ( + "context" + "io/fs" + "os" + "path/filepath" + + "github.com/ark-network/ark/internal/interface/grpc/permissions" + "github.com/ark-network/tools/macaroons" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +var ( + adminMacaroonFile = "admin.macaroon" + walletMacaroonFile = "wallet.macaroon" + managerMacaroonFile = "manager.macaroon" + roMacaroonFile = "readonly.macaroon" + + macFiles = map[string][]bakery.Op{ + adminMacaroonFile: permissions.AdminPermissions(), + walletMacaroonFile: permissions.WalletPermissions(), + managerMacaroonFile: permissions.ManagerPermissions(), + roMacaroonFile: permissions.ReadOnlyPermissions(), + } +) + +// genMacaroons generates four macaroon files; one admin-level, one for +// updating the strategy of a market, one for updating its price and one +// read-only. Admin and read-only can also be used to generate more granular +// macaroons. +func genMacaroons( + ctx context.Context, svc *macaroons.Service, datadir string, +) (bool, error) { + adminMacFile := filepath.Join(datadir, adminMacaroonFile) + walletMacFile := filepath.Join(datadir, walletMacaroonFile) + managerMacFile := filepath.Join(datadir, managerMacaroonFile) + roMacFile := filepath.Join(datadir, roMacaroonFile) + if pathExists(adminMacFile) || pathExists(walletMacFile) || + pathExists(managerMacFile) || pathExists(roMacFile) { + return false, nil + } + + // Let's create the datadir if it doesn't exist. + if err := makeDirectoryIfNotExists(datadir); err != nil { + return false, err + } + + for macFilename, macPermissions := range macFiles { + mktMacBytes, err := svc.BakeMacaroon(ctx, macPermissions) + if err != nil { + return false, err + } + macFile := filepath.Join(datadir, macFilename) + perms := fs.FileMode(0644) + if macFilename == adminMacaroonFile { + perms = 0600 + } + if err := os.WriteFile(macFile, mktMacBytes, perms); err != nil { + os.Remove(macFile) + return false, err + } + } + + return true, nil +} + +func makeDirectoryIfNotExists(path string) error { + if pathExists(path) { + return nil + } + return os.MkdirAll(path, os.ModeDir|0755) +} + +func pathExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} diff --git a/server/internal/interface/grpc/permissions/.gitkeep b/server/internal/interface/grpc/permissions/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/server/internal/interface/grpc/permissions/permissions.go b/server/internal/interface/grpc/permissions/permissions.go new file mode 100644 index 0000000..440ddae --- /dev/null +++ b/server/internal/interface/grpc/permissions/permissions.go @@ -0,0 +1,188 @@ +package permissions + +import ( + "fmt" + + "gopkg.in/macaroon-bakery.v2/bakery" + + arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" + grpchealth "google.golang.org/grpc/health/grpc_health_v1" +) + +const ( + EntityWallet = "wallet" + EntityAdmin = "admin" + EntityManager = "manager" + EntityArk = "ark" + EntityHealth = "health" +) + +// ReadOnlyPermissions returns the permissions of the macaroon readonly.macaroon. +// This grants access to the read action for all entities. +func ReadOnlyPermissions() []bakery.Op { + return []bakery.Op{ + { + Entity: EntityWallet, + Action: "read", + }, + { + Entity: EntityManager, + Action: "read", + }, + } +} + +// WalletPermissions returns the permissions of the macaroon wallet.macaroon. +// This grants access to the all actions for the wallet entity. +func WalletPermissions() []bakery.Op { + return []bakery.Op{ + { + Entity: EntityWallet, + Action: "read", + }, + { + Entity: EntityWallet, + Action: "write", + }, + } +} + +// ManagerPermissions returns the permissions of the macaroon manager.macaroon. +// This grants access to the all actions for the manager entity. +func ManagerPermissions() []bakery.Op { + return []bakery.Op{ + { + Entity: EntityManager, + Action: "read", + }, + { + Entity: EntityManager, + Action: "write", + }, + } +} + +// AdminPermissions returns the permissions of the macaroon admin.macaroon. +// This grants access to the all actions for all entities. +func AdminPermissions() []bakery.Op { + return []bakery.Op{ + { + Entity: EntityManager, + Action: "read", + }, + { + Entity: EntityManager, + Action: "write", + }, + { + Entity: EntityWallet, + Action: "read", + }, + { + Entity: EntityWallet, + Action: "write", + }, + } +} + +// Whitelist returns the list of all whitelisted methods with the relative +// entity and action. +func Whitelist() map[string][]bakery.Op { + return map[string][]bakery.Op{ + fmt.Sprintf("/%s/GenSeed", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "read", + }}, + fmt.Sprintf("/%s/Create", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "write", + }}, + fmt.Sprintf("/%s/Restore", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "write", + }}, + fmt.Sprintf("/%s/Unlock", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "write", + }}, + fmt.Sprintf("/%s/GetStatus", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "read", + }}, + fmt.Sprintf("/%s/RegisterPayment", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "write", + }}, + fmt.Sprintf("/%s/ClaimPayment", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "write", + }}, + fmt.Sprintf("/%s/FinalizePayment", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "write", + }}, + fmt.Sprintf("/%s/GetRound", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/GetRoundById", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/GetEventStream", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/Ping", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/ListVtxos", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/GetInfo", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "read", + }}, + fmt.Sprintf("/%s/Onboard", arkv1.ArkService_ServiceDesc.ServiceName): {{ + Entity: EntityArk, + Action: "write", + }}, + fmt.Sprintf("/%s/Check", grpchealth.Health_ServiceDesc.ServiceName): {{ + Entity: EntityHealth, + Action: "read", + }}, + } +} + +// AllPermissionsByMethod returns a mapping of the RPC server calls to the +// permissions they require. +func AllPermissionsByMethod() map[string][]bakery.Op { + return map[string][]bakery.Op{ + fmt.Sprintf("/%s/Lock", arkv1.WalletService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "write", + }}, + fmt.Sprintf("/%s/DeriveAddress", arkv1.WalletService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "write", + }}, + fmt.Sprintf("/%s/GetBalance", arkv1.WalletService_ServiceDesc.ServiceName): {{ + Entity: EntityWallet, + Action: "read", + }}, + fmt.Sprintf("/%s/GetScheduledSweep", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "read", + }}, + fmt.Sprintf("/%s/GetRoundDetails", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "read", + }}, + fmt.Sprintf("/%s/GetRounds", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "read", + }}, + } +} diff --git a/server/internal/interface/grpc/permissions/permissions_test.go b/server/internal/interface/grpc/permissions/permissions_test.go new file mode 100644 index 0000000..9be8313 --- /dev/null +++ b/server/internal/interface/grpc/permissions/permissions_test.go @@ -0,0 +1,48 @@ +package permissions_test + +import ( + "fmt" + "testing" + + grpchealth "google.golang.org/grpc/health/grpc_health_v1" + + "github.com/ark-network/ark/internal/interface/grpc/permissions" + "github.com/stretchr/testify/require" + + arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" +) + +func TestRestrictedMethods(t *testing.T) { + allMethods := make([]string, 0) + for _, m := range arkv1.AdminService_ServiceDesc.Methods { + allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.AdminService_ServiceDesc.ServiceName, m.MethodName)) + } + for _, m := range arkv1.WalletService_ServiceDesc.Methods { + allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.WalletService_ServiceDesc.ServiceName, m.MethodName)) + } + + allPermissions := permissions.AllPermissionsByMethod() + for _, method := range allMethods { + _, ok := allPermissions[method] + require.True(t, ok, fmt.Sprintf("missing permission for %s", method)) + } +} + +func TestWhitelistedMethods(t *testing.T) { + allMethods := make([]string, 0) + for _, m := range arkv1.ArkService_ServiceDesc.Methods { + allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.ArkService_ServiceDesc.ServiceName, m.MethodName)) + } + + for _, v := range arkv1.WalletInitializerService_ServiceDesc.Methods { + allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.WalletInitializerService_ServiceDesc.ServiceName, v.MethodName)) + } + + allMethods = append(allMethods, fmt.Sprintf("/%s/%s", grpchealth.Health_ServiceDesc.ServiceName, "Check")) + + whitelist := permissions.Whitelist() + for _, m := range allMethods { + _, ok := whitelist[m] + require.True(t, ok, fmt.Sprintf("missing %s in whitelist", m)) + } +} diff --git a/server/internal/interface/grpc/service.go b/server/internal/interface/grpc/service.go index c14c8b2..8a855aa 100644 --- a/server/internal/interface/grpc/service.go +++ b/server/internal/interface/grpc/service.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net/http" + "path/filepath" "strings" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" @@ -13,6 +14,8 @@ import ( interfaces "github.com/ark-network/ark/internal/interface" "github.com/ark-network/ark/internal/interface/grpc/handlers" "github.com/ark-network/ark/internal/interface/grpc/interceptors" + "github.com/ark-network/tools/kvdb" + "github.com/ark-network/tools/macaroons" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" log "github.com/sirupsen/logrus" "golang.org/x/net/http2" @@ -24,11 +27,22 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) +const ( + macaroonsLocation = "ark" + macaroonsDbFile = "macaroons.db" + macaroonsFolder = "macaroons" + + tlsKeyFile = "key.pem" + tlsCertFile = "cert.pem" + tlsFolder = "tls" +) + type service struct { - config Config - appConfig *appconfig.Config - server *http.Server - grpcServer *grpc.Server + config Config + appConfig *appconfig.Config + server *http.Server + grpcServer *grpc.Server + macaroonSvc *macaroons.Service } func NewService( @@ -41,7 +55,41 @@ func NewService( return nil, fmt.Errorf("invalid app config: %s", err) } - return &service{svcConfig, appConfig, nil, nil}, nil + var macaroonSvc *macaroons.Service + if !svcConfig.NoMacaroons { + macaroonDB, err := kvdb.Create( + kvdb.BoltBackendName, + filepath.Join(svcConfig.Datadir, macaroonsDbFile), + true, + kvdb.DefaultDBTimeout, + ) + if err != nil { + return nil, err + } + + keyStore, err := macaroons.NewRootKeyStorage(macaroonDB) + if err != nil { + return nil, err + } + svc, err := macaroons.NewService( + keyStore, macaroonsLocation, false, macaroons.IPLockChecker, + ) + if err != nil { + return nil, err + } + macaroonSvc = svc + } + + if !svcConfig.insecure() { + if err := generateOperatorTLSKeyCert( + svcConfig.tlsDatadir(), svcConfig.TLSExtraIPs, svcConfig.TLSExtraDomains, + ); err != nil { + return nil, err + } + log.Debugf("generated TLS key pair at path: %s", svcConfig.tlsDatadir()) + } + + return &service{svcConfig, appConfig, nil, nil, macaroonSvc}, nil } func (s *service) Start() error { @@ -55,7 +103,12 @@ func (s *service) Stop() { } func (s *service) start(withAppSvc bool) error { - if err := s.newServer(withAppSvc); err != nil { + tlsConfig, err := s.config.tlsConfig() + if err != nil { + return err + } + + if err := s.newServer(tlsConfig, withAppSvc); err != nil { return err } @@ -92,17 +145,14 @@ func (s *service) stop(withAppSvc bool) { } } -func (s *service) newServer(withAppSvc bool) error { +func (s *service) newServer(tlsConfig *tls.Config, withAppSvc bool) error { grpcConfig := []grpc.ServerOption{ - interceptors.UnaryInterceptor(s.config.AuthUser, s.config.AuthPass), - interceptors.StreamInterceptor(), - } - if !s.config.NoTLS { - return fmt.Errorf("tls termination not supported yet") + interceptors.UnaryInterceptor(s.macaroonSvc), + interceptors.StreamInterceptor(s.macaroonSvc), } creds := insecure.NewCredentials() if !s.config.insecure() { - creds = credentials.NewTLS(s.config.tlsConfig()) + creds = credentials.NewTLS(tlsConfig) } grpcConfig = append(grpcConfig, grpc.Creds(creds)) @@ -123,9 +173,14 @@ func (s *service) newServer(withAppSvc bool) error { adminHandler := handlers.NewAdminHandler(s.appConfig.AdminService(), appSvc) arkv1.RegisterAdminServiceServer(grpcServer, adminHandler) - walletHandler := handlers.NewWalletHandler(s.appConfig.WalletService(), s.onUnlock) + walletHandler := handlers.NewWalletHandler(s.appConfig.WalletService()) arkv1.RegisterWalletServiceServer(grpcServer, walletHandler) + walletInitHandler := handlers.NewWalletInitializerHandler( + s.appConfig.WalletService(), s.onInit, s.onUnlock, + ) + arkv1.RegisterWalletInitializerServiceServer(grpcServer, walletInitHandler) + healthHandler := handlers.NewHealthHandler() grpchealth.RegisterHealthServer(grpcServer, healthHandler) @@ -143,8 +198,18 @@ func (s *service) newServer(withAppSvc bool) error { if err != nil { return err } + + customMatcher := func(key string) (string, bool) { + switch key { + case "X-Macaroon": + return "macaroon", true + default: + return key, false + } + } // Reverse proxy grpc-gateway. gwmux := runtime.NewServeMux( + runtime.WithIncomingHeaderMatcher(customMatcher), runtime.WithHealthzEndpoint(grpchealth.NewHealthClient(conn)), runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ @@ -167,6 +232,11 @@ func (s *service) newServer(withAppSvc bool) error { ); err != nil { return err } + if err := arkv1.RegisterWalletInitializerServiceHandler( + ctx, gwmux, conn, + ); err != nil { + return err + } if withAppSvc { if err := arkv1.RegisterArkServiceHandler( ctx, gwmux, conn, @@ -188,13 +258,13 @@ func (s *service) newServer(withAppSvc bool) error { s.server = &http.Server{ Addr: s.config.address(), Handler: httpServerHandler, - TLSConfig: s.config.tlsConfig(), + TLSConfig: tlsConfig, } return nil } -func (s *service) onUnlock() { +func (s *service) onUnlock(password string) { withoutAppSvc := false s.stop(withoutAppSvc) @@ -202,6 +272,46 @@ func (s *service) onUnlock() { if err := s.start(withAppSvc); err != nil { panic(err) } + + if s.config.NoMacaroons { + return + } + + pwd := []byte(password) + datadir := s.config.macaroonsDatadir() + if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil { + if err != macaroons.ErrAlreadyUnlocked { + log.WithError(err).Warn("failed to unlock macaroon store") + } + } + + done, err := genMacaroons( + context.Background(), s.macaroonSvc, datadir, + ) + if err != nil { + log.WithError(err).Warn("failed to create macaroons") + } + if done { + log.Debugf("created and stored macaroons at path %s", datadir) + } +} + +func (s *service) onInit(password string) { + if s.config.NoMacaroons { + return + } + + pwd := []byte(password) + datadir := s.config.macaroonsDatadir() + if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil { + log.WithError(err).Warn("failed to initialize macaroon store") + } + if _, err := genMacaroons( + context.Background(), s.macaroonSvc, datadir, + ); err != nil { + log.WithError(err).Warn("failed to create macaroons") + } + log.Debugf("generated macaroons at path %s", datadir) } func router( diff --git a/server/internal/interface/grpc/tls.go b/server/internal/interface/grpc/tls.go new file mode 100644 index 0000000..69b1a5e --- /dev/null +++ b/server/internal/interface/grpc/tls.go @@ -0,0 +1,205 @@ +package grpcservice + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "time" +) + +func generateOperatorTLSKeyCert( + datadir string, extraIPs, extraDomains []string, +) error { + if err := makeDirectoryIfNotExists(datadir); err != nil { + return err + } + keyPath := filepath.Join(datadir, tlsKeyFile) + certPath := filepath.Join(datadir, tlsCertFile) + + // if key and cert files already exist nothing to do here. + if pathExists(keyPath) && pathExists(certPath) { + return nil + } + + organization := "ark" + now := time.Now() + validUntil := now.AddDate(1, 0, 0) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + + // Generate a serial number that's below the serialNumberLimit. + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return fmt.Errorf("failed to generate serial number: %s", err) + } + + // Collect the host's IP addresses, including loopback, in a slice. + ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + + if len(extraIPs) > 0 { + for _, ip := range extraIPs { + ipAddresses = append(ipAddresses, net.ParseIP(ip)) + } + } + + // addIP appends an IP address only if it isn't already in the slice. + addIP := func(ipAddr net.IP) { + for _, ip := range ipAddresses { + if net.IP.Equal(ip, ipAddr) { + return + } + } + ipAddresses = append(ipAddresses, ipAddr) + } + + // Add all the interface IPs that aren't already in the slice. + addrs, err := net.InterfaceAddrs() + if err != nil { + return err + } + for _, a := range addrs { + ipAddr, _, err := net.ParseCIDR(a.String()) + if err == nil { + addIP(ipAddr) + } + } + + host, err := os.Hostname() + if err != nil { + return err + } + + dnsNames := []string{host} + if host != "localhost" { + dnsNames = append(dnsNames, "localhost") + } + + if len(extraDomains) > 0 { + dnsNames = append(dnsNames, extraDomains...) + } + + dnsNames = append(dnsNames, "unix", "unixpacket") + + priv, err := createOrLoadTLSKey(keyPath) + if err != nil { + return err + } + + keybytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return err + } + + // construct certificate template + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: host, + }, + NotBefore: now.Add(-time.Hour * 24), + NotAfter: validUntil, + + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + + DNSNames: dnsNames, + IPAddresses: ipAddresses, + } + + derBytes, err := x509.CreateCertificate( + rand.Reader, &template, &template, &priv.PublicKey, priv, + ) + if err != nil { + return fmt.Errorf("failed to create certificate: %v", err) + } + + certBuf := &bytes.Buffer{} + if err := pem.Encode( + certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}, + ); err != nil { + return fmt.Errorf("failed to encode certificate: %v", err) + } + + keyBuf := &bytes.Buffer{} + if err := pem.Encode( + keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}, + ); err != nil { + return fmt.Errorf("failed to encode private key: %v", err) + } + + if err := os.WriteFile(certPath, certBuf.Bytes(), 0644); err != nil { + return err + } + if err := os.WriteFile(keyPath, keyBuf.Bytes(), 0600); err != nil { + os.Remove(certPath) + return err + } + + return nil +} + +func createOrLoadTLSKey(keyPath string) (*ecdsa.PrivateKey, error) { + if !pathExists(keyPath) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } + + b, err := os.ReadFile(keyPath) + if err != nil { + return nil, err + } + + key, err := privateKeyFromPEM(b) + if err != nil { + return nil, err + } + return key.(*ecdsa.PrivateKey), nil +} + +func privateKeyFromPEM(pemBlock []byte) (crypto.PrivateKey, error) { + var derBlock *pem.Block + for { + derBlock, pemBlock = pem.Decode(pemBlock) + if derBlock == nil { + return nil, fmt.Errorf("tls: failed to find any PEM data in key input") + } + if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { + break + } + } + return parsePrivateKey(derBlock.Bytes) +} + +func parsePrivateKey(der []byte) (crypto.PrivateKey, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + return key, nil + default: + return nil, fmt.Errorf("tls: found unknown private key type in PKCS#8 wrapping") + } + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, fmt.Errorf("tls: failed to parse private key") +} diff --git a/server/pkg/.gitkeep b/server/pkg/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/server/pkg/kvdb/LICENSE b/server/pkg/kvdb/LICENSE new file mode 100644 index 0000000..cfab3da --- /dev/null +++ b/server/pkg/kvdb/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/server/pkg/kvdb/backend.go b/server/pkg/kvdb/backend.go new file mode 100644 index 0000000..603ae4e --- /dev/null +++ b/server/pkg/kvdb/backend.go @@ -0,0 +1,274 @@ +//go:build !js +// +build !js + +package kvdb + +import ( + "context" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "time" + + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Import to register backend. + log "github.com/sirupsen/logrus" +) + +const ( + // DefaultTempDBFileName is the default name of the temporary bolt DB + // file that we'll use to atomically compact the primary DB file on + // startup. + DefaultTempDBFileName = "temp-dont-use.db" + + // LastCompactionFileNameSuffix is the suffix we append to the file name + // of a database file to record the timestamp when the last compaction + // occurred. + LastCompactionFileNameSuffix = ".last-compacted" +) + +var ( + byteOrder = binary.BigEndian +) + +// fileExists returns true if the file exists, and false otherwise. +func fileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + + return true +} + +// BoltBackendConfig is a struct that holds settings specific to the bolt +// database backend. +type BoltBackendConfig struct { + // DBPath is the directory path in which the database file should be + // stored. + DBPath string + + // DBFileName is the name of the database file. + DBFileName string + + // NoFreelistSync, if true, prevents the database from syncing its + // freelist to disk, resulting in improved performance at the expense of + // increased startup time. + NoFreelistSync bool + + // AutoCompact specifies if a Bolt based database backend should be + // automatically compacted on startup (if the minimum age of the + // database file is reached). This will require additional disk space + // for the compacted copy of the database but will result in an overall + // lower database size after the compaction. + AutoCompact bool + + // AutoCompactMinAge specifies the minimum time that must have passed + // since a bolt database file was last compacted for the compaction to + // be considered again. + AutoCompactMinAge time.Duration + + // DBTimeout specifies the timeout value to use when opening the wallet + // database. + DBTimeout time.Duration +} + +// GetBoltBackend opens (or creates if doesn't exits) a bbolt backed database +// and returns a kvdb.Backend wrapping it. +func GetBoltBackend(cfg *BoltBackendConfig) (Backend, error) { + dbFilePath := filepath.Join(cfg.DBPath, cfg.DBFileName) + + // Is this a new database? + if !fileExists(dbFilePath) { + if !fileExists(cfg.DBPath) { + if err := os.MkdirAll(cfg.DBPath, 0700); err != nil { + return nil, err + } + } + + return Create( + BoltBackendName, dbFilePath, + cfg.NoFreelistSync, cfg.DBTimeout, + ) + } + + // This is an existing database. We might want to compact it on startup + // to free up some space. + if cfg.AutoCompact { + if err := compactAndSwap(cfg); err != nil { + return nil, err + } + } + + return Open( + BoltBackendName, dbFilePath, + cfg.NoFreelistSync, cfg.DBTimeout, + ) +} + +// compactAndSwap will attempt to write a new temporary DB file to disk with +// the compacted database content, then atomically swap (via rename) the old +// file for the new file by updating the name of the new file to the old. +func compactAndSwap(cfg *BoltBackendConfig) error { + sourceName := cfg.DBFileName + + // If the main DB file isn't set, then we can't proceed. + if sourceName == "" { + return fmt.Errorf("cannot compact DB with empty name") + } + sourceFilePath := filepath.Join(cfg.DBPath, sourceName) + tempDestFilePath := filepath.Join(cfg.DBPath, DefaultTempDBFileName) + + // Let's find out how long ago the last compaction of the source file + // occurred and possibly skip compacting it again now. + lastCompactionDate, err := lastCompactionDate(sourceFilePath) + if err != nil { + return fmt.Errorf("cannot determine last compaction date of "+ + "source DB file: %v", err) + } + compactAge := time.Since(lastCompactionDate) + if cfg.AutoCompactMinAge != 0 && compactAge <= cfg.AutoCompactMinAge { + log.Infof("Not compacting database file at %v, it was last "+ + "compacted at %v (%v ago), min age is set to %v", + sourceFilePath, lastCompactionDate, + compactAge.Truncate(time.Second), cfg.AutoCompactMinAge) + return nil + } + + log.Infof("Compacting database file at %v", sourceFilePath) + + // If the old temporary DB file still exists, then we'll delete it + // before proceeding. + if _, err := os.Stat(tempDestFilePath); err == nil { + log.Infof("Found old temp DB @ %v, removing before swap", + tempDestFilePath) + + err = os.Remove(tempDestFilePath) + if err != nil { + return fmt.Errorf("unable to remove old temp DB file: "+ + "%v", err) + } + } + + // Now that we know the staging area is clear, we'll create the new + // temporary DB file and close it before we write the new DB to it. + tempFile, err := os.Create(tempDestFilePath) + if err != nil { + return fmt.Errorf("unable to create temp DB file: %w", err) + } + if err := tempFile.Close(); err != nil { + return fmt.Errorf("unable to close file: %w", err) + } + + // With the file created, we'll start the compaction and remove the + // temporary file all together once this method exits. + defer func() { + // This will only succeed if the rename below fails. If the + // compaction is successful, the file won't exist on exit + // anymore so no need to log an error here. + _ = os.Remove(tempDestFilePath) + }() + c := &compacter{ + srcPath: sourceFilePath, + dstPath: tempDestFilePath, + dbTimeout: cfg.DBTimeout, + } + initialSize, newSize, err := c.execute() + if err != nil { + return fmt.Errorf("error during compact: %w", err) + } + + log.Infof("DB compaction of %v successful, %d -> %d bytes (gain=%.2fx)", + sourceFilePath, initialSize, newSize, + float64(initialSize)/float64(newSize)) + + // We try to store the current timestamp in a file with the suffix + // .last-compacted so we can figure out how long ago the last compaction + // was. But since this shouldn't fail the compaction process itself, we + // only log the error. Worst case if this file cannot be written is that + // we compact on every startup. + err = updateLastCompactionDate(sourceFilePath) + if err != nil { + log.Warnf("Could not update last compaction timestamp in "+ + "%s%s: %v", sourceFilePath, + LastCompactionFileNameSuffix, err) + } + + log.Infof("Swapping old DB file from %v to %v", tempDestFilePath, + sourceFilePath) + + // Finally, we'll attempt to atomically rename the temporary file to + // the main back up file. If this succeeds, then we'll only have a + // single file on disk once this method exits. + return os.Rename(tempDestFilePath, sourceFilePath) +} + +// lastCompactionDate returns the date the given database file was last +// compacted or a zero time.Time if no compaction was recorded before. The +// compaction date is read from a file in the same directory and with the same +// name as the DB file, but with the suffix ".last-compacted". +func lastCompactionDate(dbFile string) (time.Time, error) { + zeroTime := time.Unix(0, 0) + + tsFile := fmt.Sprintf("%s%s", dbFile, LastCompactionFileNameSuffix) + if !fileExists(tsFile) { + return zeroTime, nil + } + + tsBytes, err := os.ReadFile(tsFile) + if err != nil { + return zeroTime, err + } + + tsNano := byteOrder.Uint64(tsBytes) + return time.Unix(0, int64(tsNano)), nil +} + +// updateLastCompactionDate stores the current time as a timestamp in a file +// in the same directory and with the same name as the DB file, but with the +// suffix ".last-compacted". +func updateLastCompactionDate(dbFile string) error { + var tsBytes [8]byte + byteOrder.PutUint64(tsBytes[:], uint64(time.Now().UnixNano())) + + tsFile := fmt.Sprintf("%s%s", dbFile, LastCompactionFileNameSuffix) + return os.WriteFile(tsFile, tsBytes[:], 0600) +} + +// GetTestBackend opens (or creates if doesn't exist) a bbolt or etcd +// backed database (for testing), and returns a kvdb.Backend and a cleanup +// func. Whether to create/open bbolt or embedded etcd database is based +// on the TestBackend constant which is conditionally compiled with build tag. +// The passed path is used to hold all db files, while the name is only used +// for bbolt. +func GetTestBackend(path, name string) (Backend, func(), error) { + empty := func() {} + + // Note that for tests, we expect only one db backend build flag + // (or none) to be set at a time and thus one of the following switch + // cases should ever be true + switch { + case EtcdBackend: + etcdConfig, cancel, err := StartEtcdTestBackend(path, 0, 0, "") + if err != nil { + return nil, empty, err + } + backend, err := Open( + EtcdBackendName, context.TODO(), etcdConfig, + ) + return backend, cancel, err + + default: + db, err := GetBoltBackend(&BoltBackendConfig{ + DBPath: path, + DBFileName: name, + NoFreelistSync: true, + DBTimeout: DefaultDBTimeout, + }) + if err != nil { + return nil, nil, err + } + return db, empty, nil + } +} diff --git a/server/pkg/kvdb/backend_js.go b/server/pkg/kvdb/backend_js.go new file mode 100644 index 0000000..f177440 --- /dev/null +++ b/server/pkg/kvdb/backend_js.go @@ -0,0 +1,48 @@ +package kvdb + +import ( + "fmt" + "time" +) + +// BoltBackendConfig is a struct that holds settings specific to the bolt +// database backend. +type BoltBackendConfig struct { + // DBPath is the directory path in which the database file should be + // stored. + DBPath string + + // DBFileName is the name of the database file. + DBFileName string + + // NoFreelistSync, if true, prevents the database from syncing its + // freelist to disk, resulting in improved performance at the expense of + // increased startup time. + NoFreelistSync bool + + // AutoCompact specifies if a Bolt based database backend should be + // automatically compacted on startup (if the minimum age of the + // database file is reached). This will require additional disk space + // for the compacted copy of the database but will result in an overall + // lower database size after the compaction. + AutoCompact bool + + // AutoCompactMinAge specifies the minimum time that must have passed + // since a bolt database file was last compacted for the compaction to + // be considered again. + AutoCompactMinAge time.Duration + + // DBTimeout specifies the timeout value to use when opening the wallet + // database. + DBTimeout time.Duration +} + +// GetBoltBackend opens (or creates if doesn't exits) a bbolt backed database +// and returns a kvdb.Backend wrapping it. +func GetBoltBackend(cfg *BoltBackendConfig) (Backend, error) { + return nil, fmt.Errorf("bolt backend not supported in WebAssembly") +} + +func GetTestBackend(path, name string) (Backend, func(), error) { + return nil, nil, fmt.Errorf("bolt backend not supported in WebAssembly") +} diff --git a/server/pkg/kvdb/bolt_compact.go b/server/pkg/kvdb/bolt_compact.go new file mode 100644 index 0000000..48d998c --- /dev/null +++ b/server/pkg/kvdb/bolt_compact.go @@ -0,0 +1,283 @@ +// The code in this file is an adapted version of the bbolt compact command +// implemented in this file: +// https://github.com/etcd-io/bbolt/blob/master/cmd/bbolt/main.go + +//go:build !js +// +build !js + +package kvdb + +import ( + "encoding/hex" + "fmt" + "os" + "path" + "time" + + "github.com/lightningnetwork/lnd/healthcheck" + log "github.com/sirupsen/logrus" + "go.etcd.io/bbolt" +) + +const ( + // defaultResultFileSizeMultiplier is the default multiplier we apply to + // the current database size to calculate how big it could possibly get + // after compacting, in case the database is already at its optimal size + // and compaction causes it to grow. This should normally not be the + // case but we really want to avoid not having enough disk space for the + // compaction, so we apply a safety margin of 10%. + defaultResultFileSizeMultiplier = float64(1.1) + + // defaultTxMaxSize is the default maximum number of operations that + // are allowed to be executed in a single transaction. + defaultTxMaxSize = 65536 + + // bucketFillSize is the fill size setting that is used for each new + // bucket that is created in the compacted database. This setting is not + // persisted and is therefore only effective for the compaction itself. + // Because during the compaction we only append data a fill percent of + // 100% is optimal for performance. + bucketFillSize = 1.0 +) + +type compacter struct { + srcPath string + dstPath string + txMaxSize int64 + + // dbTimeout specifies the timeout value used when opening the db. + dbTimeout time.Duration +} + +// execute opens the source and destination databases and then compacts the +// source into destination and returns the size of both files as a result. +func (cmd *compacter) execute() (int64, int64, error) { + if cmd.txMaxSize == 0 { + cmd.txMaxSize = defaultTxMaxSize + } + + // Ensure source file exists. + fi, err := os.Stat(cmd.srcPath) + if err != nil { + return 0, 0, fmt.Errorf("error determining source database "+ + "size: %v", err) + } + initialSize := fi.Size() + marginSize := float64(initialSize) * defaultResultFileSizeMultiplier + + // Before opening any of the databases, let's first make sure we have + // enough free space on the destination file system to create a full + // copy of the source DB (worst-case scenario if the compaction doesn't + // actually shrink the file size). + destFolder := path.Dir(cmd.dstPath) + freeSpace, err := healthcheck.AvailableDiskSpace(destFolder) + if err != nil { + return 0, 0, fmt.Errorf("error determining free disk space on "+ + "%s: %v", destFolder, err) + } + log.Debugf("Free disk space on compaction destination file system: "+ + "%d bytes", freeSpace) + if freeSpace < uint64(marginSize) { + return 0, 0, fmt.Errorf("could not start compaction, "+ + "destination folder %s only has %d bytes of free disk "+ + "space available while we need at least %d for worst-"+ + "case compaction", destFolder, freeSpace, uint64(marginSize)) + } + + // Open source database. We open it in read only mode to avoid (and fix) + // possible freelist sync problems. + src, err := bbolt.Open(cmd.srcPath, 0444, &bbolt.Options{ + ReadOnly: true, + Timeout: cmd.dbTimeout, + }) + if err != nil { + return 0, 0, fmt.Errorf("error opening source database: %w", + err) + } + defer func() { + if err := src.Close(); err != nil { + log.Errorf("Compact error: closing source DB: %v", err) + } + }() + + // Open destination database. + dst, err := bbolt.Open(cmd.dstPath, fi.Mode(), &bbolt.Options{ + Timeout: cmd.dbTimeout, + }) + if err != nil { + return 0, 0, fmt.Errorf("error opening destination database: "+ + "%w", err) + } + defer func() { + if err := dst.Close(); err != nil { + log.Errorf("Compact error: closing dest DB: %v", err) + } + }() + + // Run compaction. + if err := cmd.compact(dst, src); err != nil { + return 0, 0, fmt.Errorf("error running compaction: %w", err) + } + + // Report stats on new size. + fi, err = os.Stat(cmd.dstPath) + if err != nil { + return 0, 0, fmt.Errorf("error determining destination "+ + "database size: %w", err) + } else if fi.Size() == 0 { + return 0, 0, fmt.Errorf("zero db size") + } + + return initialSize, fi.Size(), nil +} + +// compact tries to create a compacted copy of the source database in a new +// destination database. +func (cmd *compacter) compact(dst, src *bbolt.DB) error { + // Commit regularly, or we'll run out of memory for large datasets if + // using one transaction. + var size int64 + tx, err := dst.Begin(true) + if err != nil { + return err + } + defer func() { + _ = tx.Rollback() + }() + + if err := cmd.walk(src, func(keys [][]byte, k, v []byte, seq uint64) error { + // On each key/value, check if we have exceeded tx size. + sz := int64(len(k) + len(v)) + if size+sz > cmd.txMaxSize && cmd.txMaxSize != 0 { + // Commit previous transaction. + if err := tx.Commit(); err != nil { + return err + } + + // Start new transaction. + tx, err = dst.Begin(true) + if err != nil { + return err + } + size = 0 + } + size += sz + + // Create bucket on the root transaction if this is the first + // level. + nk := len(keys) + if nk == 0 { + bkt, err := tx.CreateBucket(k) + if err != nil { + return err + } + if err := bkt.SetSequence(seq); err != nil { + return err + } + return nil + } + + // Create buckets on subsequent levels, if necessary. + b := tx.Bucket(keys[0]) + if nk > 1 { + for _, k := range keys[1:] { + b = b.Bucket(k) + } + } + + // Fill the entire page for best compaction. + b.FillPercent = bucketFillSize + + // If there is no value then this is a bucket call. + if v == nil { + bkt, err := b.CreateBucket(k) + if err != nil { + return err + } + if err := bkt.SetSequence(seq); err != nil { + return err + } + return nil + } + + // Otherwise treat it as a key/value pair. + return b.Put(k, v) + }); err != nil { + return err + } + + return tx.Commit() +} + +// walkFunc is the type of the function called for keys (buckets and "normal" +// values) discovered by Walk. keys is the list of keys to descend to the bucket +// owning the discovered key/value pair k/v. +type walkFunc func(keys [][]byte, k, v []byte, seq uint64) error + +// walk walks recursively the bolt database db, calling walkFn for each key it +// finds. +func (cmd *compacter) walk(db *bbolt.DB, walkFn walkFunc) error { + return db.View(func(tx *bbolt.Tx) error { + return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { + // This will log the top level buckets only to give the + // user some sense of progress. + log.Debugf("Compacting top level bucket '%s'", + LoggableKeyName(name)) + + return cmd.walkBucket( + b, nil, name, nil, b.Sequence(), walkFn, + ) + }) + }) +} + +// LoggableKeyName returns a printable name of the given key. +func LoggableKeyName(key []byte) string { + strKey := string(key) + if hasSpecialChars(strKey) { + return hex.EncodeToString(key) + } + + return strKey +} + +// hasSpecialChars returns true if any of the characters in the given string +// cannot be printed. +func hasSpecialChars(s string) bool { + for _, b := range s { + if !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z') && + !(b >= '0' && b <= '9') && b != '-' && b != '_' { + + return true + } + } + + return false +} + +// walkBucket recursively walks through a bucket. +func (cmd *compacter) walkBucket(b *bbolt.Bucket, keyPath [][]byte, k, v []byte, + seq uint64, fn walkFunc) error { + + // Execute callback. + if err := fn(keyPath, k, v, seq); err != nil { + return err + } + + // If this is not a bucket then stop. + if v != nil { + return nil + } + + // Iterate over each child key/value. + keyPath = append(keyPath, k) + return b.ForEach(func(k, v []byte) error { + if v == nil { + bkt := b.Bucket(k) + return cmd.walkBucket( + bkt, keyPath, k, nil, bkt.Sequence(), fn, + ) + } + return cmd.walkBucket(b, keyPath, k, v, b.Sequence(), fn) + }) +} diff --git a/server/pkg/kvdb/bolt_fixture.go b/server/pkg/kvdb/bolt_fixture.go new file mode 100644 index 0000000..0ac54b5 --- /dev/null +++ b/server/pkg/kvdb/bolt_fixture.go @@ -0,0 +1,35 @@ +package kvdb + +import ( + "path/filepath" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +type boltFixture struct { + t *testing.T + tempDir string +} + +func NewBoltFixture(t *testing.T) *boltFixture { + return &boltFixture{ + t: t, + tempDir: t.TempDir(), + } +} + +func (b *boltFixture) NewBackend() walletdb.DB { + dbPath := filepath.Join(b.tempDir) + + db, err := GetBoltBackend(&BoltBackendConfig{ + DBPath: dbPath, + DBFileName: "test.db", + NoFreelistSync: true, + DBTimeout: DefaultDBTimeout, + }) + require.NoError(b.t, err) + + return db +} diff --git a/server/pkg/kvdb/bolt_test.go b/server/pkg/kvdb/bolt_test.go new file mode 100644 index 0000000..69366c5 --- /dev/null +++ b/server/pkg/kvdb/bolt_test.go @@ -0,0 +1,83 @@ +package kvdb + +import ( + "testing" + + "github.com/btcsuite/btcwallet/walletdb" +) + +func TestBolt(t *testing.T) { + tests := []struct { + name string + test func(*testing.T, walletdb.DB) + }{ + { + name: "read cursor empty interval", + test: testReadCursorEmptyInterval, + }, + { + name: "read cursor non empty interval", + test: testReadCursorNonEmptyInterval, + }, + { + name: "read write cursor", + test: testReadWriteCursor, + }, + { + name: "read write cursor with bucket and value", + test: testReadWriteCursorWithBucketAndValue, + }, + { + name: "bucket creation", + test: testBucketCreation, + }, + { + name: "bucket deletion", + test: testBucketDeletion, + }, + { + name: "bucket for each", + test: testBucketForEach, + }, + { + name: "bucket for each with error", + test: testBucketForEachWithError, + }, + { + name: "bucket sequence", + test: testBucketSequence, + }, + { + name: "key clash", + test: testKeyClash, + }, + { + name: "bucket create delete", + test: testBucketCreateDelete, + }, + { + name: "tx manual commit", + test: testTxManualCommit, + }, + { + name: "tx rollback", + test: testTxRollback, + }, + { + name: "prefetch", + test: testPrefetch, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + f := NewBoltFixture(t) + + test.test(t, f.NewBackend()) + }) + } +} diff --git a/server/pkg/kvdb/config.go b/server/pkg/kvdb/config.go new file mode 100644 index 0000000..821daa4 --- /dev/null +++ b/server/pkg/kvdb/config.go @@ -0,0 +1,45 @@ +package kvdb + +import "time" + +const ( + // BoltBackendName is the name of the backend that should be passed into + // kvdb.Create to initialize a new instance of kvdb.Backend backed by a + // live instance of bbolt. + BoltBackendName = "bdb" + + // EtcdBackendName is the name of the backend that should be passed into + // kvdb.Create to initialize a new instance of kvdb.Backend backed by a + // live instance of etcd. + EtcdBackendName = "etcd" + + // PostgresBackendName is the name of the backend that should be passed + // into kvdb.Create to initialize a new instance of kvdb.Backend backed + // by a live instance of postgres. + PostgresBackendName = "postgres" + + // SqliteBackendName is the name of the backend that should be passed + // into kvdb.Create to initialize a new instance of kvdb.Backend backed + // by a live instance of sqlite. + SqliteBackendName = "sqlite" + + // DefaultBoltAutoCompactMinAge is the default minimum time that must + // have passed since a bolt database file was last compacted for the + // compaction to be considered again. + DefaultBoltAutoCompactMinAge = time.Hour * 24 * 7 + + // DefaultDBTimeout specifies the default timeout value when opening + // the bbolt database. + DefaultDBTimeout = time.Second * 60 +) + +// BoltConfig holds bolt configuration. +type BoltConfig struct { + NoFreelistSync bool `long:"nofreelistsync" description:"Whether the databases used within lnd should sync their freelist to disk. This is set to true by default, meaning we don't sync the free-list resulting in improved memory performance during operation, but with an increase in startup time."` + + AutoCompact bool `long:"auto-compact" description:"Whether the databases used within lnd should automatically be compacted on every startup (and if the database has the configured minimum age). This is disabled by default because it requires additional disk space to be available during the compaction that is freed afterwards. In general compaction leads to smaller database files."` + + AutoCompactMinAge time.Duration `long:"auto-compact-min-age" description:"How long ago the last compaction of a database file must be for it to be considered for auto compaction again. Can be set to 0 to compact on every startup."` + + DBTimeout time.Duration `long:"dbtimeout" description:"Specify the timeout value used when opening the database."` +} diff --git a/server/pkg/kvdb/debug.go b/server/pkg/kvdb/debug.go new file mode 100644 index 0000000..c480954 --- /dev/null +++ b/server/pkg/kvdb/debug.go @@ -0,0 +1,9 @@ +//go:build dev +// +build dev + +package kvdb + +const ( + // Switch on extra debug code. + etcdDebug = true +) diff --git a/server/pkg/kvdb/etcd/bucket.go b/server/pkg/kvdb/etcd/bucket.go new file mode 100644 index 0000000..033f49c --- /dev/null +++ b/server/pkg/kvdb/etcd/bucket.go @@ -0,0 +1,139 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "crypto/sha256" +) + +const ( + bucketIDLength = 32 +) + +var ( + valuePostfix = []byte{0x00} + bucketPostfix = []byte{0xFF} + sequencePrefix = []byte("$seq$") +) + +// makeBucketID returns a deterministic key for the passed byte slice. +// Currently it returns the sha256 hash of the slice. +func makeBucketID(key []byte) [bucketIDLength]byte { + return sha256.Sum256(key) +} + +// isValidBucketID checks if the passed slice is the required length to be a +// valid bucket id. +func isValidBucketID(s []byte) bool { + return len(s) == bucketIDLength +} + +// makeKey concatenates parent, key and postfix into one byte slice. +// The postfix indicates the use of this key (whether bucket or value), while +// parent refers to the parent bucket. +func makeKey(parent, key, postfix []byte) []byte { + keyBuf := make([]byte, len(parent)+len(key)+len(postfix)) + copy(keyBuf, parent) + copy(keyBuf[len(parent):], key) + copy(keyBuf[len(parent)+len(key):], postfix) + + return keyBuf +} + +// makeBucketKey returns a bucket key from the passed parent bucket id and +// the key. +func makeBucketKey(parent []byte, key []byte) []byte { + return makeKey(parent, key, bucketPostfix) +} + +// makeValueKey returns a value key from the passed parent bucket id and +// the key. +func makeValueKey(parent []byte, key []byte) []byte { + return makeKey(parent, key, valuePostfix) +} + +// makeSequenceKey returns a sequence key of the passed parent bucket id. +func makeSequenceKey(parent []byte) []byte { + keyBuf := make([]byte, len(sequencePrefix)+len(parent)) + copy(keyBuf, sequencePrefix) + copy(keyBuf[len(sequencePrefix):], parent) + return keyBuf +} + +// isBucketKey returns true if the passed key is a bucket key, meaning it +// keys a bucket name. +func isBucketKey(key string) bool { + if len(key) < bucketIDLength+1 { + return false + } + + return key[len(key)-1] == bucketPostfix[0] +} + +// getKey chops out the key from the raw key (by removing the bucket id +// prefixing the key and the postfix indicating whether it is a bucket or +// a value key) +func getKey(rawKey string) []byte { + return []byte(rawKey[bucketIDLength : len(rawKey)-1]) +} + +// getKeyVal chops out the key from the raw key (by removing the bucket id +// prefixing the key and the postfix indicating whether it is a bucket or +// a value key) and also returns the appropriate value for the key, which is +// nil in case of buckets (or the set value otherwise). +func getKeyVal(kv *KV) ([]byte, []byte) { + var val []byte + + if !isBucketKey(kv.key) { + val = []byte(kv.val) + } + + return getKey(kv.key), val +} + +// BucketKey is a helper function used in tests to create a bucket key from +// passed bucket list. +func BucketKey(buckets ...string) string { + var bucketKey []byte + + rootID := makeBucketID([]byte(etcdDefaultRootBucketId)) + parent := rootID[:] + + for _, bucketName := range buckets { + bucketKey = makeBucketKey(parent, []byte(bucketName)) + id := makeBucketID(bucketKey) + parent = id[:] + } + + return string(bucketKey) +} + +// BucketVal is a helper function used in tests to create a bucket value (the +// value for a bucket key) from the passed bucket list. +func BucketVal(buckets ...string) string { + id := makeBucketID([]byte(BucketKey(buckets...))) + return string(id[:]) +} + +// ValueKey is a helper function used in tests to create a value key from the +// passed key and bucket list. +func ValueKey(key string, buckets ...string) string { + rootID := makeBucketID([]byte(etcdDefaultRootBucketId)) + bucket := rootID[:] + + for _, bucketName := range buckets { + bucketKey := makeBucketKey(bucket, []byte(bucketName)) + id := makeBucketID(bucketKey) + bucket = id[:] + } + + return string(makeValueKey(bucket, []byte(key))) +} + +// SequenceKey is a helper function used in tests or external tools to create a +// sequence key from the passed bucket list. +func SequenceKey(buckets ...string) string { + id := makeBucketID([]byte(BucketKey(buckets...))) + return string(makeSequenceKey(id[:])) +} diff --git a/server/pkg/kvdb/etcd/bucket_test.go b/server/pkg/kvdb/etcd/bucket_test.go new file mode 100644 index 0000000..3582b6e --- /dev/null +++ b/server/pkg/kvdb/etcd/bucket_test.go @@ -0,0 +1,34 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "crypto/sha256" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestBucketKey tests that a key for a bucket can be created correctly. +func TestBucketKey(t *testing.T) { + rootID := sha256.Sum256([]byte("@")) + key := append(rootID[:], []byte("foo")...) + key = append(key, 0xff) + require.Equal(t, string(key), BucketKey("foo")) +} + +// TestBucketVal tests that a key for a bucket value can be created correctly. +func TestBucketVal(t *testing.T) { + rootID := sha256.Sum256([]byte("@")) + key := append(rootID[:], []byte("foo")...) + key = append(key, 0xff) + + keyID := sha256.Sum256(key) + require.Equal(t, string(keyID[:]), BucketVal("foo")) +} + +// TestSequenceKey tests that a key for a sequence can be created correctly. +func TestSequenceKey(t *testing.T) { + require.Contains(t, SequenceKey("foo", "bar", "baz"), "$seq$") +} diff --git a/server/pkg/kvdb/etcd/commit_queue.go b/server/pkg/kvdb/etcd/commit_queue.go new file mode 100644 index 0000000..4ca10d0 --- /dev/null +++ b/server/pkg/kvdb/etcd/commit_queue.go @@ -0,0 +1,213 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "container/list" + "context" + "sync" +) + +// commitQueue is a simple execution queue to manage conflicts for transactions +// and thereby reduce the number of times conflicting transactions need to be +// retried. When a new transaction is added to the queue, we first upgrade the +// read/write counts in the queue's own accounting to decide whether the new +// transaction has any conflicting dependencies. If the transaction does not +// conflict with any other, then it is committed immediately, otherwise it'll be +// queued up for later execution. +// The algorithm is described in: http://www.cs.umd.edu/~abadi/papers/vll-vldb13.pdf +type commitQueue struct { + ctx context.Context + mx sync.Mutex + readerMap map[string]int + writerMap map[string]int + + queue *list.List + queueMx sync.Mutex + queueCond *sync.Cond + + shutdown chan struct{} +} + +type commitQueueTxn struct { + commitLoop func() + blocked bool + rset []string + wset []string +} + +// NewCommitQueue creates a new commit queue, with the passed abort context. +func NewCommitQueue(ctx context.Context) *commitQueue { + q := &commitQueue{ + ctx: ctx, + readerMap: make(map[string]int), + writerMap: make(map[string]int), + queue: list.New(), + shutdown: make(chan struct{}), + } + q.queueCond = sync.NewCond(&q.queueMx) + + // Start the queue consumer loop. + go q.mainLoop() + + return q +} + +// Stop signals the queue to stop after the queue context has been canceled and +// waits until the has stopped. +func (c *commitQueue) Stop() { + // Signal the queue's condition variable to ensure the mainLoop reliably + // unblocks to check for the exit condition. + c.queueCond.Signal() + <-c.shutdown +} + +// Add increases lock counts and queues up tx commit closure for execution. +// Transactions that don't have any conflicts are executed immediately by +// "downgrading" the count mutex to allow concurrency. +func (c *commitQueue) Add(commitLoop func(), rset []string, wset []string) { + c.mx.Lock() + blocked := false + + // Mark as blocked if there's any writer changing any of the keys in + // the read set. Do not increment the reader counts yet as we'll need to + // use the original reader counts when scanning through the write set. + for _, key := range rset { + if c.writerMap[key] > 0 { + blocked = true + break + } + } + + // Mark as blocked if there's any writer or reader for any of the keys + // in the write set. + for _, key := range wset { + blocked = blocked || c.readerMap[key] > 0 || c.writerMap[key] > 0 + + // Increment the writer count. + c.writerMap[key] += 1 + } + + // Finally we can increment the reader counts for keys in the read set. + for _, key := range rset { + c.readerMap[key] += 1 + } + + c.queueCond.L.Lock() + c.queue.PushBack(&commitQueueTxn{ + commitLoop: commitLoop, + blocked: blocked, + rset: rset, + wset: wset, + }) + c.queueCond.L.Unlock() + + c.mx.Unlock() + + c.queueCond.Signal() +} + +// done decreases lock counts of the keys in the read/write sets. +func (c *commitQueue) done(rset []string, wset []string) { + c.mx.Lock() + defer c.mx.Unlock() + + for _, key := range rset { + c.readerMap[key] -= 1 + if c.readerMap[key] == 0 { + delete(c.readerMap, key) + } + } + + for _, key := range wset { + c.writerMap[key] -= 1 + if c.writerMap[key] == 0 { + delete(c.writerMap, key) + } + } +} + +// mainLoop executes queued transaction commits for transactions that have +// dependencies. The queue ensures that the top element doesn't conflict with +// any other transactions and therefore can be executed freely. +func (c *commitQueue) mainLoop() { + defer close(c.shutdown) + + for { + // Wait until there are no unblocked transactions being + // executed, and for there to be at least one blocked + // transaction in our queue. + c.queueCond.L.Lock() + for c.queue.Front() == nil { + c.queueCond.Wait() + + // Check the exit condition before looping again. + select { + case <-c.ctx.Done(): + c.queueCond.L.Unlock() + return + default: + } + } + + // Now collect all txns until we find the next blocking one. + // These shouldn't conflict (if the precollected read/write + // keys sets don't grow), meaning we can safely commit them + // in parallel. + work := make([]*commitQueueTxn, 1) + e := c.queue.Front() + work[0] = c.queue.Remove(e).(*commitQueueTxn) + + for { + e := c.queue.Front() + if e == nil { + break + } + + next := e.Value.(*commitQueueTxn) + if !next.blocked { + work = append(work, next) + c.queue.Remove(e) + } else { + // We found the next blocking txn which means + // the block of work needs to be cut here. + break + } + } + + c.queueCond.L.Unlock() + + // Check if we need to exit before continuing. + select { + case <-c.ctx.Done(): + return + default: + } + + var wg sync.WaitGroup + wg.Add(len(work)) + + // Fire up N goroutines where each will run its commit loop + // and then clean up the reader/writer maps. + for _, txn := range work { + go func(txn *commitQueueTxn) { + defer wg.Done() + txn.commitLoop() + + // We can safely cleanup here as done only + // holds the main mutex. + c.done(txn.rset, txn.wset) + }(txn) + } + + wg.Wait() + + // Check if we need to exit before continuing. + select { + case <-c.ctx.Done(): + return + default: + } + } +} diff --git a/server/pkg/kvdb/etcd/commit_queue_test.go b/server/pkg/kvdb/etcd/commit_queue_test.go new file mode 100644 index 0000000..900b08c --- /dev/null +++ b/server/pkg/kvdb/etcd/commit_queue_test.go @@ -0,0 +1,100 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestCommitQueue tests that non-conflicting transactions commit concurrently, +// while conflicting transactions are queued up. +func TestCommitQueue(t *testing.T) { + // The duration of each commit. + const commitDuration = time.Millisecond * 500 + const numCommits = 5 + + var wg sync.WaitGroup + commits := make([]string, numCommits) + idx := int32(-1) + + commit := func(tag string, sleep bool) func() { + return func() { + defer wg.Done() + + // Update our log of commit order. Avoid blocking + // by preallocating the commit log and increasing + // the log index atomically. + if sleep { + time.Sleep(commitDuration) + } + + i := atomic.AddInt32(&idx, 1) + commits[i] = tag + } + } + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + q := NewCommitQueue(ctx) + defer q.Stop() + defer cancel() + + wg.Add(numCommits) + t1 := time.Now() + + // Tx1 (long): reads: key1, key2, writes: key3, conflict: none + q.Add( + commit("free", true), + []string{"key1", "key2"}, + []string{"key3"}, + ) + // Tx2: reads: key1, key2, writes: key3, conflict: Tx1 + q.Add( + commit("blocked1", false), + []string{"key1", "key2"}, + []string{"key3"}, + ) + // Tx3 (long): reads: key1, writes: key4, conflict: none + q.Add( + commit("free", true), + []string{"key1", "key2"}, + []string{"key4"}, + ) + // Tx4 (long): reads: key1, writes: none, conflict: none + q.Add( + commit("free", true), + []string{"key1", "key2"}, + []string{}, + ) + // Tx4: reads: key2, writes: key4 conflict: Tx3 + q.Add( + commit("blocked2", false), + []string{"key2"}, + []string{"key4"}, + ) + + // Wait for all commits. + wg.Wait() + t2 := time.Now() + + // Expected total execution time: delta. + // 2 * commitDuration <= delta < 3 * commitDuration + delta := t2.Sub(t1) + require.LessOrEqual(t, int64(commitDuration*2), int64(delta)) + require.Greater(t, int64(commitDuration*3), int64(delta)) + + // Expect that the non-conflicting "free" transactions are executed + // before the blocking ones, and the blocking ones are executed in + // the order of addition. + require.Equal(t, + []string{"free", "blocked1", "free", "free", "blocked2"}, + commits, + ) +} diff --git a/server/pkg/kvdb/etcd/config.go b/server/pkg/kvdb/etcd/config.go new file mode 100644 index 0000000..e8cbaf9 --- /dev/null +++ b/server/pkg/kvdb/etcd/config.go @@ -0,0 +1,90 @@ +package etcd + +import "fmt" + +// Config holds etcd configuration alongside with configuration related to our higher level interface. +// +//nolint:lll +type Config struct { + Embedded bool `long:"embedded" description:"Use embedded etcd instance instead of the external one. Note: use for testing only."` + + EmbeddedClientPort uint16 `long:"embedded_client_port" description:"Client port to use for the embedded instance. Note: use for testing only."` + + EmbeddedPeerPort uint16 `long:"embedded_peer_port" description:"Peer port to use for the embedded instance. Note: use for testing only."` + + EmbeddedLogFile string `long:"embedded_log_file" description:"Optional log file to use for embedded instance logs. note: use for testing only."` + + Host string `long:"host" description:"Etcd database host. Supports multiple hosts separated by a comma."` + + User string `long:"user" description:"Etcd database user."` + + Pass string `long:"pass" description:"Password for the database user."` + + Namespace string `long:"namespace" description:"The etcd namespace to use."` + + DisableTLS bool `long:"disabletls" description:"Disable TLS for etcd connection. Caution: use for development only."` + + CertFile string `long:"cert_file" description:"Path to the TLS certificate for etcd RPC."` + + KeyFile string `long:"key_file" description:"Path to the TLS private key for etcd RPC."` + + InsecureSkipVerify bool `long:"insecure_skip_verify" description:"Whether we intend to skip TLS verification"` + + CollectStats bool `long:"collect_stats" description:"Whether to collect etcd commit stats."` + + MaxMsgSize int `long:"max_msg_size" description:"The maximum message size in bytes that we may send to etcd."` + + // SingleWriter should be set to true if we intend to only allow a + // single writer to the database at a time. + SingleWriter bool +} + +// CloneWithSubNamespace clones the current configuration and returns a new +// instance with the given sub namespace applied by appending it to the main +// namespace. +func (c *Config) CloneWithSubNamespace(subNamespace string) *Config { + ns := c.Namespace + if len(ns) == 0 { + ns = subNamespace + } else { + ns = fmt.Sprintf("%s/%s", ns, subNamespace) + } + + return &Config{ + Embedded: c.Embedded, + EmbeddedClientPort: c.EmbeddedClientPort, + EmbeddedPeerPort: c.EmbeddedPeerPort, + Host: c.Host, + User: c.User, + Pass: c.Pass, + Namespace: ns, + DisableTLS: c.DisableTLS, + CertFile: c.CertFile, + KeyFile: c.KeyFile, + InsecureSkipVerify: c.InsecureSkipVerify, + CollectStats: c.CollectStats, + MaxMsgSize: c.MaxMsgSize, + SingleWriter: c.SingleWriter, + } +} + +// CloneWithSingleWriter clones the current configuration and returns a new +// instance with the single writer property set to true. +func (c *Config) CloneWithSingleWriter() *Config { + return &Config{ + Embedded: c.Embedded, + EmbeddedClientPort: c.EmbeddedClientPort, + EmbeddedPeerPort: c.EmbeddedPeerPort, + Host: c.Host, + User: c.User, + Pass: c.Pass, + Namespace: c.Namespace, + DisableTLS: c.DisableTLS, + CertFile: c.CertFile, + KeyFile: c.KeyFile, + InsecureSkipVerify: c.InsecureSkipVerify, + CollectStats: c.CollectStats, + MaxMsgSize: c.MaxMsgSize, + SingleWriter: true, + } +} diff --git a/server/pkg/kvdb/etcd/db.go b/server/pkg/kvdb/etcd/db.go new file mode 100644 index 0000000..03d93c6 --- /dev/null +++ b/server/pkg/kvdb/etcd/db.go @@ -0,0 +1,323 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "fmt" + "io" + "runtime" + "strings" + "sync" + "time" + + "github.com/btcsuite/btcwallet/walletdb" + "go.etcd.io/etcd/client/pkg/v3/transport" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/namespace" +) + +const ( + // etcdConnectionTimeout is the timeout until successful connection to + // the etcd instance. + etcdConnectionTimeout = 10 * time.Second + + // etcdLongTimeout is a timeout for longer taking etcd operations. + etcdLongTimeout = 30 * time.Second + + // etcdDefaultRootBucketId is used as the root bucket key. Note that + // the actual key is not visible, since all bucket keys are hashed. + etcdDefaultRootBucketId = "@" +) + +// callerStats holds commit stats for a specific caller. Currently it only +// holds the max stat, meaning that for a particular caller the largest +// commit set is recorded. +type callerStats struct { + count int + commitStats CommitStats +} + +func (s callerStats) String() string { + return fmt.Sprintf("count: %d, retries: %d, rset: %d, wset: %d", + s.count, s.commitStats.Retries, s.commitStats.Rset, + s.commitStats.Wset) +} + +// commitStatsCollector collects commit stats for commits succeeding +// and also for commits failing. +type commitStatsCollector struct { + sync.RWMutex + succ map[string]*callerStats + fail map[string]*callerStats +} + +// newCommitStatsCollector creates a new commitStatsCollector instance. +func newCommitStatsCollector() *commitStatsCollector { + return &commitStatsCollector{ + succ: make(map[string]*callerStats), + fail: make(map[string]*callerStats), + } +} + +// PrintStats returns collected stats pretty printed into a string. +func (c *commitStatsCollector) PrintStats() string { + c.RLock() + defer c.RUnlock() + + s := "\nFailure:\n" + for k, v := range c.fail { + s += fmt.Sprintf("%s\t%s\n", k, v) + } + + s += "\nSuccess:\n" + for k, v := range c.succ { + s += fmt.Sprintf("%s\t%s\n", k, v) + } + + return s +} + +// updateStatsMap updatess commit stats map for a caller. +func updateStatMap( + caller string, stats CommitStats, m map[string]*callerStats) { + + if _, ok := m[caller]; !ok { + m[caller] = &callerStats{} + } + + curr := m[caller] + curr.count++ + + // Update only if the total commit set is greater or equal. + currTotal := curr.commitStats.Rset + curr.commitStats.Wset + if currTotal <= (stats.Rset + stats.Wset) { + curr.commitStats = stats + } +} + +// callback is an STM commit stats callback passed which can be passed +// using a WithCommitStatsCallback to the STM upon construction. +func (c *commitStatsCollector) callback(succ bool, stats CommitStats) { + caller := "unknown" + + // Get the caller. As this callback is called from + // the backend interface that means we need to ascend + // 4 frames in the callstack. + _, file, no, ok := runtime.Caller(4) + if ok { + caller = fmt.Sprintf("%s#%d", file, no) + } + + c.Lock() + defer c.Unlock() + + if succ { + updateStatMap(caller, stats, c.succ) + } else { + updateStatMap(caller, stats, c.fail) + } +} + +// db holds a reference to the etcd client connection. +type db struct { + cfg Config + ctx context.Context + cancel func() + cli *clientv3.Client + commitStatsCollector *commitStatsCollector + txQueue *commitQueue + txMutex sync.RWMutex +} + +// Enforce db implements the walletdb.DB interface. +var _ walletdb.DB = (*db)(nil) + +// NewEtcdClient creates a new etcd v3 API client. +func NewEtcdClient(ctx context.Context, cfg Config) (*clientv3.Client, + context.Context, func(), error) { + + clientCfg := clientv3.Config{ + Endpoints: strings.Split(cfg.Host, ","), + DialTimeout: etcdConnectionTimeout, + Username: cfg.User, + Password: cfg.Pass, + MaxCallSendMsgSize: cfg.MaxMsgSize, + } + + if !cfg.DisableTLS { + tlsInfo := transport.TLSInfo{ + CertFile: cfg.CertFile, + KeyFile: cfg.KeyFile, + InsecureSkipVerify: cfg.InsecureSkipVerify, + } + + tlsConfig, err := tlsInfo.ClientConfig() + if err != nil { + return nil, nil, nil, err + } + + clientCfg.TLS = tlsConfig + } + + ctx, cancel := context.WithCancel(ctx) + clientCfg.Context = ctx + cli, err := clientv3.New(clientCfg) + if err != nil { + cancel() + return nil, nil, nil, err + } + + // Apply the namespace. + cli.KV = namespace.NewKV(cli.KV, cfg.Namespace) + cli.Watcher = namespace.NewWatcher(cli.Watcher, cfg.Namespace) + cli.Lease = namespace.NewLease(cli.Lease, cfg.Namespace) + + return cli, ctx, cancel, nil +} + +// newEtcdBackend returns a db object initialized with the passed backend +// config. If etcd connection cannot be established, then returns error. +func newEtcdBackend(ctx context.Context, cfg Config) (*db, error) { + cli, ctx, cancel, err := NewEtcdClient(ctx, cfg) + if err != nil { + return nil, err + } + + backend := &db{ + cfg: cfg, + ctx: ctx, + cancel: cancel, + cli: cli, + txQueue: NewCommitQueue(ctx), + } + + if cfg.CollectStats { + backend.commitStatsCollector = newCommitStatsCollector() + } + + return backend, nil +} + +// getSTMOptions creates all STM options based on the backend config. +func (db *db) getSTMOptions() []STMOptionFunc { + opts := []STMOptionFunc{ + WithAbortContext(db.ctx), + } + + if db.cfg.CollectStats { + opts = append(opts, + WithCommitStatsCallback(db.commitStatsCollector.callback), + ) + } + + return opts +} + +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). The passed reset function is called before the start of the +// transaction and can be used to reset intermediate state. As callers may +// expect retries of the f closure (depending on the database backend used), the +// reset function will be called before each retry respectively. +func (db *db) View(f func(tx walletdb.ReadTx) error, reset func()) error { + if db.cfg.SingleWriter { + db.txMutex.RLock() + defer db.txMutex.RUnlock() + } + + apply := func(stm STM) error { + reset() + return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil)) + } + + _, err := RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...) + return err +} + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. As callers may expect retries of the f closure, the reset function +// will be called before each retry respectively. +func (db *db) Update(f func(tx walletdb.ReadWriteTx) error, reset func()) error { + if db.cfg.SingleWriter { + db.txMutex.Lock() + defer db.txMutex.Unlock() + } + + apply := func(stm STM) error { + reset() + return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil)) + } + + _, err := RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...) + return err +} + +// PrintStats returns all collected stats pretty printed into a string. +func (db *db) PrintStats() string { + if db.commitStatsCollector != nil { + return db.commitStatsCollector.PrintStats() + } + + return "" +} + +// BeginReadWriteTx opens a database read+write transaction. +func (db *db) BeginReadWriteTx() (walletdb.ReadWriteTx, error) { + var locker sync.Locker + if db.cfg.SingleWriter { + db.txMutex.Lock() + locker = &db.txMutex + } + + return newReadWriteTx( + NewSTM(db.cli, db.txQueue, db.getSTMOptions()...), + etcdDefaultRootBucketId, locker, + ), nil +} + +// BeginReadTx opens a database read transaction. +func (db *db) BeginReadTx() (walletdb.ReadTx, error) { + var locker sync.Locker + if db.cfg.SingleWriter { + db.txMutex.RLock() + locker = db.txMutex.RLocker() + } + + return newReadWriteTx( + NewSTM(db.cli, db.txQueue, db.getSTMOptions()...), + etcdDefaultRootBucketId, locker, + ), nil +} + +// Copy writes a copy of the database to the provided writer. This call will +// start a read-only transaction to perform all operations. +// This function is part of the walletdb.Db interface implementation. +func (db *db) Copy(w io.Writer) error { + ctx, cancel := context.WithTimeout(db.ctx, etcdLongTimeout) + defer cancel() + + readCloser, err := db.cli.Snapshot(ctx) + if err != nil { + return err + } + + _, err = io.Copy(w, readCloser) + + return err +} + +// Close cleanly shuts down the database and syncs all data. +// This function is part of the walletdb.Db interface implementation. +func (db *db) Close() error { + err := db.cli.Close() + db.cancel() + db.txQueue.Stop() + return err +} diff --git a/server/pkg/kvdb/etcd/db_test.go b/server/pkg/kvdb/etcd/db_test.go new file mode 100644 index 0000000..59e29fc --- /dev/null +++ b/server/pkg/kvdb/etcd/db_test.go @@ -0,0 +1,99 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "bytes" + "context" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +// TestDump tests that the Dump() method creates a one-to-one copy of the +// database content. +func TestDump(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + + db, err := newEtcdBackend(context.TODO(), f.BackendConfig()) + require.NoError(t, err) + + err = db.Update(func(tx walletdb.ReadWriteTx) error { + // "apple" + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, apple) + + require.NoError(t, apple.Put([]byte("key"), []byte("val"))) + return nil + }, func() {}) + + // Expect non-zero copy. + var buf bytes.Buffer + + require.NoError(t, db.Copy(&buf)) + require.Greater(t, buf.Len(), 0) + require.Nil(t, err) + + expected := map[string]string{ + BucketKey("apple"): BucketVal("apple"), + ValueKey("key", "apple"): "val", + } + require.Equal(t, expected, f.Dump()) +} + +// TestAbortContext tests that an update on the database is aborted if the +// database's main context in cancelled. +func TestAbortContext(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + + ctx, cancel := context.WithCancel(context.Background()) + + config := f.BackendConfig() + + // Pass abort context and abort right away. + db, err := newEtcdBackend(ctx, config) + require.NoError(t, err) + cancel() + + // Expect that the update will fail. + err = db.Update(func(tx walletdb.ReadWriteTx) error { + _, err := tx.CreateTopLevelBucket([]byte("bucket")) + require.Error(t, err, "context canceled") + + return nil + }, func() {}) + + require.Error(t, err, "context canceled") + + // No changes in the DB. + require.Equal(t, map[string]string{}, f.Dump()) +} + +// TestNewEtcdClient tests that an etcd v3 client can be created correctly. +func TestNewEtcdClient(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + + client, ctx, cancel, err := NewEtcdClient( + context.Background(), f.BackendConfig(), + ) + require.NoError(t, err) + t.Cleanup(cancel) + + _, err = client.Put(ctx, "foo/bar", "baz") + require.NoError(t, err) + + resp, err := client.Get(ctx, "foo/bar") + require.NoError(t, err) + + require.Len(t, resp.Kvs, 1) + require.Equal(t, "baz", string(resp.Kvs[0].Value)) +} diff --git a/server/pkg/kvdb/etcd/debug.go b/server/pkg/kvdb/etcd/debug.go new file mode 100644 index 0000000..f76d3f9 --- /dev/null +++ b/server/pkg/kvdb/etcd/debug.go @@ -0,0 +1,9 @@ +//go:build dev +// +build dev + +package etcd + +const ( + // Switch on extra debug code. + etcdDebug = true +) diff --git a/server/pkg/kvdb/etcd/driver.go b/server/pkg/kvdb/etcd/driver.go new file mode 100644 index 0000000..2642c13 --- /dev/null +++ b/server/pkg/kvdb/etcd/driver.go @@ -0,0 +1,80 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcwallet/walletdb" +) + +const ( + dbType = "etcd" +) + +// parseArgs parses the arguments from the walletdb Open/Create methods. +func parseArgs(funcName string, args ...interface{}) (context.Context, + *Config, error) { + + if len(args) != 2 { + return nil, nil, fmt.Errorf("invalid number of arguments to "+ + "%s.%s -- expected: context.Context, etcd.Config", + dbType, funcName, + ) + } + + ctx, ok := args[0].(context.Context) + if !ok { + return nil, nil, fmt.Errorf("argument 0 to %s.%s is invalid "+ + "-- expected: context.Context", + dbType, funcName, + ) + } + + config, ok := args[1].(*Config) + if !ok { + return nil, nil, fmt.Errorf("argument 1 to %s.%s is invalid -- "+ + "expected: etcd.Config", + dbType, funcName, + ) + } + + return ctx, config, nil +} + +// createDBDriver is the callback provided during driver registration that +// creates, initializes, and opens a database for use. +func createDBDriver(args ...interface{}) (walletdb.DB, error) { + ctx, config, err := parseArgs("Create", args...) + if err != nil { + return nil, err + } + + return newEtcdBackend(ctx, *config) +} + +// openDBDriver is the callback provided during driver registration that opens +// an existing database for use. +func openDBDriver(args ...interface{}) (walletdb.DB, error) { + ctx, config, err := parseArgs("Open", args...) + if err != nil { + return nil, err + } + + return newEtcdBackend(ctx, *config) +} + +func init() { + // Register the driver. + driver := walletdb.Driver{ + DbType: dbType, + Create: createDBDriver, + Open: openDBDriver, + } + if err := walletdb.RegisterDriver(driver); err != nil { + panic(fmt.Sprintf("Failed to regiser database driver '%s': %v", + dbType, err)) + } +} diff --git a/server/pkg/kvdb/etcd/driver_test.go b/server/pkg/kvdb/etcd/driver_test.go new file mode 100644 index 0000000..bca3293 --- /dev/null +++ b/server/pkg/kvdb/etcd/driver_test.go @@ -0,0 +1,31 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +func TestOpenCreateFailure(t *testing.T) { + t.Parallel() + + db, err := walletdb.Open(dbType) + require.Error(t, err) + require.Nil(t, db) + + db, err = walletdb.Open(dbType, "wrong") + require.Error(t, err) + require.Nil(t, db) + + db, err = walletdb.Create(dbType) + require.Error(t, err) + require.Nil(t, db) + + db, err = walletdb.Create(dbType, "wrong") + require.Error(t, err) + require.Nil(t, db) +} diff --git a/server/pkg/kvdb/etcd/embed.go b/server/pkg/kvdb/etcd/embed.go new file mode 100644 index 0000000..107fca1 --- /dev/null +++ b/server/pkg/kvdb/etcd/embed.go @@ -0,0 +1,118 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "fmt" + "net" + "net/url" + "sync/atomic" + "time" + + "go.etcd.io/etcd/server/v3/embed" +) + +const ( + // readyTimeout is the time until the embedded etcd instance should start. + readyTimeout = 10 * time.Second + + // defaultEtcdPort is the start of the range for listening ports of + // embedded etcd servers. Ports are monotonically increasing starting + // from this number and are determined by the results of getFreePort(). + defaultEtcdPort = 2379 + + // defaultNamespace is the namespace we'll use in our embedded etcd + // instance. Since it is only used for testing, we'll use the namespace + // name "test/" for this. Note that the namespace can be any string, + // the trailing / is not required. + defaultNamespace = "test/" +) + +var ( + // lastPort is the last port determined to be free for use by a new + // embedded etcd server. It should be used atomically. + lastPort uint32 = defaultEtcdPort +) + +// getFreePort returns the first port that is available for listening by a new +// embedded etcd server. It panics if no port is found and the maximum available +// TCP port is reached. +func getFreePort() int { + port := atomic.AddUint32(&lastPort, 1) + for port < 65535 { + // If there are no errors while attempting to listen on this + // port, close the socket and return it as available. + addr := fmt.Sprintf("127.0.0.1:%d", port) + l, err := net.Listen("tcp4", addr) + if err == nil { + err := l.Close() + if err == nil { + return int(port) + } + } + port = atomic.AddUint32(&lastPort, 1) + } + + // No ports available? Must be a mistake. + panic("no ports available for listening") +} + +// NewEmbeddedEtcdInstance creates an embedded etcd instance for testing, +// listening on random open ports. Returns the backend config and a cleanup +// func that will stop the etcd instance. +func NewEmbeddedEtcdInstance(path string, clientPort, peerPort uint16, + logFile string) (*Config, func(), error) { + + cfg := embed.NewConfig() + cfg.Dir = path + + // To ensure that we can submit large transactions. + cfg.MaxTxnOps = 16384 + cfg.MaxRequestBytes = 16384 * 1024 + cfg.Logger = "zap" + if logFile != "" { + cfg.LogLevel = "info" + cfg.LogOutputs = []string{logFile} + } else { + cfg.LogLevel = "error" + } + + // Listen on random free ports if no ports were specified. + if clientPort == 0 { + clientPort = uint16(getFreePort()) + } + + if peerPort == 0 { + peerPort = uint16(getFreePort()) + } + + clientURL := fmt.Sprintf("127.0.0.1:%d", clientPort) + peerURL := fmt.Sprintf("127.0.0.1:%d", peerPort) + cfg.LCUrls = []url.URL{{Host: clientURL}} + cfg.LPUrls = []url.URL{{Host: peerURL}} + + etcd, err := embed.StartEtcd(cfg) + if err != nil { + return nil, nil, err + } + + select { + case <-etcd.Server.ReadyNotify(): + case <-time.After(readyTimeout): + etcd.Close() + return nil, nil, + fmt.Errorf("etcd failed to start after: %v", readyTimeout) + } + + connConfig := &Config{ + Host: "http://" + clientURL, + InsecureSkipVerify: true, + Namespace: defaultNamespace, + MaxMsgSize: int(cfg.MaxRequestBytes), + } + + return connConfig, func() { + etcd.Close() + }, nil +} diff --git a/server/pkg/kvdb/etcd/fixture.go b/server/pkg/kvdb/etcd/fixture.go new file mode 100644 index 0000000..b7a697f --- /dev/null +++ b/server/pkg/kvdb/etcd/fixture.go @@ -0,0 +1,135 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/namespace" +) + +const ( + // testEtcdTimeout is used for all RPC calls initiated by the test fixture. + testEtcdTimeout = 5 * time.Second +) + +// EtcdTestFixture holds internal state of the etcd test fixture. +type EtcdTestFixture struct { + t *testing.T + cli *clientv3.Client + config *Config +} + +// NewTestEtcdInstance creates an embedded etcd instance for testing, listening +// on random open ports. Returns the connection config and a cleanup func that +// will stop the etcd instance. +func NewTestEtcdInstance(t *testing.T, path string) (*Config, func()) { + t.Helper() + + config, cleanup, err := NewEmbeddedEtcdInstance(path, 0, 0, "") + if err != nil { + t.Fatalf("error while staring embedded etcd instance: %v", err) + } + + return config, cleanup +} + +// NewEtcdTestFixture creates a new etcd-test fixture. This is helper +// object to facilitate etcd tests and ensure pre- and post-conditions. +func NewEtcdTestFixture(t *testing.T) *EtcdTestFixture { + tmpDir := t.TempDir() + + config, etcdCleanup := NewTestEtcdInstance(t, tmpDir) + t.Cleanup(etcdCleanup) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: strings.Split(config.Host, ","), + Username: config.User, + Password: config.Pass, + }) + if err != nil { + t.Fatalf("unable to create etcd test fixture: %v", err) + } + + // Apply the default namespace (since that's what we use in tests). + cli.KV = namespace.NewKV(cli.KV, defaultNamespace) + cli.Watcher = namespace.NewWatcher(cli.Watcher, defaultNamespace) + cli.Lease = namespace.NewLease(cli.Lease, defaultNamespace) + + return &EtcdTestFixture{ + t: t, + cli: cli, + config: config, + } +} + +func (f *EtcdTestFixture) NewBackend(singleWriter bool) walletdb.DB { + cfg := f.BackendConfig() + if singleWriter { + cfg.SingleWriter = true + } + + db, err := newEtcdBackend(context.TODO(), cfg) + require.NoError(f.t, err) + + return db +} + +// Put puts a string key/value into the test etcd database. +func (f *EtcdTestFixture) Put(key, value string) { + ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout) + defer cancel() + + _, err := f.cli.Put(ctx, key, value) + if err != nil { + f.t.Fatalf("etcd test fixture failed to put: %v", err) + } +} + +// Get queries a key and returns the stored value from the test etcd database. +func (f *EtcdTestFixture) Get(key string) string { + ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout) + defer cancel() + + resp, err := f.cli.Get(ctx, key) + if err != nil { + f.t.Fatalf("etcd test fixture failed to get: %v", err) + } + + if len(resp.Kvs) > 0 { + return string(resp.Kvs[0].Value) + } + + return "" +} + +// Dump scans and returns all key/values from the test etcd database. +func (f *EtcdTestFixture) Dump() map[string]string { + ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout) + defer cancel() + + resp, err := f.cli.Get(ctx, "\x00", clientv3.WithFromKey()) + if err != nil { + f.t.Fatalf("etcd test fixture failed to get: %v", err) + } + + result := make(map[string]string) + for _, kv := range resp.Kvs { + result[string(kv.Key)] = string(kv.Value) + } + + return result +} + +// BackendConfig returns the backend config for connecting to the embedded +// etcd instance. +func (f *EtcdTestFixture) BackendConfig() Config { + return *f.config +} diff --git a/server/pkg/kvdb/etcd/nodebug.go b/server/pkg/kvdb/etcd/nodebug.go new file mode 100644 index 0000000..1a9767a --- /dev/null +++ b/server/pkg/kvdb/etcd/nodebug.go @@ -0,0 +1,9 @@ +//go:build !dev +// +build !dev + +package etcd + +const ( + // Switch off extra debug code. + etcdDebug = false +) diff --git a/server/pkg/kvdb/etcd/readwrite_bucket.go b/server/pkg/kvdb/etcd/readwrite_bucket.go new file mode 100644 index 0000000..38b5585 --- /dev/null +++ b/server/pkg/kvdb/etcd/readwrite_bucket.go @@ -0,0 +1,437 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "strconv" + + "github.com/btcsuite/btcwallet/walletdb" +) + +// readWriteBucket stores the bucket id and the buckets transaction. +type readWriteBucket struct { + // id is used to identify the bucket and is created by + // hashing the parent id with the bucket key. For each key/value, + // sub-bucket or the bucket sequence the bucket id is used with the + // appropriate prefix to prefix the key. + id []byte + + // key is the bucket key. + key []byte + + // tx holds the parent transaction. + tx *readWriteTx +} + +// newReadWriteBucket creates a new rw bucket with the passed transaction +// and bucket id. +func newReadWriteBucket(tx *readWriteTx, key, id []byte) *readWriteBucket { + return &readWriteBucket{ + id: id, + key: key, + tx: tx, + } +} + +// NestedReadBucket retrieves a nested read bucket with the given key. +// Returns nil if the bucket does not exist. +func (b *readWriteBucket) NestedReadBucket(key []byte) walletdb.ReadBucket { + return b.NestedReadWriteBucket(key) +} + +// ForEach invokes the passed function with every key/value pair in +// the bucket. This includes nested buckets, in which case the value +// is nil, but it does not include the key/value pairs within those +// nested buckets. +func (b *readWriteBucket) ForEach(cb func(k, v []byte) error) error { + prefix := string(b.id) + + // Get the first matching key that is in the bucket. + kv, err := b.tx.stm.First(prefix) + if err != nil { + return err + } + + for kv != nil { + key, val := getKeyVal(kv) + + if err := cb(key, val); err != nil { + return err + } + + // Step to the next key. + kv, err = b.tx.stm.Next(prefix, kv.key) + if err != nil { + return err + } + } + + return nil +} + +// ForAll is an optimized version of ForEach for the case when we know we will +// fetch all (or almost all) items. +// +// NOTE: ForAll differs from ForEach in that no additional queries can +// be executed within the callback. +func (b *readWriteBucket) ForAll(cb func(k, v []byte) error) error { + // When we opened this bucket, we fetched the bucket key using the STM + // which put a revision "lock" in the read set. We can leverage this + // by incrementing the revision on the bucket, making any transaction + // retry that'd touch this same bucket. This way we can safely read all + // keys from the bucket and not cache them in the STM. + // To increment the bucket's revision, we simply put in the bucket key + // value again (which is idempotent if the bucket has just been created). + b.tx.stm.Put(string(b.key), string(b.id)) + + // TODO(bhandras): page size should be configurable in ForAll. + return b.tx.stm.FetchRangePaginatedRaw( + string(b.id), 1000, + func(kv KV) error { + key, val := getKeyVal(&kv) + return cb(key, val) + }, + ) +} + +// Get returns the value for the given key. Returns nil if the key does +// not exist in this bucket. +func (b *readWriteBucket) Get(key []byte) []byte { + // Return nil if the key is empty. + if len(key) == 0 { + return nil + } + + // Fetch the associated value. + val, err := b.tx.stm.Get(string(makeValueKey(b.id, key))) + if err != nil { + // TODO: we should return the error once the + // kvdb inteface is extended. + return nil + } + + if val == nil { + return nil + } + + return val +} + +func (b *readWriteBucket) ReadCursor() walletdb.ReadCursor { + return newReadWriteCursor(b) +} + +// NestedReadWriteBucket retrieves a nested bucket with the given key. +// Returns nil if the bucket does not exist. +func (b *readWriteBucket) NestedReadWriteBucket(key []byte) walletdb.ReadWriteBucket { + if len(key) == 0 { + return nil + } + + // Get the bucket id (and return nil if bucket doesn't exist). + bucketKey := makeBucketKey(b.id, key) + bucketVal, err := b.tx.stm.Get(string(bucketKey)) + if err != nil { + // TODO: we should return the error once the + // kvdb inteface is extended. + return nil + } + + if !isValidBucketID(bucketVal) { + return nil + } + + // Return the bucket with the fetched bucket id. + return newReadWriteBucket(b.tx, bucketKey, bucketVal) +} + +// assertNoValue checks if the value for the passed key exists. +func (b *readWriteBucket) assertNoValue(key []byte) error { + if !etcdDebug { + return nil + } + + val, err := b.tx.stm.Get(string(makeValueKey(b.id, key))) + if err != nil { + return err + } + + if val != nil { + return walletdb.ErrIncompatibleValue + } + + return nil +} + +// assertNoBucket checks if the bucket for the passed key exists. +func (b *readWriteBucket) assertNoBucket(key []byte) error { + if !etcdDebug { + return nil + } + + val, err := b.tx.stm.Get(string(makeBucketKey(b.id, key))) + if err != nil { + return err + } + + if val != nil { + return walletdb.ErrIncompatibleValue + } + + return nil +} + +// CreateBucket creates and returns a new nested bucket with the given +// key. Returns ErrBucketExists if the bucket already exists, +// ErrBucketNameRequired if the key is empty, or ErrIncompatibleValue +// if the key value is otherwise invalid for the particular database +// implementation. Other errors are possible depending on the +// implementation. +func (b *readWriteBucket) CreateBucket(key []byte) ( + walletdb.ReadWriteBucket, error) { + + if len(key) == 0 { + return nil, walletdb.ErrBucketNameRequired + } + + // Check if the bucket already exists. + bucketKey := makeBucketKey(b.id, key) + + bucketVal, err := b.tx.stm.Get(string(bucketKey)) + if err != nil { + return nil, err + } + + if isValidBucketID(bucketVal) { + return nil, walletdb.ErrBucketExists + } + + if err := b.assertNoValue(key); err != nil { + return nil, err + } + + // Create a deterministic bucket id from the bucket key. + newID := makeBucketID(bucketKey) + + // Create the bucket. + b.tx.stm.Put(string(bucketKey), string(newID[:])) + + return newReadWriteBucket(b.tx, bucketKey, newID[:]), nil +} + +// CreateBucketIfNotExists creates and returns a new nested bucket with +// the given key if it does not already exist. Returns +// ErrBucketNameRequired if the key is empty or ErrIncompatibleValue +// if the key value is otherwise invalid for the particular database +// backend. Other errors are possible depending on the implementation. +func (b *readWriteBucket) CreateBucketIfNotExists(key []byte) ( + walletdb.ReadWriteBucket, error) { + + if len(key) == 0 { + return nil, walletdb.ErrBucketNameRequired + } + + // Check for the bucket and create if it doesn't exist. + bucketKey := makeBucketKey(b.id, key) + + bucketVal, err := b.tx.stm.Get(string(bucketKey)) + if err != nil { + return nil, err + } + + if !isValidBucketID(bucketVal) { + if err := b.assertNoValue(key); err != nil { + return nil, err + } + + newID := makeBucketID(bucketKey) + b.tx.stm.Put(string(bucketKey), string(newID[:])) + + return newReadWriteBucket(b.tx, bucketKey, newID[:]), nil + } + + // Otherwise return the bucket with the fetched bucket id. + return newReadWriteBucket(b.tx, bucketKey, bucketVal), nil +} + +// DeleteNestedBucket deletes the nested bucket and its sub-buckets +// pointed to by the passed key. All values in the bucket and sub-buckets +// will be deleted as well. +func (b *readWriteBucket) DeleteNestedBucket(key []byte) error { + // TODO shouldn't empty key return ErrBucketNameRequired ? + if len(key) == 0 { + return walletdb.ErrIncompatibleValue + } + + // Get the bucket first. + bucketKey := string(makeBucketKey(b.id, key)) + + bucketVal, err := b.tx.stm.Get(bucketKey) + if err != nil { + return err + } + + if !isValidBucketID(bucketVal) { + return walletdb.ErrBucketNotFound + } + + // Enqueue the top level bucket id. + queue := [][]byte{bucketVal} + + // Traverse the buckets breadth first. + for len(queue) != 0 { + if !isValidBucketID(queue[0]) { + return walletdb.ErrBucketNotFound + } + + id := queue[0] + queue = queue[1:] + + kv, err := b.tx.stm.First(string(id)) + if err != nil { + return err + } + + for kv != nil { + b.tx.stm.Del(kv.key) + + if isBucketKey(kv.key) { + queue = append(queue, []byte(kv.val)) + } + + kv, err = b.tx.stm.Next(string(id), kv.key) + if err != nil { + return err + } + } + + // Finally delete the sequence key for the bucket. + b.tx.stm.Del(string(makeSequenceKey(id))) + } + + // Delete the top level bucket and sequence key. + b.tx.stm.Del(bucketKey) + b.tx.stm.Del(string(makeSequenceKey(bucketVal))) + + return nil +} + +// Put updates the value for the passed key. +// Returns ErrKeyRequired if the passed key is empty. +func (b *readWriteBucket) Put(key, value []byte) error { + if len(key) == 0 { + return walletdb.ErrKeyRequired + } + + if err := b.assertNoBucket(key); err != nil { + return err + } + + // Update the transaction with the new value. + b.tx.stm.Put(string(makeValueKey(b.id, key)), string(value)) + + return nil +} + +// Delete deletes the key/value pointed to by the passed key. +// Returns ErrKeyRequired if the passed key is empty. +func (b *readWriteBucket) Delete(key []byte) error { + if key == nil { + return nil + } + if len(key) == 0 { + return walletdb.ErrKeyRequired + } + + // Update the transaction to delete the key/value. + b.tx.stm.Del(string(makeValueKey(b.id, key))) + + return nil +} + +// ReadWriteCursor returns a new read-write cursor for this bucket. +func (b *readWriteBucket) ReadWriteCursor() walletdb.ReadWriteCursor { + return newReadWriteCursor(b) +} + +// Tx returns the buckets transaction. +func (b *readWriteBucket) Tx() walletdb.ReadWriteTx { + return b.tx +} + +// NextSequence returns an auto-incrementing sequence number for this bucket. +// Note that this is not a thread safe function and as such it must not be used +// for synchronization. +func (b *readWriteBucket) NextSequence() (uint64, error) { + seq := b.Sequence() + 1 + + return seq, b.SetSequence(seq) +} + +// SetSequence updates the sequence number for the bucket. +func (b *readWriteBucket) SetSequence(v uint64) error { + // Convert the number to string. + val := strconv.FormatUint(v, 10) + + // Update the transaction with the new value for the sequence key. + b.tx.stm.Put(string(makeSequenceKey(b.id)), val) + + return nil +} + +// Sequence returns the current sequence number for this bucket without +// incrementing it. +func (b *readWriteBucket) Sequence() uint64 { + val, err := b.tx.stm.Get(string(makeSequenceKey(b.id))) + if err != nil { + // TODO: This update kvdb interface such that error + // may be returned here. + return 0 + } + + if val == nil { + // If the sequence number is not yet + // stored, then take the default value. + return 0 + } + + // Otherwise try to parse a 64-bit unsigned integer from the value. + num, _ := strconv.ParseUint(string(val), 10, 64) + + return num +} + +func flattenMap(m map[string]struct{}) []string { + result := make([]string, len(m)) + i := 0 + + for key := range m { + result[i] = key + i++ + } + + return result +} + +// Prefetch will prefetch all keys in the passed-in paths as well as all bucket +// keys along the paths. +func (b *readWriteBucket) Prefetch(paths ...[]string) { + keys := make(map[string]struct{}) + ranges := make(map[string]struct{}) + + for _, path := range paths { + parent := b.id + for _, bucket := range path { + bucketKey := makeBucketKey(parent, []byte(bucket)) + keys[string(bucketKey[:])] = struct{}{} + + id := makeBucketID(bucketKey) + parent = id[:] + } + + ranges[string(parent)] = struct{}{} + } + + b.tx.stm.Prefetch(flattenMap(keys), flattenMap(ranges)) +} diff --git a/server/pkg/kvdb/etcd/readwrite_cursor.go b/server/pkg/kvdb/etcd/readwrite_cursor.go new file mode 100644 index 0000000..272e481 --- /dev/null +++ b/server/pkg/kvdb/etcd/readwrite_cursor.go @@ -0,0 +1,127 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +// readWriteCursor holds a reference to the cursors bucket, the value +// prefix and the current key used while iterating. +type readWriteCursor struct { + // bucket holds the reference to the parent bucket. + bucket *readWriteBucket + + // prefix holds the value prefix which is in front of each + // value key in the bucket. + prefix string + + // currKey holds the current key of the cursor. + currKey string +} + +func newReadWriteCursor(bucket *readWriteBucket) *readWriteCursor { + return &readWriteCursor{ + bucket: bucket, + prefix: string(bucket.id), + } +} + +// First positions the cursor at the first key/value pair and returns +// the pair. +func (c *readWriteCursor) First() (key, value []byte) { + // Get the first key with the value prefix. + kv, err := c.bucket.tx.stm.First(c.prefix) + if err != nil { + // TODO: revise this once kvdb interface supports errors + return nil, nil + } + + if kv != nil { + c.currKey = kv.key + return getKeyVal(kv) + } + + return nil, nil +} + +// Last positions the cursor at the last key/value pair and returns the +// pair. +func (c *readWriteCursor) Last() (key, value []byte) { + kv, err := c.bucket.tx.stm.Last(c.prefix) + if err != nil { + // TODO: revise this once kvdb interface supports errors + return nil, nil + } + + if kv != nil { + c.currKey = kv.key + return getKeyVal(kv) + } + + return nil, nil +} + +// Next moves the cursor one key/value pair forward and returns the new +// pair. +func (c *readWriteCursor) Next() (key, value []byte) { + kv, err := c.bucket.tx.stm.Next(c.prefix, c.currKey) + if err != nil { + // TODO: revise this once kvdb interface supports errors + return nil, nil + } + + if kv != nil { + c.currKey = kv.key + return getKeyVal(kv) + } + + return nil, nil +} + +// Prev moves the cursor one key/value pair backward and returns the new +// pair. +func (c *readWriteCursor) Prev() (key, value []byte) { + kv, err := c.bucket.tx.stm.Prev(c.prefix, c.currKey) + if err != nil { + // TODO: revise this once kvdb interface supports errors + return nil, nil + } + + if kv != nil { + c.currKey = kv.key + return getKeyVal(kv) + } + + return nil, nil +} + +// Seek positions the cursor at the passed seek key. If the key does +// not exist, the cursor is moved to the next key after seek. Returns +// the new pair. +func (c *readWriteCursor) Seek(seek []byte) (key, value []byte) { + // Seek to the first key with prefix + seek. If that key is not present + // STM will seek to the next matching key with prefix. + kv, err := c.bucket.tx.stm.Seek(c.prefix, c.prefix+string(seek)) + if err != nil { + // TODO: revise this once kvdb interface supports errors + return nil, nil + } + + if kv != nil { + c.currKey = kv.key + return getKeyVal(kv) + } + + return nil, nil +} + +// Delete removes the current key/value pair the cursor is at without +// invalidating the cursor. Returns ErrIncompatibleValue if attempted +// when the cursor points to a nested bucket. +func (c *readWriteCursor) Delete() error { + if isBucketKey(c.currKey) { + c.bucket.DeleteNestedBucket(getKey(c.currKey)) + } else { + c.bucket.Delete(getKey(c.currKey)) + } + + return nil +} diff --git a/server/pkg/kvdb/etcd/readwrite_tx.go b/server/pkg/kvdb/etcd/readwrite_tx.go new file mode 100644 index 0000000..d690b6e --- /dev/null +++ b/server/pkg/kvdb/etcd/readwrite_tx.go @@ -0,0 +1,139 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "sync" + + "github.com/btcsuite/btcwallet/walletdb" +) + +// readWriteTx holds a reference to the STM transaction. +type readWriteTx struct { + // stm is the reference to the parent STM. + stm STM + + // rootBucketID holds the sha256 hash of the root bucket id, which is used + // for key space spearation. + rootBucketID [bucketIDLength]byte + + // active is true if the transaction hasn't been committed yet. + active bool + + // lock is passed on for manual txns when the backend is instantiated + // such that we read/write lock transactions to ensure a single writer. + lock sync.Locker +} + +// newReadWriteTx creates an rw transaction with the passed STM. +func newReadWriteTx(stm STM, prefix string, lock sync.Locker) *readWriteTx { + return &readWriteTx{ + stm: stm, + active: true, + lock: lock, + rootBucketID: makeBucketID([]byte(prefix)), + } +} + +// rooBucket is a helper function to return the always present +// pseudo root bucket. +func rootBucket(tx *readWriteTx) *readWriteBucket { + return newReadWriteBucket(tx, tx.rootBucketID[:], tx.rootBucketID[:]) +} + +// RootBucket will return a handle to the root bucket. This is not a real handle +// but just a wrapper around the root bucket ID to allow derivation of child +// keys. +func (tx *readWriteTx) RootBucket() walletdb.ReadBucket { + return rootBucket(tx) +} + +// ReadBucket opens the root bucket for read only access. If the bucket +// described by the key does not exist, nil is returned. +func (tx *readWriteTx) ReadBucket(key []byte) walletdb.ReadBucket { + return rootBucket(tx).NestedReadWriteBucket(key) +} + +// ForEachBucket iterates through all top level buckets. +func (tx *readWriteTx) ForEachBucket(fn func(key []byte) error) error { + root := rootBucket(tx) + // We can safely use ForEach here since on the top level there are + // no values, only buckets. + return root.ForEach(func(key []byte, val []byte) error { + if val != nil { + // A non-nil value would mean that we have a non + // walletdb/kvdb compatible database containing + // arbitrary key/values. + return walletdb.ErrInvalid + } + + return fn(key) + }) +} + +// Rollback closes the transaction, discarding changes (if any) if the +// database was modified by a write transaction. +func (tx *readWriteTx) Rollback() error { + // If the transaction has been closed roolback will fail. + if !tx.active { + return walletdb.ErrTxClosed + } + + if tx.lock != nil { + defer tx.lock.Unlock() + } + + // Rollback the STM and set the tx to inactive. + tx.stm.Rollback() + tx.active = false + + return nil +} + +// ReadWriteBucket opens the root bucket for read/write access. If the +// bucket described by the key does not exist, nil is returned. +func (tx *readWriteTx) ReadWriteBucket(key []byte) walletdb.ReadWriteBucket { + return rootBucket(tx).NestedReadWriteBucket(key) +} + +// CreateTopLevelBucket creates the top level bucket for a key if it +// does not exist. The newly-created bucket it returned. +func (tx *readWriteTx) CreateTopLevelBucket(key []byte) (walletdb.ReadWriteBucket, error) { + return rootBucket(tx).CreateBucketIfNotExists(key) +} + +// DeleteTopLevelBucket deletes the top level bucket for a key. This +// errors if the bucket can not be found or the key keys a single value +// instead of a bucket. +func (tx *readWriteTx) DeleteTopLevelBucket(key []byte) error { + return rootBucket(tx).DeleteNestedBucket(key) +} + +// Commit commits the transaction if not already committed. Will return +// error if the underlying STM fails. +func (tx *readWriteTx) Commit() error { + // Commit will fail if the transaction is already committed. + if !tx.active { + return walletdb.ErrTxClosed + } + + if tx.lock != nil { + defer tx.lock.Unlock() + } + + // Try committing the transaction. + if err := tx.stm.Commit(); err != nil { + return err + } + + // Mark the transaction as not active after commit. + tx.active = false + + return nil +} + +// OnCommit sets the commit callback (overriding if already set). +func (tx *readWriteTx) OnCommit(cb func()) { + tx.stm.OnCommit(cb) +} diff --git a/server/pkg/kvdb/etcd/readwrite_tx_test.go b/server/pkg/kvdb/etcd/readwrite_tx_test.go new file mode 100644 index 0000000..b5758f7 --- /dev/null +++ b/server/pkg/kvdb/etcd/readwrite_tx_test.go @@ -0,0 +1,92 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +func TestChangeDuringManualTx(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + + db, err := newEtcdBackend(context.TODO(), f.BackendConfig()) + require.NoError(t, err) + + tx, err := db.BeginReadWriteTx() + require.Nil(t, err) + require.NotNil(t, tx) + + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + require.NoError(t, apple.Put([]byte("testKey"), []byte("testVal"))) + + // Try overwriting the bucket key. + f.Put(BucketKey("apple"), "banana") + + // TODO: translate error + require.NotNil(t, tx.Commit()) + require.Equal(t, map[string]string{ + BucketKey("apple"): "banana", + }, f.Dump()) +} + +func TestChangeDuringUpdate(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + + db, err := newEtcdBackend(context.TODO(), f.BackendConfig()) + require.NoError(t, err) + + count := 0 + + err = db.Update(func(tx walletdb.ReadWriteTx) error { + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, apple) + + require.NoError(t, apple.Put([]byte("key"), []byte("value"))) + + if count == 0 { + f.Put(ValueKey("key", "apple"), "new_value") + f.Put(ValueKey("key2", "apple"), "value2") + } + + cursor := apple.ReadCursor() + k, v := cursor.First() + require.Equal(t, []byte("key"), k) + require.Equal(t, []byte("value"), v) + require.Equal(t, v, apple.Get([]byte("key"))) + + k, v = cursor.Next() + if count == 0 { + require.Nil(t, k) + require.Nil(t, v) + } else { + require.Equal(t, []byte("key2"), k) + require.Equal(t, []byte("value2"), v) + } + + count++ + return nil + }, func() {}) + + require.Nil(t, err) + require.Equal(t, count, 2) + + expected := map[string]string{ + BucketKey("apple"): BucketVal("apple"), + ValueKey("key", "apple"): "value", + ValueKey("key2", "apple"): "value2", + } + require.Equal(t, expected, f.Dump()) +} diff --git a/server/pkg/kvdb/etcd/stm.go b/server/pkg/kvdb/etcd/stm.go new file mode 100644 index 0000000..3c933d5 --- /dev/null +++ b/server/pkg/kvdb/etcd/stm.go @@ -0,0 +1,1210 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "fmt" + "math" + "strings" + "time" + + "github.com/google/btree" + pb "go.etcd.io/etcd/api/v3/etcdserverpb" + v3 "go.etcd.io/etcd/client/v3" +) + +const ( + // rpcTimeout is the timeout for all RPC calls to etcd. It is set to 30 + // seconds to avoid blocking the server for too long but give reasonable + // time for etcd to respond. If any operations would take longer than 30 + // seconds that generally means there's a problem with the etcd server + // or the network resulting in degraded performance in which case we + // want LND to fail fast. Due to the underlying gRPC implementation in + // etcd calls without a timeout can hang indefinitely even in the case + // of network partitions or other critical failures. + rpcTimeout = time.Second * 30 +) + +type CommitStats struct { + Rset int + Wset int + Retries int +} + +// KV stores a key/value pair. +type KV struct { + key string + val string +} + +// STM is an interface for software transactional memory. +// All calls that return error will do so only if STM is manually handled and +// abort the apply closure otherwise. In both case the returned error is a +// DatabaseError. +type STM interface { + // Get returns the value for a key and inserts the key in the txn's read + // set. Returns nil if there's no matching key, or the key is empty. + Get(key string) ([]byte, error) + + // Put adds a value for a key to the txn's write set. + Put(key, val string) + + // Del adds a delete operation for the key to the txn's write set. + Del(key string) + + // First returns the first k/v that begins with prefix or nil if there's + // no such k/v pair. If the key is found it is inserted to the txn's + // read set. Returns nil if there's no match. + First(prefix string) (*KV, error) + + // Last returns the last k/v that begins with prefix or nil if there's + // no such k/v pair. If the key is found it is inserted to the txn's + // read set. Returns nil if there's no match. + Last(prefix string) (*KV, error) + + // Prev returns the previous k/v before key that begins with prefix or + // nil if there's no such k/v. If the key is found it is inserted to the + // read set. Returns nil if there's no match. + Prev(prefix, key string) (*KV, error) + + // Next returns the next k/v after key that begins with prefix or nil + // if there's no such k/v. If the key is found it is inserted to the + // txn's read set. Returns nil if there's no match. + Next(prefix, key string) (*KV, error) + + // Seek will return k/v at key beginning with prefix. If the key doesn't + // exists Seek will return the next k/v after key beginning with prefix. + // If a matching k/v is found it is inserted to the txn's read set. Returns + // nil if there's no match. + Seek(prefix, key string) (*KV, error) + + // OnCommit calls the passed callback func upon commit. + OnCommit(func()) + + // Commit attempts to apply the txn's changes to the server. + // Commit may return CommitError if transaction is outdated and needs retry. + Commit() error + + // Rollback entries the read and write sets such that a subsequent commit + // won't alter the database. + Rollback() + + // Prefetch prefetches the passed keys and prefixes. For prefixes it'll + // fetch the whole range. + Prefetch(keys []string, prefix []string) + + // FetchRangePaginatedRaw will fetch the range with the passed prefix up + // to the passed limit per page. + FetchRangePaginatedRaw(prefix string, limit int64, + cb func(kv KV) error) error +} + +// CommitError is used to check if there was an error +// due to stale data in the transaction. +type CommitError struct{} + +// Error returns a static string for CommitError for +// debugging/logging purposes. +func (e CommitError) Error() string { + return "commit failed" +} + +// DatabaseError is used to wrap errors that are not +// related to stale data in the transaction. +type DatabaseError struct { + msg string + err error +} + +// Unwrap returns the wrapped error in a DatabaseError. +func (e *DatabaseError) Unwrap() error { + return e.err +} + +// Error simply converts DatabaseError to a string that +// includes both the message and the wrapped error. +func (e DatabaseError) Error() string { + return fmt.Sprintf("etcd error: %v - %v", e.msg, e.err) +} + +// stmGet is the result of a read operation, a value and the mod revision of the +// key/value. +type stmGet struct { + KV + rev int64 +} + +// Less implements less operator for btree.BTree. +func (c *stmGet) Less(than btree.Item) bool { + return c.key < than.(*stmGet).key +} + +// readSet stores all reads done in an STM. +type readSet struct { + // tree stores the items in the read set. + tree *btree.BTree + + // fullRanges stores full range prefixes. + fullRanges map[string]struct{} +} + +// stmPut stores a value and an operation (put/delete). +type stmPut struct { + val string + op v3.Op +} + +// writeSet stroes all writes done in an STM. +type writeSet map[string]stmPut + +// stm implements repeatable-read software transactional memory +// over etcd. +type stm struct { + // client is an etcd client handling all RPC communications + // to the etcd instance/cluster. + client *v3.Client + + // manual is set to true for manual transactions which don't + // execute in the STM run loop. + manual bool + + // txQueue is lightweight contention manager, which is used to detect + // transaction conflicts and reduce retries. + txQueue *commitQueue + + // options stores optional settings passed by the user. + options *STMOptions + + // rset holds read key values and revisions. + rset *readSet + + // wset holds overwritten keys and their values. + wset writeSet + + // getOpts are the opts used for gets. + getOpts []v3.OpOption + + // revision stores the snapshot revision after first read. + revision int64 + + // onCommit gets called upon commit. + onCommit func() + + // callCount tracks the number of times we called into etcd. + callCount int +} + +// STMOptions can be used to pass optional settings +// when an STM is created. +type STMOptions struct { + // ctx holds an externally provided abort context. + ctx context.Context + commitStatsCallback func(bool, CommitStats) +} + +// STMOptionFunc is a function that updates the passed STMOptions. +type STMOptionFunc func(*STMOptions) + +// WithAbortContext specifies the context for permanently +// aborting the transaction. +func WithAbortContext(ctx context.Context) STMOptionFunc { + return func(so *STMOptions) { + so.ctx = ctx + } +} + +func WithCommitStatsCallback(cb func(bool, CommitStats)) STMOptionFunc { + return func(so *STMOptions) { + so.commitStatsCallback = cb + } +} + +// RunSTM runs the apply function by creating an STM using serializable snapshot +// isolation, passing it to the apply and handling commit errors and retries. +func RunSTM(cli *v3.Client, apply func(STM) error, txQueue *commitQueue, + so ...STMOptionFunc) (int, error) { + + stm := makeSTM(cli, false, txQueue, so...) + err := runSTM(stm, apply) + + return stm.callCount, err +} + +// NewSTM creates a new STM instance, using serializable snapshot isolation. +func NewSTM(cli *v3.Client, txQueue *commitQueue, so ...STMOptionFunc) STM { + return makeSTM(cli, true, txQueue, so...) +} + +// makeSTM is the actual constructor of the stm. It first apply all passed +// options then creates the stm object and resets it before returning. +func makeSTM(cli *v3.Client, manual bool, txQueue *commitQueue, + so ...STMOptionFunc) *stm { + + opts := &STMOptions{ + ctx: cli.Ctx(), + } + + // Apply all functional options. + for _, fo := range so { + fo(opts) + } + + s := &stm{ + client: cli, + manual: manual, + txQueue: txQueue, + options: opts, + rset: newReadSet(), + } + + // Reset read and write set. + s.rollback(true) + + return s +} + +// runSTM implements the run loop of the STM, running the apply func, catching +// errors and handling commit. The loop will quit on every error except +// CommitError which is used to indicate a necessary retry. +func runSTM(s *stm, apply func(STM) error) error { + var ( + retries int + stats CommitStats + executeErr error + ) + + done := make(chan struct{}) + + execute := func() { + defer close(done) + + for { + select { + // Check if the STM is aborted and break the retry loop + // if it is. + case <-s.options.ctx.Done(): + executeErr = fmt.Errorf("aborted") + return + + default: + } + + stats, executeErr = s.commit() + + // Re-apply only upon commit error (meaning the + // keys were changed). + if _, ok := executeErr.(CommitError); !ok { + // Anything that's not a CommitError + // aborts the transaction. + return + } + + // Rollback the write set before trying to re-apply. + // Upon commit we retrieved the latest version of all + // previously fetched keys and ranges so we don't need + // to rollback the read set. + s.rollback(false) + retries++ + + // Re-apply the transaction closure. + if executeErr = apply(s); executeErr != nil { + return + } + } + } + + // Run the tx closure to construct the read and write sets. + // Also we expect that if there are no conflicting transactions + // in the queue, then we only run apply once. + if preApplyErr := apply(s); preApplyErr != nil { + return preApplyErr + } + + // Make a copy of the read/write set keys here. The reason why we need + // to do this is because subsequent applies may change (shrink) these + // sets and so when we decrease reference counts in the commit queue in + // done(...) we'd potentially miss removing references which would + // result in queueing up transactions and contending DB access. + // Copying these strings is cheap due to Go's immutable string which is + // always a reference. + rkeys := make([]string, s.rset.tree.Len()) + wkeys := make([]string, len(s.wset)) + + i := 0 + s.rset.tree.Ascend(func(item btree.Item) bool { + rkeys[i] = item.(*stmGet).key + i++ + + return true + }) + + i = 0 + for key := range s.wset { + wkeys[i] = key + i++ + } + + // Queue up the transaction for execution. + s.txQueue.Add(execute, rkeys, wkeys) + + // Wait for the transaction to execute, or break if aborted. + select { + case <-done: + case <-s.options.ctx.Done(): + return context.Canceled + } + + if s.options.commitStatsCallback != nil { + stats.Retries = retries + s.options.commitStatsCallback(executeErr == nil, stats) + } + + return executeErr +} + +func newReadSet() *readSet { + return &readSet{ + tree: btree.New(5), + fullRanges: make(map[string]struct{}), + } +} + +// add inserts key/values to to read set. +func (rs *readSet) add(responses []*pb.ResponseOp) { + for _, resp := range responses { + getResp := resp.GetResponseRange() + for _, kv := range getResp.Kvs { + rs.addItem( + string(kv.Key), string(kv.Value), kv.ModRevision, + ) + } + } +} + +// addFullRange adds all full ranges to the read set. +func (rs *readSet) addFullRange(prefixes []string, responses []*pb.ResponseOp) { + for i, resp := range responses { + getResp := resp.GetResponseRange() + for _, kv := range getResp.Kvs { + rs.addItem( + string(kv.Key), string(kv.Value), kv.ModRevision, + ) + } + + rs.fullRanges[prefixes[i]] = struct{}{} + } +} + +// presetItem presets a key to zero revision if not already present in the read +// set. +func (rs *readSet) presetItem(key string) { + item := &stmGet{ + KV: KV{ + key: key, + }, + rev: 0, + } + + if !rs.tree.Has(item) { + rs.tree.ReplaceOrInsert(item) + } +} + +// addItem adds a single new key/value to the read set (if not already present). +func (rs *readSet) addItem(key, val string, modRevision int64) { + item := &stmGet{ + KV: KV{ + key: key, + val: val, + }, + rev: modRevision, + } + + rs.tree.ReplaceOrInsert(item) +} + +// hasFullRange checks if the read set has a full range prefetched. +func (rs *readSet) hasFullRange(prefix string) bool { + _, ok := rs.fullRanges[prefix] + return ok +} + +// next returns the pre-fetched next value of the prefix. If matchKey is true, +// it'll simply return the key/value that matches the passed key. +func (rs *readSet) next(prefix, key string, matchKey bool) (*stmGet, bool) { + pivot := &stmGet{ + KV: KV{ + key: key, + }, + } + + var result *stmGet + rs.tree.AscendGreaterOrEqual( + pivot, + func(item btree.Item) bool { + next := item.(*stmGet) + if (!matchKey && next.key == key) || next.rev == 0 { + return true + } + + if strings.HasPrefix(next.key, prefix) { + result = next + } + + return false + }, + ) + + return result, result != nil +} + +// prev returns the pre-fetched prev key/value of the prefix from key. +func (rs *readSet) prev(prefix, key string) (*stmGet, bool) { + pivot := &stmGet{ + KV: KV{ + key: key, + }, + } + + var result *stmGet + + rs.tree.DescendLessOrEqual( + pivot, func(item btree.Item) bool { + prev := item.(*stmGet) + if prev.key == key || prev.rev == 0 { + return true + } + + if strings.HasPrefix(prev.key, prefix) { + result = prev + } + + return false + }, + ) + + return result, result != nil +} + +// last returns the last key/value of the passed range (if prefetched). +func (rs *readSet) last(prefix string) (*stmGet, bool) { + // We create an artificial key here that is just one step away from the + // prefix. This way when we try to get the first item with our prefix + // before this newly crafted key we'll make sure it's the last element + // of our range. + key := []byte(prefix) + key[len(key)-1] += 1 + + return rs.prev(prefix, string(key)) +} + +// clear completely clears the readset. +func (rs *readSet) clear() { + rs.tree.Clear(false) + rs.fullRanges = make(map[string]struct{}) +} + +// getItem returns the matching key/value from the readset. +func (rs *readSet) getItem(key string) (*stmGet, bool) { + pivot := &stmGet{ + KV: KV{ + key: key, + }, + rev: 0, + } + item := rs.tree.Get(pivot) + if item != nil { + return item.(*stmGet), true + } + + // It's possible that although this key isn't in the read set, we + // fetched a full range the key is prefixed with. In this case we'll + // insert the key with zero revision. + for prefix := range rs.fullRanges { + if strings.HasPrefix(key, prefix) { + rs.tree.ReplaceOrInsert(pivot) + return pivot, true + } + } + + return nil, false +} + +// prefetchSet is a helper to create an op slice of all OpGet's that represent +// fetched keys appended with a slice of all OpGet's representing all prefetched +// full ranges. +func (rs *readSet) prefetchSet() []v3.Op { + ops := make([]v3.Op, 0, rs.tree.Len()) + + rs.tree.Ascend(func(item btree.Item) bool { + key := item.(*stmGet).key + for prefix := range rs.fullRanges { + // Do not add the key if it has been prefetched in a + // full range. + if strings.HasPrefix(key, prefix) { + return true + } + } + + ops = append(ops, v3.OpGet(key)) + return true + }) + + for prefix := range rs.fullRanges { + ops = append(ops, v3.OpGet(prefix, v3.WithPrefix())) + } + + return ops +} + +// getFullRanges returns all prefixes that we prefetched. +func (rs *readSet) getFullRanges() []string { + prefixes := make([]string, 0, len(rs.fullRanges)) + + for prefix := range rs.fullRanges { + prefixes = append(prefixes, prefix) + } + + return prefixes +} + +// cmps returns a compare list which will serve as a precondition testing that +// the values in the read set didn't change. +func (rs *readSet) cmps() []v3.Cmp { + cmps := make([]v3.Cmp, 0, rs.tree.Len()) + + rs.tree.Ascend(func(item btree.Item) bool { + get := item.(*stmGet) + cmps = append( + cmps, v3.Compare(v3.ModRevision(get.key), "=", get.rev), + ) + + return true + }) + + return cmps +} + +// cmps returns a cmp list testing no writes have happened past rev. +func (ws writeSet) cmps(rev int64) []v3.Cmp { + cmps := make([]v3.Cmp, 0, len(ws)) + for key := range ws { + cmps = append(cmps, v3.Compare(v3.ModRevision(key), "<", rev)) + } + + return cmps +} + +// puts is the list of ops for all pending writes. +func (ws writeSet) puts() []v3.Op { + puts := make([]v3.Op, 0, len(ws)) + for _, v := range ws { + puts = append(puts, v.op) + } + + return puts +} + +// FetchRangePaginatedRaw will fetch the range with the passed prefix up to the +// passed limit per page. +func (s *stm) FetchRangePaginatedRaw(prefix string, limit int64, + cb func(kv KV) error) error { + + s.callCount++ + + opts := []v3.OpOption{ + v3.WithSort(v3.SortByKey, v3.SortAscend), + v3.WithRange(v3.GetPrefixRangeEnd(prefix)), + v3.WithLimit(limit), + } + + key := prefix + for { + timeoutCtx, cancel := context.WithTimeout( + s.options.ctx, rpcTimeout, + ) + defer cancel() + + resp, err := s.client.Get( + timeoutCtx, key, append(opts, s.getOpts...)..., + ) + if err != nil { + return DatabaseError{ + msg: "stm.fetch() failed", + err: err, + } + } + + // Fill the read set with key/values returned. + for _, kv := range resp.Kvs { + err := cb(KV{string(kv.Key), string(kv.Value)}) + + if err != nil { + return err + } + } + + // We've reached the range end. + if !resp.More { + break + } + + // Continue from the page end + "\x00". + key = string(resp.Kvs[len(resp.Kvs)-1].Key) + "\x00" + } + + return nil +} + +// fetch is a helper to fetch key/value given options. If a value is returned +// then fetch will try to fix the STM's snapshot revision (if not already set). +// We'll also cache the returned key/value in the read set. +func (s *stm) fetch(key string, opts ...v3.OpOption) ([]KV, error) { + s.callCount++ + + timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout) + defer cancel() + + resp, err := s.client.Get( + timeoutCtx, key, append(opts, s.getOpts...)..., + ) + if err != nil { + return nil, DatabaseError{ + msg: "stm.fetch() failed", + err: err, + } + } + + // Set revision and serializable options upon first fetch + // for any subsequent fetches. + if s.getOpts == nil { + s.revision = resp.Header.Revision + s.getOpts = []v3.OpOption{ + v3.WithRev(s.revision), + v3.WithSerializable(), + } + } + + if len(resp.Kvs) == 0 { + // Add assertion to the read set which will extend our commit + // constraint such that the commit will fail if the key is + // present in the database. + s.rset.addItem(key, "", 0) + } + + var result []KV + + // Fill the read set with key/values returned. + for _, kv := range resp.Kvs { + key := string(kv.Key) + val := string(kv.Value) + + // Add to read set. + s.rset.addItem(key, val, kv.ModRevision) + + result = append(result, KV{key, val}) + } + + return result, nil +} + +// Get returns the value for key. If there's no such +// key/value in the database or the passed key is empty +// Get will return nil. +func (s *stm) Get(key string) ([]byte, error) { + if key == "" { + return nil, nil + } + + // Return freshly written value if present. + if put, ok := s.wset[key]; ok { + if put.op.IsDelete() { + return nil, nil + } + + return []byte(put.val), nil + } + + // Return value if alread in read set. + if getValue, ok := s.rset.getItem(key); ok { + // Return the value if the rset contains an existing key. + if getValue.rev != 0 { + return []byte(getValue.val), nil + } else { + return nil, nil + } + } + + // Fetch and return value. + kvs, err := s.fetch(key) + if err != nil { + return nil, err + } + + if len(kvs) > 0 { + return []byte(kvs[0].val), nil + } + + // Return empty result if key not in DB. + return nil, nil +} + +// First returns the first key/value matching prefix. If there's no key starting +// with prefix, Last will return nil. +func (s *stm) First(prefix string) (*KV, error) { + return s.next(prefix, prefix, true) +} + +// Last returns the last key/value with prefix. If there's no key starting with +// prefix, Last will return nil. +func (s *stm) Last(prefix string) (*KV, error) { + var ( + kv KV + found bool + ) + + if s.rset.hasFullRange(prefix) { + if item, ok := s.rset.last(prefix); ok { + kv = item.KV + found = true + } + } else { + // As we don't know the full range, fetch the last + // key/value with this prefix first. + resp, err := s.fetch(prefix, v3.WithLastKey()...) + if err != nil { + return nil, err + } + + if len(resp) > 0 { + kv = resp[0] + found = true + } + } + + // Now make sure there's nothing in the write set + // that is a better match, meaning it has the same + // prefix but is greater or equal than the current + // best candidate. Note that this is not efficient + // when the write set is large! + for k, put := range s.wset { + if put.op.IsDelete() { + continue + } + + if strings.HasPrefix(k, prefix) && k >= kv.key { + kv.key = k + kv.val = put.val + found = true + } + } + + if found { + return &kv, nil + } + + return nil, nil +} + +// Prev returns the prior key/value before key (with prefix). If there's no such +// key Prev will return nil. +func (s *stm) Prev(prefix, startKey string) (*KV, error) { + var kv, result KV + + fetchKey := startKey + matchFound := false + + for { + if s.rset.hasFullRange(prefix) { + if item, ok := s.rset.prev(prefix, fetchKey); ok { + kv = item.KV + } else { + break + } + } else { + + // Ask etcd to retrieve one key that is a + // match in descending order from the passed key. + opts := []v3.OpOption{ + v3.WithRange(fetchKey), + v3.WithSort(v3.SortByKey, v3.SortDescend), + v3.WithLimit(1), + } + + kvs, err := s.fetch(prefix, opts...) + if err != nil { + return nil, err + } + + if len(kvs) == 0 { + break + } + + kv = kvs[0] + } + + // WithRange and WithPrefix can't be used + // together, so check prefix here. If the + // returned key no longer has the prefix, + // then break out. + if !strings.HasPrefix(kv.key, prefix) { + break + } + + // Fetch the prior key if this is deleted. + if put, ok := s.wset[kv.key]; ok && put.op.IsDelete() { + fetchKey = kv.key + continue + } + + result = kv + matchFound = true + + break + } + + // Closure holding all checks to find a possibly + // better match. + matches := func(key string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + + if !matchFound { + return key < startKey + } + + // matchFound == true + return result.key <= key && key < startKey + } + + // Now go trough the write set and check + // if there's an even better match. + for k, put := range s.wset { + if !put.op.IsDelete() && matches(k) { + result.key = k + result.val = put.val + matchFound = true + } + } + + if !matchFound { + return nil, nil + } + + return &result, nil +} + +// Next returns the next key/value after key (with prefix). If there's no such +// key Next will return nil. +func (s *stm) Next(prefix string, key string) (*KV, error) { + return s.next(prefix, key, false) +} + +// Seek "seeks" to the key (with prefix). If the key doesn't exists it'll get +// the next key with the same prefix. If no key fills this criteria, Seek will +// return nil. +func (s *stm) Seek(prefix, key string) (*KV, error) { + return s.next(prefix, key, true) +} + +// next will try to retrieve the next match that has prefix and starts with the +// passed startKey. If includeStartKey is set to true, it'll return the value +// of startKey (essentially implementing seek). +func (s *stm) next(prefix, startKey string, includeStartKey bool) (*KV, error) { + var kv, result KV + + fetchKey := startKey + firstFetch := true + matchFound := false + + for { + if s.rset.hasFullRange(prefix) { + matchKey := includeStartKey && firstFetch + firstFetch = false + if item, ok := s.rset.next( + prefix, fetchKey, matchKey, + ); ok { + kv = item.KV + } else { + break + } + } else { + // Ask etcd to retrieve one key that is a + // match in ascending order from the passed key. + opts := []v3.OpOption{ + v3.WithFromKey(), + v3.WithSort(v3.SortByKey, v3.SortAscend), + v3.WithLimit(1), + } + + // By default we include the start key too + // if it is a full match. + if includeStartKey && firstFetch { + firstFetch = false + } else { + // If we'd like to retrieve the first key + // after the start key. + fetchKey += "\x00" + } + + kvs, err := s.fetch(fetchKey, opts...) + if err != nil { + return nil, err + } + + if len(kvs) == 0 { + break + } + + kv = kvs[0] + + // WithRange and WithPrefix can't be used + // together, so check prefix here. If the + // returned key no longer has the prefix, + // then break the fetch loop. + if !strings.HasPrefix(kv.key, prefix) { + break + } + } + + // Move on to fetch starting with the next + // key if this one is marked deleted. + if put, ok := s.wset[kv.key]; ok && put.op.IsDelete() { + fetchKey = kv.key + continue + } + + result = kv + matchFound = true + + break + } + + // Closure holding all checks to find a possibly + // better match. + matches := func(k string) bool { + if !strings.HasPrefix(k, prefix) { + return false + } + + if includeStartKey && !matchFound { + return startKey <= k + } + + if !includeStartKey && !matchFound { + return startKey < k + } + + if includeStartKey && matchFound { + return startKey <= k && k <= result.key + } + + // !includeStartKey && matchFound. + return startKey < k && k <= result.key + } + + // Now go trough the write set and check + // if there's an even better match. + for k, put := range s.wset { + if !put.op.IsDelete() && matches(k) { + result.key = k + result.val = put.val + matchFound = true + } + } + + if !matchFound { + return nil, nil + } + + return &result, nil +} + +// Put sets the value of the passed key. The actual put will happen upon commit. +func (s *stm) Put(key, val string) { + s.wset[key] = stmPut{ + val: val, + op: v3.OpPut(key, val), + } +} + +// Del marks a key as deleted. The actual delete will happen upon commit. +func (s *stm) Del(key string) { + s.wset[key] = stmPut{ + val: "", + op: v3.OpDelete(key), + } +} + +// OnCommit sets the callback that is called upon committing the STM +// transaction. +func (s *stm) OnCommit(cb func()) { + s.onCommit = cb +} + +// Prefetch will prefetch the passed keys and prefixes in one transaction. +// Keys and prefixes that we already have will be skipped. +func (s *stm) Prefetch(keys []string, prefixes []string) { + fetchKeys := make([]string, 0, len(keys)) + for _, key := range keys { + if _, ok := s.rset.getItem(key); !ok { + fetchKeys = append(fetchKeys, key) + } + } + + fetchPrefixes := make([]string, 0, len(prefixes)) + for _, prefix := range prefixes { + if s.rset.hasFullRange(prefix) { + continue + } + fetchPrefixes = append(fetchPrefixes, prefix) + } + + if len(fetchKeys) == 0 && len(fetchPrefixes) == 0 { + return + } + + prefixOpts := append( + []v3.OpOption{v3.WithPrefix()}, s.getOpts..., + ) + + timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout) + defer cancel() + + txn := s.client.Txn(timeoutCtx) + ops := make([]v3.Op, 0, len(fetchKeys)+len(fetchPrefixes)) + + for _, key := range fetchKeys { + ops = append(ops, v3.OpGet(key, s.getOpts...)) + } + for _, key := range fetchPrefixes { + ops = append(ops, v3.OpGet(key, prefixOpts...)) + } + + txn.Then(ops...) + txnresp, err := txn.Commit() + s.callCount++ + + if err != nil { + return + } + + // Set revision and serializable options upon first fetch for any + // subsequent fetches. + if s.getOpts == nil { + s.revision = txnresp.Header.Revision + s.getOpts = []v3.OpOption{ + v3.WithRev(s.revision), + v3.WithSerializable(), + } + } + + // Preset keys to "not-present" (revision set to zero). + for _, key := range fetchKeys { + s.rset.presetItem(key) + } + + // Set prefetched keys. + s.rset.add(txnresp.Responses[:len(fetchKeys)]) + + // Set prefetched ranges. + s.rset.addFullRange(fetchPrefixes, txnresp.Responses[len(fetchKeys):]) +} + +// commit builds the final transaction and tries to execute it. If commit fails +// because the keys have changed return a CommitError, otherwise return a +// DatabaseError. +func (s *stm) commit() (CommitStats, error) { + rset := s.rset.cmps() + wset := s.wset.cmps(s.revision + 1) + + stats := CommitStats{ + Rset: len(rset), + Wset: len(wset), + } + + // Create the compare set. + cmps := append(rset, wset...) + + // Create a transaction with the optional abort context. + timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout) + defer cancel() + txn := s.client.Txn(timeoutCtx) + + // If the compare set holds, try executing the puts. + txn = txn.If(cmps...) + txn = txn.Then(s.wset.puts()...) + + // Prefetch keys and ranges in case of conflict to save as many + // round-trips as possible. + txn = txn.Else(s.rset.prefetchSet()...) + + s.callCount++ + txnresp, err := txn.Commit() + if err != nil { + return stats, DatabaseError{ + msg: "stm.Commit() failed", + err: err, + } + } + + // Call the commit callback if the transaction was successful. + if txnresp.Succeeded { + if s.onCommit != nil { + s.onCommit() + } + + return stats, nil + } + + // Determine where our fetched full ranges begin in the response. + prefixes := s.rset.getFullRanges() + firstPrefixResp := len(txnresp.Responses) - len(prefixes) + + // Clear reload and preload it with the prefetched keys and ranges. + s.rset.clear() + s.rset.add(txnresp.Responses[:firstPrefixResp]) + s.rset.addFullRange(prefixes, txnresp.Responses[firstPrefixResp:]) + + // Set our revision boundary. + s.revision = txnresp.Header.Revision + s.getOpts = []v3.OpOption{ + v3.WithRev(s.revision), + v3.WithSerializable(), + } + + // Return CommitError indicating that the transaction can be retried. + return stats, CommitError{} +} + +// Commit simply calls commit and the commit stats callback if set. +func (s *stm) Commit() error { + stats, err := s.commit() + + if s.options.commitStatsCallback != nil { + s.options.commitStatsCallback(err == nil, stats) + } + + return err +} + +// Rollback resets the STM. This is useful for uncommitted transaction rollback +// and also used in the STM main loop to reset state if commit fails. +func (s *stm) Rollback() { + s.rollback(true) +} + +// rollback will reset the read and write sets. If clearReadSet is false we'll +// only reset the write set. +func (s *stm) rollback(clearReadSet bool) { + if clearReadSet { + s.rset.clear() + s.revision = math.MaxInt64 - 1 + s.getOpts = nil + } + + s.wset = make(map[string]stmPut) +} diff --git a/server/pkg/kvdb/etcd/stm_test.go b/server/pkg/kvdb/etcd/stm_test.go new file mode 100644 index 0000000..614019a --- /dev/null +++ b/server/pkg/kvdb/etcd/stm_test.go @@ -0,0 +1,412 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func reverseKVs(a []KV) []KV { + for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { + a[i], a[j] = a[j], a[i] + } + + return a +} + +func TestPutToEmpty(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + ctx, cancel := context.WithCancel(context.Background()) + + txQueue := NewCommitQueue(ctx) + t.Cleanup(func() { + cancel() + txQueue.Stop() + }) + + db, err := newEtcdBackend(ctx, f.BackendConfig()) + require.NoError(t, err) + + apply := func(stm STM) error { + stm.Put("123", "abc") + return nil + } + + callCount, err := RunSTM(db.cli, apply, txQueue) + require.NoError(t, err) + require.Equal(t, 1, callCount) + + require.Equal(t, "abc", f.Get("123")) +} + +func TestGetPutDel(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + ctx, cancel := context.WithCancel(context.Background()) + + txQueue := NewCommitQueue(ctx) + t.Cleanup(func() { + cancel() + txQueue.Stop() + }) + + testKeyValues := []KV{ + {"a", "1"}, + {"b", "2"}, + {"c", "3"}, + {"d", "4"}, + {"e", "5"}, + } + + // Extra 2 => Get(x), Commit() + expectedCallCount := len(testKeyValues) + 2 + + for _, kv := range testKeyValues { + f.Put(kv.key, kv.val) + } + + db, err := newEtcdBackend(ctx, f.BackendConfig()) + require.NoError(t, err) + + apply := func(stm STM) error { + // Get some non existing keys. + v, err := stm.Get("") + require.NoError(t, err) + require.Nil(t, v) + + // Fetches: 1. + v, err = stm.Get("x") + require.NoError(t, err) + require.Nil(t, v) + + // Get all existing keys. Fetches: len(testKeyValues) + for _, kv := range testKeyValues { + v, err = stm.Get(kv.key) + require.NoError(t, err) + require.Equal(t, []byte(kv.val), v) + } + + // Overwrite, then delete an existing key. + stm.Put("c", "6") + + v, err = stm.Get("c") + require.NoError(t, err) + require.Equal(t, []byte("6"), v) + + stm.Del("c") + + v, err = stm.Get("c") + require.NoError(t, err) + require.Nil(t, v) + + // Re-add the deleted key. + stm.Put("c", "7") + + v, err = stm.Get("c") + require.NoError(t, err) + require.Equal(t, []byte("7"), v) + + // Add a new key. + stm.Put("x", "x") + + v, err = stm.Get("x") + require.NoError(t, err) + require.Equal(t, []byte("x"), v) + + return nil + } + + callCount, err := RunSTM(db.cli, apply, txQueue) + require.NoError(t, err) + require.Equal(t, expectedCallCount, callCount) + + require.Equal(t, "1", f.Get("a")) + require.Equal(t, "2", f.Get("b")) + require.Equal(t, "7", f.Get("c")) + require.Equal(t, "4", f.Get("d")) + require.Equal(t, "5", f.Get("e")) + require.Equal(t, "x", f.Get("x")) +} + +func TestFirstLastNextPrev(t *testing.T) { + t.Parallel() + + testFirstLastNextPrev(t, nil, nil, 41) + testFirstLastNextPrev(t, nil, []string{"k"}, 4) + testFirstLastNextPrev(t, nil, []string{"k", "w"}, 2) + testFirstLastNextPrev(t, []string{"kb"}, nil, 42) + testFirstLastNextPrev(t, []string{"kb", "ke"}, nil, 42) + testFirstLastNextPrev(t, []string{"kb", "ke", "w"}, []string{"k", "w"}, 2) +} + +func testFirstLastNextPrev(t *testing.T, prefetchKeys []string, + prefetchRange []string, expectedCallCount int) { + + f := NewEtcdTestFixture(t) + ctx, cancel := context.WithCancel(context.Background()) + + txQueue := NewCommitQueue(ctx) + t.Cleanup(func() { + cancel() + txQueue.Stop() + }) + + testKeyValues := []KV{ + {"kb", "1"}, + {"kc", "2"}, + {"kda", "3"}, + {"ke", "4"}, + {"w", "w"}, + } + for _, kv := range testKeyValues { + f.Put(kv.key, kv.val) + } + + db, err := newEtcdBackend(ctx, f.BackendConfig()) + require.NoError(t, err) + + apply := func(stm STM) error { + stm.Prefetch(prefetchKeys, prefetchRange) + + // First/Last on valid multi item interval. + kv, err := stm.First("k") + require.NoError(t, err) + require.Equal(t, &KV{"kb", "1"}, kv) + + kv, err = stm.Last("k") + require.NoError(t, err) + require.Equal(t, &KV{"ke", "4"}, kv) + + // First/Last on single item interval. + kv, err = stm.First("w") + require.NoError(t, err) + require.Equal(t, &KV{"w", "w"}, kv) + + kv, err = stm.Last("w") + require.NoError(t, err) + require.Equal(t, &KV{"w", "w"}, kv) + + // Non existing. + val, err := stm.Get("ke1") + require.Nil(t, val) + require.Nil(t, err) + + val, err = stm.Get("ke2") + require.Nil(t, val) + require.Nil(t, err) + + // Next/Prev on start/end. + kv, err = stm.Next("k", "ke") + require.NoError(t, err) + require.Nil(t, kv) + + // Non existing. + val, err = stm.Get("ka") + require.Nil(t, val) + require.Nil(t, err) + + kv, err = stm.Prev("k", "kb") + require.NoError(t, err) + require.Nil(t, kv) + + // Next/Prev in the middle. + kv, err = stm.Next("k", "kc") + require.NoError(t, err) + require.Equal(t, &KV{"kda", "3"}, kv) + + kv, err = stm.Prev("k", "ke") + require.NoError(t, err) + require.Equal(t, &KV{"kda", "3"}, kv) + + // Delete first item, then add an item before the + // deleted one. Check that First/Next will "jump" + // over the deleted item and return the new first. + stm.Del("kb") + stm.Put("ka", "0") + + kv, err = stm.First("k") + require.NoError(t, err) + require.Equal(t, &KV{"ka", "0"}, kv) + + kv, err = stm.Prev("k", "kc") + require.NoError(t, err) + require.Equal(t, &KV{"ka", "0"}, kv) + + // Similarly test that a new end is returned if + // the old end is deleted first. + stm.Del("ke") + stm.Put("kf", "5") + + kv, err = stm.Last("k") + require.NoError(t, err) + require.Equal(t, &KV{"kf", "5"}, kv) + + kv, err = stm.Next("k", "kda") + require.NoError(t, err) + require.Equal(t, &KV{"kf", "5"}, kv) + + // Overwrite one in the middle. + stm.Put("kda", "6") + + kv, err = stm.Next("k", "kc") + require.NoError(t, err) + require.Equal(t, &KV{"kda", "6"}, kv) + + // Add three in the middle, then delete one. + stm.Put("kdb", "7") + stm.Put("kdc", "8") + stm.Put("kdd", "9") + stm.Del("kdc") + + // Check that stepping from first to last returns + // the expected sequence. + var kvs []KV + + curr, err := stm.First("k") + require.NoError(t, err) + + for curr != nil { + kvs = append(kvs, *curr) + curr, err = stm.Next("k", curr.key) + require.NoError(t, err) + } + + expected := []KV{ + {"ka", "0"}, + {"kc", "2"}, + {"kda", "6"}, + {"kdb", "7"}, + {"kdd", "9"}, + {"kf", "5"}, + } + require.Equal(t, expected, kvs) + + // Similarly check that stepping from last to first + // returns the expected sequence. + kvs = []KV{} + + curr, err = stm.Last("k") + require.NoError(t, err) + + for curr != nil { + kvs = append(kvs, *curr) + curr, err = stm.Prev("k", curr.key) + require.NoError(t, err) + } + + expected = reverseKVs(expected) + require.Equal(t, expected, kvs) + + return nil + } + + callCount, err := RunSTM(db.cli, apply, txQueue) + require.NoError(t, err) + require.Equal(t, expectedCallCount, callCount) + + require.Equal(t, "0", f.Get("ka")) + require.Equal(t, "2", f.Get("kc")) + require.Equal(t, "6", f.Get("kda")) + require.Equal(t, "7", f.Get("kdb")) + require.Equal(t, "9", f.Get("kdd")) + require.Equal(t, "5", f.Get("kf")) + require.Equal(t, "w", f.Get("w")) +} + +func TestCommitError(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + ctx, cancel := context.WithCancel(context.Background()) + + txQueue := NewCommitQueue(ctx) + t.Cleanup(func() { + cancel() + txQueue.Stop() + }) + + db, err := newEtcdBackend(ctx, f.BackendConfig()) + require.NoError(t, err) + + // Preset DB state. + f.Put("123", "xyz") + + // Count the number of applies. + cnt := 0 + + apply := func(stm STM) error { + // STM must have the key/value. + val, err := stm.Get("123") + require.NoError(t, err) + + if cnt == 0 { + require.Equal(t, []byte("xyz"), val) + + // Put a conflicting key/value during the first apply. + f.Put("123", "def") + } + + // We'd expect to + stm.Put("123", "abc") + + cnt++ + return nil + } + + callCount, err := RunSTM(db.cli, apply, txQueue) + require.NoError(t, err) + require.Equal(t, 2, cnt) + // Get() + 2 * Commit(). + require.Equal(t, 3, callCount) + + require.Equal(t, "abc", f.Get("123")) +} + +func TestManualTxError(t *testing.T) { + t.Parallel() + + f := NewEtcdTestFixture(t) + ctx, cancel := context.WithCancel(context.Background()) + + txQueue := NewCommitQueue(ctx) + t.Cleanup(func() { + cancel() + txQueue.Stop() + }) + + db, err := newEtcdBackend(ctx, f.BackendConfig()) + require.NoError(t, err) + + // Preset DB state. + f.Put("123", "xyz") + + stm := NewSTM(db.cli, txQueue) + + val, err := stm.Get("123") + require.NoError(t, err) + require.Equal(t, []byte("xyz"), val) + + // Put a conflicting key/value. + f.Put("123", "def") + + // Should still get the original version. + val, err = stm.Get("123") + require.NoError(t, err) + require.Equal(t, []byte("xyz"), val) + + // Commit will fail with CommitError. + err = stm.Commit() + var e CommitError + require.True(t, errors.As(err, &e)) + + // We expect that the transacton indeed did not commit. + require.Equal(t, "def", f.Get("123")) +} diff --git a/server/pkg/kvdb/etcd/walletdb_interface_test.go b/server/pkg/kvdb/etcd/walletdb_interface_test.go new file mode 100644 index 0000000..13c57e3 --- /dev/null +++ b/server/pkg/kvdb/etcd/walletdb_interface_test.go @@ -0,0 +1,19 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package etcd + +import ( + "context" + "testing" + + "github.com/btcsuite/btcwallet/walletdb/walletdbtest" +) + +// TestWalletDBInterface performs the WalletDB interface test suite for the +// etcd database driver. +func TestWalletDBInterface(t *testing.T) { + f := NewEtcdTestFixture(t) + cfg := f.BackendConfig() + walletdbtest.TestInterface(t, dbType, context.TODO(), &cfg) +} diff --git a/server/pkg/kvdb/etcd_test.go b/server/pkg/kvdb/etcd_test.go new file mode 100644 index 0000000..a943c2c --- /dev/null +++ b/server/pkg/kvdb/etcd_test.go @@ -0,0 +1,171 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package kvdb + +import ( + "fmt" + "testing" + + "github.com/ark-network/tools/kvdb/etcd" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +var ( + bkey = etcd.BucketKey + bval = etcd.BucketVal + vkey = etcd.ValueKey +) + +func TestEtcd(t *testing.T) { + tests := []struct { + name string + debugOnly bool + test func(*testing.T, walletdb.DB) + expectedDb map[string]string + }{ + { + name: "read cursor empty interval", + test: testReadCursorEmptyInterval, + }, + { + name: "read cursor non empty interval", + test: testReadCursorNonEmptyInterval, + }, + { + name: "read write cursor", + test: testReadWriteCursor, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + vkey("a", "apple"): "0", + vkey("c", "apple"): "3", + vkey("cx", "apple"): "x", + vkey("cy", "apple"): "y", + vkey("da", "apple"): "3", + vkey("f", "apple"): "5", + }, + }, + { + name: "read write cursor with bucket and value", + test: testReadWriteCursorWithBucketAndValue, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + bkey("apple", "pear"): bval("apple", "pear"), + vkey("key", "apple"): "val", + }, + }, + { + name: "bucket creation", + test: testBucketCreation, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + bkey("apple", "mango"): bval("apple", "mango"), + bkey("apple", "banana", "pear"): bval("apple", "banana", "pear"), + }, + }, + { + name: "bucket deletion", + test: testBucketDeletion, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + vkey("key1", "apple", "banana"): "val1", + vkey("key3", "apple", "banana"): "val3", + }, + }, + { + name: "bucket for each", + test: testBucketForEach, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + vkey("key1", "apple"): "val1", + vkey("key2", "apple"): "val2", + vkey("key3", "apple"): "val3", + vkey("key1", "apple", "banana"): "val1", + vkey("key2", "apple", "banana"): "val2", + vkey("key3", "apple", "banana"): "val3", + }, + }, + { + name: "bucket for each with error", + test: testBucketForEachWithError, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + bkey("apple", "pear"): bval("apple", "pear"), + vkey("key1", "apple"): "val1", + vkey("key2", "apple"): "val2", + }, + }, + { + name: "bucket sequence", + test: testBucketSequence, + }, + { + name: "key clash", + debugOnly: true, + test: testKeyClash, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + bkey("apple", "banana"): bval("apple", "banana"), + vkey("key", "apple"): "val", + }, + }, + { + name: "bucket create delete", + test: testBucketCreateDelete, + expectedDb: map[string]string{ + vkey("banana", "apple"): "value", + bkey("apple"): bval("apple"), + }, + }, + { + name: "tx manual commit", + test: testTxManualCommit, + expectedDb: map[string]string{ + bkey("apple"): bval("apple"), + vkey("testKey", "apple"): "testVal", + }, + }, + { + name: "tx rollback", + test: testTxRollback, + expectedDb: map[string]string{}, + }, + { + name: "prefetch", + test: testPrefetch, + expectedDb: map[string]string{}, + }, + } + + for _, test := range tests { + test := test + + if test.debugOnly && !etcdDebug { + continue + } + + rwLock := []bool{false, true} + for _, doRwLock := range rwLock { + name := fmt.Sprintf("%v/RWLock=%v", test.name, doRwLock) + + t.Run(name, func(t *testing.T) { + t.Parallel() + + f := etcd.NewEtcdTestFixture(t) + + test.test(t, f.NewBackend(doRwLock)) + + if test.expectedDb != nil { + dump := f.Dump() + require.Equal(t, test.expectedDb, dump) + } + }) + } + } +} diff --git a/server/pkg/kvdb/go.mod b/server/pkg/kvdb/go.mod new file mode 100644 index 0000000..17a2bcc --- /dev/null +++ b/server/pkg/kvdb/go.mod @@ -0,0 +1,88 @@ +module github.com/ark-network/tools/kvdb + +go 1.22.4 + +require ( + github.com/btcsuite/btcwallet/walletdb v1.4.2 + github.com/davecgh/go-spew v1.1.1 + github.com/google/btree v1.1.2 + github.com/lightningnetwork/lnd/healthcheck v1.2.5 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.9.0 + go.etcd.io/bbolt v1.3.10 + go.etcd.io/etcd/api/v3 v3.5.15 + go.etcd.io/etcd/client/pkg/v3 v3.5.15 + go.etcd.io/etcd/client/v3 v3.5.15 + go.etcd.io/etcd/server/v3 v3.5.15 +) + +require ( + cloud.google.com/go/compute v1.23.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd v0.23.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect + github.com/lightningnetwork/lnd/tor v1.0.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/miekg/dns v1.1.43 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.11.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + go.etcd.io/etcd/client/v2 v2.305.15 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/raft/v3 v3.5.15 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect + go.opentelemetry.io/otel v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.20.0 // indirect + go.opentelemetry.io/otel/sdk v1.20.0 // indirect + go.opentelemetry.io/otel/trace v1.20.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.14.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/server/pkg/kvdb/go.sum b/server/pkg/kvdb/go.sum new file mode 100644 index 0000000..cc8fbac --- /dev/null +++ b/server/pkg/kvdb/go.sum @@ -0,0 +1,437 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= +github.com/btcsuite/btcd v0.23.2 h1:/YOgUp25sdCnP5ho6Hl3s0E438zlX+Kak7E6TgBgoT0= +github.com/btcsuite/btcd v0.23.2/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet/walletdb v1.4.2 h1:zwZZ+zaHo4mK+FAN6KeK85S3oOm+92x2avsHvFAhVBE= +github.com/btcsuite/btcwallet/walletdb v1.4.2/go.mod h1:7ZQ+BvOEre90YT7eSq8bLoxTsgXidUzA/mqbRS114CQ= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lightningnetwork/lnd/healthcheck v1.2.5 h1:aTJy5xeBpcWgRtW/PGBDe+LMQEmNm/HQewlQx2jt7OA= +github.com/lightningnetwork/lnd/healthcheck v1.2.5/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= +github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4= +github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk= +github.com/lightningnetwork/lnd/tor v1.0.0 h1:wvEc7I+Y7IOtPglVP3cVBbYhiVhc7uTd7cMF9gQRzwA= +github.com/lightningnetwork/lnd/tor v1.0.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v2 v2.305.15 h1:VG2xbf8Vz1KJh65Ar2V5eDmfkp1bpzkSEHlhJM3usp8= +go.etcd.io/etcd/client/v2 v2.305.15/go.mod h1:Ad5dRjPVb/n5yXgAWQ/hXzuXXkBk0Y658ocuXYaUU48= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.etcd.io/etcd/pkg/v3 v3.5.15 h1:/Iu6Sr3iYaAjy++8sIDoZW9/EfhcwLZwd4FOZX2mMOU= +go.etcd.io/etcd/pkg/v3 v3.5.15/go.mod h1:e3Acf298sPFmTCGTrnGvkClEw9RYIyPtNzi1XM8rets= +go.etcd.io/etcd/raft/v3 v3.5.15 h1:jOA2HJF7zb3wy8H/pL13e8geWqkEa/kUs0waUggZC0I= +go.etcd.io/etcd/raft/v3 v3.5.15/go.mod h1:k3r7P4seEiUcgxOPLp+mloJWV3Q4QLPGNvy/OgC8OtM= +go.etcd.io/etcd/server/v3 v3.5.15 h1:x35jrWnZgsRwMsFsUJIUdT1bvzIz1B+29HjMfRYVN/E= +go.etcd.io/etcd/server/v3 v3.5.15/go.mod h1:l9jX9oa/iuArjqz0RNX/TDbc70dLXxRZo/nmPucrpFo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= +go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/pkg/kvdb/interface.go b/server/pkg/kvdb/interface.go new file mode 100644 index 0000000..8924830 --- /dev/null +++ b/server/pkg/kvdb/interface.go @@ -0,0 +1,160 @@ +package kvdb + +import ( + "github.com/btcsuite/btcwallet/walletdb" +) + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. As callers may expect retries of the f closure (depending on the +// database backend used), the reset function will be called before each retry +// respectively. +func Update(db Backend, f func(tx RwTx) error, reset func()) error { + return db.Update(f, reset) +} + +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). The passed reset function is called before the start of the +// transaction and can be used to reset intermediate state. As callers may +// expect retries of the f closure (depending on the database backend used), the +// reset function will be called before each retry respectively. +func View(db Backend, f func(tx RTx) error, reset func()) error { + return db.View(f, reset) +} + +// Batch is identical to the Update call, but it attempts to combine several +// individual Update transactions into a single write database transaction on +// an optimistic basis. This only has benefits if multiple goroutines call +// Batch. For etcd Batch simply does an Update since combination is more complex +// in that case due to STM retries. +func Batch(db Backend, f func(tx RwTx) error) error { + // Fall back to the normal Update method if the backend doesn't support + // batching. + if _, ok := db.(walletdb.BatchDB); !ok { + // Since Batch calls handle external state reset, we can safely + // pass in an empty reset closure. + return db.Update(f, func() {}) + } + + return walletdb.Batch(db, f) +} + +// Create initializes and opens a database for the specified type. The +// arguments are specific to the database type driver. See the documentation +// for the database driver for further details. +// +// ErrDbUnknownType will be returned if the database type is not registered. +var Create = walletdb.Create + +// Backend represents an ACID database. All database access is performed +// through read or read+write transactions. +type Backend = walletdb.DB + +// Open opens an existing database for the specified type. The arguments are +// specific to the database type driver. See the documentation for the database +// driver for further details. +// +// ErrDbUnknownType will be returned if the database type is not registered. +var Open = walletdb.Open + +// Driver defines a structure for backend drivers to use when they registered +// themselves as a backend which implements the Backend interface. +type Driver = walletdb.Driver + +// RBucket represents a bucket (a hierarchical structure within the +// database) that is only allowed to perform read operations. +type RBucket = walletdb.ReadBucket + +// RCursor represents a bucket cursor that can be positioned at the start or +// end of the bucket's key/value pairs and iterate over pairs in the bucket. +// This type is only allowed to perform database read operations. +type RCursor = walletdb.ReadCursor + +// RTx represents a database transaction that can only be used for reads. If +// a database update must occur, use a RwTx. +type RTx = walletdb.ReadTx + +// RwBucket represents a bucket (a hierarchical structure within the database) +// that is allowed to perform both read and write operations. +type RwBucket = walletdb.ReadWriteBucket + +// RwCursor represents a bucket cursor that can be positioned at the start or +// end of the bucket's key/value pairs and iterate over pairs in the bucket. +// This abstraction is allowed to perform both database read and write +// operations. +type RwCursor = walletdb.ReadWriteCursor + +// RwTx represents a database transaction that can be used for both reads and +// writes. When only reads are necessary, consider using a RTx instead. +type RwTx = walletdb.ReadWriteTx + +// ExtendedRTx is an extension to walletdb.ReadTx to allow prefetching of keys. +type ExtendedRTx interface { + RTx + + // RootBucket returns the "root bucket" which is pseudo bucket used + // when prefetching (keys from) top level buckets. + RootBucket() RBucket +} + +// ExtendedRBucket is an extension to walletdb.ReadBucket to allow prefetching +// of all values inside buckets. +type ExtendedRBucket interface { + RBucket + + // Prefetch will attempt to prefetch all values under a path. + Prefetch(paths ...[]string) + + // ForAll is an optimized version of ForEach. + // + // NOTE: ForAll differs from ForEach in that no additional queries can + // be executed within the callback. + ForAll(func(k, v []byte) error) error +} + +// Prefetch will attempt to prefetch all values under a path from the passed +// bucket. +func Prefetch(b RBucket, paths ...[]string) { + if bucket, ok := b.(ExtendedRBucket); ok { + bucket.Prefetch(paths...) + } +} + +// ForAll is an optimized version of ForEach with the limitation that no +// additional queries can be executed within the callback. +func ForAll(b RBucket, cb func(k, v []byte) error) error { + if bucket, ok := b.(ExtendedRBucket); ok { + return bucket.ForAll(cb) + } + + return b.ForEach(cb) +} + +// RootBucket is a wrapper to ExtendedRTx.RootBucket which does nothing if +// the implementation doesn't have ExtendedRTx. +func RootBucket(t RTx) RBucket { + if tx, ok := t.(ExtendedRTx); ok { + return tx.RootBucket() + } + + return nil +} + +var ( + // ErrBucketNotFound is returned when trying to access a bucket that + // has not been created yet. + ErrBucketNotFound = walletdb.ErrBucketNotFound + + // ErrBucketExists is returned when creating a bucket that already + // exists. + ErrBucketExists = walletdb.ErrBucketExists + + // ErrDatabaseNotOpen is returned when a database instance is accessed + // before it is opened or after it is closed. + ErrDatabaseNotOpen = walletdb.ErrDbNotOpen +) diff --git a/server/pkg/kvdb/kvdb_etcd.go b/server/pkg/kvdb/kvdb_etcd.go new file mode 100644 index 0000000..28b8152 --- /dev/null +++ b/server/pkg/kvdb/kvdb_etcd.go @@ -0,0 +1,22 @@ +//go:build kvdb_etcd +// +build kvdb_etcd + +package kvdb + +import ( + "github.com/ark-network/tools/kvdb/etcd" +) + +// EtcdBackend is conditionally set to etcd when the kvdb_etcd build tag is +// defined, allowing testing our database code with etcd backend. +const EtcdBackend = true + +// GetEtcdTestBackend creates an embedded etcd backend for testing +// storig the database at the passed path. +func StartEtcdTestBackend(path string, clientPort, peerPort uint16, + logFile string) (*etcd.Config, func(), error) { + + return etcd.NewEmbeddedEtcdInstance( + path, clientPort, peerPort, logFile, + ) +} diff --git a/server/pkg/kvdb/kvdb_no_etcd.go b/server/pkg/kvdb/kvdb_no_etcd.go new file mode 100644 index 0000000..2ffd74a --- /dev/null +++ b/server/pkg/kvdb/kvdb_no_etcd.go @@ -0,0 +1,23 @@ +//go:build !kvdb_etcd +// +build !kvdb_etcd + +package kvdb + +import ( + "fmt" + + "github.com/ark-network/tools/kvdb/etcd" +) + +// EtcdBackend is conditionally set to false when the kvdb_etcd build tag is not +// defined. This will allow testing of other database backends. +const EtcdBackend = false + +var errEtcdNotAvailable = fmt.Errorf("etcd backend not available") + +// StartEtcdTestBackend is a stub returning nil, and errEtcdNotAvailable error. +func StartEtcdTestBackend(path string, clientPort, peerPort uint16, + logFile string) (*etcd.Config, func(), error) { + + return nil, func() {}, errEtcdNotAvailable +} diff --git a/server/pkg/kvdb/nodebug.go b/server/pkg/kvdb/nodebug.go new file mode 100644 index 0000000..b29e34a --- /dev/null +++ b/server/pkg/kvdb/nodebug.go @@ -0,0 +1,9 @@ +//go:build !dev +// +build !dev + +package kvdb + +const ( + // Switch off extra debug code. + etcdDebug = false +) diff --git a/server/pkg/kvdb/prefetch_test.go b/server/pkg/kvdb/prefetch_test.go new file mode 100644 index 0000000..4d9a851 --- /dev/null +++ b/server/pkg/kvdb/prefetch_test.go @@ -0,0 +1,188 @@ +package kvdb + +import ( + "fmt" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func fetchBucket(t *testing.T, bucket walletdb.ReadBucket) map[string]string { + items := make(map[string]string) + err := bucket.ForEach(func(k, v []byte) error { + if v != nil { + items[string(k)] = string(v) + } + + return nil + }) + require.NoError(t, err) + + return items +} + +func alterBucket(t *testing.T, bucket walletdb.ReadWriteBucket, + put map[string]string, remove []string) { + + for k, v := range put { + require.NoError(t, bucket.Put([]byte(k), []byte(v))) + } + + for _, k := range remove { + require.NoError(t, bucket.Delete([]byte(k))) + } +} + +func prefetchTest(t *testing.T, db walletdb.DB, + prefetchAt []bool, put map[string]string, remove []string) { + + prefetch := func(i int, tx walletdb.ReadTx) { + require.Less(t, i, len(prefetchAt)) + if prefetchAt[i] { + Prefetch( + RootBucket(tx), + []string{"top"}, []string{"top", "bucket"}, + ) + } + } + + items := map[string]string{ + "a": "1", + "b": "2", + "c": "3", + "d": "4", + "e": "5", + } + + err := Update(db, func(tx walletdb.ReadWriteTx) error { + top, err := tx.CreateTopLevelBucket([]byte("top")) + require.NoError(t, err) + require.NotNil(t, top) + + for k, v := range items { + require.NoError(t, top.Put([]byte(k), []byte(v))) + } + + bucket, err := top.CreateBucket([]byte("bucket")) + require.NoError(t, err) + require.NotNil(t, bucket) + + for k, v := range items { + require.NoError(t, bucket.Put([]byte(k), []byte(v))) + } + + return nil + }, func() {}) + require.NoError(t, err) + + for k, v := range put { + items[k] = v + } + + for _, k := range remove { + delete(items, k) + } + + err = Update(db, func(tx walletdb.ReadWriteTx) error { + prefetch(0, tx) + top := tx.ReadWriteBucket([]byte("top")) + require.NotNil(t, top) + alterBucket(t, top, put, remove) + + prefetch(1, tx) + require.Equal(t, items, fetchBucket(t, top)) + + prefetch(2, tx) + bucket := top.NestedReadWriteBucket([]byte("bucket")) + require.NotNil(t, bucket) + alterBucket(t, bucket, put, remove) + + prefetch(3, tx) + require.Equal(t, items, fetchBucket(t, bucket)) + + return nil + }, func() {}) + require.NoError(t, err) + + err = Update(db, func(tx walletdb.ReadWriteTx) error { + return tx.DeleteTopLevelBucket([]byte("top")) + }, func() {}) + require.NoError(t, err) +} + +// testPrefetch tests that prefetching buckets works as expected even when the +// prefetch happens multiple times and the bucket contents change. Our expectation +// is that with or without prefetches, the kvdb layer works accourding to the +// interface specification. +func testPrefetch(t *testing.T, db walletdb.DB) { + tests := []struct { + put map[string]string + remove []string + }{ + { + put: nil, + remove: nil, + }, + { + put: map[string]string{ + "a": "a", + "aa": "aa", + "aaa": "aaa", + "x": "x", + "y": "y", + }, + remove: nil, + }, + { + put: map[string]string{ + "a": "a", + "aa": "aa", + "aaa": "aaa", + "x": "x", + "y": "y", + }, + remove: []string{"a", "c", "d"}, + }, + { + put: nil, + remove: []string{"b", "d"}, + }, + } + + prefetchAt := [][]bool{ + {false, false, false, false}, + {true, false, false, false}, + {false, true, false, false}, + {false, false, true, false}, + {false, false, false, true}, + {true, true, false, false}, + {true, true, true, false}, + {true, true, true, true}, + {true, false, true, true}, + {true, false, false, true}, + {true, false, true, false}, + } + + for i, test := range tests { + test := test + + for j := 0; j < len(prefetchAt); j++ { + if !t.Run( + fmt.Sprintf("prefetch %d %d", i, j), + func(t *testing.T) { + prefetchTest( + t, db, prefetchAt[j], test.put, + test.remove, + ) + }) { + + fmt.Printf("Prefetch test (%d, %d) failed:\n"+ + "testcase=%v\n prefetch=%v\n", + i, j, spew.Sdump(test), + spew.Sdump(prefetchAt[j])) + } + } + } +} diff --git a/server/pkg/kvdb/readwrite_bucket_test.go b/server/pkg/kvdb/readwrite_bucket_test.go new file mode 100644 index 0000000..2996639 --- /dev/null +++ b/server/pkg/kvdb/readwrite_bucket_test.go @@ -0,0 +1,636 @@ +package kvdb + +import ( + "fmt" + "math" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +func testBucketCreation(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + // empty bucket name + b, err := tx.CreateTopLevelBucket(nil) + require.Error(t, walletdb.ErrBucketNameRequired, err) + require.Nil(t, b) + + // empty bucket name + b, err = tx.CreateTopLevelBucket([]byte("")) + require.Error(t, walletdb.ErrBucketNameRequired, err) + require.Nil(t, b) + + // "apple" + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, apple) + + // Check bucket tx. + require.Equal(t, tx, apple.Tx()) + + // "apple" already created + b, err = tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, b) + + // "apple/banana" + banana, err := apple.CreateBucket([]byte("banana")) + require.NoError(t, err) + require.NotNil(t, banana) + + banana, err = apple.CreateBucketIfNotExists([]byte("banana")) + require.NoError(t, err) + require.NotNil(t, banana) + + // Try creating "apple/banana" again + b, err = apple.CreateBucket([]byte("banana")) + require.Error(t, walletdb.ErrBucketExists, err) + require.Nil(t, b) + + // "apple/mango" + mango, err := apple.CreateBucket([]byte("mango")) + require.Nil(t, err) + require.NotNil(t, mango) + + // "apple/banana/pear" + pear, err := banana.CreateBucket([]byte("pear")) + require.Nil(t, err) + require.NotNil(t, pear) + + // empty bucket + require.Nil(t, apple.NestedReadWriteBucket(nil)) + require.Nil(t, apple.NestedReadWriteBucket([]byte(""))) + + // "apple/pear" doesn't exist + require.Nil(t, apple.NestedReadWriteBucket([]byte("pear"))) + + // "apple/banana" exits + require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana"))) + require.NotNil(t, apple.NestedReadBucket([]byte("banana"))) + return nil + }, func() {}) + + require.Nil(t, err) +} + +func testBucketDeletion(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + // "apple" + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + // "apple/banana" + banana, err := apple.CreateBucket([]byte("banana")) + require.Nil(t, err) + require.NotNil(t, banana) + + kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}} + + for _, kv := range kvs { + require.NoError(t, banana.Put([]byte(kv.key), []byte(kv.val))) + require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key))) + } + + // Delete a k/v from "apple/banana" + require.NoError(t, banana.Delete([]byte("key2"))) + // Try getting/putting/deleting invalid k/v's. + require.Nil(t, banana.Get(nil)) + require.Error(t, walletdb.ErrKeyRequired, banana.Put(nil, []byte("val"))) + require.Error(t, walletdb.ErrKeyRequired, banana.Delete(nil)) + + // Try deleting a k/v that doesn't exist. + require.NoError(t, banana.Delete([]byte("nokey"))) + + // "apple/pear" + pear, err := apple.CreateBucket([]byte("pear")) + require.Nil(t, err) + require.NotNil(t, pear) + + // Put some values into "apple/pear" + for _, kv := range kvs { + require.Nil(t, pear.Put([]byte(kv.key), []byte(kv.val))) + require.Equal(t, []byte(kv.val), pear.Get([]byte(kv.key))) + } + + // Create nested bucket "apple/pear/cherry" + cherry, err := pear.CreateBucket([]byte("cherry")) + require.Nil(t, err) + require.NotNil(t, cherry) + + // Put some values into "apple/pear/cherry" + for _, kv := range kvs { + require.NoError(t, cherry.Put([]byte(kv.key), []byte(kv.val))) + } + + // Read back values in "apple/pear/cherry" trough a read bucket. + cherryReadBucket := pear.NestedReadBucket([]byte("cherry")) + for _, kv := range kvs { + require.Equal( + t, []byte(kv.val), + cherryReadBucket.Get([]byte(kv.key)), + ) + } + + // Try deleting some invalid buckets. + require.Error(t, + walletdb.ErrBucketNameRequired, apple.DeleteNestedBucket(nil), + ) + + // Try deleting a non existing bucket. + require.Error( + t, + walletdb.ErrBucketNotFound, + apple.DeleteNestedBucket([]byte("missing")), + ) + + // Delete "apple/pear" + require.Nil(t, apple.DeleteNestedBucket([]byte("pear"))) + + // "apple/pear" deleted + require.Nil(t, apple.NestedReadWriteBucket([]byte("pear"))) + + // "aple/banana" exists + require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana"))) + return nil + }, func() {}) + + require.Nil(t, err) +} + +type bucketIterator = func(walletdb.ReadWriteBucket, + func(key, val []byte) error) error + +func testBucketForEach(t *testing.T, db walletdb.DB) { + testBucketIterator(t, db, func(bucket walletdb.ReadWriteBucket, + callback func(key, val []byte) error) error { + + return bucket.ForEach(callback) + }) +} + +func testBucketIterator(t *testing.T, db walletdb.DB, + iterator bucketIterator) { + + err := Update(db, func(tx walletdb.ReadWriteTx) error { + // "apple" + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + // "apple/banana" + banana, err := apple.CreateBucket([]byte("banana")) + require.Nil(t, err) + require.NotNil(t, banana) + + kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}} + + // put some values into "apple" and "apple/banana" too + for _, kv := range kvs { + require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val))) + require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key))) + + require.Nil(t, banana.Put([]byte(kv.key), []byte(kv.val))) + require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key))) + } + + got := make(map[string]string) + err = apple.ForEach(func(key, val []byte) error { + got[string(key)] = string(val) + return nil + }) + + expected := map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + "banana": "", + } + + require.NoError(t, err) + require.Equal(t, expected, got) + + got = make(map[string]string) + err = iterator(banana, func(key, val []byte) error { + got[string(key)] = string(val) + return nil + }) + + require.NoError(t, err) + // remove the sub-bucket key + delete(expected, "banana") + require.Equal(t, expected, got) + + return nil + }, func() {}) + + require.Nil(t, err) +} + +func testBucketForEachWithError(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + // "apple" + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + // "apple/banana" + banana, err := apple.CreateBucket([]byte("banana")) + require.Nil(t, err) + require.NotNil(t, banana) + + // "apple/pear" + pear, err := apple.CreateBucket([]byte("pear")) + require.Nil(t, err) + require.NotNil(t, pear) + + kvs := []KV{{"key1", "val1"}, {"key2", "val2"}} + + // Put some values into "apple" and "apple/banana" too. + for _, kv := range kvs { + require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val))) + require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key))) + } + + got := make(map[string]string) + i := 0 + // Error while iterating value keys. + err = apple.ForEach(func(key, val []byte) error { + if i == 2 { + return fmt.Errorf("error") + } + + got[string(key)] = string(val) + i++ + return nil + }) + + expected := map[string]string{ + "banana": "", + "key1": "val1", + } + + require.Equal(t, expected, got) + require.Error(t, err) + + got = make(map[string]string) + i = 0 + // Erro while iterating buckets. + err = apple.ForEach(func(key, val []byte) error { + if i == 3 { + return fmt.Errorf("error") + } + + got[string(key)] = string(val) + i++ + return nil + }) + + expected = map[string]string{ + "banana": "", + "key1": "val1", + "key2": "val2", + } + + require.Equal(t, expected, got) + require.Error(t, err) + return nil + }, func() {}) + + require.Nil(t, err) +} + +func testBucketSequence(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + banana, err := apple.CreateBucket([]byte("banana")) + require.Nil(t, err) + require.NotNil(t, banana) + + require.Equal(t, uint64(0), apple.Sequence()) + require.Equal(t, uint64(0), banana.Sequence()) + + require.Nil(t, apple.SetSequence(math.MaxUint64)) + require.Equal(t, uint64(math.MaxUint64), apple.Sequence()) + + for i := uint64(0); i < uint64(5); i++ { + s, err := apple.NextSequence() + require.Nil(t, err) + require.Equal(t, i, s) + } + + return nil + }, func() {}) + + require.Nil(t, err) +} + +// TestKeyClash tests that one cannot create a bucket if a value with the same +// key exists and the same is true in reverse: that a value cannot be put if +// a bucket with the same key exists. +func testKeyClash(t *testing.T, db walletdb.DB) { + // First: + // put: /apple/key -> val + // create bucket: /apple/banana + err := Update(db, func(tx walletdb.ReadWriteTx) error { + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + require.NoError(t, apple.Put([]byte("key"), []byte("val"))) + + banana, err := apple.CreateBucket([]byte("banana")) + require.Nil(t, err) + require.NotNil(t, banana) + + return nil + }, func() {}) + + require.Nil(t, err) + + // Next try to: + // put: /apple/banana -> val => will fail (as /apple/banana is a bucket) + // create bucket: /apple/key => will fail (as /apple/key is a value) + err = Update(db, func(tx walletdb.ReadWriteTx) error { + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + require.Error(t, + walletdb.ErrIncompatibleValue, + apple.Put([]byte("banana"), []byte("val")), + ) + + b, err := apple.CreateBucket([]byte("key")) + require.Nil(t, b) + require.Error(t, walletdb.ErrIncompatibleValue, b) + + b, err = apple.CreateBucketIfNotExists([]byte("key")) + require.Nil(t, b) + require.Error(t, walletdb.ErrIncompatibleValue, b) + + return nil + }, func() {}) + + require.Nil(t, err) +} + +// TestBucketCreateDelete tests that creating then deleting then creating a +// bucket succeeds. +func testBucketCreateDelete(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, apple) + + banana, err := apple.CreateBucket([]byte("banana")) + require.NoError(t, err) + require.NotNil(t, banana) + + return nil + }, func() {}) + require.NoError(t, err) + + err = Update(db, func(tx walletdb.ReadWriteTx) error { + apple := tx.ReadWriteBucket([]byte("apple")) + require.NotNil(t, apple) + require.NoError(t, apple.DeleteNestedBucket([]byte("banana"))) + + return nil + }, func() {}) + require.NoError(t, err) + + err = Update(db, func(tx walletdb.ReadWriteTx) error { + apple := tx.ReadWriteBucket([]byte("apple")) + require.NotNil(t, apple) + require.NoError(t, apple.Put([]byte("banana"), []byte("value"))) + + return nil + }, func() {}) + require.NoError(t, err) +} + +func testTopLevelBucketCreation(t *testing.T, db walletdb.DB) { + require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error { + // Try to delete all data (there is none). + err := tx.DeleteTopLevelBucket([]byte("top")) + require.ErrorIs(t, walletdb.ErrBucketNotFound, err) + + // Create top level bucket. + top, err := tx.CreateTopLevelBucket([]byte("top")) + require.NoError(t, err) + require.NotNil(t, top) + + // Create second top level bucket with special characters. + top2, err := tx.CreateTopLevelBucket([]byte{1, 2, 3}) + require.NoError(t, err) + require.NotNil(t, top2) + + top2 = tx.ReadWriteBucket([]byte{1, 2, 3}) + require.NotNil(t, top2) + + // List top level buckets. + var tlKeys [][]byte + require.NoError(t, tx.ForEachBucket(func(k []byte) error { + tlKeys = append(tlKeys, k) + return nil + })) + require.Equal(t, [][]byte{{1, 2, 3}, []byte("top")}, tlKeys) + + // Create third top level bucket with special uppercase. + top3, err := tx.CreateTopLevelBucket([]byte("UpperBucket")) + require.NoError(t, err) + require.NotNil(t, top3) + + top3 = tx.ReadWriteBucket([]byte("UpperBucket")) + require.NotNil(t, top3) + + require.NoError(t, tx.DeleteTopLevelBucket([]byte("top"))) + require.NoError(t, tx.DeleteTopLevelBucket([]byte{1, 2, 3})) + require.NoError(t, tx.DeleteTopLevelBucket([]byte("UpperBucket"))) + + tx.ForEachBucket(func(k []byte) error { + require.Fail(t, "no top level buckets expected") + return nil + }) + + return nil + }, func() {})) +} + +func testBucketOperations(t *testing.T, db walletdb.DB) { + require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error { + // Create top level bucket. + top, err := tx.CreateTopLevelBucket([]byte("top")) + require.NoError(t, err) + require.NotNil(t, top) + + // Assert that key doesn't exist. + require.Nil(t, top.Get([]byte("key"))) + + require.NoError(t, top.ForEach(func(k, v []byte) error { + require.Fail(t, "unexpected data") + return nil + })) + + // Put key. + require.NoError(t, top.Put([]byte("key"), []byte("val"))) + require.Equal(t, []byte("val"), top.Get([]byte("key"))) + + // Overwrite key. + require.NoError(t, top.Put([]byte("key"), []byte("val2"))) + require.Equal(t, []byte("val2"), top.Get([]byte("key"))) + + // Put nil value. + require.NoError(t, top.Put([]byte("nilkey"), nil)) + require.Equal(t, []byte(""), top.Get([]byte("nilkey"))) + + // Put empty value. + require.NoError(t, top.Put([]byte("nilkey"), []byte{})) + require.Equal(t, []byte(""), top.Get([]byte("nilkey"))) + + // Try to create bucket with same name as previous key. + _, err = top.CreateBucket([]byte("key")) + require.ErrorIs(t, err, walletdb.ErrIncompatibleValue) + + _, err = top.CreateBucketIfNotExists([]byte("key")) + require.ErrorIs(t, err, walletdb.ErrIncompatibleValue) + + // Create sub-bucket. + sub2, err := top.CreateBucket([]byte("sub2")) + require.NoError(t, err) + require.NotNil(t, sub2) + + // Assert that re-creating the bucket fails. + _, err = top.CreateBucket([]byte("sub2")) + require.ErrorIs(t, err, walletdb.ErrBucketExists) + + // Assert that create-if-not-exists succeeds. + _, err = top.CreateBucketIfNotExists([]byte("sub2")) + require.NoError(t, err) + + // Assert that fetching the bucket succeeds. + sub2 = top.NestedReadWriteBucket([]byte("sub2")) + require.NotNil(t, sub2) + + // Try to put key with same name as bucket. + require.ErrorIs(t, top.Put([]byte("sub2"), []byte("val")), walletdb.ErrIncompatibleValue) + + // Put key into sub bucket. + require.NoError(t, sub2.Put([]byte("subkey"), []byte("subval"))) + require.Equal(t, []byte("subval"), sub2.Get([]byte("subkey"))) + + // Overwrite key in sub bucket. + require.NoError(t, sub2.Put([]byte("subkey"), []byte("subval2"))) + require.Equal(t, []byte("subval2"), sub2.Get([]byte("subkey"))) + + // Check for each result. + kvs := make(map[string][]byte) + require.NoError(t, top.ForEach(func(k, v []byte) error { + kvs[string(k)] = v + return nil + })) + require.Equal(t, map[string][]byte{ + "key": []byte("val2"), + "nilkey": []byte(""), + "sub2": nil, + }, kvs) + + // Delete key. + require.NoError(t, top.Delete([]byte("key"))) + + // Delete non-existent key. + require.NoError(t, top.Delete([]byte("keynonexistent"))) + + // Test cursor. + cursor := top.ReadWriteCursor() + k, v := cursor.First() + require.Equal(t, []byte("nilkey"), k) + require.Equal(t, []byte(""), v) + + k, v = cursor.Last() + require.Equal(t, []byte("sub2"), k) + require.Nil(t, v) + + k, v = cursor.Prev() + require.Equal(t, []byte("nilkey"), k) + require.Equal(t, []byte(""), v) + + k, v = cursor.Prev() + require.Nil(t, k) + require.Nil(t, v) + + k, v = cursor.Next() + require.Equal(t, []byte("sub2"), k) + require.Nil(t, v) + + k, v = cursor.Next() + require.Nil(t, k) + require.Nil(t, v) + + k, v = cursor.Seek([]byte("nilkey")) + require.Equal(t, []byte("nilkey"), k) + require.Equal(t, []byte(""), v) + + require.NoError(t, sub2.Put([]byte("k1"), []byte("v1"))) + require.NoError(t, sub2.Put([]byte("k2"), []byte("v2"))) + require.NoError(t, sub2.Put([]byte("k3"), []byte("v3"))) + + cursor = sub2.ReadWriteCursor() + cursor.First() + for i := 0; i < 4; i++ { + require.NoError(t, cursor.Delete()) + } + require.NoError(t, sub2.ForEach(func(k, v []byte) error { + require.Fail(t, "unexpected data") + return nil + })) + + _, err = sub2.CreateBucket([]byte("sub3")) + require.NoError(t, err) + require.ErrorIs(t, cursor.Delete(), walletdb.ErrIncompatibleValue) + + //Try to delete all data. + require.NoError(t, tx.DeleteTopLevelBucket([]byte("top"))) + require.Nil(t, tx.ReadBucket([]byte("top"))) + + return nil + }, func() {})) +} + +func testSubBucketSequence(t *testing.T, db walletdb.DB) { + require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error { + // Create top level bucket. + top, err := tx.CreateTopLevelBucket([]byte("top")) + require.NoError(t, err) + require.NotNil(t, top) + + // Create sub-bucket. + sub2, err := top.CreateBucket([]byte("sub2")) + require.NoError(t, err) + require.NotNil(t, sub2) + + // Test sequence. + require.Equal(t, uint64(0), top.Sequence()) + + require.NoError(t, top.SetSequence(100)) + require.Equal(t, uint64(100), top.Sequence()) + + require.NoError(t, top.SetSequence(101)) + require.Equal(t, uint64(101), top.Sequence()) + + next, err := top.NextSequence() + require.NoError(t, err) + require.Equal(t, uint64(102), next) + + next, err = sub2.NextSequence() + require.NoError(t, err) + require.Equal(t, uint64(1), next) + + return nil + }, func() {})) +} diff --git a/server/pkg/kvdb/readwrite_cursor_test.go b/server/pkg/kvdb/readwrite_cursor_test.go new file mode 100644 index 0000000..de70196 --- /dev/null +++ b/server/pkg/kvdb/readwrite_cursor_test.go @@ -0,0 +1,316 @@ +package kvdb + +import ( + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +func testReadCursorEmptyInterval(t *testing.T, db walletdb.DB) { + err := Update(db, func(tx walletdb.ReadWriteTx) error { + b, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, b) + + return nil + }, func() {}) + require.NoError(t, err) + + err = View(db, func(tx walletdb.ReadTx) error { + b := tx.ReadBucket([]byte("apple")) + require.NotNil(t, b) + + cursor := b.ReadCursor() + k, v := cursor.First() + require.Nil(t, k) + require.Nil(t, v) + + k, v = cursor.Next() + require.Nil(t, k) + require.Nil(t, v) + + k, v = cursor.Last() + require.Nil(t, k) + require.Nil(t, v) + + k, v = cursor.Prev() + require.Nil(t, k) + require.Nil(t, v) + + return nil + }, func() {}) + require.NoError(t, err) +} + +func testReadCursorNonEmptyInterval(t *testing.T, db walletdb.DB) { + testKeyValues := []KV{ + {"b", "1"}, + {"c", "2"}, + {"da", "3"}, + {"e", "4"}, + } + + err := Update(db, func(tx walletdb.ReadWriteTx) error { + b, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, b) + + for _, kv := range testKeyValues { + require.NoError(t, b.Put([]byte(kv.key), []byte(kv.val))) + } + return nil + }, func() {}) + + require.NoError(t, err) + + err = View(db, func(tx walletdb.ReadTx) error { + b := tx.ReadBucket([]byte("apple")) + require.NotNil(t, b) + + // Iterate from the front. + var kvs []KV + cursor := b.ReadCursor() + k, v := cursor.First() + + for k != nil && v != nil { + kvs = append(kvs, KV{string(k), string(v)}) + k, v = cursor.Next() + } + require.Equal(t, testKeyValues, kvs) + + // Iterate from the back. + kvs = []KV{} + k, v = cursor.Last() + + for k != nil && v != nil { + kvs = append(kvs, KV{string(k), string(v)}) + k, v = cursor.Prev() + } + require.Equal(t, reverseKVs(testKeyValues), kvs) + + // Random access + perm := []int{3, 0, 2, 1} + for _, i := range perm { + k, v := cursor.Seek([]byte(testKeyValues[i].key)) + require.Equal(t, []byte(testKeyValues[i].key), k) + require.Equal(t, []byte(testKeyValues[i].val), v) + } + + // Seek to nonexisting key. + k, v = cursor.Seek(nil) + require.Equal(t, "b", string(k)) + require.Equal(t, "1", string(v)) + + k, v = cursor.Seek([]byte("x")) + require.Nil(t, k) + require.Nil(t, v) + + return nil + }, func() {}) + + require.NoError(t, err) +} + +func testReadWriteCursor(t *testing.T, db walletdb.DB) { + testKeyValues := []KV{ + {"b", "1"}, + {"c", "2"}, + {"da", "3"}, + {"e", "4"}, + } + + count := len(testKeyValues) + + // Pre-store the first half of the interval. + require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error { + b, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, b) + + for i := 0; i < count/2; i++ { + err = b.Put( + []byte(testKeyValues[i].key), + []byte(testKeyValues[i].val), + ) + require.NoError(t, err) + } + return nil + }, func() {})) + + err := Update(db, func(tx walletdb.ReadWriteTx) error { + b := tx.ReadWriteBucket([]byte("apple")) + require.NotNil(t, b) + + // Store the second half of the interval. + for i := count / 2; i < count; i++ { + err := b.Put( + []byte(testKeyValues[i].key), + []byte(testKeyValues[i].val), + ) + require.NoError(t, err) + } + + cursor := b.ReadWriteCursor() + + // First on valid interval. + fk, fv := cursor.First() + require.Equal(t, []byte("b"), fk) + require.Equal(t, []byte("1"), fv) + + // Prev(First()) = nil + k, v := cursor.Prev() + require.Nil(t, k) + require.Nil(t, v) + + // Last on valid interval. + lk, lv := cursor.Last() + require.Equal(t, []byte("e"), lk) + require.Equal(t, []byte("4"), lv) + + // Next(Last()) = nil + k, v = cursor.Next() + require.Nil(t, k) + require.Nil(t, v) + + // Delete first item, then add an item before the + // deleted one. Check that First/Next will "jump" + // over the deleted item and return the new first. + _, _ = cursor.First() + require.NoError(t, cursor.Delete()) + require.NoError(t, b.Put([]byte("a"), []byte("0"))) + fk, fv = cursor.First() + + require.Equal(t, []byte("a"), fk) + require.Equal(t, []byte("0"), fv) + + k, v = cursor.Next() + require.Equal(t, []byte("c"), k) + require.Equal(t, []byte("2"), v) + + // Similarly test that a new end is returned if + // the old end is deleted first. + _, _ = cursor.Last() + require.NoError(t, cursor.Delete()) + require.NoError(t, b.Put([]byte("f"), []byte("5"))) + + lk, lv = cursor.Last() + require.Equal(t, []byte("f"), lk) + require.Equal(t, []byte("5"), lv) + + k, v = cursor.Prev() + require.Equal(t, []byte("da"), k) + require.Equal(t, []byte("3"), v) + + // Overwrite k/v in the middle of the interval. + require.NoError(t, b.Put([]byte("c"), []byte("3"))) + k, v = cursor.Prev() + require.Equal(t, []byte("c"), k) + require.Equal(t, []byte("3"), v) + + // Insert new key/values. + require.NoError(t, b.Put([]byte("cx"), []byte("x"))) + require.NoError(t, b.Put([]byte("cy"), []byte("y"))) + + k, v = cursor.Next() + require.Equal(t, []byte("cx"), k) + require.Equal(t, []byte("x"), v) + + k, v = cursor.Next() + require.Equal(t, []byte("cy"), k) + require.Equal(t, []byte("y"), v) + + expected := []KV{ + {"a", "0"}, + {"c", "3"}, + {"cx", "x"}, + {"cy", "y"}, + {"da", "3"}, + {"f", "5"}, + } + + // Iterate from the front. + var kvs []KV + k, v = cursor.First() + + for k != nil && v != nil { + kvs = append(kvs, KV{string(k), string(v)}) + k, v = cursor.Next() + } + require.Equal(t, expected, kvs) + + // Iterate from the back. + kvs = []KV{} + k, v = cursor.Last() + + for k != nil && v != nil { + kvs = append(kvs, KV{string(k), string(v)}) + k, v = cursor.Prev() + } + require.Equal(t, reverseKVs(expected), kvs) + + return nil + }, func() {}) + + require.NoError(t, err) +} + +// testReadWriteCursorWithBucketAndValue tests that cursors are able to iterate +// over both bucket and value keys if both are present in the iterated bucket. +func testReadWriteCursorWithBucketAndValue(t *testing.T, db walletdb.DB) { + + // Pre-store the first half of the interval. + require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error { + b, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, b) + + require.NoError(t, b.Put([]byte("key"), []byte("val"))) + + b1, err := b.CreateBucket([]byte("banana")) + require.NoError(t, err) + require.NotNil(t, b1) + + b2, err := b.CreateBucket([]byte("pear")) + require.NoError(t, err) + require.NotNil(t, b2) + + return nil + }, func() {})) + + err := View(db, func(tx walletdb.ReadTx) error { + b := tx.ReadBucket([]byte("apple")) + require.NotNil(t, b) + + cursor := b.ReadCursor() + + // First on valid interval. + k, v := cursor.First() + require.Equal(t, []byte("banana"), k) + require.Nil(t, v) + + k, v = cursor.Next() + require.Equal(t, []byte("key"), k) + require.Equal(t, []byte("val"), v) + + k, v = cursor.Last() + require.Equal(t, []byte("pear"), k) + require.Nil(t, v) + + k, v = cursor.Seek([]byte("k")) + require.Equal(t, []byte("key"), k) + require.Equal(t, []byte("val"), v) + + k, v = cursor.Seek([]byte("banana")) + require.Equal(t, []byte("banana"), k) + require.Nil(t, v) + + k, v = cursor.Next() + require.Equal(t, []byte("key"), k) + require.Equal(t, []byte("val"), v) + + return nil + }, func() {}) + + require.NoError(t, err) +} diff --git a/server/pkg/kvdb/readwrite_tx_test.go b/server/pkg/kvdb/readwrite_tx_test.go new file mode 100644 index 0000000..b07e2ef --- /dev/null +++ b/server/pkg/kvdb/readwrite_tx_test.go @@ -0,0 +1,49 @@ +package kvdb + +import ( + "testing" + + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +func testTxManualCommit(t *testing.T, db walletdb.DB) { + tx, err := db.BeginReadWriteTx() + require.NoError(t, err) + require.NotNil(t, tx) + + committed := false + + tx.OnCommit(func() { + committed = true + }) + + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.NoError(t, err) + require.NotNil(t, apple) + require.NoError(t, apple.Put([]byte("testKey"), []byte("testVal"))) + + banana, err := tx.CreateTopLevelBucket([]byte("banana")) + require.NoError(t, err) + require.NotNil(t, banana) + require.NoError(t, banana.Put([]byte("testKey"), []byte("testVal"))) + require.NoError(t, tx.DeleteTopLevelBucket([]byte("banana"))) + + require.NoError(t, tx.Commit()) + require.True(t, committed) +} + +func testTxRollback(t *testing.T, db walletdb.DB) { + tx, err := db.BeginReadWriteTx() + require.Nil(t, err) + require.NotNil(t, tx) + + apple, err := tx.CreateTopLevelBucket([]byte("apple")) + require.Nil(t, err) + require.NotNil(t, apple) + + require.NoError(t, apple.Put([]byte("testKey"), []byte("testVal"))) + + require.NoError(t, tx.Rollback()) + require.Error(t, walletdb.ErrTxClosed, tx.Commit()) +} diff --git a/server/pkg/kvdb/test.go b/server/pkg/kvdb/test.go new file mode 100644 index 0000000..483862e --- /dev/null +++ b/server/pkg/kvdb/test.go @@ -0,0 +1,14 @@ +package kvdb + +type KV struct { + key string + val string +} + +func reverseKVs(a []KV) []KV { + for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { + a[i], a[j] = a[j], a[i] + } + + return a +} diff --git a/server/pkg/kvdb/test_utils.go b/server/pkg/kvdb/test_utils.go new file mode 100644 index 0000000..e35b224 --- /dev/null +++ b/server/pkg/kvdb/test_utils.go @@ -0,0 +1,26 @@ +package kvdb + +import ( + "fmt" + "os" + "testing" +) + +// RunTests is a helper function to run the tests in a package with +// initialization and tear-down of a test kvdb backend. +func RunTests(m *testing.M) { + var close func() error + + // os.Exit() does not respect defer statements + code := m.Run() + + if close != nil { + err := close() + if err != nil { + fmt.Printf("Error: %v\n", err) + } + } + + os.Exit(code) + +} diff --git a/server/pkg/macaroons/LICENSE b/server/pkg/macaroons/LICENSE new file mode 100644 index 0000000..cfab3da --- /dev/null +++ b/server/pkg/macaroons/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/server/pkg/macaroons/README.md b/server/pkg/macaroons/README.md new file mode 100644 index 0000000..4e2396b --- /dev/null +++ b/server/pkg/macaroons/README.md @@ -0,0 +1,150 @@ +# macaroons + +This is a more detailed, technical description of how macaroons work and how +authentication and authorization is implemented in `lnd`. + +For a more high-level overview see +[macaroons.md in the docs](../docs/macaroons.md). + +## Root key + +At startup, if the option `--no-macaroons` is **not** used, a Bolt DB key/value +store named `data/macaroons.db` is created with a bucket named `macrootkeys`. +In this DB the following two key/value pairs are stored: + +* Key `0`: the encrypted root key (32 bytes). + * If the root key does not exist yet, 32 bytes of pseudo-random data is + generated and used. +* Key `enckey`: the parameters used to derive a secret encryption key from a + passphrase. + * The following parameters are stored: `

` + * `salt`: 32 byte of random data used as salt for the `scrypt` key + derivation. + * `digest`: sha256 hashed key derived from the `scrypt` operation. Is used + to verify if the password is correct. + * `N`, `P`, `R`: Parameters used for the `scrypt` operation. + * The root key is symmetrically encrypted with the derived secret key, using + the `secretbox` method of the library + [btcsuite/golangcrypto](https://github.com/btcsuite/golangcrypto). + * If the option `--noseedbackup` is used, then the default passphrase + `hello` is used to encrypt the root key. + +## Generated macaroons + +With the root key set up, `lnd` continues with creating three macaroon files: + +* `invoice.macaroon`: Grants read and write access to all invoice related gRPC + commands (like generating an address or adding an invoice). Can be used for a + web shop application for example. Paying an invoice is not possible, even if + the name might suggest it. The permission `offchain` is needed to pay an + invoice which is currently only granted in the admin macaroon. +* `readonly.macaroon`: Grants read-only access to all gRPC commands. Could be + given to a monitoring application for example. +* `admin.macaroon`: Grants full read and write access to all gRPC commands. + This is used by the `lncli` client. + +These three macaroons all have the location field set to `lnd` and have no +conditions/first party caveats or third party caveats set. + +The access restrictions are implemented with a list of entity/action pairs that +is mapped to the gRPC functions by the `rpcserver.go`. +For example, the permissions for the `invoice.macaroon` looks like this: + +```go + // invoicePermissions is a slice of all the entities that allows a user + // to only access calls that are related to invoices, so: streaming + // RPCs, generating, and listening invoices. + invoicePermissions = []bakery.Op{ + { + Entity: "invoices", + Action: "read", + }, + { + Entity: "invoices", + Action: "write", + }, + { + Entity: "address", + Action: "read", + }, + { + Entity: "address", + Action: "write", + }, + } +``` + +## Constraints / First party caveats + +There are currently two constraints implemented that can be used by `lncli` to +restrict the macaroon it uses to communicate with the gRPC interface. These can +be found in `constraints.go`: + +* `TimeoutConstraint`: Set a timeout in seconds after which the macaroon is no + longer valid. + This constraint can be set by adding the parameter `--macaroontimeout xy` to + the `lncli` command. +* `IPLockConstraint`: Locks the macaroon to a specific IP address. + This constraint can be set by adding the parameter `--macaroonip a.b.c.d` to + the `lncli` command. + +## Bakery + +As of lnd `v0.9.0-beta` there is a macaroon bakery available through gRPC and +command line. +Users can create their own macaroons with custom permissions if the provided +default macaroons (`admin`, `invoice` and `readonly`) are not sufficient. + +For example, a macaroon that is only allowed to manage peers with a default root +key `0` would be created with the following command: + +```shell +$ lncli bakemacaroon peers:read peers:write +``` + +For even more fine-grained permission control, it is also possible to specify +single RPC method URIs that are allowed to be accessed by a macaroon. This can +be achieved by passing `uri:` pairs to `bakemacaroon`, for example: + +```shell +$ lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion +``` + +The macaroon created by this call would only be allowed to call the `GetInfo` and +`GetVersion` methods instead of all methods that have similar permissions (like +`info:read` for example). + +A full list of available entity/action pairs and RPC method URIs can be queried +by using the `lncli listpermissions` command. + +### Upgrading from v0.8.0-beta or earlier + +Users upgrading from a version prior to `v0.9.0-beta` might get a `permission +denied ` error when trying to use the `lncli bakemacaroon` command. +This is because the bakery requires a new permission (`macaroon/generate`) to +access. +Users can obtain a new `admin.macaroon` that contains this permission by +removing all three default macaroons (`admin.macaroon`, `invoice.macaroon` and +`readonly.macaroon`, **NOT** the `macaroons.db`!) from their +`data/chain///` directory inside the lnd data directory and +restarting lnd. + + +## Root key rotation + +To manage the root keys used by macaroons, there are `listmacaroonids` and +`deletemacaroonid` available through gPRC and command line. +Users can view a list of all macaroon root key IDs that are in use using: + +```shell +$ lncli listmacaroonids +``` + +And remove a specific macaroon root key ID using command: + +```shell +$ lncli deletemacaroonid root_key_id +``` + +Be careful with the `deletemacaroonid` command as when a root key is deleted, +**all the macaroons created from it are invalidated**. diff --git a/server/pkg/macaroons/auth.go b/server/pkg/macaroons/auth.go new file mode 100644 index 0000000..72a130d --- /dev/null +++ b/server/pkg/macaroons/auth.go @@ -0,0 +1,53 @@ +package macaroons + +import ( + "context" + "encoding/hex" + + macaroon "gopkg.in/macaroon.v2" +) + +// MacaroonCredential wraps a macaroon to implement the +// credentials.PerRPCCredentials interface. +type MacaroonCredential struct { + *macaroon.Macaroon +} + +// RequireTransportSecurity implements the PerRPCCredentials interface. +func (m MacaroonCredential) RequireTransportSecurity() bool { + return true +} + +// GetRequestMetadata implements the PerRPCCredentials interface. This method +// is required in order to pass the wrapped macaroon into the gRPC context. +// With this, the macaroon will be available within the request handling scope +// of the ultimate gRPC server implementation. +func (m MacaroonCredential) GetRequestMetadata(ctx context.Context, + uri ...string) (map[string]string, error) { + + macBytes, err := m.MarshalBinary() + if err != nil { + return nil, err + } + + md := make(map[string]string) + md["macaroon"] = hex.EncodeToString(macBytes) + return md, nil +} + +// NewMacaroonCredential returns a copy of the passed macaroon wrapped in a +// MacaroonCredential struct which implements PerRPCCredentials. +func NewMacaroonCredential(m *macaroon.Macaroon) (MacaroonCredential, error) { + ms := MacaroonCredential{} + + // The macaroon library's Clone() method has a subtle bug that doesn't + // correctly clone all caveats. We need to use our own, safe clone + // function instead. + var err error + ms.Macaroon, err = SafeCopyMacaroon(m) + if err != nil { + return ms, err + } + + return ms, nil +} diff --git a/server/pkg/macaroons/constraints.go b/server/pkg/macaroons/constraints.go new file mode 100644 index 0000000..642f8ed --- /dev/null +++ b/server/pkg/macaroons/constraints.go @@ -0,0 +1,250 @@ +package macaroons + +import ( + "bytes" + "context" + "fmt" + "net" + "strings" + "time" + + "google.golang.org/grpc/peer" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + macaroon "gopkg.in/macaroon.v2" +) + +const ( + // CondLndCustom is the first party caveat condition name that is used + // for all custom caveats in lnd. Every custom caveat entry will be + // encoded as the string + // "lnd-custom " + // in the serialized macaroon. We choose a single space as the delimiter + // between the because that is also used by the macaroon bakery library. + CondLndCustom = "lnd-custom" +) + +// CustomCaveatAcceptor is an interface that contains a single method for +// checking whether a macaroon with the given custom caveat name should be +// accepted or not. +type CustomCaveatAcceptor interface { + // CustomCaveatSupported returns nil if a macaroon with the given custom + // caveat name can be validated by any component in lnd (for example an + // RPC middleware). If no component is registered to handle the given + // custom caveat then an error must be returned. This method only checks + // the availability of a validating component, not the validity of the + // macaroon itself. + CustomCaveatSupported(customCaveatName string) error +} + +// Constraint type adds a layer of indirection over macaroon caveats. +type Constraint func(*macaroon.Macaroon) error + +// Checker type adds a layer of indirection over macaroon checkers. A Checker +// returns the name of the checker and the checker function; these are used to +// register the function with the bakery service's compound checker. +type Checker func() (string, checkers.Func) + +// AddConstraints returns new derived macaroon by applying every passed +// constraint and tightening its restrictions. +func AddConstraints(mac *macaroon.Macaroon, + cs ...Constraint) (*macaroon.Macaroon, error) { + + // The macaroon library's Clone() method has a subtle bug that doesn't + // correctly clone all caveats. We need to use our own, safe clone + // function instead. + newMac, err := SafeCopyMacaroon(mac) + if err != nil { + return nil, err + } + + for _, constraint := range cs { + if err := constraint(newMac); err != nil { + return nil, err + } + } + return newMac, nil +} + +// Each *Constraint function is a functional option, which takes a pointer +// to the macaroon and adds another restriction to it. For each *Constraint, +// the corresponding *Checker is provided if not provided by default. + +// TimeoutConstraint restricts the lifetime of the macaroon +// to the amount of seconds given. +func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + macaroonTimeout := time.Duration(seconds) + requestTimeout := time.Now().Add(time.Second * macaroonTimeout) + caveat := checkers.TimeBeforeCaveat(requestTimeout) + return mac.AddFirstPartyCaveat([]byte(caveat.Condition)) + } +} + +// IPLockConstraint locks macaroon to a specific IP address. +// If address is an empty string, this constraint does nothing to +// accommodate default value's desired behavior. +func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + if ipAddr != "" { + macaroonIPAddr := net.ParseIP(ipAddr) + if macaroonIPAddr == nil { + return fmt.Errorf("incorrect macaroon IP-" + + "lock address") + } + caveat := checkers.Condition("ipaddr", + macaroonIPAddr.String()) + return mac.AddFirstPartyCaveat([]byte(caveat)) + } + return nil + } +} + +// IPLockChecker accepts client IP from the validation context and compares it +// with IP locked in the macaroon. It is of the `Checker` type. +func IPLockChecker() (string, checkers.Func) { + return "ipaddr", func(ctx context.Context, cond, arg string) error { + // Get peer info and extract IP address from it for macaroon + // check. + pr, ok := peer.FromContext(ctx) + if !ok { + return fmt.Errorf("unable to get peer info from context") + } + peerAddr, _, err := net.SplitHostPort(pr.Addr.String()) + if err != nil { + return fmt.Errorf("unable to parse peer address") + } + + if !net.ParseIP(arg).Equal(net.ParseIP(peerAddr)) { + msg := "macaroon locked to different IP address" + return fmt.Errorf(msg) + } + return nil + } +} + +// CustomConstraint returns a function that adds a custom caveat condition to +// a macaroon. +func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + // We rely on a name being set for the interception, so don't + // allow creating a caveat without a name in the first place. + if name == "" { + return fmt.Errorf("name cannot be empty") + } + + // The inner (custom) condition is optional. + outerCondition := fmt.Sprintf("%s %s", name, condition) + if condition == "" { + outerCondition = name + } + + caveat := checkers.Condition(CondLndCustom, outerCondition) + return mac.AddFirstPartyCaveat([]byte(caveat)) + } +} + +// CustomChecker returns a Checker function that is used by the macaroon bakery +// library to check whether a custom caveat is supported by lnd in general or +// not. Support in this context means: An additional gRPC interceptor was set up +// that validates the content (=condition) of the custom caveat. If such an +// interceptor is in place then the acceptor should return a nil error. If no +// interceptor exists for the custom caveat in the macaroon of a request context +// then a non-nil error should be returned and the macaroon is rejected as a +// whole. +func CustomChecker(acceptor CustomCaveatAcceptor) Checker { + // We return the general name of all lnd custom macaroons and a function + // that splits the outer condition to extract the name of the custom + // condition and the condition itself. In the bakery library that's used + // here, a caveat always has the following form: + // + // + // + // Because a checker function needs to be bound to the condition name we + // have to choose a static name for the first part ("lnd-custom", see + // CondLndCustom. Otherwise we'd need to register a new Checker function + // for each custom caveat that's registered. To allow for a generic + // custom caveat handling, we just add another layer and expand the + // initial into + // + // " " + // + // The full caveat string entry of a macaroon that uses this generic + // mechanism would therefore look like this: + // + // "lnd-custom " + checker := func(_ context.Context, _, outerCondition string) error { + if outerCondition != strings.TrimSpace(outerCondition) { + return fmt.Errorf("unexpected white space found in " + + "caveat condition") + } + if outerCondition == "" { + return fmt.Errorf("expected custom caveat, got empty " + + "string") + } + + // The condition part of the original caveat is now name and + // condition of the custom caveat (we add a layer of conditions + // to allow one custom checker to work for all custom lnd + // conditions that implement arbitrary business logic). + parts := strings.Split(outerCondition, " ") + customCaveatName := parts[0] + + return acceptor.CustomCaveatSupported(customCaveatName) + } + + return func() (string, checkers.Func) { + return CondLndCustom, checker + } +} + +// HasCustomCaveat tests if the given macaroon has a custom caveat with the +// given custom caveat name. +func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool { + if mac == nil { + return false + } + + caveatPrefix := []byte(fmt.Sprintf( + "%s %s", CondLndCustom, customCaveatName, + )) + for _, caveat := range mac.Caveats() { + if bytes.HasPrefix(caveat.Id, caveatPrefix) { + return true + } + } + + return false +} + +// GetCustomCaveatCondition returns the custom caveat condition for the given +// custom caveat name from the given macaroon. +func GetCustomCaveatCondition(mac *macaroon.Macaroon, + customCaveatName string) string { + + if mac == nil { + return "" + } + + caveatPrefix := []byte(fmt.Sprintf( + "%s %s ", CondLndCustom, customCaveatName, + )) + for _, caveat := range mac.Caveats() { + // The caveat id has a format of + // "lnd-custom [custom-caveat-name] [custom-caveat-condition]" + // and we only want the condition part. If we match the prefix + // part we return the condition that comes after the prefix. + if bytes.HasPrefix(caveat.Id, caveatPrefix) { + caveatSplit := strings.SplitN( + string(caveat.Id), + string(caveatPrefix), + 2, + ) + if len(caveatSplit) == 2 { + return caveatSplit[1] + } + } + } + + // We didn't find a condition for the given custom caveat name. + return "" +} diff --git a/server/pkg/macaroons/constraints_test.go b/server/pkg/macaroons/constraints_test.go new file mode 100644 index 0000000..8136a1d --- /dev/null +++ b/server/pkg/macaroons/constraints_test.go @@ -0,0 +1,159 @@ +package macaroons_test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/ark-network/tools/macaroons" + "github.com/stretchr/testify/require" + macaroon "gopkg.in/macaroon.v2" +) + +var ( + testRootKey = []byte("dummyRootKey") + testID = []byte("dummyId") + testLocation = "lnd" + testVersion = macaroon.LatestVersion + expectedTimeCaveatSubstring = fmt.Sprintf("time-before %d", time.Now().Year()) +) + +func createDummyMacaroon(t *testing.T) *macaroon.Macaroon { + dummyMacaroon, err := macaroon.New( + testRootKey, testID, testLocation, testVersion, + ) + require.NoError(t, err, "Error creating initial macaroon") + return dummyMacaroon +} + +// TestAddConstraints tests that constraints can be added to an existing +// macaroon and therefore tighten its restrictions. +func TestAddConstraints(t *testing.T) { + t.Parallel() + + // We need a dummy macaroon to start with. Create one without + // a bakery, because we mock everything anyway. + initialMac := createDummyMacaroon(t) + + // Now add a constraint and make sure we have a cloned macaroon + // with the constraint applied instead of a mutated initial one. + newMac, err := macaroons.AddConstraints( + initialMac, macaroons.TimeoutConstraint(1), + ) + require.NoError(t, err, "Error adding constraint") + if &newMac == &initialMac { + t.Fatalf("Initial macaroon has been changed, something " + + "went wrong!") + } + + // Finally, test that the constraint has been added. + if len(initialMac.Caveats()) == len(newMac.Caveats()) { + t.Fatalf("No caveat has been added to the macaroon when " + + "constraint was applied") + } +} + +// TestTimeoutConstraint tests that a caveat for the lifetime of +// a macaroon is created. +func TestTimeoutConstraint(t *testing.T) { + t.Parallel() + + // Get a configured version of the constraint function. + constraintFunc := macaroons.TimeoutConstraint(3) + + // Now we need a dummy macaroon that we can apply the constraint + // function to. + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + require.NoError(t, err, "Error applying timeout constraint") + + // Finally, check that the created caveat has an + // acceptable value. + if !strings.HasPrefix( + string(testMacaroon.Caveats()[0].Id), + expectedTimeCaveatSubstring, + ) { + + t.Fatalf("Added caveat '%s' does not meet the expectations!", + testMacaroon.Caveats()[0].Id) + } +} + +// TestTimeoutConstraint tests that a caveat for the lifetime of +// a macaroon is created. +func TestIpLockConstraint(t *testing.T) { + t.Parallel() + + // Get a configured version of the constraint function. + constraintFunc := macaroons.IPLockConstraint("127.0.0.1") + + // Now we need a dummy macaroon that we can apply the constraint + // function to. + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + require.NoError(t, err, "Error applying timeout constraint") + + // Finally, check that the created caveat has an + // acceptable value. + if string(testMacaroon.Caveats()[0].Id) != "ipaddr 127.0.0.1" { + t.Fatalf("Added caveat '%s' does not meet the expectations!", + testMacaroon.Caveats()[0].Id) + } +} + +// TestIPLockBadIP tests that an IP constraint cannot be added if the +// provided string is not a valid IP address. +func TestIPLockBadIP(t *testing.T) { + t.Parallel() + + constraintFunc := macaroons.IPLockConstraint("127.0.0/800") + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + if err == nil { + t.Fatalf("IPLockConstraint with bad IP should fail.") + } +} + +// TestCustomConstraint tests that a custom constraint with a name and value can +// be added to a macaroon. +func TestCustomConstraint(t *testing.T) { + t.Parallel() + + // Test a custom caveat with a value first. + constraintFunc := macaroons.CustomConstraint("unit-test", "test-value") + testMacaroon := createDummyMacaroon(t) + require.NoError(t, constraintFunc(testMacaroon)) + + require.Equal( + t, []byte("lnd-custom unit-test test-value"), + testMacaroon.Caveats()[0].Id, + ) + require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) + require.False(t, macaroons.HasCustomCaveat(nil, "foo")) + + customCaveatCondition := macaroons.GetCustomCaveatCondition( + testMacaroon, "unit-test", + ) + require.Equal(t, customCaveatCondition, "test-value") + + // Custom caveats don't necessarily need a value, just the name is fine + // too to create a tagged macaroon. + constraintFunc = macaroons.CustomConstraint("unit-test", "") + testMacaroon = createDummyMacaroon(t) + require.NoError(t, constraintFunc(testMacaroon)) + + require.Equal( + t, []byte("lnd-custom unit-test"), testMacaroon.Caveats()[0].Id, + ) + require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) + + customCaveatCondition = macaroons.GetCustomCaveatCondition( + testMacaroon, "unit-test", + ) + require.Equal(t, customCaveatCondition, "") +} diff --git a/server/pkg/macaroons/context.go b/server/pkg/macaroons/context.go new file mode 100644 index 0000000..c73b084 --- /dev/null +++ b/server/pkg/macaroons/context.go @@ -0,0 +1,44 @@ +package macaroons + +import ( + "context" + "fmt" +) + +var ( + // RootKeyIDContextKey is the key to get rootKeyID from context. + RootKeyIDContextKey = contextKey{"rootkeyid"} + + // ErrContextRootKeyID is used when the supplied context doesn't have + // a root key ID. + ErrContextRootKeyID = fmt.Errorf("failed to read root key ID " + + "from context") +) + +// contextKey is the type we use to identify values in the context. +type contextKey struct { + Name string +} + +// ContextWithRootKeyID passes the root key ID value to context. +func ContextWithRootKeyID(ctx context.Context, + value interface{}) context.Context { + + return context.WithValue(ctx, RootKeyIDContextKey, value) +} + +// RootKeyIDFromContext retrieves the root key ID from context using the key +// RootKeyIDContextKey. +func RootKeyIDFromContext(ctx context.Context) ([]byte, error) { + id, ok := ctx.Value(RootKeyIDContextKey).([]byte) + if !ok { + return nil, ErrContextRootKeyID + } + + // Check that the id is not empty. + if len(id) == 0 { + return nil, ErrMissingRootKeyID + } + + return id, nil +} diff --git a/server/pkg/macaroons/go.mod b/server/pkg/macaroons/go.mod new file mode 100644 index 0000000..3367f7f --- /dev/null +++ b/server/pkg/macaroons/go.mod @@ -0,0 +1,99 @@ +module github.com/ark-network/tools/macaroons + +go 1.22.4 + +replace github.com/ark-network/tools/kvdb => ../kvdb + +require ( + github.com/ark-network/tools/kvdb v0.0.0-00010101000000-000000000000 + github.com/btcsuite/btcwallet v0.16.9 + github.com/btcsuite/btcwallet/walletdb v1.4.2 + github.com/stretchr/testify v1.9.0 + google.golang.org/grpc v1.65.0 + gopkg.in/macaroon-bakery.v2 v2.3.0 + gopkg.in/macaroon.v2 v2.1.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd v0.23.4 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.2 // indirect + github.com/btcsuite/btcd/btcutil v1.1.3 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/lightninglabs/neutrino/cache v1.1.0 // indirect + github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect + github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect + github.com/lightningnetwork/lnd/tlv v1.0.2 // indirect + github.com/lightningnetwork/lnd/tor v1.0.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/miekg/dns v1.1.43 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.11.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.etcd.io/etcd/api/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/v2 v2.305.15 // indirect + go.etcd.io/etcd/client/v3 v3.5.15 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/raft/v3 v3.5.15 // indirect + go.etcd.io/etcd/server/v3 v3.5.15 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect + go.opentelemetry.io/otel v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.20.0 // indirect + go.opentelemetry.io/otel/sdk v1.20.0 // indirect + go.opentelemetry.io/otel/trace v1.20.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/errgo.v1 v1.0.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/server/pkg/macaroons/go.sum b/server/pkg/macaroons/go.sum new file mode 100644 index 0000000..d7c6c35 --- /dev/null +++ b/server/pkg/macaroons/go.sum @@ -0,0 +1,492 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= +github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.2.2 h1:5uxe5YjoCq+JeOpg0gZSNHuFgeogrocBYxvg6w9sAgc= +github.com/btcsuite/btcd/btcec/v2 v2.2.2/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.16.9 h1:hLAzEJvsiSn+r6j374G7ThnrYD/toa+Lv7l1Rm6+0oM= +github.com/btcsuite/btcwallet v0.16.9/go.mod h1:T3DjEAMZYIqQ28l+ixlB6DX4mFJXCX8Pzz+yACQcLsc= +github.com/btcsuite/btcwallet/walletdb v1.4.2 h1:zwZZ+zaHo4mK+FAN6KeK85S3oOm+92x2avsHvFAhVBE= +github.com/btcsuite/btcwallet/walletdb v1.4.2/go.mod h1:7ZQ+BvOEre90YT7eSq8bLoxTsgXidUzA/mqbRS114CQ= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= +github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= +github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= +github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl2uZhryo= +github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= +github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightninglabs/neutrino/cache v1.1.0 h1:szZIhVabiQIsGzJjhvo76sj05Au+zVotj2M34EquGME= +github.com/lightninglabs/neutrino/cache v1.1.0/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= +github.com/lightningnetwork/lnd/healthcheck v1.2.5 h1:aTJy5xeBpcWgRtW/PGBDe+LMQEmNm/HQewlQx2jt7OA= +github.com/lightningnetwork/lnd/healthcheck v1.2.5/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= +github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4= +github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk= +github.com/lightningnetwork/lnd/tlv v1.0.2 h1:LG7H3Uw/mHYGnEeHRPg+STavAH+UsFvuBflD0PzcYFQ= +github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4= +github.com/lightningnetwork/lnd/tor v1.0.0 h1:wvEc7I+Y7IOtPglVP3cVBbYhiVhc7uTd7cMF9gQRzwA= +github.com/lightningnetwork/lnd/tor v1.0.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v2 v2.305.15 h1:VG2xbf8Vz1KJh65Ar2V5eDmfkp1bpzkSEHlhJM3usp8= +go.etcd.io/etcd/client/v2 v2.305.15/go.mod h1:Ad5dRjPVb/n5yXgAWQ/hXzuXXkBk0Y658ocuXYaUU48= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.etcd.io/etcd/pkg/v3 v3.5.15 h1:/Iu6Sr3iYaAjy++8sIDoZW9/EfhcwLZwd4FOZX2mMOU= +go.etcd.io/etcd/pkg/v3 v3.5.15/go.mod h1:e3Acf298sPFmTCGTrnGvkClEw9RYIyPtNzi1XM8rets= +go.etcd.io/etcd/raft/v3 v3.5.15 h1:jOA2HJF7zb3wy8H/pL13e8geWqkEa/kUs0waUggZC0I= +go.etcd.io/etcd/raft/v3 v3.5.15/go.mod h1:k3r7P4seEiUcgxOPLp+mloJWV3Q4QLPGNvy/OgC8OtM= +go.etcd.io/etcd/server/v3 v3.5.15 h1:x35jrWnZgsRwMsFsUJIUdT1bvzIz1B+29HjMfRYVN/E= +go.etcd.io/etcd/server/v3 v3.5.15/go.mod h1:l9jX9oa/iuArjqz0RNX/TDbc70dLXxRZo/nmPucrpFo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= +go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= +gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= +gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= +gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic= +gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc= +gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= +gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/pkg/macaroons/security.go b/server/pkg/macaroons/security.go new file mode 100644 index 0000000..e4e3ffb --- /dev/null +++ b/server/pkg/macaroons/security.go @@ -0,0 +1,11 @@ +package macaroons + +import "github.com/btcsuite/btcwallet/snacl" + +var ( + // Below are the default scrypt parameters that are used when creating + // the encryption key for the macaroon database with snacl.NewSecretKey. + scryptN = snacl.DefaultN + scryptR = snacl.DefaultR + scryptP = snacl.DefaultP +) diff --git a/server/pkg/macaroons/security_integration.go b/server/pkg/macaroons/security_integration.go new file mode 100644 index 0000000..f7d039f --- /dev/null +++ b/server/pkg/macaroons/security_integration.go @@ -0,0 +1,14 @@ +//go:build integration + +package macaroons + +import "github.com/btcsuite/btcwallet/waddrmgr" + +func init() { + // Below are the reduced scrypt parameters that are used when creating + // the encryption key for the macaroon database with snacl.NewSecretKey. + // We use very low values for our itest/rpctest to speed things up. + scryptN = waddrmgr.FastScryptOptions.N + scryptR = waddrmgr.FastScryptOptions.R + scryptP = waddrmgr.FastScryptOptions.P +} diff --git a/server/pkg/macaroons/security_test.go b/server/pkg/macaroons/security_test.go new file mode 100644 index 0000000..48c18e3 --- /dev/null +++ b/server/pkg/macaroons/security_test.go @@ -0,0 +1,12 @@ +package macaroons + +import "github.com/btcsuite/btcwallet/waddrmgr" + +func init() { + // Below are the reduced scrypt parameters that are used when creating + // the encryption key for the macaroon database with snacl.NewSecretKey. + // We use very low values for our itest/rpctest to speed things up. + scryptN = waddrmgr.FastScryptOptions.N + scryptR = waddrmgr.FastScryptOptions.R + scryptP = waddrmgr.FastScryptOptions.P +} diff --git a/server/pkg/macaroons/service.go b/server/pkg/macaroons/service.go new file mode 100644 index 0000000..c1dabac --- /dev/null +++ b/server/pkg/macaroons/service.go @@ -0,0 +1,399 @@ +package macaroons + +import ( + "context" + "encoding/hex" + "fmt" + + "google.golang.org/grpc/metadata" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + macaroon "gopkg.in/macaroon.v2" +) + +var ( + // ErrMissingRootKeyID specifies the root key ID is missing. + ErrMissingRootKeyID = fmt.Errorf("missing root key ID") + + // ErrDeletionForbidden is used when attempting to delete the + // DefaultRootKeyID or the encryptedKeyID. + ErrDeletionForbidden = fmt.Errorf("the specified ID cannot be deleted") + + // PermissionEntityCustomURI is a special entity name for a permission + // that does not describe an entity:action pair but instead specifies a + // specific URI that needs to be granted access to. This can be used for + // more fine-grained permissions where a macaroon only grants access to + // certain methods instead of a whole list of methods that define the + // same entity:action pairs. For example: uri:/lnrpc.Lightning/GetInfo + // only gives access to the GetInfo call. + PermissionEntityCustomURI = "uri" + + // ErrUnknownVersion is returned when a macaroon is of an unknown + // is presented. + ErrUnknownVersion = fmt.Errorf("unknown macaroon version") + + // ErrInvalidID is returned when a macaroon ID is invalid. + ErrInvalidID = fmt.Errorf("invalid ID") +) + +// MacaroonValidator is an interface type that can check if macaroons are valid. +type MacaroonValidator interface { + // ValidateMacaroon extracts the macaroon from the context's gRPC + // metadata, checks its signature, makes sure all specified permissions + // for the called method are contained within and finally ensures all + // caveat conditions are met. A non-nil error is returned if any of the + // checks fail. + ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, fullMethod string) error +} + +// ExtendedRootKeyStore is an interface augments the existing +// macaroons.RootKeyStorage interface by adding a number of additional utility +// methods such as encrypting and decrypting the root key given a password. +type ExtendedRootKeyStore interface { + bakery.RootKeyStore + + // Close closes the RKS and zeros out any in-memory encryption keys. + Close() error + + // CreateUnlock calls the underlying root key store's CreateUnlock and + // returns the result. + CreateUnlock(password *[]byte) error + + // ListMacaroonIDs returns all the root key ID values except the value + // of encryptedKeyID. + ListMacaroonIDs(ctxt context.Context) ([][]byte, error) + + // DeleteMacaroonID removes one specific root key ID. If the root key + // ID is found and deleted, it will be returned. + DeleteMacaroonID(ctxt context.Context, rootKeyID []byte) ([]byte, error) + + // ChangePassword calls the underlying root key store's ChangePassword + // and returns the result. + ChangePassword(oldPw, newPw []byte) error + + // GenerateNewRootKey calls the underlying root key store's + // GenerateNewRootKey and returns the result. + GenerateNewRootKey() error + + // SetRootKey calls the underlying root key store's SetRootKey and + // returns the result. + SetRootKey(rootKey []byte) error +} + +// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the +// root key service encryption keys, as well as utility methods to validate a +// macaroon against the bakery and gRPC middleware for macaroon-based auth. +type Service struct { + bakery.Bakery + + rks bakery.RootKeyStore + + // ExternalValidators is a map between an absolute gRPC URIs and the + // corresponding external macaroon validator to be used for that URI. + // If no external validator for an URI is specified, the service will + // use the internal validator. + ExternalValidators map[string]MacaroonValidator + + // StatelessInit denotes if the service was initialized in the stateless + // mode where no macaroon files should be created on disk. + StatelessInit bool +} + +// NewService returns a service backed by the macaroon DB backend. The `checks` +// argument can be any of the `Checker` type functions defined in this package, +// or a custom checker if desired. This constructor prevents double-registration +// of checkers to prevent panics, so listing the same checker more than once is +// not harmful. Default checkers, such as those for `allow`, `time-before`, +// `declared`, and `error` caveats are registered automatically and don't need +// to be added. +func NewService(keyStore bakery.RootKeyStore, location string, + statelessInit bool, checks ...Checker) (*Service, error) { + + macaroonParams := bakery.BakeryParams{ + Location: location, + RootKeyStore: keyStore, + // No third-party caveat support for now. + // TODO(aakselrod): Add third-party caveat support. + Locator: nil, + Key: nil, + } + + svc := bakery.New(macaroonParams) + + // Register all custom caveat checkers with the bakery's checker. + // TODO(aakselrod): Add more checks as required. + checker := svc.Checker.FirstPartyCaveatChecker.(*checkers.Checker) + for _, check := range checks { + cond, fun := check() + if !isRegistered(checker, cond) { + checker.Register(cond, "std", fun) + } + } + + return &Service{ + Bakery: *svc, + rks: keyStore, + ExternalValidators: make(map[string]MacaroonValidator), + StatelessInit: statelessInit, + }, nil +} + +// isRegistered checks to see if the required checker has already been +// registered in order to avoid a panic caused by double registration. +func isRegistered(c *checkers.Checker, name string) bool { + if c == nil { + return false + } + + for _, info := range c.Info() { + if info.Name == name && + info.Prefix == "" && + info.Namespace == "std" { + return true + } + } + + return false +} + +// RegisterExternalValidator registers a custom, external macaroon validator for +// the specified absolute gRPC URI. That validator is then fully responsible to +// make sure any macaroon passed for a request to that URI is valid and +// satisfies all conditions. +func (svc *Service) RegisterExternalValidator(fullMethod string, + validator MacaroonValidator) error { + + if validator == nil { + return fmt.Errorf("validator cannot be nil") + } + + _, ok := svc.ExternalValidators[fullMethod] + if ok { + return fmt.Errorf("external validator for method %s already "+ + "registered", fullMethod) + } + + svc.ExternalValidators[fullMethod] = validator + return nil +} + +// ValidateMacaroon validates the capabilities of a given request given a +// bakery service, context, and uri. Within the passed context.Context, we +// expect a macaroon to be encoded as request metadata using the key +// "macaroon". +func (svc *Service) ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, fullMethod string) error { + + // Get macaroon bytes from context and unmarshal into macaroon. + macHex, err := RawMacaroonFromContext(ctx) + if err != nil { + return err + } + + // With the macaroon obtained, we'll now decode the hex-string encoding. + macBytes, err := hex.DecodeString(macHex) + if err != nil { + return err + } + + return svc.CheckMacAuth( + ctx, macBytes, requiredPermissions, fullMethod, + ) +} + +// CheckMacAuth checks that the macaroon is not disobeying any caveats and is +// authorized to perform the operation the user wants to perform. +func (svc *Service) CheckMacAuth(ctx context.Context, macBytes []byte, + requiredPermissions []bakery.Op, fullMethod string) error { + + // With the macaroon obtained, we'll now unmarshal it from binary into + // its concrete struct representation. + mac := &macaroon.Macaroon{} + err := mac.UnmarshalBinary(macBytes) + if err != nil { + return err + } + + // Ensure that the macaroon is using the exact same version as we + // expect. In the future, we can relax this check to phase in new + // versions. + if mac.Version() != macaroon.V2 { + return fmt.Errorf("%w: %v", ErrUnknownVersion, + mac.Version()) + } + + // Run a similar version check on the ID used for the macaroon as well. + const minIDLength = 1 + if len(mac.Id()) < minIDLength { + return ErrInvalidID + } + if mac.Id()[0] != byte(bakery.Version3) { + return ErrInvalidID + } + + // Check the method being called against the permitted operation, the + // expiration time and IP address and return the result. + authChecker := svc.Checker.Auth(macaroon.Slice{mac}) + _, err = authChecker.Allow(ctx, requiredPermissions...) + + // If the macaroon contains broad permissions and checks out, we're + // done. + if err == nil { + return nil + } + + // To also allow the special permission of "uri:" to be a + // valid permission, we need to check it manually in case there is no + // broader scope permission defined. + _, err = authChecker.Allow(ctx, bakery.Op{ + Entity: PermissionEntityCustomURI, + Action: fullMethod, + }) + return err +} + +// Close closes the database that underlies the RootKeyStore and zeroes the +// encryption keys. +func (svc *Service) Close() error { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.Close() + } + + return nil +} + +// CreateUnlock calls the underlying root key store's CreateUnlock and returns +// the result. +func (svc *Service) CreateUnlock(password *[]byte) error { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.CreateUnlock(password) + } + + return nil +} + +// NewMacaroon wraps around the function Oven.NewMacaroon with the defaults, +// - version is always bakery.LatestVersion; +// - caveats is always nil. +// +// In addition, it takes a rootKeyID parameter, and puts it into the context. +// The context is passed through Oven.NewMacaroon(), in which calls the function +// RootKey(), that reads the context for rootKeyID. +func (svc *Service) NewMacaroon( + ctx context.Context, rootKeyID []byte, + ops ...bakery.Op) (*bakery.Macaroon, error) { + + // Check rootKeyID is not called with nil or empty bytes. We want the + // caller to be aware the value of root key ID used, so we won't replace + // it with the DefaultRootKeyID if not specified. + if len(rootKeyID) == 0 { + return nil, ErrMissingRootKeyID + } + + // Pass the root key ID to context. + ctx = ContextWithRootKeyID(ctx, rootKeyID) + + return svc.Oven.NewMacaroon(ctx, bakery.LatestVersion, nil, ops...) +} + +// ListMacaroonIDs returns all the root key ID values except the value of +// encryptedKeyID. +func (svc *Service) ListMacaroonIDs(ctxt context.Context) ([][]byte, error) { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.ListMacaroonIDs(ctxt) + } + + return nil, nil +} + +// DeleteMacaroonID removes one specific root key ID. If the root key ID is +// found and deleted, it will be returned. +func (svc *Service) DeleteMacaroonID(ctxt context.Context, + rootKeyID []byte) ([]byte, error) { + + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.DeleteMacaroonID(ctxt, rootKeyID) + } + + return nil, nil +} + +// GenerateNewRootKey calls the underlying root key store's GenerateNewRootKey +// and returns the result. +func (svc *Service) GenerateNewRootKey() error { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.GenerateNewRootKey() + } + + return nil +} + +// SetRootKey calls the underlying root key store's SetRootKey and returns the +// result. +func (svc *Service) SetRootKey(rootKey []byte) error { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.SetRootKey(rootKey) + } + + return nil +} + +// ChangePassword calls the underlying root key store's ChangePassword and +// returns the result. +func (svc *Service) ChangePassword(oldPw, newPw []byte) error { + if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok { + return boltRKS.ChangePassword(oldPw, newPw) + } + + return nil +} + +// bakeMacaroon creates a new macaroon with newest version and the given +// permissions then returns it binary serialized. +func (svc *Service) BakeMacaroon( + ctx context.Context, permissions []bakery.Op, +) ([]byte, error) { + mac, err := svc.NewMacaroon( + ctx, DefaultRootKeyID, permissions..., + ) + if err != nil { + return nil, err + } + + return mac.M().MarshalBinary() +} + +// RawMacaroonFromContext is a helper function that extracts a raw macaroon +// from the given incoming gRPC request context. +func RawMacaroonFromContext(ctx context.Context) (string, error) { + // Get macaroon bytes from context and unmarshal into macaroon. + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", fmt.Errorf("unable to get metadata from context") + } + if len(md["macaroon"]) != 1 { + return "", fmt.Errorf("expected 1 macaroon, got %d", + len(md["macaroon"])) + } + + return md["macaroon"][0], nil +} + +// SafeCopyMacaroon creates a copy of a macaroon that is safe to be used and +// modified. This is necessary because the macaroon library's own Clone() method +// is unsafe for certain edge cases, resulting in both the cloned and the +// original macaroons to be modified. +func SafeCopyMacaroon(mac *macaroon.Macaroon) (*macaroon.Macaroon, error) { + macBytes, err := mac.MarshalBinary() + if err != nil { + return nil, err + } + + newMac := &macaroon.Macaroon{} + if err := newMac.UnmarshalBinary(macBytes); err != nil { + return nil, err + } + + return newMac, nil +} diff --git a/server/pkg/macaroons/service_test.go b/server/pkg/macaroons/service_test.go new file mode 100644 index 0000000..8442376 --- /dev/null +++ b/server/pkg/macaroons/service_test.go @@ -0,0 +1,362 @@ +package macaroons_test + +import ( + "context" + "encoding/hex" + "path" + "testing" + + "github.com/ark-network/tools/kvdb" + "github.com/ark-network/tools/macaroons" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + macaroon "gopkg.in/macaroon.v2" +) + +var ( + testOperation = bakery.Op{ + Entity: "testEntity", + Action: "read", + } + testOperationURI = bakery.Op{ + Entity: macaroons.PermissionEntityCustomURI, + Action: "SomeMethod", + } + defaultPw = []byte("hello") +) + +// setupTestRootKeyStorage creates a dummy root key storage by +// creating a temporary macaroons.db and initializing it with the +// default password of 'hello'. Only the path to the temporary +// DB file is returned, because the service will open the file +// and read the store on its own. +func setupTestRootKeyStorage(t *testing.T) kvdb.Backend { + db, err := kvdb.Create( + kvdb.BoltBackendName, path.Join(t.TempDir(), "macaroons.db"), true, + kvdb.DefaultDBTimeout, + ) + require.NoError(t, err, "Error opening store DB") + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + store, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err, "Error creating root key store") + + err = store.CreateUnlock(&defaultPw) + require.NoError(t, store.Close()) + require.NoError(t, err, "error creating unlock") + + return db +} + +// TestNewService tests the creation of the macaroon service. +func TestNewService(t *testing.T) { + t.Parallel() + + // First, initialize a dummy DB file with a store that the service + // can read from. Make sure the file is removed in the end. + db := setupTestRootKeyStorage(t) + + rootKeyStore, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err) + + // Second, create the new service instance, unlock it and pass in a + // checker that we expect it to add to the bakery. + service, err := macaroons.NewService( + rootKeyStore, "lnd", false, macaroons.IPLockChecker, + ) + require.NoError(t, err, "Error creating new service") + defer service.Close() + err = service.CreateUnlock(&defaultPw) + require.NoError(t, err, "Error unlocking root key storage") + + // Third, check if the created service can bake macaroons. + _, err = service.NewMacaroon(context.TODO(), nil, testOperation) + if err != macaroons.ErrMissingRootKeyID { + t.Fatalf("Received %v instead of ErrMissingRootKeyID", err) + } + + macaroon, err := service.NewMacaroon( + context.TODO(), macaroons.DefaultRootKeyID, testOperation, + ) + require.NoError(t, err, "Error creating macaroon from service") + if macaroon.Namespace().String() != "std:" { + t.Fatalf("The created macaroon has an invalid namespace: %s", + macaroon.Namespace().String()) + } + + // Finally, check if the service has been initialized correctly and + // the checker has been added. + var checkerFound = false + checker := service.Checker.FirstPartyCaveatChecker.(*checkers.Checker) + for _, info := range checker.Info() { + if info.Name == "ipaddr" && + info.Prefix == "" && + info.Namespace == "std" { + checkerFound = true + } + } + if !checkerFound { + t.Fatalf("Checker '%s' not found in service.", "ipaddr") + } +} + +// TestValidateMacaroon tests the validation of a macaroon that is in an +// incoming context. +func TestValidateMacaroon(t *testing.T) { + t.Parallel() + + // First, initialize the service and unlock it. + db := setupTestRootKeyStorage(t) + rootKeyStore, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err) + service, err := macaroons.NewService( + rootKeyStore, "lnd", false, macaroons.IPLockChecker, + ) + require.NoError(t, err, "Error creating new service") + defer service.Close() + + err = service.CreateUnlock(&defaultPw) + require.NoError(t, err, "Error unlocking root key storage") + + // Then, create a new macaroon that we can serialize. + macaroon, err := service.NewMacaroon( + context.TODO(), macaroons.DefaultRootKeyID, testOperation, + testOperationURI, + ) + require.NoError(t, err, "Error creating macaroon from service") + macaroonBinary, err := macaroon.M().MarshalBinary() + require.NoError(t, err, "Error serializing macaroon") + + // Because the macaroons are always passed in a context, we need to + // mock one that has just the serialized macaroon as a value. + md := metadata.New(map[string]string{ + "macaroon": hex.EncodeToString(macaroonBinary), + }) + mockContext := metadata.NewIncomingContext(context.Background(), md) + + // Finally, validate the macaroon against the required permissions. + err = service.ValidateMacaroon( + mockContext, []bakery.Op{testOperation}, "FooMethod", + ) + require.NoError(t, err, "Error validating the macaroon") + + // If the macaroon has the method specific URI permission, the list of + // required entity/action pairs is irrelevant. + err = service.ValidateMacaroon( + mockContext, []bakery.Op{{Entity: "irrelevant"}}, "SomeMethod", + ) + require.NoError(t, err, "Error validating the macaroon") +} + +// TestListMacaroonIDs checks that ListMacaroonIDs returns the expected result. +func TestListMacaroonIDs(t *testing.T) { + t.Parallel() + + // First, initialize a dummy DB file with a store that the service + // can read from. Make sure the file is removed in the end. + db := setupTestRootKeyStorage(t) + + // Second, create the new service instance, unlock it and pass in a + // checker that we expect it to add to the bakery. + rootKeyStore, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err) + service, err := macaroons.NewService( + rootKeyStore, "lnd", false, macaroons.IPLockChecker, + ) + require.NoError(t, err, "Error creating new service") + defer service.Close() + + err = service.CreateUnlock(&defaultPw) + require.NoError(t, err, "Error unlocking root key storage") + + // Third, make 3 new macaroons with different root key IDs. + expectedIDs := [][]byte{{1}, {2}, {3}} + for _, v := range expectedIDs { + _, err := service.NewMacaroon(context.TODO(), v, testOperation) + require.NoError(t, err, "Error creating macaroon from service") + } + + // Finally, check that calling List return the expected values. + ids, _ := service.ListMacaroonIDs(context.TODO()) + require.Equal(t, expectedIDs, ids, "root key IDs mismatch") +} + +// TestDeleteMacaroonID removes the specific root key ID. +func TestDeleteMacaroonID(t *testing.T) { + t.Parallel() + + ctxb := context.Background() + + // First, initialize a dummy DB file with a store that the service + // can read from. Make sure the file is removed in the end. + db := setupTestRootKeyStorage(t) + + // Second, create the new service instance, unlock it and pass in a + // checker that we expect it to add to the bakery. + rootKeyStore, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err) + service, err := macaroons.NewService( + rootKeyStore, "lnd", false, macaroons.IPLockChecker, + ) + require.NoError(t, err, "Error creating new service") + defer service.Close() + + err = service.CreateUnlock(&defaultPw) + require.NoError(t, err, "Error unlocking root key storage") + + // Third, checks that removing encryptedKeyID returns an error. + encryptedKeyID := []byte("enckey") + _, err = service.DeleteMacaroonID(ctxb, encryptedKeyID) + require.Equal(t, macaroons.ErrDeletionForbidden, err) + + // Fourth, checks that removing DefaultKeyID returns an error. + _, err = service.DeleteMacaroonID(ctxb, macaroons.DefaultRootKeyID) + require.Equal(t, macaroons.ErrDeletionForbidden, err) + + // Fifth, checks that removing empty key id returns an error. + _, err = service.DeleteMacaroonID(ctxb, []byte{}) + require.Equal(t, macaroons.ErrMissingRootKeyID, err) + + // Sixth, checks that removing a non-existed key id returns nil. + nonExistedID := []byte("test-non-existed") + deletedID, err := service.DeleteMacaroonID(ctxb, nonExistedID) + require.NoError(t, err, "deleting macaroon ID got an error") + require.Nil(t, deletedID, "deleting non-existed ID should return nil") + + // Seventh, make 3 new macaroons with different root key IDs, and delete + // one. + expectedIDs := [][]byte{{1}, {2}, {3}} + for _, v := range expectedIDs { + _, err := service.NewMacaroon(ctxb, v, testOperation) + require.NoError(t, err, "Error creating macaroon from service") + } + deletedID, err = service.DeleteMacaroonID(ctxb, expectedIDs[0]) + require.NoError(t, err, "deleting macaroon ID got an error") + + // Finally, check that the ID is deleted. + require.Equal(t, expectedIDs[0], deletedID, "expected ID to be removed") + ids, _ := service.ListMacaroonIDs(ctxb) + require.Equal(t, expectedIDs[1:], ids, "root key IDs mismatch") +} + +// TestCloneMacaroons tests that macaroons can be cloned correctly and that +// modifications to the copy don't affect the original. +func TestCloneMacaroons(t *testing.T) { + t.Parallel() + + // Get a configured version of the constraint function. + constraintFunc := macaroons.TimeoutConstraint(3) + + // Now we need a dummy macaroon that we can apply the constraint + // function to. + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + require.NoError(t, err) + + // Check that the caveat has an empty location. + require.Equal( + t, "", testMacaroon.Caveats()[0].Location, + "expected caveat location to be empty, found: %s", + testMacaroon.Caveats()[0].Location, + ) + + // Make a copy of the macaroon. + newMacCred, err := macaroons.NewMacaroonCredential(testMacaroon) + require.NoError(t, err) + + newMac := newMacCred.Macaroon + require.Equal( + t, "", newMac.Caveats()[0].Location, + "expected new caveat location to be empty, found: %s", + newMac.Caveats()[0].Location, + ) + + // They should be deep equal as well. + testMacaroonBytes, err := testMacaroon.MarshalBinary() + require.NoError(t, err) + newMacBytes, err := newMac.MarshalBinary() + require.NoError(t, err) + require.Equal(t, testMacaroonBytes, newMacBytes) + + // Modify the caveat location on the old macaroon. + testMacaroon.Caveats()[0].Location = "mars" + + // The old macaroon's caveat location should be changed. + require.Equal( + t, "mars", testMacaroon.Caveats()[0].Location, + "expected caveat location to be empty, found: %s", + testMacaroon.Caveats()[0].Location, + ) + + // The new macaroon's caveat location should stay untouched. + require.Equal( + t, "", newMac.Caveats()[0].Location, + "expected new caveat location to be empty, found: %s", + newMac.Caveats()[0].Location, + ) +} + +// TestMacaroonVersionDecode tests that we'll reject macaroons with an unknown +// version. +func TestMacaroonVersionDecode(t *testing.T) { + t.Parallel() + + ctxb := context.Background() + + // First, initialize a dummy DB file with a store that the service + // can read from. Make sure the file is removed in the end. + db := setupTestRootKeyStorage(t) + + // Second, create the new service instance, unlock it and pass in a + // checker that we expect it to add to the bakery. + rootKeyStore, err := macaroons.NewRootKeyStorage(db) + require.NoError(t, err) + + service, err := macaroons.NewService( + rootKeyStore, "lnd", false, macaroons.IPLockChecker, + ) + require.NoError(t, err, "Error creating new service") + + defer service.Close() + + t.Run("invalid_version", func(t *testing.T) { + // Now that we have our sample service, we'll make a new custom + // macaroon with an unknown version. + testMac, err := macaroon.New( + testRootKey, testID, testLocation, 1, + ) + require.NoError(t, err) + + // Next, we'll serialize the macaroon to the binary form, + // modifying the first byte to signal an unknown version. + testMacBytes, err := testMac.MarshalBinary() + require.NoError(t, err) + + // If we attempt to check the mac auth, then we should get a + // failure that the version is unknown. + err = service.CheckMacAuth(ctxb, testMacBytes, nil, "") + require.ErrorIs(t, err, macaroons.ErrUnknownVersion) + }) + + t.Run("invalid_id", func(t *testing.T) { + // We'll now make a macaroon with a valid version, but modify + // the ID to be rejected. + badID := []byte{} + testMac, err := macaroon.New( + testRootKey, badID, testLocation, testVersion, + ) + require.NoError(t, err) + + testMacBytes, err := testMac.MarshalBinary() + require.NoError(t, err) + + // If we attempt to check the mac auth, then we should get a + // failure that the ID is bad. + err = service.CheckMacAuth(ctxb, testMacBytes, nil, "") + require.ErrorIs(t, err, macaroons.ErrInvalidID) + }) +} diff --git a/server/pkg/macaroons/store.go b/server/pkg/macaroons/store.go new file mode 100644 index 0000000..6cea314 --- /dev/null +++ b/server/pkg/macaroons/store.go @@ -0,0 +1,532 @@ +package macaroons + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "io" + "sync" + + "github.com/ark-network/tools/kvdb" + "github.com/btcsuite/btcwallet/snacl" + "github.com/btcsuite/btcwallet/walletdb" +) + +const ( + // RootKeyLen is the length of a root key. + RootKeyLen = 32 +) + +var ( + // rootKeyBucketName is the name of the root key store bucket. + rootKeyBucketName = []byte("macrootkeys") + + // DefaultRootKeyID is the ID of the default root key. The first is + // just 0, to emulate the memory storage that comes with bakery. + DefaultRootKeyID = []byte("0") + + // encryptionKeyID is the name of the database key that stores the + // encryption key, encrypted with a salted + hashed password. The + // format is 32 bytes of salt, and the rest is encrypted key. + encryptionKeyID = []byte("enckey") + + // ErrAlreadyUnlocked specifies that the store has already been + // unlocked. + ErrAlreadyUnlocked = fmt.Errorf("macaroon store already unlocked") + + // ErrStoreLocked specifies that the store needs to be unlocked with + // a password. + ErrStoreLocked = fmt.Errorf("macaroon store is locked") + + // ErrPasswordRequired specifies that a nil password has been passed. + ErrPasswordRequired = fmt.Errorf("a non-nil password is required") + + // ErrKeyValueForbidden is used when the root key ID uses encryptedKeyID as + // its value. + ErrKeyValueForbidden = fmt.Errorf("root key ID value is not allowed") + + // ErrRootKeyBucketNotFound specifies that there is no macaroon root key + // bucket yet which can/should only happen if the store has been + // corrupted or was initialized incorrectly. + ErrRootKeyBucketNotFound = fmt.Errorf("root key bucket not found") + + // ErrEncKeyNotFound specifies that there was no encryption key found + // even if one was expected to be generated. + ErrEncKeyNotFound = fmt.Errorf("macaroon encryption key not found") + + // ErrDefaultRootKeyNotFound is returned when the default root key is + // not found in the DB when it is expected to be. + ErrDefaultRootKeyNotFound = fmt.Errorf("default root key not found") +) + +// RootKeyStorage implements the bakery.RootKeyStorage interface. +type RootKeyStorage struct { + kvdb.Backend + + encKeyMtx sync.RWMutex + encKey *snacl.SecretKey +} + +// NewRootKeyStorage creates a RootKeyStorage instance. +func NewRootKeyStorage(db kvdb.Backend) (*RootKeyStorage, error) { + // If the store's bucket doesn't exist, create it. + err := kvdb.Update(db, func(tx kvdb.RwTx) error { + _, err := tx.CreateTopLevelBucket(rootKeyBucketName) + return err + }, func() {}) + if err != nil { + return nil, err + } + + // Return the DB wrapped in a RootKeyStorage object. + return &RootKeyStorage{ + Backend: db, + encKey: nil, + }, nil +} + +// CreateUnlock sets an encryption key if one is not already set, otherwise it +// checks if the password is correct for the stored encryption key. +func (r *RootKeyStorage) CreateUnlock(password *[]byte) error { + r.encKeyMtx.Lock() + defer r.encKeyMtx.Unlock() + + // Check if we've already unlocked the store; return an error if so. + if r.encKey != nil { + return ErrAlreadyUnlocked + } + + // Check if a nil password has been passed; return an error if so. + if password == nil { + return ErrPasswordRequired + } + + return kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(encryptionKeyID) + if len(dbKey) > 0 { + // We've already stored a key, so try to unlock with + // the password. + encKey := &snacl.SecretKey{} + err := encKey.Unmarshal(dbKey) + if err != nil { + return err + } + + err = encKey.DeriveKey(password) + if err != nil { + return err + } + + r.encKey = encKey + return nil + } + + // We haven't yet stored a key, so create a new one. + encKey, err := snacl.NewSecretKey( + password, scryptN, scryptR, scryptP, + ) + if err != nil { + return err + } + + err = bucket.Put(encryptionKeyID, encKey.Marshal()) + if err != nil { + return err + } + + r.encKey = encKey + return nil + }, func() {}) +} + +// ChangePassword decrypts all the macaroon root keys with the old password and +// then encrypts them again with the new password. +func (r *RootKeyStorage) ChangePassword(oldPw, newPw []byte) error { + // We need the store to already be unlocked. With this we can make sure + // that there already is a key in the DB. + if r.encKey == nil { + return ErrStoreLocked + } + + // Check if a nil password has been passed; return an error if so. + if oldPw == nil || newPw == nil { + return ErrPasswordRequired + } + + return kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + + // The encryption key must be present, otherwise we are in the + // wrong state to change the password. + encKeyDB := bucket.Get(encryptionKeyID) + if len(encKeyDB) == 0 { + return ErrEncKeyNotFound + } + + // Unmarshal parameters for old encryption key and derive the + // old key with them. + encKeyOld := &snacl.SecretKey{} + err := encKeyOld.Unmarshal(encKeyDB) + if err != nil { + return err + } + err = encKeyOld.DeriveKey(&oldPw) + if err != nil { + return err + } + + // Create a new encryption key from the new password. + encKeyNew, err := snacl.NewSecretKey( + &newPw, scryptN, scryptR, scryptP, + ) + if err != nil { + return err + } + + // foundDefaultRootKey is used to keep track of if we have + // found and re-encrypted the default root key so that we can + // return an error if it is not found. + var foundDefaultRootKey bool + err = bucket.ForEach(func(k, v []byte) error { + // Skip the key if it is the encryption key ID since + // we do not want to re-encrypt this. + if bytes.Equal(k, encryptionKeyID) { + return nil + } + + if bytes.Equal(k, DefaultRootKeyID) { + foundDefaultRootKey = true + } + + // Now try to decrypt the root key with the old + // encryption key, encrypt it with the new one and then + // store it in the DB. + decryptedKey, err := encKeyOld.Decrypt(v) + if err != nil { + return err + } + + encryptedKey, err := encKeyNew.Encrypt(decryptedKey) + if err != nil { + return err + } + + return bucket.Put(k, encryptedKey) + }) + if err != nil { + return err + } + + if !foundDefaultRootKey { + return ErrDefaultRootKeyNotFound + } + + // Finally, store the new encryption key parameters in the DB + // as well. + err = bucket.Put(encryptionKeyID, encKeyNew.Marshal()) + if err != nil { + return err + } + + r.encKey = encKeyNew + return nil + }, func() {}) +} + +// Get implements the Get method for the bakery.RootKeyStorage interface. +func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) { + r.encKeyMtx.RLock() + defer r.encKeyMtx.RUnlock() + + if r.encKey == nil { + return nil, ErrStoreLocked + } + var rootKey []byte + err := kvdb.View(r.Backend, func(tx kvdb.RTx) error { + bucket := tx.ReadBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(id) + if len(dbKey) == 0 { + return fmt.Errorf("root key with id %s doesn't exist", + string(id)) + } + + decKey, err := r.encKey.Decrypt(dbKey) + if err != nil { + return err + } + + rootKey = make([]byte, len(decKey)) + copy(rootKey[:], decKey) + return nil + }, func() { + rootKey = nil + }) + if err != nil { + return nil, err + } + + return rootKey, nil +} + +// RootKey implements the RootKey method for the bakery.RootKeyStorage +// interface. +func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) { + r.encKeyMtx.RLock() + defer r.encKeyMtx.RUnlock() + + if r.encKey == nil { + return nil, nil, ErrStoreLocked + } + var rootKey []byte + + // Read the root key ID from the context. If no key is specified in the + // context, an error will be returned. + id, err := RootKeyIDFromContext(ctx) + if err != nil { + return nil, nil, err + } + + if bytes.Equal(id, encryptionKeyID) { + return nil, nil, ErrKeyValueForbidden + } + + err = kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + dbKey := bucket.Get(id) + + // If there's a root key stored in the bucket, decrypt it and + // return it. + if len(dbKey) != 0 { + decKey, err := r.encKey.Decrypt(dbKey) + if err != nil { + return err + } + + rootKey = make([]byte, len(decKey)) + copy(rootKey[:], decKey[:]) + return nil + } + + // Otherwise, create a new root key, encrypt it, + // and store it in the bucket. + newKey, err := generateAndStoreNewRootKey(bucket, id, r.encKey) + rootKey = newKey + return err + }, func() { + rootKey = nil + }) + if err != nil { + return nil, nil, err + } + + return rootKey, id, nil +} + +// GenerateNewRootKey generates a new macaroon root key, replacing the previous +// root key if it existed. +func (r *RootKeyStorage) GenerateNewRootKey() error { + // We need the store to already be unlocked. With this we can make sure + // that there already is a key in the DB that can be replaced. + if r.encKey == nil { + return ErrStoreLocked + } + return kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + + // The default root key should be created even if it does not + // yet exist, so we do this separately from the rest of the + // root keys. + _, err := generateAndStoreNewRootKey( + bucket, DefaultRootKeyID, r.encKey, + ) + if err != nil { + return err + } + + // Now iterate over all the other root keys that may exist + // and re-generate each of them. + return bucket.ForEach(func(k, v []byte) error { + if bytes.Equal(k, encryptionKeyID) { + return nil + } + + if bytes.Equal(k, DefaultRootKeyID) { + return nil + } + + _, err := generateAndStoreNewRootKey( + bucket, k, r.encKey, + ) + + return err + }) + }, func() {}) +} + +// SetRootKey sets the default macaroon root key, replacing the previous root +// key if it existed. +func (r *RootKeyStorage) SetRootKey(rootKey []byte) error { + if r.encKey == nil { + return ErrStoreLocked + } + if len(rootKey) != RootKeyLen { + return fmt.Errorf("root key must be %v bytes", + RootKeyLen) + } + + encryptedKey, err := r.encKey.Encrypt(rootKey) + if err != nil { + return err + } + + return kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + if bucket == nil { + return ErrRootKeyBucketNotFound + } + + return bucket.Put(DefaultRootKeyID, encryptedKey) + }, func() {}) +} + +// Close closes the underlying database and zeroes the encryption key stored +// in memory. +func (r *RootKeyStorage) Close() error { + r.encKeyMtx.Lock() + defer r.encKeyMtx.Unlock() + + if r.encKey != nil { + r.encKey.Zero() + r.encKey = nil + } + + // Since we're not responsible for _creating_ the connection to our DB + // backend, we also shouldn't close it. This should be handled + // externally as to not interfere with remote DB connections in case we + // need to open/close the store twice as happens in the password change + // case. + return nil +} + +// generateAndStoreNewRootKey creates a new random RootKeyLen-byte root key, +// encrypts it with the given encryption key and stores it in the bucket. +// Any previously set key will be overwritten. +func generateAndStoreNewRootKey(bucket walletdb.ReadWriteBucket, id []byte, + key *snacl.SecretKey) ([]byte, error) { + + rootKey := make([]byte, RootKeyLen) + if _, err := io.ReadFull(rand.Reader, rootKey); err != nil { + return nil, err + } + + encryptedKey, err := key.Encrypt(rootKey) + if err != nil { + return nil, err + } + return rootKey, bucket.Put(id, encryptedKey) +} + +// ListMacaroonIDs returns all the root key ID values except the value of +// encryptedKeyID. +func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) { + r.encKeyMtx.RLock() + defer r.encKeyMtx.RUnlock() + + // Check it's unlocked. + if r.encKey == nil { + return nil, ErrStoreLocked + } + + var rootKeySlice [][]byte + + // Read all the items in the bucket and append the keys, which are the + // root key IDs we want. + err := kvdb.View(r.Backend, func(tx kvdb.RTx) error { + // appendRootKey is a function closure that appends root key ID + // to rootKeySlice. + appendRootKey := func(k, _ []byte) error { + // Only append when the key value is not encryptedKeyID. + if !bytes.Equal(k, encryptionKeyID) { + rootKeySlice = append(rootKeySlice, k) + } + return nil + } + + return tx.ReadBucket(rootKeyBucketName).ForEach(appendRootKey) + }, func() { + rootKeySlice = nil + }) + if err != nil { + return nil, err + } + + return rootKeySlice, nil +} + +// DeleteMacaroonID removes one specific root key ID. If the root key ID is +// found and deleted, it will be returned. +func (r *RootKeyStorage) DeleteMacaroonID( + _ context.Context, rootKeyID []byte) ([]byte, error) { + + r.encKeyMtx.RLock() + defer r.encKeyMtx.RUnlock() + + // Check it's unlocked. + if r.encKey == nil { + return nil, ErrStoreLocked + } + + // Check the rootKeyID is not empty. + if len(rootKeyID) == 0 { + return nil, ErrMissingRootKeyID + } + + // Deleting encryptedKeyID or DefaultRootKeyID is not allowed. + if bytes.Equal(rootKeyID, encryptionKeyID) || + bytes.Equal(rootKeyID, DefaultRootKeyID) { + + return nil, ErrDeletionForbidden + } + + var rootKeyIDDeleted []byte + err := kvdb.Update(r.Backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket(rootKeyBucketName) + + // Check the key can be found. If not, return nil. + if bucket.Get(rootKeyID) == nil { + return nil + } + + // Once the key is found, we do the deletion. + if err := bucket.Delete(rootKeyID); err != nil { + return err + } + rootKeyIDDeleted = rootKeyID + + return nil + }, func() { + rootKeyIDDeleted = nil + }) + if err != nil { + return nil, err + } + + return rootKeyIDDeleted, nil +} diff --git a/server/pkg/macaroons/store_test.go b/server/pkg/macaroons/store_test.go new file mode 100644 index 0000000..6a3e769 --- /dev/null +++ b/server/pkg/macaroons/store_test.go @@ -0,0 +1,273 @@ +package macaroons_test + +import ( + "context" + "crypto/rand" + "path" + "testing" + + "github.com/ark-network/tools/kvdb" + "github.com/ark-network/tools/macaroons" + "github.com/btcsuite/btcwallet/snacl" + "github.com/stretchr/testify/require" +) + +var ( + defaultRootKeyIDContext = macaroons.ContextWithRootKeyID( + context.Background(), macaroons.DefaultRootKeyID, + ) + + nonDefaultRootKeyIDContext = macaroons.ContextWithRootKeyID( + context.Background(), []byte{1}, + ) +) + +// newTestStore creates a new bolt DB in a temporary directory and then +// initializes a root key storage for that DB. +func newTestStore(t *testing.T) (string, *macaroons.RootKeyStorage) { + tempDir := t.TempDir() + + store := openTestStore(t, tempDir) + + return tempDir, store +} + +// openTestStore opens an existing bolt DB and then initializes a root key +// storage for that DB. +func openTestStore(t *testing.T, tempDir string) *macaroons.RootKeyStorage { + db, err := kvdb.Create( + kvdb.BoltBackendName, path.Join(tempDir, "weks.db"), true, + kvdb.DefaultDBTimeout, + ) + require.NoError(t, err) + + store, err := macaroons.NewRootKeyStorage(db) + if err != nil { + _ = db.Close() + t.Fatalf("Error creating root key store: %v", err) + } + + t.Cleanup(func() { + _ = store.Close() + _ = db.Close() + }) + + return store +} + +// TestStore tests the normal use cases of the store like creating, unlocking, +// reading keys and closing it. +func TestStore(t *testing.T) { + tempDir, store := newTestStore(t) + + _, _, err := store.RootKey(context.TODO()) + require.Equal(t, macaroons.ErrStoreLocked, err) + + _, err = store.Get(context.TODO(), nil) + require.Equal(t, macaroons.ErrStoreLocked, err) + + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + + // Check ErrContextRootKeyID is returned when no root key ID found in + // context. + _, _, err = store.RootKey(context.TODO()) + require.Equal(t, macaroons.ErrContextRootKeyID, err) + + // Check ErrMissingRootKeyID is returned when empty root key ID is used. + emptyKeyID := make([]byte, 0) + badCtx := macaroons.ContextWithRootKeyID(context.TODO(), emptyKeyID) + _, _, err = store.RootKey(badCtx) + require.Equal(t, macaroons.ErrMissingRootKeyID, err) + + // Create a context with illegal root key ID value. + encryptedKeyID := []byte("enckey") + badCtx = macaroons.ContextWithRootKeyID(context.TODO(), encryptedKeyID) + _, _, err = store.RootKey(badCtx) + require.Equal(t, macaroons.ErrKeyValueForbidden, err) + + // Create a context with root key ID value. + key, id, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + rootID := id + require.Equal(t, macaroons.DefaultRootKeyID, rootID) + + key2, err := store.Get(defaultRootKeyIDContext, id) + require.NoError(t, err) + require.Equal(t, key, key2) + + badpw := []byte("badweks") + err = store.CreateUnlock(&badpw) + require.Equal(t, macaroons.ErrAlreadyUnlocked, err) + + _ = store.Close() + _ = store.Backend.Close() + + // Between here and the re-opening of the store, it's possible to get + // a double-close, but that's not such a big deal since the tests will + // fail anyway in that case. + store = openTestStore(t, tempDir) + + err = store.CreateUnlock(&badpw) + require.Equal(t, snacl.ErrInvalidPassword, err) + + err = store.CreateUnlock(nil) + require.Equal(t, macaroons.ErrPasswordRequired, err) + + _, _, err = store.RootKey(defaultRootKeyIDContext) + require.Equal(t, macaroons.ErrStoreLocked, err) + + _, err = store.Get(defaultRootKeyIDContext, nil) + require.Equal(t, macaroons.ErrStoreLocked, err) + + err = store.CreateUnlock(&pw) + require.NoError(t, err) + + key, err = store.Get(defaultRootKeyIDContext, rootID) + require.NoError(t, err) + require.Equal(t, key, key2) + + key, id, err = store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.Equal(t, key, key2) + require.Equal(t, rootID, id) +} + +// TestStoreGenerateNewRootKey tests that root keys can be replaced with new +// ones in the store without changing the password. +func TestStoreGenerateNewRootKey(t *testing.T) { + _, store := newTestStore(t) + + // The store must be unlocked to replace the root key. + err := store.GenerateNewRootKey() + require.Equal(t, macaroons.ErrStoreLocked, err) + + // Unlock the store. + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + + // Read the default root key. + oldRootKey1, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + // Read the non-default root-key. + oldRootKey2, _, err := store.RootKey(nonDefaultRootKeyIDContext) + require.NoError(t, err) + + // Replace the root keys with new random keys. + err = store.GenerateNewRootKey() + require.NoError(t, err) + + // Finally, read both root keys from the DB and compare them to the ones + // we got returned earlier. This makes sure that the encryption/ + // decryption of the key in the DB worked as expected too. + newRootKey1, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.NotEqual(t, oldRootKey1, newRootKey1) + + newRootKey2, _, err := store.RootKey(nonDefaultRootKeyIDContext) + require.NoError(t, err) + require.NotEqual(t, oldRootKey2, newRootKey2) +} + +// TestStoreSetRootKey tests that a root key can be set to a specified value. +func TestStoreSetRootKey(t *testing.T) { + _, store := newTestStore(t) + + // Create a new random key + rootKey := make([]byte, 32) + _, err := rand.Read(rootKey) + require.NoError(t, err) + + // The store must be unlocked to set the root key. + err = store.SetRootKey(rootKey) + require.Equal(t, macaroons.ErrStoreLocked, err) + + // Unlock the store and read the current key. + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + oldRootKey, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + // Ensure the new key is different from the old key. + require.NotEqual(t, oldRootKey, rootKey) + + // Replace the root key with the new key. + err = store.SetRootKey(rootKey) + require.NoError(t, err) + + // Finally, read the root key from the DB and compare it to the one + // we created earlier. This makes sure that the encryption/ + // decryption of the key in the DB worked as expected too. + newRootKey, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.Equal(t, rootKey, newRootKey) +} + +// TestStoreChangePassword tests that the password for the store can be changed +// without changing the root keys. +func TestStoreChangePassword(t *testing.T) { + tempDir, store := newTestStore(t) + + // The store must be unlocked to replace the root keys. + err := store.ChangePassword(nil, nil) + require.Equal(t, macaroons.ErrStoreLocked, err) + + // Unlock the DB and read the current default root key and one other + // non-default root key. Both of these should stay the same after + // changing the password for the test to succeed. + pw := []byte("weks") + err = store.CreateUnlock(&pw) + require.NoError(t, err) + + rootKey1, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + + rootKey2, _, err := store.RootKey(nonDefaultRootKeyIDContext) + require.NoError(t, err) + + // Both passwords must be set. + err = store.ChangePassword(nil, nil) + require.Equal(t, macaroons.ErrPasswordRequired, err) + + // Make sure that an error is returned if we try to change the password + // without the correct old password. + wrongPw := []byte("wrong") + newPw := []byte("newpassword") + err = store.ChangePassword(wrongPw, newPw) + require.Equal(t, snacl.ErrInvalidPassword, err) + + // Now really do change the password. + err = store.ChangePassword(pw, newPw) + require.NoError(t, err) + + // Close the store. This will close the underlying DB and we need to + // create a new store instance. Let's make sure we can't use it again + // after closing. + err = store.Close() + require.NoError(t, err) + err = store.Backend.Close() + require.NoError(t, err) + + err = store.CreateUnlock(&newPw) + require.Error(t, err) + + // Let's open it again and try unlocking with the new password. + store = openTestStore(t, tempDir) + err = store.CreateUnlock(&newPw) + require.NoError(t, err) + + // Finally, read the root keys from the DB using the new password and + // make sure that both root keys stayed the same. + rootKeyDB1, _, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.Equal(t, rootKey1, rootKeyDB1) + + rootKeyDB2, _, err := store.RootKey(nonDefaultRootKeyIDContext) + require.NoError(t, err) + require.Equal(t, rootKey2, rootKeyDB2) +} diff --git a/server/scripts/build b/server/scripts/build index 88357c0..f62daa1 100755 --- a/server/scripts/build +++ b/server/scripts/build @@ -13,6 +13,6 @@ ARCH=$(eval "go env GOARCH") pushd $PARENT_PATH mkdir -p build -GO111MODULE=on go build -o build/arkd-$OS-$ARCH cmd/arkd/main.go +GO111MODULE=on go build -o build/arkd-$OS-$ARCH ./cmd/arkd popd \ No newline at end of file diff --git a/server/test/e2e/covenant/e2e_test.go b/server/test/e2e/covenant/e2e_test.go index 5c605ce..90c655a 100644 --- a/server/test/e2e/covenant/e2e_test.go +++ b/server/test/e2e/covenant/e2e_test.go @@ -91,35 +91,6 @@ func TestOnboard(t *testing.T) { require.Equal(t, balanceBefore+1000, balance.Offchain.Total) } -func TestTrustedOnboard(t *testing.T) { - var balance utils.ArkBalance - balanceStr, err := runArkCommand("balance") - require.NoError(t, err) - - require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) - balanceBefore := balance.Offchain.Total - - onboardStr, err := runArkCommand("onboard", "--trusted", "--password", utils.Password) - require.NoError(t, err) - - var result utils.ArkTrustedOnboard - require.NoError(t, json.Unmarshal([]byte(onboardStr), &result)) - - _, err = utils.RunCommand("nigiri", "faucet", "--liquid", result.OnboardAddress) - require.NoError(t, err) - - _, err = utils.RunCommand("nigiri", "faucet", "--liquid", result.OnboardAddress) - require.NoError(t, err) - - time.Sleep(5 * time.Second) - - balanceStr, err = runArkCommand("balance") - require.NoError(t, err) - - require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) - require.Equal(t, balanceBefore+(2*(ONE_BTC-30)), balance.Offchain.Total) -} - func TestSendOffchain(t *testing.T) { _, err := runArkCommand("onboard", "--amount", "1000", "--password", utils.Password) require.NoError(t, err)