JWT support (#3561)
This commit is contained in:
parent
f89c604fb0
commit
a18b39c9c1
|
@ -497,10 +497,16 @@ type
|
|||
|
||||
proposerBoosting* {.
|
||||
hidden
|
||||
desc: "Enable proposer boosting; temporary option feature gate (debugging; option will be removed)",
|
||||
desc: "Enable proposer boosting; temporary option feature gate (debugging; option may be removed without warning)",
|
||||
defaultValue: false
|
||||
name: "proposer-boosting-debug" }: bool
|
||||
|
||||
useJwt* {.
|
||||
hidden
|
||||
desc: "Enable JWT authentication headers; temporary option feature gate (debugging; option may be remove without warning)",
|
||||
defaultValue: false
|
||||
name: "use-jwt-debug" }: bool
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/sync/optimistic.md#fork-choice-poisoning
|
||||
safeSlotsToImportOptimistically* {.
|
||||
hidden
|
||||
|
|
|
@ -23,6 +23,9 @@ import
|
|||
".."/[beacon_chain_db, beacon_node_status, beacon_clock],
|
||||
./merkle_minimal
|
||||
|
||||
from std/times import getTime, inSeconds, initTime, `-`
|
||||
from ../spec/engine_authentication import getSignedIatToken
|
||||
|
||||
export
|
||||
web3Types, deques
|
||||
|
||||
|
@ -107,7 +110,7 @@ type
|
|||
eth1Network: Option[Eth1Network]
|
||||
depositContractAddress*: Eth1Address
|
||||
forcePolling: bool
|
||||
jwtSecret: seq[byte]
|
||||
jwtSecret: Option[seq[byte]]
|
||||
|
||||
dataProvider: Web3DataProviderRef
|
||||
latestEth1Block: Option[FullBlockId]
|
||||
|
@ -117,6 +120,7 @@ type
|
|||
|
||||
runFut: Future[void]
|
||||
stopFut: Future[void]
|
||||
getBeaconTime: GetBeaconTimeFn
|
||||
|
||||
when hasGenesisDetection:
|
||||
genesisValidators: seq[ImmutableValidatorData]
|
||||
|
@ -862,13 +866,28 @@ proc getBlockProposalData*(chain: var Eth1Chain,
|
|||
template getBlockProposalData*(m: Eth1Monitor,
|
||||
state: ForkedHashedBeaconState,
|
||||
finalizedEth1Data: Eth1Data,
|
||||
finalizedStateDepositIndex: uint64): BlockProposalEth1Data =
|
||||
getBlockProposalData(m.depositsChain, state, finalizedEth1Data, finalizedStateDepositIndex)
|
||||
finalizedStateDepositIndex: uint64):
|
||||
BlockProposalEth1Data =
|
||||
getBlockProposalData(
|
||||
m.depositsChain, state, finalizedEth1Data, finalizedStateDepositIndex)
|
||||
|
||||
proc getJsonRpcRequestHeaders(jwtSecret: Option[seq[byte]]):
|
||||
auto =
|
||||
if jwtSecret.isSome:
|
||||
let secret = jwtSecret.get
|
||||
(proc(): seq[(string, string)] =
|
||||
# https://www.rfc-editor.org/rfc/rfc6750#section-6.1.1
|
||||
@[("Authorization", "Bearer " & getSignedIatToken(
|
||||
secret, (getTime() - initTime(0, 0)).inSeconds))])
|
||||
else:
|
||||
(proc(): seq[(string, string)] = @[])
|
||||
|
||||
proc new*(T: type Web3DataProvider,
|
||||
depositContractAddress: Eth1Address,
|
||||
web3Url: string): Future[Result[Web3DataProviderRef, string]] {.async.} =
|
||||
let web3Fut = newWeb3(web3Url)
|
||||
web3Url: string,
|
||||
jwtSecret: Option[seq[byte]]):
|
||||
Future[Result[Web3DataProviderRef, string]] {.async.} =
|
||||
let web3Fut = newWeb3(web3Url, getJsonRpcRequestHeaders(jwtSecret))
|
||||
yield web3Fut or sleepAsync(chronos.seconds(10))
|
||||
if (not web3Fut.finished) or web3Fut.failed:
|
||||
await cancelAndWait(web3Fut)
|
||||
|
@ -905,10 +924,12 @@ proc init*(T: type Eth1Chain, cfg: RuntimeConfig, db: BeaconChainDB): T =
|
|||
proc createInitialDepositSnapshot*(
|
||||
depositContractAddress: Eth1Address,
|
||||
depositContractDeployedAt: BlockHashOrNumber,
|
||||
web3Url: string): Future[Result[DepositContractSnapshot, string]] {.async.} =
|
||||
web3Url: string,
|
||||
jwtSecret: Option[seq[byte]]): Future[Result[DepositContractSnapshot, string]]
|
||||
{.async.} =
|
||||
|
||||
let dataProviderRes =
|
||||
await Web3DataProvider.new(depositContractAddress, web3Url)
|
||||
await Web3DataProvider.new(depositContractAddress, web3Url, jwtSecret)
|
||||
if dataProviderRes.isErr:
|
||||
return err(dataProviderRes.error)
|
||||
var dataProvider = dataProviderRes.get
|
||||
|
@ -929,11 +950,12 @@ proc createInitialDepositSnapshot*(
|
|||
proc init*(T: type Eth1Monitor,
|
||||
cfg: RuntimeConfig,
|
||||
db: BeaconChainDB,
|
||||
getBeaconTime: GetBeaconTimeFn,
|
||||
web3Urls: seq[string],
|
||||
depositContractSnapshot: Option[DepositContractSnapshot],
|
||||
eth1Network: Option[Eth1Network],
|
||||
forcePolling: bool,
|
||||
jwtSecret: seq[byte]): T =
|
||||
jwtSecret: Option[seq[byte]]): T =
|
||||
doAssert web3Urls.len > 0
|
||||
var web3Urls = web3Urls
|
||||
for url in mitems(web3Urls):
|
||||
|
@ -945,6 +967,7 @@ proc init*(T: type Eth1Monitor,
|
|||
T(state: Initialized,
|
||||
depositsChain: Eth1Chain.init(cfg, db),
|
||||
depositContractAddress: cfg.DEPOSIT_CONTRACT_ADDRESS,
|
||||
getBeaconTime: getBeaconTime,
|
||||
web3Urls: web3Urls,
|
||||
eth1Network: eth1Network,
|
||||
eth1Progress: newAsyncEvent(),
|
||||
|
@ -973,7 +996,8 @@ proc detectPrimaryProviderComingOnline(m: Eth1Monitor) {.async.} =
|
|||
while m.runFut == initialRunFut:
|
||||
let tempProviderRes = await Web3DataProvider.new(
|
||||
m.depositContractAddress,
|
||||
web3Url)
|
||||
web3Url,
|
||||
m.jwtSecret)
|
||||
|
||||
if tempProviderRes.isErr:
|
||||
await sleepAsync(checkInterval)
|
||||
|
@ -1007,7 +1031,8 @@ proc ensureDataProvider*(m: Eth1Monitor) {.async.} =
|
|||
inc m.startIdx
|
||||
|
||||
m.dataProvider = block:
|
||||
let v = await Web3DataProvider.new(m.depositContractAddress, web3Url)
|
||||
let v = await Web3DataProvider.new(
|
||||
m.depositContractAddress, web3Url, m.jwtSecret)
|
||||
if v.isErr():
|
||||
raise (ref CatchableError)(msg: v.error())
|
||||
v.get()
|
||||
|
@ -1382,8 +1407,10 @@ proc start(m: Eth1Monitor, delayBeforeStart: Duration) {.gcsafe.} =
|
|||
proc start*(m: Eth1Monitor) =
|
||||
m.start(0.seconds)
|
||||
|
||||
proc getEth1BlockHash*(url: string, blockId: RtBlockIdentifier): Future[BlockHash] {.async.} =
|
||||
let web3 = await newWeb3(url)
|
||||
proc getEth1BlockHash*(
|
||||
url: string, blockId: RtBlockIdentifier, jwtSecret: Option[seq[byte]]):
|
||||
Future[BlockHash] {.async.} =
|
||||
let web3 = await newWeb3(url, getJsonRpcRequestHeaders(jwtSecret))
|
||||
try:
|
||||
let blk = awaitWithRetries(
|
||||
web3.provider.eth_getBlockByNumber(blockId, false))
|
||||
|
@ -1392,7 +1419,8 @@ proc getEth1BlockHash*(url: string, blockId: RtBlockIdentifier): Future[BlockHas
|
|||
await web3.close()
|
||||
|
||||
proc testWeb3Provider*(web3Url: Uri,
|
||||
depositContractAddress: Eth1Address) {.async.} =
|
||||
depositContractAddress: Eth1Address,
|
||||
jwtSecret: Option[seq[byte]]) {.async.} =
|
||||
template mustSucceed(action: static string, expr: untyped): untyped =
|
||||
try: expr
|
||||
except CatchableError as err:
|
||||
|
@ -1401,7 +1429,8 @@ proc testWeb3Provider*(web3Url: Uri,
|
|||
|
||||
let
|
||||
web3 = mustSucceed "connect to web3 provider":
|
||||
await newWeb3($web3Url)
|
||||
await newWeb3(
|
||||
$web3Url, getJsonRpcRequestHeaders(jwtSecret))
|
||||
networkVersion = mustSucceed "get network version":
|
||||
awaitWithRetries web3.provider.net_version()
|
||||
latestBlock = mustSucceed "get latest block":
|
||||
|
|
|
@ -405,6 +405,15 @@ proc init*(T: type BeaconNode,
|
|||
fatal "--finalized-checkpoint-block cannot be specified without --finalized-checkpoint-state"
|
||||
quit 1
|
||||
|
||||
let jwtSecret = rng[].checkJwtSecret(string(config.dataDir), config.jwtSecret)
|
||||
if jwtSecret.isErr:
|
||||
fatal "Specified a JWT secret file which couldn't be loaded",
|
||||
err = jwtSecret.error
|
||||
quit 1
|
||||
|
||||
# The JWT secret created always exists, it just might not always be used
|
||||
let optJwtSecret = if config.useJwt: some jwtSecret.get else: none(seq[byte])
|
||||
|
||||
template getDepositContractSnapshot: auto =
|
||||
if depositContractSnapshot.isSome:
|
||||
depositContractSnapshot
|
||||
|
@ -412,7 +421,8 @@ proc init*(T: type BeaconNode,
|
|||
let snapshotRes = waitFor createInitialDepositSnapshot(
|
||||
cfg.DEPOSIT_CONTRACT_ADDRESS,
|
||||
depositContractDeployedAt,
|
||||
config.web3Urls[0])
|
||||
config.web3Urls[0],
|
||||
optJwtSecret)
|
||||
if snapshotRes.isErr:
|
||||
fatal "Failed to locate the deposit contract deployment block",
|
||||
depositContract = cfg.DEPOSIT_CONTRACT_ADDRESS,
|
||||
|
@ -423,12 +433,6 @@ proc init*(T: type BeaconNode,
|
|||
else:
|
||||
none(DepositContractSnapshot)
|
||||
|
||||
let jwtSecret = rng[].checkJwtSecret(string(config.dataDir), config.jwtSecret)
|
||||
if jwtSecret.isErr:
|
||||
fatal "Specified a JWT secret file which couldn't be loaded",
|
||||
err = jwtSecret.error
|
||||
quit 1
|
||||
|
||||
var eth1Monitor: Eth1Monitor
|
||||
if not ChainDAGRef.isInitialized(db).isOk():
|
||||
var
|
||||
|
@ -453,11 +457,12 @@ proc init*(T: type BeaconNode,
|
|||
let eth1Monitor = Eth1Monitor.init(
|
||||
cfg,
|
||||
db,
|
||||
nil,
|
||||
config.web3Urls,
|
||||
getDepositContractSnapshot(),
|
||||
eth1Network,
|
||||
config.web3ForcePolling,
|
||||
jwtSecret.get)
|
||||
optJwtSecret)
|
||||
|
||||
eth1Monitor.loadPersistedDeposits()
|
||||
|
||||
|
@ -539,6 +544,7 @@ proc init*(T: type BeaconNode,
|
|||
validatorMonitor, networkGenesisValidatorsRoot)
|
||||
beaconClock = BeaconClock.init(
|
||||
getStateField(dag.headState, genesis_time))
|
||||
getBeaconTime = beaconClock.getBeaconTimeFn()
|
||||
|
||||
if config.weakSubjectivityCheckpoint.isSome:
|
||||
dag.checkWeakSubjectivityCheckpoint(
|
||||
|
@ -548,11 +554,12 @@ proc init*(T: type BeaconNode,
|
|||
eth1Monitor = Eth1Monitor.init(
|
||||
cfg,
|
||||
db,
|
||||
getBeaconTime,
|
||||
config.web3Urls,
|
||||
getDepositContractSnapshot(),
|
||||
eth1Network,
|
||||
config.web3ForcePolling,
|
||||
jwtSecret.get)
|
||||
optJwtSecret)
|
||||
|
||||
let rpcServer = if config.rpcEnabled:
|
||||
RpcServer.init(config.rpcAddress, config.rpcPort)
|
||||
|
@ -612,7 +619,6 @@ proc init*(T: type BeaconNode,
|
|||
netKeys = getPersistentNetKeys(rng[], config)
|
||||
nickname = if config.nodeName == "auto": shortForm(netKeys)
|
||||
else: config.nodeName
|
||||
getBeaconTime = beaconClock.getBeaconTimeFn()
|
||||
network = createEth2Node(
|
||||
rng, config, netKeys, cfg, dag.forkDigests, getBeaconTime,
|
||||
getStateField(dag.headState, genesis_validators_root))
|
||||
|
@ -1742,11 +1748,22 @@ proc doCreateTestnet*(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.rais
|
|||
for i in 0 ..< launchPadDeposits.len:
|
||||
deposits.add(launchPadDeposits[i] as DepositData)
|
||||
|
||||
let jwtSecret = rng.checkJwtSecret(string(config.dataDir), config.jwtSecret)
|
||||
if jwtSecret.isErr:
|
||||
fatal "Specified a JWT secret file which couldn't be loaded",
|
||||
err = jwtSecret.error
|
||||
quit 1
|
||||
|
||||
let
|
||||
startTime = uint64(times.toUnix(times.getTime()) + config.genesisOffset)
|
||||
outGenesis = config.outputGenesis.string
|
||||
eth1Hash = if config.web3Urls.len == 0: eth1BlockHash
|
||||
else: (waitFor getEth1BlockHash(config.web3Urls[0], blockId("latest"))).asEth2Digest
|
||||
else: (waitFor getEth1BlockHash(
|
||||
config.web3Urls[0], blockId("latest"),
|
||||
if config.useJwt:
|
||||
some jwtSecret.get
|
||||
else:
|
||||
none(seq[byte]))).asEth2Digest
|
||||
cfg = getRuntimeConfig(config.eth2Network)
|
||||
var
|
||||
initialState = newClone(initialize_beacon_state_from_eth1(
|
||||
|
@ -1819,12 +1836,25 @@ proc doRecord(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
|||
of RecordCmd.print:
|
||||
echo $config.recordPrint
|
||||
|
||||
proc doWeb3Cmd(config: BeaconNodeConf) {.raises: [Defect, CatchableError].} =
|
||||
proc doWeb3Cmd(config: BeaconNodeConf, rng: var BrHmacDrbgContext)
|
||||
{.raises: [Defect, CatchableError].} =
|
||||
case config.web3Cmd:
|
||||
of Web3Cmd.test:
|
||||
let metadata = config.loadEth2Network()
|
||||
let
|
||||
metadata = config.loadEth2Network()
|
||||
jwtSecret = rng.checkJwtSecret(string(config.dataDir), config.jwtSecret)
|
||||
|
||||
if jwtSecret.isErr:
|
||||
fatal "Specified a JWT secret file which couldn't be loaded",
|
||||
err = jwtSecret.error
|
||||
quit 1
|
||||
|
||||
waitFor testWeb3Provider(config.web3TestUrl,
|
||||
metadata.cfg.DEPOSIT_CONTRACT_ADDRESS)
|
||||
metadata.cfg.DEPOSIT_CONTRACT_ADDRESS,
|
||||
if config.useJwt:
|
||||
some jwtSecret.get
|
||||
else:
|
||||
none(seq[byte]))
|
||||
|
||||
proc doSlashingExport(conf: BeaconNodeConf) {.raises: [IOError, Defect].}=
|
||||
let
|
||||
|
@ -1889,7 +1919,7 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [Defect, CatchableEr
|
|||
of BNStartUpCmd.deposits: doDeposits(config, rng[])
|
||||
of BNStartUpCmd.wallets: doWallets(config, rng[])
|
||||
of BNStartUpCmd.record: doRecord(config, rng[])
|
||||
of BNStartUpCmd.web3: doWeb3Cmd(config)
|
||||
of BNStartUpCmd.web3: doWeb3Cmd(config, rng[])
|
||||
of BNStartUpCmd.slashingdb: doSlashingInterchange(config)
|
||||
of BNStartupCmd.trustedNodeSync:
|
||||
let
|
||||
|
|
|
@ -19,7 +19,7 @@ proc base64urlEncode(x: auto): string =
|
|||
# encoding quirks.
|
||||
base64.encode(x, safe = true).replace("=", "")
|
||||
|
||||
func getIatToken*(time: uint64): JsonNode =
|
||||
func getIatToken*(time: int64): JsonNode =
|
||||
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.8/src/engine/authentication.md#jwt-claims
|
||||
# "Required: iat (issued-at) claim. The EL SHOULD only accept iat timestamps
|
||||
# which are within +-5 seconds from the current time."
|
||||
|
@ -46,7 +46,7 @@ proc getSignedToken*(key: openArray[byte], payload: string): string =
|
|||
|
||||
signingInput & "." & base64_urlencode(sha256.hmac(key, signingInput).data)
|
||||
|
||||
proc getSignedIatToken*(key: openArray[byte], time: uint64): string =
|
||||
proc getSignedIatToken*(key: openArray[byte], time: int64): string =
|
||||
getSignedToken(key, $getIatToken(time))
|
||||
|
||||
proc checkJwtSecret*(
|
||||
|
@ -74,11 +74,12 @@ proc checkJwtSecret*(
|
|||
rng.brHmacDrbgGenerate(newSecret)
|
||||
try:
|
||||
writeFile(jwtSecretPath, newSecret.to0xHex())
|
||||
except IOError as e:
|
||||
except IOError as exc:
|
||||
# Allow continuing to run, though this is effectively fatal for a merge
|
||||
# client using authentication. This keeps it lower-risk initially.
|
||||
warn "Could not write JWT secret to data directory",
|
||||
jwtSecretPath
|
||||
jwtSecretPath,
|
||||
err = exc.msg
|
||||
return ok(newSecret)
|
||||
|
||||
try:
|
||||
|
|
|
@ -15,27 +15,27 @@ import
|
|||
suite "engine API authentication":
|
||||
test "getIatToken":
|
||||
check:
|
||||
$getIatToken(0) == "{\"iat\":0}"
|
||||
$getIatToken(1) == "{\"iat\":1}"
|
||||
$getIatToken(2) == "{\"iat\":2}"
|
||||
$getIatToken(14) == "{\"iat\":14}"
|
||||
$getIatToken(60) == "{\"iat\":60}"
|
||||
$getIatToken(95) == "{\"iat\":95}"
|
||||
$getIatToken(487) == "{\"iat\":487}"
|
||||
$getIatToken(529) == "{\"iat\":529}"
|
||||
$getIatToken(666) == "{\"iat\":666}"
|
||||
$getIatToken(2669) == "{\"iat\":2669}"
|
||||
$getIatToken(6082) == "{\"iat\":6082}"
|
||||
$getIatToken(6234) == "{\"iat\":6234}"
|
||||
$getIatToken(230158) == "{\"iat\":230158}"
|
||||
$getIatToken(675817) == "{\"iat\":675817}"
|
||||
$getIatToken(695159) == "{\"iat\":695159}"
|
||||
$getIatToken(19257188) == "{\"iat\":19257188}"
|
||||
$getIatToken(52639657) == "{\"iat\":52639657}"
|
||||
$getIatToken(71947005) == "{\"iat\":71947005}"
|
||||
$getIatToken(1169144470) == "{\"iat\":1169144470}"
|
||||
$getIatToken(2931679730'u64) == "{\"iat\":2931679730}"
|
||||
$getIatToken(3339327695'u64) == "{\"iat\":3339327695}"
|
||||
$getIatToken(0) == "{\"iat\":0}"
|
||||
$getIatToken(1) == "{\"iat\":1}"
|
||||
$getIatToken(2) == "{\"iat\":2}"
|
||||
$getIatToken(14) == "{\"iat\":14}"
|
||||
$getIatToken(60) == "{\"iat\":60}"
|
||||
$getIatToken(95) == "{\"iat\":95}"
|
||||
$getIatToken(487) == "{\"iat\":487}"
|
||||
$getIatToken(529) == "{\"iat\":529}"
|
||||
$getIatToken(666) == "{\"iat\":666}"
|
||||
$getIatToken(2669) == "{\"iat\":2669}"
|
||||
$getIatToken(6082) == "{\"iat\":6082}"
|
||||
$getIatToken(6234) == "{\"iat\":6234}"
|
||||
$getIatToken(230158) == "{\"iat\":230158}"
|
||||
$getIatToken(675817) == "{\"iat\":675817}"
|
||||
$getIatToken(695159) == "{\"iat\":695159}"
|
||||
$getIatToken(19257188) == "{\"iat\":19257188}"
|
||||
$getIatToken(52639657) == "{\"iat\":52639657}"
|
||||
$getIatToken(71947005) == "{\"iat\":71947005}"
|
||||
$getIatToken(1169144470) == "{\"iat\":1169144470}"
|
||||
$getIatToken(2931679730) == "{\"iat\":2931679730}"
|
||||
$getIatToken(3339327695) == "{\"iat\":3339327695}"
|
||||
|
||||
test "HS256 JWS signing":
|
||||
let secret = mapIt("secret", byte(it))
|
||||
|
@ -71,5 +71,5 @@ suite "engine API authentication":
|
|||
getSignedIatToken(secret, 52639657) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjUyNjM5NjU3fQ.O0cI2U_kEW1MbWyXcAh146mRU2CwzMNegAQit_1-TNU"
|
||||
getSignedIatToken(secret, 71947005) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjcxOTQ3MDA1fQ.pQwPWxMHzWGvTTfRfWKiGX8qEI2NcZbnB3ruh4Wcftg"
|
||||
getSignedIatToken(secret, 1169144470) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjExNjkxNDQ0NzB9.5JS0pVVh1g8hxO_PDQpwCvFnh1tdRtodpALXU1xol4I"
|
||||
getSignedIatToken(secret, 2931679730'u64) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjI5MzE2Nzk3MzB9.0ZR8DiVy6Y_pOleGC9Ti3M8ShtH5hyCBhceO1C2OTj0"
|
||||
getSignedIatToken(secret, 3339327695'u64) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjMzMzkzMjc2OTV9.ZRYaNrsvcIzppVeNorYUgEmVXcwOOQbqPlCQcoAaO4k"
|
||||
getSignedIatToken(secret, 2931679730) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjI5MzE2Nzk3MzB9.0ZR8DiVy6Y_pOleGC9Ti3M8ShtH5hyCBhceO1C2OTj0"
|
||||
getSignedIatToken(secret, 3339327695) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjMzMzkzMjc2OTV9.ZRYaNrsvcIzppVeNorYUgEmVXcwOOQbqPlCQcoAaO4k"
|
||||
|
|
Loading…
Reference in New Issue