diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ab5c0aac3..f7211beada 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -56,9 +56,7 @@ jobs: - name: Setup ubuntu container run: | apt update - apt install -yqq build-essential make software-properties-common - add-apt-repository -y ppa:git-core/candidate - apt update && apt install -yqq git zip unzip zlib1g-dev zlib1g libzlcore-dev yasm + apt install -yqq build-essential make git zip unzip zlib1g-dev zlib1g libzlcore-dev yasm - name: Check out code uses: actions/checkout@v4 @@ -97,15 +95,24 @@ jobs: - name: Install dependencies run: | - apt update \ - && apt install -yqq software-properties-common curl apt-transport-https lsb-release \ - && curl -fsSl https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - \ - && add-apt-repository "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-14 main" \ - && apt update \ - && apt -yqq install \ - nasm clang-14 clang-tools-14 lld-14 build-essential pkg-config autoconf git python3 \ - gcc-mingw-w64 libgcc-9-dev-arm64-cross mingw-w64-tools gcc-mingw-w64-x86-64 mingw-w64-x86-64-dev \ - golang-goprotobuf-dev protobuf-compiler-grpc + apt update + apt install -yqq ca-certificates curl lsb-release + + if ! apt-cache show clang-14 >/dev/null 2>&1; then + install -d -m 0755 /usr/share/keyrings + curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 \ + -o /usr/share/keyrings/apt.llvm.org.asc \ + https://apt.llvm.org/llvm-snapshot.gpg.key + printf '%s\n' \ + "deb [signed-by=/usr/share/keyrings/apt.llvm.org.asc] https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-14 main" \ + > /etc/apt/sources.list.d/llvm-toolchain-14.list + apt update + fi + + apt install -yqq \ + nasm clang-14 clang-tools-14 lld-14 build-essential pkg-config autoconf git python3 \ + gcc-mingw-w64 libgcc-9-dev-arm64-cross mingw-w64-tools gcc-mingw-w64-x86-64 mingw-w64-x86-64-dev \ + golang-goprotobuf-dev protobuf-compiler-grpc update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-14 30 \ && update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 30 \ @@ -124,11 +131,13 @@ jobs: if: matrix.target.GOOS == 'windows' env: CC: x86_64-w64-mingw32-gcc + ZLIB_VERSION: 1.2.11 run: | - apt install -yqq wget - wget https://zlib.net/fossils/zlib-1.2.11.tar.gz - tar -xzf zlib-1.2.11.tar.gz - cd zlib-1.2.11 + curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 \ + -o "zlib-${ZLIB_VERSION}.tar.gz" \ + "https://github.com/madler/zlib/archive/refs/tags/v${ZLIB_VERSION}.tar.gz" + tar -xzf "zlib-${ZLIB_VERSION}.tar.gz" + cd "zlib-${ZLIB_VERSION}" ./configure --prefix=/usr/x86_64-w64-mingw32 --static make make install diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index c96ba10adc..c8b24212d1 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -4,10 +4,14 @@ ### Breaking Changes 🚨🚨 +* [#3959](https://github.com/livepeer/go-livepeer/pull/3959) remote-signer: Refuse to start when `/generate-live-payment` would be unauthenticated (`-remoteSignerWebhookUrl` unset) on a publicly-accessible `-httpAddr`; pass `-remoteSignerAllowNoAuth` to override (@rickstaa) + ### Features âš’ #### General +- [#3944](https://github.com/livepeer/go-livepeer/pull/3944) Bridge slog level to the glog `-v` flag so `-v` controls newer subsystem logging (@rickstaa) + #### Broadcaster #### Orchestrator @@ -18,6 +22,8 @@ #### General +* [#3962](https://github.com/livepeer/go-livepeer/pull/3962) remote-signer: Default `-cliAddr` to a loopback address in remote signer mode so the node no longer fails to start by binding the CLI server to `:80` (@rickstaa) + #### Broadcaster * [a6c4b1e](https://github.com/livepeer/go-livepeer/commit/a6c4b1ef70d8f4d3da0e7d8164ac8d1faf80ad0e) pm: Reject ticket params with a zero expiration block so they are subjected to the economic caps (@rickstaa) diff --git a/README.md b/README.md index 701dd8e12f..ca94fc69c3 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This project requires `go` and a unix shell. ## Getting Started -To get started, clone the repo and follow the [installation guide](https://docs.livepeer.org/guides/orchestrating/install-go-livepeer). +To get started, clone the repo and follow the [installation guide](https://docs.livepeer.org/v2/orchestrators/setup/install). Next, follow [the guide to set up a private ETH network with the Livepeer protocol deployed](cmd/devtool/README.md). diff --git a/cmd/devtool/devtool/devtool_utils.go b/cmd/devtool/devtool/devtool_utils.go index b8d822398e..af9d76b8d9 100644 --- a/cmd/devtool/devtool/devtool_utils.go +++ b/cmd/devtool/devtool/devtool_utils.go @@ -289,7 +289,15 @@ func (d *Devtool) InitializeRound() error { } else { err = d.Client.CheckTx(tx) if err != nil { - glog.Errorf("Error initializng round: %v", err) + initialized, initErr := d.Client.CurrentRoundInitialized() + if initErr == nil && initialized { + glog.Infof("Round initialized despite failed transaction: %v", err) + return nil + } + if initErr != nil { + glog.Errorf("Error checking initialized round after failed transaction: %v", initErr) + } + glog.Errorf("Error initializing round: %v", err) return err } } diff --git a/cmd/livepeer/livepeer.go b/cmd/livepeer/livepeer.go index af895ab503..60c6b89de0 100755 --- a/cmd/livepeer/livepeer.go +++ b/cmd/livepeer/livepeer.go @@ -7,6 +7,7 @@ import ( "context" "flag" "fmt" + "log/slog" "os" "os/signal" "runtime" @@ -53,6 +54,12 @@ func main() { vFlag.Value.Set(*verbosity) + // Bridge slog (used by newer subsystems) to the glog -v level until we + // migrate off glog; without this, slog.Debug is dropped regardless of -v. + if glog.V(6) { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + if *mistJSON { mistconnector.PrintMistConfigJson( "livepeer", diff --git a/cmd/livepeer/starter/flags.go b/cmd/livepeer/starter/flags.go index 8a8c724f2e..d9d0be84ba 100644 --- a/cmd/livepeer/starter/flags.go +++ b/cmd/livepeer/starter/flags.go @@ -140,10 +140,14 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig { cfg.TestOrchAvail = fs.Bool("startupAvailabilityCheck", *cfg.TestOrchAvail, "Set to false to disable the startup Orchestrator availability check on the configured serviceAddr") cfg.RemoteSigner = fs.Bool("remoteSigner", *cfg.RemoteSigner, "Set to true to run remote signer service") cfg.RemoteSignerUrl = fs.String("remoteSignerUrl", *cfg.RemoteSignerUrl, "URL of remote signer service to use (e.g., http://localhost:8935). Gateway only.") + cfg.RemoteSignerAddress = fs.String("remoteSignerAddress", *cfg.RemoteSignerAddress, "Gateway only. Optional Ethereum address to pin remote signer identity when the signer uses Turnkey multi-address mode") cfg.RemoteSignerHeaders = fs.String("remoteSignerHeaders", *cfg.RemoteSignerHeaders, "Map of headers to use for gateway remote signer requests. e.g. 'header:val,header2:val2'") cfg.RemoteSignerWebhookURL = fs.String("remoteSignerWebhookUrl", *cfg.RemoteSignerWebhookURL, "Authentication webhook URL called by remote signer during GenerateLivePayment") cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'") + cfg.RemoteSignerAllowNoAuth = fs.Bool("remoteSignerAllowNoAuth", *cfg.RemoteSignerAllowNoAuth, "Allow an unauthenticated remote signer on a public -httpAddr (no webhook). UNSAFE: signs payments from this node's deposit for any reachable caller; restrict access externally (proxy/private network).") cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers") + cfg.TurnkeyOrg = fs.String("turnkeyOrg", *cfg.TurnkeyOrg, "Remote signer only. Turnkey organization id; when set, Ethereum keys are managed in Turnkey instead of a local keystore") + cfg.TurnkeyApiKeyName = fs.String("turnkeyApiKeyName", *cfg.TurnkeyApiKeyName, "Name of the Turnkey API key in ~/.turnkey/keys// (used with -turnkeyOrg)") // Gateway metrics cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers") diff --git a/cmd/livepeer/starter/starter.go b/cmd/livepeer/starter/starter.go index cf3aca3dbd..adf589cf3a 100755 --- a/cmd/livepeer/starter/starter.go +++ b/cmd/livepeer/starter/starter.go @@ -44,6 +44,7 @@ import ( "github.com/livepeer/livepeer-data/pkg/event" "github.com/livepeer/lpms/ffmpeg" "github.com/olekukonko/tablewriter" + sdk "github.com/tkhq/go-sdk" ) var ( @@ -75,6 +76,7 @@ const ( OrchestratorCliPort = "7935" TranscoderCliPort = "6935" AIWorkerCliPort = "4935" + RemoteSignerCliPort = "3935" RefreshPerfScoreInterval = 10 * time.Minute ) @@ -169,10 +171,14 @@ type LivepeerConfig struct { TestOrchAvail *bool RemoteSigner *bool RemoteSignerUrl *string + RemoteSignerAddress *string RemoteSignerHeaders *string RemoteSignerWebhookURL *string RemoteSignerWebhookHeaders *string + RemoteSignerAllowNoAuth *bool RemoteDiscovery *bool + TurnkeyOrg *string + TurnkeyApiKeyName *string AIRunnerImage *string AIRunnerImageOverrides *string AIVerboseLogs *bool @@ -310,10 +316,14 @@ func DefaultLivepeerConfig() LivepeerConfig { defaultTestOrchAvail := true defaultRemoteSigner := false defaultRemoteSignerUrl := "" + defaultRemoteSignerAddress := "" defaultRemoteSignerHeaders := "" defaultRemoteSignerWebhookURL := "" defaultRemoteSignerWebhookHeaders := "" + defaultRemoteSignerAllowNoAuth := false defaultRemoteDiscovery := false + defaultTurnkeyOrg := "" + defaultTurnkeyApiKeyName := "default" // Gateway logs defaultKafkaBootstrapServers := "" @@ -437,10 +447,14 @@ func DefaultLivepeerConfig() LivepeerConfig { TestOrchAvail: &defaultTestOrchAvail, RemoteSigner: &defaultRemoteSigner, RemoteSignerUrl: &defaultRemoteSignerUrl, + RemoteSignerAddress: &defaultRemoteSignerAddress, RemoteSignerHeaders: &defaultRemoteSignerHeaders, RemoteSignerWebhookURL: &defaultRemoteSignerWebhookURL, RemoteSignerWebhookHeaders: &defaultRemoteSignerWebhookHeaders, + RemoteSignerAllowNoAuth: &defaultRemoteSignerAllowNoAuth, RemoteDiscovery: &defaultRemoteDiscovery, + TurnkeyOrg: &defaultTurnkeyOrg, + TurnkeyApiKeyName: &defaultTurnkeyApiKeyName, // Gateway logs KafkaBootstrapServers: &defaultKafkaBootstrapServers, @@ -486,6 +500,8 @@ func (cfg LivepeerConfig) PrintConfig(w io.Writer) { } func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { + var turnkeyAdminClient *sdk.Client + if *cfg.MaxSessions == "auto" && *cfg.Orchestrator { if *cfg.Transcoder { glog.Exit("-maxSessions 'auto' cannot be used when both -orchestrator and -transcoder are specified") @@ -705,6 +721,9 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { exit("Remote signer mode requires on-chain network") } } + if *cfg.TurnkeyOrg != "" && !*cfg.RemoteSigner { + exit("-turnkeyOrg requires -remoteSigner") + } if *cfg.Redeemer { n.NodeType = core.RedeemerNode @@ -846,15 +865,72 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { } defer gpm.Stop() - am, err := eth.NewAccountManager(ethcommon.HexToAddress(*cfg.EthAcctAddr), keystoreDir, chainID, *cfg.EthPassword) - if err != nil { - glog.Errorf("Error creating Ethereum account manager: %v", err) - return - } + var am eth.AccountManager + if *cfg.TurnkeyOrg != "" { + if n.NodeType != core.RemoteSignerNode { + glog.Exit("-turnkeyOrg is only supported when running as a remote signer (-remoteSigner)") + } + tkClient, err := sdk.New(sdk.WithAPIKeyName(*cfg.TurnkeyApiKeyName)) + if err != nil { + glog.Exit("Failed to create Turnkey client: ", err) + } + turnkeyAdminClient = tkClient + orgID := *cfg.TurnkeyOrg + accts, err := eth.ListTurnkeyEthereumAccounts(tkClient, orgID) + if err != nil { + glog.Exit("Failed to list Turnkey Ethereum accounts: ", err) + } + var signAddr ethcommon.Address + if *cfg.EthAcctAddr != "" { + signAddr = ethcommon.HexToAddress(*cfg.EthAcctAddr) + found := false + for _, a := range accts { + if a.Address == signAddr { + found = true + break + } + } + if !found { + glog.Exit("-ethAcctAddr does not match any Turnkey Ethereum account in the organization") + } + } else if len(accts) > 0 { + signAddr = accts[0].Address + } else { + wname := fmt.Sprintf("livepeer-remote-signer-%d", time.Now().Unix()) + _, addr, err := eth.TurnkeyCreateWallet(tkClient, orgID, wname) + if err != nil { + glog.Exit("No Turnkey wallets in org and failed to create one: ", err) + } + glog.Infof("Created Turnkey wallet with default Ethereum address %s", addr.Hex()) + signAddr = addr + accts, err = eth.ListTurnkeyEthereumAccounts(tkClient, orgID) + if err != nil { + glog.Errorf("Warning: failed to refresh Turnkey account list: %v", err) + accts = []eth.TurnkeyWalletAccount{{OrganizationID: orgID, Address: signAddr}} + } + } + tkAm := eth.NewTurnkeyAccountManager(tkClient, orgID, chainID, signAddr) + am = tkAm + n.TurnkeyMode = true + n.TurnkeyOrgID = orgID + n.TurnkeyAccount = tkAm + addrList := make([]ethcommon.Address, 0, len(accts)) + for _, a := range accts { + addrList = append(addrList, a.Address) + } + n.ReplaceTurnkeyAddressBook(addrList) + } else { + var err error + am, err = eth.NewAccountManager(ethcommon.HexToAddress(*cfg.EthAcctAddr), keystoreDir, chainID, *cfg.EthPassword) + if err != nil { + glog.Errorf("Error creating Ethereum account manager: %v", err) + return + } - if err := am.Unlock(*cfg.EthPassword); err != nil { - glog.Errorf("Error unlocking Ethereum account: %v", err) - return + if err := am.Unlock(*cfg.EthPassword); err != nil { + glog.Errorf("Error unlocking Ethereum account: %v", err) + return + } } tm := eth.NewTransactionManager(backend, gpm, am, *cfg.TxTimeout, *cfg.MaxTxReplacements) @@ -1628,7 +1704,12 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { } glog.Info("Retrieving OrchestratorInfo fields from remote signer: ", url) - fields, err := server.GetOrchInfoSig(url, n.RemoteSignerHeaders) + pinAddr := "" + if *cfg.RemoteSignerAddress != "" { + pinAddr = *cfg.RemoteSignerAddress + n.GatewayRemoteSignerAddress = ethcommon.HexToAddress(pinAddr) + } + fields, err := server.GetOrchInfoSig(url, pinAddr, n.RemoteSignerHeaders) if err != nil { glog.Exit("Unable to query remote signer: ", err) } @@ -1778,6 +1859,8 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { *cfg.CliAddr = defaultAddr(*cfg.CliAddr, "127.0.0.1", TranscoderCliPort) } else if n.NodeType == core.AIWorkerNode { *cfg.CliAddr = defaultAddr(*cfg.CliAddr, "127.0.0.1", AIWorkerCliPort) + } else if n.NodeType == core.RemoteSignerNode { + *cfg.CliAddr = defaultAddr(*cfg.CliAddr, "127.0.0.1", RemoteSignerCliPort) } // Apply default capabilities if not running as a transcoder. @@ -1860,6 +1943,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { if err != nil { exit("Error creating Livepeer server: err=%q", err) } + s.TurnkeyAdmin = turnkeyAdminClient ec := make(chan error) tc := make(chan struct{}) @@ -1947,9 +2031,25 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { // Start remote signer server go func() { *cfg.HttpAddr = defaultAddr(*cfg.HttpAddr, "127.0.0.1", OrchestratorRpcPort) + + // Refuse to start a public signer with no webhook auth. It would sign payments + // from this node's deposit for any caller (override with -remoteSignerAllowNoAuth). + if n.RemoteSignerWebhookURL == nil { + isLocalHTTP, err := isLocalURL("https://" + *cfg.HttpAddr) + if err != nil { + exit("Error checking for local -httpAddr: %v", err) + } + if !isLocalHTTP && !*cfg.RemoteSignerAllowNoAuth { + exit("Refusing to start: remote signer on public -httpAddr %s with no "+ + "-remoteSignerWebhookUrl signs payments from this node's deposit for any caller. "+ + "Set the webhook, or pass -remoteSignerAllowNoAuth to override.", *cfg.HttpAddr) + } + glog.Warning("WARNING: remote signer has no webhook auth. /generate-live-payment is " + + "UNAUTHENTICATED and signs payments from this node's deposit. Set -remoteSignerWebhookUrl.") + } + glog.Info("Starting remote signer server on ", *cfg.HttpAddr) - err := server.StartRemoteSignerServer(s, *cfg.HttpAddr) - if err != nil { + if err := server.StartRemoteSignerServer(s, *cfg.HttpAddr); err != nil { exit("Error starting remote signer server: err=%q", err) } }() diff --git a/cmd/livepeer_cli/livepeer_cli.go b/cmd/livepeer_cli/livepeer_cli.go index e73773cfdb..f34136c645 100644 --- a/cmd/livepeer_cli/livepeer_cli.go +++ b/cmd/livepeer_cli/livepeer_cli.go @@ -54,6 +54,7 @@ func main() { w.orchestrator = w.isOrchestrator() w.redeemer = w.isRedeemer() w.checkNet() + w.turnkey = w.status().TurnkeyMode w.run() return nil @@ -71,6 +72,7 @@ type wizard struct { redeemer bool testnet bool offchain bool + turnkey bool in *bufio.Reader // Wrapper around stdin to allow reading user input } @@ -80,6 +82,7 @@ type wizardOpt struct { testnet bool orchestrator bool notOrchestrator bool + turnkeyOnly bool } func (w *wizard) initializeOptions() []wizardOpt { @@ -112,6 +115,10 @@ func (w *wizard) initializeOptions() []wizardOpt { }, testnet: true}, {desc: "Sign a message", invoke: w.signMessage}, {desc: "Sign typed data", invoke: w.signTypedData}, + {desc: "Turnkey: List wallets / Ethereum addresses", invoke: w.turnkeyListWallets, turnkeyOnly: true}, + {desc: "Turnkey: Create new wallet", invoke: w.turnkeyCreateWallet, turnkeyOnly: true}, + {desc: "Turnkey: Create new account (derive address)", invoke: w.turnkeyCreateAccount, turnkeyOnly: true}, + {desc: "Turnkey: Select default signing address", invoke: w.turnkeySelectAddress, turnkeyOnly: true}, {desc: "Vote in a governance poll", invoke: w.vote, orchestrator: true}, {desc: "Vote on a treasury proposal", invoke: w.voteOnProposal, orchestrator: true}, {desc: "Set max ticket face value", invoke: w.setMaxFaceValue, orchestrator: true}, @@ -132,6 +139,9 @@ func (w *wizard) filterOptions(options []wizardOpt) []wizardOpt { if opt.testnet && !w.testnet { continue } + if opt.turnkeyOnly && !w.turnkey { + continue + } if !opt.orchestrator && !opt.notOrchestrator || isOrchestratorOrRedeemer && opt.orchestrator || !isOrchestratorOrRedeemer && opt.notOrchestrator { filtered = append(filtered, opt) } diff --git a/cmd/livepeer_cli/wizard_turnkey.go b/cmd/livepeer_cli/wizard_turnkey.go new file mode 100644 index 0000000000..f8d0fded1c --- /dev/null +++ b/cmd/livepeer_cli/wizard_turnkey.go @@ -0,0 +1,88 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +func (w *wizard) turnkeyBase() string { + return fmt.Sprintf("http://%s:%s", w.host, w.httpPort) +} + +func (w *wizard) turnkeyListWallets() { + resp, err := http.Get(w.turnkeyBase() + "/turnkey/wallets") + if err != nil { + log.Error("Turnkey list wallets failed", "err", err) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Error("Turnkey list wallets", "status", resp.StatusCode, "body", string(body)) + return + } + fmt.Println(string(body)) +} + +func (w *wizard) turnkeyCreateWallet() { + fmt.Println("Wallet name?") + name := w.readString() + payload, _ := json.Marshal(map[string]string{"walletName": name}) + resp, err := http.Post(w.turnkeyBase()+"/turnkey/create-wallet", "application/json", bytes.NewReader(payload)) + if err != nil { + log.Error("Turnkey create wallet failed", "err", err) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Error("Turnkey create wallet", "status", resp.StatusCode, "body", string(body)) + return + } + fmt.Println(string(body)) +} + +func (w *wizard) turnkeyCreateAccount() { + fmt.Println("Wallet ID?") + wid := w.readString() + payload, _ := json.Marshal(map[string]string{"walletId": wid}) + resp, err := http.Post(w.turnkeyBase()+"/turnkey/create-account", "application/json", bytes.NewReader(payload)) + if err != nil { + log.Error("Turnkey create account failed", "err", err) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Error("Turnkey create account", "status", resp.StatusCode, "body", string(body)) + return + } + fmt.Println(string(body)) +} + +func (w *wizard) turnkeySelectAddress() { + fmt.Println("Ethereum address (0x…)?") + addr := w.readString() + if !strings.HasPrefix(addr, "0x") { + addr = "0x" + addr + } + payload, _ := json.Marshal(map[string]string{"address": addr}) + resp, err := http.Post(w.turnkeyBase()+"/turnkey/select-address", "application/json", bytes.NewReader(payload)) + if err != nil { + log.Error("Turnkey select address failed", "err", err) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Error("Turnkey select address", "status", resp.StatusCode, "body", string(body)) + return + } + fmt.Println(string(body)) +} diff --git a/common/types.go b/common/types.go index 1fef7d7886..2946e396ae 100644 --- a/common/types.go +++ b/common/types.go @@ -39,6 +39,12 @@ type NodeStatus struct { RegisteredTranscoders []RemoteTranscoderInfo LocalTranscoding bool // Indicates orchestrator that is also transcoder BroadcasterPrices map[string]*big.Rat + // Turnkey remote signer: true when using Turnkey for Ethereum signing. + TurnkeyMode bool `json:"turnkeyMode,omitempty"` + // Turnkey organization id (remote signer only). + TurnkeyOrgID string `json:"turnkeyOrgId,omitempty"` + // Ethereum addresses available in Turnkey for this org (remote signer only). + TurnkeyAddresses []string `json:"turnkeyAddresses,omitempty"` // xxx add transcoder's version here } diff --git a/core/livepeernode.go b/core/livepeernode.go index b6971b6f96..197534c1ef 100644 --- a/core/livepeernode.go +++ b/core/livepeernode.go @@ -149,13 +149,23 @@ type LivepeerNode struct { ExtraNodes int // Gateway fields for remote signers - RemoteSignerUrl *url.URL // URL of remote signer service to use (gateway only) - RemoteSignerHeaders map[string]string // Headers to use for gateway remote signer requests + RemoteSignerUrl *url.URL // URL of remote signer service to use (gateway only) + RemoteSignerHeaders map[string]string // Headers to use for gateway remote signer requests + RemoteEthAddr ethcommon.Address // eth address of the remote signer + // GatewayRemoteSignerAddress optionally pins the gateway to a specific remote signer ETH identity (Turnkey multi-address). + GatewayRemoteSignerAddress ethcommon.Address + InfoSig []byte // sig over eth address for the OrchestratorInfo request + RemoteDiscovery bool // expose remote discovery endpoint when enabled + + // Turnkey (remote signer only): org id, address book, and the concrete account manager when using Turnkey signing. + TurnkeyMode bool + TurnkeyOrgID string + TurnkeyAccount *eth.TurnkeyAccountManager + turnkeyMu sync.RWMutex + turnkeyAddrs map[ethcommon.Address]struct{} + RemoteSignerWebhookURL *url.URL // Authentication webhook URL called by remote signer during GenerateLivePayment RemoteSignerWebhookHeaders map[string]string // Headers to use for remote signer webhook requests - RemoteEthAddr ethcommon.Address // eth address of the remote signer - InfoSig []byte // sig over eth address for the OrchestratorInfo request - RemoteDiscovery bool // expose remote discovery endpoint when enabled // Thread safety for config fields mu sync.RWMutex @@ -399,3 +409,44 @@ func (n *LivepeerNode) GetPriceForJob(senderEthAddress string, extCapability str return jobPrice } + +// ReplaceTurnkeyAddressBook sets the set of Ethereum addresses this remote signer may use for Turnkey signing. +func (n *LivepeerNode) ReplaceTurnkeyAddressBook(addrs []ethcommon.Address) { + if n == nil { + return + } + n.turnkeyMu.Lock() + defer n.turnkeyMu.Unlock() + n.turnkeyAddrs = make(map[ethcommon.Address]struct{}) + for _, a := range addrs { + n.turnkeyAddrs[a] = struct{}{} + } +} + +// TurnkeySigningAddressAllowed returns true if addr is in the Turnkey address book (or if the book is unset / Turnkey off). +func (n *LivepeerNode) TurnkeySigningAddressAllowed(addr ethcommon.Address) bool { + if n == nil || !n.TurnkeyMode || n.TurnkeyAccount == nil { + return true + } + n.turnkeyMu.RLock() + defer n.turnkeyMu.RUnlock() + if len(n.turnkeyAddrs) == 0 { + return true + } + _, ok := n.turnkeyAddrs[addr] + return ok +} + +// TurnkeyAddressList returns a snapshot of known Turnkey Ethereum addresses (for status / CLI). +func (n *LivepeerNode) TurnkeyAddressList() []ethcommon.Address { + if n == nil { + return nil + } + n.turnkeyMu.RLock() + defer n.turnkeyMu.RUnlock() + out := make([]ethcommon.Address, 0, len(n.turnkeyAddrs)) + for a := range n.turnkeyAddrs { + out = append(out, a) + } + return out +} diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go index b753d366f8..c6706c1280 100644 --- a/discovery/discovery_test.go +++ b/discovery/discovery_test.go @@ -1477,9 +1477,8 @@ func TestOrchestratorPool_GetOrchestrators_SuspendedOrchs(t *testing.T) { wg := sync.WaitGroup{} orchCb := func() error { return nil } - oldOrchInfo := serverGetOrchInfo - defer func() { wg.Wait(); serverGetOrchInfo = oldOrchInfo }() - serverGetOrchInfo = func(ctx context.Context, bcast common.Broadcaster, server *url.URL, params server.GetOrchestratorInfoParams) (*net.OrchestratorInfo, error) { + defer wg.Wait() + getOrchInfo := func(ctx context.Context, bcast common.Broadcaster, server *url.URL, params server.GetOrchestratorInfoParams) (*net.OrchestratorInfo, error) { defer wg.Done() err := orchCb() return &net.OrchestratorInfo{ @@ -1488,6 +1487,7 @@ func TestOrchestratorPool_GetOrchestrators_SuspendedOrchs(t *testing.T) { } pool := NewOrchestratorPool(&stubBroadcaster{}, addresses, common.Score_Trusted, []string{}, 50*time.Millisecond) + pool.getOrchInfo = getOrchInfo // suspend https://127.0.0.1:8938 sus := newStubSuspender() diff --git a/discovery/wh_discovery.go b/discovery/wh_discovery.go index ea4ca53409..13a3066eba 100644 --- a/discovery/wh_discovery.go +++ b/discovery/wh_discovery.go @@ -13,6 +13,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/golang/glog" "github.com/livepeer/go-livepeer/common" + "github.com/livepeer/go-livepeer/net" + "github.com/livepeer/go-livepeer/server" ) type webhookResponse struct { @@ -30,6 +32,7 @@ type webhookPool struct { bcast common.Broadcaster discoveryTimeout time.Duration ignoreCapacityCheck bool + getOrchInfo func(context.Context, common.Broadcaster, *url.URL, server.GetOrchestratorInfoParams) (*net.OrchestratorInfo, error) } func NewWebhookPool(bcast common.Broadcaster, callback *url.URL, discoveryTimeout time.Duration) *webhookPool { @@ -56,6 +59,7 @@ func (cfg WebhookPoolConfig) New() *webhookPool { bcast: cfg.Broadcaster, discoveryTimeout: cfg.DiscoveryTimeout, ignoreCapacityCheck: cfg.IgnoreCapacityCheck, + getOrchInfo: serverGetOrchInfo, } go p.getInfos() return p @@ -99,7 +103,7 @@ func (w *webhookPool) getInfos() ([]common.OrchestratorLocalInfo, error) { discoveryTimeout: w.discoveryTimeout, extraNodes: w.bcast.ExtraNodes(), ignoreCapacityCheck: w.ignoreCapacityCheck, - getOrchInfo: serverGetOrchInfo, + getOrchInfo: w.getOrchInfo, } w.mu.Lock() diff --git a/doc/remote-signer.md b/doc/remote-signer.md index 55334322eb..4fd7131941 100644 --- a/doc/remote-signer.md +++ b/doc/remote-signer.md @@ -51,9 +51,9 @@ The remote signer is intended to be its own standalone node type. The `-remoteSi **The remote signer requires an on-chain network**. It cannot run with `-network=offchain` because it must have on-chain Ethereum connectivity to sign and manage payment tickets. -The remote signer must have typical Ethereum flags configured (examples: `-network`, `-ethUrl`, `-ethController`, keystore/password flags). See the go-livepeer [devtool](https://github.com/livepeer/go-livepeer/blob/92bdb59f169056e3d1beba9b511554ea5d9eda72/cmd/devtool/devtool.go#L200-L212) for an example of what flags might be required. +The remote signer requires the standard Ethereum flags: `-network`, `-ethUrl`, `-ethController`, and either keystore/password flags (local keystore) or `-turnkeyOrg` (Turnkey — see [Turnkey mode](#turnkey-mode-remote-signer) below). See the go-livepeer [devtool](https://github.com/livepeer/go-livepeer/blob/92bdb59f169056e3d1beba9b511554ea5d9eda72/cmd/devtool/devtool.go#L200-L212) for a full example. -The remote signer listens to the standard go-livepeer HTTP port (8935) by default. To change the listening port or interface, use the `-httpAddr` flag. +The remote signer listens to the standard go-livepeer HTTP port (8935) by default. Change the port or interface with the `-httpAddr` flag. The CLI webserver defaults to `127.0.0.1:3935` (loopback only). Override it with `-cliAddr`. Example (fill in the placeholders for your environment): @@ -141,7 +141,62 @@ If there are errors about too many tickets (eg `numTickets ... exceeds maximum o For PM configuration details and how these knobs interact, see `doc/payments.md`. -### Payment Authentication +## Turnkey mode (remote signer) + +In Turnkey mode, the remote signer delegates Ethereum key custody to [Turnkey](https://www.turnkey.com/) secure enclaves instead of a local keystore. Private keys are generated and stored entirely within Turnkey; the node holds only an API key. + +### Flags + +- **`-turnkeyOrg `** — Turnkey organization ID. **Must** be used with `-remoteSigner`. When set, the node uses Turnkey for all signing and skips the local keystore. The standard chain flags (`-ethUrl`, `-network`, `-ethController`) are still required. +- **`-turnkeyApiKeyName `** — Name of the API key loaded from `~/.turnkey/keys//` (default: `default`). Follows the [tkhq/go-sdk](https://github.com/tkhq/go-sdk) key-store convention. + +If the organization has no Ethereum accounts on first start, the node automatically creates a Turnkey wallet and logs its address. + +### Signing semantics + +The Turnkey integration keeps Ethereum hashing in `go-livepeer` and uses Turnkey only for custody plus ECDSA signing. The signer computes the final 32-byte Ethereum digest locally before calling `SignRawPayload`: + +- **`Sign(msg)`** uses the EIP-191 text-hash path (`accounts.TextHash(msg)`). +- **`SignTypedData(...)`** computes the EIP-712 digest locally. +- **`SignTx(...)`** stays on Turnkey's transaction-signing API rather than the raw-payload path. + +Because the raw-payload request already receives the final digest, the signer sends that value to Turnkey with **`HASH_FUNCTION_NO_OP`**. This preserves parity with the local keystore implementation and avoids applying an extra hash inside Turnkey. + +Do **not** switch this flow to **`HASH_FUNCTION_KECCAK256`** unless the payload contract changes as well. In the current design, that would hash an already-computed Ethereum digest a second time and produce signatures that no longer match the rest of the stack. + +### Multi-address signing + +A single Turnkey remote signer can manage multiple Ethereum addresses. The node maintains an address book of all Ethereum-format accounts in the organization and uses it to authorize signing requests. + +- **`POST /sign-orchestrator-info?address=0x…`** — Selects which address signs the orchestrator info response. Defaults to the node’s current default signing address. +- **`POST /generate-live-payment`** — Accepts an optional JSON field **`signerAddress`** to select which identity signs PM tickets for that session. The returned `state` blob records the chosen address, so all follow-up requests remain pinned to the same identity. + +Gateways pin a specific remote signer identity with: + +- **`-remoteSignerAddress 0x…`** — Passed as `signerAddress` in every `/generate-live-payment` request and as the `?address` parameter on `/sign-orchestrator-info`, ensuring the gateway consistently uses one Turnkey address. + +### Wallet management API + +When Turnkey mode is active, the signer exposes the following management endpoints on the same HTTP server. Apply the same network controls used for the rest of the signer. + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/turnkey/wallets` | List all wallet IDs, account IDs, and Ethereum addresses in the org | +| `POST` | `/turnkey/create-wallet` | Body: `{"walletName":"..."}` — create a new HD wallet and derive its first Ethereum account | +| `POST` | `/turnkey/create-account` | Body: `{"walletId":"..."}` — derive the next `m/44'/60'/0'/0/n` Ethereum account from an existing wallet | +| `POST` | `/turnkey/select-address` | Body: `{"address":"0x..."}` — set the node’s default signing address | + +`livepeer-cli` displays Turnkey menu entries only when `/status` reports `turnkeyMode: true`. + +### Observability + +`/status` exposes three Turnkey-specific fields: + +- **`turnkeyMode`** — `true` when the node is using Turnkey signing. +- **`turnkeyOrgId`** — The configured organization ID. +- **`turnkeyAddresses`** — Sorted list of Ethereum addresses (hex) currently in the org’s address book. + +## Payment Authentication The remote signer's payment endpoint (POST `/generate-live-payment`) supports an optional authentication webhook that can be called during every request. This allows operators to enforce external authorization or policy checks before the signer commits to updated payment state. diff --git a/docker/Dockerfile b/docker/Dockerfile index 6ed4a49705..efa33a4d47 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,12 +12,13 @@ ENV GOARCH="$TARGETARCH" \ CGO_LDFLAGS="-L/usr/local/cuda_${TARGETARCH}/lib64" RUN apt update \ - && apt install -yqq software-properties-common curl apt-transport-https lsb-release \ - && curl -fsSL https://dl.google.com/go/go1.21.5.linux-${BUILDARCH}.tar.gz | tar -C /usr/local -xz \ - && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \ - && add-apt-repository "deb [arch=${BUILDARCH}] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ - && curl -fsSl https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - \ - && add-apt-repository "deb [arch=${BUILDARCH}] https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-14 main" \ + && apt install -yqq ca-certificates curl lsb-release \ + && install -d -m 0755 /usr/share/keyrings \ + && curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 https://dl.google.com/go/go1.21.5.linux-${BUILDARCH}.tar.gz | tar -C /usr/local -xz \ + && curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 -o /usr/share/keyrings/docker.asc https://download.docker.com/linux/ubuntu/gpg \ + && printf '%s\n' "deb [arch=${BUILDARCH} signed-by=/usr/share/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list \ + && curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 -o /usr/share/keyrings/apt.llvm.org.asc https://apt.llvm.org/llvm-snapshot.gpg.key \ + && printf '%s\n' "deb [arch=${BUILDARCH} signed-by=/usr/share/keyrings/apt.llvm.org.asc] https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-14 main" > /etc/apt/sources.list.d/llvm-toolchain-14.list \ && apt update \ && apt -yqq install clang-14 clang-tools-14 lld-14 build-essential pkg-config autoconf git python docker-ce-cli pciutils gcc-multilib libgcc-8-dev-arm64-cross gcc-mingw-w64-x86-64 zlib1g zlib1g-dev libx264-dev libfdk-aac-dev diff --git a/eth/turnkey.go b/eth/turnkey.go new file mode 100644 index 0000000000..6202b15e09 --- /dev/null +++ b/eth/turnkey.go @@ -0,0 +1,436 @@ +package eth + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + sdk "github.com/tkhq/go-sdk" + "github.com/tkhq/go-sdk/pkg/api/client/signing" + "github.com/tkhq/go-sdk/pkg/api/models" + "github.com/tkhq/go-sdk/pkg/util" +) + +// TurnkeyAccountManager implements AccountManager using Turnkey secure signing. +// Message signing matches the keystore path: ECDSA over accounts.TextHash(msg),65-byte sig with v in {27,28}, low-S. +type TurnkeyAccountManager struct { + client *sdk.Client + orgID string + chainID *big.Int + signer types.Signer + + mu sync.Mutex + // signWith is the Turnkey SignWith identifier (Ethereum address hex). + signWith string + signRawPayloadFn func(orgID, signWith, payloadHex string) (r, s, v string, err error) + signTransactionFn func(orgID, signWith, unsignedTxHex string) (signedTxHex string, err error) +} + +// NewTurnkeyAccountManager constructs a Turnkey-backed account manager. +// signWith must be a Turnkey wallet account Ethereum address (0x-prefixed) that this API key may sign for. +func NewTurnkeyAccountManager( + client *sdk.Client, + orgID string, + chainID *big.Int, + signWith ethcommon.Address, +) *TurnkeyAccountManager { + return &TurnkeyAccountManager{ + client: client, + orgID: orgID, + chainID: chainID, + signer: types.LatestSignerForChainID(chainID), + signWith: signWith.Hex(), + } +} + +// WithSigningAddress runs fn while temporarily using addr for Turnkey SignWith. +// The lock is not held during fn to avoid deadlock when fn calls Sign/SignTx. +func (t *TurnkeyAccountManager) WithSigningAddress(addr ethcommon.Address, fn func() error) error { + if t == nil { + return fmt.Errorf("turnkey account manager is nil") + } + t.mu.Lock() + prev := t.signWith + t.signWith = addr.Hex() + t.mu.Unlock() + defer func() { + t.mu.Lock() + t.signWith = prev + t.mu.Unlock() + }() + return fn() +} + +// SetSigningAddress updates the default signing address (e.g. CLI select). +func (t *TurnkeyAccountManager) SetSigningAddress(addr ethcommon.Address) { + if t == nil { + return + } + t.mu.Lock() + t.signWith = addr.Hex() + t.mu.Unlock() +} + +// SigningAddress returns the active Turnkey SignWith address. +func (t *TurnkeyAccountManager) SigningAddress() ethcommon.Address { + if t == nil { + return ethcommon.Address{} + } + t.mu.Lock() + s := t.signWith + t.mu.Unlock() + return ethcommon.HexToAddress(s) +} + +func (t *TurnkeyAccountManager) activeSignWith() string { + t.mu.Lock() + s := t.signWith + t.mu.Unlock() + return s +} + +func (t *TurnkeyAccountManager) Unlock(string) error { + return nil +} + +func (t *TurnkeyAccountManager) Lock() error { + return nil +} + +func (t *TurnkeyAccountManager) Account() accounts.Account { + return accounts.Account{Address: t.SigningAddress()} +} + +func (t *TurnkeyAccountManager) Sign(msg []byte) ([]byte, error) { + digest := accounts.TextHash(msg) + return t.signDigest(digest) +} + +func (t *TurnkeyAccountManager) SignTypedData(typedData apitypes.TypedData) ([]byte, error) { + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return nil, err + } + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return nil, err + } + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + sighash := crypto.Keccak256(rawData) + return t.signDigest(sighash) +} + +func (t *TurnkeyAccountManager) signDigest(digest []byte) ([]byte, error) { + if len(digest) != 32 { + return nil, fmt.Errorf("turnkey signDigest: expected 32-byte hash, got %d", len(digest)) + } + payloadHex := hex.EncodeToString(digest) + return t.signRawPayloadHex(payloadHex) +} + +func (t *TurnkeyAccountManager) signRawPayloadHex(payloadHex string) ([]byte, error) { + signWith := t.activeSignWith() + var rStr, sStr, vStr string + var err error + if t.signRawPayloadFn != nil { + rStr, sStr, vStr, err = t.signRawPayloadFn(t.orgID, signWith, payloadHex) + } else { + rStr, sStr, vStr, err = t.turnkeySignRawPayload(payloadHex) + } + if err != nil { + return nil, err + } + return assembleRSV(rStr, sStr, vStr) +} + +func (t *TurnkeyAccountManager) turnkeySignRawPayload(payloadHex string) (r, s, v string, err error) { + actType := string(models.ActivityTypeSignRawPayloadV2) + sw := t.activeSignWith() + params := signing.NewSignRawPayloadParams().WithBody(&models.SignRawPayloadRequest{ + OrganizationID: &t.orgID, + TimestampMs: util.RequestTimestamp(), + Parameters: &models.SignRawPayloadIntentV2{ + Encoding: models.PayloadEncodingHexadecimal.Pointer(), + // Ethereum-specific hashing is performed locally before this call + // (EIP-191 text hash, EIP-712 digest, etc), so Turnkey must sign the + // provided 32-byte digest without applying another hash function. + HashFunction: models.HashFunctionNoOp.Pointer(), + Payload: &payloadHex, + SignWith: &sw, + }, + Type: &actType, + }) + + resp, err := t.client.V0().Signing.SignRawPayload(params, t.client.Authenticator) + if err != nil { + return "", "", "", err + } + res := resp.Payload.Activity.Result.SignRawPayloadResult + if res == nil || res.R == nil || res.S == nil || res.V == nil { + return "", "", "", fmt.Errorf("turnkey SignRawPayload: missing r/s/v in response") + } + return *res.R, *res.S, *res.V, nil +} + +func assembleRSV(rStr, sStr, vStr string) ([]byte, error) { + rBytes, err := decodeHexUint256(rStr) + if err != nil { + return nil, fmt.Errorf("decode r: %w", err) + } + sBytes, err := decodeHexUint256(sStr) + if err != nil { + return nil, fmt.Errorf("decode s: %w", err) + } + if len(rBytes) > 32 || len(sBytes) > 32 { + return nil, fmt.Errorf("r/s too long") + } + rPadded := make([]byte, 32) + sPadded := make([]byte, 32) + copy(rPadded[32-len(rBytes):], rBytes) + copy(sPadded[32-len(sBytes):], sBytes) + + vByte, err := parseV(vStr) + if err != nil { + return nil, err + } + + n := crypto.S256().Params().N + halfN := new(big.Int).Rsh(n, 1) + sInt := new(big.Int).SetBytes(sPadded) + vNorm := vByte + if sInt.Cmp(halfN) > 0 { + sInt.Sub(n, sInt) + fill32(sPadded, sInt) + if vNorm == 27 || vNorm == 28 { + vNorm ^= 1 // flip 27<->28 + } else if vNorm <= 1 { + vNorm ^= 1 + } + } + if vNorm <= 1 { + vNorm += 27 + } + if vNorm != 27 && vNorm != 28 { + return nil, fmt.Errorf("invalid v after normalization: %d", vNorm) + } + out := make([]byte, 65) + copy(out[0:32], rPadded) + copy(out[32:64], sPadded) + out[64] = vNorm + return out, nil +} + +func fill32(dst []byte, x *big.Int) { + b := x.Bytes() + if len(b) > 32 { + copy(dst, b[len(b)-32:]) + return + } + copy(dst[32-len(b):], b) +} + +func decodeHexUint256(s string) ([]byte, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "0x") + if s == "" { + return nil, fmt.Errorf("empty hex") + } + return hex.DecodeString(s) +} + +func parseV(vStr string) (byte, error) { + vStr = strings.TrimSpace(vStr) + vStr = strings.TrimPrefix(vStr, "0x") + if vStr == "" { + return 0, fmt.Errorf("empty v") + } + if len(vStr) <= 2 { + b, err := hex.DecodeString(vStr) + if err != nil { + return 0, err + } + if len(b) != 1 { + return 0, fmt.Errorf("invalid v") + } + return b[0], nil + } + // decimal integer string + v := new(big.Int) + if _, ok := v.SetString(vStr, 10); ok { + return byte(v.Uint64()), nil + } + b, err := hex.DecodeString(vStr) + if err != nil { + return 0, err + } + if len(b) == 1 { + return b[0], nil + } + return 0, fmt.Errorf("unrecognized v encoding") +} + +func (t *TurnkeyAccountManager) SignTx(tx *types.Transaction) (*types.Transaction, error) { + raw, err := t.signTransactionRLP(tx) + if err != nil { + return nil, err + } + var out types.Transaction + if err := out.UnmarshalBinary(raw); err != nil { + return nil, err + } + return &out, nil +} + +func (t *TurnkeyAccountManager) signTransactionRLP(tx *types.Transaction) ([]byte, error) { + signWith := t.activeSignWith() + // Turnkey's Ethereum pre-parser rejects typed-tx MarshalBinary() payloads + // because go-ethereum includes empty yParity/r/s. Encode the unsigned + // signing payload instead (type byte + RLP without signature fields). + // abi/bind also omits ChainID on DynamicFeeTx; local signers inject it at + // hash time, but Turnkey reads it from the RLP, so pass our chainID. + unsignedBytes, err := marshalUnsignedEthereumTx(tx, t.chainID) + if err != nil { + return nil, err + } + unsignedHex := hex.EncodeToString(unsignedBytes) + var rlpHex string + if t.signTransactionFn != nil { + rlpHex, err = t.signTransactionFn(t.orgID, signWith, unsignedHex) + if err != nil { + return nil, err + } + } else { + actType := string(models.ActivityTypeSignTransactionV2) + sw := signWith + utx := unsignedHex + params := signing.NewSignTransactionParams().WithBody(&models.SignTransactionRequest{ + OrganizationID: &t.orgID, + TimestampMs: util.RequestTimestamp(), + Parameters: &models.SignTransactionIntentV2{ + SignWith: &sw, + Type: models.TransactionTypeEthereum.Pointer(), + UnsignedTransaction: &utx, + }, + Type: &actType, + }) + resp, err := t.client.V0().Signing.SignTransaction(params, t.client.Authenticator) + if err != nil { + return nil, err + } + if resp.Payload.Activity.Result.SignTransactionResult == nil || + resp.Payload.Activity.Result.SignTransactionResult.SignedTransaction == nil { + return nil, fmt.Errorf("turnkey SignTransaction: missing signedTransaction") + } + rlpHex = *resp.Payload.Activity.Result.SignTransactionResult.SignedTransaction + } + rlpHex = strings.TrimPrefix(strings.TrimSpace(rlpHex), "0x") + return hex.DecodeString(rlpHex) +} + +// marshalUnsignedEthereumTx serializes a tx for Turnkey SignTransaction. +// Typed transactions omit yParity/r/s; legacy keeps go-ethereum MarshalBinary. +// chainIDFallback is used when the tx has no ChainID (common for abi/bind DynamicFeeTx). +func marshalUnsignedEthereumTx(tx *types.Transaction, chainIDFallback *big.Int) ([]byte, error) { + chainID, err := effectiveTxChainID(tx, chainIDFallback) + if err != nil { + return nil, err + } + switch tx.Type() { + case types.LegacyTxType: + return tx.MarshalBinary() + case types.AccessListTxType: + payload, err := rlp.EncodeToBytes([]interface{}{ + chainID, + tx.Nonce(), + tx.GasPrice(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.AccessList(), + }) + if err != nil { + return nil, err + } + return append([]byte{types.AccessListTxType}, payload...), nil + case types.DynamicFeeTxType: + payload, err := rlp.EncodeToBytes([]interface{}{ + chainID, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.AccessList(), + }) + if err != nil { + return nil, err + } + return append([]byte{types.DynamicFeeTxType}, payload...), nil + case types.BlobTxType: + payload, err := rlp.EncodeToBytes([]interface{}{ + chainID, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.AccessList(), + tx.BlobGasFeeCap(), + tx.BlobHashes(), + }) + if err != nil { + return nil, err + } + return append([]byte{types.BlobTxType}, payload...), nil + default: + return nil, fmt.Errorf("turnkey: unsupported ethereum tx type %d", tx.Type()) + } +} + +func effectiveTxChainID(tx *types.Transaction, fallback *big.Int) (*big.Int, error) { + if id := tx.ChainId(); id != nil && id.Sign() != 0 { + return id, nil + } + if fallback != nil && fallback.Sign() != 0 { + return fallback, nil + } + return nil, fmt.Errorf("turnkey: missing chain id for unsigned tx type %d", tx.Type()) +} + +func (t *TurnkeyAccountManager) CreateTransactOpts(gasLimit uint64) (*bind.TransactOpts, error) { + from := t.SigningAddress() + return &bind.TransactOpts{ + From: from, + Nonce: nil, + Signer: t.makeSigner(from), + Value: nil, + GasPrice: nil, + GasFeeCap: nil, + GasTipCap: nil, + GasLimit: gasLimit, + Context: nil, + }, nil +} + +func (t *TurnkeyAccountManager) makeSigner(from ethcommon.Address) bind.SignerFn { + return func(addr ethcommon.Address, transaction *types.Transaction) (*types.Transaction, error) { + if addr != from { + return nil, fmt.Errorf("turnkey transact signer: expected from %s, got %s", from.Hex(), addr.Hex()) + } + return t.SignTx(transaction) + } +} diff --git a/eth/turnkey_test.go b/eth/turnkey_test.go new file mode 100644 index 0000000000..0937718707 --- /dev/null +++ b/eth/turnkey_test.go @@ -0,0 +1,335 @@ +package eth + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + lpcrypto "github.com/livepeer/go-livepeer/crypto" + "github.com/livepeer/go-livepeer/pm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func turnkeyTestKeyAndKeystore(t *testing.T) (*keystore.KeyStore, *ecdsa.PrivateKey, accounts.Account, string) { + t.Helper() + dir := t.TempDir() + ks := keystore.NewPlaintextKeyStore(dir) + key, err := crypto.GenerateKey() + require.NoError(t, err) + acct, err := ks.ImportECDSA(key, "") + require.NoError(t, err) + return ks, key, acct, dir +} + +func mockTurnkeyDigestSigner(t *testing.T, key *ecdsa.PrivateKey, onDigest func([]byte)) func(string, string, string) (string, string, string, error) { + t.Helper() + return func(_, _, payloadHex string) (string, string, string, error) { + digest, err := hex.DecodeString(payloadHex) + require.NoError(t, err) + require.Len(t, digest, 32, "Turnkey must receive the final 32-byte Ethereum digest; KECCAK256 here would double-hash the payload") + if onDigest != nil { + onDigest(append([]byte(nil), digest...)) + } + sig, err := crypto.Sign(digest, key) + require.NoError(t, err) + return hex.EncodeToString(sig[0:32]), + hex.EncodeToString(sig[32:64]), + hex.EncodeToString([]byte{sig[64]}), + nil + } +} + +func TestTurnkeySignMatchesKeystore(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, dir := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + am, err := NewAccountManager(acct.Address, dir, chainID, "") + require.NoError(err) + require.NoError(am.Unlock("")) + + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + tk.signRawPayloadFn = mockTurnkeyDigestSigner(t, key, nil) + + msg := []byte("livepeer turnkey sign parity") + sigKs, err := am.Sign(msg) + require.NoError(err) + sigTk, err := tk.Sign(msg) + require.NoError(err) + assert.Equal(sigKs, sigTk) + assert.True(lpcrypto.VerifySig(acct.Address, msg, sigTk)) +} + +func TestTurnkeySignTypedDataMatchesKeystore(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, dir := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + am, err := NewAccountManager(acct.Address, dir, chainID, "") + require.NoError(err) + require.NoError(am.Unlock("")) + + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + tk.signRawPayloadFn = mockTurnkeyDigestSigner(t, key, nil) + + var td apitypes.TypedData + require.NoError(json.Unmarshal([]byte(jsonTypedData), &td)) + + sigKs, err := am.SignTypedData(td) + require.NoError(err) + sigTk, err := tk.SignTypedData(td) + require.NoError(err) + assert.Equal(sigKs, sigTk) +} + +func TestTurnkeySignRawPayloadUsesPrecomputedEthereumDigests(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, _ := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + + var seen [][]byte + tk.signRawPayloadFn = mockTurnkeyDigestSigner(t, key, func(digest []byte) { + seen = append(seen, digest) + }) + + msg := []byte("turnkey prehashed message") + _, err := tk.Sign(msg) + require.NoError(err) + require.Len(seen, 1) + assert.Equal(accounts.TextHash(msg), seen[0]) + + var td apitypes.TypedData + require.NoError(json.Unmarshal([]byte(jsonTypedData), &td)) + _, err = tk.SignTypedData(td) + require.NoError(err) + require.Len(seen, 2) + + domainSeparator, err := td.HashStruct("EIP712Domain", td.Domain.Map()) + require.NoError(err) + typedDataHash, err := td.HashStruct(td.PrimaryType, td.Message) + require.NoError(err) + rawData := []byte("\x19\x01" + string(domainSeparator) + string(typedDataHash)) + assert.Equal(crypto.Keccak256(rawData), seen[1]) +} + +func TestTurnkeySignTxMatchesKeystore(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, _ := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + signer := types.LatestSignerForChainID(chainID) + + to := ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + tx := types.NewTx(&types.LegacyTx{ + Nonce: 3, + To: &to, + Value: big.NewInt(1000), + Gas: 21000, + GasPrice: big.NewInt(5), + Data: nil, + }) + + sigKs, err := types.SignTx(tx, signer, key) + require.NoError(err) + ksRaw, err := sigKs.MarshalBinary() + require.NoError(err) + + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + tk.signTransactionFn = mockTurnkeyTxSigner(t, key, chainID, tx) + + sigTk, err := tk.SignTx(tx) + require.NoError(err) + tkRaw, err := sigTk.MarshalBinary() + require.NoError(err) + assert.Equal(ksRaw, tkRaw) +} + +func TestTurnkeySignTxMatchesKeystore_DynamicFee(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, _ := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + signer := types.LatestSignerForChainID(chainID) + + to := ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + // abi/bind omits ChainID on DynamicFeeTx; local signers inject it at hash time. + tx := types.NewTx(&types.DynamicFeeTx{ + Nonce: 3, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(5), + Gas: 21000, + To: &to, + Value: big.NewInt(1000), + Data: nil, + }) + require.True(tx.ChainId() == nil || tx.ChainId().Sign() == 0) + + sigKs, err := types.SignTx(tx, signer, key) + require.NoError(err) + ksRaw, err := sigKs.MarshalBinary() + require.NoError(err) + + unsigned, err := marshalUnsignedEthereumTx(tx, chainID) + require.NoError(err) + mb, err := tx.MarshalBinary() + require.NoError(err) + assert.NotEqual(hex.EncodeToString(mb), hex.EncodeToString(unsigned)) + assert.Equal(byte(types.DynamicFeeTxType), unsigned[0]) + // Encoded chain id must be the fallback (777 = 0x0309), not empty/0. + assert.Contains(hex.EncodeToString(unsigned), "820309") + + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + tk.signTransactionFn = mockTurnkeyTxSigner(t, key, chainID, tx) + + sigTk, err := tk.SignTx(tx) + require.NoError(err) + tkRaw, err := sigTk.MarshalBinary() + require.NoError(err) + assert.Equal(ksRaw, tkRaw) +} + +func mockTurnkeyTxSigner( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + tx *types.Transaction, +) func(orgID, signWith, unsignedTxHex string) (string, error) { + t.Helper() + return func(_, _, unsignedHex string) (string, error) { + want, err := marshalUnsignedEthereumTx(tx, chainID) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(want), unsignedHex) + + signer := types.LatestSignerForChainID(chainID) + sig, err := crypto.Sign(signer.Hash(tx).Bytes(), key) + require.NoError(t, err) + stx, err := tx.WithSignature(signer, sig) + require.NoError(t, err) + out, err := stx.MarshalBinary() + require.NoError(t, err) + return hex.EncodeToString(out), nil + } +} + +func TestTurnkeyAssembleRSV_LowSAndV27(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + n := crypto.S256().Params().N + halfN := new(big.Int).Rsh(n, 1) + + key, err := crypto.GenerateKey() + require.NoError(err) + addr := crypto.PubkeyToAddress(key.PublicKey) + msg := []byte("high-s scenario") + digest := accounts.TextHash(msg) + sig, err := crypto.Sign(digest, key) + require.NoError(err) + + rHex := hex.EncodeToString(sig[0:32]) + sInt := new(big.Int).SetBytes(sig[32:64]) + // Mirror a signer that returned high-S: s' = N - s, flip recovery bit. + sInt.Sub(n, sInt) + sHex := hex.EncodeToString(fill32Bytes(sInt)) + vIn := sig[64] ^ 1 + vHex := hex.EncodeToString([]byte{vIn}) + + out, err := assembleRSV(rHex, sHex, vHex) + require.NoError(err) + require.Len(out, 65) + sOut := new(big.Int).SetBytes(out[32:64]) + assert.True(sOut.Cmp(halfN) <= 0) + assert.Contains([]byte{27, 28}, out[64]) + assert.True(lpcrypto.VerifySig(addr, msg, out)) +} + +func fill32Bytes(x *big.Int) []byte { + b := x.Bytes() + out := make([]byte, 32) + copy(out[32-len(b):], b) + return out +} + +func TestTurnkeyTicketAndInfoSigPathsMatchKeystore(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, key, acct, dir := turnkeyTestKeyAndKeystore(t) + chainID := big.NewInt(777) + am, err := NewAccountManager(acct.Address, dir, chainID, "") + require.NoError(err) + require.NoError(am.Unlock("")) + + tk := NewTurnkeyAccountManager(nil, "test-org", chainID, acct.Address) + tk.signRawPayloadFn = mockTurnkeyDigestSigner(t, key, nil) + + ticket := &pm.Ticket{ + Recipient: acct.Address, + Sender: acct.Address, + FaceValue: big.NewInt(1), + WinProb: big.NewInt(2), + SenderNonce: 3, + RecipientRandHash: ethcommon.Hash{4}, + CreationRound: 5, + CreationRoundBlockHash: ethcommon.Hash{6}, + } + th := ticket.Hash().Bytes() + sigKs, err := am.Sign(th) + require.NoError(err) + sigTk, err := tk.Sign(th) + require.NoError(err) + assert.Equal(sigKs, sigTk) + assert.True(lpcrypto.VerifySig(acct.Address, th, sigTk)) + + addrHex := []byte(acct.Address.Hex()) + pre := crypto.Keccak256(addrHex) + sigKs2, err := am.Sign(pre) + require.NoError(err) + sigTk2, err := tk.Sign(pre) + require.NoError(err) + assert.Equal(sigKs2, sigTk2) + assert.True(lpcrypto.VerifySig(acct.Address, pre, sigTk2)) + + stateJSON := []byte(`{"stateID":"x","orch":"y"}`) + sigKs3, err := am.Sign(stateJSON) + require.NoError(err) + sigTk3, err := tk.Sign(stateJSON) + require.NoError(err) + assert.Equal(sigKs3, sigTk3) + assert.True(lpcrypto.VerifySig(acct.Address, stateJSON, sigTk3)) +} + +func TestTurnkeyWithSigningAddress_SwitchesSignWith(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + _, _, acct, _ := turnkeyTestKeyAndKeystore(t) + other := ethcommon.HexToAddress("0x2222222222222222222222222222222222222222") + chainID := big.NewInt(1) + + tk := NewTurnkeyAccountManager(nil, "org", chainID, acct.Address) + var seen string + err := tk.WithSigningAddress(other, func() error { + seen = tk.activeSignWith() + return nil + }) + require.NoError(err) + assert.Equal(other.Hex(), seen) + assert.Equal(acct.Address, tk.SigningAddress()) +} diff --git a/eth/turnkey_wallets.go b/eth/turnkey_wallets.go new file mode 100644 index 0000000000..3b72fea51a --- /dev/null +++ b/eth/turnkey_wallets.go @@ -0,0 +1,197 @@ +package eth + +import ( + "fmt" + "strconv" + "strings" + + ethcommon "github.com/ethereum/go-ethereum/common" + sdk "github.com/tkhq/go-sdk" + "github.com/tkhq/go-sdk/pkg/api/client/wallets" + "github.com/tkhq/go-sdk/pkg/api/models" + "github.com/tkhq/go-sdk/pkg/util" +) + +// TurnkeyWalletAccount is one Ethereum account managed by Turnkey. +type TurnkeyWalletAccount struct { + OrganizationID string + WalletID string + AccountID string + Address ethcommon.Address +} + +// DefaultEthereumDerivationPath is the standard first-account path for Ethereum (BIP-44). +const DefaultEthereumDerivationPath = `m/44'/60'/0'/0/0` + +func ethereumWalletAccountParams(path string) *models.WalletAccountParams { + return &models.WalletAccountParams{ + AddressFormat: models.AddressFormatEthereum.Pointer(), + Curve: models.CurveSecp256k1.Pointer(), + PathFormat: models.PathFormatBip32.Pointer(), + Path: &path, + } +} + +// ListTurnkeyEthereumAccounts returns all Ethereum-format wallet accounts in the organization. +func ListTurnkeyEthereumAccounts(c *sdk.Client, orgID string) ([]TurnkeyWalletAccount, error) { + if c == nil { + return nil, fmt.Errorf("turnkey client is nil") + } + params := wallets.NewGetWalletAccountsParams().WithBody(&models.GetWalletAccountsRequest{ + OrganizationID: &orgID, + }) + resp, err := c.V0().Wallets.GetWalletAccounts(params, c.Authenticator) + if err != nil { + return nil, err + } + payload := resp.GetPayload() + if payload == nil { + return nil, nil + } + var out []TurnkeyWalletAccount + for _, a := range payload.Accounts { + if a == nil || a.Address == nil || a.AddressFormat == nil { + continue + } + if *a.AddressFormat != models.AddressFormatEthereum { + continue + } + wid := "" + if a.WalletID != nil { + wid = *a.WalletID + } + aid := "" + if a.WalletAccountID != nil { + aid = *a.WalletAccountID + } + out = append(out, TurnkeyWalletAccount{ + OrganizationID: orgID, + WalletID: wid, + AccountID: aid, + Address: ethcommon.HexToAddress(*a.Address), + }) + } + return out, nil +} + +// TurnkeyCreateWallet creates a new HD wallet with one derived Ethereum account. +func TurnkeyCreateWallet(c *sdk.Client, orgID, walletName string) (walletID string, firstAddress ethcommon.Address, err error) { + if c == nil { + return "", ethcommon.Address{}, fmt.Errorf("turnkey client is nil") + } + act := models.CreateWalletRequestTypeACTIVITYTYPECREATEWALLET + path := DefaultEthereumDerivationPath + req := &models.CreateWalletRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: &act, + Parameters: &models.CreateWalletIntent{ + WalletName: &walletName, + Accounts: []*models.WalletAccountParams{ + ethereumWalletAccountParams(path), + }, + }, + } + resp, err := c.V0().Wallets.CreateWallet(wallets.NewCreateWalletParams().WithBody(req), c.Authenticator) + if err != nil { + return "", ethcommon.Address{}, err + } + if resp.Payload == nil || resp.Payload.Activity == nil || resp.Payload.Activity.Result == nil { + return "", ethcommon.Address{}, fmt.Errorf("turnkey CreateWallet: empty activity result") + } + res := resp.Payload.Activity.Result.CreateWalletResult + if res == nil || res.WalletID == nil || len(res.Addresses) == 0 { + return "", ethcommon.Address{}, fmt.Errorf("turnkey CreateWallet: missing walletId or addresses") + } + return *res.WalletID, ethcommon.HexToAddress(res.Addresses[0]), nil +} + +// TurnkeyCreateWalletAccount derives a new Ethereum account on an existing wallet using the next BIP-44 index. +func TurnkeyCreateWalletAccount(c *sdk.Client, orgID, walletID string) (addr ethcommon.Address, err error) { + if c == nil { + return ethcommon.Address{}, fmt.Errorf("turnkey client is nil") + } + idx, err := nextEthereumDerivationIndex(c, orgID, walletID) + if err != nil { + return ethcommon.Address{}, err + } + path := fmt.Sprintf(`m/44'/60'/0'/0/%d`, idx) + act := models.CreateWalletAccountsRequestTypeACTIVITYTYPECREATEWALLETACCOUNTS + req := &models.CreateWalletAccountsRequest{ + OrganizationID: &orgID, + TimestampMs: util.RequestTimestamp(), + Type: &act, + Parameters: &models.CreateWalletAccountsIntent{ + WalletID: &walletID, + Accounts: []*models.WalletAccountParams{ + ethereumWalletAccountParams(path), + }, + }, + } + resp, err := c.V0().Wallets.CreateWalletAccounts(wallets.NewCreateWalletAccountsParams().WithBody(req), c.Authenticator) + if err != nil { + return ethcommon.Address{}, err + } + if resp.Payload == nil || resp.Payload.Activity == nil || resp.Payload.Activity.Result == nil { + return ethcommon.Address{}, fmt.Errorf("turnkey CreateWalletAccounts: empty activity result") + } + res := resp.Payload.Activity.Result.CreateWalletAccountsResult + if res == nil || len(res.Addresses) == 0 { + return ethcommon.Address{}, fmt.Errorf("turnkey CreateWalletAccounts: missing addresses") + } + return ethcommon.HexToAddress(res.Addresses[0]), nil +} + +func nextEthereumDerivationIndex(c *sdk.Client, orgID, walletID string) (int, error) { + wid := walletID + params := wallets.NewGetWalletAccountsParams().WithBody(&models.GetWalletAccountsRequest{ + OrganizationID: &orgID, + WalletID: &wid, + }) + resp, err := c.V0().Wallets.GetWalletAccounts(params, c.Authenticator) + if err != nil { + return 0, err + } + maxIdx := -1 + if resp.Payload != nil { + for _, a := range resp.Payload.Accounts { + if a == nil || a.Path == nil || a.AddressFormat == nil { + continue + } + if *a.AddressFormat != models.AddressFormatEthereum { + continue + } + idx, ok := parseBIP44EthereumIndex(*a.Path) + if ok && idx > maxIdx { + maxIdx = idx + } + } + } + return maxIdx + 1, nil +} + +func parseBIP44EthereumIndex(path string) (int, bool) { + path = strings.TrimSpace(path) + const prefix = `m/44'/60'/0'/0/` + if !strings.HasPrefix(path, prefix) { + return 0, false + } + tail := strings.TrimSpace(path[len(prefix):]) + if tail == "" { + return 0, false + } + n, err := strconv.Atoi(tail) + if err != nil { + return 0, false + } + return n, true +} + +// TurnkeyAddressMap builds a lookup from Ethereum address to wallet metadata. +func TurnkeyAddressMap(accts []TurnkeyWalletAccount) map[ethcommon.Address]TurnkeyWalletAccount { + m := make(map[ethcommon.Address]TurnkeyWalletAccount) + for _, a := range accts { + m[a.Address] = a + } + return m +} diff --git a/go.mod b/go.mod index 3aa10c761e..6f0701badb 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/segmentio/kafka-go v0.4.47 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.34.0 + github.com/tkhq/go-sdk v0.17.0 github.com/urfave/cli v1.22.12 go.opencensus.io v0.24.0 go.uber.org/goleak v1.3.0 @@ -47,8 +48,19 @@ require ( ) require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asticode/go-astikit v0.30.0 // indirect github.com/asticode/go-astits v1.13.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.4 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.26.0 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/strfmt v0.21.7 // indirect + github.com/go-openapi/validate v0.22.1 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/ice/v4 v4.0.7 // indirect @@ -62,6 +74,7 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.33.0 // indirect @@ -99,7 +112,7 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/deepmap/oapi-codegen v1.6.0 // indirect @@ -194,7 +207,7 @@ require ( github.com/minio/minio-go/v7 v7.0.66 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -220,7 +233,7 @@ require ( github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polydawn/refmt v0.89.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.3.0 // indirect diff --git a/go.sum b/go.sum index 26a0e656bd..bab1e92cf8 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= @@ -82,6 +84,9 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= @@ -126,6 +131,8 @@ github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86c github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 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/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -175,8 +182,9 @@ github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= @@ -278,12 +286,45 @@ github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AE github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -292,6 +333,30 @@ github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -341,6 +406,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -355,6 +421,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= @@ -385,6 +452,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -531,6 +599,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -554,6 +623,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/karalabe/usb v0.0.2 h1:M6QQBNxF+CQ8OFvxrT90BA0qBOXymndZnk5q235mFc4= github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= @@ -563,6 +634,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= @@ -572,6 +644,7 @@ github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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.2/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/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= @@ -630,8 +703,11 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -677,8 +753,10 @@ github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= @@ -705,6 +783,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= @@ -749,12 +828,15 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oapi-codegen/nethttp-middleware v1.0.1 h1:ZWvwfnMU0eloHX1VEJmQscQm3741t0vCm0eSIie1NIo= github.com/oapi-codegen/nethttp-middleware v1.0.1/go.mod h1:P7xtAvpoqNB+5obR9qRCeefH7YlXWSK3KgPs/9WB8tE= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -779,6 +861,7 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= @@ -829,8 +912,9 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= -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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= @@ -876,6 +960,8 @@ github.com/rabbitmq/rabbitmq-stream-go-client v1.1.1/go.mod h1:2pRPe6/8y2ZenIbnu github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -904,6 +990,8 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 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= @@ -921,6 +1009,7 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -953,6 +1042,9 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tkhq/go-sdk v0.17.0 h1:w9o3d8j0rgGv6vlQ3iVNR+sGlckqSv5AIwhoBNpHZX4= +github.com/tkhq/go-sdk v0.17.0/go.mod h1:/tvvLtpziuRSRPxRy+vMgujdWGGcwMlrUMOB5BidcH8= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -989,8 +1081,12 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1000,6 +1096,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= @@ -1011,6 +1108,11 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1059,17 +1161,20 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= @@ -1149,6 +1254,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1173,6 +1279,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ 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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1194,10 +1301,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1237,6 +1347,7 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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= @@ -1302,9 +1413,13 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1455,6 +1570,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks 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-20200227125254-8fa46927fb4f/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.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1479,6 +1595,8 @@ 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-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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= diff --git a/install_ffmpeg.sh b/install_ffmpeg.sh index 5401791a6c..ad4ab22897 100755 --- a/install_ffmpeg.sh +++ b/install_ffmpeg.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash echo 'WARNING: downloading and executing lpms/install_ffmpeg.sh, use it directly in case of issues' -curl https://raw.githubusercontent.com/livepeer/lpms/5fe66b1044f876b47248c36aed581622fbbe093d/install_ffmpeg.sh | bash -s $1 +curl https://raw.githubusercontent.com/livepeer/lpms/bc67cafe732afb08053328483f73cf000a28d330/install_ffmpeg.sh | bash -s $1 diff --git a/server/cliserver_test.go b/server/cliserver_test.go index 6b3f9800cc..9032fcc774 100644 --- a/server/cliserver_test.go +++ b/server/cliserver_test.go @@ -254,6 +254,27 @@ func TestGetContractAddresses(t *testing.T) { assert.Equal("{}", string(body)) } +func TestTurnkeyRoutesRegisteredOnCliServer(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + n, _ := core.NewLivepeerNode(ð.StubClient{}, "./tmp", nil) + n.TurnkeyMode = true + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + s, _ := NewLivepeerServer(ctx, "127.0.0.1:1938", n, true, "") + mux := s.cliWebServerHandlers("addr") + srv := httptest.NewServer(mux) + defer srv.Close() + + res, err := http.Get(fmt.Sprintf("%s/turnkey/wallets", srv.URL)) + require.NoError(err) + defer res.Body.Close() + // Route is registered on the CLI server; without Turnkey config the handler + // itself returns service unavailable. + assert.Equal(http.StatusServiceUnavailable, res.StatusCode) +} + func TestGetDelegatorInfo(t *testing.T) { srv := newMockServer() defer srv.Close() @@ -342,8 +363,7 @@ func TestRegisteredOrchestrators(t *testing.T) { eth.TranscoderPoolError = errors.New("error") res, err = http.Get(fmt.Sprintf("%s/registeredOrchestrators", srv.URL)) + require.Nil(err) defer res.Body.Close() - - assert.Nil(err) assert.Equal(http.StatusInternalServerError, res.StatusCode) } diff --git a/server/live_payment.go b/server/live_payment.go index 63a37a21ba..d0e493ae58 100644 --- a/server/live_payment.go +++ b/server/live_payment.go @@ -239,6 +239,9 @@ func (r *remotePaymentSender) RequestPayment(ctx context.Context, segmentInfo *S Type: RemoteType_LiveVideoToVideo, Capabilities: capsBytes, } + if r.node.GatewayRemoteSignerAddress != (ethcommon.Address{}) { + reqPayload.SignerAddress = r.node.GatewayRemoteSignerAddress.Hex() + } body, err := json.Marshal(reqPayload) if err != nil { diff --git a/server/mediaserver.go b/server/mediaserver.go index 45737feef0..4d4613330a 100644 --- a/server/mediaserver.go +++ b/server/mediaserver.go @@ -12,6 +12,7 @@ import ( "io" "io/ioutil" "math/big" + "sort" "mime/multipart" "net" "net/http" @@ -37,6 +38,7 @@ import ( "github.com/livepeer/go-livepeer/common" "github.com/livepeer/go-livepeer/core" lpmscore "github.com/livepeer/lpms/core" + sdk "github.com/tkhq/go-sdk" ffmpeg "github.com/livepeer/lpms/ffmpeg" "github.com/livepeer/lpms/segmenter" "github.com/livepeer/lpms/stream" @@ -133,6 +135,9 @@ type LivepeerServer struct { outSegmentTimeout time.Duration byocSrv *byoc.BYOCGatewayServer + + // TurnkeyAdmin is non-nil for remote signers using Turnkey (wallet HTTP API). + TurnkeyAdmin *sdk.Client } func (s *LivepeerServer) SetContextFromUnitTest(c context.Context) { @@ -1641,6 +1646,18 @@ func (s *LivepeerServer) GetNodeStatus() *common.NodeStatus { res.BroadcasterPrices = s.LivepeerNode.GetBasePrices() + if s.LivepeerNode != nil { + res.TurnkeyMode = s.LivepeerNode.TurnkeyMode + res.TurnkeyOrgID = s.LivepeerNode.TurnkeyOrgID + addrs := s.LivepeerNode.TurnkeyAddressList() + sort.Slice(addrs, func(i, j int) bool { + return addrs[i].Hex() < addrs[j].Hex() + }) + for _, a := range addrs { + res.TurnkeyAddresses = append(res.TurnkeyAddresses, a.Hex()) + } + } + return res } diff --git a/server/remote_signer.go b/server/remote_signer.go index f1a3fb548a..896be0a19b 100644 --- a/server/remote_signer.go +++ b/server/remote_signer.go @@ -38,6 +38,19 @@ func (ls *LivepeerServer) SignOrchestratorInfo(w http.ResponseWriter, r *http.Re remoteAddr := getRemoteAddr(r) clog.Info(ctx, "Orch info signature request", "ip", remoteAddr) + if tk := ls.LivepeerNode.TurnkeyAccount; tk != nil { + if q := strings.TrimSpace(r.URL.Query().Get("address")); q != "" { + addr := ethcommon.HexToAddress(q) + if !ls.LivepeerNode.TurnkeySigningAddressAllowed(addr) { + respondJsonError(ctx, w, fmt.Errorf("unknown or disallowed signing address"), http.StatusBadRequest) + return + } + prev := tk.SigningAddress() + tk.SetSigningAddress(addr) + defer tk.SetSigningAddress(prev) + } + } + // Get the broadcaster (signer) // In remote signer mode, we may not have an OrchestratorPool, so create a broadcaster directly gw := core.NewBroadcaster(ls.LivepeerNode) @@ -70,9 +83,7 @@ func (ls *LivepeerServer) SignOrchestratorInfo(w http.ResponseWriter, r *http.Re _ = json.NewEncoder(w).Encode(results) } -// StartRemoteSignerServer starts the HTTP server for remote signer mode -func StartRemoteSignerServer(ls *LivepeerServer, bind string) error { - // Register the remote signer endpoints +func registerRemoteSignerHandlers(ls *LivepeerServer) { ls.HTTPMux.Handle("POST /sign-orchestrator-info", http.HandlerFunc(ls.SignOrchestratorInfo)) ls.HTTPMux.Handle("POST /generate-live-payment", http.HandlerFunc(ls.GenerateLivePayment)) if ls.LivepeerNode.RemoteDiscovery { @@ -85,6 +96,11 @@ func StartRemoteSignerServer(ls *LivepeerServer, bind string) error { ls.GetOrchestrators(rdp, w, r) })) } +} + +// StartRemoteSignerServer starts the HTTP server for remote signer mode +func StartRemoteSignerServer(ls *LivepeerServer, bind string) error { + registerRemoteSignerHandlers(ls) // Start the HTTP server glog.Info("Starting Remote Signer server on ", bind) @@ -144,6 +160,7 @@ type RemotePaymentState struct { PMSessionID string LastUpdate time.Time OrchestratorAddress ethcommon.Address + SignerAddress string `json:"signerAddress,omitempty"` AuthExpiry int64 SenderNonce uint32 Balance string @@ -178,6 +195,9 @@ type RemotePaymentRequest struct { // Capabilities to include in the ticket. Optional; may be set for the lv2v job type. Capabilities []byte `json:"capabilities"` + + // Optional Ethereum address (0x…) selecting which Turnkey identity signs (remote signer Turnkey mode). + SignerAddress string `json:"signerAddress,omitempty"` } // Returned by the remote signer and includes a new payment plus updated state. @@ -217,12 +237,18 @@ func signState(ls *LivepeerServer, stateBytes []byte) ([]byte, error) { } // verifyStateSignature verifies that sig is a valid signature over stateBytes produced -// by the remote signer's Ethereum account. -func verifyStateSignature(ls *LivepeerServer, stateBytes []byte, sig []byte) error { +// by the remote signer's Ethereum account (or the address recorded in state for Turnkey multi-address). +func verifyStateSignature(ls *LivepeerServer, stateBytes []byte, sig []byte, state *RemotePaymentState) error { if ls == nil || ls.LivepeerNode == nil || ls.LivepeerNode.Eth == nil { return fmt.Errorf("ethereum client not configured for remote signer") } addr := ls.LivepeerNode.Eth.Account().Address + if state != nil && state.SignerAddress != "" { + addr = ethcommon.HexToAddress(state.SignerAddress) + } + if ls.LivepeerNode.TurnkeyAccount != nil && !ls.LivepeerNode.TurnkeySigningAddressAllowed(addr) { + return fmt.Errorf("signer address not allowed for Turnkey remote signer") + } if !lpcrypto.VerifySig(addr, stateBytes, sig) { return fmt.Errorf("invalid state signature") } @@ -342,13 +368,13 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req reqState, reqSig := req.State.State, req.State.Sig hasState := len(reqState) != 0 || len(reqSig) != 0 if hasState { - if err := verifyStateSignature(ls, reqState, reqSig); err != nil { - err = errors.New("invalid sig") + if err := json.Unmarshal(reqState, &state); err != nil { + err = errors.New("invalid state") respondJsonError(ctx, w, err, http.StatusBadRequest) return } - if err := json.Unmarshal(reqState, &state); err != nil { - err = errors.New("invalid state") + if err := verifyStateSignature(ls, reqState, reqSig, state); err != nil { + err = errors.New("invalid sig") respondJsonError(ctx, w, err, http.StatusBadRequest) return } @@ -357,6 +383,13 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req respondJsonError(ctx, w, err, http.StatusBadRequest) return } + if q := strings.TrimSpace(req.SignerAddress); q != "" && state.SignerAddress != "" { + if !strings.EqualFold(q, state.SignerAddress) { + err := fmt.Errorf("signerAddress does not match signed state") + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + } state.SequenceNumber++ } else { state = &RemotePaymentState{ @@ -365,6 +398,37 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req InitialPricePerUnit: priceInfo.PricePerUnit, InitialPixelsPerUnit: priceInfo.PixelsPerUnit, } + if q := strings.TrimSpace(req.SignerAddress); q != "" { + state.SignerAddress = ethcommon.HexToAddress(q).Hex() + } else { + state.SignerAddress = ls.LivepeerNode.Eth.Account().Address.Hex() + } + } + + effectiveSigner := ls.LivepeerNode.Eth.Account().Address + if state.SignerAddress != "" { + effectiveSigner = ethcommon.HexToAddress(state.SignerAddress) + } + if q := strings.TrimSpace(req.SignerAddress); q != "" { + reqAddr := ethcommon.HexToAddress(q) + if state.SignerAddress != "" && reqAddr != effectiveSigner { + err := fmt.Errorf("signerAddress does not match payment state") + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + if state.SignerAddress == "" { + state.SignerAddress = reqAddr.Hex() + effectiveSigner = reqAddr + } + } + if tk := ls.LivepeerNode.TurnkeyAccount; tk != nil { + if !ls.LivepeerNode.TurnkeySigningAddressAllowed(effectiveSigner) { + respondJsonError(ctx, w, fmt.Errorf("signer address not allowed"), http.StatusBadRequest) + return + } + prev := tk.SigningAddress() + tk.SetSigningAddress(effectiveSigner) + defer tk.SetSigningAddress(prev) } stateID := core.ManifestID(state.StateID) @@ -650,17 +714,23 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req _ = json.NewEncoder(w).Encode(resp) } -// Gateway helper that calls the remote signer service for the GetOrchestratorInfo signature -func GetOrchInfoSig(remoteSignerHost *url.URL, headers map[string]string) (*OrchInfoSigResponse, error) { +// Gateway helper that calls the remote signer service for the GetOrchestratorInfo signature. +// When address is non-empty, it is passed as ?address=0x… for Turnkey multi-address signers. +func GetOrchInfoSig(remoteSignerHost *url.URL, address string, headers map[string]string) (*OrchInfoSigResponse, error) { - url := remoteSignerHost.ResolveReference(&url.URL{Path: "/sign-orchestrator-info"}) + u := remoteSignerHost.ResolveReference(&url.URL{Path: "/sign-orchestrator-info"}) + if strings.TrimSpace(address) != "" { + q := u.Query() + q.Set("address", strings.TrimSpace(address)) + u.RawQuery = q.Encode() + } // Create HTTP client with timeout client := &http.Client{ Timeout: 30 * time.Second, } - req, err := http.NewRequest(http.MethodPost, url.String(), nil) + req, err := http.NewRequest(http.MethodPost, u.String(), nil) if err != nil { return nil, err } diff --git a/server/remote_signer_test.go b/server/remote_signer_test.go index 375eb82c39..50211273c7 100644 --- a/server/remote_signer_test.go +++ b/server/remote_signer_test.go @@ -1319,12 +1319,32 @@ func TestRemoteSigner_Discovery_RefreshesAfterInterval(t *testing.T) { }) } +func TestRegisterRemoteSignerHandlers_DoesNotExposeTurnkeyRoutes(t *testing.T) { + require := require.New(t) + + ls := &LivepeerServer{ + HTTPMux: http.NewServeMux(), + LivepeerNode: &core.LivepeerNode{ + TurnkeyMode: true, + RemoteDiscovery: false, + }, + } + + registerRemoteSignerHandlers(ls) + + req := httptest.NewRequest(http.MethodGet, "/turnkey/wallets", nil) + rr := httptest.NewRecorder() + ls.HTTPMux.ServeHTTP(rr, req) + require.Equal(http.StatusNotFound, rr.Code) +} + func TestGetOrchInfoSig_SendsConfiguredHeaders(t *testing.T) { require := require.New(t) remoteTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(http.MethodPost, r.Method) require.Equal("/sign-orchestrator-info", r.URL.Path) + require.Equal("0x1234", r.URL.Query().Get("address")) require.Equal("application/json", r.Header.Get("Content-Type")) require.Equal("Bearer gateway-token", r.Header.Get("Authorization")) @@ -1339,7 +1359,7 @@ func TestGetOrchInfoSig_SendsConfiguredHeaders(t *testing.T) { remoteURL, err := url.Parse(remoteTS.URL) require.NoError(err) - resp, err := GetOrchInfoSig(remoteURL, map[string]string{"Authorization": "Bearer gateway-token"}) + resp, err := GetOrchInfoSig(remoteURL, "0x1234", map[string]string{"Authorization": "Bearer gateway-token"}) require.NoError(err) require.Equal([]byte{0x12, 0x34}, []byte(resp.Address)) require.Equal([]byte{0xab, 0xcd}, []byte(resp.Signature)) diff --git a/server/turnkey_http.go b/server/turnkey_http.go new file mode 100644 index 0000000000..6456f502ea --- /dev/null +++ b/server/turnkey_http.go @@ -0,0 +1,169 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/golang/glog" + "github.com/livepeer/go-livepeer/clog" + "github.com/livepeer/go-livepeer/core" + "github.com/livepeer/go-livepeer/eth" +) + +type turnkeyWalletRow struct { + WalletID string `json:"walletId"` + AccountID string `json:"accountId"` + Address string `json:"address"` +} + +// TurnkeyListWallets returns Ethereum accounts in the Turnkey org (remote signer). +func (ls *LivepeerServer) TurnkeyListWallets(w http.ResponseWriter, r *http.Request) { + ctx := clog.AddVal(r.Context(), "request_id", string(core.RandomManifestID())) + if ls.TurnkeyAdmin == nil || ls.LivepeerNode == nil || ls.LivepeerNode.TurnkeyOrgID == "" { + respondJsonError(ctx, w, errTurnkeyNotConfigured, http.StatusServiceUnavailable) + return + } + accts, err := eth.ListTurnkeyEthereumAccounts(ls.TurnkeyAdmin, ls.LivepeerNode.TurnkeyOrgID) + if err != nil { + clog.Errorf(ctx, "turnkey list wallets: %v", err) + respondJsonError(ctx, w, err, http.StatusBadGateway) + return + } + rows := make([]turnkeyWalletRow, 0, len(accts)) + for _, a := range accts { + rows = append(rows, turnkeyWalletRow{ + WalletID: a.WalletID, + AccountID: a.AccountID, + Address: a.Address.Hex(), + }) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(rows) +} + +type turnkeyCreateWalletBody struct { + WalletName string `json:"walletName"` +} + +// TurnkeyCreateWalletHTTP creates a new Turnkey HD wallet with a default Ethereum account. +func (ls *LivepeerServer) TurnkeyCreateWalletHTTP(w http.ResponseWriter, r *http.Request) { + ctx := clog.AddVal(r.Context(), "request_id", string(core.RandomManifestID())) + if ls.TurnkeyAdmin == nil || ls.LivepeerNode == nil || ls.LivepeerNode.TurnkeyOrgID == "" { + respondJsonError(ctx, w, errTurnkeyNotConfigured, http.StatusServiceUnavailable) + return + } + var body turnkeyCreateWalletBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + name := strings.TrimSpace(body.WalletName) + if name == "" { + respondJsonError(ctx, w, errTurnkeyWalletNameRequired, http.StatusBadRequest) + return + } + wid, addr, err := eth.TurnkeyCreateWallet(ls.TurnkeyAdmin, ls.LivepeerNode.TurnkeyOrgID, name) + if err != nil { + clog.Errorf(ctx, "turnkey create wallet: %v", err) + respondJsonError(ctx, w, err, http.StatusBadGateway) + return + } + ls.refreshTurnkeyAddressBook(ctx) + glog.Info("Turnkey wallet created ", "walletId", wid, "address", addr.Hex()) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "walletId": wid, + "address": addr.Hex(), + }) +} + +type turnkeyCreateAccountBody struct { + WalletID string `json:"walletId"` +} + +// TurnkeyCreateAccountHTTP derives another Ethereum address on an existing wallet. +func (ls *LivepeerServer) TurnkeyCreateAccountHTTP(w http.ResponseWriter, r *http.Request) { + ctx := clog.AddVal(r.Context(), "request_id", string(core.RandomManifestID())) + if ls.TurnkeyAdmin == nil || ls.LivepeerNode == nil || ls.LivepeerNode.TurnkeyOrgID == "" { + respondJsonError(ctx, w, errTurnkeyNotConfigured, http.StatusServiceUnavailable) + return + } + var body turnkeyCreateAccountBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + wid := strings.TrimSpace(body.WalletID) + if wid == "" { + respondJsonError(ctx, w, errTurnkeyWalletIDRequired, http.StatusBadRequest) + return + } + addr, err := eth.TurnkeyCreateWalletAccount(ls.TurnkeyAdmin, ls.LivepeerNode.TurnkeyOrgID, wid) + if err != nil { + clog.Errorf(ctx, "turnkey create account: %v", err) + respondJsonError(ctx, w, err, http.StatusBadGateway) + return + } + ls.refreshTurnkeyAddressBook(ctx) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"address": addr.Hex()}) +} + +type turnkeySelectAddressBody struct { + Address string `json:"address"` +} + +// TurnkeySelectAddressHTTP sets the default Turnkey signing address for this remote signer. +func (ls *LivepeerServer) TurnkeySelectAddressHTTP(w http.ResponseWriter, r *http.Request) { + ctx := clog.AddVal(r.Context(), "request_id", string(core.RandomManifestID())) + if ls.LivepeerNode == nil || ls.LivepeerNode.TurnkeyAccount == nil { + respondJsonError(ctx, w, errTurnkeyNotConfigured, http.StatusServiceUnavailable) + return + } + var body turnkeySelectAddressBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + addr := ethcommon.HexToAddress(strings.TrimSpace(body.Address)) + if addr == (ethcommon.Address{}) { + respondJsonError(ctx, w, errTurnkeyAddressRequired, http.StatusBadRequest) + return + } + if !ls.LivepeerNode.TurnkeySigningAddressAllowed(addr) { + respondJsonError(ctx, w, errTurnkeyAddressNotAllowed, http.StatusBadRequest) + return + } + ls.LivepeerNode.TurnkeyAccount.SetSigningAddress(addr) + glog.Info("Turnkey default signing address set to ", addr.Hex()) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"address": addr.Hex()}) +} + +func (ls *LivepeerServer) refreshTurnkeyAddressBook(ctx context.Context) { + if ls.TurnkeyAdmin == nil || ls.LivepeerNode == nil || ls.LivepeerNode.TurnkeyOrgID == "" { + return + } + accts, err := eth.ListTurnkeyEthereumAccounts(ls.TurnkeyAdmin, ls.LivepeerNode.TurnkeyOrgID) + if err != nil { + clog.Errorf(ctx, "refresh turnkey address book: %v", err) + return + } + list := make([]ethcommon.Address, 0, len(accts)) + for _, a := range accts { + list = append(list, a.Address) + } + ls.LivepeerNode.ReplaceTurnkeyAddressBook(list) +} + +var ( + errTurnkeyNotConfigured = errors.New("turnkey not configured on this node") + errTurnkeyWalletNameRequired = errors.New("walletName is required") + errTurnkeyWalletIDRequired = errors.New("walletId is required") + errTurnkeyAddressRequired = errors.New("address is required") + errTurnkeyAddressNotAllowed = errors.New("address is not in the Turnkey address book") +) diff --git a/server/webserver.go b/server/webserver.go index d05e0c399f..6ce70ef924 100644 --- a/server/webserver.go +++ b/server/webserver.go @@ -112,6 +112,15 @@ func (s *LivepeerServer) cliWebServerHandlers(bindAddr string) *http.ServeMux { mux.Handle("/getLogLevel", getLogLevelHandler()) mux.Handle("/debug", s.debugHandler()) + if s.LivepeerNode != nil && s.LivepeerNode.TurnkeyMode { + // Turnkey management endpoints are intentionally exposed on the + // CLI listener only, not on the remote signer -http listener. + mux.Handle("GET /turnkey/wallets", http.HandlerFunc(s.TurnkeyListWallets)) + mux.Handle("POST /turnkey/create-wallet", http.HandlerFunc(s.TurnkeyCreateWalletHTTP)) + mux.Handle("POST /turnkey/create-account", http.HandlerFunc(s.TurnkeyCreateAccountHTTP)) + mux.Handle("POST /turnkey/select-address", http.HandlerFunc(s.TurnkeySelectAddressHTTP)) + } + // Metrics if monitor.Enabled { mux.Handle("/metrics", monitor.Exporter)