Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ New Staking Contract #453

Merged
merged 19 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ hyle-contracts = { path = "./contracts", package = "hyle-contracts" }
hydentity = { path = "./contracts/hydentity" }
hyllar = { path = "./contracts/hyllar" }
staking = { path = "./contracts/staking" }
bonsai-runner = { path = "./crates/bonsai-runner" }
config = "0.14.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json"] }
Expand Down
43 changes: 3 additions & 40 deletions contract-sdk/src/guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use bincode::{Decode, Encode};
use serde::de::DeserializeOwned;

use crate::{
flatten_blobs, Blob, BlobIndex, ContractInput, Digestable, HyleOutput, Identity,
StructuredBlob, StructuredBlobData,
flatten_blobs,
utils::{parse_blob, parse_structured_blob},
ContractInput, Digestable, HyleOutput, Identity, StructuredBlob, StructuredBlobData,
};

#[cfg(feature = "risc0")]
Expand Down Expand Up @@ -95,44 +96,6 @@ where
Ok((input, parsed_blob, caller))
}

pub fn parse_blob<Parameters>(blobs: &[Blob], index: &BlobIndex) -> Parameters
where
Parameters: Decode,
{
let blob = match blobs.get(index.0) {
Some(v) => v,
None => {
panic!("unable to find the payload");
}
};

let (parameters, _) =
bincode::decode_from_slice(blob.data.0.as_slice(), bincode::config::standard())
.expect("Failed to decode payload");
parameters
}

pub fn parse_structured_blob<Parameters>(
blobs: &[Blob],
index: &BlobIndex,
) -> StructuredBlob<Parameters>
where
Parameters: Decode,
{
let blob = match blobs.get(index.0) {
Some(v) => v,
None => {
panic!("unable to find the payload");
}
};

let parsed_blob: StructuredBlob<Parameters> = StructuredBlob::try_from(blob.clone())
.unwrap_or_else(|e| {
panic!("Failed to decode blob: {:?}", e);
});
parsed_blob
}

pub fn commit<State>(input: ContractInput<State>, new_state: State, res: crate::RunResult)
where
State: Digestable,
Expand Down
1 change: 1 addition & 0 deletions contract-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod erc20;
#[cfg(any(feature = "risc0", feature = "sp1"))]
pub mod guest;
pub mod identity_provider;
pub mod utils;

#[cfg(feature = "tracing")]
pub use tracing;
Expand Down
41 changes: 41 additions & 0 deletions contract-sdk/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use bincode::Decode;

use crate::{Blob, BlobIndex, StructuredBlob};

pub fn parse_blob<Parameters>(blobs: &[Blob], index: &BlobIndex) -> Parameters
where
Parameters: Decode,
{
let blob = match blobs.get(index.0) {
Some(v) => v,
None => {
panic!("unable to find the payload");
}
};

let (parameters, _) =
bincode::decode_from_slice(blob.data.0.as_slice(), bincode::config::standard())
.expect("Failed to decode payload");
parameters
}

pub fn parse_structured_blob<Parameters>(
blobs: &[Blob],
index: &BlobIndex,
) -> StructuredBlob<Parameters>
where
Parameters: Decode,
{
let blob = match blobs.get(index.0) {
Some(v) => v,
None => {
panic!("unable to find the payload");
}
};

let parsed_blob: StructuredBlob<Parameters> = StructuredBlob::try_from(blob.clone())
.unwrap_or_else(|e| {
panic!("Failed to decode blob: {:?}", e);
});
parsed_blob
}
Binary file modified contracts/amm/amm.img
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/amm/amm.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c4139707b00dff1ea901fac1cb5edf7bddbf740c1815f2943c38626cbdf3841e
5dcae35e603495a426fa821a0d15fe43268a99addead70b87057078c925578e7
4 changes: 2 additions & 2 deletions contracts/amm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ fn main() {
};
}

