From 74e098d5b4dfbb6765f7b034b90f77d2eaa15fea Mon Sep 17 00:00:00 2001 From: Mihail Dobrev Date: Wed, 15 Jan 2025 20:48:59 +0200 Subject: [PATCH 1/5] fix: hyperchain sync (#2035) --------- Co-authored-by: Valentin Atanasov --- .github/actions/node-setup/action.yml | 2 +- .iex.exs | 2 + Dockerfile | 2 +- docker-compose-hc.yml | 50 ++ docs/swagger_v3/base.yaml | 7 + docs/swagger_v3/hyperchain.spec.yaml | 355 ++++++++++++++ hyperchain/accounts.json | 8 + hyperchain/aeternity.yaml | 78 +++ hyperchain/contracts.json | 60 +++ lib/ae_mdw/application.ex | 10 + lib/ae_mdw/collection.ex | 3 + lib/ae_mdw/contract.ex | 17 +- lib/ae_mdw/db/model.ex | 86 +++- lib/ae_mdw/db/mutations/epoch_mutation.ex | 76 +++ lib/ae_mdw/db/mutations/leader_mutation.ex | 89 ++++ lib/ae_mdw/db/mutations/stats_mutation.ex | 2 +- lib/ae_mdw/db/sync/block.ex | 25 + lib/ae_mdw/hyperchain.ex | 464 ++++++++++++++++++ lib/ae_mdw/node.ex | 25 +- lib/ae_mdw/node/db.ex | 60 ++- lib/ae_mdw/stats.ex | 52 +- lib/ae_mdw/sync/hyperchain.ex | 97 ++++ lib/ae_mdw/util/encoding.ex | 3 + .../controllers/hyperchain_controller.ex | 107 ++++ lib/ae_mdw_web/plugs/hyperchain_plug.ex | 24 + lib/ae_mdw_web/plugs/paginated_plug.ex | 5 +- lib/ae_mdw_web/router.ex | 17 + mix.exs | 3 + scripts/do.sh | 11 + test/ae_mdw/stats_test.exs | 4 +- .../controllers/name_controller_test.exs | 4 +- 31 files changed, 1715 insertions(+), 33 deletions(-) create mode 100644 docker-compose-hc.yml create mode 100644 docs/swagger_v3/hyperchain.spec.yaml create mode 100644 hyperchain/accounts.json create mode 100644 hyperchain/aeternity.yaml create mode 100644 hyperchain/contracts.json create mode 100644 lib/ae_mdw/db/mutations/epoch_mutation.ex create mode 100644 lib/ae_mdw/db/mutations/leader_mutation.ex create mode 100644 lib/ae_mdw/hyperchain.ex create mode 100644 lib/ae_mdw/sync/hyperchain.ex create mode 100644 lib/ae_mdw_web/controllers/hyperchain_controller.ex create mode 100644 lib/ae_mdw_web/plugs/hyperchain_plug.ex diff --git a/.github/actions/node-setup/action.yml b/.github/actions/node-setup/action.yml index ee031bb54..159294899 100644 --- a/.github/actions/node-setup/action.yml +++ b/.github/actions/node-setup/action.yml @@ -7,5 +7,5 @@ runs: - name: Setup Node shell: bash run: | - curl -L https://github.com/aeternity/aeternity/releases/download/v7.0.0/aeternity-v7.0.0-ubuntu-x86_64.tar.gz -o aeternity.tgz\ + curl -L https://github.com/aeternity/aeternity/releases/download/v7.3.0-rc3/aeternity-v7.3.0-rc3-ubuntu-x86_64.tar.gz -o aeternity.tgz\ && mkdir -p ${NODEROOT}/rel/aeternity && tar xf aeternity.tgz -C ${NODEROOT}/rel/aeternity && cp -rf ${NODEROOT}/rel/aeternity/lib/ ${NODEROOT} diff --git a/.iex.exs b/.iex.exs index 63b29fef2..b9c894061 100644 --- a/.iex.exs +++ b/.iex.exs @@ -10,6 +10,8 @@ alias AeMdw.Contract alias AeMdw.Database alias AeMdw.Validate alias AeMdw.Sync.AsyncTasks +alias AeMdw.Hyperchain +alias AeMdw.Db.RocksDbCF require Model require Ex2ms diff --git a/Dockerfile b/Dockerfile index 167e6c045..86e70eecf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ # ARG ELIXIR_VERSION=1.17.3 ARG OTP_VERSION=26.2.5.3 -ARG NODE_VERSION=7.2.0 +ARG NODE_VERSION=7.3.0-rc3 ARG DEBIAN_VERSION=bullseye-20240926-slim ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" diff --git a/docker-compose-hc.yml b/docker-compose-hc.yml new file mode 100644 index 000000000..c3021ebd2 --- /dev/null +++ b/docker-compose-hc.yml @@ -0,0 +1,50 @@ +services: + ae_mdw_hc: + build: + context: . + dockerfile: ./Dockerfile + args: + RUNNER_IMAGE: "hexpm/elixir:1.17.3-erlang-26.2.5.3-debian-bullseye-20240926-slim" + USER: root + MIX_ENV: dev + NODE_IMAGE: aeternity/aeternity:v7.3.0-rc1 + PATH_PREFIX: "/v3" + image: aeternity/ae_mdw_dev${IMAGE_NAME_SUFFIX:-}:latest + ports: + - "4000:4000" #MDW's default port + - "4001:4001" #MDW's websocket default port + - "3113:3113" #Node's default internal API port + - "3013:3013" #Node's default external API port + - "3014:3014" #Node's channels default websocket port + volumes: + - ${PWD}/data_hc/mnesia:/home/aeternity/node/local/rel/aeternity/data/mnesia + - ${PWD}/data_hc/mdw.db:/home/aeternity/node/local/rel/aeternity/data/mdw.db + - ${PWD}/hyperchain/aeternity.yaml:/home/aeternity/aeternity.yaml + - ${PWD}/docker/aeternity-dev.yaml:/home/aeternity/aeternity-dev.yaml + - ${PWD}/docker/accounts.json:/home/aeternity/node/local/rel/aeternity/data/aecore/.genesis/accounts_test.json + - ${PWD}/hyperchains/accounts.json:/home/aeternity/node/local/rel/aeternity/data/aecore/.ceres/hc_devnet_accounts.json + - ${PWD}/hyperchains/contracts.json:/home/aeternity/node/local/rel/aeternity/data/aecore/.ceres/hc_devnet_contracts.json + - ${PWD}/priv:/home/aeternity/node/ae_mdw/priv + - ${PWD}:/app + - ${PWD}/docker/gitconfig:/root/.gitconfig + environment: + - AETERNITY_CONFIG=${AETERNITY_CONFIG:-/home/aeternity/aeternity.yaml} + networks: + ae_mdw_net_hc: + aliases: + - mdw.aeternity.localhost + localnet_default: + node_sdk_hc: + image: node:20-alpine + working_dir: /app + volumes: + - ${PWD}/node_sdk:/app + entrypoint: "" + networks: + - ae_mdw_net_hc +networks: + ae_mdw_net_hc: + name: ae_mdw_net_hc + driver: bridge + localnet_default: + external: true diff --git a/docs/swagger_v3/base.yaml b/docs/swagger_v3/base.yaml index af3796ed0..321c7335e 100644 --- a/docs/swagger_v3/base.yaml +++ b/docs/swagger_v3/base.yaml @@ -26,6 +26,13 @@ components: schema: type: string pattern: '(gen):\d+(-\d+)?' + HyperchainScopeParam: + in: query + name: scope + description: 'Scopes results in a hyperchain epoch range' + schema: + type: string + pattern: '(epoch):\d+(-\d+)?' DirectionParam: in: query name: direction diff --git a/docs/swagger_v3/hyperchain.spec.yaml b/docs/swagger_v3/hyperchain.spec.yaml new file mode 100644 index 000000000..e6d391a3a --- /dev/null +++ b/docs/swagger_v3/hyperchain.spec.yaml @@ -0,0 +1,355 @@ +schemas: + Schedule: + description: Schedule information + type: object + properties: + height: + type: integer + example: 1 + leader: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + required: + - height + - leader + Validator: + description: Validator information + type: object + properties: + epoch: + type: integer + example: 1 + validator: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + total_stakes: + type: integer + example: 1000000000000000000 + delegates: + type: integer + example: 5 + rewards_earned: + type: integer + example: 1000000000000000000 + pinning_history: + type: object + additionalProperties: + type: integer + example: {"1": 1000000000000000000, "2": 2000000000000000000} + required: + - epoch + - validator + - total_stakes + - delegates + - rewards_earned + - pinning_history + Delegate: + description: Delegate information + type: object + properties: + epoch: + type: integer + example: 1 + delegate: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + stake: + type: integer + example: 1000000000000000000 + validator: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + required: + - epoch + - delegate + - stake + - validator + EpochInfo: + description: Epoch information + type: object + properties: + epoch: + type: integer + example: 1 + first: + type: integer + example: 1 + last: + type: integer + example: 10 + length: + type: integer + example: 10 + seed: + type: string + example: "kh_1111111111111111111111111111111111111111111111111" + last_pin_height: + type: integer + example: 10 + parent_block_hash: + type: string + example: "kh_1111111111111111111111111111111111111111111111111" + last_leader: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + epoch_start_time: + type: integer + example: 1629820800000 + validators: + type: array + items: + type: object + properties: + validator: + type: string + example: "ak_1111111111111111111111111111111111111111111111111" + stake: + type: integer + example: 1000000000000000000 + required: + - epoch + - first + - last + - length + - seed + - last_pin_height + - parent_block_hash + - last_leader + - epoch_start_time + - validators +paths: + /hyperchain/epochs: + get: + deprecated: false + description: Get Epoch information + operationId: GetEpochs + parameters: + - $ref: '#/components/parameters/DirectionParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/HyperchainScopeParam' + responses: + '200': + description: Returns paginated list of information about Epochs + content: + application/json: + schema: + allOf: + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/EpochInfo' + - $ref: '#/components/schemas/PaginatedResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/epochs/top: + get: + deprecated: false + description: Get Top Epoch information + operationId: GetEpochTop + responses: + '200': + description: Returns information about the top epoch + content: + application/json: + schema: + $ref: '#/components/schemas/EpochInfo' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/validators: + get: + deprecated: false + description: Get Validators information + operationId: GetValidators + parameters: + - $ref: '#/components/parameters/DirectionParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/HyperchainScopeParam' + responses: + '200': + description: Returns validator information + content: + application/json: + schema: + allOf: + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Validator' + - $ref: '#/components/schemas/PaginatedResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/validators/{validator}: + get: + deprecated: false + description: Get Validator information + operationId: GetValidator + parameters: + - name: validator + in: path + description: Validator address + required: true + schema: + $ref: '#/components/schemas/AccountAddress' + responses: + '200': + description: Returns validator information + content: + application/json: + schema: + $ref: '#/components/schemas/Validator' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/validators/{validator}/delegates: + get: + deprecated: false + description: Get Delegates information for validator + operationId: GetValidatorDelegates + parameters: + - name: validator + in: path + description: Validator address + required: true + schema: + $ref: '#/components/schemas/AccountAddress' + - $ref: '#/components/parameters/DirectionParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/HyperchainScopeParam' + responses: + '200': + description: Returns delegates information + content: + application/json: + schema: + allOf: + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Delegate' + - $ref: '#/components/schemas/PaginatedResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/validators/{validator}/delegates/top: + get: + deprecated: false + description: Get top Delegates information for validator + operationId: GetTopValidatorDelegates + parameters: + - name: validator + in: path + description: Validator address + required: true + schema: + $ref: '#/components/schemas/AccountAddress' + - $ref: '#/components/parameters/DirectionParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/HyperchainScopeParam' + responses: + '200': + description: Returns top validator delegates information + content: + application/json: + schema: + allOf: + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Delegate' + - $ref: '#/components/schemas/PaginatedResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/schedule: + get: + deprecated: false + description: Get Schedule information + operationId: GetSchedules + parameters: + - $ref: '#/components/parameters/DirectionParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/HyperchainScopeParam' + responses: + '200': + description: Returns schedule information + content: + application/json: + schema: + allOf: + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Schedule' + - $ref: '#/components/schemas/PaginatedResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /hyperchain/schedule/height/{height}: + get: + deprecated: false + description: Get Schedule information + operationId: GetSchedule + parameters: + - name: height + in: path + description: Schedule height + required: true + schema: + type: integer + responses: + '200': + description: Returns schedule information + content: + application/json: + schema: + $ref: '#/components/schemas/Schedule' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' diff --git a/hyperchain/accounts.json b/hyperchain/accounts.json new file mode 100644 index 000000000..9d472f4a5 --- /dev/null +++ b/hyperchain/accounts.json @@ -0,0 +1,8 @@ +{ + "ak_26z22vHaRx4wndWJY43JCnfY2x1RYWEa8F2MF6iPchQwJcWwqx": 1000000000000000000000000000000000000000000000000, + "ak_DnSMeui4uxreJA8NXPVkbJrhwjZ9oEp1y8hE7q4FrpqKEMGLa": 3100000000000000000000000000, + "ak_YuCypRSAkPkYkUhcbyNhob2D2iKshCEijLrNricUSkNeQ3XNk": 3100000000000000000000000000, + "ak_2bMCPWriTf7HcJ2Py7SWuqG9CM3Z3Mux5AnuM4PQqaZ457FfWW": 3100000000000000000000000000, + "ak_UeA2rsmQkUzhS81DrhvmSNrFSViNrUoE75vGRm3SvZ1bKRm38": 3100000000000000000000000000 +} + diff --git a/hyperchain/aeternity.yaml b/hyperchain/aeternity.yaml new file mode 100644 index 000000000..0fb929f39 --- /dev/null +++ b/hyperchain/aeternity.yaml @@ -0,0 +1,78 @@ +chain: + consensus: + '0': + config: + child_block_time: 3000 + child_epoch_length: 600 + contract_owner: 'ak_11111111111111111111111111111115rHyByZ' + default_pinning_behavior: true + election_contract: 'ct_LRbi65kmLtE7YMkG6mvG5TxAXTsPJDZjAtsPuaXtRyPA7gnfJ' + fixed_coinbase: 100 + parent_chain: + consensus: + network_id: 'devnet' + type: 'AE2AE' + parent_epoch_length: 10 + polling: + fetch_interval: 500 + nodes: + - 'http://localhost:13013' + start_height: 10 + pinners: + - parent_chain_account: + owner: 'ak_YuCypRSAkPkYkUhcbyNhob2D2iKshCEijLrNricUSkNeQ3XNk' + priv: '982656c160c428591e177f63bf5c8091497a480e1b137b9d985cc06b6495b1666055e85c1ece02a46a73864de9d21444fe852a3a26fd7a1f9164697064af03bc' + pub: 'ak_jRmFmxZDa6NPsEdPrVtKp9546jKJTmqYqAewc9ivn7abfrHjR' + - parent_chain_account: + owner: 'ak_2bMCPWriTf7HcJ2Py7SWuqG9CM3Z3Mux5AnuM4PQqaZ457FfWW' + priv: '3d59b9ce1be2a49f9512a3ef2e3209918ba96b9ac4f7e7df5529a1adf6904ea53cd203720974d8ff1afdd4425fa40a0cbc8c41c8ef35118f67c4bf42c9d54643' + pub: 'ak_TnaKwd3QPL6VFJsWqb2LMz16hWh3SA4J6jjjaWiPqG2wmSBnq' + - parent_chain_account: + owner: 'ak_UeA2rsmQkUzhS81DrhvmSNrFSViNrUoE75vGRm3SvZ1bKRm38' + priv: 'c1c1bc302828ca238933b5323e3caebe291294244bdd117ff442542f6618519f27973ccf20fd0e3aa2814154782bba4fdf568f548417c3c3b81d5fb077d20b94' + pub: 'ak_JSJ159FpcGb7UWD8mu3Sef3tP62MB7966wXPsXVRXNfXNiFCR' + pinning_reward_value: 1000 + rewards_contract: 'ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK' + stakers: + - hyper_chain_account: + priv: '492541e275cd28b011cba088f754adc067164115755ace8549e2ca1c82854466486ee0357316e9caca590456faf5fcf8eed50fa64ae7ac05348ef84d34c6eac1' + pub: 'ak_YuCypRSAkPkYkUhcbyNhob2D2iKshCEijLrNricUSkNeQ3XNk' + - hyper_chain_account: + priv: 'e0974f38b5b3314eb8e4bf081834a1637998b38cc59409c9b81416de50838781d1b01b2c2d1ab8b5402cf5a061d1491ec06fabc6c18b4fd6fcbd4e77acd6609e' + pub: 'ak_2bMCPWriTf7HcJ2Py7SWuqG9CM3Z3Mux5AnuM4PQqaZ457FfWW' + - hyper_chain_account: + priv: '25a3056fb11451d0d3b9b98bab97cb2714b61937e9797ea0f8157cd62e6116c03ec2eb1a4c86475e65c43eb05bb9570e1dda71aa4ea3993d0309cffc5f9006c5' + pub: 'ak_UeA2rsmQkUzhS81DrhvmSNrFSViNrUoE75vGRm3SvZ1bKRm38' + staking_contract: 'ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK' + type: 'hyperchain' + db_direct_access: true + hard_forks: + '6': 0 + persist: true + currency: + name: Hyper Aeternity + symbol: HAE + display: + network_name: Local Hyperchain + +fork_management: + network_id: 'hc_devnet' + +http: + endpoints: + hyperchain: true + dry-run: true + +peers: ['aenode://pp_2ETVSXALx7WwgWTstCHxEQ3QNetRCZJfacPCc331pNvcHw2ENL@localhost:23015'] +include_default_peers: false + +mining: + autostart: false + beneficiary: 'ak_2b8YNe9XoFDJtwaCAc6A4LLLj2sSUNqoJth66xRa87EQPGwxe7' + +logging: + level: warning + +metrics: + port: 0 + diff --git a/hyperchain/contracts.json b/hyperchain/contracts.json new file mode 100644 index 000000000..00a8240c0 --- /dev/null +++ b/hyperchain/contracts.json @@ -0,0 +1,60 @@ +{ + "contracts": [ + { + "abi_version": 3, + "vm_version": 8, + "amount": 0, + "nonce": 1, + "call_data": "cb_KxFE1kQfG2+K08IbzsztoP//wBN1Y+0=", + "code": "cb_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AgwBAAwDAIAICAMA/BGbORmmNwMHBxc3AAAMAgIMAggCAxFH86/lBgMI/unIn1AANwAHVQAEAxHcRUEh/ursXDUCNwI3BkcARwAHB2cHBxcHNwZHAEcABwdnBwcXKB4ABAAUGgIAAimcBAACAP7u4AmIAjcENwJ39+cAJ+cAJ+cAJyfnADMEBgcMDDUGAAY2BgIGDAIADAECKBwCACgcAAACAAcMCDQVBAIEGgkCABoJBgIGAwA0KAACDAEAAgMRidwMPzQUAgQ0AAA0FAIENDADAP78CA0dAjcCNwJ39yfnACfnADMEAgcMCAYDBAwBAgwBAAIDEYncDD8MAQAEAxGN7MvfAQMDuQZSLz8RAZy6tGEuTWFpblN0YWtpbmcubG9ja19zdGFrZV8RAjAQInUuTWFpblN0YWtpbmcuYXNzZXJ0X3ZhbGlkYXRvchEDvFByqS5NYWluU3Rha2luZy5sb2NrZWRfc3Rha2UuJWxhbWJkYS4xOTYuMjcuMBETd9gGOS5MaXN0LnJldmVyc2VfERox5GxVLk1haW5TdGFraW5nLmRlcG9zaXRfESa0rRA1Lkxpc3QucmV2ZXJzZREqFOeGfS5NYWluU3Rha2luZy5nZXRfc3Rha2VkX2Ftb3VudF8RLRDJvR1kZXBvc2l0ES9lBWRZZ2V0X2F2YWlsYWJsZV9iYWxhbmNlXxE3XMD0dS5hZGRfcmV3YXJkcy4lbGFtYmRhLjEwMS41My4xETiOBG9Fc29ydGVkX3ZhbGlkYXRvcnMRQQ/MnSF3aXRoZHJhdxFDDIOYhS5NYWluU3Rha2luZy5hc3NlcnRfcHJvdG9jb2xfY2FsbBFDD99qcS5hZGRfcmV3YXJkcy4lbGFtYmRhLjEwNC41LjMRRNZEHxFpbml0EUbkss0lLkxpc3QuYXNjEUfzr+VNLk1haW5TdGFraW5nLnN0YWtlXxFJ3oipWS5NYWluU3Rha2luZy53aXRoZHJhd18RTFyKc40uc29ydGVkX3ZhbGlkYXRvcnMuJWxhbWJkYS4xMTUuMTQuMBFO4fjyQS5NYWluU3Rha2luZy5tYXgRVCfYh20ubG9ja19zdGFrZS4lbGFtYmRhLjExMS41LjARVRDeKi1nZXRfcmVzdGFrZRFV+KUQRS5MaXN0Lm1lcmdlX3BhaXJzEVrbON4Vc3Rha2URW7JsNkVnZXRfc3Rha2VkX2Ftb3VudBFeMTkFUS5NYWluU3Rha2luZy5zdGFrZV92EV534SUtLkxpc3QuZm9sZGwRaHbW31kuTGlzdEludGVybmFsLmZsYXRfbWFwEW4abUk1bmV3X3ZhbGlkYXRvchFwTHmORWdldF9jdXJyZW50X2Vwb2NoEXi0ufZpLk1haW5TdGFraW5nLmFkanVzdF9zdGFrZV8Re7zT6WkuTWFpblN0YWtpbmcuY21wX3ZhbGlkYXRvchF9g3MJdS5NYWluU3Rha2luZy5sb29rdXBfdmFsaWRhdG9yEYlmdIU1c3Rha2luZ19wb3dlchGJ3Aw/US5MaXN0Lm1vbm90b25pY19zdWJzEYqfebSNLnNvcnRlZF92YWxpZGF0b3JzLiVsYW1iZGEuMTE4LjE1LjERjezL3z0uTGlzdC5tZXJnZV9hbGwRkEO0nWkuTWFpblN0YWtpbmcudW5sb2NrX3N0YWtlXxGRL7hgWWdldF92YWxpZGF0b3JfY29udHJhY3QRk7CCx4UuTWFpblN0YWtpbmcubWF4LiVsYW1iZGEuMjAwLjIzLjARm//zBi1zZXRfcmVzdGFrZRGd3U6yXS5NYWluU3Rha2luZy53aXRoZHJhd192EZ8XBeVlLk1haW5TdGFraW5nLmxvY2tlZF9zdGFrZRGf7kAjMWFkanVzdF9zdGFrZRGgAot4JS5MaXN0Lm1hcBGgnx6AJS5QYWlyLnNuZBG0Wb99KWxvY2tfc3Rha2URuDGpK2UuTWFpblN0YWtpbmcuYXNzZXJ0X293bmVyEbye+RN1LmFkZF9yZXdhcmRzLiVsYW1iZGEuMTAxLjM3LjARwlJ+J2kuTWFpblN0YWtpbmcuYXNzZXJ0X3NpZ25lchHD8i88NS5MaXN0LmZvcmVhY2gRz63GBm0uTWFpblN0YWtpbmcuYWRqdXN0X3N0YWtlX3YR1ElUMlVnZXRfYXZhaWxhYmxlX2JhbGFuY2UR2aap3i1hZGRfcmV3YXJkcxHZqfDJdS5hZGRfcmV3YXJkcy4lbGFtYmRhLjEwMy4zMS4yEdnn19VNZ2V0X3ZhbGlkYXRvcl9zdGF0ZRHb+BxsLS5MaXN0Lm1lcmdlEdxFQSFJZ2V0X3RvdGFsX2JhbGFuY2VfEemKM11dLk1haW5TdGFraW5nLmFkZF9yZXdhcmQR6cifUEVnZXRfdG90YWxfYmFsYW5jZRHq7Fw1WS5NYWluU3Rha2luZy5kZXBvc2l0X3YR7uAJiCkuTGlzdC5kZXNjEfwIDR0pLkxpc3Quc29ydIIvAIU4LjAuMABL0+L9", + "owner_pubkey": "ak_11111111111111111111111111111115rHyByZ", + "pubkey": "ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK" + }, + { + "abi_version": 3, + "vm_version": 8, + "amount": 0, + "nonce": 2, + "call_data": "cb_KxFE1kQfG58CoCmQRGuzLDSzySG/5UcFOdoQ1iYy6a6fhJXVahjufrESSdmdMQ==", + "code": "cb_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XxFshEuekS52YWxpZGF0b3Jfc2NoZWR1bGUuJWxhbWJkYS4xMzQuNTEuMhF89ZduMWVwb2NoX2xlbmd0aBGFbj1JQWVwb2NoX2luZm9fZXBvY2gRj81wtF0uSENFbGVjdGlvbi5wYXlfcmV3YXJkcxGeKv15kS52YWxpZGF0b3Jfc2NoZWR1bGUuJWxhbWJkYS4xMzIuMzUuMBGgAot4JS5MaXN0Lm1hcBGgnx6AJS5QYWlyLnNuZBGkPMxUGWxlYWRlchGzWtUSFWVwb2NoEbl/R3dpLkhDRWxlY3Rpb24ucGlja192YWxpZGF0b3IRx+q8gg1waW4RyfUKmaUuSENFbGVjdGlvbi5wYXlfcmV3YXJkc18uJWxhbWJkYS4xODAuNDEuMBHUp/e0bS5IQ0VsZWN0aW9uLmNhbGNfY2Fycnlfb3ZlchHY3HD3PXBpbl9yZXdhcmRfaW5mbxHZMbf5kS52YWxpZGF0b3Jfc2NoZWR1bGUuJWxhbWJkYS4xMzIuNTEuMRHn0dDAgS5IQ0VsZWN0aW9uLmFzc2VydF9wcm90b2NvbF9jYWxsEe3ZIWVBc3Rha2luZ19jb250cmFjdBHxjvhrSXZhbGlkYXRvcl9zY2hlZHVsZRH9g6lHKWVwb2NoX2luZm+CLwCFOC4wLjAAvgFz1A==", + "owner_pubkey": "ak_11111111111111111111111111111115rHyByZ", + "pubkey": "ct_LRbi65kmLtE7YMkG6mvG5TxAXTsPJDZjAtsPuaXtRyPA7gnfJ" + } + ], + "calls": [ + { + "abi_version": 3, + "amount": 1000000000000000000000000, + "call_data": "cb_KxFuGm1JO58AoEhu4DVzFunKylkEVvr1/Pju1Q+mSuesBTSO+E00xurBnwCgSG7gNXMW6crKWQRW+vX8+O7VD6ZK56wFNI74TTTG6sF/EziZWA==", + "caller": "ak_YuCypRSAkPkYkUhcbyNhob2D2iKshCEijLrNricUSkNeQ3XNk", + "contract_pubkey": "ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK", + "nonce": 1, + "fee": 1000000000000000, + "gas": 1000000, + "gas_price": 1000000000 + }, + { + "abi_version": 3, + "amount": 1000000000000000000000000, + "call_data": "cb_KxFuGm1JO58AoNGwGywtGri1QCz1oGHRSR7Ab6vGwYtP1vy9Tnes1mCenwCg0bAbLC0auLVALPWgYdFJHsBvq8bBi0/W/L1Od6zWYJ5/SKiGfw==", + "caller": "ak_2bMCPWriTf7HcJ2Py7SWuqG9CM3Z3Mux5AnuM4PQqaZ457FfWW", + "contract_pubkey": "ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK", + "nonce": 1, + "fee": 1000000000000000, + "gas": 1000000, + "gas_price": 1000000000 + }, + { + "abi_version": 3, + "amount": 1000000000000000000000000, + "call_data": "cb_KxFuGm1JO58AoD7C6xpMhkdeZcQ+sFu5Vw4d2nGqTqOZPQMJz/xfkAbFnwCgPsLrGkyGR15lxD6wW7lXDh3acapOo5k9AwnP/F+QBsV/hBf1Ig==", + "caller": "ak_UeA2rsmQkUzhS81DrhvmSNrFSViNrUoE75vGRm3SvZ1bKRm38", + "contract_pubkey": "ct_KJgjAXMtRF68AbT5A2aC9fTk8PA4WFv26cFSY27fXs6FtYQHK", + "nonce": 1, + "fee": 1000000000000000, + "gas": 1000000, + "gas_price": 1000000000 + } + ] +} + diff --git a/lib/ae_mdw/application.ex b/lib/ae_mdw/application.ex index 540cc4c7d..ad9a20e84 100644 --- a/lib/ae_mdw/application.ex +++ b/lib/ae_mdw/application.ex @@ -14,6 +14,7 @@ defmodule AeMdw.Application do alias AeMdw.Db.RocksDb alias AeMdw.Db.Sync.ObjectKeys alias AeMdw.EtsCache + alias AeMdw.Sync.Hyperchain alias AeMdw.Sync.Watcher alias AeMdwWeb.Websocket.BroadcasterCache alias AeMdw.Sync.MutationsCache @@ -21,11 +22,13 @@ defmodule AeMdw.Application do alias AeMdw.Sync.SyncingQueue require Model + require Logger use Application @impl Application def start(_type, _args) do + hyperchain_checks() build_rev = Application.fetch_env!(:ae_mdw, :build_revision) :persistent_term.put({:ae_mdw, :build_revision}, build_rev) @@ -142,4 +145,11 @@ defmodule AeMdw.Application do AeMdwWeb.Endpoint.config_change(changed, removed) :ok end + + defp hyperchain_checks() do + if Hyperchain.hyperchain?() and not Hyperchain.connected_to_parent?() do + Logger.error("Hyperchain is enabled but not connected to parent chain") + raise "Hyperchain is enabled but not connected to parent chain" + end + end end diff --git a/lib/ae_mdw/collection.ex b/lib/ae_mdw/collection.ex index 1b8dce461..5d973a266 100644 --- a/lib/ae_mdw/collection.ex +++ b/lib/ae_mdw/collection.ex @@ -19,9 +19,12 @@ defmodule AeMdw.Collection do @type is_reversed?() :: boolean() @type has_cursor?() :: boolean() @type direction_limit() :: {direction(), is_reversed?(), limit(), has_cursor?()} + @type pagination() :: direction_limit() @type pagination_cursor() :: {binary(), is_reversed?()} | nil @type stream_fn() :: (direction() -> Enumerable.t()) + @type range() :: {:gen, Range.t()} | {:txi, Range.t()} | {:epoch, Range.t()} | nil + @doc """ Paginates a list or stream or records into a list of items and it's next cursor (if any). diff --git a/lib/ae_mdw/contract.ex b/lib/ae_mdw/contract.ex index f6fb12014..f2f28ce76 100644 --- a/lib/ae_mdw/contract.ex +++ b/lib/ae_mdw/contract.ex @@ -3,6 +3,7 @@ defmodule AeMdw.Contract do AE smart contracts type (signatures) and previous calls information based on direct chain info. """ + alias AeMdw.Sync.Hyperchain alias AeMdw.Blocks alias AeMdw.Db.Name alias AeMdw.EtsCache @@ -452,17 +453,29 @@ defmodule AeMdw.Contract do if txs_taken != [] do header = :aec_blocks.to_header(micro_block) + height = :aec_headers.height(header) consensus = :aec_headers.consensus_module(header) node = :aec_chain_state.wrap_block(micro_block) time = :aec_block_insertion.node_time(node) prev_hash = :aec_block_insertion.node_prev_hash(node) - prev_key_hash = :aec_block_insertion.node_prev_key_hash(node) + + prev_key_hash = + if Hyperchain.hyperchain?() do + [{hash, _key_block_header}] = + :aec_db.find_key_headers_and_hash_at_height(height) + + hash + else + :aec_block_insertion.node_prev_key_hash(node) + end + {:value, prev_key_header} = :aec_db.find_header(prev_key_hash) {:value, trees_in, _difficulty, _fork_id, _fees, _fraud} = :aec_db.find_block_state_and_data(prev_hash, true) - trees_in = consensus.state_pre_transform_micro_node(node, trees_in) + trees_in = Node.state_pre_transform_micro_node(consensus, height, node, trees_in) + env = :aetx_env.tx_env_from_key_header(prev_key_header, prev_key_hash, time, prev_hash) {:ok, _sigs, _trees, events} = diff --git a/lib/ae_mdw/db/model.ex b/lib/ae_mdw/db/model.ex index d832c1605..02e3601b5 100644 --- a/lib/ae_mdw/db/model.ex +++ b/lib/ae_mdw/db/model.ex @@ -7,6 +7,7 @@ defmodule AeMdw.Db.Model do alias AeMdw.Contracts alias AeMdw.Db.Contract, as: DbContract alias AeMdw.Db.IntTransfer + alias AeMdw.Sync.Hyperchain alias AeMdw.Names alias AeMdw.Node alias AeMdw.Node.Db @@ -1311,6 +1312,69 @@ defmodule AeMdw.Db.Model do tx: mempool_tx() ) + @hyperchain_leader_at_height_defaults [index: 0, leader: <<>>] + defrecord :hyperchain_leader_at_height, @hyperchain_leader_at_height_defaults + + @type hyperchain_leader_at_height_index() :: height() + @type hyperchain_leader_at_height() :: + record(:hyperchain_leader_at_height, + index: hyperchain_leader_at_height_index(), + leader: pubkey() + ) + + @epoch_info_defaults [index: 0, first: 0, last: 0, length: 0, seed: <<>>] + defrecord :epoch_info, @epoch_info_defaults + + @type epoch_info_index() :: Hyperchain.epoch() + @type epoch_info() :: + record(:epoch_info, + index: epoch_info_index(), + first: Blocks.height(), + last: Blocks.height(), + length: non_neg_integer(), + seed: binary() | :undefined + ) + + @validator_defaults [index: {<<>>, 0}, stake: 0] + defrecord :validator, @validator_defaults + + @type validator_index() :: {pubkey(), Hyperchain.epoch()} + @type validator() :: record(:validator, index: validator_index(), stake: non_neg_integer()) + + @rev_validator_defaults [index: {0, <<>>}] + defrecord :rev_validator, @rev_validator_defaults + + @type rev_validator_index() :: {Hyperchain.epoch(), pubkey()} + @type rev_validator() :: record(:rev_validator, index: rev_validator_index()) + + @pin_info_defaults [index: 0, leader: <<>>, reward: 0] + defrecord :pin_info, @pin_info_defaults + + @type pin_info_index() :: Hyperchain.epoch() + @type pin_info() :: + record(:pin_info, + index: pin_info_index(), + leader: pubkey(), + reward: amount() + ) + + @leader_pin_info_defaults [index: {<<>>, 0}, reward: 0] + defrecord :leader_pin_info, @leader_pin_info_defaults + + @type leader_pin_info_index() :: {pubkey(), Hyperchain.epoch()} + @type leader_pin_info() :: + record(:leader_pin_info, + index: leader_pin_info_index(), + reward: amount() + ) + + @delegate_defaults [index: {<<>>, 0, <<>>}, stake: 0] + defrecord :delegate, @delegate_defaults + + # index: {validator_pk, epoch, delegate_pk} + @type delegate_index() :: {pubkey(), Blocks.height(), pubkey()} + @type delegate() :: record(:delegate, index: delegate_index(), stake: non_neg_integer()) + ################################################################################ # starts with only chain_tables and add them progressively by groups @@ -1323,7 +1387,8 @@ defmodule AeMdw.Db.Model do name_tables(), oracle_tables(), stat_tables(), - tasks_tables() + tasks_tables(), + hyperchain_tables() ]) end @@ -1466,6 +1531,18 @@ defmodule AeMdw.Db.Model do ] end + defp hyperchain_tables() do + [ + AeMdw.Db.Model.HyperchainLeaderAtHeight, + AeMdw.Db.Model.EpochInfo, + AeMdw.Db.Model.Validator, + AeMdw.Db.Model.RevValidator, + AeMdw.Db.Model.PinInfo, + AeMdw.Db.Model.LeaderPinInfo, + AeMdw.Db.Model.Delegate + ] + end + @spec record(atom()) :: atom() def record(AeMdw.Db.Model.BalanceAccount), do: :balance_account def record(AeMdw.Db.Model.AccountBalance), do: :account_balance @@ -1572,4 +1649,11 @@ defmodule AeMdw.Db.Model do def record(AeMdw.Db.Model.Mempool), do: :mempool def record(AeMdw.Db.Model.DexPair), do: :dex_pair def record(AeMdw.Db.Model.DexTokenSymbol), do: :dex_token_symbol + def record(AeMdw.Db.Model.HyperchainLeaderAtHeight), do: :hyperchain_leader_at_height + def record(AeMdw.Db.Model.EpochInfo), do: :epoch_info + def record(AeMdw.Db.Model.Validator), do: :validator + def record(AeMdw.Db.Model.RevValidator), do: :rev_validator + def record(AeMdw.Db.Model.PinInfo), do: :pin_info + def record(AeMdw.Db.Model.LeaderPinInfo), do: :leader_pin_info + def record(AeMdw.Db.Model.Delegate), do: :delegate end diff --git a/lib/ae_mdw/db/mutations/epoch_mutation.ex b/lib/ae_mdw/db/mutations/epoch_mutation.ex new file mode 100644 index 000000000..8eb4ffd05 --- /dev/null +++ b/lib/ae_mdw/db/mutations/epoch_mutation.ex @@ -0,0 +1,76 @@ +defmodule AeMdw.Db.EpochMutation do + @moduledoc """ + Possibly put the new epochs for a hyperchain + """ + + alias AeMdw.Blocks + alias AeMdw.Db.Model + alias AeMdw.Db.State + alias AeMdw.Sync.Hyperchain + + require Model + + @derive AeMdw.Db.Mutation + defstruct [:height] + + @opaque t() :: %__MODULE__{ + height: Blocks.height() + } + + @spec new(Blocks.height()) :: t() + def new(height) do + %__MODULE__{ + height: height + } + end + + @spec execute(t(), State.t()) :: State.t() + def execute(%__MODULE__{height: height}, state) do + if new_epoch?(state, height) do + put_new_epoch_info(state, height) + else + state + end + end + + defp new_epoch?(state, height) do + not State.exists?(state, Model.EpochInfo, height) + end + + defp put_new_epoch_info(state, height) do + {:ok, + %{ + first: first, + last: last, + length: length, + seed: seed, + epoch: epoch, + validators: validators + }} = Hyperchain.epoch_info_at_height(height) + + state = + State.put( + state, + Model.EpochInfo, + Model.epoch_info( + index: epoch, + first: first, + last: last, + length: length, + seed: seed + ) + ) + + Enum.reduce(validators, state, fn {pubkey, stake}, state -> + state + |> State.put( + Model.Validator, + Model.validator(index: {pubkey, epoch}, stake: stake) + ) + |> State.put( + Model.RevValidator, + Model.rev_validator(index: {epoch, pubkey}) + ) + end) + end +end diff --git a/lib/ae_mdw/db/mutations/leader_mutation.ex b/lib/ae_mdw/db/mutations/leader_mutation.ex new file mode 100644 index 000000000..945ff79c1 --- /dev/null +++ b/lib/ae_mdw/db/mutations/leader_mutation.ex @@ -0,0 +1,89 @@ +defmodule AeMdw.Db.LeaderMutation do + @moduledoc """ + Possibly put the new leaders for a hyperchain + """ + + alias AeMdw.Blocks + alias AeMdw.Db.Model + alias AeMdw.Db.State + alias AeMdw.Node + alias AeMdw.Sync.Hyperchain + + require Model + require Logger + + @derive AeMdw.Db.Mutation + defstruct [:height] + + @opaque t() :: %__MODULE__{ + height: Blocks.height() + } + + @spec new(Blocks.height()) :: t() + def new(height) do + %__MODULE__{ + height: height + } + end + + @spec execute(t(), State.t()) :: State.t() + def execute(%__MODULE__{height: height}, state) do + if new_epoch?(state, height) do + put_new_leaders(state, height) + else + state + end + end + + @spec put_delegates(State.t(), Node.height(), Node.epoch(), Hyperchain.leader()) :: State.t() + def put_delegates(state, start_height, epoch, leader) do + start_height + |> Hyperchain.get_delegates(leader) + |> case do + {:ok, delegates} -> + Enum.reduce(delegates, state, fn {delegate, stake}, acc_state -> + State.put( + acc_state, + Model.Delegate, + Model.delegate(index: {leader, epoch, delegate}, stake: stake) + ) + end) + + :error -> + Logger.error( + "Error fetching delegates for leader #{inspect(leader)} at height #{start_height} in epoch #{epoch}" + ) + + state + end + end + + defp new_epoch?(state, height) do + not State.exists?(state, Model.HyperchainLeaderAtHeight, height) + end + + defp put_new_leaders(state, height) do + height + |> Hyperchain.epoch_info_at_height() + |> case do + {:ok, %{epoch: epoch, first: _start_height}} -> + height + |> Hyperchain.leaders_for_epoch_at_height() + |> Enum.reduce(state, fn {leader, height}, state -> + state + |> State.put( + Model.HyperchainLeaderAtHeight, + Model.hyperchain_leader_at_height(index: height, leader: leader) + ) + |> State.put( + Model.Validator, + Model.validator(index: {leader, epoch}) + ) + |> State.put( + Model.RevValidator, + Model.rev_validator(index: {epoch, leader}) + ) + end) + end + end +end diff --git a/lib/ae_mdw/db/mutations/stats_mutation.ex b/lib/ae_mdw/db/mutations/stats_mutation.ex index 42e39b6b4..464dfcbb1 100644 --- a/lib/ae_mdw/db/mutations/stats_mutation.ex +++ b/lib/ae_mdw/db/mutations/stats_mutation.ex @@ -230,7 +230,7 @@ defmodule AeMdw.Db.StatsMutation do defp get(state, stat_sync_key, default), do: State.get_stat(state, stat_sync_key, default) - defp fetch_total_stat(_state, 0) do + defp fetch_total_stat(_state, height) when height == 0 or height == 1 do Model.total_stat() end diff --git a/lib/ae_mdw/db/sync/block.ex b/lib/ae_mdw/db/sync/block.ex index ea6433fdf..30df2087f 100644 --- a/lib/ae_mdw/db/sync/block.ex +++ b/lib/ae_mdw/db/sync/block.ex @@ -15,11 +15,14 @@ defmodule AeMdw.Db.Sync.Block do 3.3. Get the mutations from each micro-block and execute them. """ + alias AeMdw.Db.UpdateBalanceAccountMutation alias AeMdw.Blocks alias AeMdw.Db.BlockStatisticsMutation + alias AeMdw.Db.EpochMutation alias AeMdw.Db.Model alias AeMdw.Db.IntTransfer alias AeMdw.Db.KeyBlockMutation + alias AeMdw.Db.LeaderMutation alias AeMdw.Db.NamesExpirationMutation alias AeMdw.Db.OraclesExpirationMutation alias AeMdw.Db.State @@ -28,6 +31,7 @@ defmodule AeMdw.Db.Sync.Block do alias AeMdw.Db.WriteMutation alias AeMdw.Db.Mutation alias AeMdw.Db.TypeCountersMutation + alias AeMdw.Sync.Hyperchain alias AeMdw.Sync.MutationsCache alias AeMdw.Log alias AeMdw.Node, as: AE @@ -93,6 +97,14 @@ defmodule AeMdw.Db.Sync.Block do WriteMutation.new(Model.Block, key_block) end + genesis_accounts_balances_mutation = + if height == 0 do + :aec_fork_block_settings.genesis_accounts() + |> Enum.map(fn {pk, amount} -> + UpdateBalanceAccountMutation.new(pk, amount) + end) + end + block_rewards_mutation = if height >= AE.min_block_reward_height() and :aec_consensus_hc != :aec_consensus.get_genesis_consensus_module() do @@ -102,6 +114,7 @@ defmodule AeMdw.Db.Sync.Block do gen_mutations = [ kb0_mutation, + genesis_accounts_balances_mutation, block_rewards_mutation, NamesExpirationMutation.new(height), OraclesExpirationMutation.new(height), @@ -114,6 +127,10 @@ defmodule AeMdw.Db.Sync.Block do starting_from_mb0? ), next_kb_mutation + | hyperchain_mutations([ + EpochMutation.new(height), + LeaderMutation.new(height) + ]) ] |> Enum.reject(&is_nil/1) @@ -123,6 +140,14 @@ defmodule AeMdw.Db.Sync.Block do end) end + defp hyperchain_mutations(mutations) do + if Hyperchain.hyperchain?() do + mutations + else + [] + end + end + defp micro_block_mutations(mblock, mbi, first_txi, false = _use_cache?) do {:ok, mb_hash} = :aec_headers.hash_header(:aec_blocks.to_micro_header(mblock)) diff --git a/lib/ae_mdw/hyperchain.ex b/lib/ae_mdw/hyperchain.ex new file mode 100644 index 000000000..5a1f9cbed --- /dev/null +++ b/lib/ae_mdw/hyperchain.ex @@ -0,0 +1,464 @@ +defmodule AeMdw.Hyperchain do + @moduledoc """ + Module for hyperchain related functions. + """ + alias AeMdw.Node + alias AeMdw.Node.Db + alias AeMdw.Blocks + alias AeMdw.Collection + alias AeMdw.Db.Model + alias AeMdw.Db.State + alias AeMdw.Error + alias AeMdw.Error.Input, as: ErrInput + alias AeMdw.Util.Encoding + alias AeMdw.Sync.Hyperchain + + require Model + + @type validator() :: %{ + total_stakes: non_neg_integer(), + delegates: non_neg_integer(), + rewards_earned: non_neg_integer(), + pinning_history: %{Blocks.height() => non_neg_integer()}, + validator: Db.pubkey(), + epoch: Blocks.height() + } + + @spec fetch_epochs( + State.t(), + Collection.pagination(), + Collection.range(), + Collection.cursor() + ) :: + {:ok, {Collection.cursor(), [Hyperchain.leader()], Collection.cursor()}} + def fetch_epochs(state, pagination, scope, cursor) do + with {:ok, scope} <- deserialize_epoch_scope(scope) do + cursor = deserialize_numeric_cursor(cursor) + + fn direction -> + Collection.stream(state, Model.EpochInfo, direction, scope, cursor) + end + |> Collection.paginate( + pagination, + &render_epoch_info(state, &1), + &serialize_numeric_cursor/1 + ) + |> then(&{:ok, &1}) + end + end + + @spec fetch_epoch_top(State.t()) :: {:ok, map()} | {:error, Error.t()} + def fetch_epoch_top(state) do + current_height = State.height(state) + + current_height + |> Hyperchain.epoch_info_at_height() + |> case do + {:ok, %{epoch: epoch}} -> + {:ok, render_epoch_info(state, epoch)} + + error when error in [:not_found, :error] -> + {:error, ErrInput.NotFound.exception(value: "epoch at height #{current_height}")} + end + end + + @spec fetch_leaders_schedule( + State.t(), + Collection.pagination(), + Collection.range(), + Collection.cursor() + ) :: + {:ok, {Collection.cursor(), [Hyperchain.leader()], Collection.cursor()}} + def fetch_leaders_schedule(state, pagination, scope, cursor) do + with {:ok, scope} <- deserialize_leaders_scope(scope) do + cursor = deserialize_numeric_cursor(cursor) + + fn direction -> + Collection.stream(state, Model.HyperchainLeaderAtHeight, direction, scope, cursor) + end + |> Collection.paginate( + pagination, + &render_leader(state, &1), + &serialize_numeric_cursor/1 + ) + |> then(&{:ok, &1}) + end + end + + @spec fetch_leaders_schedule_at_height(State.t(), Blocks.height()) :: Hyperchain.leader() + def fetch_leaders_schedule_at_height(state, height) do + case State.get(state, Model.HyperchainLeaderAtHeight, height) do + {:ok, Model.hyperchain_leader_at_height(index: ^height) = leader} -> + {:ok, render_leader(state, leader)} + + :not_found -> + {:error, ErrInput.NotFound.exception(value: "height #{height}")} + end + end + + @spec fetch_validators( + State.t(), + Collection.pagination(), + Collection.range(), + Collection.cursor() + ) :: + {:ok, {Collection.cursor(), [validator()], Collection.cursor()}} | {:error, term()} + def fetch_validators(state, pagination, scope, cursor) do + with {:ok, scope} <- deserialize_validator_scope(scope) do + cursor = deserialize_validator_cursor(cursor) + + fn direction -> + Collection.stream(state, Model.RevValidator, direction, scope, cursor) + end + |> Collection.paginate( + pagination, + &render_validator(state, &1), + &serialize_validator_cursor/1 + ) + |> then(&{:ok, &1}) + end + end + + @spec fetch_validators_top( + State.t(), + Collection.pagination(), + Collection.cursor() + ) :: + {:ok, {Collection.cursor(), [validator()], Collection.cursor()}} | {:error, term()} + def fetch_validators_top(state, pagination, cursor) do + with current_height <- State.height(state), + {:ok, %{epoch: epoch}} <- Hyperchain.epoch_info_at_height(current_height) do + scope = {:epoch, epoch..epoch} + fetch_validators(state, pagination, scope, cursor) + end + end + + @spec fetch_validator(State.t(), term()) :: {:ok, validator()} + def fetch_validator(state, validator_id) do + with {:ok, pubkey} <- Encoding.safe_decode(:account_pubkey, validator_id), + current_height <- State.height(state), + {:ok, %{epoch: epoch}} <- Hyperchain.epoch_info_at_height(current_height), + {:ok, validator} <- State.get(state, Model.Validator, {pubkey, epoch}) do + {:ok, render_validator(state, validator)} + end + end + + @spec fetch_delegates( + State.t(), + Db.pubkey(), + Collection.pagination(), + Collection.range(), + Collection.cursor() + ) :: {:ok, {Collection.cursor(), [Db.pubkey()], Collection.cursor()}} + def fetch_delegates(state, validator_id, pagination, scope, cursor) do + with {:ok, pubkey} <- Encoding.safe_decode(:account_pubkey, validator_id), + {:ok, scope} <- deserialize_validator_delegates_scope(scope) do + cursor = + deserialize_validator_delegates_cursor(cursor) + + scope = + case scope do + nil -> + Collection.generate_key_boundary({pubkey, Collection.integer(), Collection.binary()}) + + {first_epoch, last_epoch} -> + Collection.generate_key_boundary( + {pubkey, Collection.gen_range(first_epoch, last_epoch), Collection.binary()} + ) + end + + fn direction -> + Collection.stream( + state, + Model.Delegate, + direction, + scope, + cursor + ) + end + |> Collection.paginate( + pagination, + &render_validator_delegate(state, &1), + &serialize_validator_delegates_cursor/1 + ) + |> then(&{:ok, &1}) + end + end + + @spec fetch_delegates_top( + State.t(), + Db.pubkey(), + Collection.pagination(), + Collection.cursor() + ) :: {:ok, {Collection.cursor(), [Db.pubkey()], Collection.cursor()}} + + def fetch_delegates_top(state, validator_id, pagination, cursor) do + with current_height <- State.height(state), + {:ok, %{epoch: epoch}} <- Hyperchain.epoch_info_at_height(current_height) do + scope = {:epoch, epoch..epoch} + fetch_delegates(state, validator_id, pagination, scope, cursor) + end + end + + defp render_validator( + state, + {epoch, pubkey} + ) do + validator = State.fetch!(state, Model.Validator, {pubkey, epoch}) + render_validator(state, validator) + end + + defp render_validator( + state, + Model.validator(index: {pubkey, epoch}, stake: stake) + ) do + total_rewards = + case State.get(state, Model.Miner, pubkey) do + {:ok, Model.miner(total_reward: total_reward)} -> + total_reward + + :not_found -> + 0 + end + + pinning_history = + state + |> Collection.stream( + Model.LeaderPinInfo, + Collection.generate_key_boundary({pubkey, Collection.integer()}) + ) + |> Enum.into(%{}, fn key -> + Model.leader_pin_info(index: {^pubkey, epoch}, reward: reward) = + State.fetch!(state, Model.LeaderPinInfo, key) + + {epoch, reward} + end) + + %{ + total_stakes: stake, + delegates: get_delegates(state, epoch, pubkey), + rewards_earned: total_rewards, + pinning_history: pinning_history, + validator: Encoding.encode_account(pubkey), + epoch: epoch + } + end + + defp get_delegates(state, epoch, pubkey) do + state + |> Collection.stream( + Model.Delegate, + :backward, + Collection.generate_key_boundary({pubkey, epoch, Collection.binary()}), + nil + ) + |> Enum.count() + end + + defp render_leader(state, leader_height) when is_integer(leader_height) do + state + |> State.fetch!(Model.HyperchainLeaderAtHeight, leader_height) + |> then(&render_leader(state, &1)) + end + + defp render_leader(_state, Model.hyperchain_leader_at_height(index: height, leader: leader)) do + %{height: height, leader: Encoding.encode_account(leader)} + end + + defp render_epoch_info(state, epoch_index) when is_integer(epoch_index) do + epoch = + Model.epoch_info(index: ^epoch_index) = State.fetch!(state, Model.EpochInfo, epoch_index) + + render_epoch_info(state, epoch) + end + + defp render_epoch_info( + state, + Model.epoch_info(index: epoch, first: first, last: last, length: length, seed: seed) + ) do + last_pin_height = first - 1 + + {:ok, last_block} = + :aec_chain.get_key_block_by_height(last_pin_height) + + epoch_start_time = :aec_blocks.time_in_msecs(last_block) + + last_leader_height = + state + |> State.height() + |> case do + top when top > last -> + last + + top when top < first -> + last + + top -> + top + end + + Model.hyperchain_leader_at_height(leader: last_leader) = + State.fetch!(state, Model.HyperchainLeaderAtHeight, last_leader_height) + + validators = + state + |> Collection.stream( + Model.RevValidator, + :backward, + Collection.generate_key_boundary({epoch, Collection.binary()}), + nil + ) + |> Enum.map(fn {^epoch, pubkey} -> + Model.validator(stake: stake) = + State.fetch!(state, Model.Validator, {pubkey, epoch}) + + %{validator: Encoding.encode_account(pubkey), stake: stake} + end) + + %{ + epoch: epoch, + first: first, + last: last, + length: length, + seed: seed, + last_pin_height: last_pin_height, + last_leader: Encoding.encode_account(last_leader), + epoch_start_time: epoch_start_time, + validators: validators + } + end + + defp render_validator_delegate(state, {leader, epoch, delegate}) do + Model.delegate(index: {^leader, ^epoch, ^delegate}, stake: stake) = + State.fetch!(state, Model.Delegate, {leader, epoch, delegate}) + + %{ + delegate: Encoding.encode_account(delegate), + stake: stake, + epoch: epoch, + validator: Encoding.encode_account(leader) + } + end + + defp deserialize_leaders_scope(scope) do + case scope do + nil -> + {:ok, nil} + + {:gen, first_gen..last_gen//_step} -> + {:ok, {first_gen, last_gen}} + + {:epoch, first_epoch..last_epoch//_step} -> + with {:ok, epoch_length} <- Node.epoch_length(last_epoch), + {:ok, first_gen} <- Node.epoch_start_height(first_epoch), + {:ok, last_gen} <- Node.epoch_start_height(last_epoch) do + {:ok, {first_gen, last_gen + epoch_length - 1}} + else + {:error, error} -> + {:error, ErrInput.Scope.exception(value: error)} + end + end + end + + defp deserialize_validator_scope(scope) do + case scope do + nil -> + {:ok, nil} + + {:epoch, first_epoch..last_epoch//_step} -> + {:ok, + Collection.generate_key_boundary( + {Collection.gen_range(first_epoch, last_epoch), Collection.binary()} + )} + + _otherwise -> + {:error, ErrInput.Scope.exception(value: "invalid epoch scope")} + end + end + + defp deserialize_epoch_scope(scope) do + case scope do + nil -> + {:ok, nil} + + {:epoch, first_epoch..last_epoch//_step} -> + {:ok, {first_epoch, last_epoch}} + + _otherwise -> + {:error, ErrInput.Scope.exception(value: "invalid epoch scope")} + end + end + + defp deserialize_validator_delegates_scope(scope) do + case scope do + nil -> + {:ok, nil} + + {:epoch, first_epoch..last_epoch//_step} -> + {:ok, {first_epoch, last_epoch}} + + _otherwise -> + {:error, ErrInput.Scope.exception(value: "invalid epoch scope")} + end + end + + defp serialize_numeric_cursor(nil) do + nil + end + + defp serialize_numeric_cursor(height) do + height + |> :erlang.term_to_binary() + |> Base.encode64() + end + + defp deserialize_numeric_cursor(nil) do + nil + end + + defp deserialize_numeric_cursor(bin) do + bin + |> Base.decode64!() + |> :erlang.binary_to_term() + end + + defp deserialize_validator_cursor(nil) do + nil + end + + defp deserialize_validator_cursor(bin) do + bin + |> Base.decode64!() + |> :erlang.binary_to_term() + end + + defp serialize_validator_cursor(nil) do + nil + end + + defp serialize_validator_cursor({_pubkey, _epoch} = rev_validator_index) do + rev_validator_index + |> :erlang.term_to_binary() + |> Base.encode64() + end + + defp serialize_validator_delegates_cursor(nil) do + nil + end + + defp serialize_validator_delegates_cursor({_pubkey, _epoch, _delegate} = delegate_index) do + delegate_index + |> :erlang.term_to_binary() + |> Base.encode64() + end + + defp deserialize_validator_delegates_cursor(nil) do + nil + end + + defp deserialize_validator_delegates_cursor(bin) do + bin + |> Base.decode64!() + |> :erlang.binary_to_term() + end +end diff --git a/lib/ae_mdw/node.ex b/lib/ae_mdw/node.ex index 078449e47..9e4da13ca 100644 --- a/lib/ae_mdw/node.ex +++ b/lib/ae_mdw/node.ex @@ -66,6 +66,7 @@ defmodule AeMdw.Node do @type hashrate() :: non_neg_integer() @type difficulty() :: non_neg_integer() + @type epoch() :: non_neg_integer() @opaque signed_tx() :: tuple() @opaque aetx() :: tuple() @@ -92,6 +93,8 @@ defmodule AeMdw.Node do @typep method_hash :: binary() @typep method_signature :: {list(), any()} + @typep consensus :: atom() + @typep trees_in :: term() @spec aex9_signatures :: %{method_hash() => method_signature()} defmemo aex9_signatures() do @@ -327,6 +330,21 @@ defmodule AeMdw.Node do :aetx.type_to_swagger_name(tx_type) end + @spec epoch_start_height(epoch()) :: {:ok, height()} | {:error, atom()} + def epoch_start_height(epoch) do + :aec_chain_hc.epoch_start_height(epoch) + end + + @spec state_pre_transform_micro_node(consensus(), height(), node(), trees_in()) :: trees_in() + def state_pre_transform_micro_node(consensus, height, node, trees_in) do + try do + consensus.state_pre_transform_micro_node(height, node, trees_in) + rescue + UndefinedFunctionError -> + consensus.state_pre_transform_micro_node(node, trees_in) + end + end + @spec tx_prefixes :: MapSet.t() defmemo tx_prefixes() do tx_types() @@ -345,6 +363,11 @@ defmodule AeMdw.Node do |> MapSet.new() end + @spec epoch_length(epoch()) :: {:ok, non_neg_integer()} | {:error, atom()} + defmemo epoch_length(epoch) do + :aec_chain_hc.epoch_length(epoch) + end + defp map_by_function_hash(signatures) do Map.new(signatures, fn {k, v} -> {Contract.function_hash(k), v} end) end @@ -402,7 +425,7 @@ defmodule AeMdw.Node do defmemop tx_groups_map() do type_groups_map = - ~w(oracle name contract channel spend ga paying)a + ~w(oracle name contract channel spend ga paying hc)a |> Map.new(&{to_string(&1), &1}) tx_types() diff --git a/lib/ae_mdw/node/db.ex b/lib/ae_mdw/node/db.ex index cdbe92576..278eca1ad 100644 --- a/lib/ae_mdw/node/db.ex +++ b/lib/ae_mdw/node/db.ex @@ -3,6 +3,7 @@ defmodule AeMdw.Node.Db do import AeMdw.Util + alias AeMdw.Sync.Hyperchain alias AeMdw.Blocks alias AeMdw.Db.Model alias AeMdw.DryRun.Runner @@ -31,19 +32,27 @@ defmodule AeMdw.Node.Db do @spec get_blocks_per_height(Blocks.height(), Blocks.block_hash() | Blocks.height()) :: [ {Blocks.height(), [micro_block()], Blocks.block_hash() | nil} ] - def get_blocks_per_height(from_height, block_hash) when is_binary(block_hash), - do: get_blocks_per_height(from_height, block_hash, nil) + def get_blocks_per_height(from_height, block_hash_or_height) do + if Hyperchain.hyperchain?() do + get_blocks_per_height_hyperchain(from_height, block_hash_or_height) + else + blocks_per_height(from_height, block_hash_or_height) + end + end + + defp blocks_per_height(from_height, block_hash) when is_binary(block_hash), + do: blocks_per_height(from_height, block_hash, nil) - def get_blocks_per_height(from_height, to_height) when is_integer(to_height) do + defp blocks_per_height(from_height, to_height) when is_integer(to_height) do {:ok, header} = :aec_chain.get_key_header_by_height(to_height + 1) last_mb_hash = :aec_headers.prev_hash(header) {:ok, last_kb_hash} = :aec_headers.hash_header(header) - get_blocks_per_height(from_height, last_mb_hash, last_kb_hash) + blocks_per_height(from_height, last_mb_hash, last_kb_hash) end - defp get_blocks_per_height(from_height, last_mb_hash, last_kb_hash) do + defp blocks_per_height(from_height, last_mb_hash, last_kb_hash) do {:ok, root_hash} = :aec_chain.genesis_block() |> :aec_blocks.to_header() @@ -69,6 +78,41 @@ defmodule AeMdw.Node.Db do |> Enum.reverse() end + defp get_blocks_per_height_hyperchain(from_height, to_height) when is_integer(to_height) do + {:ok, header} = :aec_chain.get_key_header_by_height(to_height + 1) + {:ok, last_kb_hash} = :aec_headers.hash_header(header) + + get_blocks_per_height_hyperchain(from_height, last_kb_hash) + end + + defp get_blocks_per_height_hyperchain(from_height, last_kb_hash) when is_binary(last_kb_hash) do + {:ok, root_hash} = + :aec_chain.genesis_block() + |> :aec_blocks.to_header() + |> :aec_headers.hash_header() + + last_kb_hash + |> Stream.unfold(fn + ^root_hash -> + nil + + last_kb_hash -> + prev_key_hash = + last_kb_hash + |> :aec_db.get_block() + |> :aec_blocks.prev_key_hash() + + {:ok, %{key_block: prev_key_block, micro_blocks: micro_blocks}} = + :aec_chain.get_generation_by_hash(prev_key_hash, :backward) + + {{prev_key_block, micro_blocks, last_kb_hash}, prev_key_hash} + end) + |> Enum.take_while(fn {key_block, _micro_blocks, _last_kb_hash} -> + :aec_blocks.height(key_block) >= from_height + end) + |> Enum.reverse() + end + defp get_kb_mbs(last_mb_hash) do last_mb_hash |> Stream.unfold(fn block_hash -> @@ -300,8 +344,10 @@ defmodule AeMdw.Node.Db do |> :aec_blocks.to_header() |> :aec_headers.consensus_module() - node - |> consensus_mod.state_pre_transform_micro_node(trees_in) + height = :aec_blocks.height(micro_block) + + consensus_mod + |> Node.state_pre_transform_micro_node(height, node, trees_in) |> :aec_trees.accounts() end diff --git a/lib/ae_mdw/stats.ex b/lib/ae_mdw/stats.ex index 6dbf7f2c8..93d45334d 100644 --- a/lib/ae_mdw/stats.ex +++ b/lib/ae_mdw/stats.ex @@ -17,6 +17,7 @@ defmodule AeMdw.Stats do alias AeMdw.Util alias AeMdw.Validate alias AeMdw.Node.Db, as: NodeDb + alias AeMdw.Sync.Hyperchain require Model @@ -139,23 +140,35 @@ defmodule AeMdw.Stats do def fetch_stats(state) do with {:ok, Model.stat(payload: {tps, tps_block_hash})} <- State.get(state, Model.Stat, @tps_stat_key), - {:ok, Model.stat(payload: miners_count)} <- - State.get(state, Model.Stat, @miners_count_stat_key), {:ok, milliseconds_per_block} <- milliseconds_per_block(state) do {{last_24hs_txs_count, trend}, {last_24hs_tx_fees_average, fees_trend}} = last_24hs_txs_count_and_fee_with_trend(state) - {:ok, - %{ - max_transactions_per_second: tps, - max_transactions_per_second_block_hash: Enc.encode(:key_block_hash, tps_block_hash), - miners_count: miners_count, - last_24hs_transactions: last_24hs_txs_count, - transactions_trend: trend, - last_24hs_average_transaction_fees: last_24hs_tx_fees_average, - fees_trend: fees_trend, - milliseconds_per_block: milliseconds_per_block - }} + stats = + %{ + max_transactions_per_second: tps, + max_transactions_per_second_block_hash: Enc.encode(:key_block_hash, tps_block_hash), + last_24hs_transactions: last_24hs_txs_count, + transactions_trend: trend, + last_24hs_average_transaction_fees: last_24hs_tx_fees_average, + fees_trend: fees_trend, + milliseconds_per_block: milliseconds_per_block + } + + stats = + if Hyperchain.hyperchain?() do + validators_count = + state |> State.height() |> Hyperchain.validators_at_height() |> length() + + Map.put(stats, :validators_count, validators_count) + else + Model.stat(payload: miners_count) = + State.fetch!(state, Model.Stat, @miners_count_stat_key) + + Map.put(stats, :miners_count, miners_count) + end + + {:ok, stats} else _no_stats -> {:error, ErrInput.NotFound.exception(value: "no stats")} @@ -690,16 +703,27 @@ defmodule AeMdw.Stats do end end + @spec milliseconds_per_block(State.t()) :: {:ok, non_neg_integer() | nil} | {:error, reason()} defp milliseconds_per_block(state) do with {:ok, first_block} <- :aec_chain.get_key_block_by_height(1), {:ok, last_gen} <- DbUtil.last_gen(state), - {:ok, last_block} <- :aec_chain.get_key_block_by_height(last_gen) do + {:ok, last_block} <- get_last_key_block(last_gen) do first_block_time = :aec_blocks.time_in_msecs(first_block) last_block_time = :aec_blocks.time_in_msecs(last_block) {:ok, div(last_block_time - first_block_time, last_gen)} + else + {:error, :chain_too_short} -> {:ok, nil} + error -> error + end + end + + defp get_last_key_block(gen) do + case :aec_chain.get_key_block_by_height(gen) do + {:ok, block} -> {:ok, block} + {:error, :chain_too_short} -> :aec_chain.get_key_block_by_height(gen - 1) end end end diff --git a/lib/ae_mdw/sync/hyperchain.ex b/lib/ae_mdw/sync/hyperchain.ex new file mode 100644 index 000000000..16c592a3d --- /dev/null +++ b/lib/ae_mdw/sync/hyperchain.ex @@ -0,0 +1,97 @@ +defmodule AeMdw.Sync.Hyperchain do + @moduledoc """ + This module is responsible for syncing of the hyperchain + """ + alias AeMdw.Blocks + alias AeMdw.Node.Db + + @type epoch() :: non_neg_integer() + @type epoch_info() :: %{ + first: Blocks.height(), + last: Blocks.height(), + length: non_neg_integer(), + seed: binary() | :undefined, + epoch: epoch(), + validators: list({Db.pubkey(), non_neg_integer()}) + } + @type leader() :: Blocks.key_header() + + @spec hyperchain?() :: boolean() + def hyperchain?() do + case :aeu_env.user_config(["chain", "consensus", "0", "type"]) do + {:ok, "hyperchain"} -> true + _error -> false + end + end + + @spec connected_to_parent?() :: boolean() + def connected_to_parent?() do + :aec_consensus_hc.get_entropy_hash(1) != {:error, :not_in_cache} + end + + @spec epoch_info_at_height(Blocks.height()) :: {:ok, epoch_info()} | :error + def epoch_info_at_height(height) do + with {:ok, kb_hash} <- :aec_chain_state.get_key_block_hash_at_height(height), + {_tx_env, _trees} = run_env <- + :aetx_env.tx_env_and_trees_from_hash(:aetx_transaction, kb_hash), + {:ok, epoch} <- :aec_chain_hc.epoch(run_env) do + :aec_chain_hc.epoch_info_for_epoch(run_env, epoch) + end + end + + @spec leaders_for_epoch_at_height(Blocks.height()) :: [{leader(), Blocks.height()}] + def leaders_for_epoch_at_height(height) do + {:ok, kb_hash} = :aec_chain_state.get_key_block_hash_at_height(height) + {_tx_env, _trees} = run_env = :aetx_env.tx_env_and_trees_from_hash(:aetx_transaction, kb_hash) + {:ok, epoch} = :aec_chain_hc.epoch(run_env) + + {:ok, %{seed: seed, validators: validators, length: length, first: first} = _epoch_info} = + :aec_chain_hc.epoch_info_for_epoch(run_env, epoch) + + {:ok, seed} = + case seed do + :undefined -> + :aec_consensus_hc.get_entropy_hash(epoch) + + otherwise -> + {:ok, otherwise} + end + + {:ok, schedule} = :aec_chain_hc.validator_schedule(run_env, seed, validators, length) + + Enum.with_index(schedule, first) + end + + @spec validators_at_height(Blocks.height()) :: [term()] + def validators_at_height(height) do + {:ok, %{validators: validators}} = epoch_info_at_height(height) + validators + end + + @spec get_delegates(Blocks.height(), Db.pubkey()) :: {:ok, map()} | {:error, term()} | :error + def get_delegates(height, pubkey) do + with {:ok, kb_hash} <- :aec_chain_state.get_key_block_hash_at_height(height), + {tx_env, trees} <- :aetx_env.tx_env_and_trees_from_hash(:aetx_transaction, kb_hash), + {:ok, + {:tuple, + {_ct, _address, _creation_height, _stake, _pending_stake, _stake_limit, _is_online, + state}}} <- + :aec_consensus_hc.call_consensus_contract_result( + :staking, + tx_env, + trees, + ~c"get_validator_state", + [:aefa_fate_code.encode_arg({:address, pubkey})] + ) do + {:tuple, + {_main_staking_ct, _unstake_deley, _pending_unstake_amount, _pending_unstake, _name, + _description, _image_url, delegates, _shares}} = state + + delegates + |> Enum.into(%{}, fn {{:address, pubkey}, stake} -> + {pubkey, stake} + end) + |> then(&{:ok, &1}) + end + end +end diff --git a/lib/ae_mdw/util/encoding.ex b/lib/ae_mdw/util/encoding.ex index ca9d1c2e1..e5d01878f 100644 --- a/lib/ae_mdw/util/encoding.ex +++ b/lib/ae_mdw/util/encoding.ex @@ -32,4 +32,7 @@ defmodule AeMdw.Util.Encoding do @spec encode_block(atom(), binary()) :: encoded_hash() def encode_block(:key, hash), do: encode(:key_block_hash, hash) def encode_block(:micro, hash), do: encode(:micro_block_hash, hash) + + @spec safe_decode(atom(), encoded_hash()) :: {:ok, pubkey()} | :error + defdelegate safe_decode(type, pk), to: :aeser_api_encoder end diff --git a/lib/ae_mdw_web/controllers/hyperchain_controller.ex b/lib/ae_mdw_web/controllers/hyperchain_controller.ex new file mode 100644 index 000000000..0e2901dbd --- /dev/null +++ b/lib/ae_mdw_web/controllers/hyperchain_controller.ex @@ -0,0 +1,107 @@ +defmodule AeMdwWeb.HyperchainController do + use AeMdwWeb, :controller + + alias AeMdw.Hyperchain + alias AeMdw.Validate + alias AeMdwWeb.FallbackController + alias AeMdwWeb.Util, as: WebUtil + alias AeMdwWeb.Plugs.HyperchainPlug + alias AeMdwWeb.Plugs.PaginatedPlug + alias Plug.Conn + + plug(HyperchainPlug) + plug(PaginatedPlug, order_by: ~w(expiration activation deactivation name)a) + action_fallback(FallbackController) + + @spec epochs(Conn.t(), map()) :: Conn.t() + def epochs(%Conn{assigns: assigns} = conn, _params) do + %{state: state, pagination: pagination, cursor: cursor, scope: scope} = + assigns + + with {:ok, epochs} <- + Hyperchain.fetch_epochs(state, pagination, scope, cursor) do + WebUtil.render(conn, epochs) + end + end + + @spec epochs_top(Conn.t(), map()) :: Conn.t() + def epochs_top(%Conn{assigns: %{state: state}} = conn, _params) do + with {:ok, epoch} <- Hyperchain.fetch_epoch_top(state) do + format_json(conn, epoch) + end + end + + @spec schedule(Conn.t(), map()) :: Conn.t() + def schedule(%Conn{assigns: assigns} = conn, _params) do + %{state: state, pagination: pagination, cursor: cursor, scope: scope} = + assigns + + with {:ok, schedule} <- + Hyperchain.fetch_leaders_schedule(state, pagination, scope, cursor) do + WebUtil.render(conn, schedule) + end + end + + @spec schedule_at_height(Conn.t(), map()) :: Conn.t() + def schedule_at_height(%Conn{assigns: %{state: state}} = conn, %{"height" => height}) do + with {:ok, height} <- Validate.nonneg_int(height), + {:ok, leader} <- Hyperchain.fetch_leaders_schedule_at_height(state, height) do + format_json(conn, leader) + end + end + + @spec validators(Conn.t(), map()) :: Conn.t() + def validators(%Conn{assigns: assigns} = conn, _params) do + %{state: state, pagination: pagination, cursor: cursor, scope: scope} = + assigns + + with {:ok, validators} <- + Hyperchain.fetch_validators(state, pagination, scope, cursor) do + WebUtil.render(conn, validators) + end + end + + @spec validators_top(Conn.t(), map()) :: Conn.t() + def validators_top(%Conn{assigns: assigns} = conn, _params) do + %{state: state, pagination: pagination, cursor: cursor} = + assigns + + with {:ok, validators} <- + Hyperchain.fetch_validators_top(state, pagination, cursor) do + WebUtil.render(conn, validators) + end + end + + @spec validator(Conn.t(), map()) :: Conn.t() + def validator(%Conn{assigns: %{state: state}} = conn, %{"validator_id" => validator_id}) do + with {:ok, validator} <- Hyperchain.fetch_validator(state, validator_id) do + format_json(conn, validator) + end + end + + @spec validator_delegates(Conn.t(), map()) :: Conn.t() + def validator_delegates(%Conn{assigns: assigns} = conn, %{ + "validator_id" => validator_id + }) do + %{state: state, pagination: pagination, cursor: cursor, scope: scope} = + assigns + + with {:ok, delegates} <- + Hyperchain.fetch_delegates(state, validator_id, pagination, scope, cursor) do + WebUtil.render(conn, delegates) + end + end + + @spec validator_delegates_top(Conn.t(), map()) :: Conn.t() + def validator_delegates_top(%Conn{assigns: assigns} = conn, %{ + "validator_id" => validator_id + }) do + %{state: state, pagination: pagination, cursor: cursor} = + assigns + + with {:ok, delegates} <- + Hyperchain.fetch_delegates_top(state, validator_id, pagination, cursor) do + WebUtil.render(conn, delegates) + end + end +end diff --git a/lib/ae_mdw_web/plugs/hyperchain_plug.ex b/lib/ae_mdw_web/plugs/hyperchain_plug.ex new file mode 100644 index 000000000..45921a6bf --- /dev/null +++ b/lib/ae_mdw_web/plugs/hyperchain_plug.ex @@ -0,0 +1,24 @@ +defmodule AeMdwWeb.Plugs.HyperchainPlug do + @moduledoc """ + Prevents action if the controller is not on a hyperchain node. + """ + + alias Plug.Conn + alias Phoenix.Controller + alias AeMdw.Sync.Hyperchain + + @spec init(Plug.opts()) :: Plug.opts() + def init(opts), do: opts + + @spec call(Conn.t(), Plug.opts()) :: Conn.t() + def call(conn, _opts) do + if Hyperchain.hyperchain?() do + conn + else + conn + |> Conn.put_status(:bad_request) + |> Controller.json(%{"error" => "Not on a hyperchain node"}) + |> Conn.halt() + end + end +end diff --git a/lib/ae_mdw_web/plugs/paginated_plug.ex b/lib/ae_mdw_web/plugs/paginated_plug.ex index bbf7b2ba6..efdbdf269 100644 --- a/lib/ae_mdw_web/plugs/paginated_plug.ex +++ b/lib/ae_mdw_web/plugs/paginated_plug.ex @@ -17,7 +17,8 @@ defmodule AeMdwWeb.Plugs.PaginatedPlug do @scope_types %{ "gen" => :gen, "txi" => :txi, - "time" => :time + "time" => :time, + "epoch" => :epoch } @scope_types_keys Map.keys(@scope_types) @@ -117,7 +118,7 @@ defmodule AeMdwWeb.Plugs.PaginatedPlug do _txi_scope? = false, state ) - when scope_type in ["gen", "time"] do + when scope_type in ["gen", "time", "epoch"] do extract_direction_and_scope(params, true, state) end diff --git a/lib/ae_mdw_web/router.ex b/lib/ae_mdw_web/router.ex index df484a5fe..6db301eb8 100644 --- a/lib/ae_mdw_web/router.ex +++ b/lib/ae_mdw_web/router.ex @@ -126,6 +126,23 @@ defmodule AeMdwWeb.Router do get "/debug/dex/:contract_id/swaps", DexController, :debug_contract_swaps get "/wealth", WealthController, :wealth + get "/hyperchain/schedule", HyperchainController, :schedule + get "/hyperchain/schedule/height/:height", HyperchainController, :schedule_at_height + get "/hyperchain/epochs", HyperchainController, :epochs + get "/hyperchain/epochs/top", HyperchainController, :epochs_top + get "/hyperchain/validators", HyperchainController, :validators + get "/hyperchain/validators/top", HyperchainController, :validators_top + + get "/hyperchain/validators/:validator_id/delegates", + HyperchainController, + :validator_delegates + + get "/hyperchain/validators/:validator_id/delegates/top", + HyperchainController, + :validator_delegates_top + + get "/hyperchain/validators/:validator_id", HyperchainController, :validator + get "/api", UtilController, :static_file, assigns: %{filepath: "static/swagger/swagger_v3.json"} end diff --git a/mix.exs b/mix.exs index 1b7bcb865..6bbef234d 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,9 @@ defmodule AeMdw.MixProject do :aec_block_insertion, :aec_chain, :aec_chain_state, + :aec_chain_hc, :aec_consensus, + :aec_consensus_hc, :aec_consensus_bitcoin_ng, :aec_dev_reward, :aec_dry_run, @@ -55,6 +57,7 @@ defmodule AeMdw.MixProject do :aect_create_tx, :aect_dispatch, :aect_state_tree, + :aefa_fate_code, :aega_attach_tx, :aega_call, :aega_meta_tx, diff --git a/scripts/do.sh b/scripts/do.sh index b195b32df..85cf4efb2 100755 --- a/scripts/do.sh +++ b/scripts/do.sh @@ -24,6 +24,12 @@ case $1 in iex --sname $NAME -S mix phx.server ;; + "hc-shell") + mix local.hex --force && mix local.rebar --force && mix deps.get + AETERNITY_CONFIG=./hyperchain/aeternity.yaml iex --sname $NAME -S mix phx.server + ;; + + "remsh") iex --sname console --remsh $NAME ;; @@ -32,6 +38,11 @@ case $1 in docker-compose -f docker-compose-dev.yml run --rm --workdir=/app --entrypoint="" --use-aliases --service-ports ae_mdw /bin/bash ;; + "hc-docker-shell") + maybe_create_db_directory "./data_hc" + docker-compose -f docker-compose-hc.yml run --rm --workdir=/app --entrypoint="" --use-aliases --service-ports ae_mdw_hc /bin/bash + ;; + "testnet-docker-shell") maybe_create_db_directory "./data_testnet" docker-compose -f docker-compose-dev-testnet.yml run --rm --workdir=/app --entrypoint="" --use-aliases --service-ports ae_mdw_testnet /bin/bash diff --git a/test/ae_mdw/stats_test.exs b/test/ae_mdw/stats_test.exs index a60a10ec7..57d216916 100644 --- a/test/ae_mdw/stats_test.exs +++ b/test/ae_mdw/stats_test.exs @@ -2,6 +2,7 @@ defmodule AeMdw.StatsTest do use ExUnit.Case import Mock + alias AeMdw.Sync.Hyperchain alias AeMdw.Db.State alias AeMdw.Db.MemStore alias AeMdw.Db.Model @@ -38,7 +39,8 @@ defmodule AeMdw.StatsTest do time_in_msecs: fn :first_block -> now - 10 * three_minutes :other_block -> now - end} + end}, + {Hyperchain, [], hyperchain?: fn -> false end} ]) do assert {:ok, %{ diff --git a/test/ae_mdw_web/controllers/name_controller_test.exs b/test/ae_mdw_web/controllers/name_controller_test.exs index 14d579a6b..ce1f81fcb 100644 --- a/test/ae_mdw_web/controllers/name_controller_test.exs +++ b/test/ae_mdw_web/controllers/name_controller_test.exs @@ -1289,9 +1289,9 @@ defmodule AeMdwWeb.NameControllerTest do end end - test "renders empty result when no blocks", %{conn: conn} do + test "renders empty result when no blocks", %{conn: conn, store: store} do assert %{"data" => [], "next" => nil, "prev" => nil} = - conn |> get("/v3/names/auctions") |> json_response(200) + conn |> get("/v3/names/auctions") |> with_store(store) |> json_response(200) end test "renders error when parameter by is invalid", %{conn: conn} do From ddf61e520b343a208f64ca06a93c32367ca48329 Mon Sep 17 00:00:00 2001 From: Sebastian Borrazas Date: Wed, 15 Jan 2025 16:24:05 -0300 Subject: [PATCH 2/5] chore: get rid of runtime startup warnings (#2039) --- config/config.exs | 5 ++++- lib/ae_mdw/application.ex | 3 ++- mix.lock | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config/config.exs b/config/config.exs index 4c35d2744..ff9017ada 100644 --- a/config/config.exs +++ b/config/config.exs @@ -5,6 +5,9 @@ alias AeMdw.Db.Model node_root = System.get_env("NODEROOT", "../aeternity/_build/local/") +config :esbuild, + version: "0.8.2" + config :ae_mdw, AeMdw.Db.RocksDb, data_dir: "#{node_root}/rel/aeternity/data/mdw.db", drop_tables: [ @@ -33,7 +36,7 @@ config :ae_mdw, AeMdwWeb.Endpoint, url: [host: "localhost"], secret_key_base: "kATf71kudJsgA1dgCQKcmgelicqJHG8EID8rwROwJHpWHb53EdzW7YDclJZ8mxLP", render_errors: [view: AeMdwWeb.ErrorView, accepts: ~w(html json)], - pubsub: [name: AeMdw.PubSub, adapter: Phoenix.PubSub.PG2], + pubsub_server: AeMdw.PubSub, live_view: [signing_salt: "Oy680JAN"], code_reloader: false, watchers: [], diff --git a/lib/ae_mdw/application.ex b/lib/ae_mdw/application.ex index ad9a20e84..965ac8cc5 100644 --- a/lib/ae_mdw/application.ex +++ b/lib/ae_mdw/application.ex @@ -48,7 +48,8 @@ defmodule AeMdw.Application do AeMdwWeb.Supervisor, AeMdwWeb.Websocket.Supervisor, AeMdw.Sync.Supervisor, - AeMdw.APM.Telemetry + AeMdw.APM.Telemetry, + {Phoenix.PubSub, [name: AeMdw.PubSub, adapter: Phoenix.PubSub.PG2]} ] children = diff --git a/mix.lock b/mix.lock index 072a7022d..0d0441f66 100644 --- a/mix.lock +++ b/mix.lock @@ -3,17 +3,17 @@ "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, + "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"}, - "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, + "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"}, "excoveralls": {:hex, :excoveralls, "0.14.6", "610e921e25b180a8538229ef547957f7e04bd3d3e9a55c7c5b7d24354abbba70", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0eceddaa9785cfcefbf3cd37812705f9d8ad34a758e513bb975b081dce4eb11e"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, From 35af3fab1be41d598ff86a54a6502a30e4dda5c2 Mon Sep 17 00:00:00 2001 From: Mihail Dobrev Date: Thu, 16 Jan 2025 16:36:38 +0200 Subject: [PATCH 3/5] chore: bump release-please version to 4 (#2062) --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index f3f28dd6f..ff0a8f699 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -7,7 +7,7 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: GoogleCloudPlatform/release-please-action@v2 + - uses: googleapis/release-please-action@v4 with: token: ${{secrets.BOT_GITHUB_TOKEN}} release-type: elixir From 2903e0a98e50b05740080318924d82cf07d3b213 Mon Sep 17 00:00:00 2001 From: Mihail Dobrev Date: Fri, 17 Jan 2025 13:33:12 +0200 Subject: [PATCH 4/5] chore: downgrade release please (#2070) --- .github/workflows/release-please.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index ff0a8f699..48db15cd0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -7,8 +7,9 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@v3 with: token: ${{secrets.BOT_GITHUB_TOKEN}} release-type: elixir + pull-request-title-pattern: "chore: release${component} ${version}" changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"ci","section":"CI / CD","hidden":false},{"type":"test","section":"Testing","hidden":false},{"type":"refactor","section":"Refactorings","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false}]' From 61dcf198935ac08791ea042ddd71dde3b49cd5ac Mon Sep 17 00:00:00 2001 From: aeternity-bot <35604848+aeternity-bot@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:50:11 +0200 Subject: [PATCH 5/5] chore: release 1.97.0 (#2069) --- CHANGELOG.md | 14 ++++++++++++++ mix.exs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512d5db92..355b9a442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.97.0](https://github.com/aeternity/ae_mdw/compare/v1.96.2...v1.97.0) (2025-01-17) + + +### Features + +* add hyperchain sync support ([74e098d](https://github.com/aeternity/ae_mdw/commit/74e098d5b4dfbb6765f7b034b90f77d2eaa15fea)) + + +### Miscellaneous + +* bump release-please version to 4 ([#2062](https://github.com/aeternity/ae_mdw/issues/2062)) ([35af3fa](https://github.com/aeternity/ae_mdw/commit/35af3fab1be41d598ff86a54a6502a30e4dda5c2)) +* downgrade release please ([#2070](https://github.com/aeternity/ae_mdw/issues/2070)) ([2903e0a](https://github.com/aeternity/ae_mdw/commit/2903e0a98e50b05740080318924d82cf07d3b213)) +* get rid of runtime startup warnings ([#2039](https://github.com/aeternity/ae_mdw/issues/2039)) ([ddf61e5](https://github.com/aeternity/ae_mdw/commit/ddf61e520b343a208f64ca06a93c32367ca48329)) + ### [1.96.2](https://www.github.com/aeternity/ae_mdw/compare/v1.96.1...v1.96.2) (2025-01-07) diff --git a/mix.exs b/mix.exs index 6bbef234d..1905ed0df 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule AeMdw.MixProject do def project() do [ app: :ae_mdw, - version: "1.96.2", + version: "1.97.0", elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), elixirc_options: [