Skip to content

Commit ad45763

Browse files
authored
Merge branch 'main' into feat/remote-signer-keyring
2 parents 6403814 + baf4340 commit ad45763

38 files changed

Lines changed: 2116 additions & 309 deletions

README.md

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
11
# Daemon
22

3-
**Note:** Daemon services code was adopted from dydx [](https://github.com/dydxprotocol/v4-chain/tree/main/protocol/daemons) and reconfigured.
3+
**Note:** Daemon services code was adopted from [dYdX](https://github.com/dydxprotocol/v4-chain/tree/main/protocol/daemons) and reconfigured.
4+
5+
## Configuration
6+
7+
The daemon loads environment variables from the current directory's `.env` file, or from `../.env` when run from a subdirectory. See [`env.example`](./env.example) for a complete starting point.
8+
9+
Most CLI flags can also be provided as environment variables by uppercasing the flag name and replacing `-` or `.` with `_`, for example `--keyring-backend` becomes `KEYRING_BACKEND`. `LAYER_HOME` is preferred for the Layer home directory so the daemon does not accidentally use the shell's `HOME`.
10+
11+
Layer endpoint configuration can be provided with comma-separated environment variables:
12+
13+
```sh
14+
RPC_NODES=http://node_endpoint1:26657,http://node_endpoint2:26657
15+
GRPC_NODES=127.0.0.1:9090,node2:9090
16+
```
17+
18+
The first endpoint in each list is treated as the primary endpoint. Later entries are used as ordered fallbacks.
19+
20+
Both endpoint types are required when starting the reporter daemon:
21+
22+
- `GRPC_NODES` / `--grpc` configures Cosmos gRPC query services.
23+
- `RPC_NODES` / `--node` configures CometBFT RPC. The reporter uses this for startup chain ID validation, block/status polling, transaction broadcast, and transaction lookup while waiting for inclusion.
24+
25+
Endpoint env vars take precedence over the existing CLI flags:
26+
27+
- `RPC_NODES` is preferred over `--node`.
28+
- `GRPC_NODES` is preferred over `--grpc`.
29+
- If an env var is unset, the daemon preserves the old behavior by using the matching flag value as a single endpoint.
30+
31+
At startup, the daemon checks the configured gRPC and CometBFT RPC endpoints for a matching chain ID and starts with the first healthy matching endpoints. The reporter keeps both endpoint lists and falls back to later nodes for network/client failures. gRPC fallback is used for reporter chain queries, while RPC fallback is used for status checks, transaction lookup, and transaction broadcast. The reporter also periodically probes the primary endpoints and switches back when they are healthy again. It does not switch endpoints for semantic chain failures such as out-of-gas responses, non-zero tx result codes, or normal tx-not-found polling.
32+
33+
The pricefeed client is started with the selected gRPC endpoint only. Endpoint-list fallback currently applies to the reporter client's chain query and transaction paths, not to the pricefeed client.
34+
35+
Ethereum JSON-RPC configuration uses the same comma-separated primary/fallback pattern:
36+
37+
```sh
38+
BRIDGE_CHAIN_RPC_NODES=https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY,https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
39+
```
40+
41+
`BRIDGE_CHAIN_RPC_NODES` is used by token bridge deposit monitoring. The first endpoint is tried first; later entries are ordered fallbacks.
42+
43+
Ethereum mainnet custom query contract reads use the built-in mainnet endpoint templates by default. If `BRIDGE_CHAIN_RPC_NODES` points at a non-mainnet bridge chain such as Sepolia, set `ETH_MAINNET_RPC_NODES` to a comma-separated Ethereum mainnet endpoint list for custom queries.
44+
45+
Custom query API keys are read from the generated `custom_query_config.toml` entries that reference environment placeholders. The current built-in templates use `CMC_PRO_API_KEY`, `CGPRO_API_KEY`, and `SUBGRAPH_API_KEY`.
446

547
## Task loops
648

749
## PriceFetcher
850

9-
- Will query exchanges for prices once or multiple times based on wether the api supports single vs multi markets; ie wether an api needs to be queried for each pair individually or can return multiple pairs at once, [See here for exchange details](./constants/static_exchange_details.go).
51+
- Will query exchanges for prices once or multiple times based on whether the API supports single vs multi markets; i.e. whether an API needs to be queried for each pair individually or can return multiple pairs at once. [See here for exchange details](./constants/static_exchange_details.go).
1052

1153
## PriceEncoder
1254

13-
- Will update cache with the queried prices and encode appropriately also make adjustments as necessary based on if adjustByMarket is defined.
55+
- Will update the cache with queried prices, encode them appropriately, and make adjustments when `adjustByMarket` is defined.
1456

1557
### Configuration
1658

@@ -34,7 +76,7 @@ example:
3476

3577
```go
3678
[[market_params]]
37-
ExchangeConfigJson = "{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHBTC\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHBTC\",\"adjustByMarket\":\"BTC-USD\"}]}" // this is just an example to show how to use adjustByMarket. you can use ETH-USD without adjustbymarket
79+
ExchangeConfigJson = "{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHBTC\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHBTC\",\"adjustByMarket\":\"BTC-USD\"}]}" // This is an example showing how to use adjustByMarket. You can use ETH-USD without adjustByMarket.
3880
Exponent = -6
3981
Id = 2
4082
MinExchanges = 1
@@ -51,7 +93,7 @@ type MarketParam struct {
5193
Pair string
5294
// Static value. The exponent of the price.
5395
// For example if `Exponent == -5` then a `Value` of `1,000,000,000`
54-
// represents $10,000`. Therefore `10 ^ Exponent` represents the smallest
96+
// represents "$10,000". Therefore `10 ^ Exponent` represents the smallest
5597
// price step (in dollars) that can be recorded.
5698
Exponent int32
5799
// The minimum number of exchanges that should be reporting a live price for
@@ -63,7 +105,7 @@ type MarketParam struct {
63105
// A string of json that encodes the configuration for resolving the price
64106
// of this market on various exchanges.
65107
ExchangeConfigJson string
66-
// Query data is the market pair represention in layer
108+
// Query data is the market pair representation in layer
67109
QueryData string
68110
}
69111
```
@@ -74,10 +116,54 @@ A price is valid by default up to 30 seconds; to change this to a different defa
74116
**Also:** Config files are written to homedir/.layer/config/.
75117
To change/add exchange details or market pairs edit the files `pricefeed_exchange_config.toml` or `market_params.toml` respectively.
76118

119+
## Keyring Password File
120+
121+
When running the reporter daemon with `--keyring-backend file`, set `KEYRING_PASSWORD_FILE` to a file containing the keyring password. This lets the daemon unlock the account without requiring an interactive terminal prompt.
122+
123+
For this systemd file-keyring setup, set `LAYER_HOME` to the same home directory that contains the daemon config and keyring files. This keeps the non-interactive service from resolving `home` from the service user's shell environment instead of the intended Layer home.
124+
125+
Example systemd service snippet:
126+
127+
```ini
128+
[Service]
129+
User=reporter
130+
Environment="KEYRING_PASSWORD_FILE=/etc/layer-daemons/reporter-keyring-password"
131+
Environment="LAYER_HOME=/home/reporter/.layer"
132+
Environment="GRPC_NODES=your-grpc-host:9090,your-fallback-grpc-host:9090"
133+
Environment="RPC_NODES=tcp://your-rpc-host:26657,tcp://your-fallback-rpc-host:26657"
134+
ExecStart=/usr/local/bin/reporterd \
135+
--keyring-backend file \
136+
--from your-key-name
137+
```
138+
139+
Create the password file so only the service user can read it:
140+
141+
```bash
142+
sudo install -d -m 700 -o reporter -g reporter /etc/layer-daemons
143+
sudo install -m 600 -o reporter -g reporter /dev/null /etc/layer-daemons/reporter-keyring-password
144+
sudo sh -c 'printf "%s\n" "YOUR_KEYRING_PASSWORD" > /etc/layer-daemons/reporter-keyring-password'
145+
```
146+
147+
Make sure the service `User` can read the file. When `KEYRING_PASSWORD_FILE` is set, startup fails and the daemon exits if the file cannot be read, is empty, or cannot unlock the configured `--from` account. If `KEYRING_PASSWORD_FILE` is not set, the daemon falls back to reading the keyring password from stdin.
148+
149+
## Reward Withdrawals And Auto-Unbonding
150+
151+
The reporter periodically withdraws earned tips/rewards with `MsgWithdrawTip`. The interval is configured by `WITHDRAW_FREQUENCY` in seconds and defaults to `43200` (12 hours). By default, the validator operator address is derived from the reporter account address. If the reporter account is delegated to a different validator, set `REPORTERS_VALIDATOR_ADDRESS` to that validator's `tellorvaloper...` address.
152+
153+
Auto-unbonding is optional and can be configured by CLI flags or equivalent environment variables:
154+
155+
| Flag | Environment | Type | Default | Description |
156+
|------|-------------|------|---------|-------------|
157+
| `--auto-unbonding-frequency` | `AUTO_UNBONDING_FREQUENCY` | uint32 | `0` | Enables unbonding every N days (`0` = disabled, valid enabled range is 1-21). |
158+
| `--auto-unbonding-amount` | `AUTO_UNBONDING_AMOUNT` | uint32 | `0` | Amount of `loya` to unbond each time. Required when frequency is enabled. |
159+
| `--auto-unbonding-max-stake-percentage` | `AUTO_UNBONDING_MAX_STAKE_PERCENTAGE` | decimal string | `0.0` | Optional cap from `0.0` to `1.0`; if the configured amount exceeds this share of stake, the unbond is skipped. |
160+
161+
Gas estimates are cached per transaction type. `--refresh-gas-estimates-interval` / `REFRESH_GAS_ESTIMATES_INTERVAL` resets cached estimates and gas-adjustment levels periodically; it defaults to `12h`, and values `<=0` disable the refresh loop.
162+
77163
### Median Server
78164

79-
Median server was added for a way to query median values that were from an endpoint or cli. See usage [here](../x/oracle/client/cli/query_all_get_median.go).
80-
All median values or median value given query data using the following commands respectively.
165+
The median server can query median values from an endpoint or the CLI. See usage [here](../x/oracle/client/cli/query_all_get_median.go).
166+
Query all median values, or a median value for specific query data, using the following commands respectively.
81167
`layerd query oracle get-all-median-values`
82168
`layerd query oracle get-median-value <querydata>`
83169

@@ -114,12 +200,12 @@ The Price Guard is a safety mechanism that prevents the reporter from submitting
114200

115201
### Flags
116202

117-
| Flag | Type | Description | Required (if enabled) |
118-
|------|------|-------------|---------------------|
119-
| `--price-guard-enabled` | bool | Enables the price guard mechanism | No |
120-
| `--price-guard-threshold` | float64 | Maximum allowed percentage change (e.g., 0.5 = 50%). Submissions exceeding this change from the last reported price will be blocked. | Yes |
121-
| `--price-guard-max-age` | duration | Time after which a stored price is considered expired (e.g., "1h"). If the last price is expired, the new price is accepted regardless of deviation. | Yes |
122-
| `--price-guard-update-on-blocked` | bool | If true, updates the internal "last known price" to the new value even if submission was blocked. If false, keeps the old price as the baseline. | Yes |
203+
| Flag | Environment | Type | Description | Required (if enabled) |
204+
|------|-------------|------|-------------|-----------------------|
205+
| `--price-guard-enabled` | `PRICE_GUARD_ENABLED` | bool | Enables the price guard mechanism. | No |
206+
| `--price-guard-threshold` | `PRICE_GUARD_THRESHOLD` | float64 | Maximum allowed percentage change (e.g. `0.5` = 50%). Submissions exceeding this change from the last reported price are blocked. | Yes |
207+
| `--price-guard-max-age` | `PRICE_GUARD_MAX_AGE` | duration | Time after which a stored price is considered expired (e.g. `1h`). If the last price is expired, the new price is accepted regardless of deviation. | Yes |
208+
| `--price-guard-update-on-blocked` | `PRICE_GUARD_UPDATE_ON_BLOCKED` | bool | If true, updates the internal "last known price" to the new value even if submission was blocked. If false, keeps the old price as the baseline. | Yes |
123209

124210
### Notes
125211

@@ -131,3 +217,46 @@ The Price Guard is a safety mechanism that prevents the reporter from submitting
131217
4. **Update on Blocked:**
132218
- If `true`: A blocked price becomes the new baseline for future checks.
133219
- If `false`: The old price remains the baseline; future submissions must be within threshold of the *old* price.
220+
221+
## Auto balance-to-keep
222+
223+
The reporter daemon can keep a target **loya** balance in the reporter wallet and automatically bridge any excess to Ethereum once per day. This uses Layer’s `MsgWithdrawTokens` bridge message (`isBridge` gas bucket, same tx pipeline as other bridge operations).
224+
225+
### Flags
226+
227+
| Flag | Environment | Type | Default | Description |
228+
|------|-------------|------|---------|-------------|
229+
| `--auto-balance-to-keep` | `AUTO_BALANCE_TO_KEEP` | uint64 | `0` | Target wallet balance in **loya** (`0` = disabled). Any amount above this, minus the gas reserve below, is bridged. |
230+
| `--auto-balance-execution-time` | `AUTO_BALANCE_EXECUTION_TIME` | string | `00:00` | UTC time to check balance and bridge, format **`HH:MM`** with hour 0-23 and minute 0-59 (e.g. `03:00`, `15:30`). |
231+
| `--auto-balance-bridge-to-eth-addr` | `AUTO_BALANCE_BRIDGE_TO_ETH_ADDR` | string | `""` | Ethereum recipient for bridged tokens. Required when `--auto-balance-to-keep > 0`. May include or omit the `0x` prefix. Validated with standard hex address checks at startup. |
232+
233+
### Behavior
234+
235+
1. **Schedule:** Once per UTC day at `--auto-balance-execution-time`, the daemon queries the reporter wallet's `loya` balance.
236+
2. **Amount:** `bridge_amount = wallet_balance - auto-balance-to-keep - 1_000_000` (a fixed **1 TRB** reserve in loya is left for future gas). If `bridge_amount <= 0`, nothing is sent.
237+
3. **Broadcast:** The transaction uses the shared broadcast path with RPC endpoint fallback and gas-adjustment retries for out-of-gas responses. Other failures are logged; the next balance check happens at the next scheduled UTC time.
238+
4. **Shutdown:** Bridge txs are enqueued with `trySend` so shutdown does not panic on a closed channel.
239+
240+
### Startup validation
241+
242+
When `--auto-balance-to-keep > 0`, the reporter **fails to start** if:
243+
244+
- `--auto-balance-bridge-to-eth-addr` is missing or not a valid Ethereum address
245+
- `--auto-balance-execution-time` is not valid `HH:MM` (hour 0-23, minute 0-59)
246+
247+
### Example
248+
249+
Keep 5 TRB in the wallet (5_000_000 loya), run the check daily at 03:00 UTC, and bridge excess to an Ethereum address:
250+
251+
```bash
252+
LAYER_HOME=/home/reporter/.layer \
253+
GRPC_NODES=your-grpc-host:9090 \
254+
RPC_NODES=tcp://your-rpc-host:26657 \
255+
reporterd \
256+
--from your-key-name \
257+
--auto-balance-to-keep=5000000 \
258+
--auto-balance-execution-time=03:00 \
259+
--auto-balance-bridge-to-eth-addr=0x0000000000000000000000000000000000000000
260+
```
261+
262+
**Note:** Amounts are in **loya** (micro-denom), not whole TRB. `1 TRB = 1_000_000 loya`.

app.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ type App struct {
4646
func NewApp(
4747
ctx context.Context,
4848
logger log.Logger,
49-
chainId,
50-
grpcAddress,
49+
chainId string,
50+
grpcEndpoints []string,
51+
rpcEndpoints []string,
5152
homePath string,
5253
prometheusPort int,
5354
) *App {
@@ -140,7 +141,7 @@ func NewApp(
140141
// Use cancellable context instead of context.Background
141142
ctx,
142143
daemonFlags,
143-
grpcAddress,
144+
grpcEndpoints[0],
144145
logger,
145146
&daemontypes.GrpcClientImpl{},
146147
marketParamsConfig,
@@ -161,16 +162,18 @@ func NewApp(
161162
// Use cancellable context instead of context.Background
162163
ctx,
163164
daemonFlags,
164-
grpcAddress,
165+
grpcEndpoints,
165166
&daemontypes.GrpcClientImpl{},
166167
marketParamsConfig,
167168
indexPriceCache,
168169
tokenDepositsCache,
169170
tokenBridgeTipsCache,
170171
queries,
171172
chainId,
173+
rpcEndpoints,
172174
); err != nil {
173175
logger.Error("Reporter client failed to start", "error", err)
176+
os.Exit(1)
174177
}
175178
}()
176179

cmd/chain_id.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"time"
78

89
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
@@ -12,29 +13,66 @@ import (
1213
"github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
1314
)
1415

15-
// detectChainID queries both the gRPC endpoint and the CometBFT RPC endpoint
16-
// for the chain ID, validates that they agree, and returns the chain ID.
17-
// Returns an error if either endpoint is unreachable or if they return
18-
// different chain IDs.
19-
func detectChainID(ctx context.Context, grpcAddr, nodeRPCAddr string) (string, error) {
20-
detectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
21-
defer cancel()
16+
type detectedEndpointChainID struct {
17+
endpoint string
18+
chainID string
19+
}
20+
21+
type chainIDDetector func(context.Context, string) (string, error)
22+
23+
var chainIDEndpointDetectTimeout = 15 * time.Second
2224

23-
grpcChainID, err := chainIDFromGRPC(detectCtx, grpcAddr)
25+
func detectChainIDFromEndpoints(ctx context.Context, grpcAddrs, nodeRPCAddrs []string) (string, string, string, error) {
26+
grpcChainIDs, err := detectEndpointChainIDs(ctx, "gRPC", grpcAddrs, chainIDFromGRPC)
2427
if err != nil {
25-
return "", fmt.Errorf("failed to detect chain ID via gRPC (%s): %w", grpcAddr, err)
28+
return "", "", "", err
2629
}
2730

28-
rpcChainID, err := chainIDFromRPC(detectCtx, nodeRPCAddr)
31+
rpcChainIDs, err := detectEndpointChainIDs(ctx, "node RPC", nodeRPCAddrs, chainIDFromRPC)
2932
if err != nil {
30-
return "", fmt.Errorf("failed to detect chain ID via node RPC (%s): %w", nodeRPCAddr, err)
33+
return "", "", "", err
34+
}
35+
36+
if err := validateReachableChainIDs("gRPC", grpcChainIDs); err != nil {
37+
return "", "", "", err
38+
}
39+
if err := validateReachableChainIDs("node RPC", rpcChainIDs); err != nil {
40+
return "", "", "", err
3141
}
42+
if grpcChainIDs[0].chainID != rpcChainIDs[0].chainID {
43+
return "", "", "", fmt.Errorf("chain ID mismatch: gRPC returned %q, node RPC returned %q", grpcChainIDs[0].chainID, rpcChainIDs[0].chainID)
44+
}
45+
46+
return grpcChainIDs[0].chainID, grpcChainIDs[0].endpoint, rpcChainIDs[0].endpoint, nil
47+
}
3248

33-
if grpcChainID != rpcChainID {
34-
return "", fmt.Errorf("chain ID mismatch: gRPC returned %q, node RPC returned %q", grpcChainID, rpcChainID)
49+
func detectEndpointChainIDs(ctx context.Context, endpointType string, endpoints []string, detector chainIDDetector) ([]detectedEndpointChainID, error) {
50+
var detected []detectedEndpointChainID
51+
var errs []string
52+
for _, endpoint := range endpoints {
53+
endpointCtx, cancel := context.WithTimeout(ctx, chainIDEndpointDetectTimeout)
54+
chainID, err := detector(endpointCtx, endpoint)
55+
cancel()
56+
if err != nil {
57+
errs = append(errs, fmt.Sprintf("%s: %v", endpoint, err))
58+
continue
59+
}
60+
detected = append(detected, detectedEndpointChainID{endpoint: endpoint, chainID: chainID})
3561
}
62+
if len(detected) == 0 {
63+
return nil, fmt.Errorf("failed to detect chain ID via any %s endpoint: %s", endpointType, strings.Join(errs, "; "))
64+
}
65+
return detected, nil
66+
}
3667

37-
return grpcChainID, nil
68+
func validateReachableChainIDs(endpointType string, detected []detectedEndpointChainID) error {
69+
expected := detected[0].chainID
70+
for _, item := range detected[1:] {
71+
if item.chainID != expected {
72+
return fmt.Errorf("%s endpoints disagree on chain ID: %s returned %q, %s returned %q", endpointType, detected[0].endpoint, expected, item.endpoint, item.chainID)
73+
}
74+
}
75+
return nil
3876
}
3977

4078
func chainIDFromGRPC(ctx context.Context, grpcAddr string) (string, error) {

0 commit comments

Comments
 (0)