let execution_state = ExecutionContext {
let execution_ctx = ExecutionContext {
callees_blobs: callees_blobs.into(),
caller,
};
let amm_state = input.initial_state.clone();
let mut amm_contract = AmmContract::new(execution_state, parsed_blob.contract_name, amm_state);
let mut amm_contract = AmmContract::new(execution_ctx, parsed_blob.contract_name, amm_state);

let amm_action = parsed_blob.data.parameters;

Expand Down
Binary file modified contracts/hydentity/hydentity.img
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/hydentity/hydentity.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b79c5b90b5ea1edda3366304eef494f3923d3747e91a90333f7c8976b80cb3ca
8b71902dc20d7e2693645365169506e11cb4eab19e1f8bbbad262cbd60ecd22d
Binary file modified contracts/hyllar/hyllar.img
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/hyllar/hyllar.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bfe8689c748594ae6c53916a688366bd494b07998706a71ab770bbf2b7524348
9cdb9039b39c5799c41629d736d69062eea31d06586c64e097d69714a908575b
205 changes: 50 additions & 155 deletions contracts/staking/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,195 +1,90 @@
use core::fmt;

use anyhow::Result;
use bincode::{Decode, Encode};
use model::ValidatorPublicKey;
use sdk::{info, Blob, BlobData, ContractName, Digestable};
use model::{BlockHeight, ValidatorPublicKey};
use sdk::{
caller::{CalleeBlobs, CallerCallee, ExecutionContext, MutCalleeBlobs},
Blob, BlobData, BlobIndex, ContractName, Identity, StructuredBlobData,
};
use serde::{Deserialize, Serialize};
use state::OnChainState;

pub mod model;
pub mod state;

#[cfg(feature = "metadata")]
pub mod metadata {
pub const STAKING_ELF: &[u8] = include_bytes!("../staking.img");
pub const PROGRAM_ID: [u8; 32] = sdk::str_to_u8(include_str!("../staking.txt"));
}

#[derive(Encode, Decode, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct RewardsClaim {
block_heights: Vec<BlockHeight>,
}

/// Enum representing the actions that can be performed by the IdentityVerification contract.
#[derive(Encode, Decode, Debug, Clone)]
#[derive(Encode, Decode, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum StakingAction {
Stake { amount: u128 },
Delegate { validator: ValidatorPublicKey },
Distribute { claim: RewardsClaim },
}

impl StakingAction {
pub fn as_blob(self, contract_name: ContractName) -> Blob {
pub fn as_blob(
self,
contract_name: ContractName,
caller: Option<BlobIndex>,
callees: Option<Vec<BlobIndex>>,
) -> Blob {
Blob {
contract_name,
data: BlobData(
bincode::encode_to_vec(self, bincode::config::standard())
.expect("failed to encode program inputs"),
),
data: BlobData::from(StructuredBlobData {
caller,
callees,
parameters: self.clone(),
}),
}
}
}

#[derive(Debug, Encode, Decode, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Staker {
pub pubkey: ValidatorPublicKey,
pub stake: Stake,
pub struct StakingContract {
exec_ctx: ExecutionContext,
state: state::Staking,
}

impl fmt::Display for Staker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} with stake {:?}", self.pubkey, self.stake.amount)
impl CallerCallee for StakingContract {
fn caller(&self) -> &Identity {
&self.exec_ctx.caller
}
}

#[derive(Debug, Encode, Decode, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Stake {
pub amount: u64,
}

#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq, Hash)]
pub struct Staking {
stakers: Vec<Staker>,
bonded: Vec<ValidatorPublicKey>,
total_bond: u64,
}

/// Minimal stake necessary to be part of consensus
pub const MIN_STAKE: u64 = 32;

