Skip to content

Commit

Permalink
feat(engine)!: publish template as substate (tari-project#1214)
Browse files Browse the repository at this point in the history
Description
---
When registering a new template on Layer-2 it should be represented as a
new substate that contains the whole binary, so it can be used to call
functions to create new components etc..
This PR contains the changes to create a new substate from a submitted
template registration/publishing.
**Please note** that these changes are working only in 1 shard group,
multi shard group version (downloading template binaries from foreign
shards) will be in another PR!

Motivation and Context
---

How Has This Been Tested?
---
1. Delete `data/processes` directory to start a fresh swarm (if there
was data left)
2. Start a new swarm with possibly 1 shard group (10 VNs max for now)
3. Create a new account
4. Add at least `200.000` tokens to it through free test tokens faucet
(on UI)
5. Build an example wasm template locally somewhere
I used this example template:
```rust
use tari_template_lib::prelude::*;

#[template]
mod counter {
    use super::*;

    pub struct Counter {
        value: u32,
    }

    impl Counter {
        pub fn new() -> Component<Self> {
            Component::new(Self { value: 0 })
                .with_access_rules(AccessRules::allow_all())
                .create()
        }

        pub fn value(&self) -> u32 {
            self.value
        }

        pub fn increase(&mut self) {
            self.value += 1;
        }

        pub fn decrease(&mut self) {
            self.value -= 1;
        }
    }
}

```
6. Publish the template to swarm
Example code (using `tari-dan` dependencies from this branch):
```rust
    let wallet_daemon_jrpc_port = 12027;
    let account_name = String::from("acc");
    let mut client = wallet_daemon_client(wallet_daemon_jrpc_port).await?;

        let AccountGetResponse { account, public_key, } = client
            .accounts_get(ComponentAddressOrName::Name(account_name.clone()))
            .await?;
        let account_component_address = account.address
            .as_component_address()
            .expect("Failed to get component address");
    
        // publish wasm template
        let wasm_binary = fs::read("./templates/counter/target/wasm32-unknown-unknown/release/counter.wasm")?;
        let tx = Transaction::builder()
            .fee_transaction_pay_from_component(account_component_address, Amount(200_000))
            .publish_template(wasm_binary)
            .build_unsigned_transaction();
        let transaction_submit_req = TransactionSubmitRequest {
            transaction: tx,
            signing_key_index: Some(account.key_index),
            detect_inputs: true,
            detect_inputs_use_unversioned: true,
            proof_ids: vec![],
            autofill_inputs: vec![],
        };
        let resp = client.submit_transaction(transaction_submit_req).await?;
        println!("Submit RESP: {resp:?}");
        let wait_req = TransactionWaitResultRequest {
            transaction_id: resp.transaction_id,
            timeout_secs: Some(120),
        };
        let wait_resp = client.wait_transaction_result(wait_req).await?;
        println!("TX RESP: {wait_resp:?}");
    
        let substate_diff = wait_resp
            .result
            .expect("No result")
            .result
            .expect("Failed to obtain substate diffs");
        let mut substate_id: Option<PublishedTemplateAddress> = None;
        let mut version: Option<u32> = None;
        for (addr, data) in substate_diff.up_iter() {
            if let SubstateId::Template(val) = addr {
                substate_id = Some(*val);
                version = Some(data.version());
                break;
            }
        }
    
        let addr = substate_id.unwrap().as_hash()?;
        println!("Published template address: {}", addr);
```

7. Test the new template by creating a new component out of that and
call it
```rust
    let counter_template_address = TemplateAddress::from_hex("d7e6f5cd2b717c83c86d3b3abf046a4caa0947e04b4e88de97a94a63ad19e382").unwrap();
println!("Creating counter component...");
    let counter_component_result = create_component(wallet_daemon_jrpc_port, account_name.clone(), counter_template_address).await?;
    println!("Counter component has been created!");
    let counter_component_substate_diff = counter_component_result
        .result
        .expect("No result")
        .result
        .expect("Failed to obtain substate diffs");
    let mut component_substate_id: Option<ComponentAddress> = None;
    let mut component_version: Option<u32> = None;
    for (addr, data) in counter_component_substate_diff.up_iter() {
        if let SubstateId::Component(_) = addr {
            let component = data.substate_value().component().unwrap();
            println!("{:?} - {:?}", component.entity_id.to_string(), component.module_name);
            component_substate_id = Some(ComponentAddress::try_from(addr.clone()).unwrap());
            component_version = Some(data.version());
            break;
        }
    }
    
    // call component
    let counter_comp_address = component_substate_id.unwrap();
    let counter_comp_version = component_version.unwrap();
    println!("{counter_comp_address:?}, {counter_comp_version:?}");
    
    call_component(wallet_daemon_jrpc_port, account_name, 0, &counter_comp_address, counter_comp_version, "increase").await?;
```

```rust
pub async fn create_component(
    wallet_daemon_jrpc_port: u16, 
    account_name: String, 
    template_address: TemplateAddress,
) -> anyhow::Result<TransactionWaitResultResponse> {
    let mut client = wallet_daemon_client(wallet_daemon_jrpc_port).await?;
    let AccountGetResponse { account, .. } = client
        .accounts_get(ComponentAddressOrName::Name(account_name.clone()))
        .await?;
    let transaction = tari_transaction::Transaction::builder()
        .fee_transaction_pay_from_component(account.address.as_component_address().unwrap(), Amount(2000))
        .call_function(template_address, String::from("new"), vec![])
        .build_unsigned_transaction();
    let transaction_submit_req = TransactionSubmitRequest {
        transaction,
        signing_key_index: Some(account.key_index),
        detect_inputs: true,
        detect_inputs_use_unversioned: false,
        proof_ids: vec![],
        autofill_inputs: vec![],
    };
    let resp = client.submit_transaction(transaction_submit_req).await?;
    let wait_req = TransactionWaitResultRequest {
        transaction_id: resp.transaction_id,
        timeout_secs: Some(120),
    };
    let wait_resp = client.wait_transaction_result(wait_req).await?;
    
    Ok(wait_resp)
}
```

```rust
pub async fn call_component(
    wallet_daemon_jrpc_port: u16,
    account_name: String,
    account_version: u32,
    input_comp_address: &ComponentAddress,
    input_comp_version: u32,
    method: &str,
) -> anyhow::Result<()> {
    let mut client = wallet_daemon_client(wallet_daemon_jrpc_port).await?;
    let AccountGetResponse { account, .. } = client
        .accounts_get(ComponentAddressOrName::Name(account_name.clone()))
        .await?;
    let account_component_address = account.address
        .as_component_address()
        .expect("Failed to get component address");

    // build tx
    let transaction = Transaction::builder()
        .fee_transaction_pay_from_component(account_component_address, Amount(1000))
        .call_method(*input_comp_address, method, vec![])
        .with_inputs(vec![
            SubstateRequirement::new(
                account_component_address.into(),
                Some(account_version),
            ),
            SubstateRequirement::new(
                (*input_comp_address).into(),
                Some(input_comp_version),
            ),
        ])
        .build_unsigned_transaction();
    // let transaction = Transaction::builder()
    //     .fee_transaction_pay_from_component(account_component_address, Amount(1000))
    //     .call_method(*input_comp_address, method, vec![])
    //     .build_unsigned_transaction();
    
    // send transactions (to create rejected ones)
    let mut join_set = JoinSet::new();
    // for _ in 0..2 {
    for _ in 0..1 {
        let tx = transaction.clone();
        join_set.spawn(async move {
            let mut client = wallet_daemon_client(wallet_daemon_jrpc_port).await?;
            let submit_req = TransactionSubmitRequest {
                transaction: tx,
                signing_key_index: Some(account.key_index),
                autofill_inputs: vec![],
                detect_inputs: true,
                detect_inputs_use_unversioned: false,
                proof_ids: vec![],
            };

            let submit_resp = client.submit_transaction(submit_req).await?;
            let wait_req = TransactionWaitResultRequest {
                transaction_id: submit_resp.transaction_id,
                timeout_secs: Some(120),
            };
            let resp = client
                .wait_transaction_result(wait_req)
                .await
                .map_err(|e| anyhow::Error::msg(e.to_string()))?;

            if let Some(reason) = resp.result.as_ref().and_then(|finalize| finalize.reject()) {
                bail!("Calling component result rejected: {}", reason);
            } 
            
            Ok(())
        });
    }

    while let Some(result) = join_set.join_next().await {
        match result {
            Ok(res) => match res {
                Ok(_) => {
                    println!("Transaction sent successfully!");
                }
                Err(error) => {
                    bail!("Transaction submit error: {error:?}");
                }
            }
            Err(error) => {
                bail!("Join error: {error:?}");
            }
        }
    }
    
    Ok(())
}
```
8. Check results in Wallet UI
- Template publish

![image](https://github.com/user-attachments/assets/a26904b2-60dc-48b6-8b58-088c62bacc28)

![image](https://github.com/user-attachments/assets/051242e1-15d1-4797-8273-e956bd881bcf)

![image](https://github.com/user-attachments/assets/f10a4e29-c38b-457c-b775-654b151d2bf1)

- Component creation

![image](https://github.com/user-attachments/assets/ef5a83d0-d533-47cf-8df7-2f653097d228)

![image](https://github.com/user-attachments/assets/7d6a9182-501f-488f-9953-f4ef83cc5a60)

- Component call

![image](https://github.com/user-attachments/assets/5c0ec274-cad4-41c4-8c95-78964724422f)

![image](https://github.com/user-attachments/assets/fd0ab37b-b1fb-4fec-926a-5c4845610111)


What process can a PR reviewer use to test or verify this change?
---
Do the test

Breaking Changes
---

- [ ] None
- [x] Requires data directory to be deleted
- [ ] Other - Please specify

---------

Co-authored-by: Stan Bondi <[email protected]>
  • Loading branch information
ksrichard and sdbondi authored Dec 12, 2024
1 parent 613d9c2 commit 0f42d39
Show file tree
Hide file tree
Showing 79 changed files with 1,488 additions and 1,000 deletions.
4 changes: 4 additions & 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 applications/tari_dan_app_utilities/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ tokio = { workspace = true, features = [
] }
tokio-stream = { workspace = true, features = ["sync"] }
config = { workspace = true }
url = { workspace = true }
93 changes: 51 additions & 42 deletions applications/tari_dan_app_utilities/src/base_layer_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use std::time::Duration;

use log::*;
use reqwest::Url;
use tari_base_node_client::{
grpc::GrpcBaseNodeClient,
types::{BaseLayerMetadata, BlockInfo},
Expand Down Expand Up @@ -61,45 +62,47 @@ use tari_dan_storage::{
StorageError,
};
use tari_dan_storage_sqlite::{error::SqliteStorageError, global::SqliteGlobalDbAdapter};
use tari_engine_types::{confidential::UnclaimedConfidentialOutput, substate::SubstateId};
use tari_engine_types::{confidential::UnclaimedConfidentialOutput, substate::SubstateId, TemplateAddress};
use tari_epoch_manager::{base_layer::EpochManagerHandle, EpochManagerError, EpochManagerReader};
use tari_shutdown::ShutdownSignal;
use tari_state_store_sqlite::SqliteStateStore;
use tari_template_lib::models::{EncryptedData, TemplateAddress, UnclaimedConfidentialOutputAddress};
use tari_template_lib::models::{EncryptedData, UnclaimedConfidentialOutputAddress};
use tokio::{task, task::JoinHandle, time};
use url::ParseError;

use crate::template_manager::interface::{TemplateManagerError, TemplateManagerHandle, TemplateRegistration};
use crate::template_manager::interface::{TemplateExecutable, TemplateManagerError, TemplateManagerHandle};

const LOG_TARGET: &str = "tari::dan::base_layer_scanner";

pub fn spawn<TAddr: NodeAddressable + 'static>(
global_db: GlobalDb<SqliteGlobalDbAdapter<TAddr>>,
base_node_client: GrpcBaseNodeClient,
epoch_manager: EpochManagerHandle<TAddr>,
template_manager: TemplateManagerHandle,
shutdown: ShutdownSignal,
consensus_constants: ConsensusConstants,
shard_store: SqliteStateStore<TAddr>,
scan_base_layer: bool,
base_layer_scanning_interval: Duration,
validator_node_sidechain_id: Option<RistrettoPublicKey>,
template_sidechain_id: Option<RistrettoPublicKey>,
burnt_utxo_sidechain_id: Option<RistrettoPublicKey>,
// TODO: remove when base layer template registration is removed too
template_manager: TemplateManagerHandle,
template_sidechain_id: Option<PublicKey>,
) -> JoinHandle<anyhow::Result<()>> {
task::spawn(async move {
let base_layer_scanner = BaseLayerScanner::new(
global_db,
base_node_client,
epoch_manager,
template_manager,
shutdown,
consensus_constants,
shard_store,
scan_base_layer,
base_layer_scanning_interval,
validator_node_sidechain_id,
template_sidechain_id,
burnt_utxo_sidechain_id,
template_manager,
template_sidechain_id,
);

base_layer_scanner.start().await?;
Expand All @@ -116,32 +119,33 @@ pub struct BaseLayerScanner<TAddr> {
next_block_hash: Option<FixedHash>,
base_node_client: GrpcBaseNodeClient,
epoch_manager: EpochManagerHandle<TAddr>,
template_manager: TemplateManagerHandle,
shutdown: ShutdownSignal,
consensus_constants: ConsensusConstants,
state_store: SqliteStateStore<TAddr>,
scan_base_layer: bool,
base_layer_scanning_interval: Duration,
has_attempted_scan: bool,
validator_node_sidechain_id: Option<PublicKey>,
template_sidechain_id: Option<PublicKey>,
burnt_utxo_sidechain_id: Option<PublicKey>,
// TODO: remove template related data, when removed base layer template registration support
template_manager: TemplateManagerHandle,
template_sidechain_id: Option<PublicKey>,
}

impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
pub fn new(
global_db: GlobalDb<SqliteGlobalDbAdapter<TAddr>>,
base_node_client: GrpcBaseNodeClient,
epoch_manager: EpochManagerHandle<TAddr>,
template_manager: TemplateManagerHandle,
shutdown: ShutdownSignal,
consensus_constants: ConsensusConstants,
state_store: SqliteStateStore<TAddr>,
scan_base_layer: bool,
base_layer_scanning_interval: Duration,
validator_node_sidechain_id: Option<PublicKey>,
template_sidechain_id: Option<PublicKey>,
burnt_utxo_sidechain_id: Option<PublicKey>,
template_manager: TemplateManagerHandle,
template_sidechain_id: Option<PublicKey>,
) -> Self {
Self {
global_db,
Expand All @@ -152,16 +156,16 @@ impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
next_block_hash: None,
base_node_client,
epoch_manager,
template_manager,
shutdown,
consensus_constants,
state_store,
scan_base_layer,
base_layer_scanning_interval,
has_attempted_scan: false,
validator_node_sidechain_id,
template_sidechain_id,
burnt_utxo_sidechain_id,
template_manager,
template_sidechain_id,
}
}

Expand Down Expand Up @@ -344,14 +348,14 @@ impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
if sidechain_feature.sidechain_public_key() != self.validator_node_sidechain_id.as_ref() {
debug!(
target: LOG_TARGET,
"Ignoring code template registration for sidechain ID {:?}. Local node's sidechain ID: {:?}",
"Ignoring code template registration for sidechain ID {:?}.",
sidechain_feature.sidechain_public_key(),
self.template_sidechain_id,
);
continue;
}
trace!(target: LOG_TARGET, "New validator node registration scanned: {reg:?}");
},
// TODO: remove completely SideChainFeature::CodeTemplateRegistration at some point
SideChainFeatureData::CodeTemplateRegistration(reg) => {
if sidechain_feature.sidechain_public_key() != self.template_sidechain_id.as_ref() {
debug!(
Expand Down Expand Up @@ -460,6 +464,32 @@ impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
Ok(())
}

async fn register_code_template_registration(
&mut self,
template_name: String,
template_address: TemplateAddress,
registration: CodeTemplateRegistration,
block_info: &BlockInfo,
) -> Result<(), BaseLayerScannerError> {
info!(
target: LOG_TARGET,
"🌠 new template found with address {} at height {}", template_address, block_info.height
);
self.template_manager
.add_template(
registration.author_public_key,
template_address,
TemplateExecutable::DownloadableWasm(
Url::parse(registration.binary_url.as_str())?,
registration.binary_sha,
),
Some(template_name),
)
.await?;

Ok(())
}

async fn update_validators(&mut self, epoch: Epoch) -> Result<(), BaseLayerScannerError> {
info!(
target: LOG_TARGET,
Expand Down Expand Up @@ -508,8 +538,8 @@ impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
) -> Result<(), BaseLayerScannerError> {
let commitment_address = UnclaimedConfidentialOutputAddress::try_from_commitment(output.commitment.as_bytes())
.map_err(|e|
// Technically impossible, but anyway
BaseLayerScannerError::InvalidSideChainUtxoResponse(format!("Invalid commitment: {}", e)))?;
// Technically impossible, but anyway
BaseLayerScannerError::InvalidSideChainUtxoResponse(format!("Invalid commitment: {}", e)))?;
let substate_id = SubstateId::UnclaimedConfidentialOutput(commitment_address);
let consensus_constants = self.epoch_manager.get_base_layer_consensus_constants().await?;
let epoch = consensus_constants.height_to_epoch(block_info.height);
Expand Down Expand Up @@ -605,29 +635,6 @@ impl<TAddr: NodeAddressable + 'static> BaseLayerScanner<TAddr> {
Ok(())
}

async fn register_code_template_registration(
&mut self,
template_name: String,
template_address: TemplateAddress,
registration: CodeTemplateRegistration,
block_info: &BlockInfo,
) -> Result<(), BaseLayerScannerError> {
info!(
target: LOG_TARGET,
"🌠 new template found with address {} at height {}", template_address, block_info.height
);
let template = TemplateRegistration {
template_name,
template_address,
registration,
mined_height: block_info.height,
mined_hash: block_info.hash,
};
self.template_manager.add_template(template).await?;

Ok(())
}

fn set_last_scanned_block(&mut self, tip: FixedHash, block_info: &BlockInfo) -> Result<(), BaseLayerScannerError> {
let mut tx = self.global_db.create_transaction()?;
let mut metadata = self.global_db.metadata(&mut tx);
Expand All @@ -652,8 +659,6 @@ pub enum BaseLayerScannerError {
SqliteStorageError(#[from] SqliteStorageError),
#[error("Epoch manager error: {0}")]
EpochManagerError(#[from] EpochManagerError),
#[error("Template manager error: {0}")]
TemplateManagerError(#[from] TemplateManagerError),
#[error("Base node client error: {0}")]
BaseNodeError(#[from] BaseNodeClientError),
#[error("Invalid side chain utxo response: {0}")]
Expand All @@ -667,6 +672,10 @@ pub enum BaseLayerScannerError {
PublicKeyConversion(ByteArrayError),
#[error("GRPC conversion error: {0}")]
GrpcConversion(String),
#[error("Template manager error: {0}")]
TemplateManagerError(#[from] TemplateManagerError),
#[error("URL parse error: {0}")]
UrlParse(#[from] ParseError),
}

enum BlockchainProgression {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// TODO: rewrite downloader to get template from other peer(s) OR completely drop this concept and implement somewhere
// else

use bytes::Bytes;
use futures::{future::BoxFuture, stream::FuturesUnordered};
use prost::bytes;
Expand Down
Loading

0 comments on commit 0f42d39

Please sign in to comment.