diff --git a/worker-manager/Cargo.toml b/worker-manager/Cargo.toml index ef7694c..1f5d159 100644 --- a/worker-manager/Cargo.toml +++ b/worker-manager/Cargo.toml @@ -25,12 +25,17 @@ default = [] backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-std = { git = "https://github.com/scrtlabs/cosmwasm", tag = "v1.1.9-secret" } -cosmwasm-storage = { git = "https://github.com/scrtlabs/cosmwasm", tag = "v1.1.9-secret" } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11" } +cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.11" } +secret-toolkit = { version = "0.10.0", default-features = false, features = [ + "storage" +] } schemars = { version = "0.8.11" } serde = { version = "1.0" } thiserror = { version = "1.0" } cosmwasm-schema = "1.0.0" +sha2 = "0.10.8" +hex = "0.4.3" # Uncomment these for some common extra tools # secret-toolkit = { git = "https://github.com/scrtlabs/secret-toolkit", tag = "v0.8.0" } diff --git a/worker-manager/Makefile b/worker-manager/Makefile index 5f026e8..38f7944 100644 --- a/worker-manager/Makefile +++ b/worker-manager/Makefile @@ -38,7 +38,7 @@ build-mainnet-reproducible: .PHONY: compress-wasm compress-wasm: - cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + cp ./target/wasm32-unknown-unknown/release/claive_worker_manager.wasm ./contract.wasm @## The following line is not necessary, may work only on linux (extra size optimization) @# wasm-opt -Os ./contract.wasm -o ./contract.wasm cat ./contract.wasm | gzip -9 > ./contract.wasm.gz diff --git a/worker-manager/Readme.md b/worker-manager/Readme.md new file mode 100644 index 0000000..fcd2d76 --- /dev/null +++ b/worker-manager/Readme.md @@ -0,0 +1,146 @@ +# Worker Manager Smart Contract + +This smart contract is designed to manage a list of workers. Each worker is registered with their IP address, payment wallet, and an attestation report. The contract allows administrators and workers to interact with and update worker information, as well as query the current state of registered workers. + +## Features + +- **Register a Worker**: Allows the registration of a worker with their IP address, payment wallet, and attestation report. +- **Set Worker Wallet**: Allows a worker to update their payment wallet. +- **Set Worker Address**: Allows a worker to update their IP address. +- **Query Workers**: Allows querying of all registered workers. +- **Liveliness Reporting**: A placeholder for liveliness reporting (yet to be implemented). +- **Work Reporting**: A placeholder for work reporting (yet to be implemented). + +## Contract Structure + +### Messages + +- **InstantiateMsg**: Used for instantiating the contract with an administrator. +- **ExecuteMsg**: Used to execute contract actions like registering a worker, setting worker details, etc. +- **QueryMsg**: Used to query the state of the contract, such as fetching workers or liveliness challenges. + +### State + +The contract stores the state using the following data structures: + +- **State**: Holds the admin address. +- **Worker**: Stores a worker's information, such as IP address, payment wallet, and attestation report. +- **WORKERS_MAP**: A mapping that associates a worker's IP address with their information. + +## Functions + +### `try_register_worker` + +Registers a worker by adding their IP address, payment wallet, and attestation report to the storage. + +### `try_set_worker_wallet` + +Allows a worker to update their payment wallet. It searches for a worker using the sender's address and updates their wallet. + +### `try_set_worker_address` + +Allows a worker to update their IP address. It searches for a worker using the sender's address and updates their IP address. + +### `try_report_liveliness` + +A placeholder function for reporting worker liveliness. This feature has yet to be implemented. + +### `try_report_work` + +A placeholder function for reporting a worker's work. This feature has yet to be implemented. + +### `query_workers` + +Queries all workers in storage and returns a list of their information. + +### `query_liveliness_challenge` + +Returns a liveliness challenge (placeholder response). This feature has yet to be implemented. + +## How to Deploy + +1. **Set up the CosmWasm environment**: + - Install the required tools for working with CosmWasm contracts, such as Rust, Cargo, and CosmWasm CLI. + +2. **Compile the Contract**: + - Use `make build` to compile the contract to WebAssembly (Wasm). + +3. **Deploy the Contract**: + - Deploy the compiled contract to the Secret Network using the `secretcli` or other relevant tools. + +4. **Interact with the Contract**: + - Once deployed, you can interact with the contract using `secretcli` or by sending transactions via the Secret Network REST or gRPC endpoints. + +## Example Queries and Transactions + +### Register a Worker + +To register a worker, execute the `RegisterWorker` action with the required parameters: + +```bash +secretcli tx compute exec '{"register_worker":{"ip_address":"192.168.1.1","payment_wallet":"secret1xyz","attestation_report":""}}' --from +``` + +### Query Workers + +To query all workers, execute the `GetWorkers` query: + +```bash +secretcli q compute query '{"get_workers": {"signature":, "subscriber_public_key":}}' +``` + +This Python example demonstrates signing a message with your private key and verifying it using the Secret SDK and secp256k1. + +```python +from secret_sdk.client.lcd import LCDClient +from secret_sdk.key.mnemonic import MnemonicKey +import secp256k1 + +chain_id = 'pulsar-3' +node_url = 'https://api.pulsar.scrttestnet.com' +mnemonic = 'grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar' + +mk = MnemonicKey(mnemonic=mnemonic) +secret = LCDClient(chain_id=chain_id, url=node_url) +wallet = secret.wallet(mk) + +private_key_secp = secp256k1.PrivateKey(bytes.fromhex(mk.private_key.hex())) +message = bytes.fromhex(mk.public_key.key.hex()) + +signature = private_key_secp.ecdsa_sign(message) +signature_der = private_key_secp.ecdsa_serialize_compact(signature) + +print("signature: ", signature_der.hex()) + +sig = private_key_secp.ecdsa_deserialize_compact(signature_der) + +is_valid = private_key_secp.pubkey.ecdsa_verify(message, sig) +print("is_valid: ", is_valid) +``` + +Expected output: + +```bash +pubkey: 034ee8249f67e136139c3ed94ad63288f6c1de45ce66fa883247211a698f440cdf +signature: 164a70762475db7f55b2fa2932ac35f761e27628ca13e9b8512137e256d93ea027ebcbb6725f23b3f720a4e00d5e57cbdc60c785437d943c62efba2bc5f61d85 +is_valid: True +``` + +### Set Worker Wallet + +To update a worker's wallet: + +```bash +secretcli tx compute exec '{"set_worker_wallet":{"ip_address":"192.168.1.1", "payment_wallet":"secret1newwallet"}}' --from +``` +### Set Worker Address + +To update a worker's wallet: + +```bash +secretcli tx compute exec '{"set_worker_address": {"new_ip_address": "", "old_ip_address": ">"}}' --from +``` + +### Notes + +- Liveliness reporting and work reporting are placeholder features and have not been implemented yet. \ No newline at end of file diff --git a/worker-manager/integration-tests/.env b/worker-manager/integration-tests/.env new file mode 100644 index 0000000..70e6096 --- /dev/null +++ b/worker-manager/integration-tests/.env @@ -0,0 +1,4 @@ +CHAIN_ID=pulsar-3 +CONTRACT_ADDRESS=secret1xy3tf8f80k7czypsn2j29z3jfcxny5x244znv7 +NODE_URL=https://api.pulsar.scrttestnet.com +MNEMONIC='grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar' \ No newline at end of file diff --git a/worker-manager/integration-tests/modify_worker.py b/worker-manager/integration-tests/modify_worker.py new file mode 100644 index 0000000..a71d156 --- /dev/null +++ b/worker-manager/integration-tests/modify_worker.py @@ -0,0 +1,73 @@ +import os +from time import sleep + +from dotenv import load_dotenv +from secret_sdk.client.lcd import LCDClient +from secret_sdk.client.localsecret import LocalSecret, main_net_chain_id, test_net_chain_id +from secret_sdk.core.coins import Coins +from secret_sdk.key.mnemonic import MnemonicKey +from secret_sdk.protobuf.cosmos.tx.v1beta1 import BroadcastMode + +load_dotenv() +chain_id = os.getenv('CHAIN_ID') +contract = os.getenv('CONTRACT_ADDRESS') +node_url = os.getenv('NODE_URL') +mnemonic = os.getenv('MNEMONIC') + +print("chain_id: " + chain_id) +print("node_url: " + node_url) +print("contract: " + contract) +print("mnemonic: " + mnemonic) + +mk = MnemonicKey(mnemonic=mnemonic) +secret = LCDClient(chain_id=chain_id, url=node_url) +wallet = secret.wallet(mk) + +wallet_public_key = str(wallet.key.acc_address) + +print("wallet_public_key: " + wallet_public_key) + +contract_address = contract +sent_funds = Coins('100uscrt') + +handle_msg = {"set_worker_address": {"new_ip_address": "192.168.0.3", "old_ip_address": "192.168.0.1"}} + +t = wallet.execute_tx( + contract_addr=contract_address, + handle_msg=handle_msg, + transfer_amount=sent_funds, +) + +print(t) + +assert t.code == 0, f"Transaction failed with code {t.code}: {t.rawlog}" +print("Transaction successful:", t.txhash) + +sleep(10) + +# test set_worker_wallet + +tx_info = secret.tx.tx_info( + tx_hash=t.txhash, +) +print("Transaction info:", tx_info) + +handle_msg = {"set_worker_wallet": {"payment_wallet": "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450s07", "ip_address": "192.168.0.3"}} + +t = wallet.execute_tx( + contract_addr=contract_address, + handle_msg=handle_msg, + transfer_amount=sent_funds, +) + +print(t) + +assert t.code == 0, f"Transaction failed with code {t.code}: {t.rawlog}" +print("Transaction successful:", t.txhash) + +sleep(10) + +tx_info = secret.tx.tx_info( + tx_hash=t.txhash, +) +print("Transaction info:", tx_info) diff --git a/worker-manager/integration-tests/query_list_of_workers.py b/worker-manager/integration-tests/query_list_of_workers.py new file mode 100644 index 0000000..7acca3e --- /dev/null +++ b/worker-manager/integration-tests/query_list_of_workers.py @@ -0,0 +1,21 @@ +import os + +from dotenv import load_dotenv +from secret_sdk.client.lcd import LCDClient + +load_dotenv() +chain_id = os.getenv('CHAIN_ID') +contract = os.getenv('CONTRACT_ADDRESS') +node_url = os.getenv('NODE_URL') + +print("chain_id: " + chain_id) +print("node_url: " + node_url) +print("contract: " + contract) + +secret = LCDClient(chain_id=chain_id, url=node_url) + +query = {"get_workers": {"signature": "", "subscriber_public_key": ""}} + +result = secret.wasm.contract_query(contract, query) + +print(result) diff --git a/worker-manager/integration-tests/register_worker.py b/worker-manager/integration-tests/register_worker.py new file mode 100644 index 0000000..315cbd5 --- /dev/null +++ b/worker-manager/integration-tests/register_worker.py @@ -0,0 +1,49 @@ +import os +from time import sleep + +from dotenv import load_dotenv +from secret_sdk.client.lcd import LCDClient +from secret_sdk.core.coins import Coins +from secret_sdk.key.mnemonic import MnemonicKey + +load_dotenv() +chain_id = os.getenv('CHAIN_ID') +contract = os.getenv('CONTRACT_ADDRESS') +node_url = os.getenv('NODE_URL') +mnemonic = os.getenv('MNEMONIC') + +print("chain_id: " + chain_id) +print("node_url: " + node_url) +print("contract: " + contract) +print("mnemonic: " + mnemonic) + +mk = MnemonicKey(mnemonic=mnemonic) +secret = LCDClient(chain_id=chain_id, url=node_url) +wallet = secret.wallet(mk) + +wallet_public_key = str(wallet.key.acc_address) + +print("wallet_public_key: " + wallet_public_key) + +contract_address = contract +sent_funds = Coins('10000uscrt') + +handle_msg = {"register_worker" : {"ip_address" : "192.0.0.1", "payment_wallet" : wallet_public_key, "attestation_report": ""}} + +t = wallet.execute_tx( + contract_addr=contract_address, + handle_msg=handle_msg, + transfer_amount=sent_funds, +) + +print(t) + +assert t.code == 0, f"Transaction failed with code {t.code}: {t.rawlog}" +print("Transaction successful:", t.txhash) + +sleep(10) + +tx_info = secret.tx.tx_info( + tx_hash=t.txhash, +) +print("Transaction info:", tx_info) diff --git a/worker-manager/src/bin/schema.rs b/worker-manager/src/bin/schema.rs index 3237571..db78ece 100644 --- a/worker-manager/src/bin/schema.rs +++ b/worker-manager/src/bin/schema.rs @@ -4,7 +4,7 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use claive_worker_manager::msg::{ - ExecuteMsg, GetLivelinessChallengeResponse, GetNextWorkerResponse, InstantiateMsg, QueryMsg, + ExecuteMsg, GetLivelinessChallengeResponse, GetWorkersResponse, InstantiateMsg, QueryMsg, }; use claive_worker_manager::state::State; @@ -18,6 +18,6 @@ fn main() { export_schema(&schema_for!(ExecuteMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(State), &out_dir); - export_schema(&schema_for!(GetNextWorkerResponse), &out_dir); + export_schema(&schema_for!(GetWorkersResponse), &out_dir); export_schema(&schema_for!(GetLivelinessChallengeResponse), &out_dir); } diff --git a/worker-manager/src/contract.rs b/worker-manager/src/contract.rs index 75fb682..37e5459 100644 --- a/worker-manager/src/contract.rs +++ b/worker-manager/src/contract.rs @@ -1,13 +1,16 @@ -use std::net::{IpAddr, Ipv4Addr}; - +use crate::msg::{ + ExecuteMsg, GetLivelinessChallengeResponse, GetModelsResponse, GetURLsResponse, + GetWorkersResponse, InstantiateMsg, MigrateMsg, QueryMsg, +}; +use crate::state::{config, State, Worker, WORKERS_MAP}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; +use sha2::{Digest, Sha256}; -use crate::msg::{ - ExecuteMsg, GetLivelinessChallengeResponse, GetNextWorkerResponse, InstantiateMsg, QueryMsg, -}; -use crate::state::{config, State}; +const _SUBSCRIBER_CONTRACT_ADDRESS: &str = "secret1ttm9axv8hqwjv3qxvxseecppsrw4cd68getrvr"; +const _SUBSCRIBER_CONTRACT_CODE_HASH: &str = + "c67de4cbe83764424192372e39abc0e040150d890600adefd6358abb6f0165ae"; #[entry_point] pub fn instantiate( @@ -36,22 +39,31 @@ pub fn execute( ) -> StdResult { match msg { ExecuteMsg::RegisterWorker { - public_key, - signature, ip_address, payment_wallet, attestation_report, + worker_type, } => try_register_worker( deps, info, - public_key, - signature, ip_address, payment_wallet, attestation_report, + worker_type, ), - ExecuteMsg::SetWorkerWallet {} => try_set_worker_wallet(deps, info), - ExecuteMsg::SetWorkerAddress {} => try_set_worker_address(deps, info), + ExecuteMsg::SetWorkerWallet { + ip_address, + payment_wallet, + } => try_set_worker_wallet(deps, info, ip_address, payment_wallet), + ExecuteMsg::SetWorkerAddress { + new_ip_address, + old_ip_address, + } => try_set_worker_address(deps, info, new_ip_address, old_ip_address), + ExecuteMsg::SetWorkerType { + ip_address, + worker_type, + } => try_set_worker_type(deps, info, ip_address, worker_type), + ExecuteMsg::RemoveWorker { ip_address } => try_remove_worker(deps, info, ip_address), ExecuteMsg::ReportLiveliness {} => try_report_liveliness(deps, info), ExecuteMsg::ReportWork {} => try_report_work(deps, info), } @@ -60,24 +72,124 @@ pub fn execute( pub fn try_register_worker( _deps: DepsMut, _info: MessageInfo, - _public_key: String, - _signature: String, - _ip_address: IpAddr, + _ip_address: String, _payment_wallet: String, _attestation_report: String, + _worker_type: String, ) -> StdResult { - // TODO: IMPLEMENT ME - Err(StdError::generic_err("not implemented")) + return Err(StdError::generic_err("Only admin can register workers")); + // Check if the sender is the admin + // let config = config_read(deps.storage); + // let state = config.load()?; + // if info.sender != state.admin { + // return Err(StdError::generic_err("Only admin can register workers")); + // } + + // let worker = Worker { + // ip_address, + // payment_wallet, + // attestation_report, + // worker_type, + // }; + + // WORKERS_MAP.insert(deps.storage, &worker.ip_address, &worker)?; + + // Ok(Response::new().set_data(to_binary(&worker)?)) } -pub fn try_set_worker_wallet(_deps: DepsMut, _info: MessageInfo) -> StdResult { - // TODO: IMPLEMENT ME - Err(StdError::generic_err("not implemented")) +pub fn try_set_worker_wallet( + deps: DepsMut, + info: MessageInfo, + ip_address: String, + payment_wallet: String, +) -> StdResult { + let worker_entry = WORKERS_MAP.get(deps.storage, &ip_address); + if let Some(worker) = worker_entry { + if info.sender != worker.payment_wallet { + return Err(StdError::generic_err( + "Only the owner has the authority to modify the payment wallet", + )); + } + let worker = Worker { + payment_wallet, + ..worker + }; + + WORKERS_MAP.insert(deps.storage, &worker.ip_address, &worker)?; + Ok(Response::new().set_data(to_binary(&worker)?)) + } else { + Err(StdError::generic_err("Didn't find worker")) + } } -pub fn try_set_worker_address(_deps: DepsMut, _info: MessageInfo) -> StdResult { - // TODO: IMPLEMENT ME - Err(StdError::generic_err("not implemented")) +pub fn try_set_worker_address( + deps: DepsMut, + info: MessageInfo, + new_ip_address: String, + old_ip_address: String, +) -> StdResult { + let worker_entry = WORKERS_MAP.get(deps.storage, &old_ip_address); + if let Some(worker) = worker_entry { + if info.sender != worker.payment_wallet { + return Err(StdError::generic_err( + "Only the owner has the authority to modify the IP address", + )); + } + let worker = Worker { + ip_address: new_ip_address.clone(), + ..worker + }; + WORKERS_MAP.remove(deps.storage, &old_ip_address)?; + WORKERS_MAP.insert(deps.storage, &new_ip_address, &worker)?; + Ok(Response::new().set_data(to_binary(&worker)?)) + } else { + Err(StdError::generic_err("Could not find the worker")) + } +} + +pub fn try_set_worker_type( + deps: DepsMut, + info: MessageInfo, + ip_address: String, + worker_type: String, +) -> StdResult { + let worker_entry = WORKERS_MAP.get(deps.storage, &ip_address); + if let Some(worker) = worker_entry { + if info.sender != worker.payment_wallet { + return Err(StdError::generic_err( + "Only the owner has the authority to modify the worker_type", + )); + } + let worker = Worker { + worker_type, + ..worker + }; + + WORKERS_MAP.insert(deps.storage, &worker.ip_address, &worker)?; + Ok(Response::new().set_data(to_binary(&worker)?)) + } else { + Err(StdError::generic_err("Didn't find worker")) + } +} + +pub fn try_remove_worker( + deps: DepsMut, + info: MessageInfo, + ip_address: String, +) -> StdResult { + let worker_entry = WORKERS_MAP.get(deps.storage, &ip_address); + if let Some(worker) = worker_entry { + if info.sender != worker.payment_wallet { + return Err(StdError::generic_err( + "Only the owner has the authority to remove the worker", + )); + } + + WORKERS_MAP.remove(deps.storage, &ip_address)?; + Ok(Response::new().set_data(to_binary(&worker)?)) + } else { + Err(StdError::generic_err("Didn't find worker")) + } } pub fn try_report_liveliness(_deps: DepsMut, _info: MessageInfo) -> StdResult { @@ -93,25 +205,165 @@ pub fn try_report_work(_deps: DepsMut, _info: MessageInfo) -> StdResult StdResult { match msg { - QueryMsg::GetNextWorker { + QueryMsg::GetWorkers { signature, subscriber_public_key, - } => to_binary(&query_next_worker(deps, signature, subscriber_public_key)?), + } => to_binary(&query_workers(deps, signature, subscriber_public_key)?), QueryMsg::GetLivelinessChallenge {} => to_binary(&query_liveliness_challenge(deps)?), + QueryMsg::GetModels { + // signature, + // subscriber_public_key, + } => to_binary(&query_models(deps)?), + QueryMsg::GetURLs { + // signature, + // subscriber_public_key, + model, + } => to_binary(&query_urls(deps, model)?), + } +} + +#[entry_point] +pub fn migrate(_deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + match msg { + MigrateMsg::Migrate {} => Ok(Response::default()), + MigrateMsg::StdError {} => Err(StdError::generic_err("this is an std error")), } } -fn query_next_worker( +fn signature_verification( + deps: Deps, + signature: String, + sender_public_key: String, +) -> StdResult { + let public_key_hex = sender_public_key.clone(); + let signature_hex = signature.clone(); + + let public_key_bytes = hex::decode(public_key_hex.clone()) + .map_err(|_| StdError::generic_err("Invalid public key hex"))?; + + let signature_bytes = + hex::decode(signature_hex).map_err(|_| StdError::generic_err("Invalid signature hex"))?; + + let message_hash = Sha256::digest(public_key_bytes.clone()); + + deps + .api + .secp256k1_verify(&message_hash, &signature_bytes, &public_key_bytes) + .map_err(|e| { + StdError::generic_err("Failed to verify signature: ".to_string() + &e.to_string()) + }) +} + +fn query_workers( + deps: Deps, + signature: String, + sender_public_key: String, +) -> StdResult { + let verify = signature_verification(deps, signature, sender_public_key)?; + if !verify { + return Err(StdError::generic_err("Signature verification failed")); + } + + // let subs = SubscriberStatusQuery { + // subscriber_status: SubscriberStatus { + // public_key: sender_public_key, + // }, + // }; + + // let query_msg = to_binary(&subs)?; + + // let res: Result = deps.querier.query( + // &cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart { + // contract_addr: SUBSCRIBER_CONTRACT_ADDRESS.into(), + // code_hash: SUBSCRIBER_CONTRACT_CODE_HASH.into(), + // msg: query_msg, + // }), + // ); + + // match res { + // Ok(subscriber_status) => { + // if subscriber_status.active { + // let workers: Vec<_> = WORKERS_MAP + // .iter(deps.storage)? + // .map(|x| { + // if let Ok((_, worker)) = x { + // Some(worker) + // } else { + // None + // } + // }) + // .filter_map(|x| x) + // .collect(); + + // Ok(GetWorkersResponse { workers }) + // } else { + // Err(StdError::generic_err("Subscriber isn't active")) + // } + // } + // Err(err) => Err(StdError::generic_err( + // "Failed to deserialize subscriber response: ".to_string() + &err.to_string(), + // )), + // } + + let workers: Vec<_> = WORKERS_MAP + .iter(deps.storage)? + .filter_map(|x| x.ok().map(|(_, worker)| worker)) + .collect(); + + Ok(GetWorkersResponse { workers }) +} + +fn query_models( _deps: Deps, - _signature: String, - _sender_public_key: String, -) -> StdResult { - // TODO: IMPLEMENT ME - Ok(GetNextWorkerResponse { - ip_address: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + // signature: String, + // sender_public_key: String, +) -> StdResult { + // let verify = signature_verification(deps, signature, sender_public_key)?; + // if !verify { + // return Err(StdError::generic_err("Signature verification failed")); + // } + + Ok(GetModelsResponse { + models: vec!["deepseek-r1:70b".into(), "llama3.2-vision".into(), "gemma3:4b".into(), "stt-whisper".into(),"tts-kokoro".into()], }) } +fn query_urls( + _deps: Deps, + // signature: String, + // sender_public_key: String, + model: Option, +) -> StdResult { + // let verify = signature_verification(deps, signature, sender_public_key)?; + // if !verify { + // return Err(StdError::generic_err("Signature verification failed")); + // } + + let urls = match model.as_deref() { + // LLM models + Some("deepseek-r1:70b") | Some("gemma3:4b") | Some("llama3.2-vision") => { + vec!["https://secretai-rytn.scrtlabs.com:21434".into()] + } + + // Speech-to-text + Some("stt-whisper") => { + vec!["https://secretai-rytn.scrtlabs.com:25436".into()] + } + + // Text-to-speech + Some("tts-kokoro") => { + vec!["https://secretai-rytn.scrtlabs.com:25435".into()] + } + + // Default → no urls + _ => { + vec![] + } + }; + + Ok(GetURLsResponse { urls }) +} + fn query_liveliness_challenge(_deps: Deps) -> StdResult { // TODO: IMPLEMENT ME Ok(GetLivelinessChallengeResponse {}) @@ -119,9 +371,15 @@ fn query_liveliness_challenge(_deps: Deps) -> StdResult ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("admin", &[]); + let msg = InstantiateMsg {}; + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg); + + (res, deps) + } + + fn register_worker( + deps: &mut OwnedDeps, + ip_address: String, + payment_wallet: String, + attestation_report: String, + worker_type: String, + ) -> StdResult { + let execute_msg = ExecuteMsg::RegisterWorker { + ip_address, + payment_wallet: payment_wallet.clone(), + attestation_report, + worker_type, + }; + execute( + deps.as_mut(), + mock_env(), + mock_info(&payment_wallet, &[]), + execute_msg, + ) + } + + #[test] + fn register_worker_success() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let res = register_worker( + &mut deps, + IP_ADDRESS.into(), + PAYMENT_WALLET.into(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + + let worker: Worker = from_binary(&res.data.unwrap()).unwrap(); + assert_eq!( + worker, + Worker { + ip_address: IP_ADDRESS.into(), + payment_wallet: PAYMENT_WALLET.into(), + attestation_report: ATTESTATION_REPORT.into(), + worker_type: WORKER_TYPE.into(), + } + ); + } + + #[test] + fn set_worker_wallet() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let res = register_worker( + &mut deps, + IP_ADDRESS.into(), + PAYMENT_WALLET.into(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let new_payment_wallet = "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450007".to_string(); + let execute_msg = ExecuteMsg::SetWorkerWallet { + ip_address: IP_ADDRESS.into(), + payment_wallet: new_payment_wallet.clone(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PAYMENT_WALLET, &[]), + execute_msg, + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let worker: Worker = from_binary(&res.data.unwrap()).unwrap(); + assert_eq!( + worker, + Worker { + ip_address: IP_ADDRESS.into(), + payment_wallet: new_payment_wallet, + attestation_report: ATTESTATION_REPORT.into(), + worker_type: WORKER_TYPE.into(), + } + ); + } + + #[test] + fn set_worker_wallet_unauthorized() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let res = register_worker( + &mut deps, + IP_ADDRESS.into(), + PAYMENT_WALLET.into(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let new_payment_wallet = "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450007".to_string(); + let execute_msg = ExecuteMsg::SetWorkerWallet { + ip_address: IP_ADDRESS.into(), + payment_wallet: new_payment_wallet.clone(), + }; + + // set as sender foreign wallet + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(&new_payment_wallet, &[]), + execute_msg, + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + StdError::generic_err("Only the owner has the authority to modify the payment wallet",) + ); + } + + #[test] + fn set_worker_address() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let res = register_worker( + &mut deps, + IP_ADDRESS.into(), + PAYMENT_WALLET.into(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let new_ip_address = String::from("147.4.4.7"); + let execute_msg = ExecuteMsg::SetWorkerAddress { + new_ip_address: new_ip_address.clone(), + old_ip_address: IP_ADDRESS.into(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PAYMENT_WALLET, &[]), + execute_msg, + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let worker: Worker = from_binary(&res.data.unwrap()).unwrap(); + assert_eq!( + worker, + Worker { + ip_address: new_ip_address, + payment_wallet: PAYMENT_WALLET.into(), + attestation_report: ATTESTATION_REPORT.into(), + worker_type: WORKER_TYPE.into(), + } + ); + } + + #[test] + fn set_worker_address_unauthorized() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let res = register_worker( + &mut deps, + IP_ADDRESS.into(), + PAYMENT_WALLET.into(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let new_ip_address = String::from("147.4.4.7"); + let execute_msg = ExecuteMsg::SetWorkerAddress { + new_ip_address: new_ip_address.clone(), + old_ip_address: IP_ADDRESS.into(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("fake_acc", &[]), + execute_msg, + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + StdError::generic_err("Only the owner has the authority to modify the IP address",) + ); + } + + #[test] + fn remove_worker() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let ip_address_1 = String::from("127.0.0.1"); + let ip_address_2 = String::from("127.0.0.2"); + + let payment_wallet_1 = "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450s03".to_string(); + + let res = register_worker( + &mut deps, + ip_address_1.clone(), + payment_wallet_1.clone(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let res = register_worker( + &mut deps, + ip_address_2.clone(), + payment_wallet_1.clone(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let execute_msg = ExecuteMsg::RemoveWorker { + ip_address: ip_address_1.clone(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PAYMENT_WALLET, &[]), + execute_msg, + ) + .unwrap(); + let worker: Worker = from_binary(&res.data.unwrap()).unwrap(); + assert_eq!(worker.ip_address, ip_address_1); + + let execute_msg = ExecuteMsg::RemoveWorker { + ip_address: ip_address_2.clone(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PAYMENT_WALLET, &[]), + execute_msg, + ) + .unwrap(); + let worker: Worker = from_binary(&res.data.unwrap()).unwrap(); + assert_eq!(worker.ip_address, ip_address_2); + + let execute_msg = ExecuteMsg::RemoveWorker { + ip_address: ip_address_2.clone(), + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PAYMENT_WALLET, &[]), + execute_msg, + ); + assert!(res.is_err()); + } + + #[test] + fn query_workers() { + let (res, mut deps) = init_contract(); + assert_eq!(res.unwrap().messages.len(), 0); + + let ip_address_1 = String::from("127.0.0.1"); + let ip_address_2 = String::from("127.0.0.2"); + let ip_address_3 = String::from("127.0.0.3"); + + let payment_wallet_1 = "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450s03".to_string(); + let payment_wallet_2 = "secret1ap26qrlp8mcq2pg6r47w43l0y8zkqm8a450s07".to_string(); + + let res = register_worker( + &mut deps, + ip_address_1.clone(), + payment_wallet_1.clone(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let res = register_worker( + &mut deps, + ip_address_2.clone(), + payment_wallet_1.clone(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let res = register_worker( + &mut deps, + ip_address_3.clone(), + payment_wallet_2.clone(), + ATTESTATION_REPORT.into(), + WORKER_TYPE.into(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + let message = + hex::decode("034ee8249f67e136139c3ed94ad63288f6c1de45ce66fa883247211a698f440cdf") + .unwrap(); + let priv_key = + hex::decode("f0a7b67eb9a719d54f8a9bfbfb187d8c296b97911a05bf5ca30494823e46beb6") + .unwrap(); + + let sign = deps.api.secp256k1_sign(&message, &priv_key).unwrap(); + + let query_msg = QueryMsg::GetWorkers { + signature: sign.encode_hex(), + subscriber_public_key: + "034ee8249f67e136139c3ed94ad63288f6c1de45ce66fa883247211a698f440cdf".to_string(), + }; + let res = query(deps.as_ref(), mock_env(), query_msg); + + assert!(res.is_ok()); + // dbg!(res); + } } diff --git a/worker-manager/src/msg.rs b/worker-manager/src/msg.rs index 3626e84..8e9b3f2 100644 --- a/worker-manager/src/msg.rs +++ b/worker-manager/src/msg.rs @@ -1,8 +1,8 @@ -use std::net::IpAddr; - use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::state::Worker; + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] pub struct InstantiateMsg {} @@ -10,14 +10,26 @@ pub struct InstantiateMsg {} #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { RegisterWorker { - public_key: String, - signature: String, - ip_address: IpAddr, + ip_address: String, payment_wallet: String, attestation_report: String, + worker_type: String, + }, + SetWorkerWallet { + ip_address: String, + payment_wallet: String, + }, + SetWorkerAddress { + new_ip_address: String, + old_ip_address: String, + }, + SetWorkerType { + ip_address: String, + worker_type: String, + }, + RemoveWorker { + ip_address: String, }, - SetWorkerWallet {}, - SetWorkerAddress {}, ReportLiveliness {}, ReportWork {}, } @@ -25,18 +37,64 @@ pub enum ExecuteMsg { #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetNextWorker { + GetWorkers { signature: String, subscriber_public_key: String, }, + GetModels { + // signature: String, + // subscriber_public_key: String, + }, + GetURLs { + // signature: String, + // subscriber_public_key: String, + model: Option, + }, GetLivelinessChallenge {}, } // We define a custom struct for each query response #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] -pub struct GetNextWorkerResponse { - pub ip_address: IpAddr, +pub struct GetWorkersResponse { + pub workers: Vec, +} + +// We define a custom struct for each query response +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct GetModelsResponse { + pub models: Vec, +} + +// We define a custom struct for each query response +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct GetURLsResponse { + pub urls: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] pub struct GetLivelinessChallengeResponse {} + +// We define a custom struct for each query response +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct SubscriberStatusResponse { + pub active: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SubscriberStatusQuery { + pub subscriber_status: SubscriberStatus, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SubscriberStatus { + pub public_key: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum MigrateMsg { + Migrate {}, + StdError {}, +} diff --git a/worker-manager/src/state.rs b/worker-manager/src/state.rs index decfe48..ff48cdd 100644 --- a/worker-manager/src/state.rs +++ b/worker-manager/src/state.rs @@ -1,4 +1,5 @@ use schemars::JsonSchema; +use secret_toolkit::storage::Keymap; use serde::{Deserialize, Serialize}; use cosmwasm_std::{Addr, Storage}; @@ -11,6 +12,15 @@ pub struct State { pub admin: Addr, } +pub static WORKERS_MAP: Keymap = Keymap::new(b"workers"); +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct Worker { + pub ip_address: String, + pub payment_wallet: String, + pub attestation_report: String, + pub worker_type: String, +} + pub fn config(storage: &mut dyn Storage) -> Singleton { singleton(storage, CONFIG_KEY) }