Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions byoc/job_orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ func (r *mockJobOrchestrator) Sign(msg []byte) ([]byte, error) {
func (r *mockJobOrchestrator) ExtraNodes() int {
return r.extraNodes
}
func (r *mockJobOrchestrator) OrchInfoSig() []byte {
if r.node != nil && len(r.node.InfoSig) > 0 {
return r.node.InfoSig
}
sig, err := r.Sign([]byte(r.Address().Hex()))
if err != nil {
return nil
}
return sig
}
func (r *mockJobOrchestrator) VerifySig(addr ethcommon.Address, msg string, sig []byte) bool {
return r.verifySignature(addr, msg, sig)
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/livepeer/starter/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig {

// flags
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.")

// Gateway metrics
cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers")
Expand Down
63 changes: 62 additions & 1 deletion cmd/livepeer/starter/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@
OrchBlacklist *string
OrchMinLivepeerVersion *string
TestOrchAvail *bool
RemoteSigner *bool
RemoteSignerUrl *string
AIRunnerImage *string
AIRunnerImageOverrides *string
AIVerboseLogs *bool
Expand Down Expand Up @@ -302,6 +304,8 @@

// Flags
defaultTestOrchAvail := true
defaultRemoteSigner := false
defaultRemoteSignerUrl := ""

// Gateway logs
defaultKafkaBootstrapServers := ""
Expand Down Expand Up @@ -422,7 +426,9 @@
OrchMinLivepeerVersion: &defaultMinLivepeerVersion,

// Flags
TestOrchAvail: &defaultTestOrchAvail,
TestOrchAvail: &defaultTestOrchAvail,
RemoteSigner: &defaultRemoteSigner,
RemoteSignerUrl: &defaultRemoteSignerUrl,

// Gateway logs
KafkaBootstrapServers: &defaultKafkaBootstrapServers,
Expand Down Expand Up @@ -679,8 +685,17 @@
}
}

// Validate remote signer mode
if *cfg.RemoteSigner {
if *cfg.Network == "offchain" {
exit("Remote signer mode requires on-chain network")
}
}

if *cfg.Redeemer {
n.NodeType = core.RedeemerNode
} else if *cfg.RemoteSigner {
n.NodeType = core.RemoteSignerNode
} else if *cfg.Orchestrator {
n.NodeType = core.OrchestratorNode
if !*cfg.Transcoder {
Expand Down Expand Up @@ -1115,7 +1130,7 @@
}
maxPricePerUnit, currency, err := parsePricePerUnit(*cfg.MaxPricePerUnit)
if err != nil {
panic(fmt.Errorf("The maximum price per unit must be a valid integer with an optional currency, provided %v instead\n", *cfg.MaxPricePerUnit))

Check warning on line 1133 in cmd/livepeer/starter/starter.go

View workflow job for this annotation

GitHub Actions / Run tests defined for the project

error strings should not be capitalized or end with punctuation or a newline
}

if maxPricePerUnit.Sign() > 0 {
Expand Down Expand Up @@ -1567,6 +1582,40 @@
}

bcast := core.NewBroadcaster(n)

// Populate infoSig with remote signer if configured
if *cfg.RemoteSignerUrl != "" {
url, err := url.Parse(*cfg.RemoteSignerUrl)
if err != nil {
glog.Exit("Invalid remote signer URL: ", err)
}
if url.Scheme == "" || url.Host == "" {
// Usually something like `host:port` or just plain `host`
// Prepend https:// for convenience
url, err = url.Parse("https://" + *cfg.RemoteSignerUrl)
if err != nil {
glog.Exit("Adding HTTPS to remote signer URL failed: ", err)
}
}

glog.Info("Retrieving OrchestratorInfo fields from remote signer: ", url)
fields, err := server.GetOrchInfoSig(url)
if err != nil {
glog.Exit("Unable to query remote signer: ", err)
}
n.RemoteSignerUrl = url
n.RemoteEthAddr = ethcommon.BytesToAddress(fields.Address)
n.InfoSig = fields.Signature
glog.Info("Using Ethereum address from remote signer: ", n.RemoteEthAddr)
} else {
// Use local signing
infoSig, err := bcast.Sign([]byte(fmt.Sprintf("%v", bcast.Address().Hex())))
if err != nil {
glog.Exit("Unable to generate info sig: ", err)
}
n.InfoSig = infoSig
}

orchBlacklist := parseOrchBlacklist(cfg.OrchBlacklist)
if *cfg.OrchPerfStatsURL != "" && *cfg.Region != "" {
glog.Infof("Using Performance Stats, region=%s, URL=%s, minPerfScore=%v", *cfg.Region, *cfg.OrchPerfStatsURL, *cfg.MinPerfScore)
Expand Down Expand Up @@ -1793,6 +1842,18 @@
}()
}

// Start remote signer server if in remote signer mode
if n.NodeType == core.RemoteSignerNode {
go func() {
*cfg.HttpAddr = defaultAddr(*cfg.HttpAddr, "127.0.0.1", OrchestratorRpcPort)
glog.Info("Starting remote signer server on ", *cfg.HttpAddr)
err := server.StartRemoteSignerServer(s, *cfg.HttpAddr)
if err != nil {
exit("Error starting remote signer server: err=%q", err)
}
}()
}

go func() {
if core.OrchestratorNode != n.NodeType {
return
Expand Down
1 change: 1 addition & 0 deletions common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type NodeStatus struct {
type Broadcaster interface {
Address() ethcommon.Address
Sign([]byte) ([]byte, error)
OrchInfoSig() []byte
ExtraNodes() int
}

Expand Down
14 changes: 13 additions & 1 deletion core/broadcaster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,23 @@ func (bcast *broadcaster) Sign(msg []byte) ([]byte, error) {
return bcast.node.Eth.Sign(crypto.Keccak256(msg))
}
func (bcast *broadcaster) Address() ethcommon.Address {
if bcast.node == nil || bcast.node.Eth == nil {
if bcast.node == nil {
return ethcommon.Address{}
}
if (bcast.node.RemoteEthAddr != ethcommon.Address{}) {
return bcast.node.RemoteEthAddr
}
if bcast.node.Eth == nil {
return ethcommon.Address{}
}
return bcast.node.Eth.Account().Address
}
func (bcast *broadcaster) OrchInfoSig() []byte {
if bcast == nil || bcast.node == nil {
return nil
}
return bcast.node.InfoSig
}
func (bcast *broadcaster) ExtraNodes() int {
if bcast == nil || bcast.node == nil {
return 0
Expand Down
9 changes: 9 additions & 0 deletions core/livepeernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"github.com/livepeer/go-livepeer/common"
"github.com/livepeer/go-livepeer/eth"
lpmon "github.com/livepeer/go-livepeer/monitor"

ethcommon "github.com/ethereum/go-ethereum/common"
)

var ErrTranscoderAvail = errors.New("ErrTranscoderUnavailable")
Expand All @@ -48,6 +50,7 @@ const (
TranscoderNode
RedeemerNode
AIWorkerNode
RemoteSignerNode
)

var nodeTypeStrs = map[NodeType]string{
Expand All @@ -57,6 +60,7 @@ var nodeTypeStrs = map[NodeType]string{
TranscoderNode: "transcoder",
RedeemerNode: "redeemer",
AIWorkerNode: "aiworker",
RemoteSignerNode: "remotesigner",
}

func (t NodeType) String() string {
Expand Down Expand Up @@ -144,6 +148,11 @@ type LivepeerNode struct {
Sender pm.Sender
ExtraNodes int

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

// Thread safety for config fields
mu sync.RWMutex
StorageConfigs map[string]*transcodeConfig
Expand Down
1 change: 1 addition & 0 deletions discovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type stubBroadcaster struct{}
func (s *stubBroadcaster) Sign(msg []byte) ([]byte, error) { return []byte{}, nil }
func (s *stubBroadcaster) Address() ethcommon.Address { return ethcommon.Address{} }
func (s *stubBroadcaster) ExtraNodes() int { return 0 }
func (s *stubBroadcaster) OrchInfoSig() []byte { return nil }

func TestNewDBOrchestratorPoolCache_NilEthClient_ReturnsError(t *testing.T) {
assert := assert.New(t)
Expand Down
96 changes: 96 additions & 0 deletions doc/remote-signer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Remote signer

The **remote signer** is a standalone `go-livepeer` node mode that separates **Ethereum key custody + signing** from the gateway’s **untrusted media handling**. It is intended to:

- Improve security posture by removing Ethereum hot keys from the media processing path
- Enable web3-less gateway implementations natively on additional platforms such as browser, mobile, serverless and embedded backend apps
- Enable third-party payment operators to manage crypto payments separately from those managing media operations.

## Current implementation status

Remote signing was designed to initially target **Live AI** (`live-video-to-video`).

Support for other workloads may be added in the future.

The on-chain service registry is not used for Live AI workloads right now, so orchestrator discovery is not implemented as part of the remote signer. The gateway can learn about orchestrators via an orchestrator webhook (`-orchWebhookUrl`) or a static list (`-orchAddr`).

This allows a gateway to run in offchain mode while still working with on-chain orchestrators.

## Architecture

At a high level, the gateway uses the remote signer to handle Ethereum-related operations such as generating signatures or probabilistic micropayment tickets:

```mermaid
sequenceDiagram
participant RemoteSigner as RemoteSigner
participant Gateway as Gateway
participant Orchestrator as Orchestrator

Gateway->>RemoteSigner: POST /sign-orchestrator-info
RemoteSigner-->>Gateway: {address, signature}
Gateway->>Orchestrator: GetOrchestratorInfo(Address=address,Sig=signature)
Orchestrator-->>Gateway: OrchestratorInfo (incl TicketParams)

Note over Gateway,RemoteSigner: Live AI payments (asynchronous)
Gateway->>RemoteSigner: POST /generate-live-payment (orchInfo + signerState)
RemoteSigner-->>Gateway: {payment, segCreds, signerState'}
Gateway->>Orchestrator: POST /payment (headers: Livepeer-Payment, Livepeer-Segment)
Orchestrator-->>Gateway: PaymentResult (incl updated OrchestratorInfo)
```

## Usage

### Remote signer node

Start a remote signer by enabling the mode flag:

- `-remoteSigner=true`: run the remote signer service

The remote signer is intended to be its own standalone node type. The `-remoteSigner` flag cannot be combined with other mode flags such as `-gateway`, `-orchestrator`, `-transcoder`, etc.

**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 listens to the standard go-livepeer HTTP port (8935) by default. To change the listening port or interface, use the `-httpAddr` flag.

Example (fill in the placeholders for your environment):

```bash
./livepeer \
-remoteSigner \
-network mainnet \
-httpAddr 127.0.0.1:7936 \
-ethUrl <eth-rpc-url> \
-ethPassword <password-or-password-file>
...
```

### Gateway node

Configure a gateway to use a remote signer with:

- `-remoteSignerUrl <url>`: base URL of the remote signer service (**gateway only**)

If `-remoteSignerUrl` is set, the gateway will query the signer at startup and fail fast if it cannot reach the signer.

**No Ethereum flags are necessary on the gateway** in this mode. Omit the `-network` flag entirely here; this makes the gateway run in offchain mode, but it will still be able to send work to on-chain orchestrators with the `-remoteSignerUrl` flag enabled.

By default, if no URL scheme is provided, https is assumed and prepended to the remote signer URL. To override this (eg, to use a http:// URL) then include the scheme, eg `-remoteSignerUrl http://signer-host:port`

Example:

```bash
./livepeer \
-gateway \
-httpAddr :9935 \
-remoteSignerUrl http://127.0.0.1:7936 \
-orchAddr localhost:8935 \
-v 6
```

## Operational + security guidance

For the moment, remote signers are intended to sit behind infrastructure controls rather than being exposed directly to end-users. For example, run the remote signer on a private network or behind an authenticated proxy. Do not expose the remote signer to unauthenticated end-users. Run the remote signer close to gateways on a private network; protect it like you would an internal wallet service.

Remote signers are stateless, so signer nodes can operate in a redundant configuration (eg, round-robin DNS, anycasting) with no special gateway-side configuration.
Loading
Loading