Merge branch 'stable' into unstable

This commit is contained in:
Zahary Karadjov 2023-03-17 17:51:39 +02:00
commit 4d1b2dd9f5
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
15 changed files with 315 additions and 53 deletions

View File

@ -161,10 +161,19 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
+ Testing uints inputs - valid OK
```
OK: 10/12 Fail: 0/12 Skip: 2/12
## EL Configuration
```diff
+ Empty config file OK
+ Invalid URls OK
+ New style config files OK
+ Old style config files OK
+ URL parsing OK
```
OK: 5/5 Fail: 0/5 Skip: 0/5
## Eth1 monitor
```diff
+ Deposits chain OK
+ Rewrite HTTPS Infura URLs OK
+ Rewrite URLs OK
+ Roundtrip engine RPC V1 and bellatrix ExecutionPayload representations OK
+ Roundtrip engine RPC V2 and capella ExecutionPayload representations OK
+ Roundtrip engine RPC V3 and deneb ExecutionPayload representations OK
@ -626,4 +635,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
OK: 9/9 Fail: 0/9 Skip: 0/9
---TOTAL---
OK: 347/352 Fail: 0/352 Skip: 5/352
OK: 352/357 Fail: 0/357 Skip: 5/357

View File

@ -1,7 +1,40 @@
2023-03-14 v23.3.1
==================
Nimbus `v23.3.1` is a `medium-urgency` point release addressing a number of accidental configuration handling breaking changes that were shipped in the `v23.3.0` release. It also improves the stability of Nimbus when paired with a Besu execution client and improves the fault-tolerance when driving multiple execution clients.
### Fixes
* Nimbus was performing `eth_getLogs` request with parameters that were exceeding the default `--rpc-max-logs-range=1000` limit on Besu. This was a non-fatal issue that resulted in slower deposit syncing speed and the frequent warning message "Connection to EL node degraded". The limit will be increased in the next mainnet release of Besu, but Nimbus `v23.3.1` honours the existing limit at the cost of a slightly slower syncing speed with all other execution clients:
https://github.com/status-im/nimbus-eth2/commit/6fb48aca7dedc7ba3c6b2f2ae8a4926ddcf7a00e
* `v23.3.0` did not support Engine API URLs which don't specify a protocol in the URL (e.g. `http`, `https`, `ws` or `wss`). `v23.3.1` is backwards-compatible with all previous Nimbus releases:
https://github.com/status-im/nimbus-eth2/commit/3a35809a02b4fbe23b2dc843806ec81f67521c6d
* `v23.3.0` produced a parsing error on TOML configuration files that specify the `web3-url` parameter as an array of strings. `v23.3.1` is backwards-compatible with all previous Nimbus releases and introduces a new more convenient way for specifying the Engine API configuration in TOML:
https://nimbus.guide/eth1.html#running-multiple-execution-clients
https://github.com/status-im/nimbus-eth2/commit/46f48269ef899f19cd9932b27d30c68e2ccf035b
* `v23.3.0` removed the hidden configuration option `--web3-force-polling` which remained in use by some users. `v23.3.1` restores the option as a deprecated one. Please note that all hidden configuration options are intended for use only by the Nimbus development team for testing purposes:
https://github.com/status-im/nimbus-eth2/commit/ee610cbf34cebea24576c25bf6702de4205a260a
* The release addresses a potential crash triggered by Engine API connections experiencing frequent error responses:
https://github.com/status-im/nimbus-eth2/commit/d899a6a834c083a62e1246eade5027a7019ace82
* The release addresses a potential issue where a single non-synced execution client may cause the Nimbus sync state to revert to `synced/opt`, even when all validator duties can be performed through the remaining execution clients that are still synced:
https://github.com/status-im/nimbus-eth2/commit/d899a6a834c083a62e1246eade5027a7019ace82
2023-03-11 v23.3.0
==================
Nimbus `v23.3.0` is `low-urgency` upgrade bringing full support for the upcoming Capella hard-fork on the Goerli testnet. Keep an eye out for future mainnet releases!
Nimbus `v23.3.0` is a `low-urgency` upgrade bringing full support for the upcoming Capella hard-fork on the Goerli testnet. Keep an eye out for future mainnet releases!
### Improvements