impl Staking {
pub fn new() -> Self {
Staking {
stakers: Default::default(),
bonded: Default::default(),
total_bond: 0,
}
fn callee_blobs(&self) -> CalleeBlobs {
CalleeBlobs(self.exec_ctx.callees_blobs.borrow())
}

/// Add a staking validator
pub fn add_staker(&mut self, staker: Staker) -> Result<(), String> {
if self.stakers.iter().any(|s| s.pubkey == staker.pubkey) {
return Err("Validator already staking".to_string());
}
info!("💰 New staker {}", staker);
self.stakers.push(staker);
Ok(())
}

/// list bonded validators
pub fn bonded(&self) -> &Vec<ValidatorPublicKey> {
&self.bonded
}

/// Unbond a staking validator
pub fn unbond(&mut self, validator: &ValidatorPublicKey) -> Result<Stake, String> {
if let Some(staker) = self.stakers.iter().find(|s| &s.pubkey == validator) {
if !self.bonded.contains(validator) {
return Err("Validator already unbonded".to_string());
}
self.bonded.retain(|v| v != validator);
self.total_bond -= staker.stake.amount;
Ok(staker.stake)
} else {
Err("Validator not staking".to_string())
}
}

/// Get the total bonded amount
pub fn total_bond(&self) -> u64 {
self.total_bond
fn mut_callee_blobs(&self) -> MutCalleeBlobs {
MutCalleeBlobs(self.exec_ctx.callees_blobs.borrow_mut())
}
}

/// Get the stake for a validator
pub fn get_stake(&self, validator: &ValidatorPublicKey) -> Option<Stake> {
self.stakers
.iter()
.find(|s| &s.pubkey == validator)
.map(|s| s.stake)
impl StakingContract {
pub fn new(exec_ctx: ExecutionContext, state: state::Staking) -> Self {
StakingContract { exec_ctx, state }
}

/// Bond a staking validator
pub fn bond(&mut self, validator: ValidatorPublicKey) -> Result<Stake, String> {
info!("🔐 Bonded validator {}", validator);
if let Some(staker) = self.stakers.iter().find(|s| s.pubkey == validator) {
if self.bonded.contains(&validator) {
return Err("Validator already bonded".to_string());
pub fn execute_action(&mut self, action: StakingAction) -> Result<String, String> {
match action {
StakingAction::Stake { amount } => self.state.stake(self.caller().clone(), amount),
StakingAction::Delegate { validator } => {
self.state.delegate_to(self.caller().clone(), validator)
}
self.bonded.push(validator);
self.bonded.sort(); // TODO insert in order?
self.total_bond += staker.stake.amount;
Ok(staker.stake)
} else {
Err("Validator not staking".to_string())
StakingAction::Distribute { claim: _ } => todo!(),
}
}

pub fn is_bonded(&self, pubkey: &ValidatorPublicKey) -> bool {
self.bonded.iter().any(|v| v == pubkey)
}
}

impl Default for Staking {
fn default() -> Self {
Self::new()
}
}

impl Digestable for Staking {
fn as_digest(&self) -> sdk::StateDigest {
sdk::StateDigest(
bincode::encode_to_vec(self, bincode::config::standard())
.expect("Failed to encode Balances"),
)
}
}

impl TryFrom<sdk::StateDigest> for Staking {
type Error = anyhow::Error;

fn try_from(state: sdk::StateDigest) -> Result<Self, Self::Error> {
let (balances, _) = bincode::decode_from_slice(&state.0, bincode::config::standard())
.map_err(|_| anyhow::anyhow!("Could not decode start height"))?;
Ok(balances)
}
}

#[cfg(test)]
mod test {

use super::*;
#[test]
fn test_staking() {
let mut staking = Staking::new();
let staker = Staker {
pubkey: ValidatorPublicKey::default(),
stake: Stake { amount: 100 },
};
assert!(staking.add_staker(staker.clone()).is_ok());
assert_eq!(staking.total_bond(), 0);
let stake = staking.bond(staker.pubkey.clone()).unwrap();
assert_eq!(staking.total_bond(), 100);
assert_eq!(stake.amount, 100);
let stake = staking.unbond(&staker.pubkey).unwrap();
assert_eq!(staking.total_bond(), 0);
assert_eq!(stake.amount, 100);
pub fn on_chain_state(&self) -> OnChainState {
self.state.on_chain_state()
}

#[test]
fn test_errors() {
let mut staking = Staking::new();
let staker = Staker {
pubkey: ValidatorPublicKey::default(),
stake: Stake { amount: 100 },
};
assert!(staking.bond(staker.pubkey.clone()).is_err());
assert!(staking.unbond(&staker.pubkey).is_err());
assert!(staking.add_staker(staker.clone()).is_ok());
assert!(staking.bond(staker.pubkey.clone()).is_ok());
assert!(staking.bond(staker.pubkey.clone()).is_err());
assert!(staking.unbond(&staker.pubkey).is_ok());
assert!(staking.unbond(&staker.pubkey).is_err());
pub fn state(self) -> state::Staking {
self.state
}
}
Loading
Loading