Running the Hello-world-AVS
This hands-on example guide demonstrates how to integrate an AVS with the Imua network
To start building your AVS, it's recommended to use a localnet of our chain for faster testing. In addition to the localnet, we have created a few convenience binaries for testing.
0. Pre-requisites
Install dependencies which are needed to compile the binary as well as jq
and foundry
(link).
1. Running a single-validator localnet
git clone https://github.com/imua-xyz/imuachain.git
cd imuachain
# Use the last released version instead of bleeding edge
git checkout v1.1.2
make install
# set the local environment variable, required for the price feeder tool
export ALCHEMY_API_KEY=
# apply a small patch to make the run process a bit more dev friendly
git apply <<'EOF'
diff --git a/local_node.sh b/local_node.sh
index c4424b26..9358748f 100755
--- a/local_node.sh
+++ b/local_node.sh
@@ -1,5 +1,11 @@
#!/usr/bin/env bash
+# check that ALCHEMY_API_KEY is set
+if [ -z "$ALCHEMY_API_KEY" ]; then
+ echo "ALCHEMY_API_KEY is not set"
+ exit 0
+fi
+
KEYS[0]="dev0"
KEYS[1]="dev1"
KEYS[2]="dev2"
@@ -219,8 +225,8 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then
oracle_env_chainlink_content=$(
cat <<EOF
urls:
- mainnet: https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}
- sepolia: https://eth-sepolia.g.alchemy.com/v2/{ALCHEMY_API_KEY}
+ mainnet: https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}
+ sepolia: https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}
tokens:
ETHUSDT: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419_mainnet
AAVEUSDT: 0x547a514d5e3769680Ce22B2361c10Ea13619e8a9_mainnet
EOF
./local_node.sh
Certain errors may happen during the make install
step, the first of which is seen on systems with GCC15+ and the second is seen on older CPUs.
If you get an error
blst.h:27:15: note: 'bool' is a keyword with '-std=c23' onwards
, setCGO_CFLAGS="-std=gnu11"
If the error is
Caught SIGILL in blst_cgo_init, consult /bindings/go/README.md
, setCGO_CFLAGS="-O -D__BLST_PORTABLE__"
andCGO_CFLAGS_ALLOW="-O -D__BLST_PORTABLE__"
.
Once the node is running, you will see logs like INF indexed block events height=1 module=txindex server=node
.
Each such localnet comes with 4 pre-funded keys by default: the local_funded_account
and dev0
, dev1
and dev2
. Of these, the mnemonic (and thus the address) for the first key is fixed (im18cggcpvwspnd5c6ny8wrqxpffj5zmhkl3agtrj
) while that for the other 3 is ephemeral. The keys can be listed by using this command.
$ imuad keys list --home ~/.tmp-imuad --output json | jq
[
{
"name": "dev0",
"type": "local",
"address": "im1t8tm9khsyfx8rzty3els6llwuxarq0axkx9fa3",
"pubkey": "{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"A2C9UwiZO38DLRE2ckVr0xBio+lgjoNmxq3dlKkbG2dN\"}"
},
{
"name": "dev1",
"type": "local",
"address": "im1xawerj0w5wg0uqrxf3h6kr5edzh74kuy6trhe7",
"pubkey": "{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"A1lNHXPimdqLNvzSkhc/ANzDcpg39P8BgxAjgF1LybIv\"}"
},
{
"name": "dev2",
"type": "local",
"address": "im17vfu2x8gydv9jqjczfj0meufuam7xxdxryqlmg",
"pubkey": "{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"Al6d+YMUKp+00/GBgC9i1FvqNHK3FRKyVOHaP4/ZN/w/\"}"
},
{
"name": "local_funded_account",
"type": "local",
"address": "im18cggcpvwspnd5c6ny8wrqxpffj5zmhkl3agtrj",
"pubkey": "{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"Av7dV4L/SjuZGuR86oAR3YaY4QuwDYE4dANgeVBGN6nx\"}"
}
]
The operator can be registered into the network. As mentioned previously, this registration is the first step for any new operator; the next step is to opt into an AVS.
imuad --home ~/.tmp-imuad tx operator register-operator \
--meta-info "Operator1" \
--commission-rate 0.5 \
--commission-max-rate 1 \
--commission-max-change-rate 1 \
--from dev1 \
--keyring-backend test \
--fees 50000000000hua \
--chain-id imuachainlocalnet_232-1
For the next step, the Ethereum version of the private keys for dev0
and dev1
are required. Export them by using the following commands.
## Both of these keys change across localnet deployments, so your value may be different.
## If you have your own address, you can fund it with `bank send`
## And add it to the keystore with `unsafe-import-eth-key` or `add --recover` depending on whether you have a private key or a mnemonic available.
# The first key will be the AVS owner.
imuad keys unsafe-export-eth-key dev0 --home ~/.tmp-imuad
2A16922303BA1AD1D0506E338CD23B3412A808F0DC5F5AAE4E4A097C8E6C2664
# The second key is the operator.
# It changes across deployments of the localnet, so your value may be different.
imuad keys unsafe-export-eth-key dev1 --home ~/.tmp-imuad
2A16922303BA1AD1D0506E338CD23B3412A808F0DC5F5AAE4E4A097C8E6C2664
2. AVS registration
Use the key management binary to set up your private keys.
cd ..
git clone https://github.com/imua-xyz/hello-world-avs.git
cd hello-world-avs
make build
# Import AVS owner key (dev0)
./imua-key importKey \
--key-type ecdsa \
--private-key 2A16922303BA1AD1D0506E338CD23B3412A808F0DC5F5AAE4E4A097C8E6C2664 \
--output-dir tests/keys/avs.ecdsa.key.json
# Import AVS operator key (dev1)
./imua-key importKey \
--key-type ecdsa \
--private-key 49AE7BC10B01EF2538FA91BD23D3EADEF9760C764FB5791EAF27CBE8820BE7AC \
--output-dir tests/keys/operator.ecdsa.key.json
# Import BLS key (sample below)
./imua-key importKey \
--key-type bls \
--private-key 1c0599ffc52d512fd5b549fa050833e7d3bba12969d09d70a16441384e5a8a3a \
--output-dir tests/keys/test.bls.key.json
# To generate your own BLS key, use the
# `generate` verb of `imua-key` and look at the
# contents of `private_key_hex.txt` in the subfolder
Configure the config.yaml
file in hello-world-avs/config.yaml
with the following line items:
avs_owner_address
: The hex address of thedev0
account, which can be derived by using the following command.
imuad keys parse $(imuad --home ~/.tmp-imuad keys show -a dev0) --output json | jq -r .bytes | cast 2a
operator_address
: The hex address of thedev1
account, which can be derived similarly. This address is the one with which we registered the operator.
imuad keys parse $(imuad --home ~/.tmp-imuad keys show -a dev1) --output json | jq -r .bytes | cast 2a
avs_owner_addresses
: The bech32 address of the operator as well as that of the owner (solely for this example) are included.
# debug mode on false
production: false
# the `dev0` hex address
avs_owner_address: 0xb1330e7f09B539E5494e72B383F1Ffb10314a1A9
# the `dev1` hex address
operator_address: 0x16fbBC5Af7255bB4fA6b6353CfF5328DeEA38F43
# deployed address, will be populated
avs_address: 0x10Ed22D975453A5D4031440D51624552E4f204D5
# URLs for RPC
eth_rpc_url: http://127.0.0.1:8545
eth_ws_url: ws://127.0.0.1:8546
# location of the key files
avs_ecdsa_private_key_store_path: tests/keys/avs.ecdsa.key.json
operator_ecdsa_private_key_store_path: tests/keys/operator.ecdsa.key.json
bls_private_key_store_path: tests/keys/test.bls.key.json
# API for the AVS SDK, not required for this step
node_api_ip_port_address: 0.0.0.0:9010
enable_node_api: false
# We have already registered the operator previously
register_operator_on_startup: false
## AVS registration parameters
# Name of the AVS
avs_name: "hello-avs"
# The amount of minimum stake required, in USD.
min_stake_amount: 1
# The owner of the AVS (see `avs_owner_address`) above,
# and the `operator` (for this example).
avs_owner_addresses:
- "0xb1330e7f09B539E5494e72B383F1Ffb10314a1A9"
- "0x16fbBC5Af7255bB4fA6b6353CfF5328DeEA38F43"
# List of operators that can opt-in
# Leave blank to permit anyone
whitelist_addresses:
- "0xb1330e7f09B539E5494e72B383F1Ffb10314a1A9"
- "0x16fbBC5Af7255bB4fA6b6353CfF5328DeEA38F43"
# The assets which are accepted as delegation
# For localnet, this is the USDT Ethereum mainnet asset
asset_ids:
- "0xdac17f958d2ee523a2206206994597c13d831ec7_0x65"
# The epoch, whose duration is used for time keeping
epoch_identifier: minute
# The number of epochs it takes for an operator to unbond
avs_unbonding_period: 7
# The minimum self delegation required to opt-in
# Setting this to non-0 will prevent operators from opting-in
# Unless they delegate first
min_self_delegation: 0
# Will be populated
avs_reward_address: 0x10Ed22D975453A5D4031440D51624552E4f204D5
# Will be populated
avs_slash_address: 0x10Ed22D975453A5D4031440D51624552E4f204D5
# Will be populated
task_address: 0x10Ed22D975453A5D4031440D51624552E4f204D5
# The minimum number of operators required for a task to begin
mini_opt_in_operators: 1
# The minimum amount of stake (whether delegated or self) required
# for a task to begin
min_total_stake_amount: 3
# The reward percentage
avs_reward_proportion: 3
# The slash percentage
avs_slash_proportion: 3
## Task creation parameters
# A new task is created every 100 seconds
create_task_interval: 100
# The deadline for responding to a task, measured in epochs
task_response_period: 3
# The deadline to challenge a task, measured in epochs
task_challenge_period: 3
# The percentage of vote required for a task to complete
threshold_percentage: 100
# The deadline to aggregate a task
task_statistical_period: 3
## Operative parameters to opt in
# The deposit quanity
deposit_amount: 100
# The delegated quantity
delegate_amount: 100
# The staker address on the client chain
# The private key for this staker is not required
staker: 0xa53f68563D22EB0dAFAA871b6C08a6852f91d627
Finally, run the avsbinary
which will deploy the AVS contract and populate the contract deployments in the same file.
./avsbinary --config config.yaml
Result:
2025/07/12 11:33:03 initializing avs
2025-07-12T11:33:03.336+0400 INFO logging/zap_logger.go:49 AVS_ECDSA_KEY_PASSWORD env var not set. using empty string
2025-07-12T11:33:03.336+0400 INFO logging/zap_logger.go:49 avsSender: {"avsSender": "0xb1330e7f09B539E5494e72B383F1Ffb10314a1A9"}
2025-07-12T11:33:03.336+0400 INFO logging/zap_logger.go:49 AVSOwnerAddress: {"AVSOwnerAddress": "0xb1330e7f09B539E5494e72B383F1Ffb10314a1A9"}
2025-07-12T11:33:03.339+0400 INFO logging/zap_logger.go:49 AVS_ADDRESS env var not set. will deploy avs contract
2025-07-12T11:33:04.349+0400 INFO logging/zap_logger.go:69 tx hash: 0x0916423b80d1d2fb1dc708e7f5fe465e3f1f958d4c42eb16e477a68e53a062bd
2025-07-12T11:33:04.349+0400 INFO logging/zap_logger.go:69 contract address: 0x15eAafEB9f21d8c4E10CAE740b71fDEf10baFC3c
2025-07-12T11:33:11.316+0400 DEBUG logging/zap_logger.go:45 Estimating gas and nonce {"tx": "0x16c97b7f08adde9b38eebb044ff1e23e6c442b8a13c226b69484ba714da94f43"}
2025-07-12T11:33:11.334+0400 DEBUG logging/zap_logger.go:45 Getting signer for tx {"tx": "0xee638234bffece83df3064f2cc01909a4d6e46964faf47dd441b828f11260a2c"}
2025-07-12T11:33:12.287+0400 DEBUG logging/zap_logger.go:45 Sending transaction {"tx": "0xee638234bffece83df3064f2cc01909a4d6e46964faf47dd441b828f11260a2c"}
2025-07-12T11:33:14.300+0400 INFO logging/zap_logger.go:69 tx hash: 0x16c97b7f08adde9b38eebb044ff1e23e6c442b8a13c226b69484ba714da94f43
2025-07-12T11:33:14.303+0400 INFO logging/zap_logger.go:69 Starting avs.
2025-07-12T11:33:14.303+0400 INFO logging/zap_logger.go:69 Avs owner set to send new task every 100 seconds
2025-07-12T11:33:34.303+0400 INFO logging/zap_logger.go:49 Avs sending new task
2025-07-12T11:33:34.306+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 1, "max_attempts": 25}
2025-07-12T11:33:40.310+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 2, "max_attempts": 25}
2025-07-12T11:33:46.314+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 3, "max_attempts": 25}
2025-07-12T11:33:52.318+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 4, "max_attempts": 25}
2025-07-12T11:33:58.322+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 5, "max_attempts": 25}
2025-07-12T11:34:04.326+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 6, "max_attempts": 25}
2025-07-12T11:34:10.330+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 7, "max_attempts": 25}
2025-07-12T11:34:16.333+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 8, "max_attempts": 25}
2025-07-12T11:34:22.337+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 9, "max_attempts": 25}
At this moment, no operator has opted into the AVS so the reported AVS USD value is 0.
3. Operator opt-in
In another terminal window, have the operator opt into the AVS.
imuad tx operator \
opt-into-avs $(cat config.yaml | grep avs_address | awk '{print $2}') \
--from dev1 \
--home ~/.tmp-imuad \
--keyring-backend test \
--fees 50000000000hua \
--chain-id imuachainlocalnet_232-1
Verify operator has opted into the AVS.
imuad query operator get-avs-list $(imuad --home ~/.tmp-imuad keys show -a dev1)
# your output may be different
avs_list:
- 0x15eaafeb9f21d8c4e10cae740b71fdef10bafc3c
To avoid Sybil attacks, the operator must have a self-delegation meeting the minimum self-delegation of the AVS before opting in. In this example, that number is 0, so the opt-in can proceed. In other cases, the deposit and delegation is required to be done before the opt-in.
4. Deposit and delegate
Deposit and delegate with operator module in hello-world-avs
./operatorbinary --config config.yaml
Once it is done, you will see OperatorOptedUSDValue is zero or negative
for a maximum of 1 minute. This happens because the USD value is updated only once every epoch.
Start the task distributor ./avsbinary --config config.yaml
once again (if it panicked).
2025-07-12T15:00:57.007+0400 INFO logging/zap_logger.go:69 Avs owner set to send new task every 100 seconds
2025-07-12T15:01:17.007+0400 INFO logging/zap_logger.go:49 Avs sending new task
2025-07-12T15:01:17.010+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 1, "max_attempts": 25}
2025-07-12T15:01:23.013+0400 INFO logging/zap_logger.go:49 AVS USD value is zero or negative {"avs usd value": "0.000000000000000000", "attempt": 2, "max_attempts": 25}
2025-07-12T15:01:30.012+0400 DEBUG logging/zap_logger.go:45 Estimating gas and nonce {"tx": "0xff16018a041d6f39d483d4a2201bbc1bc745bc5779177259b7a5a80fbacca1ae"}
2025-07-12T15:01:30.027+0400 DEBUG logging/zap_logger.go:45 Getting signer for tx {"tx": "0x6792064e56d53d7ae42cae6c3e2192562faded68381af9ecb6e583f4c49a3541"}
2025-07-12T15:01:30.962+0400 DEBUG logging/zap_logger.go:45 Sending transaction {"tx": "0x6792064e56d53d7ae42cae6c3e2192562faded68381af9ecb6e583f4c49a3541"}
2025-07-12T15:01:32.975+0400 INFO logging/zap_logger.go:69 tx hash: 0xff16018a041d6f39d483d4a2201bbc1bc745bc5779177259b7a5a80fbacca1ae
2025-07-12T15:02:37.007+0400 INFO logging/zap_logger.go:49 sendNewTask-num: {"taskNum": 2}
2025-07-12T15:02:37.007+0400 INFO logging/zap_logger.go:49 Avs sending new task
2025-07-12T15:02:37.991+0400 DEBUG logging/zap_logger.go:45 Estimating gas and nonce {"tx": "0x9eab95994efd41dc040bbfffdca47275dfddcdee39637998d2ba7257c52457c2"}
2025-07-12T15:02:38.007+0400 DEBUG logging/zap_logger.go:45 Getting signer for tx {"tx": "0x43bdbf5097d4baaf28744a192ccf6c71e6750c1411b8b4545cd0b3d68073a5ce"}
2025-07-12T15:02:38.985+0400 DEBUG logging/zap_logger.go:45 Sending transaction {"tx": "0x43bdbf5097d4baaf28744a192ccf6c71e6750c1411b8b4545cd0b3d68073a5ce"}
2025-07-12T15:02:40.997+0400 INFO logging/zap_logger.go:69 tx hash: 0x9eab95994efd41dc040bbfffdca47275dfddcdee39637998d2ba7257c52457c2
The operator binary will then record the receipt of these tasks and start performing them.
2025-07-12T15:01:31.435+0400 INFO logging/zap_logger.go:49 New Task Created {"TaskID": 1, "Issuer": "0x7b2E1748e51B3B00eCa5707E6035C9F86937a5d5", "Name": "czvgO", "NumberToBeSquared": 203}
2025-07-12T15:02:40.881+0400 INFO logging/zap_logger.go:49 New Task Created {"TaskID": 2, "Issuer": "0x7b2E1748e51B3B00eCa5707E6035C9F86937a5d5", "Name": "Vy49S", "NumberToBeSquared": 103}
2025-07-12T15:03:30.180+0400 INFO logging/zap_logger.go:49 Execute Phase One Submission Task {"currentEpoch": 5, "startingEpoch": 4, "taskResponsePeriod": 3}
2025-07-12T15:03:30.180+0400 INFO logging/zap_logger.go:49 Submitting task response for task response period {"taskAddr": "0x4BD8eb6ee30B8d5dCF1D550d2d675f8f6C15e2a4", "taskId": 1, "operator-addr": "0x11f4039858c7Fdc47b9695d913070a3D6Dc362E1"}
2025-07-12T15:03:31.205+0400 DEBUG logging/zap_logger.go:45 Estimating gas and nonce {"tx": "0xa4bcf412f998fd6f2e8729c8fc2e1899af801596dc8147fece47ed578b2645a8"}
2025-07-12T15:03:31.220+0400 DEBUG logging/zap_logger.go:45 Getting signer for tx {"tx": "0x651eb0ff35ebadd50f3f81cb202a53138b73889fd4459577c3f68fe3c0565f94"}
2025-07-12T15:03:32.144+0400 DEBUG logging/zap_logger.go:45 Sending transaction {"tx": "0x651eb0ff35ebadd50f3f81cb202a53138b73889fd4459577c3f68fe3c0565f94"}
2025-07-12T15:03:34.154+0400 INFO logging/zap_logger.go:69 tx hash: 0xa4bcf412f998fd6f2e8729c8fc2e1899af801596dc8147fece47ed578b2645a8
2025-07-12T15:03:34.154+0400 INFO logging/zap_logger.go:49 Phase 1 submission completed successfully
5. Checking task status
Replace 1
in the command below with the task ID to check its status.
$ imuad q avs TaskInfo $(cat config.yaml | grep avs_address | awk '{print $2}') 1
actual_threshold: ""
eligible_reward_operators: []
eligible_slash_operators: []
err_signed_operators: []
hash: 0bJ2uX5hdj1+bfBgDIwwGEoSodtxDAsKh4NlwXDERVQ=
is_expected: false
name: czvgO
no_signed_operators: []
operator_active_power:
operator_power_list:
- active_power: "297680.671100000000000000"
operator_address: im1z86q8xzccl7ug7ukjhv3xpc284kuxchp4reqlc
opt_in_operators:
- im1z86q8xzccl7ug7ukjhv3xpc284kuxchp4reqlc
signed_operators:
- im1z86q8xzccl7ug7ukjhv3xpc284kuxchp4reqlc
starting_epoch: "4"
task_challenge_period: "3"
task_contract_address: 0x4bd8eb6ee30b8d5dcf1d550d2d675f8f6c15e2a4
task_id: "1"
task_response_period: "3"
task_statistical_period: "3"
task_total_power: "297680.671100000000000000"
threshold_percentage: "100"
Depending on the time (measured as the number of epochs) since the task was submitted, some of these fields may not be populated.
Individually, an operator's task submission status can also be queried.
imuad query avs SubmitTaskResult \
$(cat config.yaml | grep avs_address | awk '{print $2}') \
1 \
$(imuad --home ~/.tmp-imuad keys show -a dev1)
info:
bls_signature: mf/XbcgSWxEoZyq+CsZ6k0/NzHoqrCpZv1JmIrAhWqYHC+Zvxav9ASgAkI/ITxPOEKQ7Gz9NF5JQEhonAO725wzZtp5KSHVjR60OwJwWIpZ5aFUpEAfMMp5YSu/bfpFa
operator_address: im1z86q8xzccl7ug7ukjhv3xpc284kuxchp4reqlc
phase: PHASE_DO_COMMIT
task_contract_address: 0x4BD8eb6ee30B8d5dCF1D550d2d675f8f6C15e2a4
task_id: "1"
task_response: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACg+Q==
task_response_hash: 0x647c99b6e934191bb88ede36058cce2bd77f54335770dfb2af4da06b6c03bf96

Last updated