View File

@ -176,6 +176,11 @@ type
desc: "A directory containing era files"
name: "era-dir" .}: Option[InputDir]
web3ForcePolling* {.
hidden
desc: "Force the use of polling when determining the head block of Eth1 (obsolete)"
name: "web3-force-polling" .}: Option[bool]
web3Urls* {.
desc: "One or more execution layer Engine API URLs"
name: "web3-url" .}: seq[EngineApiUrlConfigValue]

View File

@ -1,10 +1,16 @@
import
std/[options, strutils, uri],
stew/results, chronicles, confutils,
confutils/toml/defs as confTomlDefs,
confutils/toml/std/net as confTomlNet,
confutils/toml/std/uri as confTomlUri,
json_serialization, # for logging
toml_serialization, toml_serialization/lexer,
../spec/engine_authentication
export
toml_serialization, confTomlDefs, confTomlNet, confTomlUri
type
EngineApiRole* = enum
DepositSyncing = "sync-deposits"
@ -20,8 +26,8 @@ type
EngineApiUrlConfigValue* = object
url*: string # TODO: Use the URI type here
jwtSecret*: Option[string]
jwtSecretFile*: Option[InputFile]
jwtSecret* {.serializedFieldName: "jwt-secret".}: Option[string]
jwtSecretFile* {.serializedFieldName: "jwt-secret-file".}: Option[InputFile]
roles*: Option[EngineApiRoles]
const
@ -97,9 +103,9 @@ proc parseCmdArg*(T: type EngineApiUrlConfigValue, input: string): T
if uri.anchor != "":
for key, value in decodeQuery(uri.anchor):
case key
of "jwtSecret":
of "jwtSecret", "jwt-secret":
jwtSecret = some value
of "jwtSecretFile":
of "jwtSecretFile", "jwt-secret-file":
jwtSecretFile = some InputFile.parseCmdArg(value)
of "roles":
var uriRoles: EngineApiRoles = {}
@ -126,6 +132,26 @@ proc parseCmdArg*(T: type EngineApiUrlConfigValue, input: string): T
jwtSecretFile: jwtSecretFile,
roles: roles)
proc readValue*(reader: var TomlReader, value: var EngineApiUrlConfigValue)
{.raises: [Defect, SerializationError, IOError].} =
if reader.lex.readable and reader.lex.peekChar in ['\'', '"']:
# If the input is a string, we'll reuse the command-line parsing logic
value = try: parseCmdArg(EngineApiUrlConfigValue, reader.readValue(string))
except ValueError as err:
reader.lex.raiseUnexpectedValue("Valid Engine API URL expected: " & err.msg)
else:
# Else, we'll use the standard object-serializer in TOML
toml_serialization.readValue(reader, value)
proc fixupWeb3Urls*(web3Url: var string) =
var normalizedUrl = toLowerAscii(web3Url)
if not (normalizedUrl.startsWith("https://") or
normalizedUrl.startsWith("http://") or
normalizedUrl.startsWith("wss://") or
normalizedUrl.startsWith("ws://")):
warn "The Web3 URL does not specify a protocol. Assuming a WebSocket server", web3Url
web3Url = "ws://" & web3Url
proc toFinalUrl*(confValue: EngineApiUrlConfigValue,
confJwtSecret: Option[seq[byte]]): Result[EngineApiUrl, cstring] =
if confValue.jwtSecret.isSome and confValue.jwtSecretFile.isSome:
@ -138,8 +164,11 @@ proc toFinalUrl*(confValue: EngineApiUrlConfigValue,
else:
confJwtSecret
var url = confValue.url
fixupWeb3Urls(url)
ok EngineApiUrl.init(
url = confValue.url,
url = url,
jwtSecret = jwtSecret,
roles = confValue.roles.get(defaultEngineApiRoles))
@ -163,12 +192,3 @@ proc toFinalEngineApiUrls*(elUrls: seq[EngineApiUrlConfigValue],
fatal "Invalid EL configuration", err = error
quit 1
result.add engineApiUrl
proc fixupWeb3Urls*(web3Url: var string) =
var normalizedUrl = toLowerAscii(web3Url)
if not (normalizedUrl.startsWith("https://") or
normalizedUrl.startsWith("http://") or
normalizedUrl.startsWith("wss://") or
normalizedUrl.startsWith("ws://")):
warn "The Web3 URL does not specify a protocol. Assuming a WebSocket server", web3Url
web3Url = "ws://" & web3Url

View File

@ -57,7 +57,29 @@ contract(DepositContract):
const
hasDepositRootChecks = defined(has_deposit_root_checks)
targetBlocksPerLogsRequest = 5000'u64 # This is roughly a day of Eth1 blocks
targetBlocksPerLogsRequest = 1000'u64
# TODO
#
# This is currently set to 1000, because this was the default maximum
# value in Besu circa our 22.3.0 release. Previously, we've used 5000,
# but this was effectively forcing the fallback logic in `syncBlockRange`
# to always execute multiple requests before getting a successful response.
#
# Besu have raised this default to 5000 in https://github.com/hyperledger/besu/pull/5209
# which is expected to ship in their next release.
#
# Full deposits sync time with various values for this parameter:
#
# Blocks per request | Geth running on the same host | Geth running on a more distant host
# ----------------------------------------------------------------------------------------
# 1000 | 11m 20s | 22m
# 5000 | 5m 20s | 15m 40s
# 100000 | 4m 10s | not tested
#
# The number of requests scales linearly with the parameter value as you would expect.
#
# These results suggest that it would be reasonable for us to get back to 5000 once the
# Besu release is well-spread within their userbase.
# Engine API timeouts
engineApiConnectionTimeout = 5.seconds # How much we wait before giving up connecting to the Engine API
@ -1038,7 +1060,7 @@ type
oldStatusIsOk
disagreement
func compareStatuses(prevStatus, newStatus: PayloadExecutionStatus): StatusRelation =
func compareStatuses(newStatus, prevStatus: PayloadExecutionStatus): StatusRelation =
case prevStatus
of PayloadExecutionStatus.syncing:
if newStatus == PayloadExecutionStatus.syncing:
@ -1896,9 +1918,8 @@ proc syncBlockRange(m: ELManager,
for i in 0 ..< blocksWithDeposits.len:
let blk = blocksWithDeposits[i]
await fetchTimestamp(connection, rpcClient, blk)
if blk.number > fullSyncFromBlock:
await fetchTimestamp(connection, rpcClient, blk)
let lastBlock = m.eth1Chain.blocks.peekLast
for n in max(lastBlock.number + 1, fullSyncFromBlock) ..< blk.number:
debug "Obtaining block without deposits", blockNum = n
@ -1966,6 +1987,15 @@ proc syncEth1Chain(m: ELManager, connection: ELConnection) {.async.} =
let rpcClient = awaitOrRaiseOnTimeout(connection.connectedRpcClient(),
1.seconds)
let
# BEWARE
# `connectedRpcClient` guarantees that connection.web3 will not be
# `none` here, but it's not safe to initialize this later (e.g closer
# to where it's used) because `connection.web3` may be set to `none`
# at any time after a failed request. Luckily, the `contractSender`
# object is very cheap to create.
depositContract = connection.web3.get.contractSender(
DepositContract, m.depositContractAddress)
shouldProcessDeposits = not (
m.depositContractAddress.isZeroMemory or
m.eth1Chain.finalizedBlockHash.data.isZeroMemory)
@ -2064,8 +2094,6 @@ proc syncEth1Chain(m: ELManager, connection: ELConnection) {.async.} =
if shouldProcessDeposits and
latestBlock.number.uint64 > m.cfg.ETH1_FOLLOW_DISTANCE:
let depositContract = connection.web3.get.contractSender(
DepositContract, m.depositContractAddress)
await m.syncBlockRange(connection,
rpcClient,
depositContract,

View File

@ -1807,6 +1807,7 @@ proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref HmacDrbgContext) {.rai
ignoreDeprecatedOption terminalTotalDifficultyOverride
ignoreDeprecatedOption optimistic
ignoreDeprecatedOption validatorMonitorTotals
ignoreDeprecatedOption web3ForcePolling
createPidFile(config.dataDir.string / "beacon_node.pid")

View File

@ -18,7 +18,7 @@ when not defined(nimscript):
const
versionMajor* = 23
versionMinor* = 3
versionBuild* = 0
versionBuild* = 1
versionBlob* = "stateofus" # Single word - ends up in the default graffiti

View File

@ -104,7 +104,21 @@ You can increase the resilience of your setup and eliminate any downtime during
```
!!! tip
You can use a different secret for each connection by specifying `jwtSecret` or `jwtSecretFile` as a query parameter in the anchor section of the URL (e.g. `http://127.0.0.1:8551/#jwtSecret=0x12345...` or `http://127.0.0.1:8551/#jwtSecretFile=/tmp/jwtsecret`).
You can use a different secret for each connection by specifying `jwt-secret` or `jwt-secret-file` as a query parameter in the anchor section of the URL (e.g. `http://127.0.0.1:8551/#jwt-secret=0x12345...` or `http://127.0.0.1:8551/#jwt-secret-file=/tmp/jwtsecret`). If you use a [TOML config file](./options.html#configuration-files), you can also use the following more natural syntax:
```toml
data-dir = "my-data-dir"
rest = true
...
[[el]]
url = "http://127.0.0.1:8551"
jwt-secret-file="/path/to/jwt/file"
[[el]]
url = "http://192.168.1.2:8551"
jwt-secret = ""
```
As long as any of execution clients remains operational and fully synced, Nimbus will keep performing all validator duties.

View File

@ -10,15 +10,18 @@ import
type
CliFlags = object
network {.
network* {.
defaultValue: "mainnet"
name: "network".}: string
elUrls {.
elUrls* {.
name: "el".}: seq[EngineApiUrlConfigValue]
jwtSecret {.
jwtSecret* {.
name: "jwt-secret".}: Option[InputFile]
outDepositsFile {.
outDepositsFile* {.
name: "out-deposits-file".}: Option[OutFile]
configFile* {.
desc: "Loads the configuration from a TOML file"
name: "config-file" .}: Option[InputFile]
proc main(flags: CliFlags) {.async.} =
let
@ -70,4 +73,8 @@ proc main(flags: CliFlags) {.async.} =
info "All deposits downloaded"
waitFor main(load CliFlags)
waitFor main(
load(CliFlags,
secondarySources = proc (config: CliFlags, sources: auto) =
if config.configFile.isSome:
sources.addConfigFile(Toml, config.configFile.get)))

View File

@ -24,6 +24,7 @@ import # Unit test
./test_discovery,
./test_engine_authentication,
./test_eth1_monitor,
./test_el_conf,
./test_eth2_ssz_serialization,
./test_exit_pool,
./test_forks,

1
tests/media/jwt.hex Normal file
View File

@ -0,0 +1 @@
e59c86b378f1acd987a2598a53716082f6f2eb23b4ed4fcacc16d64e8dcb1884

163
tests/test_el_conf.nim Normal file
View File

@ -0,0 +1,163 @@
# beacon_chain
# Copyright (c) 2021-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.used.}
import
unittest2, confutils,
stew/byteutils,
../beacon_chain/eth1/el_conf,
../beacon_chain/spec/engine_authentication
type
ExampleConfigFile = object
dataDir* {.name: "data-dir".}: string
el* {.name: "el".}: seq[EngineApiUrlConfigValue]
proc loadExampleConfig(content: string, cmdLine = newSeq[string]()): ExampleConfigFile =
ExampleConfigFile.load(
cmdLine = cmdLine,
secondarySources = proc (config: ExampleConfigFile, sources: auto) =
sources.addConfigFileContent(Toml, content))
const
validJwtToken = parseJwtTokenValue(
"aa95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098cc").get
suite "EL Configuration":
test "URL parsing":
let url1 = EngineApiUrlConfigValue.parseCmdArg("localhost:8484")
check:
url1.url == "localhost:8484"
url1.roles.isNone
url1.jwtSecret.isNone
url1.jwtSecretFile.isNone
let
url1Final1 = url1.toFinalUrl(some validJwtToken)
url1Final2 = url1.toFinalUrl(none seq[byte])
check:
url1Final1.isOk
url1Final1.get.url == "ws://localhost:8484"
url1Final1.get.jwtSecret.get == validJwtToken
url1Final1.get.roles == defaultEngineApiRoles
url1Final2.isOk
url1Final2.get.url == "ws://localhost:8484"
url1Final2.get.jwtSecret.isNone
url1Final2.get.roles == defaultEngineApiRoles
let url2 = EngineApiUrlConfigValue.parseCmdArg(
"https://eth-node.io:2020#jwt-secret-file=tests/media/jwt.hex")
check:
url2.url == "https://eth-node.io:2020"
url2.roles.isNone
url2.jwtSecret.isNone
url2.jwtSecretFile.get.string == "tests/media/jwt.hex"
let url3 = EngineApiUrlConfigValue.parseCmdArg(
"http://localhost/#roles=sync-deposits&jwt-secret=ee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba")
check:
url3.url == "http://localhost/"
url3.roles == some({DepositSyncing})
url3.jwtSecret == some("ee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba")
url3.jwtSecretFile.isNone
let url3Final = url3.toFinalUrl(some validJwtToken)
check:
url3Final.isOk
url3Final.get.jwtSecret.get.toHex == "ee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba"
url3Final.get.roles == {DepositSyncing}
let url4 = EngineApiUrlConfigValue.parseCmdArg(
"localhost#roles=sync-deposits,validate-blocks&jwt-secret=ee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba23")
check:
url4.url == "localhost"
url4.roles == some({DepositSyncing, BlockValidation})
url4.jwtSecret == some("ee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba23")
url4.jwtSecretFile.isNone
let url4Final = url4.toFinalUrl(some validJwtToken)
check:
not url4Final.isOk # the JWT secret is invalid
let url5 = EngineApiUrlConfigValue.parseCmdArg(
"http://127.0.0.1:9090/#roles=sync-deposits,validate-blocks,produce-blocks,sync-deposits")
check:
url5.url == "http://127.0.0.1:9090/"
url5.roles == some({DepositSyncing, BlockValidation, BlockProduction})
url5.jwtSecret.isNone
url5.jwtSecretFile.isNone
test "Invalid URls":
template testInvalidUrl(url: string) =
expect ValueError:
echo "This URL should be invalid: ", EngineApiUrlConfigValue.parseCmdArg(url)
testInvalidUrl "http://127.0.0.1:9090/#roles="
testInvalidUrl "http://127.0.0.1:9090/#roles=sy"
testInvalidUrl "http://127.0.0.1:9090/#roles=sync-deposits,"
testInvalidUrl "http://127.0.0.1:9090/#roles=sync-deposits;validate-blocks"
testInvalidUrl "http://127.0.0.1:9090/#roles=validate-blocks,sync-deps"
test "Old style config files":
let cfg = loadExampleConfig """
data-dir = "/foo"
el = ["http://localhost:8585", "eth-data.io#roles=sync-deposits", "wss://eth-nodes.io/21312432"]
"""
check:
cfg.dataDir == "/foo"
cfg.el.len == 3
cfg.el[0].url == "http://localhost:8585"
cfg.el[1].url == "eth-data.io"
cfg.el[1].roles == some({DepositSyncing})
cfg.el[2].url == "wss://eth-nodes.io/21312432"
test "New style config files":
let cfg = loadExampleConfig """
data-dir = "my-data-dir"
[[el]]
url = "http://localhost:8585"
jwt-secret-file = "tests/media/jwt.hex"
[[el]]
url = "eth-data.io"
roles = ["sync-deposits", "produce-blocks"]
[[el]]
url = "wss://eth-nodes.io/21312432"
jwt-secret = "0xee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba"
"""
check:
cfg.dataDir == "my-data-dir"
cfg.el.len == 3
cfg.el[0].url == "http://localhost:8585"
cfg.el[0].roles.isNone
cfg.el[0].jwtSecret.isNone
cfg.el[0].jwtSecretFile.get.string == "tests/media/jwt.hex"
cfg.el[1].url == "eth-data.io"
cfg.el[1].roles == some({DepositSyncing, BlockProduction})
cfg.el[1].jwtSecret.isNone
cfg.el[1].jwtSecretFile.isNone
cfg.el[2].url == "wss://eth-nodes.io/21312432"
cfg.el[2].roles.isNone
cfg.el[2].jwtSecret.get == "0xee95565a2cc95553d4bf2185f58658939ba3074ce5695cbabfab4a1eaf7098ba"
cfg.el[2].jwtSecretFile.isNone
test "Empty config file":
let cfg = loadExampleConfig("", cmdLine = @["--data-dir=foo"])
check:
cfg.dataDir == "foo"
cfg.el.len == 0

View File

@ -23,39 +23,19 @@ from ../beacon_chain/spec/presets import
MAX_BYTES_PER_TRANSACTION, MAX_EXTRA_DATA_BYTES, MAX_TRANSACTIONS_PER_PAYLOAD
suite "Eth1 monitor":
test "Rewrite HTTPS Infura URLs":
test "Rewrite URLs":
var
mainnetWssUrl = "wss://mainnet.infura.io/ws/v3/6224f3c792cc443fafb64e70a98f871e"
mainnetHttpUrl = "http://mainnet.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
mainnetHttpsUrl = "https://mainnet.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
goerliWssUrl = "wss://goerli.infura.io/ws/v3/6224f3c792cc443fafb64e70a98f871e"
goerliHttpUrl = "http://goerli.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
goerliHttpsUrl = "https://goerli.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
gethHttpUrl = "http://localhost:8545"
gethHttpsUrl = "https://localhost:8545"
gethWsUrl = "ws://localhost:8545"
unspecifiedProtocolUrl = "localhost:8545"
fixupWeb3Urls mainnetWssUrl
fixupWeb3Urls mainnetHttpUrl
fixupWeb3Urls mainnetHttpsUrl
fixupWeb3Urls goerliWssUrl
fixupWeb3Urls goerliHttpUrl
fixupWeb3Urls goerliHttpsUrl
fixupWeb3Urls gethHttpUrl
fixupWeb3Urls gethHttpsUrl
fixupWeb3Urls gethWsUrl
fixupWeb3Urls unspecifiedProtocolUrl
check:
mainnetWssUrl == "wss://mainnet.infura.io/ws/v3/6224f3c792cc443fafb64e70a98f871e"
mainnetHttpUrl == "http://mainnet.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
mainnetHttpsUrl == "https://mainnet.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
goerliWssUrl == "wss://goerli.infura.io/ws/v3/6224f3c792cc443fafb64e70a98f871e"
goerliHttpUrl == "http://goerli.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
goerliHttpsUrl == "https://goerli.infura.io/v3/6224f3c792cc443fafb64e70a98f871e"
gethHttpUrl == "http://localhost:8545"
gethHttpsUrl == "https://localhost:8545"
unspecifiedProtocolUrl == "ws://localhost:8545"

@ -1 +1 @@
Subproject commit 56f4db90f7923a4d6814837dda9f44c8955c52a4
Subproject commit c4c11c52ce5c5f48188069243d29b48b37e41e53

@ -1 +1 @@
Subproject commit a243648f241c205b5b7fc72abb0f9c14a2812b3a
Subproject commit 86d477136f105f04bfd0dd7c0e939593d81fc581