Skip to content
Open
3 changes: 3 additions & 0 deletions cmd/livepeer/starter/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ 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.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/<name>/ (used with -turnkeyOrg)")

// Gateway metrics
cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers")
Expand Down
102 changes: 90 additions & 12 deletions cmd/livepeer/starter/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/livepeer/go-livepeer/pm"
"github.com/livepeer/go-livepeer/server"
"github.com/livepeer/go-livepeer/verification"
sdk "github.com/tkhq/go-sdk"
"github.com/livepeer/go-tools/drivers"
"github.com/livepeer/livepeer-data/pkg/event"
"github.com/livepeer/lpms/ffmpeg"
Expand Down Expand Up @@ -169,7 +170,10 @@ type LivepeerConfig struct {
TestOrchAvail *bool
RemoteSigner *bool
RemoteSignerUrl *string
RemoteSignerAddress *string
RemoteDiscovery *bool
TurnkeyOrg *string
TurnkeyApiKeyName *string
AIRunnerImage *string
AIRunnerImageOverrides *string
AIVerboseLogs *bool
Expand Down Expand Up @@ -307,7 +311,10 @@ func DefaultLivepeerConfig() LivepeerConfig {
defaultTestOrchAvail := true
defaultRemoteSigner := false
defaultRemoteSignerUrl := ""
defaultRemoteSignerAddress := ""
defaultRemoteDiscovery := false
defaultTurnkeyOrg := ""
defaultTurnkeyApiKeyName := "default"

// Gateway logs
defaultKafkaBootstrapServers := ""
Expand Down Expand Up @@ -429,9 +436,12 @@ func DefaultLivepeerConfig() LivepeerConfig {

// Flags
TestOrchAvail: &defaultTestOrchAvail,
RemoteSigner: &defaultRemoteSigner,
RemoteSignerUrl: &defaultRemoteSignerUrl,
RemoteDiscovery: &defaultRemoteDiscovery,
RemoteSigner: &defaultRemoteSigner,
RemoteSignerUrl: &defaultRemoteSignerUrl,
RemoteSignerAddress: &defaultRemoteSignerAddress,
RemoteDiscovery: &defaultRemoteDiscovery,
TurnkeyOrg: &defaultTurnkeyOrg,
TurnkeyApiKeyName: &defaultTurnkeyApiKeyName,

// Gateway logs
KafkaBootstrapServers: &defaultKafkaBootstrapServers,
Expand Down Expand Up @@ -475,6 +485,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")
Expand Down Expand Up @@ -694,6 +706,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
Expand Down Expand Up @@ -835,15 +850,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 {
Comment on lines +896 to +898

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently pick accts[0] when multiple Turnkey accounts exist.

This makes the signer identity restart-dependent and also loses any address selected at runtime via the Turnkey API. If more than one Ethereum account exists, require -ethAcctAddr or load a persisted default instead of relying on API order.

🔧 Safer startup behavior
-			} else if len(accts) > 0 {
-				signAddr = accts[0].Address
+			} else if len(accts) == 1 {
+				signAddr = accts[0].Address
+			} else if len(accts) > 1 {
+				glog.Exit("multiple Turnkey Ethereum accounts found; set -ethAcctAddr to choose the signing address")
 			} else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if len(accts) > 0 {
signAddr = accts[0].Address
} else {
} else if len(accts) == 1 {
signAddr = accts[0].Address
} else if len(accts) > 1 {
glog.Exit("multiple Turnkey Ethereum accounts found; set -ethAcctAddr to choose the signing address")
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/livepeer/starter/starter.go` around lines 881 - 883, Currently code
silently picks accts[0] for signAddr when multiple Turnkey accounts exist;
instead, update the startup logic around signAddr/accts to detect len(accts) > 1
and refuse to pick arbitrarily: if the CLI flag ethAcctAddr (or persisted
default) is provided, load and validate that address against accts; if not
provided and multiple accounts exist, return an error asking the user to supply
-ethAcctAddr (or load a persisted default selection) rather than selecting
accts[0]. Modify the logic that sets signAddr to consult ethAcctAddr and
persisted default before falling back to a single-account implicit choice.

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)
Expand Down Expand Up @@ -1602,7 +1674,12 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
}

glog.Info("Retrieving OrchestratorInfo fields from remote signer: ", url)
fields, err := server.GetOrchInfoSig(url)
pinAddr := ""
if *cfg.RemoteSignerAddress != "" {
pinAddr = *cfg.RemoteSignerAddress
n.GatewayRemoteSignerAddress = ethcommon.HexToAddress(pinAddr)
}
fields, err := server.GetOrchInfoSig(url, pinAddr)
if err != nil {
glog.Exit("Unable to query remote signer: ", err)
}
Expand Down Expand Up @@ -1833,6 +1910,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{})
Expand Down
10 changes: 10 additions & 0 deletions cmd/livepeer_cli/livepeer_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -80,6 +82,7 @@ type wizardOpt struct {
testnet bool
orchestrator bool
notOrchestrator bool
turnkeyOnly bool
}

func (w *wizard) initializeOptions() []wizardOpt {
Expand Down Expand Up @@ -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},
Expand All @@ -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)
}
Expand Down
88 changes: 88 additions & 0 deletions cmd/livepeer_cli/wizard_turnkey.go
Original file line number Diff line number Diff line change
@@ -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))
Comment on lines +18 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Go's net/http package, what timeout do http.Get/http.Post use by default when no custom http.Client timeout is set?

💡 Result:

In Go's net/http package, http.Get and http.Post use http.DefaultClient by default when no custom http.Client is provided. The DefaultClient is defined as var DefaultClient = &Client{}, which is the zero value of Client. The Client struct has a Timeout field of type time.Duration, and the documentation explicitly states: "A Timeout of zero means no timeout." The client.do method checks if c.Timeout > 0 before applying any deadline; since it is zero, no timeout is set. This behavior is consistent across Go versions, including recent ones like go1.25.1, and is confirmed by multiple secondary sources warning that the default client has no timeout, allowing requests to hang indefinitely.

Citations:


🏁 Script executed:

# Find the file and check its size
fd wizard_turnkey.go

Repository: eliteprox/go-livepeer

Length of output: 99


🏁 Script executed:

# Read the entire file to see all HTTP calls
cat -n cmd/livepeer_cli/wizard_turnkey.go

Repository: eliteprox/go-livepeer

Length of output: 3139


Add a bounded HTTP client for Turnkey wizard calls.

All four actions use the default http.Get/http.Post path, so a broken local socket can hang the CLI indefinitely. Please route these through a shared http.Client with a timeout.

🔧 Minimal fix sketch
 import (
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"strings"
+	"time"

 	"github.com/ethereum/go-ethereum/log"
 )
 
+var turnkeyHTTPClient = &http.Client{Timeout: 10 * time.Second}
+
 func (w *wizard) turnkeyListWallets() {
-	resp, err := http.Get(w.turnkeyBase() + "/turnkey/wallets")
+	resp, err := turnkeyHTTPClient.Get(w.turnkeyBase() + "/turnkey/wallets")
 	if err != nil {
 		log.Error("Turnkey list wallets failed", "err", err)
 		return
 	}
@@
-	resp, err := http.Post(w.turnkeyBase()+"/turnkey/create-wallet", "application/json", bytes.NewReader(payload))
+	resp, err := turnkeyHTTPClient.Post(w.turnkeyBase()+"/turnkey/create-wallet", "application/json", bytes.NewReader(payload))
@@
-	resp, err := http.Post(w.turnkeyBase()+"/turnkey/create-account", "application/json", bytes.NewReader(payload))
+	resp, err := turnkeyHTTPClient.Post(w.turnkeyBase()+"/turnkey/create-account", "application/json", bytes.NewReader(payload))
@@
-	resp, err := http.Post(w.turnkeyBase()+"/turnkey/select-address", "application/json", bytes.NewReader(payload))
+	resp, err := turnkeyHTTPClient.Post(w.turnkeyBase()+"/turnkey/select-address", "application/json", bytes.NewReader(payload))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/livepeer_cli/wizard_turnkey.go` around lines 18 - 30, The Turnkey calls
use the global http.Get/http.Post which can hang; add a shared http.Client with
a timeout on the wizard (e.g., field name HTTPClient or httpClient), initialize
it when the wizard is created (time.Duration like 10s), and replace uses of
http.Get/http.Post in turnkeyListWallets (and the other three Turnkey actions)
with w.HTTPClient.Get / w.HTTPClient.Post so all requests are bounded by the
configured timeout and reuse connections.

}

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))
}
6 changes: 6 additions & 0 deletions common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
54 changes: 52 additions & 2 deletions core/livepeernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,17 @@ type LivepeerNode struct {
// Gateway fields for remote signers
RemoteSignerUrl *url.URL
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
// 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{}

// Thread safety for config fields
mu sync.RWMutex
Expand Down Expand Up @@ -396,3 +405,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
Comment on lines +427 to +437

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail closed when the Turnkey address book is empty.

Once Turnkey mode is active, returning true on len(n.turnkeyAddrs) == 0 turns any address-book load/refresh failure into allow-all behavior for address selection and request routing. This should reject until the book is populated.

🔒 Suggested change
 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
+		return false
 	}
 	_, ok := n.turnkeyAddrs[addr]
 	return ok
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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 false
}
_, ok := n.turnkeyAddrs[addr]
return ok
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/livepeernode.go` around lines 423 - 433, The
TurnkeySigningAddressAllowed function currently allows all addresses when the
in-memory address book is empty; change it to "fail closed" so that when
n.TurnkeyMode is true and n.TurnkeyAccount != nil and len(n.turnkeyAddrs) == 0
the function returns false (deny) instead of true. Locate the
TurnkeySigningAddressAllowed method and replace the empty-book branch to return
false, keeping the existing nil checks and locking (turnkeyMu.RLock()/RUnlock())
and the final lookup behavior unchanged; update or add unit tests to assert the
deny-on-empty-book behavior.

}

// 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
}
Loading
Loading