Table of Contents generated with DocToc
- Overview
- Quick Start
- Configuration
- Testing
- Local Development
- Storage
- API
- TAG Generator for Proxy Methods
- JSONRPC Server Batch Request Limit
ChainNode is a highly available and scalable Node as a Service (NaaS) built on top of ChainStorage. Under the hooks, the on-chain data is backed by a fast key-value store, and most of the queries are served directly by the key-value store. The data is continuously replicated from ChainStorage (which in turn synchronizes the changes from a small node cluster), and re-indexed for various query patterns.
Make sure your local go version is 1.18 by running the following commands:
brew install go@1.18
brew unlink go
brew link go@1.18
To set up for the first time (only done once):
make bootstrap
Rebuild everything:
make build
ChainNode depends on the following environment variables to resolve the path of the configuration.
The directory structure is as follows: config/{namespace}/{blockchain}/{network}/{environment}.yml
.
CHAINNODE_NAMESPACE
: A{namespace}
is logical grouping of several services, each of which manages its own blockchain and network. The default namespace is chainnode. To deploy a different namespace, set the env var to the name of a subdirectory of ./config.CHAINNODE_CONFIG
: This env var, in the format of{blockchain}-{network}
, determines the blockchain and network managed by the service. The naming is defined in c3/common.CHAINNODE_ENVIRONMENT
: This env var controls the{environment}
in which the service is deployed. Possible values includeproduction
,development
, andlocal
(which is also the default value).
Endpoint group is an abstraction for one or more JSON-RPC endpoints.
EndpointProvider uses the endpoint_group
config to implement
client-side routing to the node provider.
ChainNode utilizes two endpoint groups to serve its needs to the node provider:
- primary: This endpoint group is used to proxy the requests from the downstream node providers. Certain APIs, such as eth_call and eth_getBalance, cannot be fulfilled by the storage layer, because the data is not available in ChainNode data source ChainStorage. To handle such requests, ChainNode redirect the request to this primary endpoint provider instead.
- validator: This endpoint group is used for validation purpose. For a percentage of incoming requests to ChainNode, the validation is performed by comparing the response from ChainNode and the response from this validator endpoint provider for an identical request.
If your node provider, e.g. QuickNode, already has built-in load balancing, your endpoint group may contain only one endpoint, as illustrated by the following configuration:
chain:
client:
primary:
endpoint_group: |
{
"endpoints": [
{
"name": "quicknode-foo-bar-sticky",
"url": "https://foo-bar.matic.quiknode.pro/****",
"weight": 1
}
]
}
validator:
endpoint_group: |
{
"endpoints": [
{
"name": "quicknode-foo-bar-round-robin",
"url": "https://foo-bar.matic.quiknode.pro/****",
"weight": 1
}
]
}
You may override any configuration using an environment variable. The environment variable should be prefixed with "CHAINNODE_". For nested dictionary, use underscore to separate the keys.
For example, you may override the endpoint group config at runtime by injecting the following environment variables:
- primary: CHAINNODE_CHAIN_CLIENT_PRIMARY_ENDPOINT_GROUP
- validator: CHAINNODE_CHAIN_CLIENT_VALIDATOR_ENDPOINT_GROUP
Alternatively, you may override the configuration by creating secrets.yml
within the same directory. Its attributes
will be merged into the runtime configuration and take the highest precedence. Note that this file may contain
credentials and is excluded from check-in by .gitignore
.
# Run everything
make test
# Run the controller package only
make test TARGET=internal/controller/...
# Run everything
make integration
# Run the workflow package only
make integration TARGET=internal/workflow/...
# Run TestIntegrationBlobByHashStorageTestSuite only
make integration TARGET=internal/storage/... TEST_FILTER=TestIntegrationBlobByHashStorageTestSuite
# Run everything
make functional
# Run ControllerTestSuite only
make functional TARGET=internal/controller/... TEST_FILTER=ControllerTestSuite
# Run one test in ControllerTestSuite
make functional TARGET=internal/controller/... TEST_FILTER=ControllerTestSuite/TestBlocksByHash
Start the dockers by the docker-compose file from project root folder:
make localstack
The next step is to start the server locally in a new terminal:
# Ethereum Mainnet
# Use aws local stack
# Starts API server
make server
# Setup ChainStorage SDK credentials:
export CHAINSTORAGE_SDK_AUTH_HEADER=cb-nft-api-token
export CHAINSTORAGE_SDK_AUTH_TOKEN=****
# Starts workflow workers
make worker
# If want to start testnet (goerli) server
# Use aws local stack
make server CHAINNODE_CONFIG_NAME=ethereum-goerli
make worker CHAINNODE_CONFIG_NAME=ethereum-goerli
After finishing running server and worker, manage workflows using admin CLI:
# start coordinator locally
go run ./cmd/admin workflow start --workflow coordinator --input '{}' --blockchain ethereum --env local --network mainnet
# stop coordinator locally
go run ./cmd/admin workflow stop --workflow coordinator --input '' --blockchain ethereum --env local --network mainnet
# start an ingestor for blocks collection locally.
# Note that if you already run coordinator locally, there is no need to start an individual ingestor.
# This command is used when a new ingestor is being developed.
# When local testing, please specify the desired tag inside the input struct. Example: '{"collection":"blocks", tag:1}'
go run ./cmd/admin workflow start --workflow ingestor --input '{"collection":"blocks"}' --blockchain ethereum --env local --network mainnet
# stop an ingestor for blocks collection locally
go run ./cmd/admin workflow stop --workflow ingestor --input '{}' --blockchain ethereum --env local --network mainnet --workflowID workflow.ingestor/blocks/{tag}
Inspect a table using AWS CLI:
# scan the entire table
aws dynamodb --no-sign-request --region local --endpoint-url http://localhost:4566 scan --table-name chainnode-collection-ethereum-mainnet --attributes-to-get "pk" "sk" "tag" "height" "hash"
# get a specific item
aws dynamodb --no-sign-request --region local --endpoint-url http://localhost:4566 get-item --table-name chainnode-collection-ethereum-mainnet --key '{"pk": {"S": "1#checkpoints"}, "sk": {"S": "blocks"}}'
The data within each dynamoDB item is compressed uzing gzip format. base64 -d | gunzip
can be used to
uncompress it to make it readable in the terminal. Example:
# Make data human-readable and format JSON
aws dynamodb --no-sign-request --region local --endpoint-url http://localhost:4566 get-item --table-name chainnode-collection-ethereum-mainnet --key '{"pk": {"S": "<partition_key>"}, "sk": {"S": "<sort_key>"}}' | jq -r '.Item.data.B' | base64 -d | gunzip | jq
- Local:
localhost:8000
Caveat: ChainNode only supports the following block tags: latest
, earliest
, and block number in hex format.
# eth_blockNumber: https://eth.wiki/json-rpc/API#eth_blocknumber
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber"}' | jq
# eth_getBlockByNumber: https://eth.wiki/json-rpc/API#eth_getblockbynumber
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber", "params": ["0xdad3c1", false]}' | jq
# eth_getBlockByNumber with proxying turned off
curl -s localhost:8000/v1 -H "Content-Type: application/json" -H "x-chainnode-routing-mode: native-only" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber", "params": ["pending", false]}' | jq
# eth_getBlockByHash: https://eth.wiki/json-rpc/API#eth_getblockbyhash
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByHash", "params": ["0x849a3ac8f0d81df1a645701cdb9f90e58500d2eabb80ff3b7f4e8c13f025eff2", false]}' | jq
# eth_getBlockTransactionCountByHash https://eth.wiki/json-rpc/API#eth_getblocktransactioncountbyhash
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockTransactionCountByHash", "params": ["0x849a3ac8f0d81df1a645701cdb9f90e58500d2eabb80ff3b7f4e8c13f025eff2"]}' | jq
# GetBlockTransactionCountByNumber https://eth.wiki/json-rpc/API#eth_getblocktransactioncountbynumber
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockTransactionCountByNumber", "params": ["0xdad3c1"]}' | jq
# eth_getTransactionByHash: https://eth.wiki/json-rpc/API#eth_gettransactionbyhash
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByHash", "params": ["0x633982a26e0cfba940613c52b31c664fe977e05171e35f62da2426596007e249"]}' | jq
# eth_getTransactionReceipt: https://eth.wiki/json-rpc/API#eth_gettransactionreceipt
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt", "params": ["0x633982a26e0cfba940613c52b31c664fe977e05171e35f62da2426596007e249"]}' | jq
# eth_getTransactionByBlockHashAndIndex https://eth.wiki/json-rpc/API#eth_gettransactionbyblockhashandindex
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByBlockHashAndIndex", "params": ["0x849a3ac8f0d81df1a645701cdb9f90e58500d2eabb80ff3b7f4e8c13f025eff2", "0x0"]}' | jq
# eth_getTransactionByBlockNumberAndIndex https://eth.wiki/json-rpc/API#eth_gettransactionbyblocknumberandindex
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByBlockNumberAndIndex", "params": ["0xdad3c1", "0x0"]}' | jq
# eth_getLogs: https://eth.wiki/json-rpc/API#eth_getlogs
# CAVEAT: eth_getLogs has a max block range limit of 1000 blocks.
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getLogs", "params": [{"fromBlock": "0xdad3c1", "toBlock": "0xdad3c2"}]}' | jq
# eth_call: https://eth.wiki/json-rpc/API#eth_call
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_call", "params":[{ "to": "0x514910771af9ca656af840dff83e8264ecf986ca", "data": "0x70a08231000000000000000000000000f27eee60abacb983251fea941dd7350280a538ba"}, "latest"]}' | jq
# eth_getBalance: https://eth.wiki/json-rpc/API#eth_getbalance
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params":["0x8d97689c9818892b700e27f316cc3e41e17fbeb9", "latest"]}' | jq
# eth_getCode: https://eth.wiki/json-rpc/API#eth_getcode
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getCode", "params":["0x7f268357a8c2552623316e2562d90e642bb538e5", "latest"]}' | jq
# eth_getTransactionCount: https://eth.wiki/json-rpc/API#eth_gettransactioncount
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionCount", "params":["0xe222489ae12e15713cc1d65dd0ab2f5b18721bfd", "latest"]}' | jq
# eth_chainId: https://docs.alchemy.com/alchemy/apis/ethereum/eth-chainid
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_chainId"}' | jq
# eth_sendRawTransaction: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": ["0xf889808609184e72a00082271094000000000000000000000000000000000000000080a47f74657374320000000000000000000000000000000000000000000000000000006000571ca08a8bbf888cfa37bbf0bb965423625641fc956967b81d12e23709cead01446075a01ce999b56a8a88504be365442ea61239198e23d1fce7d00fcfc5cd3b44b7215f"]}'
# eth_gasPrice: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gasprice
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_gasPrice"}'
# eth_getStorageAt: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getStorageAt", "params": ["0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b", "0x0", "latest"]}'
# eth_estimateGas: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_estimategas
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_estimateGas", "params": [{"from": "0x8d97689c9818892b700e27f316cc3e41e17fbeb9", "to": "0xd3cda913deb6f67967b99d67acdfa1712c293601", "value": "0x1"}]}'
#eth_protocolVersion: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_protocolversion
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_protocolVersion"}'
#eth_syncing: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_syncing
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_syncing"}'
# eth_feeHistory
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_feeHistory", "params": [4, "latest", [25, 75]]}'
# eth_mining: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_mining
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_mining"}'
# eth_hashrate: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_hashrate
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_hashrate"}'
# eth_accounts: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_accounts
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_accounts"}'
# eth_newFilter: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_newFilter", "params":[{}]}'
# eth_newBlockFilter: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newblockfilter
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_newBlockFilter"}'
# eth_uninstallFilter: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_uninstallfilter
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_uninstallFilter", "params":["0x81440f9af726125cb7fc671eb0f2d8728d6ad699989a"]}'
# eth_getFilterChanges: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getfilterchanges
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getFilterChanges", "params":["0x81440f9af726125cb7fc671eb0f2d8728d6ad699989a"]}'
# eth_getFilterLogs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getfilterlogs
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getFilterLogs", "params":["0x81440f9af726125cb7fc671eb0f2d8728d6ad699989a"]}'
# eth_getWork: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getwork
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_getWork"}'
# eth_submitWork: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_submitwork
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_submitWork", "params": ["0x0000000000000001", "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "0xD1FE5700000000000000000000000000D1FE5700000000000000000000000000"]}'
# eth_submitHashrate: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_submithashrate
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "eth_submitHashrate", "params":["0x500000", "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c"]}'
Caveat: ChainNode only supports the following tracer types: callTracer
.
# debug_traceBlockByHash: https://geth.ethereum.org/docs/rpc/ns-debug#debug_traceblockbyhash
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "debug_traceBlockByHash", "params": ["0xe075488f2716495e97c43f6eb2994964074a70245cca5844b308479ccbbb9ae7", {"tracer": "callTracer"}]}' | jq
# debug_traceBlockByNumber: https://geth.ethereum.org/docs/rpc/ns-debug#debug_traceblockbynumber
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "debug_traceBlockByNumber", "params": ["0xe11130", {"tracer": "callTracer"}]}' | jq
# net_version: https://eth.wiki/json-rpc/API#net_version
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "net_version"}' | jq
# net_listening: https://ethereum.org/en/developers/docs/apis/json-rpc/#net_listening
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "net_listening"}' | jq
# net_peercount: https://ethereum.org/en/developers/docs/apis/json-rpc/#net_peercount
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "net_peerCount"}' | jq
# web3_clientVersion: https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "web3_clientVersion"}'
# batch request of eth_getTransactionReceipt: https://www.jsonrpc.org/specification#batch
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '[{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt", "params": ["0x633982a26e0cfba940613c52b31c664fe977e05171e35f62da2426596007e249"]}, { "jsonrpc": "2.0", "id": 2, "method": "eth_getTransactionReceipt", "params": ["0x3a7d521b20b5684e0e9ec14aeebe8ccab67137f7d5c2589efb55b0625fcc9c6d"]}]' | jq
# bor_getAuthor: https://www.quicknode.com/docs/polygon/bor_getAuthor
curl -s localhost:8000/v1 -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": 1, "method": "bor_getAuthor", "params": ["latest"]}' | jq .
# GraphQL is available in select network: https://geth.ethereum.org/docs/interacting-with-geth/rpc/graphql
curl -s localhost:8000/v1/graphql -H "Content-Type: application/json" -d '{ "query": "query { block { number } }" }'
- Install geth following the instructions
- Run the following command:
~$ geth attach localhost:8000/v1
Welcome to the Geth JavaScript console!
instance: Geth/v1.10.19-omnibus-1fd05ab6/linux-amd64/go1.18.3
at block: 15135980 (Wed Jul 13 2022 11:51:25 GMT-0700 (PDT))
modules: debug:1.0 eth:1.0 net:1.0 rpc:1.0 web3:1.0
To exit, press ctrl-d
> eth.blockNumber
15135980
> eth.getBlockByNumber(15135980)
...
> eth.getTransaction("0xc16410b8245b404e20319b6825e846b7b0c985da8e62a1db7c66524877530194")
...
> debug.traceBlockByNumber(15135980, {"tracer": "callTracer"})
...
ChainNode Template API Generator (TAG) is a templated code generator capable of self-implementing ChainNode Proxy API methods.
- Fill in TAG Template (
internal/tag_generator/tag_template.txt
) such that it contains the method signature of all proxy methods that require implementation. - The code generator is automatically invoked via
make build
. If you would only like to call the TAG generator instead of the entire build process, runmake tag
- Code-generated implementation for proxy methods will be stored in internal/controller/ethereum/handler with all of the generated files being denoted with the
*tag_generated.go
suffix
- The code generated by TAG are based on a series of text/template files found in
internal/tag/code_templates
. Templates define the structure of the generated code while unique attributes of the structure are filled out ingenerator.go
- Example of Code Generation Template:
func (n {{.Namespace}}) {{.MethodName}}({{.ParametersAndTypes}}) (json.RawMessage, error) { return n.receiver.{{.MethodName}}({{.Parameters}}) }
- All elements of the template that are to be filled out by the generator have placeholder values:
{{.VariableName}}
- Fixed values including parentheses, newlines, tabs can be directly hard coded in the templates.
- All elements of the template that are to be filled out by the generator have placeholder values:
- Example of Using Code Generation Template in Generator:
- Code Generation Templates are filled out in the generator using Maps. Declare a map such that the keys are the placeholder values in the templates, values are the values to fill in the template
for i := range element { vars := make(map[string]interface{}) vars["Namespace"] = element[i].Namespace vars["MethodName"] = element[i].ApiName vars["ParametersAndTypes"] = parseParametersAndTypes(element[i].Parameter, element[i].ParameterType) vars["Parameters"] = parseParameters(element[i].Parameter) namespace = tag.ProcessTemplate(namespaceTmpl, vars) }
- In this example: we replace the Namespace, MethodName, ParametersAndTypes, Parameters placeholder values and substitute in the actual values to be used for code generation.
tag.ProcessTemplate
takes 2 parameters:string containing path to the template file
as well as amap containing the key value pairs to be injected into the template
. The function injects the actual values into the template file, returning a string containing the contents of the template with the injected values.
- IMPORTANT: following any changes to TAG's code templates, it is important that the unit test also be updated (procedure in the following section)
- TAG's unit test only needs to be updated when the format of TAG's code generation is changed (see section above).
- Unit test should be directly edited (
internal/utils/fixtures/tag/*
) such that the expected values now reflect the changes of the new generated code format. - Unit tests for TAG's code generation functions are stored in
internal/tag_generator/unit_test_templates/*
; responsible for testing functions used for TAG code generation process. - Unit tests for TAG's code generation output are stored in
internal/utils/fixtures/tag/*
; responsible for validating code generation output.
- The JSONRPC service batch request limit has a default limit defined per chain in the
base.yml
configuration file(note: it could also have overrides in different environments, please checkdevelopment.yml
andproduction.yml
as well). e.g. ethereum default limit is 1000