ERC-6860: Web3 URL 到 EVM 调用消息的转换
一个 HTTP 样式的 Web3 URL 到 EVM 调用消息的转换
Authors | Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn), Nicolas Deschildre (@nand2) |
---|---|
Created | 2023-09-29 |
Discussion Link | https://ethereum-magicians.org/t/eip-4804-web3-url-to-evm-call-message-translation/8300 |
Requires | EIP-137 |
Table of Contents
摘要
本标准将 RFC 3986 URI(如 web3://uniswap.eth/
)转换为 EVM 消息,例如:
EVMMessage {
To: 0xaabbccddee.... // 其中 uniswap.eth 的地址在 ENS 注册
Calldata: 0x
...
}
⚠️ 本提案通过小的更正、澄清和修改来更新 ERC-4804。
动机
目前,从 Web3 读取数据通常依赖于 Web2 代理到 Web3 区块链的转换。这种转换主要由 dApp 网站/节点服务提供商/etherscan 等代理完成,这些代理不受用户控制。这里的标准旨在为 Web2 用户提供一种直接访问 Web3 内容的简单方法,特别是链上 Web 内容,如 SVG/HTML。此外,该标准使得能够与已经兼容 URI 的其他标准(如 SVG/HTML)互操作。
规范
本规范仅定义只读(即 Solidity 的 view
函数)语义。状态修改函数可能会在未来的扩展中定义。
本规范使用 RFC 2234 的增强巴科斯-诺尔范式 (ABNF) 符号。完整的 URI 语法列于附录 A。
Web3 URL 是以下形式的 ASCII 字符串:
web3URL = schema "://" [ userinfo "@" ] contractName [ ":" chainid ] pathQuery [ "#" fragment ]
schema = "w3" / "web3"
userinfo = address
userinfo 表示哪个用户正在调用 EVM,即 EVM 调用消息中的“From”字段。如果未指定,协议将使用 0x0 作为发送者地址。
contractName = address
/ domainName
address = "0x" 20( HEXDIG HEXDIG )
domainName = *( unreserved / pct-encoded / sub-delims ) ; 如 RFC 3986 中所述
contractName 表示要调用的合约,即 EVM 调用消息中的“To”字段。如果 contractName 是一个地址,那么它将被用于“To”字段。否则,contractName 是来自域名服务的域名,并且必须解析为一个地址才能用于“To”字段。
将域名从域名服务解析为地址的方法在 ERC-6821 中针对以太坊名称服务进行了指定,并且将在后续 ERC 中针对其他名称服务进行讨论。
chainid = %x31-39 *DIGIT
chainid 表示要解析 contractName 并调用消息的链。如果未指定,协议将使用所使用的名称服务提供商的主链,例如,eth 为 1。如果没有使用名称服务提供商,则默认的 chainid 为 1。
pathQuery = mPathQuery ; 手动模式的 path+query
/ aPathQuery ; 自动模式的 path+query
pathQuery,由路径和可选查询组成,根据解析模式是“手动”还是“自动”,将具有不同的结构。
fragment = *VCHAR
fragment,像 HTTP URL 中一样,是一个字符字符串,用于引用资源,不会传输到智能合约。
web3UrlRef = web3URL
/ relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery = relMPathQuery ; 手动模式的相对 URL path+query
/ relAPathQuery ; 自动模式的相对 URL path+query
支持相对 URL,但支持程度取决于解析模式。
解析模式
一旦确定了“To”地址和 chainid,协议将通过调用“To”地址的 resolveMode
方法来检查合约的解析模式。resolveMode
的 Solidity 签名是:
function resolveMode() external returns (bytes32);
该协议当前支持两种解析模式:自动和手动。
- 如果
resolveMode
返回值是0x6d616e75616c0000000000000000000000000000000000000000000000000000
,即 bytes32 中的“manual”,将使用手动模式。 - 如果:
resolveMode
返回值是0x6175746f00000000000000000000000000000000000000000000000000000000
,即 bytes32 中的“auto”,或者resolveMode
返回值是0x0000000000000000000000000000000000000000000000000000000000000000
,或者- 对
resolveMode
的调用抛出错误(方法未实现或方法中抛出错误) 将使用自动模式。
- 否则,协议将使请求失败,并显示错误“不支持的解析模式”。
手动模式
mPathQuery = mPath [ "?" mQuery ]
mPath = mPathAbempty ; 以 "/" 开头或为空
mPathAbempty = [ *( "/" segment ) "/" segment [ "." fileExtension ] ]
segment = *pchar ; 如 RFC 3986 中所述
fileExtension = 1*( ALPHA / DIGIT )
mQuery = *( pchar / "/" / "?" ) ; 如 RFC 3986 中所述
手动模式将直接使用原始的 mPathQuery 作为消息的 calldata(不会进行百分号编码解码)。如果 mPathQuery 为空,则发送的 calldata 将为 /
(0x2f)。
返回的消息数据将被视为 ABI 编码的字节,解码后的字节将返回到前端。
默认情况下,返回到前端的 MIME 类型是 text/html
,但如果存在 fileExtension,则会被覆盖。在这种情况下,MIME 类型将从文件名扩展名中推断出来。
relMPathQuery = relMPath [ "?" mQuery ]
relMPath = mPathAbsolute ; 以 "/" 开头,但不以 "//" 开头
/ mPathNoscheme ; 以非冒号段开头
/ mPathEmpty ; 零个字符
mPathAbsolute = "/" [ segmentNz *( "/" segment ) ] [ "." fileExtension ]
mPathNoscheme = segmentNzNc *( "/" segment ) [ "." fileExtension ]
mPathEmpty = 0<pchar>
segmentNz = 1*pchar ; 如 RFC 3986 中所述
segmentNzNc = 1*( unreserved / pct-encoded / sub-delims / "@" )
; 如 RFC 3986 中所述:不包含任何冒号 ":" 的非零长度段
对手动模式相对 URL 的支持与 HTTP URL 类似:允许相对于当前合约的 URL,包括绝对路径和相对路径。
自动模式
aPathQuery = aPath [ "?" aQuery ]
aPath = [ "/" [ method *( "/" argument ) ] ]
在自动模式下,如果 aPath 为空或为 “/”,则协议将使用空 calldata 调用目标合约。否则,EVM 消息的 calldata 将使用标准的 Solidity 合约 ABI。
method = ( ALPHA / "$" / "_" ) *( ALPHA / DIGIT / "$" / "_" )
method 是要调用的函数方法字符串。
argument = boolArg
/ uintArg
/ intArg
/ addressArg
/ bytesArg
/ stringArg
boolArg = [ "bool!" ] ( "true" / "false" )
uintArg = [ "uint" [ intSizes ] "!" ] 1*DIGIT
intArg = "int" [ intSizes ] "!" 1*DIGIT
intSizes = "8" / "16" / "24" / "32" / "40" / "48" / "56" / "64" / "72" / "80" / "88" / "96" / "104" / "112" / "120" / "128" / "136" / "144" / "152" / "160" / "168" / "176" / "184" / "192" / "200" / "208" / "216" / "224" / "232" / "240" / "248" / "256"
addressArg = [ "address!" ] ( address / domainName )
bytesArg = [ "bytes!" ] bytes
/ "bytes1!0x" 1( HEXDIG HEXDIG )
/ "bytes2!0x" 2( HEXDIG HEXDIG )
...
/ "bytes32!0x" 32( HEXDIG HEXDIG )
stringArg = "string!" *pchar [ "." fileExtension ]
argument 是方法的一个参数,具有类型无关的 [ type "!" ] value
语法。如果指定了 type,则该值将被转换为相应的类型。该协议目前支持这些基本类型:bool、int、uint、int<X>、uint<X>(X 的范围从 8 到 256,步长为 8)、address、bytes<X>(X 的范围从 1 到 32)、bytes 和 string。如果未指定 type,则将按照以下顺序规则自动检测类型:
- type=”uint256”,如果 value 是数字;或者
- type=”bytes32”,如果 value 的形式为 0x+32 字节数据十六进制;或者
- type=”address”,如果 value 的形式为 0x+20 字节数据十六进制;或者
- type=”bytes”,如果 value 的形式为 0x 后跟除 20 或 32 之外的任意数量的字节;或者
- type=”bool”,如果 value 是
true
或false
;或者 - 否则 type=”address”,并将参数解析为域名。如果无法解析域名,将返回不支持的名称服务提供商错误。
aQuery = attribute *( "&" attribute )
attribute = attrName "=" attrValue
attrName = "returns"
/ "returnTypes"
attrValue = [ "(" [ retTypes ] ")" ]
retTypes = retType *( "," retType )
retType = retRawType *( "[" [ %x31-39 *DIGIT ] "]" )
retRawType = "(" retTypes ")"
/ retBaseType
retBaseType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string"
bytesSizes = %x31-39 ; 1-9
/ ( "1" / "2" ) DIGIT ; 10-29
/ "31" / "32" ; 31-32
aQuery 中的 “returns” 属性指示返回数据的格式。它遵循以太坊 ABI 函数签名的参数部分的语法(授权使用 uint
和 int
别名)。
- 如果 “returns” 属性值未定义或为空,则返回的消息数据将被视为 ABI 编码的字节,解码后的字节将被返回到前端。默认情况下,返回到前端的 MIME 类型将是未定义的,但如果最后一个参数是字符串类型并且具有 fileExtension,则会被覆盖,在这种情况下,MIME 类型将从文件名扩展名中推断出来。(请注意,fileExtension 不会从给智能合约的字符串参数中排除)
- 如果 “returns” 属性值等于 “()”,则返回的消息数据的原始字节将以 JSON 格式的数组中以 “0x” 开头的十六进制字符串的形式返回:
["0xXXXXX"]
- 否则,返回的消息数据将按照 returns 值中指定的数据类型进行 ABI 解码,并以 JSON 格式进行编码。数据的编码将遵循以太坊 JSON-RPC 格式:
- 未格式化的数据(字节、地址)将编码为十六进制,以 “0x” 为前缀,每个字节两个十六进制数字
- 数量(整数)将编码为十六进制,以 “0x” 为前缀,最紧凑的表示形式(稍微例外:零应表示为 “0x0”)
- 布尔值和字符串将是原生 JSON 布尔值和字符串
如果存在多个 “returns” 属性,则将应用最后一个 “returns” 属性的值。请注意,”returnTypes” 是 “returns” 的别名,但不建议使用,主要用于 ERC-4804 向后兼容目的。
relAPathQuery = aPath [ "?" aQuery ]
对自动模式相对 URL 的支持是有限的:允许相对于当前合约的 URL,并将引用自身(空)、/
路径或完整的方法及其参数。
示例
示例 1a
web3://w3url.eth/
其中 w3url.eth 的合约处于手动模式。
协议将从链 ID 1(主网)上的 ENS 中找到 w3url.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4]
= “0xDD473FAE” 调用该地址,这将以 ABI 类型 “(bytes32)” 返回 “manual”。在确定合约的手动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x2F” 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html
的信息。
示例 1b
web3://w3url.eth/
其中 w3url.eth 的合约处于自动模式。
协议将从链 ID 1(主网)上的 ENS 中找到 w3url.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4]
= “0xDD473FAE” 调用该地址,这将返回 ““,即合约处于自动模式。在确定合约的自动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “” 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为未定义的信息。
示例 2
web3://cyberbrokers-meta.eth/renderBroker/9999
其中 cyberbrokers-meta.eth 的合约处于自动模式。
协议将从链 ID 1(主网)上的 ENS 中找到 cyberbrokers-meta.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4]
= “0xDD473FAE” 调用该地址,这将返回 ““,即合约处于自动模式。在确定合约的自动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x” + keccak("renderBroker(uint256)")[0:4] + abi.encode(uint256(9999))
调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为未定义的信息。
示例 3
web3://vitalikblog.eth:5/
其中 vitalikblog.eth:5 的合约处于手动模式。
协议将从链 ID 5(Goerli)上的 ENS 中找到 vitalikblog.eth 的地址。然后在确定合约处于手动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x2F”,且链 ID = 5 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html
的信息。
示例 4
web3://0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170/
其中合约 “0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170” 处于手动模式。
在确定合约处于手动模式后,协议将使用 “To” = “0xe4ba0e245436b737468c206ab5c8f4950597ab7f” 和 “Calldata” = “0x2F”,且链 ID = 42170 (Arbitrum Nova) 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html
的信息。
示例 5
web3://0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/balanceOf/vitalik.eth?returns=(uint256)
其中合约 “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48” 处于自动模式。
协议将从链 ID 1(主网)上的 ENS 中找到 vitalik.eth 的地址,然后使用 vitalik.eth 的地址调用合约的 “balanceOf(address)” 方法。来自合约调用的返回数据将被视为 ABI 类型 “(uint256)”,并且解码后的数据将以 JSON 格式(如 [ "0x9184e72a000" ]
)返回到前端,并提供 MIME 类型为 application/json
的信息。
示例 6
web3://0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/balanceOf/vitalik.eth?returns=()
其中合约 “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48” 处于自动模式。
协议将从链 ID 1(主网)上的 ENS 中找到 vitalik.eth 的地址,然后调用该地址的 “balanceOf(address)” 方法。来自合约调用的返回数据将被视为原始字节,并将以 JSON 格式(如 ["0x000000000000000000000000000000000000000000000000000009184e72a000"]
)进行编码,并返回到前端,并提供 MIME 类型为 application/json
的信息。
附录 A:Web3 URL 的完整 ABNF
web3URL = schema "://" [ userinfo "@" ] contractName [ ":" chainid ] pathQuery [ "#" fragment ]
schema = "w3" / "web3"
userinfo = address
contractName = address
/ domainName
chainid = %x31-39 *DIGIT
pathQuery = mPathQuery ; 手动模式的 path+query
/ aPathQuery ; 自动模式的 path+query
fragment = *VCHAR
web3UrlRef = web3URL
/ relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery = relMPathQuery ; 手动模式的相对 URL path+query
/ relAPathQuery ; 自动模式的相对 URL path+query
mPathQuery = mPath [ "?" mQuery ]
mPath = mPathAbempty ; 以 "/" 开头或为空
relMPathQuery = relMPath [ "?" mQuery ]
relMPath = mPathAbsolute ; 以 "/" 开头,但不以 "//" 开头
/ mPathNoscheme ; 以非冒号段开头
/ mPathEmpty ; 零个字符
mPathAbempty = [ *( "/" segment ) "/" segment [ "." fileExtension ] ]
mPathAbsolute = "/" [ segmentNz *( "/" segment ) ] [ "." fileExtension ]
mPathNoscheme = segmentNzNc *( "/" segment ) [ "." fileExtension ]
mPathEmpty = 0<pchar>
segment = *pchar ; 如 RFC 3986 中所述
segmentNz = 1*pchar ; 如 RFC 3986 中所述
segmentNzNc = 1*( unreserved / pct-encoded / sub-delims / "@" )
; 如 RFC 3986 中所述:不包含任何冒号 ":" 的非零长度段
mQuery = *( pchar / "/" / "?" ) ; 如 RFC 3986 中所述
aPathQuery = aPath [ "?" aQuery ]
aPath = [ "/" [ method *( "/" argument ) ] ]
relAPathQuery = aPath [ "?" aQuery ]
method = ( ALPHA / "$" / "_" ) *( ALPHA / DIGIT / "$" / "_" )
argument = boolArg
/ uintArg
/ intArg
/ addressArg
/ bytesArg
/ stringArg
boolArg = [ "bool!" ] ( "true" / "false" )
uintArg = [ "uint" [ intSizes ] "!" ] 1*DIGIT
intArg = "int" [ intSizes ] "!" 1*DIGIT
intSizes = "8" / "16" / "24" / "32" / "40" / "48" / "56" / "64" / "72" / "80" / "88" / "96" / "104" / "112" / "120" / "128" / "136" / "144" / "152" / "160" / "168" / "176" / "184" / "192" / "200" / "208" / "216" / "224" / "232" / "240" / "248" / "256"
addressArg = [ "address!" ] ( address / domainName )
bytesArg = [ "bytes!" ] bytes
/ "bytes1!0x" 1( HEXDIG HEXDIG )
/ "bytes2!0x" 2( HEXDIG HEXDIG )
/ "bytes3!0x" 3( HEXDIG HEXDIG )
/ "bytes4!0x" 4( HEXDIG HEXDIG )
/ "bytes5!0x" 5( HEXDIG HEXDIG )
/ "bytes6!0x" 6( HEXDIG HEXDIG )
/ "bytes7!0x" 7( HEXDIG HEXDIG )
/ "bytes8!0x" 8( HEXDIG HEXDIG )
/ "bytes9!0x" 9( HEXDIG HEXDIG )
/ "bytes10!0x" 10( HEXDIG HEXDIG )
/ "bytes11!0x" 11( HEXDIG HEXDIG )
/ "bytes12!0x" 12( HEXDIG HEXDIG )
/ "bytes13!0x" 13( HEXDIG HEXDIG )
/ "bytes14!0x" 14( HEXDIG HEXDIG )
/ "bytes15!0x" 15( HEXDIG HEXDIG )
/ "bytes16!0x" 16( HEXDIG HEXDIG )
/ "bytes17!0x" 17( HEXDIG HEXDIG )
/ "bytes18!0x" 18( HEXDIG HEXDIG )
/ "bytes19!0x" 19( HEXDIG HEXDIG )
/ "bytes20!0x" 20( HEXDIG HEXDIG )
/ "bytes21!0x" 21( HEXDIG HEXDIG )
/ "bytes22!0x" 22( HEXDIG HEXDIG )
/ "bytes23!0x" 23( HEXDIG HEXDIG )
/ "bytes24!0x" 24( HEXDIG HEXDIG )
/ "bytes25!0x" 25( HEXDIG HEXDIG )
/ "bytes26!0x" 26( HEXDIG HEXDIG )
/ "bytes27!0x" 27( HEXDIG HEXDIG )
/ "bytes28!0x" 28( HEXDIG HEXDIG )
/ "bytes29!0x" 29( HEXDIG HEXDIG )
/ "bytes30!0x" 30( HEXDIG HEXDIG )
/ "bytes31!0x" 31( HEXDIG HEXDIG )
/ "bytes32!0x" 32( HEXDIG HEXDIG )
stringArg = "string!" *pchar [ "." fileExtension ]
aQuery = attribute *( "&" attribute )
attribute = attrName "=" attrValue
attrName = "returns"
/ "returnTypes"
attrValue = [ "(" [ retTypes ] ")" ]
retTypes = retType *( "," retType )
retType = retRawType *( "[" [ %x31-39 *DIGIT ] "]" )
retRawType = "(" retTypes ")"
/ retBaseType
retBaseType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string"
bytesSizes = %x31-39 ; 1-9
/ ( "1" / "2" ) DIGIT ; 10-29
/ "31" / "32" ; 31-32
domainName = *( unreserved / pct-encoded / sub-delims ) ; 如 RFC 3986 中所述
fileExtension = 1*( ALPHA / DIGIT )
address = "0x" 20( HEXDIG HEXDIG )
bytes = "0x" *( HEXDIG HEXDIG )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@" ; 如 RFC 3986 中所述
pct-encoded = "%" HEXDIG HEXDIG ; 如 RFC 3986 中所述
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" ; 如 RFC 3986 中所述
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "=" ; 如 RFC 3986 中所述
附录 B:与 ERC-4804 的不同之处
修正
- 手动模式:ERC-4804 规定不对路径 [ “?” query ] 进行解释。此 ERC 指出,实际上会对路径进行解释,以确定 MIME 类型。
- 自动模式:如果 query 中没有 returns 属性,ERC-4804 规定返回的数据被视为 ABI 编码的 bytes32。此 ERC 指出,实际上返回的数据被视为 ABI 编码的字节。
澄清
- 正式规范:此 ERC 添加了 URL 格式的 ABNF 定义。
- 解析模式:此 ERC 指出了有关如何确定解析模式的更多详细信息。
- 手动模式:此 ERC 指出了如何处理 URI 百分号编码、返回数据以及如何确定 MIME 类型。
- 自动模式:此 ERC 更详细地指出了参数值的编码,以及 returns 值的格式和处理。
- 示例:此 ERC 为示例添加了更多详细信息。
修改
- 协议名称:ERC-4804 提到了
ethereum-web3://
和eth-web3://
,这些已被删除。 - 自动模式:支持的类型:ERC-4804 仅支持 uint256、bytes32、address、bytes 和 string。此 ERC 添加了更多类型。
- 自动模式:指定 returns 属性时,返回整数的编码:ERC-4804 在示例 5 中建议将整数编码为字符串。此 ERC 指出要遵循以太坊 JSON RPC 规范,并将整数编码为十六进制字符串,并以 “0x” 为前缀。
理由
该提案的目的是为以太坊添加一个去中心化的表示层。有了这一层,我们就能够使用人类可读的 URL 在链上渲染任何 Web 内容(包括 HTML/CSS/JPG/PNG/SVG 等),从而使 EVM 可以作为去中心化的后端。该标准的设计基于以下原则:
-
人类可读。Web3 URL 应该像 Web2 URL (
http://
) 一样,易于人类识别。因此,我们支持来自名称服务的名称来代替地址,以提高可读性。此外,我们使用人类可读的方法 + 参数,而不是使用十六进制的 calldata,并将它们转换为 calldata,以提高可读性。 -
与 HTTP-URL 标准最大程度地兼容。Web3 URL 应该与 HTTP-URL 标准兼容,包括相对路径、查询、片段、百分号编码等,以便可以轻松地将对现有 HTTP-URL 的支持(例如,通过浏览器)扩展到 Web3 URL,而只需进行最少的修改。这也意味着现有的 Web2 用户可以轻松地迁移到 Web3,而只需掌握最少的该标准知识。
-
简单。我们没有在参数中提供显式类型,而是使用“最大似然”原则自动检测参数的类型,例如地址、bytes32 和 uint256。这可以大大缩短 URL 的长度,同时避免混淆。此外,也支持显式类型,以便在必要时消除混淆。
-
灵活。合约可以覆盖编码规则,以便合约可以精细地控制理解用户想要定位的实际 Web 资源。
安全注意事项
未发现任何安全注意事项。
版权
在 CC0 下放弃版权和相关权利。
Citation
Please cite this document as:
Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn), Nicolas Deschildre (@nand2), "ERC-6860: Web3 URL 到 EVM 调用消息的转换 [DRAFT]," Ethereum Improvement Proposals, no. 6860, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6860.