diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbd64a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/.testnets/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8114acf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "submodules/staking-api-service"] + path = submodules/staking-api-service + url = https://github.com/babylonchain/staking-api-service.git +[submodule "submodules/staking-indexer"] + path = submodules/staking-indexer + url = https://github.com/babylonchain/staking-indexer.git +[submodule "submodules/cli-tools"] + path = submodules/cli-tools + url = https://github.com/babylonchain/cli-tools.git +[submodule "submodules/staking-expiry-checker"] + path = submodules/staking-expiry-checker + url = https://github.com/babylonchain/staking-expiry-checker.git +[submodule "submodules/covenant-signer"] + path = submodules/covenant-signer + url = git@github.com:babylonchain/covenant-signer.git +[submodule "submodules/simple-staking"] + path = submodules/simple-staking + url = https://github.com/babylonchain/simple-staking.git diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..a9c0442 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,438 @@ +# BTC Staking Phase-1 deployment (BTC backend: bitcoind) + +## Components + +The system that emulates Babylon's Phase-1 Mainnet network +comprises the following components: + +- **BTC Staking Indexer**: Parses BTC blocks and forwards BTC Staking + transactions to a queueing system, while also persisting them to an on-disk + key-value storage +- **RabbitMQ**: Houses a set of queues containing BTC Staking transactions +- **Staking API Service**: Consumes BTC Staking transactions from the RabbitMQ + queues and stores them in a central data store, additionally accepting + unbonding requests +- **Mongo DB**: Stores BTC Staking transaction data +- **Staking Expiry Checker**: Periodically checks MongoDB for expired BTC + Stake Delegations and Unbondings +- **Unbonding Pipeline**: Forwards unbonding requests for signing to a Covenant + Emulator committee and submits them to BTC +- **Staking Dashboard**: UI that allows for creating BTC Staking transactions, + connects to the API to retrieve information about the system and historical + delegations +- **Covenant Signer**: Receives unbonding transactions and returns the same + transactions signed by the covenant emulator's key +- **Bitcoin Full Node**: Operates an emulated **regtest Bitcoin** network +- **Bitcoin Offline Wallet**: Stores the Covenant Signer member keys and signs + unbonding transactions forwarded by the Covenant Signer +- A **Global Configuration** file that contains system-wide parameters pertinent + to the processed Staking transactions +- A **Finality Provider** config file that contains information about finality + providers participating in the system + +### Expected Docker state post-deployment + +The following containers should be created as a result of the `make` command +that spins up the network: + +```shell +[+] ✔ Network artifacts_phase-1 Created 0.1s + ✔ Container bitcoindsim Started 0.6s + ✔ Container mongodb Started 0.4s + ✔ Container rabbitmq Started 0.5s + ✔ Container staking-indexer Started 1.0s + ✔ Container staking-api-service Started 1.0s + ✔ Container unbonding-pipeline Started 1.3s + ✔ Container staking-expiry-checker Started 1.0s + ✔ Container simple-staking Started 1.7s + ✔ Container bitcoindsim-signer Started 1.0s + ✔ Container covenant-signer Started 1.0s +``` + +## Inspecting the BTC Staking Phase-1 system demo + +Deploying the BTC Staking Phase-1 system through the +`make start-deployment-btcstaking-phase1-bitcoind-demo` subcommand leads to the +execution of an additional post-deployment [script](btcstaking-demo.sh) that +showcases the lifecycle of Staking, Unbonding and Withdraw operations inside the +Phase-1 system in conjunction with its Global Configuration. + +We will now analyze each step that is executed as part of the showcasing +script - more specifically, how it is performed and its outcome for the system. + +### Preface: The Demo Global Configuration + +The Global Configuration includes a series of versioned governance parameters +that greatly affect the behaviour of the system. Detailed information can be +found +[here](https://github.com/babylonchain/networks/tree/main/bbn-test-4/parameters). + +The Demo Global Configuration can be found [here](artifacts/global-params.json). +Below, we briefly mention some parameters that are critical for the +behaviour of this demonstation: +- **Version**: Defines a version of the governance parameters +- **Activation height**: Defines the BTC height from which a specific version of + the global parameters kicks in +- **Staking cap**: Defines the maximum amount of active BTC stake that the system + can accommodate +- **Min / Max staking amount**: Defines the minimum / maximum BTC stake that can + be included in a single staking transaction +- **Confirmation depth**: The amount of BTC confirmations that a staking + transaction has to gather before being processed by the system + +For this demo, we define a Global Configuration with 2 versions: +- Version 0: + - Activation height: `115` + - Staking cap: `10 BTC` + - Min / Max staking amount `1 / 10 BTC` + - Confirmation depth: `2 blocks` +- Version 0: + - Activation height: `130` + - Staking cap: `20 BTC` + - Min / Max staking amount `2 / 20 BTC` + - Confirmation depth: `2 blocks` + +### Scenario 1: No transactions are processed by the system before the first activation height + +The Staking Indexer service will only process transaction that are included in +BTC after the initial activation height, as stated in the `v0` of the staking +parameters. + +To verify this, we send a transaction before the first activation height (115) +to the system. + +#### Step by Step Details: + +* At height 111, we send 1 staking transaction with stake 6 BTC +* At height 113, we verify that the Staking Indexer disregarded it by checking + its Prometheus metric counter for staking transactions (should be zero) + +#### Sample Output + +```log +===== ### Scenario 1: No transactions are processed by the system before the first activation height ===== +Sign and send a staking transaction with stake: 6 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: e2eb30489c4e377d86cbaa80c110cf5e84c95d1909ca8f035b5a5609c75cc556 +Next bitcoin block will be produced in 10 seconds... + Current Height 112 +Next bitcoin block will be produced in 10 seconds... + Current Height 113 + Target metrics achieved! Invalid transaction count on Staking Indexer: 0 + Target metrics achieved! Active delegation count: 0 + Target metrics achieved! Total delegation count: 0 +``` + +### Scenario 2: Submit Staking Transactions and fill the staking cap + +We generate BTC staking transactions and send it to the Bitcoin regtest chain. +Once the transactions receives enough confirmations (2), they're processed by +the Staking Indexer and placed in the corresponding RabbitMQ queue. + +The Staking API Service is listening to this queue; as soon as the transaction +appears, it will be consumed and inserted to the MongoDB, with `active` status. + +After the system's active BTC TVL reaches the staking BTC cap, all incoming +staking transactions are marked as `overflow` by the Staking API and can never +be activated. These transactions can still be unbonded and withdrawn. + +![plot](./diagrams/staking.png) + +#### Step by Step Details: + +* At height 115, we send 2 staking transactions with stake 2 BTC and 8 BTC +* At height 116, we send 1 staking transaction with stake 1 BTC +* At height 117, both the initial transactions are active with total active + stake 10 BTC; the staking cap is filled +* At height 118, the latter transaction will be marked as overflow due to the + staking cap being filled + +#### Sample Output + +```log +===== Scenario 2: Submit Staking Transactions and fill the staking cap ===== +Generating enough bitcoin blocks to reach bitcoin height 115... + Reached target bitcoin height 115 +Sign and send a staking transaction with stake: 2 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b +Sign and send a staking transaction with stake: 8 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: 67fb73196d900d5932e3a25e9ec65c85c2b04783a187d671b74ecec24c358af6 +Next bitcoin block will be produced in 10 seconds... + Current Height 116 +Sign and send a staking transaction with stake: 1 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: f782cb1df71e25b249ee2bcabbd9dd08f782f15ac1132cadca665161771cad90 +Next bitcoin block will be produced in 10 seconds... + Current Height 117 +Checking transaction with ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Transaction state: active and Overflow status: false +Checking transaction with ID: 67fb73196d900d5932e3a25e9ec65c85c2b04783a187d671b74ecec24c358af6 + Target metrics achieved! Transaction state: active and Overflow status: false +Next bitcoin block will be produced in 10 seconds... + Current Height 118 +Checking transaction with ID: f782cb1df71e25b249ee2bcabbd9dd08f782f15ac1132cadca665161771cad90 + Target metrics achieved! Transaction state: active and Overflow status: true + Target metrics achieved! Active delegation count: 2 + Target metrics achieved! Total delegation count: 2 +``` + +### Scenario 3: Submit on-demand unbonding transaction and ensure the system re-opens + +We generate a BTC unbonding transaction for a previous staking transaction +and we send it to the Staking API Service. The API saves it to a MongoDB +collection containing unbonding requests, and transitions the delegation state +to `unbonding_requested`. + +The unbonding pipeline periodically polls this collection, so subsequently it +retrieves it and submits it to the Covenant Signer to obtain signatures +until the Covenant Quorum (2 signatures for this demonstration, retrieved from +the global parameters) is reached. For our demonstration purposes, one Covenant +Signer instance represents all the Covenant entities. + +The Covenant Signer in turn forwards the transaction to the Bitcoind Offline +Wallet, which again for demonstration purposes hosts all the Covenant BTC keys. +The Bitcoind Offline Wallet signs the transaction which is then +returned to the unbonding pipeline. The unbonding pipeline finally submits it +to the Bitcoin regtest chain. + +When the unbonding transaction for the **active** staking transaction is included +in a BTC block, the following events will occur: +- Its stake will be subtracted both from the active TVL and the pending TVL +- The staking will be re-enabled again, as the active TVL is again below the + staking cap + +This reopens space for staking transactions as we're again below the staking +cap. We verify that this is the case by submitting a new staking transaction. + +After the unbonding transaction receives enough confirmations (2), the Staking +Indexer forwards it to the Staking API Service through the method described +before. The Staking API Service updates the delegation state to `unbonding`. + +After the unbonding time in BTC blocks elapses (3 blocks for this +demonstration, retrieved from the global parameters), the Staking Expiry Checker +processes the transaction and sends it to the corresponding RabbitMQ queue. +Finally, the Staking API Service retrieves the event and updates the +delegation state to `unbonded`. + +![plot](./diagrams/ondemand_onbonding.png) + +#### Step by Step Details: + +* At height 119, we unbond the BTC delegation with stake 2 BTC +* At height 120: + * The unbonding delegation status shifts to `unbonding` + * The active TVL falls to 8 BTC + * We send a new staking transaction with stake 1 BTC +* At height 122: + * The unbonding time elapses and the unbonding transaction status shifts to + `unbonded` + * The new staking transaction is active + +#### Sample Output + +```log +===== Scenario 3: Submit on-demand unbonding transaction and ensure the system re-opens ===== +Next bitcoin block will be produced in 10 seconds... + Current Height 119 +{ + "staking_tx_hash_hex": "87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b", + "unbonding_tx_hex": "02000000012bf47957d228e041d45a461105df48c70b553de6a96256510c4867fa0f84eb870000000000ffffffff010cc0eb0b00000000225120e3c89f82614aad82519620e92b797e52a0d27c7fb677f5f299f86642d95c863200000000", + "unbonding_tx_hash_hex": "97e5a577f673bbae945b3553d3c4a76c38db54492b754a1fbaeee559971bb943", + "staker_signed_signature_hex": "00c9a6590718afcee2b563b268e236eec6498a7b780b735d08f980dec4f397554621143e6c03dcecb2bd39d4e4f31fe54f73f69dfa1a60e64dbdf06a00a835f5" +} +null +Checking transaction with ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Transaction state: unbonding_requested and Overflow status: false +Next bitcoin block will be produced in 10 seconds... + Current Height 120 +Checking transaction with ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Transaction state: unbonding and Overflow status: false +get unbonding tx in mongo: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Active delegation count: 1 + Target metrics achieved! Total delegation count: 2 +Sign and send a staking transaction with stake: 1 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: d4707849cff714b6c346e4f7010b6848238e695742b477563f86aebdfff04368 +Next bitcoin block will be produced in 10 seconds... + Current Height 123 +Next bitcoin block will be produced in 10 seconds... + Current Height 124 +Checking transaction with ID: d4707849cff714b6c346e4f7010b6848238e695742b477563f86aebdfff04368 + Target metrics achieved! Transaction state: active and Overflow status: false +Next bitcoin block will be produced in 10 seconds... + Current Height 125 +Checking transaction with ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Transaction state: unbonded and Overflow status: false +``` + +### Scenario 4: Withdraw an on-demand unbonded transaction + +We generate a BTC withdraw transaction for the previously unbonded transaction +for which the unbonding time has elapsed and send it to the Bitcoin regtest +chain. Once the transaction receives enough confirmations, it's processed by the +Staking Indexer and placed in the corresponding RabbitMQ queue. + +The Staking API Service retrieves the event and updates the delegation state +to `withdrawn` in MongoDB. + +![plot](./diagrams/withdraw.png) + +* At height 123, we withdraw the unbonded staking transaction +* At height 125, the withdrawal transaction is processed by the system + +#### Sample Output + +```log +===== Scenario 4: Withdraw an on-demand unbonded transaction ===== +Next bitcoin block will be produced in 10 seconds... + Current Height 126 +Send the withdrawal transactions to bitcoind regtest +Withdrawal transaction submitted to bitcoind regtest with tx ID 8870447161148236e0abb47990dfd0bc52488661f78eb6f8761e1c8528ef1a59 +Next bitcoin block will be produced in 10 seconds... + Current Height 127 +Checking transaction with ID: 87eb840ffa67480c515662a9e63d550bc748df0511465ad441e028d25779f42b + Target metrics achieved! Transaction state: withdrawn and Overflow status: false + Target metrics achieved! Active delegation count: 2 + Target metrics achieved! Total delegation count: 3 +``` + +### Scenario 5: Ensure staking parameter update is enforced + +When the BTC height of the Bitcoin regtest network reaches 130, the second +(`v1`) version of the global parameters takes effect. As demonstrated before, +in the second version the minimum BTC stake is bumped to 2 BTC. Thus, a staking +transaction adhering to the previous minimum (1 BTC) but not to the new one +should be rejected. + +We send a staking transaction of 1.5 BTC to verify this behaviour. The Staking +Indexer is the entity that will filter out the transaction and increment the +corresponding Prometheus metric counter. + +#### Step by Step Details: + +* At height 130, we submit a staking transaction with stake 1.5 BTC +* At height 132, the Staking Indexer spots the staking transaction and marks + it as invalid + +#### Sample Output + +```log +===== Scenario 5: Ensure staking parameter update is enforced ===== +Generating enough bitcoin blocks to reach bitcoin height 130... + Reached target bitcoin height 130 +Current Active Global Parameters +{ + "version": 1, + "activation_height": 130, + "staking_cap": 2000000000, + "tag": "62627434", + "covenant_pks": [ + "0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7", + "03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0", + "02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8" + ], + "covenant_quorum": 2, + "unbonding_time": 3, + "unbonding_fee": 500, + "max_staking_amount": 2000000000, + "min_staking_amount": 200000000, + "max_staking_time": 5000, + "min_staking_time": 1, + "confirmation_depth": 2 +} +Sign and send a staking transaction with stake: 1 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: dc0b74a300ee70d78e7211e66bbed460c709f90cb24014be3968bf04503bc348 +Next bitcoin block will be produced in 10 seconds... + Current Height 131 +Next bitcoin block will be produced in 10 seconds... + Current Height 132 + Target metrics achieved! Invalid transaction count on Staking Indexer: 1 +``` + +### Scenario 6: Withdraw a transaction that has exceeded its timelock + +The Staking Expiry Checker service will periodically check all staking +transactions for expired timelock periods. Transactions that qualify are sent to +the Staking API Service through a dedicated queue. This results in the +transaction state being updated to `unbonded`. + +Similar to Scenario 4, we can now unbond this transaction. + +![plot](./diagrams/expiry.png) + +#### Step by Step Details: + +* At height 133, we create a staking transaction with stake 5 BTC and a short + timelock (2 blocks) +* At height 135, the timelock has already expired; the Staking Expiry Checker + identifies this and initiates a state update to `unbonded` +* At height 136, we withdraw this staking transaction +* At height 138, the withdrawal transaction is processed by the system + +#### Sample Output + +```log +===== Scenario 6: Withdraw a transaction that has exceeded its timelock ===== +Next bitcoin block will be produced in 10 seconds... + Current Height 133 +Sign and send a staking transaction with stake: 5 BTC and staking term: 2 blocks +Staking transaction submitted to bitcoind regtest with tx ID: af7d6872f18507fc40faa4fe12c03addbabd552726d4bd1eab7582787035b05a +Next bitcoin block will be produced in 10 seconds... + Current Height 134 + Target metrics achieved! Active TVL: 900000000 + Target metrics achieved! Total TVL: 1100000000 + Target metrics achieved! Unconfirmed TVL: 1400000000 +Next bitcoin block will be produced in 10 seconds... + Current Height 135 +Checking transaction with ID: af7d6872f18507fc40faa4fe12c03addbabd552726d4bd1eab7582787035b05a + Target metrics achieved! Transaction state: unbonded and Overflow status: false + Target metrics achieved! Active TVL: 1400000000 + Target metrics achieved! Total TVL: 1600000000 + Target metrics achieved! Unconfirmed TVL: 1400000000 +Next bitcoin block will be produced in 10 seconds... + Current Height 137 +Send the withdrawal transactions to bitcoind regtest +Withdrawal transaction submitted to bitcoind regtest with tx ID 12ce64b16c11bc90008a0224334427c96cece077ef22dadf0ea11cd8f483962f +Next bitcoin block will be produced in 10 seconds... + Current Height 138 +Checking transaction with ID: af7d6872f18507fc40faa4fe12c03addbabd552726d4bd1eab7582787035b05a + Target metrics achieved! Transaction state: withdrawn and Overflow status: false + Target metrics achieved! Active delegation count: 3 + Target metrics achieved! Total delegation count: 4 +``` + +### Scenario 7: Ensure the system can survive BTC forks + +A transaction submitted into an +[uncle block](https://coinmarketcap.com/academy/glossary/uncle-block-ommer-block) +should still be processed by our system once the re-org has been completed. + +We ensure that by manually creating a re-org of 1 BTC block. + +#### Step by Step Details: + +* At height 140, we send a staking transaction with 7 BTC stake +* At height 141, transaction record in block 141, then use btc cli to invalid this block, make it uncle. +* At height 141: + * The transaction is included in a BTC block + * The block is invalidated and a new one is created, resulting in a re-org of + 1 block +* At height 142, we verify that the transaction is still processed by the system + and marked as `active` + +#### Sample Output + +```log +===== Scenario 7: Ensure the system can survive BTC forks ===== +Generating enough bitcoin blocks to reach bitcoin height 140... + Reached target bitcoin height 140 +Sign and send a staking transaction with stake: 7 BTC and staking term: 1000 blocks +Staking transaction submitted to bitcoind regtest with tx ID: 49b4b101f343d81706e08bd4ece80a0ed97aa6bfe5204e16b2b980fc7ebb9300 +Next bitcoin block will be produced in 10 seconds... + Current Height 141 +The latest bitcoin block with hash 78a8a2ead6243ade099200a3814cd2d44ce0e37533720b72663cd2859153ae3f will now be invalidated... +Next bitcoin block will be produced in 10 seconds... + Current Height 141 +Next bitcoin block will be produced in 10 seconds... + Current Height 142 +Checking transaction with ID: 49b4b101f343d81706e08bd4ece80a0ed97aa6bfe5204e16b2b980fc7ebb9300 + Target metrics achieved! Transaction state: active and Overflow status: false +===== Congratulations! All tests are passed!!! ===== +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6570c80 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +DOCKER := $(shell which docker) +GIT_TOPLEVEL := $(shell git rev-parse --show-toplevel) + +build-bitcoindsim: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/contrib/images bitcoindsim + +build-bitcoindsim-signer: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/contrib/images bitcoindsim BITCOIN_CORE_VERSION=26.1 + +build-simple-staking: + cd $(GIT_TOPLEVEL)/submodules/simple-staking && docker build -t babylonchain/simple-staking . + +build-staking-api-service: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/staking-api-service build-docker + +build-staking-expiry-checker: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/staking-expiry-checker build-docker + +staking-indexer: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/staking-indexer build-docker + +cli-tools: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/cli-tools build-docker + +build-covenant-signer: + $(MAKE) -C $(GIT_TOPLEVEL)/submodules/covenant-signer build-docker + +build-deployment-btcstaking-phase1-bitcoind: build-bitcoindsim build-bitcoindsim-signer build-simple-staking build-staking-api-service build-staking-expiry-checker staking-indexer cli-tools build-covenant-signer + +start-deployment-btcstaking-phase1-bitcoind: stop-deployment-btcstaking-phase1-bitcoind build-deployment-btcstaking-phase1-bitcoind + ./pre-deployment.sh + docker compose -f artifacts/docker-compose.yml up -d + ./post-deployment.sh + +start-deployment-btcstaking-phase1-bitcoind-demo: start-deployment-btcstaking-phase1-bitcoind + ./btcstaking-demo.sh + +stop-deployment-btcstaking-phase1-bitcoind: + docker compose -f artifacts/docker-compose.yml down + rm -rf $(CURDIR)/.testnets diff --git a/README.md b/README.md index 691914b..b525a4d 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# babylon-btcstaking-phase-1-demo \ No newline at end of file +# Babylon BTC Staking Phase-1 demo + +This repository contains all the necessary artifacts and instructions to set up +and run Babylon's Phase-1 BTC Staking system locally. + +More details about the system can be found [here](DEPLOYMENT.md). + +## Prerequisites + +1. Install Docker Desktop + + All components are executed as Docker containers on the local machine, so a + local Docker installation is required. Depending on your operating system, + you can find relevant instructions [here](https://docs.docker.com/desktop/). + +2. Install `make` + + Required to build the service binaries. One tutorial that can be followed + is [this](https://sp21.datastructur.es/materials/guides/make-install.html). + +3. Clone the repository and initialize git submodules + + The aforementioned components are included in the repo as git submodules, so + they need to be initialized accordingly. + + ```shell + git clone git@github.com:babylonchain/babylon-timestamping-demo.git + git submodule init && git submodule update + ``` + +## Repo structure + +The repository follows the below structure: + +```shell +├── artifacts +│ ├── docker-compose.yml +│ ├── ... +├── Makefile +├── post-deployment.sh +└── pre-deployment.sh +``` + +## Perform system operations;w + +To start the system **along with executing an +[additional post-deployment script](DEPLOYMENT.md#inspecting-the-btc-staking-phase-1-system-demo) +that will showcase the lifecycle of Staking requests inside the Phase-1 +system**, execute the following: + +```shell +make start-deployment-btcstaking-phase1-bitcoind-demo +``` + +Alternatively, to just start the system: + +```shell +make start-deployment-btcstaking-phase1-bitcoind +``` + +To stop the system: + +```shell +make stop-deployment-btcstaking-phase1-bitcoind diff --git a/artifacts/covenant-signer-config.toml b/artifacts/covenant-signer-config.toml new file mode 100644 index 0000000..3e0082d --- /dev/null +++ b/artifacts/covenant-signer-config.toml @@ -0,0 +1,44 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# There are two btc related configs +# 1. [btc-config] is config for btc full node which should have transaction indexing +# enabled. This node should be synced and can be open to the public. +# 2. [btc-signer-config] is config for bitcoind daemon which should have only +# wallet functionality, it should run in separate network. This bitcoind instance +# will be used to sign psbt's +[btc-config] +# Btc node host +host = "bitcoindsim:18443" +# Btc node user +user = "rpcuser" +# Btc node password +pass = "rpcpass" +# Btc network (testnet3|mainnet|regtest|simnet|signet) +network = "regtest" + +[btc-signer-config] +# Btc node host +host = "bitcoindsim-signer:18443/wallet/covenant-signer" +# Btc node user +user = "rpcuser" +# Btc node password +pass = "rpcpass" +# Btc network (testnet3|mainnet|regtest|simnet|signet) +network = "regtest" + +[server-config] +# The address to listen on +host = "0.0.0.0" + +# The port to listen on +port = 9791 + +# Read timeout in seconds +read-timeout = 15 + +# Write timeout in seconds +write-timeout = 15 + +# Idle timeout in seconds +idle-timeout = 120 \ No newline at end of file diff --git a/artifacts/covenant-signer.dat b/artifacts/covenant-signer.dat new file mode 100644 index 0000000..8c95c32 Binary files /dev/null and b/artifacts/covenant-signer.dat differ diff --git a/artifacts/docker-compose.yml b/artifacts/docker-compose.yml new file mode 100644 index 0000000..7dc57a1 --- /dev/null +++ b/artifacts/docker-compose.yml @@ -0,0 +1,174 @@ +version: "3" + +services: + bitcoindsim: + image: babylonchain/bitcoindsim:latest + platform: linux/amd64 + container_name: bitcoindsim + environment: + - RPC_PORT=18443 + - RPC_USER=rpcuser + - RPC_PASS=rpcpass + - WALLET_PASS=walletpass + - WALLET_NAME=default + - BTCSTAKER_WALLET_NAME=btcstaker + - BTCSTAKER_WALLET_ADDR_COUNT=3 + - GENERATE_INTERVAL_SECS=10 + ports: + - "18443:18443" + volumes: + - ../.testnets/bitcoin:/bitcoindsim/.bitcoin:Z + networks: + - phase-1 + + bitcoindsim-signer: + image: babylonchain/bitcoindsim:26.1 + platform: linux/amd64 + container_name: bitcoindsim-signer + environment: + - RPC_PORT=18443 + - RPC_USER=rpcuser + - RPC_PASS=rpcpass + - GENERATE_STAKER_WALLET=false + - GENERATE_INTERVAL_SECS=10 + volumes: + - ../.testnets/bitcoin-signer:/bitcoindsim/.bitcoin:Z + networks: + - phase-1 + + staking-indexer: + image: babylonchain/staking-indexer:latest + container_name: staking-indexer + environment: + - CONFIG=/home/staking-indexer/.sid/sid.conf + volumes: + - ../.testnets/staking-indexer/data:/home/staking-indexer/.sid/data:Z + - ../.testnets/staking-indexer/logs:/home/staking-indexer/.sid/logs:Z + - ../.testnets/staking-indexer/sid.conf:/home/staking-indexer/.sid/sid.conf:Z + - ../.testnets/global-params.json:/home/staking-indexer/.sid/global-params.json:Z + ports: + - "2112:2112" + depends_on: + - rabbitmq + - bitcoindsim + entrypoint: ["/bin/sh", "-c", "/bin/sid start"] + networks: + - phase-1 + restart: unless-stopped + + staking-api-service: + image: babylonchain/staking-api-service:latest + container_name: staking-api-service + ports: + - "80:8090" + environment: + - CONFIG=/home/staking-api-service/config.yml + volumes: + - ../.testnets/global-params.json:/home/staking-api-service/global-params.json:Z + - ../.testnets/finality-providers.json:/home/staking-api-service/finality-providers.json:Z + - ../.testnets/staking-api-service/staking-api-service-config.yml:/home/staking-api-service/config.yml:Z + depends_on: + - rabbitmq + - mongodb + networks: + - phase-1 + restart: unless-stopped + + staking-expiry-checker: + image: babylonchain/staking-expiry-checker:latest + container_name: staking-expiry-checker + environment: + - CONFIG=/home/staking-expiry-checker/config.yml + depends_on: + - mongodb + - rabbitmq + volumes: + - ../.testnets/staking-expiry-checker/staking-expiry-checker-config.yml:/home/staking-expiry-checker/config.yml:Z + networks: + - phase-1 + restart: unless-stopped + + simple-staking: + container_name: simple-staking + image: babylonchain/simple-staking:latest + environment: + - APP_NEXT_PUBLIC_MEMPOOL_API=https://babylon.mempool.space + - APP_NEXT_PUBLIC_API_URL=http://staking-api-service + restart: always + depends_on: + - staking-api-service + ports: + - 3000:3000 + networks: + - phase-1 + + unbonding-pipeline: + container_name: unbonding-pipeline + image: babylonchain/cli-tools:latest + restart: unless-stopped + depends_on: + - mongodb + - bitcoindsim + volumes: + - ../.testnets/global-params.json:/home/cli-tools/.tools/global-params.json:Z + - ../.testnets/unbonding-pipeline/unbonding-pipeline-config.toml:/home/cli-tools/.tools/config.toml + entrypoint: | + /bin/sh -c " + while true + do + cli-tools run-unbonding-pipeline --config /home/cli-tools/.tools/config.toml + sleep 10 + done + " + networks: + - phase-1 + + mongodb: + image: mongo:latest + container_name: mongodb + hostname: mongodb + ports: + - "27017:27017" + volumes: + - ../.testnets/mongo/init-mongo.sh:/init-mongo.sh:Z + entrypoint: [ "/init-mongo.sh" ] + networks: + - phase-1 + + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + ports: + - "5672:5672" # AMQP protocol port + - "15672:15672" # Management UI port + environment: + RABBITMQ_DEFAULT_USER: user + RABBITMQ_DEFAULT_PASS: password + volumes: + - "../.testnets/rabbitmq_data:/var/lib/rabbitmq" + networks: + - phase-1 + + covenant-signer: + image: babylonchain/covenant-signer:latest + container_name: covenant-signer + ports: + - "9791:9791" + environment: + - CONFIG= /home/covenant-signer/config.yml + entrypoint: ["/bin/sh", "-c", "/bin/covenant-signer start"] + depends_on: + - bitcoindsim + volumes: + - ../.testnets/covenant-signer/covenant-signer-config.toml:/home/covenant-signer/.signer/config.toml:Z + - ../.testnets/global-params.json:/home/covenant-signer/.signer/global-params.json:Z + networks: + - phase-1 + restart: unless-stopped +networks: + phase-1: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.15.0/25 diff --git a/artifacts/finality-providers.json b/artifacts/finality-providers.json new file mode 100644 index 0000000..684929d --- /dev/null +++ b/artifacts/finality-providers.json @@ -0,0 +1,48 @@ +{ + "finality_providers": [ + { + "description": { + "moniker": "Babylon Foundation 0", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": "0.050000000000000000", + "btc_pk": "03d5a0bb72d71993e435d6c5a70e2aa4db500a62cfaae33c56050deefee64ec0" + }, + { + "description": { + "moniker": "Babylon Foundation 1", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": "0.060000000000000000", + "btc_pk": "063deb187a4bf11c114cf825a4726e4c2c35fea5c4c44a20ff08a30a752ec7e0" + }, + { + "description": { + "moniker": "Babylon Foundation 2", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": "0.080000000000000000", + "btc_pk": "094f5861be4128861d69ea4b66a5f974943f100f55400bf26f5cce124b4c9af7" + }, + { + "description": { + "moniker": "Babylon Foundation 3", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": "0.090000000000000000", + "btc_pk": "0d2f9728abc45c0cdeefdd73f52a0e0102470e35fb689fc5bc681959a61b021f" + } + ] +} \ No newline at end of file diff --git a/artifacts/global-params.json b/artifacts/global-params.json new file mode 100644 index 0000000..0de1ef3 --- /dev/null +++ b/artifacts/global-params.json @@ -0,0 +1,42 @@ +{ + "versions": [ + { + "version": 0, + "activation_height": 115, + "staking_cap": 1000000000, + "tag": "62627434", + "covenant_pks": [ + "0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7", + "03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0", + "02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8" + ], + "covenant_quorum": 2, + "unbonding_fee": 500, + "unbonding_time": 3, + "max_staking_amount": 1000000000, + "min_staking_amount": 100000000, + "max_staking_time": 1000, + "min_staking_time": 10, + "confirmation_depth": 2 + }, + { + "version": 1, + "activation_height": 130, + "staking_cap": 2000000000, + "tag": "62627434", + "covenant_pks": [ + "0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7", + "03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0", + "02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8" + ], + "covenant_quorum": 2, + "unbonding_fee": 500, + "unbonding_time": 3, + "max_staking_amount": 2000000000, + "min_staking_amount": 200000000, + "max_staking_time": 5000, + "min_staking_time": 1, + "confirmation_depth": 2 + } + ] +} \ No newline at end of file diff --git a/artifacts/init-mongo.sh b/artifacts/init-mongo.sh new file mode 100755 index 0000000..ffea249 --- /dev/null +++ b/artifacts/init-mongo.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Start MongoDB service in the background +mongod --replSet "RS" --bind_ip_all & + +# Wait for MongoDB to start +sleep 10 + +# Initiate the replica set +mongosh --eval "rs.initiate({_id: 'RS', members: [{ _id: 0, host: 'mongodb:27017' }]})" + +# Wait for replica set to initiate +sleep 5 + +# Create the necessary indexes +mongosh --eval " +db = db.getSiblingDB('staking-api-service'); +db.unbonding_queue.createIndex({'unbonding_tx_hash_hex': 1}, {unique: true}); +db.timelock_queue.createIndex({'expire_height': 1}, {unique: false}); +db.delegations.createIndex({'staker_pk_hex': 1, 'staking_tx.start_height': -1}, {unique: false}); +" + +# Keep the container running +tail -f /dev/null diff --git a/artifacts/sid.conf b/artifacts/sid.conf new file mode 100644 index 0000000..ec04861 --- /dev/null +++ b/artifacts/sid.conf @@ -0,0 +1,80 @@ +[Application Options] +; Logging level for all subsystems {trace, debug, info, warn, error, fatal} +LogLevel = debug + +; Bitcoin network to run on {mainnet, regtest, testnet, signet} +BitcoinNetwork = regtest + +[btcconfig] +; The daemon's rpc listening address. +RPCHost = bitcoindsim:18443 + +; Username for RPC connections. +RPCUser = rpcuser + +; Password for RPC connections. +RPCPass = rpcpass + +; The maximum number of peers staker will choose from the backend node to retrieve pruned blocks from. This only applies to pruned nodes. +PrunedNodeMaxPeers = 0 + +; The interval that will be used to poll bitcoind for new blocks. Only used if rpcpolling is true. +BlockPollingInterval = 5s + +; The interval that will be used to poll bitcoind for new tx. Only used if rpcpolling is true. +TxPollingInterval = 10s + +; size of the Bitcoin blocks cache +BlockCacheSize = 20971520 + +MaxRetryTimes = 5 + +RetryInterval = 0.5s + +[dbconfig] +; The directory path in which the database file should be stored. +DBPath = /home/staking-indexer/.sid/data + +; The name of the database file. +DBFileName = bbolt.db + +; Prevents the database from syncing its freelist to disk, resulting in improved performance at the expense of increased startup time. +NoFreelistSync = true + +; Specifies if a Bolt based database backend should be automatically compacted on startup (if the minimum age of the database file is reached). This will require additional disk space for the compacted copy of the database but will result in an overall lower database size after the compaction. +AutoCompact = false + +; Specifies the minimum time that must have passed since a bolt database file was last compacted for the compaction to be considered again. +AutoCompactMinAge = 168h0m0s + +; Specifies the timeout value to use when opening the wallet database. +DBTimeout = 1m0s + +[queueconfig] +; the user name of the queue +User = user + +; the password of the queue +Password = password + +; the url of the queue +Url = rabbitmq:5672 + +; the process timeout of the queue +ProcessingTimeout = 5s + +; the maximum number of times a message will be retried +MsgMaxRetryAttempts = 10 + +; the time a message will be hold in delay queue before being sent to main queue again +ReQueueDelayTime = 300s + +; the rabbitmq queue type, either classic or quorum +QueueType = quorum + +[metricsconfig] +; IP of the Prometheus server +Host = 0.0.0.0 + +; Port of the Prometheus server +Port = 2112 \ No newline at end of file diff --git a/artifacts/staking-api-service-config.yml b/artifacts/staking-api-service-config.yml new file mode 100644 index 0000000..9d1f9af --- /dev/null +++ b/artifacts/staking-api-service-config.yml @@ -0,0 +1,26 @@ +server: + host: 0.0.0.0 + port: 8090 + write-timeout: 60s + read-timeout: 60s + idle-timeout: 60s + allowed-origins: [ "*" ] + log-level: debug + btc-net: "regtest" +db: + address: "mongodb://mongodb:27017" + db-name: staking-api-service + max-pagination-limit: 10 + db-batch-size-limit: 100 + logical-shard-count: 10 +queue: + queue_user: user # can be replaced by values in .env file + queue_password: password + url: "rabbitmq:5672" + processing_timeout: 30 + msg_max_retry_attempts: 10 + requeue_delay_time: 300 + queue_type: quorum +metrics: + host: 0.0.0.0 + port: 2112 diff --git a/artifacts/staking-expiry-checker-config.yml b/artifacts/staking-expiry-checker-config.yml new file mode 100644 index 0000000..3d72fbf --- /dev/null +++ b/artifacts/staking-expiry-checker-config.yml @@ -0,0 +1,23 @@ +poller: + interval: 5s + log-level: debug +db: + address: "mongodb://mongodb:27017" + db-name: staking-api-service +btc: + endpoint: bitcoindsim:18443 # use port 18332 for testnet, 8332 for mainnet + disable-tls: true + net-params: regtest + rpc-user: rpcuser + rpc-pass: rpcpass +queue: + queue_user: user # can be replaced by values in .env file + queue_password: password + url: "rabbitmq:5672" + processing_timeout: 5 # 5 second + msg_max_retry_attempts: 10 + requeue_delay_time: 300 + queue_type: quorum +metrics: + host: 0.0.0.0 + port: 2112 diff --git a/artifacts/unbonding-pipeline-config.toml b/artifacts/unbonding-pipeline-config.toml new file mode 100644 index 0000000..3715e32 --- /dev/null +++ b/artifacts/unbonding-pipeline-config.toml @@ -0,0 +1,31 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +[db-config] +# The network chain ID +db-name = "staking-api-service" +# The keyring's backend, where the keys are stored (os|file|kwallet|pass|test|memory) +address = "mongodb://mongodb:27017" + +[btc-config] +# Btc node host +host = "bitcoindsim:18443" +# Btc node user +user = "rpcuser" +# Btc node password +pass = "rpcpass" +# Btc network (testnet3|mainnet|regtest|simnet|signet) +network = "regtest" + +[params-config] +covenant_public_keys = ["0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7", "03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0", "02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8"] +# The quorum of the covenants required to sign the transaction +covenant_quorum = 2 +# The magic bytes of the network +magic_bytes = "62627434" + +[remote-signer-config] +# The list of signer urls in the format http://covenant_pk@signer_host:port +urls = ["http://0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7@covenant-signer:9791", "http://03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0@covenant-signer:9791", "http://02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8@covenant-signer:9791"] +# The timeout of each request to the remote signing server +timeout = 10 \ No newline at end of file diff --git a/btcstaking-demo.sh b/btcstaking-demo.sh new file mode 100755 index 0000000..6cbc471 --- /dev/null +++ b/btcstaking-demo.sh @@ -0,0 +1,402 @@ +#!/bin/sh +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +NC='\033[0m' # No Color + +BTCUSER="rpcuser" +BTCPASSWORD="rpcpass" +BTCWALLET="btcstaker" +BTCWALLETPASS="walletpass" +BTCCMD="bitcoin-cli -regtest -rpcuser=$BTCUSER -rpcpassword=$BTCPASSWORD -rpcwallet=$BTCWALLET" +BTCCLI="docker exec bitcoindsim /bin/sh -c " +LOCATE=$(dirname "$(realpath "$0")") +DIR="$LOCATE/.testnets/demo" + +# The first transaction will be used to test the withdraw path + +init() { + echo "Wait a bit for bitcoind regtest network to initialize.." + # sleep 25 + mkdir -p $DIR + echo "$YELLOW Start End to End Test $NC" +} + +create_staking_tx() { + staking_amount=$1 + staking_time=$2 + folder=$DIR/$staking_amount + staker_pk=$($BTCCLI "$BTCCMD listunspent" | jq -r '.[0].desc | split("]") | .[1] | split(")") | .[0] | .[2:]') + unsigned_staking_tx_hex=$(docker exec unbonding-pipeline /bin/sh -c "cli-tools create-phase1-staking-tx \ + --magic-bytes 62627434 \ + --staker-pk $staker_pk \ + --staking-amount $staking_amount \ + --staking-time $staking_time \ + --covenant-committee-pks 0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7 \ + --covenant-committee-pks 03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0 \ + --covenant-committee-pks 02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8 \ + --covenant-quorum 2 \ + --network regtest \ + --finality-provider-pk 03d5a0bb72d71993e435d6c5a70e2aa4db500a62cfaae33c56050deefee64ec0" | jq .staking_tx_hex) + + # echo "Sign the staking transactions through bitcoind wallet" + unsigned_staking_tx_hex=$($BTCCLI "$BTCCMD \ + fundrawtransaction $unsigned_staking_tx_hex \ + '{\"feeRate\": 0.00001, \"lockUnspents\": true}' " | jq .hex) + + # Unlock the wallet + $BTCCLI "$BTCCMD walletpassphrase $BTCWALLETPASS 600" + + # echo "Sign the staking transactions through the Bitcoin wallet connection" + staking_tx_hex=$($BTCCLI "$BTCCMD signrawtransactionwithwallet $unsigned_staking_tx_hex" | jq '.hex') + # echo "Send the staking transactions to bitcoind regtest" + staking_txid=$($BTCCLI "$BTCCMD sendrawtransaction $staking_tx_hex") + mkdir -p $folder + echo "$staking_tx_hex" > $folder/tx_hex + BTC=$(($staking_amount / 100000000)) + echo "Sign and send a staking transaction with stake: $BTC BTC and staking term: $staking_time blocks" + echo "Staking transaction submitted to bitcoind regtest with tx ID: $BLUE $staking_txid $NC" + echo "$staking_txid" > $folder/tx_id +} + +create_unbonding_tx() { + tx_hex=$1 + unbonding_time=$2 + + # Create the payload through a helper CLI on the unbonding-pipeline + unbonding_api_payload=$(docker exec unbonding-pipeline /bin/sh -c "cli-tools create-phase1-unbonding-request \ + --magic-bytes 62627434 \ + --covenant-committee-pks 0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7 \ + --covenant-committee-pks 03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0 \ + --covenant-committee-pks 02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8 \ + --covenant-quorum 2 \ + --network regtest \ + --unbonding-fee 500 \ + --unbonding-time $unbonding_time \ + --staker-wallet-address-host bitcoindsim:18443/wallet/btcstaker \ + --staker-wallet-passphrase $BTCWALLETPASS \ + --staker-wallet-rpc-user $BTCUSER \ + --staker-wallet-rpc-pass $BTCPASSWORD \ + --staking-tx-hex $tx_hex") + # Submit the payload to the Staking API Service + echo "$unbonding_api_payload" + curl -sSL localhost:80/v1/unbonding -d "$unbonding_api_payload" + echo "" +} + +create_withdraw_tx() { + tx_hex=$1 + withdraw_btc_addr=$(docker exec bitcoindsim /bin/sh -c "bitcoin-cli -regtest -rpcuser=$BTCUSER -rpcpassword=$BTCPASSWORD -rpcwallet=$BTCWALLET listunspent" \ + | jq -r '.[0].address') + + withdrawal_tx_hex=$(docker exec unbonding-pipeline /bin/sh -c "cli-tools create-phase1-withdaw-request \ + --magic-bytes 62627434 \ + --covenant-committee-pks 0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7 \ + --covenant-committee-pks 03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0 \ + --covenant-committee-pks 02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8 \ + --covenant-quorum 2 \ + --network regtest \ + --withdraw-tx-fee 1000 \ + --withdraw-tx-destination $withdraw_btc_addr \ + --staker-wallet-address-host bitcoindsim:18443/wallet/btcstaker \ + --staker-wallet-passphrase $BTCWALLETPASS \ + --staker-wallet-rpc-user $BTCUSER \ + --staker-wallet-rpc-pass $BTCPASSWORD \ + --staking-tx-hex $tx_hex" | jq -r .withdraw_tx_hex) + + echo "Send the withdrawal transactions to bitcoind regtest" + withdrawal_txid=$(docker exec bitcoindsim /bin/sh -c "bitcoin-cli -regtest -rpcuser=$BTCUSER -rpcpassword=$BTCPASSWORD -rpcwallet=$BTCWALLET \ + sendrawtransaction $withdrawal_tx_hex") + echo "Withdrawal transaction submitted to bitcoind regtest with tx ID $BLUE $withdrawal_txid $NC" +} + +create_unbonding_withdraw_tx() { + tx_hex=$1 + unbonding_hex=$2 + unbonding_time=$3 + + withdraw_btc_addr=$(docker exec bitcoindsim /bin/sh -c "bitcoin-cli -regtest -rpcuser=$BTCUSER -rpcpassword=$BTCPASSWORD -rpcwallet=$BTCWALLET listunspent" \ + | jq -r '.[0].address') + + withdrawal_tx_hex=$(docker exec unbonding-pipeline /bin/sh -c "cli-tools create-phase1-withdaw-request \ + --magic-bytes 62627434 \ + --covenant-committee-pks 0342301c4fdb5b1ab27a80a04d95c782f720874265889412a80d270feeb456f1f7 \ + --covenant-committee-pks 03a4d2276a2a09f0e14d6a74901fec0aab3d1edf0dd22a690260acca48f5d5b3c0 \ + --covenant-committee-pks 02707f3d6bf2334ecb7c336fc7babd400afa9132a34f84406b28865d06e0ba81e8 \ + --covenant-quorum 2 \ + --network regtest \ + --withdraw-tx-fee 1000 \ + --withdraw-tx-destination $withdraw_btc_addr \ + --staker-wallet-address-host bitcoindsim:18443/wallet/btcstaker \ + --staker-wallet-passphrase $BTCWALLETPASS \ + --staker-wallet-rpc-user $BTCUSER \ + --staker-wallet-rpc-pass $BTCPASSWORD \ + --staking-tx-hex $tx_hex \ + --unbonding-tx-hex $unbonding_hex \ + --unbonding-time $unbonding_time" | jq -r .withdraw_tx_hex) + + echo "Send the withdrawal transactions to bitcoind regtest" + withdrawal_txid=$(docker exec bitcoindsim /bin/sh -c "bitcoin-cli -regtest -rpcuser=$BTCUSER -rpcpassword=$BTCPASSWORD -rpcwallet=$BTCWALLET \ + sendrawtransaction $withdrawal_tx_hex") + echo "Withdrawal transaction submitted to bitcoind regtest with tx ID $BLUE $withdrawal_txid $NC" +} + +current_info() { + height=$($BTCCLI "$BTCCMD getblockcount") + echo "$BLUE Current Height $height $NC" +} + +check_mongoDB_info() { + txid=$1 + echo "Checking transaction with ID:$BLUE $1 $NC" + target_stats=$2 + target_overflow=$3 + while true; do + state=$(docker exec mongodb /bin/sh -c "mongosh staking-api-service --eval 'JSON.stringify(db.delegations.find({\"_id\": \"$txid\"}).toArray(), null, 2)'" \ + | jq -r .[].state) + + if [ "$state" = "$target_stats" ]; then + is_overflow=$(docker exec mongodb /bin/sh -c "mongosh staking-api-service --eval 'JSON.stringify(db.delegations.find({\"_id\": \"$txid\"}).toArray(), null, 2)'" \ + | jq -r .[].is_overflow) + if [ "$is_overflow" = "$target_overflow" ]; then + echo "$GREEN Target metrics achieved! Transaction state: $state and Overflow status: $target_overflow $NC" + break + fi + echo "$RED Target metrics not met; overflow status: $is_overflow, while expected state is $target_overflow $NC" + exit 1 + fi + sleep 1 + done +} + +get_unbonding_mongoDB_info() { + id=$1 + txid=$2 + echo "get unbonding tx in mongo: $BLUE $txid $NC" + unbonding_tx_hex=$(docker exec mongodb /bin/sh -c "mongosh staking-api-service --eval 'JSON.stringify(db.delegations.find({\"_id\": \"$txid\"}).toArray(), null, 2)'" \ + | jq -r .[].unbonding_tx.tx_hex) + echo $unbonding_tx_hex > $DIR/$1/unbonding_hex +} + +check_staking_status() { + expect_active_delegations=$1 + expect_total_delegations=$2 + res=$(curl -s 'localhost/v1/stats') + active_delegations=$(echo $res | jq -r .data.active_delegations ) + total_delegations=$(echo $res | jq -r .data.total_delegations ) + if [ "$active_delegations" -eq "$expect_active_delegations" ]; then + echo "$GREEN Target metrics achieved! Active delegation count: $active_delegations $NC" + else + echo "$RED Target metrics not met; Active delegation count: $active_delegations $NC" + exit 1 + fi + + if [ "$total_delegations" -eq "$expect_total_delegations" ]; then + echo "$GREEN Target metrics achieved! Total delegation count: $total_delegations $NC" + else + echo "$RED Target metrics not met; Total delegation count: $total_delegations $NC" + exit 1 + fi +} + +check_staking_tvl() { + expect_active_tvl=$1 + expect_total_tvl=$2 + expect_unconfirmed_tvl=$3 + res=$(curl -s 'localhost/v1/stats') + active_tvl=$(echo $res | jq -r .data.active_tvl ) + total_tvl=$(echo $res | jq -r .data.total_tvl ) + unconfirmed_tvl=$(echo $res | jq -r .data.unconfirmed_tvl ) + if [ "$active_tvl" -eq "$expect_active_tvl" ]; then + echo "$GREEN Target metrics achieved! Active TVL: $active_tvl $NC" + else + echo "$RED Target metrics not met; Active TVL: $active_tvl $NC" + exit 1 + fi + + if [ "$total_tvl" -eq "$expect_total_tvl" ]; then + echo "$GREEN Target metrics achieved! Total TVL: $total_tvl $NC" + else + echo "$RED Target metrics not met; Total TVL: $total_tvl $NC" + exit 1 + fi + + if [ "$unconfirmed_tvl" -eq "$expect_unconfirmed_tvl" ]; then + echo "$GREEN Target metrics achieved! Unconfirmed TVL: $unconfirmed_tvl $NC" + else + echo "$RED Target metrics not met; Unconfirmed TVL: $unconfirmed_tvl $NC" + exit 1 + fi +} + +check_indexer_metrics() { + field=$1 + type=$2 + expect_count=$3 + while true; do + count=$(curl -s localhost:2112/metrics | grep "$field" | grep "$type" | grep -v '#' | cut -d' ' -f2) + if [ $count -eq $expect_count ]; then + echo "$GREEN Target metrics achieved! Invalid transaction count on Staking Indexer: $count $NC" + break + else + sleep 2 + fi + done +} + +move_next_block() { + wait=10 + echo "Next bitcoin block will be produced in $wait seconds..." + sleep 10 + current_info +} + +move_to_block() { + echo "Generating enough bitcoin blocks to reach bitcoin height $1..." + target_block=$1 + while true; do + height=$($BTCCLI "$BTCCMD getblockcount") + if [ "$height" -eq "$target_block" ]; then + echo "$BLUE Reached target bitcoin height $target_block $NC" + break + else + sleep 3 + fi + done +} + +print_global_parameters() { + ver=$1 + echo "Current Active Global Parameters" + curl -s --location '0.0.0.0:80/v1/global-params' | jq --arg version "$ver" '.data.versions[] | select(.version == ($version | tonumber))' +} + +invalid_block() { + # Get the latest block hash + latest_block_hash=$($BTCCLI "$BTCCMD getbestblockhash") + echo "The latest bitcoin block with hash $latest_block_hash will now be invalidated..." + + # Invalidate the latest block + $BTCCLI "$BTCCMD invalidateblock $latest_block_hash" + transactions=$($BTCCLI "$BTCCMD getblock $latest_block_hash" | jq -r '.tx[]') +} + +init +print_global_parameters 0 +current_info + +echo "" +echo "$YELLOW===== ### Scenario 1: No transactions are processed by the system before the first activation height ===== $NC" +create_staking_tx 600000000 1000 # 6 BTC +move_next_block +move_next_block +check_indexer_metrics "si_total_staking_txs" "" 0 +check_staking_status 0 0 + +echo "" +echo "$YELLOW===== Scenario 2: Submit Staking Transactions and fill the staking cap ===== $NC" +move_to_block 115 +# At height 115, we send 2 txs with stake 2 BTC, 8 BTC +create_staking_tx 200000000 1000 # 2 BTC +create_staking_tx 800000000 1000 # 8 BTC + +# At height 116, we send 1 tx with stake 1 BTC +move_next_block +create_staking_tx 100000000 1000 # 1 BTC + +# At height 117, both the txs are active with total active stake 10 BTC +move_next_block +check_mongoDB_info $(cat $DIR/200000000/tx_id) "active" "false" +check_mongoDB_info $(cat $DIR/800000000/tx_id) "active" "false" + +# At height 118, this tx will be mark as overflow due to TVL exceeding the staking cap +move_next_block +check_mongoDB_info $(cat $DIR/100000000/tx_id) "active" "true" +check_staking_status 2 2 + +# At height 119, we unbond the 2 BTC delegation with unbonding_time 2 +echo "" +echo "$YELLOW===== Scenario 3: Submit on-demand unbonding transaction and ensure the system re-opens ===== $NC" +move_next_block +create_unbonding_tx $(cat $DIR/200000000/tx_hex) 3 +check_mongoDB_info $(cat $DIR/200000000/tx_id) "unbonding_requested" "false" + +# At height 121, we create 1 tx with stake 1 BTC, mongo stats change 2 BTC's tx to unbonding +move_next_block +check_mongoDB_info $(cat $DIR/200000000/tx_id) "unbonding" "false" +get_unbonding_mongoDB_info 200000000 $(cat $DIR/200000000/tx_id) +check_staking_status 1 2 +create_staking_tx 100000000 1000 # 1 BTC + +# At height 124, staked 1 tx should be active and not overflow +move_next_block +move_next_block +check_mongoDB_info $(cat $DIR/100000000/tx_id) "active" "false" + +# At height 125, mongo stats change 2 BTC tx to unbonded +move_next_block +check_mongoDB_info $(cat $DIR/200000000/tx_id) "unbonded" "false" + +echo "" +echo "$YELLOW===== Scenario 4: Withdraw an on-demand unbonded transaction ===== $NC" +# At height 126, we withdraw unbonded tx +move_next_block +create_unbonding_withdraw_tx $(cat $DIR/200000000/tx_hex) $(cat $DIR/200000000/unbonding_hex) 3 + +# At height 127, mongo stats change to withdrawn +move_next_block +check_mongoDB_info $(cat $DIR/200000000/tx_id) "withdrawn" "false" +check_staking_status 2 3 + +# At height 130 +echo "" +echo "$YELLOW===== Scenario 5: Ensure staking parameter update is enforced ===== $NC" +move_to_block 130 +print_global_parameters 1 +create_staking_tx 150000000 1000 # 1.5 BTC + +# At height 132, indexer should find out this invalid transaction and record in the metrcis +move_next_block +move_next_block +check_indexer_metrics "si_invalid_txs_counter" "unconfirmed_staking_transaction" 1 + +# At height 133 +echo "" +echo "$YELLOW===== Scenario 6: Withdraw a transaction that has exceeded its timelock ===== $NC" +move_next_block +create_staking_tx 500000000 2 # 5 BTC with 2 block timelock + +# At height 134, indexer should get the unconfirmed tx and send btc info to api service +move_next_block +sleep 3 # process into queue +check_staking_tvl 900000000 1100000000 1400000000 + +# At height 135 we wait for block timelock expired and pick up by expiry service +move_next_block +check_mongoDB_info $(cat $DIR/500000000/tx_id) "unbonded" "false" +check_staking_tvl 1400000000 1600000000 1400000000 + +# At height 137, we withdraw this tx +move_next_block +create_withdraw_tx $(cat $DIR/500000000/tx_hex) + +# At height 138, mongo stats change to withdrawn +move_next_block +check_mongoDB_info $(cat $DIR/500000000/tx_id) "withdrawn" "false" +check_staking_status 3 4 + +# At height 140 +echo "" +echo "$YELLOW===== Scenario 7: Ensure the system can survive BTC forks ===== $NC" +move_to_block 140 +create_staking_tx 700000000 1000 # 7 BTC with 1000 block timelock +move_next_block +sleep 3 +invalid_block +move_next_block +move_next_block +check_mongoDB_info $(cat $DIR/700000000/tx_id) "active" "false" + +echo "$YELLOW===== Congratulations! All tests are passed!!! ===== $NC" diff --git a/diagrams/expiry.png b/diagrams/expiry.png new file mode 100644 index 0000000..5abf3d9 Binary files /dev/null and b/diagrams/expiry.png differ diff --git a/diagrams/ondemand_onbonding.png b/diagrams/ondemand_onbonding.png new file mode 100644 index 0000000..fb73f23 Binary files /dev/null and b/diagrams/ondemand_onbonding.png differ diff --git a/diagrams/staking.png b/diagrams/staking.png new file mode 100644 index 0000000..e1c3b1e Binary files /dev/null and b/diagrams/staking.png differ diff --git a/diagrams/withdraw.png b/diagrams/withdraw.png new file mode 100644 index 0000000..445fcc2 Binary files /dev/null and b/diagrams/withdraw.png differ diff --git a/post-deployment.sh b/post-deployment.sh new file mode 100755 index 0000000..4194c30 --- /dev/null +++ b/post-deployment.sh @@ -0,0 +1,22 @@ +#!/bin/bash +RPCUSER="rpcuser" +RPCPASSWORD="rpcpass" +RPCWALLET="covenant-signer" +RPCWALLETPASS="walletpass" + +[[ "$(uname)" == "Linux" ]] && chown -R 1138:1138 .testnets/staking-indexer + +# wait bit nodes to be ready +sleep 25 + +# Restore the covenant-signer wallet +echo "Restoring the covenant-signer wallet" +docker exec bitcoindsim-signer /bin/sh -c " +bitcoin-cli -regtest -rpcuser="$RPCUSER" -rpcpassword="$RPCPASSWORD" restorewallet "$RPCWALLET" /bitcoindsim/.bitcoin/covenant-signer.dat +" + +# Unlock the covenant-signer wallet +echo "Unlocking the covenant-signer wallet" +docker exec bitcoindsim-signer /bin/sh -c " +bitcoin-cli -regtest -rpcuser="$RPCUSER" -rpcpassword="$RPCPASSWORD" -rpcwallet="$RPCWALLET" walletpassphrase "$RPCWALLETPASS" 36000 +" diff --git a/pre-deployment.sh b/pre-deployment.sh new file mode 100755 index 0000000..ac8ef2f --- /dev/null +++ b/pre-deployment.sh @@ -0,0 +1,26 @@ +#!/bin/sh +GIT_TOPLEVEL=$(git rev-parse --show-toplevel) +# Create new directory that will hold node and services' configuration +mkdir -p .testnets && chmod o+w .testnets + +# Create separate subpaths for each component and copy relevant configuration +rm -rf .testnets +mkdir -p .testnets/bitcoin +mkdir -p .testnets/bitcoin-signer +mkdir -p .testnets/rabbitmq_data +mkdir -p .testnets/staking-indexer/data .testnets/staking-indexer/logs +mkdir -p .testnets/mongo +mkdir -p .testnets/staking-api-service +mkdir -p .testnets/staking-expiry-checker +mkdir -p .testnets/unbonding-pipeline +mkdir -p .testnets/covenant-signer + +cp artifacts/sid.conf .testnets/staking-indexer/sid.conf +cp artifacts/global-params.json .testnets/ +cp artifacts/finality-providers.json .testnets/ +cp artifacts/staking-api-service-config.yml .testnets/staking-api-service +cp artifacts/init-mongo.sh .testnets/mongo +cp artifacts/staking-expiry-checker-config.yml .testnets/staking-expiry-checker +cp artifacts/unbonding-pipeline-config.toml .testnets/unbonding-pipeline +cp artifacts/covenant-signer-config.toml .testnets/covenant-signer +cp artifacts/covenant-signer.dat .testnets/bitcoin-signer diff --git a/submodules/cli-tools b/submodules/cli-tools new file mode 160000 index 0000000..6102461 --- /dev/null +++ b/submodules/cli-tools @@ -0,0 +1 @@ +Subproject commit 6102461e4a91349dedf1ddde832211f3e19d6da5 diff --git a/submodules/contrib/images/Makefile b/submodules/contrib/images/Makefile new file mode 100644 index 0000000..e4c77da --- /dev/null +++ b/submodules/contrib/images/Makefile @@ -0,0 +1,34 @@ +ifeq ($(BITCOIN_CORE_VERSION),) + BITCOINDSIM_TAG := latest +else + BITCOINDSIM_BUILD_ARG := --build-arg BITCOIN_CORE_VERSION=$(BITCOIN_CORE_VERSION) + BITCOINDSIM_TAG := $(BITCOIN_CORE_VERSION) +endif + +all: bitcoinsim + +bitcoindsim: + docker build --platform "linux/amd64" --tag babylonchain/bitcoindsim:$(BITCOINDSIM_TAG) -f bitcoindsim/Dockerfile \ + $(shell git rev-parse --show-toplevel)/submodules/contrib/images/bitcoindsim $(BITCOINDSIM_BUILD_ARG) + +ibcsim-gaia: + docker build --tag babylonchain/ibcsim-gaia -f ibcsim-gaia/Dockerfile \ + $(shell git rev-parse --show-toplevel)/submodules/contrib/images/ibcsim-gaia + +ibcsim-wasmd: + docker build --tag babylonchain/ibcsim-wasmd -f ibcsim-wasmd/Dockerfile \ + $(shell git rev-parse --show-toplevel)/submodules/contrib/images/ibcsim-wasmd + +btcdsim-rmi: + docker rmi babylonchain/btcdsim 2>/dev/null; true + +bitcoindsim-rmi: + docker rmi babylonchain/bitcoindsim 2>/dev/null; true + +ibcsim-gaia-rmi: + docker rmi babylonchain/ibcsim-gaia 2>/dev/null; true + +ibcsim-wasmd-rmi: + docker rmi babylonchain/ibcsim-wasmd 2>/dev/null; true + +.PHONY: all btcdsim btcdsim-rmi bitcoindsim bitcoindsim-rmi ibcsim-gaia ibcsim-gaia-rmi ibcsim-wasmd ibcsim-wasmd-rmi diff --git a/submodules/contrib/images/bitcoindsim/Dockerfile b/submodules/contrib/images/bitcoindsim/Dockerfile new file mode 100644 index 0000000..93984af --- /dev/null +++ b/submodules/contrib/images/bitcoindsim/Dockerfile @@ -0,0 +1,29 @@ +# Reference taken from https://www.willianantunes.com/blog/2022/04/bitcoin-node-with-regtest-mode-using-docker/ + +FROM debian:bullseye-slim + +RUN useradd --system --user-group bitcoin \ + && apt-get update -y \ + && apt-get install -y curl gnupg gosu \ + && apt-get clean \ + && apt-get install vim -y \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +ARG BITCOIN_CORE_VERSION="24.2" +ENV BITCOIN_CORE_VERSION=$BITCOIN_CORE_VERSION +ENV PATH=/opt/bitcoin-${BITCOIN_CORE_VERSION}/bin:$PATH + +RUN set -ex \ + && curl -SLO https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_CORE_VERSION}/bitcoin-${BITCOIN_CORE_VERSION}-x86_64-linux-gnu.tar.gz \ + && tar -xzf *.tar.gz -C /opt + +WORKDIR /bitcoindsim + +ENV BITCOIN_DATA=/bitcoindsim/.bitcoin +ENV BITCOIN_CONF=/bitcoindsim/.bitcoin/bitcoin.conf + +COPY wrapper.sh /bitcoindsim/wrapper.sh + +ENTRYPOINT ["/bitcoindsim/wrapper.sh"] +CMD [] +STOPSIGNAL SIGTERM diff --git a/submodules/contrib/images/bitcoindsim/wrapper.sh b/submodules/contrib/images/bitcoindsim/wrapper.sh new file mode 100755 index 0000000..1bd018f --- /dev/null +++ b/submodules/contrib/images/bitcoindsim/wrapper.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -e + +# Create bitcoin data directory and initialize bitcoin configuration file. +mkdir -p "$BITCOIN_DATA" +echo "# Enable regtest mode. +regtest=1 + +# Accept command line and JSON-RPC commands +server=1 + +# RPC user and password. +rpcuser=$RPC_USER +rpcpassword=$RPC_PASS + +txindex=1 +deprecatedrpc=create_bdb + +# Fallback fee +fallbackfee=0.00001 + +# Allow all IPs to access the RPC server. +[regtest] +rpcbind=0.0.0.0 +rpcallowip=0.0.0.0/0 +" > "$BITCOIN_CONF" + +GENERATE_STAKER_WALLET="${GENERATE_STAKER_WALLET:=true}" +echo "Starting bitcoind..." +bitcoind -regtest -datadir="$BITCOIN_DATA" -conf="$BITCOIN_CONF" -daemon +# Allow some time for bitcoind to start +sleep 3 +echo "Creating a wallet..." +bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" createwallet "$WALLET_NAME" false false "$WALLET_PASS" false false +echo "Generating 110 blocks for the first coinbases to mature..." +bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" -generate 110 + +if [[ "$GENERATE_STAKER_WALLET" == "true" ]]; then + echo "Creating a wallet and $BTCSTAKER_WALLET_ADDR_COUNT addresses for btcstaker..." + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" createwallet "$BTCSTAKER_WALLET_NAME" false false "$WALLET_PASS" false false + + BTCSTAKER_ADDRS=() + for i in `seq 0 1 $((BTCSTAKER_WALLET_ADDR_COUNT - 1))` + do + BTCSTAKER_ADDRS+=($(bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$BTCSTAKER_WALLET_NAME" getnewaddress)) + done + + # Generate a UTXO for each btc-staker address + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" walletpassphrase "$WALLET_PASS" 1 + for addr in "${BTCSTAKER_ADDRS[@]}" + do + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" sendtoaddress "$addr" 10 + done + + # Allow some time for the wallet to catch up. + sleep 5 + + echo "Checking balance..." + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" getbalance +fi + +echo "Generating a block every ${GENERATE_INTERVAL_SECS} seconds." +echo "Press [CTRL+C] to stop..." +while true +do + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" -generate 1 + if [[ "$GENERATE_STAKER_WALLET" == "true" ]]; then + echo "Periodically send funds to btcstaker addresses..." + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" walletpassphrase "$WALLET_PASS" 10 + for addr in "${BTCSTAKER_ADDRS[@]}" + do + bitcoin-cli -regtest -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" sendtoaddress "$addr" 10 + done + fi + sleep "${GENERATE_INTERVAL_SECS}" +done \ No newline at end of file diff --git a/submodules/covenant-signer b/submodules/covenant-signer new file mode 160000 index 0000000..481cc1c --- /dev/null +++ b/submodules/covenant-signer @@ -0,0 +1 @@ +Subproject commit 481cc1c55a3a65b79fef4f9e163764b684102ad6 diff --git a/submodules/simple-staking b/submodules/simple-staking new file mode 160000 index 0000000..308f35e --- /dev/null +++ b/submodules/simple-staking @@ -0,0 +1 @@ +Subproject commit 308f35e14e380a2752f2d54e007971ceff3f7f9f diff --git a/submodules/staking-api-service b/submodules/staking-api-service new file mode 160000 index 0000000..8fa79ef --- /dev/null +++ b/submodules/staking-api-service @@ -0,0 +1 @@ +Subproject commit 8fa79ef975928389297ed6a024c1da00c7f78cc9 diff --git a/submodules/staking-expiry-checker b/submodules/staking-expiry-checker new file mode 160000 index 0000000..6018d29 --- /dev/null +++ b/submodules/staking-expiry-checker @@ -0,0 +1 @@ +Subproject commit 6018d292d87ba92280c554b8652c94e961954131 diff --git a/submodules/staking-indexer b/submodules/staking-indexer new file mode 160000 index 0000000..adf5ca4 --- /dev/null +++ b/submodules/staking-indexer @@ -0,0 +1 @@ +Subproject commit adf5ca49d3e4104eb978f0e4922701081e55eecf