(deprecated) Cross-shard transfers
NOTE: This document is DEPRECATED. A new version will be published at some point.
Motivation
The purpose of the protocol is to transfer value between independent shards.
The shards could be either RChain networks or any other network that implements the protocol.
Each client trusts his own shard and it's ancestors all the way up to the root shard. Clients living in two different shards will not both trust the other party's shard.
For this reasons, when two parties are transferring value between two arbitrary shards, they should meet at the least common ancestor, where the value exchange between the two parties becomes a normal blockchain transaction.
The cross-shard value transfer protocol provides a way for each of the clients to move value from their own wallets to a parent shard or back.
Outstanding issues
- how to do weighted multi-signature
- notification on block finalization (check with Michael Birch (Unlicensed) & Former user (Deleted))
- Should we have a shard DAG in a future version?
- how to roll-back cross-shard transfers when something goes wrong during the transfer?
- how to mount an existing shard?
- How to unmount a shard without destroying it?
- Why not register all the wallets in a public WalletRegistry?
- Do we sign the "approved" contracts used for REV?
- How to avoid the situation when the validators move their stakes out of the child shard while still holding their validator status?
- How does interaction with PoS contract happens at bonding/unbonding
- how to update K in order to keep the K/N ratio the same as validators bond/unbond. Should we keep the ratio in the config and re-compute K each time?
Architecture
The shards are organised in a tree structure.
The dependencies are from child shards to parent shards.
The validators of the child chard act as clients of the parent shard.
The parent shard validators are not involved in the child shards.
Components
Common data structures
KonsensusConfig
- genesis_hash
- validators : [ ValidatorConfig ]
- k : Int
ValidatorConfig
- ip_address
- public_key
- expected_bond : Int
K-of-N consensus
KonsensusFactory
Creates the Konsensus contract. This extra layer is needed because the create call has to be confirmed by all the validators.
KonsensusFactory API | ||
---|---|---|
create | KonsensusConfig→ KonsensusFactory | Instantiates the K-of-N contract given the public keys of the validators and a given K value |
createKonsensus | Signature → Konsensus | Creates the consensus after all the signatures are accumulated. The expected signature is the name of Konsensus process, signed with the caller's private key. The call returns only when all the required signatures are accumulated. |
KonsensusProxy
This is used to control messages sent to a name. The notation KonsensusProxy[X] used further means that, after a message with K signatures is received the call will be forwarded to X.
The KonsensusProxy is instantiated by Konsensus:makeProxy. The K used is the one which Konsensus was created with, See KonsensusFactory:create
KonsensusProxy | ||
---|---|---|
send | Message → Signature → Unit | Accumulates K signatures on a message. After all the K messages are received the message is forwarded to the underlying address. The expected signature is the message signed with the callers' private key. |
KoNsensus
A smart contract running in the parent shard which supervises the consensus in the child shard by running a K-of-N algorithm.
This mechanism is required because the parent shard validators don't want to be aware of any child shards.
Processes that require k-of-n consensus are wrapped in a KonsensusProxy by calling makeProxy
Konsensus API | ||
---|---|---|
getConfig | Unit → KonsensusConfig | returns the configuration of the Konsensus contract |
getUpdater | Unit → KonsensusProxy[KonsensusUpdater] | Creates an updater for this Konsensus instance, wrapped in a KonsensusProxy that requires signatures equal to the number of public-keys |
makeProxy | Name → KonsensusProxy[_] | Creates a proxy on top of an existing name |
KonsensusUpdater
Processes that require k-of-n consensus are wrapped in a KonsensusProxy by calling makeProxy
KonsensusUpdater API | ||
---|---|---|
updateConfig | KonsensusConfig → Unit | Updates the Konsensus contract configuration with a new set of validators and a new K value. |
Child-validator management
ValidatorManager
Holds the stakes placed by the stakeholders.
K-of-N validators have to agree upon any changes to the validators list. For this reason the ValidatorManager is created be wrapped into a KonsensusProxy
ValidatorManager API | ||
---|---|---|
create | [ValidatorConfig] → (KonsensusProxy[ValidatorManager], [StakeAdder] ) | Given a list of public keys of the child shard validators and their expected deposits returns a ValidatorManager, wrapped in a KonsensusProxy and a list of StakeAdderS |
addValidator | PublicKey → StakeAdder | starts the operation of adding a new validator. The operation is finished when the new validator places his stake by calling StakeAdder.placeStake |
removeValidator | PublicKey → Unit | starts the operation of removing an existing stakeholder from the depository |
StakeAdder
This contract has the authority to place a new stake in the ValidatorManager. This has to be a separate capability and cannot be put in ValidatorManager because it doesn't have to be wrapped in the KonsensusProxy and placeStake has to be called independently by the new validator.
StakeAdder API | ||
---|---|---|
placeStake | Signature → Purse → Validator | used by child-shard validators to place their initial deposits. The expected signature is the purse name signed with a private key registered with the ValidatorManager during its creation. |
Validator
The owner of the Validator instance has the right to withdraw his stake from the Depository, to the child-shard purse containing his tokens and can make a withdraw request.
The withdraw request is finished and the purse is returned to the caller only after a call to removeStakeholder.
When all the stakeholders have removed their stakes, the depository is destroyed.
Validator API | ||
---|---|---|
withdraw | Unit → Purse | returns a purse with the deposit placed by the validator. This call completes after removeValidator is called in the stake storage |
getChildPurse | Unit → Name | returns the name of the purse residing in the child shard and holding the equivalent of the placed stake in child shard tokens |
Depository
The Depository (aka "Fort Knox") is a pair of public smart contracts running in a parent shard.
It manages the value available in the child shard.
When tokens are transferred from a child shard to the parent shard the tokens are removed from the Depository. The tokens are placed in a purse
When value is transferred from the parent shard to the child shard the value is put in the Depository.
The value in the Depository must always be positive. The value transferred out of a shard cannot exceed the value stored in the Depository.
The Depository can charge a fee for transferring tokens to the child shard.
The Depository.Deposit is the capability object used to transfer tokens to the child-shard.
The Depository.Withdrawer is the capability object used to mint tokens in the child-shard. The access to this contract has to be controlled by the validators so it is wrapped at creation time in a KonsensusProxy.
DepositoryAPI | ||
---|---|---|
create | Name → (Deposit, KonsensusProxy[Withdraw]) | The name parameter represents the name of the depository in the parent shard |
Depository.Deposit API | ||
---|---|---|
transferToChild | Purse → PublicKey → Name | destroys the content of the purse and returns the name of a new wallet located in the child-shard, minus fees. The new wallet contains tokens . The wallet can be used by the owner of the corresponding private-key The returned name is a wallet registered in the child shard |
Depository.Withdrawer API | ||
---|---|---|
withdraw | Amount → Purse | destroys the content of the purse and returns the name of a new purse located in the parent shard, minus fees. The new purse is sprouted from the Depository and its value + fees is removed from the Depository. The returned name is a wallet registered in the parent shard |
The Mint
The Mint is a pair of public smart contracts running in a child shard.
When value enters the child shard the Mint creates (aka mints) tokens in it's shard. These tokens are stored in a purse.
When value exits the child shard the Mint destroys (aka burns) tokens in it's shard. These tokens are sent to the mint in a purse.
The Mint establishes the exchange rate between parent-shard token and child-shard token.
The Mint is different than a wallet by the fact that any received funds are burned and any funds it sends are freshly minted.
The Mint can charge a fee for transferring tokens to the parent shard.
The Mint.Withdrawer is the capability object used to transfer tokens to the parent-shard.
The Mint.Minter is the capability object used to mint tokens in the child-shard. The access to this contract has to be controlled by the validators so it is wrapped at creation time in a KonsensusProxy.
MintAPI | ||
---|---|---|
create | Name → (Withdrawer, KonsensusProxy[Minter]) | The name parameter represents the name of the depository in the parent shard |
Mint.Withdrawer API | ||
---|---|---|
transferToParent | (Purse, PublicKey) → Name | destroys the content of the purse and returns the name of a new wallet located in the parent shard, minus fees. The new wallet is sprouted from the Depository and its value + fees is removed from the Depository. The wallet can be used by the owner of the corresponding private-key The returned name is a wallet registered in the parent shard |
Mint.Minter API | ||
---|---|---|
mint | Amount → Purse | Creates the required amount of tokens. The tokens are stored in a new purse. |
A purse is a contract which holds tokens. It is used as transport means for the tokens.
The purses are either created by the shard's Mint or "sprout" from another purse.
Purse API | ||
---|---|---|
split | Amount → Purse | creates a new purse containing the given amount. This amount is subtracted from the current purse. Empty purses are destroyed |
Wallets
A wallet is an address identified by a public key.
Wallet API | ||
---|---|---|
create | (Purse,PublicKey) → Wallet | Creates a wallet given a purse with the initial value and a public key. The purse is destroyed and the returned wallet will contain the value of the Purse minus fees |
deposit | Purse → Unit | Deposits a purse into this wallet. The purse is destroyed |
withdraw | Amount → Purse | Withdraws an amount from this wallet. A new purse is returned. |
getValue | Unit → Amount | returns the value stored in the wallet |
Shard tree setup
Sprout
Sprouting is the process of creating a child shard with an empty value stored inside.
This is the simplest scenario for building the shard tree.
The steps required for sprouting are:
- Konsensus setup
- Create Mint & Depository
- Stakes placement
See each of these steps described in details below
K-of-N Konsensus setup
In the parent-shard the Konsensus contract is instantiated through the following steps in order to avoid a MITM attack by the validator who initiates the process:
- The KonsensusFactory is created in the parent shard by an arbitrary child validator and registered under a public name
- The name is distributed to all the validators
- Each of the validators independently send signed messages to the above name and, this way, they validate that their correct public key is used
- When all the correct signatures are accumulated the Konsensus is created, registered under a public name and the name is returned to the child shard
In the child shard a Konsensus contract also has to be instantiated and registered under a public name. The Konsensus in the client-shard to control the access to the Mint contract.
Mint & Depository setup
The Mint and Depository contracts are created and their names registered in their respective shard.
Stake placement
Removing a stakeholder
TODO. Interaction with Pos needs to be clarified
Adding a stakeholder
TODO. Interaction with Pos needs to be clarified
Child-shard absorption
Absorbing is the process of destroying a child shard while transferring all its value in the parent shard.
The stakeholders withdraw their stakes and store them in their parent-shard wallets.
PROPOSAL:
For the remaining wallets the child shard if they are registered in the WalletRegistry, a purse containing their value minus fees will be sprouted from the Depository and stored in a wallet accessible with the same public key.
Registered child-shard wallets which contain less value than the fees are lost in the absorption process.
Unregistered child-shard wallets are also lost in the absorption process.
Mount
Through mounting, an existing shard is attached as a child of another existing shard.
TODO. We need to clarify how the mounting affects the clients of the shard
Umount
Through unmounting, a shard is detached from a shard tree and continues to live independently
TODO. We need to clarify how the unmounting affects the clients of the shard and the grandchild shards.
Atomic cross-shard value transfer
Each client only trusts his shard and the parents of his shard.
For this reason the recommended protocol is that the two parties meet in the least-common-ancestor shard.
In the above example the transfers from Alice to Bob should happen in two steps:
- Alice moves it's tokens to the root shard, which is the least common ancestor in this situation
- Alice transfers to a new wallet created with Bob's public-key in root shard
- Bob transfers his amount to "/d" shard
Parent-to-child transfer
TODO: Add diagrams and explain the steps
From @MichaelBirch's comment:
- User in parent invokes the `transfer` function of the depository args: (purse, public key, return channel); returns session id over return channel
- Block containing 1. finalized
- Child validators see 2. and each deploys a message to the k-of-n mint contract
- Mint creates a wallet with the tokens at `rho:uuid:<session id>`
- Blocks containing 3., 4. are finalized
- Child Validators send messages to parent k-of-n contract
- Confirmation sent to user over same return channel as 1.
- Blocks containing 6., 7. finalized
Note: the session id is also used in the k-of-n contract to relate the validator's message to an active request.
```rholang //session id creation
contract newId(return) =
{ new id in {
return!(*id.toByteArray)
}
} ```
Child-to-parent transfer
TODO: Add diagrams and explain the steps
From @MichaelBirch's comment:
- User in parent invokes the `transfer` function of the mint args: (purse, public key, return channel); returns session id over return channel
- Block containing 1. finalized
- Child Validators send messages to parent k-of-n contract
- Depository splits out a purse with the right number of tokens and wraps it in a wallet with the right public key, placing the result at `rho:uuid:<session id>`
- Blocks containing 3., 4. are finalized
- Child validators see 5. and each deploys a message to the k-of-n mint contract
- Confirmation sent to user over same return channel as 1.
- Blocks containing 6., 7. finalized
Useful links
https://blockstream.com/sidechains.pdf
https://en.bitcoin.it/wiki/Atomic_cross-chain_trading
https://en.bitcoin.it/wiki/Hashed_Timelock_Contracts