Skip to content

Commit

Permalink
Merge pull request #339 from valory-xyz/feat/change_staking_programs
Browse files Browse the repository at this point in the history
Change staking program
  • Loading branch information
jmoreira-valory authored Aug 21, 2024
2 parents fb69d68 + 3f9d3b8 commit 9123bcb
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 98 deletions.
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Do you want to use staking in this service? (yes/no): n
> Using this code could potentially lead to loss of funds, compromised data, or asset risk.
> Exercise caution and use this code at your own risk. Please refer to the [LICENSE](./LICENSE) file for details about the terms and conditions.
Before you proceed, ensure you have at least 20 OLAS on Gnosis Chain. For more information on staking, checkout the following [blogpost](https://www.valory.xyz/post/alpine).
Each staking program has different OLAS requirements. The script will check that your owner address meets the minimum required OLAS on the Gnosis Chain.

Clone this repository locally and execute:

Expand All @@ -57,23 +57,46 @@ chmod +x run_service.sh
./run_service.sh
```

Answer 'Yes' when prompted:
Select your preferred staking program when prompted:

```text
Do you want to use staking in this service? (yes/no): y
Please, select your staking program preference
----------------------------------------------
1) No staking
Your Olas Predict agent will still actively participate in prediction
markets, but it will not be staked within any staking program.
2) Quickstart Beta - Hobbyist
The Quickstart Beta - Hobbyist staking contract offers 100 slots for
operators running Olas Predict agents with the quickstart. It is designed as
a step up from Coastal Staker Expeditions, requiring 100 OLAS for staking.
The rewards are also more attractive than with Coastal Staker Expeditions.
3) Quickstart Beta - Expert
The Quickstart Beta - Expert staking contract offers 20 slots for operators
running Olas Predict agents with the quickstart. It is designed for
professional agent operators, requiring 1000 OLAS for staking. The rewards
are proportional to the Quickstart Beta - Hobbyist.
```

Find below a diagram of the possible status a service can be in the **Alpine staking** program:
Find below a diagram of the possible status a service can be in the staking program:

![Alpine staking FSM](images/alpine_staking_fsm.svg)
![Staking FSM](images/staking_fsm.svg)

Services can become staked by invoking the `stake()` contract method, where service parameters and deposit amounts are verified. Staked services can call the `checkpoint()` method at regular intervals, ensuring liveness checks and calculating staking incentives. In case a service remains inactive beyond the specified `maxAllowedInactivity` time, it faces eviction from the staking program, ceasing to accrue additional rewards. Staked or evicted services can unstaked by calling the `unstake()` contract method. They can do so after `minStakingDuration` has passed or if no more staking rewards are available.
Services can become staked by invoking the `stake()` contract method, where service parameters and deposit amounts are verified. Staked services can call the `checkpoint()` method at regular intervals, ensuring liveness checks and calculating staking rewards. In case a service remains inactive beyond the specified `maxAllowedInactivity` time, it faces eviction from the staking program, ceasing to accrue additional rewards. Staked or evicted services can be unstaked by calling the `unstake()` contract method. They can do so after `minStakingDuration` has passed or if no more staking rewards are available.

__Notes__:

- Staking is currently in a testing phase, so the number of trader agents that can be staked might be limited.
- In the [Alpine staking program](https://www.valory.xyz/post/alpine) services are evicted after accumulating 2 consecutive checkpoints without meeting the activity threshold.
- Services are evicted after accumulating 2 consecutive checkpoints without meeting the activity threshold.
- Currently, the minimum staking time is approximately 3 days. In particular, a service cannot be unstaked during the minimum staking period.
- Once a staking program is selected, you can reset your preference by stopping your agent by running ./stop_service.sh and then running the command

``` bash
cd trader; poetry run python ../scripts/choose_staking.py --reset; cd ..
```

Keep in mind that your service must stay for `minStakingDuration` in a staking program (typically 3 days) before you can change to a new program.

### Service is Running

Expand Down
File renamed without changes
6 changes: 2 additions & 4 deletions run_service.sh
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ] || [ "$on_chain_agent_id"

response="y"
if [ "${USE_STAKING}" = true ]; then
echo "Your service is in a staking program. Updating your on-chain service requires that it is first unstaked."
echo "If your service is in a staking program, updating your on-chain service requires that it is first unstaked."
echo "Unstaking your service will retrieve the accrued staking rewards."
echo ""
echo "Do you want to continue updating your service? (yes/no)"
Expand All @@ -909,9 +909,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ] || [ "$on_chain_agent_id"
echo "Skipping on-chain service update."
else
# unstake the service
if [ "${USE_STAKING}" = true ]; then
perform_staking_ops true
fi
perform_staking_ops true

# Check balances
suggested_amount=$suggested_top_up_default
Expand Down
25 changes: 18 additions & 7 deletions scripts/choose_staking.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,14 @@ def _get_nevermined_env_variables() -> Dict[str, str]:
use_nevermined = True

if use_nevermined:
print("A Nevermined subscription will be used to pay for the mech requests.")
print(" - A Nevermined subscription will be used to pay for the mech requests.")
return {
"MECH_CONTRACT_ADDRESS": NEVERMINED_MECH_CONTRACT_ADDRESS,
"AGENT_REGISTRY_ADDRESS": NEVERMINED_AGENT_REGISTRY_ADDRESS,
"MECH_REQUEST_PRICE": NEVERMINED_MECH_REQUEST_PRICE
}
else:
print("No Nevermined subscription set.")
print(" - No Nevermined subscription set.")
return {
"AGENT_REGISTRY_ADDRESS": "",
"MECH_REQUEST_PRICE": ""
Expand All @@ -249,24 +249,35 @@ def main() -> None:
args = parser.parse_args()

if args.reset:
env_file_vars = dotenv_values(DOTENV_PATH)
staking_program = env_file_vars.get("STAKING_PROGRAM")
print("=====================================")
print("Reset your staking program preference")
print("=====================================")
print("")
print(f"Your current staking program is set to '{staking_program}'")
response = input("Do you want to reset your staking program preference? (yes/no): ").strip().lower()
if response not in ['yes', 'y']:
return

print("")
unset_key(dotenv_path=DOTENV_PATH, key_to_unset="USE_STAKING")
unset_key(dotenv_path=DOTENV_PATH, key_to_unset="STAKING_PROGRAM")
print(f"Environment variables USE_STAKING and STAKING_PROGRAM have been reset in '{DOTENV_PATH}'.")
print("You can now execute './run_service.sh' and select a different staking program.")
print("")
return

program_id = _prompt_select_staking_program()

print("Populating staking program variables in the .env file")
print("")
print(" - Populating staking program variables in the .env file")
staking_env_variables = _get_staking_env_variables(program_id)
_set_dotenv_file_variables(staking_env_variables)

print("Populating Nevermined variables in the .env file")
print(" - Populating Nevermined variables in the .env file")
print("")
nevermined_env_variables = _get_nevermined_env_variables()
_set_dotenv_file_variables(nevermined_env_variables)
print("")
print("Finished populating the .env file.")


if __name__ == "__main__":
Expand Down
215 changes: 135 additions & 80 deletions scripts/staking.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
send_tx_and_wait_for_receipt,
)

SCRIPT_PATH = Path(__file__).resolve().parent
STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner")
DOTENV_PATH = Path(STORE_PATH, ".env")


def _format_duration(duration_seconds: int) -> str:
days, remainder = divmod(duration_seconds, 86400)
Expand Down Expand Up @@ -153,6 +157,105 @@ def _check_unstaking_availability(
return True


def _get_current_staking_program(ledger_api, service_id):
all_staking_programs = STAKING_PROGRAMS.copy()
all_staking_programs.update(DEPRECATED_STAKING_PROGRAMS)
del all_staking_programs["no_staking"]
del all_staking_programs["quickstart_alpha_everest"] # Very old program, not used likely - causes issues on "is_service_staked"

staking_program = None
staking_contract_address = None
for program, data in all_staking_programs.items():
address = data["deployment"]["stakingTokenInstanceAddress"]
if is_service_staked(
ledger_api, service_id, address
):
staking_program = program
staking_contract_address = address
print(f"Service {service_id} is staked on {program}.")
else:
print(f"Service {service_id} is not staked on {program}.")
return staking_contract_address, staking_program


def _try_unstake_service(
ledger_api: EthereumApi,
service_id: int,
owner_crypto: EthereumCrypto,
service_registry_address: str,
) -> None:

staking_contract_address, staking_program = _get_current_staking_program(ledger_api, service_id)

# Exit if not staked
if staking_contract_address is None:
sys.exit(0)

# Collect information
next_ts = get_next_checkpoint_ts(ledger_api, staking_contract_address)
liveness_period = get_liveness_period(ledger_api, staking_contract_address)
last_ts = next_ts - liveness_period
now = time.time()

if is_service_evicted(
ledger_api, service_id, staking_contract_address
):
print(
f"WARNING: Service {service_id} has been evicted from the {staking_program} staking program due to inactivity."
)
input("Press Enter to continue...")

can_unstake = _check_unstaking_availability(
ledger_api,
service_id,
staking_contract_address,
staking_program,
)

if not can_unstake:
print("Terminating script.")
sys.exit(1)

if now < next_ts:
formatted_last_ts = datetime.utcfromtimestamp(last_ts).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
formatted_next_ts = datetime.utcfromtimestamp(next_ts).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)

print(
"WARNING: Staking checkpoint call not available yet\n"
"--------------------------------------------------\n"
f"The liveness period ({liveness_period/3600} hours) has not passed since the last checkpoint call.\n"
f" - {formatted_last_ts} - Last checkpoint call.\n"
f" - {formatted_next_ts} - Next checkpoint call availability.\n"
"\n"
"If you proceed with unstaking, your agent's work done between the last checkpoint call until now will not be accounted for rewards.\n"
"(Note: To maximize agent work eligible for rewards, the recommended practice is to unstake shortly after a checkpoint has been called and stake again immediately after.)\n"
)

user_input = input(
f"Do you want to continue unstaking service {service_id} from {staking_program}? (yes/no)\n"
).lower()
print()

if user_input not in ["yes", "y"]:
print("Terminating script.")
sys.exit(1)

print(f"Unstaking service {service_id} from {staking_program}...")
unstake_txs = get_unstake_txs(
ledger_api, service_id, staking_contract_address
)
for tx in unstake_txs:
send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx)
print(
f"Successfully unstaked service {service_id} from {staking_program}."
)
sys.exit(0)


def _try_stake_service(
ledger_api: EthereumApi,
service_id: int,
Expand Down Expand Up @@ -237,91 +340,46 @@ def main() -> None:
private_key_path=args.owner_private_key_path, password=args.password
)

_unstake_all_old_programs(
ledger_api=ledger_api,
service_id=args.service_id,
owner_crypto=owner_crypto,
current_staking_contract_address=args.staking_contract_address
)
# No need to execute this instruction here, as the user can choose the staking program
#
# _unstake_all_old_programs(
# ledger_api=ledger_api,
# service_id=args.service_id,
# owner_crypto=owner_crypto,
# current_staking_contract_address=args.staking_contract_address
# )

# Collect information
next_ts = get_next_checkpoint_ts(ledger_api, args.staking_contract_address)
liveness_period = get_liveness_period(ledger_api, args.staking_contract_address)
last_ts = next_ts - liveness_period
now = time.time()
available_rewards = get_available_rewards(
ledger_api, args.staking_contract_address
)

if args.unstake:
if not is_service_staked(
ledger_api, args.service_id, args.staking_contract_address
):
# the service is not staked, so we don't need to do anything
print(f"Service {args.service_id} is not staked on {staking_program}..")
sys.exit(0)

if is_service_evicted(
ledger_api, args.service_id, args.staking_contract_address
):
print(
f"WARNING: Service {args.service_id} has been evicted from the {staking_program} staking program due to inactivity."
)
input("Press Enter to continue...")

can_unstake = _check_unstaking_availability(
ledger_api,
args.service_id,
args.staking_contract_address,
staking_program,
_try_unstake_service(
ledger_api=ledger_api,
service_id=args.service_id,
owner_crypto=owner_crypto,
service_registry_address=args.service_registry_address,
)

if not can_unstake:
print("Terminating script.")
sys.exit(1)
if is_service_staked(
ledger_api, args.service_id, args.staking_contract_address
):

if now < next_ts:
formatted_last_ts = datetime.utcfromtimestamp(last_ts).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
formatted_next_ts = datetime.utcfromtimestamp(next_ts).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
_, current_program = _get_current_staking_program(ledger_api, args.service_id)
env_file_vars = dotenv_values(DOTENV_PATH)
target_program = env_file_vars.get("STAKING_PROGRAM")

if current_program != target_program:
print(
"WARNING: Staking checkpoint call not available yet\n"
"--------------------------------------------------\n"
f"The liveness period ({liveness_period/3600} hours) has not passed since the last checkpoint call.\n"
f" - {formatted_last_ts} - Last checkpoint call.\n"
f" - {formatted_next_ts} - Next checkpoint call availability.\n"
"\n"
"If you proceed with unstaking, your agent's work done between the last checkpoint call until now will not be accounted for rewards.\n"
"(Note: To maximize agent work eligible for rewards, the recommended practice is to unstake shortly after a checkpoint has been called and stake again immediately after.)\n"
f"WARNING: Service {args.service_id} is staked on {current_program}, but target program is {target_program}. Unstaking..."
)
_try_unstake_service(
ledger_api=ledger_api,
service_id=args.service_id,
owner_crypto=owner_crypto,
service_registry_address=args.service_registry_address,
)

user_input = input(
f"Do you want to continue unstaking service {args.service_id} from {staking_program}? (yes/no)\n"
).lower()
print()

if user_input not in ["yes", "y"]:
print("Terminating script.")
sys.exit(1)

print(f"Unstaking service {args.service_id} from {staking_program}...")
unstake_txs = get_unstake_txs(
ledger_api, args.service_id, args.staking_contract_address
)
for tx in unstake_txs:
send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx)
print(
f"Successfully unstaked service {args.service_id} from {staking_program}."
)
sys.exit(0)

if is_service_staked(
ledger_api, args.service_id, args.staking_contract_address
):
if is_service_evicted(
ledger_api, args.service_id, args.staking_contract_address
):
Expand Down Expand Up @@ -371,14 +429,11 @@ def main() -> None:
print(
f"No rewards available. Unstaking service {args.service_id} from {staking_program}..."
)
unstake_txs = get_unstake_txs(
ledger_api, args.service_id, args.staking_contract_address
)
for tx in unstake_txs:
send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx)

print(
f"Successfully unstaked service {args.service_id} from {staking_program}."
_try_unstake_service(
ledger_api=ledger_api,
service_id=args.service_id,
owner_crypto=owner_crypto,
service_registry_address=args.service_registry_address,
)
sys.exit(0)

Expand Down

0 comments on commit 9123bcb

Please sign in to comment.