Local Private Cardano Cluster
Introduction
Here are my notes on deploying a local Cardano cluster. I wanted to learn more and experiment with various aspects of the Cardano ecosystem. Focusing on the infrastructure part allows me to understand the flow and interactions between different components. Additionally, I aim to improve my skills with the cardano-cli
and cardano-node
commands. I prefer CLI manipulation as it is easier for me compared to installing a bunch of dependencies for various languages.
Notes
For this cluster, I used three files: two for Docker-related tasks and one to move funds from the genesis addresses.
Prepare Container Recipe
Cardano provides a script to quickly and easily start a private cluster. I wrapped it in Docker to give me more flexibility.
Dockerfile
# docker build -t cardano-private --progress=plain .
FROM ubuntu:22.04
WORKDIR /app
RUN apt update && apt install curl git jq -y
RUN curl -OL https://github.com/IntersectMBO/cardano-node/releases/download/9.0.0/cardano-node-9.0.0-linux.tar.gz && \
tar xvzf cardano-node-9.0.0-linux.tar.gz && \
rm -f cardano-node-9.0.0-linux.tar.gz
RUN ln -s /app/bin/cardano-cli /usr/local/bin/cardano-cli && \
ln -s /app/bin/cardano-node /usr/local/bin/cardano-node
RUN git clone https://github.com/IntersectMBO/cardano-node.git
RUN sed -i cardano-node/scripts/babbage/mkfiles.sh \
-e 's|# echo "TestConwayHardForkAtEpoch: 0" >> "${ROOT}/configuration.yaml"|echo "TestConwayHardForkAtEpoch: 0" >> "${ROOT}/configuration.yaml"|'
CMD ["sleep", "infinity"]
Start and Initialize the Cluster
As of the time of writing, the cluster will be on the Conway era (2024-07-21).
docker-compose.yml
services:
launcher:
container_name: launcher
build:
context: .
volumes:
- ./cluster:/app/cardano-node/example
node1:
container_name: node1
build:
context: .
volumes:
- ./cluster:/app/cardano-node/example
working_dir: /app/cardano-node/
entrypoint: ./example/node-spo1.sh
node2:
container_name: node2
build:
context: .
volumes:
- ./cluster:/app/cardano-node/example
working_dir: /app/cardano-node/
entrypoint: ./example/node-spo2.sh
node3:
container_name: node3
build:
context: .
volumes:
- ./cluster:/app/cardano-node/example
working_dir: /app/cardano-node/
entrypoint: ./example/node-spo3.sh
ogmios:
container_name: ogmios
image: cardanosolutions/ogmios:v6.5.0
ports:
- 1337:1337
volumes:
- ./cluster:/app/cardano-node/example
command: '--node-socket /app/cardano-node/example/node-spo1/node.sock --node-config /app/cardano-node/example/configuration.yaml --host 0.0.0.0'
To build and start the cluster, execute the following commands in this order. The first time, the nodes will fail as the files aren’t created yet.
docker compose build
docker compose up
docker exec -it -w /app/cardano-node/ launcher bash -c "./scripts/babbage/mkfiles.sh"
Then, you have only 30 seconds to do the following:
docker compose up # (within 30 sec)
After that, return to the launcher container:
docker exec -it launcher bash
# Wait few seconds then it should work and show as below,
export CARDANO_NODE_SOCKET_PATH=/app/cardano-node/example/node-spo1/node.sock
cardano-cli query tip --testnet-magic 42
Example:
{
"block": 2,
"epoch": 0,
"era": "Conway",
"hash": "dd870e18b64d49a97c485085861f70c292efbaba98aecff97efed172686e97dc",
"slot": 35,
"slotInEpoch": 35,
"slotsToEpochEnd": 465,
"syncProgress": "100.00"
}
Move Funds
Reusing the launcher and execute the following:
docker exec -it launcher bash
export CARDANO_NODE_SOCKET_PATH=/app/cardano-node/example/node-spo1/node.sock
Extract the address from the genesis key:
cardano-cli signing-key-address \
--testnet-magic 42 \
--secret /app/cardano-node/example/byron-gen-command/genesis-keys.000.key > /app/cardano-node/example/byron-gen-command/genesis-keys.000.addr
cardano-cli signing-key-address \
--testnet-magic 42 \
--secret /app/cardano-node/example/byron-gen-command/genesis-keys.001.key > /app/cardano-node/example/byron-gen-command/genesis-keys.001.addr
cardano-cli signing-key-address \
--testnet-magic 42 \
--secret /app/cardano-node/example/byron-gen-command/genesis-keys.002.key > /app/cardano-node/example/byron-gen-command/genesis-keys.002.addr
Create a payment address (using the CLI): This address will receive the funds
mkdir -p /app/cardano-node/example/wallets/
cardano-cli address key-gen \
--verification-key-file /app/cardano-node/example/wallets/payment.vkey \
--signing-key-file /app/cardano-node/example/wallets/payment.skey
cardano-cli address build \
--payment-verification-key-file /app/cardano-node/example/wallets/payment.vkey \
--out-file /app/cardano-node/example/wallets/payment.addr \
--testnet-magic 42
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/payment.addr) --testnet-magic 42
Extract the wallet address from the UTXO keys:
cardano-cli address build \
--payment-verification-key-file /app/cardano-node/example/utxo-keys/utxo1.vkey \
--out-file /app/cardano-node/example/wallets/utxo1.addr \
--testnet-magic 42
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo1.addr) --testnet-magic 42
cardano-cli address build \
--payment-verification-key-file /app/cardano-node/example/utxo-keys/utxo2.vkey \
--out-file /app/cardano-node/example/wallets/utxo2.addr \
--testnet-magic 42
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo2.addr) --testnet-magic 42
cardano-cli address build \
--payment-verification-key-file /app/cardano-node/example/utxo-keys/utxo3.vkey \
--out-file /app/cardano-node/example/wallets/utxo3.addr \
--testnet-magic 42
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo3.addr) --testnet-magic 42
Goal: Transfer 100 ADA from delegated genesis address 0 to the payment address.
1,000,000 lovelace = 1 ADA
100 ADA = 100,000,000 lovelace
mkdir -p /app/cardano-node/example/wallets/transactions
cd /app/cardano-node/example/wallets/transactions
cardano-cli query protocol-parameters \
--testnet-magic 42 \
--out-file protocol.json
# Print the UTXOs from the utxo1 address,
# you need this value in order to create and send lovelace
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo1.addr) --testnet-magic 42
TxHash TxIx Amount
--------------------------------------------------------------------------------------
8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169 0 600000000000 lovelace + TxOutDatumNone
# The tx-out is the address from the payment address (created above using the cardano-cli)
cardano-cli transaction build-raw \
--tx-in 8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169#0 \
--tx-out $(cat /app/cardano-node/example/wallets/payment.addr)+0 \
--tx-out $(cat /app/cardano-node/example/wallets/utxo1.addr)+0 \
--invalid-hereafter 0 \
--fee 0 \
--out-file tx.draft
cardano-cli transaction calculate-min-fee \
--tx-body-file tx.draft \
--tx-in-count 1 \
--tx-out-count 2 \
--witness-count 1 \
--byron-witness-count 0 \
--testnet-magic 42 \
--protocol-params-file protocol.json
# fee: 165105
# 600000000000 - 100000000 - 170000 = 599899830000 <- this number will be assigned to the change address (I increase the fee a little to avoid missing lovelace in fee)
cardano-cli query tip --testnet-magic 42
# slot: 50000 + 25000 (25000 because this private network goes fast, you can put the value you want this is your private cluster !) = 75000
# Fill the information with your data:
cardano-cli transaction build-raw \
--tx-in 8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169#0 \
--tx-out $(cat /app/cardano-node/example/wallets/payment.addr)+100000000 \ # <- The ADA sent to the payment address
--tx-out $(cat /app/cardano-node/example/wallets/utxo1.addr)+599899830000 \ # <- The ADA sent back to the utxo1 address
--invalid-hereafter 75000 \
--fee 170000 \
--out-file tx.raw
cardano-cli transaction sign \
--tx-body-file tx.raw \
--signing-key-file /app/cardano-node/example/utxo-keys/utxo1.skey \
--testnet-magic 42 \
--out-file tx.signed
cardano-cli transaction submit \
--tx-file tx.signed \
--testnet-magic 42
Validation:
root@4ea4bf7ae5d4:/app/cardano-node/example#
*Command*
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo1.addr) --testnet-magic 42
*Output*
TxHash TxIx Amount
--------------------------------------------------------------------------------------
22c03ae81249a443ca0956d03ab5dca5440ba05b8d5ee2ce359f16195cd3f718 1 599899830000 lovelace + TxOutDatumNone
*Command*
cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/payment.addr) --testnet-magic 42
*Output*
TxHash TxIx Amount
--------------------------------------------------------------------------------------
22c03ae81249a443ca0956d03ab5dca5440ba05b8d5ee2ce359f16195cd3f718 0 100000000 lovelace + TxOutDatumNone
Conclusion
So far, this is where I am in the discovery process.
Next steps include creating policies, NFTs, FTs, multiple addresses, connecting Deno to the exposed socket, a dummy faucet, understanding indexing, trying to connect external wallets, and much more!
You can also access ogmios at: http://localhost:1337
Sources and References
- [Listening for Payments with the CLI](https://developers.cardano.org/docs/integrate-cardano/list
ening-for-payments-cli)
- Generating Wallet Keys
- Cardano Node Scripts
- Creating a Local Cluster with mkfiles Script
- Create simple transaction
Errors
Command failed: transaction submit Error: Error while submitting tx: ShelleyTxValidationError ShelleyBasedEraConway (ApplyTxError (ConwayUtxowFailure (UtxoFailure (ValueNotConservedUTxO (MaryValue (Coin 0) (MultiAsset (fromList []))) (MaryValue (Coin 600000000000) (MultiAsset (fromList []))))) :| [ConwayUtxowFailure (UtxoFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash "8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169"}) (TxIx {unTxIx = 0})])))]))
This likely indicates that you forgot to update the --tx-in
with the UTXO obtained from the command cardano-cli query utxo --address $(cat /app/cardano-node/example/wallets/utxo1.addr) --testnet-magic 42
.
Command failed: transaction submit Error: Error while submitting tx: ShelleyTxValidationError ShelleyBasedEraConway (ApplyTxError (ConwayUtxowFailure (UtxoFailure (ValueNotConservedUTxO (MaryValue (Coin 0) (MultiAsset (fromList []))) (MaryValue (Coin 600000000000) (MultiAsset (fromList []))))) :| [ConwayUtxowFailure (UtxoFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash "8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169"}) (TxIx {unTxIx = 0})]))),ConwayUtxowFailure (UtxoFailure (FeeTooSmallUTxO (Coin 165721) (Coin 165105))),ConwayUtxowFailure (UtxoFailure (OutsideValidityIntervalUTxO (ValidityInterval {invalidBefore = SNothing, invalidHereafter = SJust (SlotNo 1000)}) (SlotNo 9309)))]))
Due to the rapid slot generation by private nodes, it’s necessary to set the slot value higher than usual.
Command failed: transaction submit Error: Error while submitting tx: ShelleyTxValidationError ShelleyBasedEraConway (ApplyTxError (ConwayUtxowFailure (UtxoFailure (ValueNotConservedUTxO (MaryValue (Coin 0) (MultiAsset (fromList []))) (MaryValue (Coin 600000000000) (MultiAsset (fromList []))))) :| [ConwayUtxowFailure (UtxoFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash "8d5a6eac5622f7b2f021ce6106d5eca201b7c1be3f1417051578efca482f4169"}) (TxIx {unTxIx = 0})]))),ConwayUtxowFailure (UtxoFailure (FeeTooSmallUTxO (Coin 165809) (Coin 165105)))]))
I encountered an issue with my transaction fees being lower than needed. To fix this, I rounded up the fee to the nearest acceptable value. For example, I changed 165600 to 166000. This adjustment ensures that transactions are processed smoothly without errors due to insufficient fees.