Trampoline is a full stack development framework currently under development. Trampoline has three complementary purposes:
- Enable prospective developers to get started as quickly as possible
- Enable more familiar CKB developers to easily build composable services & protocols
- Make it easy for developers to utilize all of the other wonderful tools developer in the Nervos ecosystem
Points 1 and 3 are very useful for eliminating inconveniences in the development workflow, providing a “create-react-app”-like experience for fullstack ckb dapps. Further, trampoline will include (as of next week) an orchestration service that will allow developers to run a multi-chain dev net pre-configured for ckb development, with bridge, validators, etc. This also applies to multi-chain, with devs easily able to start a local L2 instance. All from a single intuitive API.
The point I really want to talk about though is point 2. As mentioned in this wonderful post on interoperation and composability in CKB, we often define standards, specifications, and protocols by describing a set of transactions, their constraints, and the “actions” or operations they represent.
This is far more semantically rich way of describing an interface than the ABI we are familiar with in the account model.
There’s still a big limitation of the CKB approach though that makes the ABI of Ethereum more useful in some situations: the ABI is machine readable. Despite the account model ABI’s emphasis on a callable interface that does not describe the semantics of the operations exposed through function names, it is at least machine readable, making it easier to get past the first step of integration with new smart contracts.
Machine Readable ABI for CKB Smart Contracts
If we could encode the much more semantically rich “ABI” of CKB transactions, in a machine readable fashion, we could achieve amazing things:
- Automatically deserialize the raw data in cells, making the activity on CKB more accessible
- Automatically interact with ckb systems without having to write code to generate the potentially complicated transactions that use a collection of different scripts (i.e., any software service that can read this ABI can automatically generate a compatible transaction… imagine how much faster we could build integrations)
- Automatically check the compatibility between different scripts. We can detect ahead of time if two different scripts on CKB (or sets of scripts) have operations which could be combined into a single transaction, which is pretty important for composable operations
We are developing as a part of Trampoline’s larger efforts a “DSL” that mirrors the natural way we describe smart contracts on CKB. A prototypical example of the DSL-version of the SUDT standard is shown below:
def Standard "SUDT" {
def Parameter "privileged" {
Is HashedValue::blake2b,
Sourced Self::Args
}
def Source "Amount" {
Is Uint(128),
From Self::Data
}
def Rule "OnlyPrivileged" {
Some(Input | Hash::blake2b(Input::lock) == "privileged")
}
def Rule "NoIssuance" {
ForAll(x in Self::Inputs, y in Self::Outputs | SumOf(x::Amount) == SumOf(y::Amount))
}
def Rule "Issuance" {
ForAll(x in Self::Inputs, y in Self::Outputs | SumOf(x::Amount) < SumOf(y::Amount))
}
def Operation "mint" {
ConstrainedBy(Rules::OnlyPrivileged),
ConstrainedBy(Rules::Issuance)
}
def Operation "transfer" {
ConstrainedBy(Rules::NoIssuance),
ConstrainedBy(Rules::OnlyPrivileged)
}
}
This DSL is inspired by the way we have come to naturally describe smart contracts on CKB, as well as by the simplicity of TLA+ - a formal language for specifying discrete dynamical systems.
The language is basically a combination of a couple special types: Parameters, Sources, Rules, Operations, and convenient structures for expressing predicates and relations. I will describe these constructs in greater detail in a subsequent post.
Initially, this DSL (named “Validity”), will be used to generate transaction generation code. Eventually, and if there is demand, we can apply it to code generation for on-chain ckb scripts as well. I believe this will be especially apt for CKB since on-chain scripts replicate (though in a different context) much of the same core logic implemented in off-chain transaction generation.
Transaction Composition with Pipelines
The second thing we are working on, and will make its way into trampoline before the Validity DSL, is a new Contract API for transaction generation. This API uses the concept of pipelining. Essentially, an empty transaction can be passed through a sequence of Contracts, each of which updates the transaction based on contract-specific concerns. Here is a prototypical example of this (this feature will be available by the end of January and its foundations can be seen in the main branch of Trampoline repository already):
use trampoline::contracts::{SudtContract, PWLockContract, NRC721Contract, ContractSource};
use trampoline::tx::{Signer, Generator};
fn main() {
// Load some accounts
let eth_key_pair = Signer::load_from_key_path(env!("KEY_PATH"));
let eth_signer = PwLockContract::default()
.signer(eth_key_pair);
let random_eth_account = PwLockContract::default()
.random_signer();
// Define operation on SUDT Contract:
// -- Transfer 1400 SUDT to a random_eth_account, refund the rest back to self
let sudt_contract = SudtContract::default().source(ContractSource::BuiltIn)
.add_input_query(SudtContract::Fields::Amount, 2000, eth_signer.as_address());
.add_output_rule(SudtContract::Fields::Amount, 1400, random_eth_account.as_address());
// Define operation on SUDT Contract:
// -- Update the token uri to v2.json (trivial example)
let nft_contract = NRC721Contract::default().source(ContractSource::BuiltIn);
.add_input_query("owner", eth_signer.as_address())
.add_output_rule("owner", eth_signer.as_address())
.add_output_rule(NRC721Contract::Fields::TokenURI, |uri| format!("{}/v2.json", uri));
// Instantiate generator with pipeline of contracts
// Verify the transaction is valid (the contracts are compatible)
// Send the transaction
let composed_tx_result = Generator::default()
.indexer("http://localhost:8116")
.rpc("http://localhost:8114")
.with_accounts(vec![eth_signer, random_eth_account])
.pipeline(vec![&sudt_contract, &nft_contract])
.synchronous_send();
}
This method of interacting with ckb smart contracts will let us more easily take advantage of composability inherent in CKB’s programming model. Further, the generator checks ahead of time by default that the transaction is valid and won’t fail (only possible due to the deterministic nature of CKB transactions at transaction-generation time). New contracts interfaces can be easily implemented (and projects generated with trampoline’s cli have a contracts
directory where these can live) but we will also add support for well known contracts in the ecosystem so that ckb dapp developers need not implement that logic in their new dapps in order to take advantage of the existing, popular contracts in the ecosystem.