Davinci Protocol
Block ExplorerValidator Explorer
  • Welcome
  • Learn
    • Execution Layer
    • Proof Of Stake Mechanism
    • Validator Mechanism
    • Staking Mechanism
  • Technical Network
    • Mainnet
    • Accounts
    • Transaction
    • Onchain Gas Transaction
  • Developer Hub
Powered by GitBook
On this page
  • Overview
  • The Deposit Contract
  • Code Of Contract
  • Deposit Receipts
  1. Learn

Staking Mechanism

PreviousValidator MechanismNextTechnical Network

Last updated 3 months ago

Overview

As a proof of stake protocol, DaVinci depends on stakers locking up capital within the protocol (deposits), and, eventually, receiving that capital back along with the rewards they have earned (withdrawals).

The form of capital that is staked is DaVin (DCOIN), DaVinci native currency. DaVin on the consensus layer exists separately, and is accounted for separately, from DaVin in normal DaVinci accounts and contracts. DaVin on the consensus layer is in the form of balances of validator accounts. Validator accounts are extremely limited: they have a balance that increases due to deposits and rewards, and decreases due to withdrawals and penalties. You cannot make transfers between validator accounts or run any kind of transaction on them. Validator account balances are tracked as part of the beacon state, and do not form part of the normal DaVinci execution state. Note that execution balances are denominated in Wei (10−1810^{-18}10−18 DCOIN), whereas validator balances are denominated in Gwei (10−9 10^{-9} 10−9 DCOIN).

For Your Information

  • Deposits are transfers of DCOIN from the execution layer to the consensus layer.

  • Withdrawals are transfers of DCOIN from the consensus layer5 to the execution layer.

  • Accounting on each layer is completely separate.

  • Stakers send transactions to the deposit contract in order to stake.

  • Staking is permissionless.

  • Withdrawals are periodic and automatic.

  • Withdrawals are either partial or full.

The Deposit Contract

The deposit contract is the means by which stakers commit their DCOIN to the protocol in order to gain the right to run a validator.

The deposit contract is a normal DaVinci smart contract running on the execution layer. Anyone wishing to place a stake in order to run a validator may send 32 DCOIN to the deposit contract via a normal DaVinci transaction.

In addition to the DCOIN transferred, the deposit transaction must contain further data as follows.

First, the public key of the validator. A validator's public key is derived from its secret signing key, and is its primary identity on the consensus layer. The staker will provide the secret signing key separately to the consensus client for normal operational use.

Second, withdrawal credentials specifying which DaVinci account rewards earned will be sent to. This will also be the address that receives the validator's full balance when it eventually exits.

Third, a signature over the public key, the withdrawal credentials, and the deposit amount, using the normal signing key. This signature's main role is to serve as a "proof of possession" of the secret key of the validator, which side-steps a nasty rogue public key attack.

Fourth, the deposit data root, which is an SSZ Merkleization of all of the above data that serves as a kind of checksum that the contract can verify.

The deposit contract does some verification on these parameters. In particular, the deposit amount is subject to checks, and the deposit data root is verified. If either of these fails then the deposit will be rejected - that is, the deposit transaction will be reverted.

However, the deposit contract does not validate the signature - the EVM does not yet have the elliptic curve apparatus to do this, and it would be prohibitively expensive to do in normal bytecode. The signature will be validated later by the consensus layer, and if found to be incorrect (for new validators) the deposit will fail, and the DCOIN will be lost.

Code Of Contract

contract DepositContract is IDepositContract, ERC165 {
    uint constant DEPOSIT_CONTRACT_TREE_DEPTH = 32;
    // NOTE: this also ensures `deposit_count` will fit into 64-bits
    uint constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1;

    bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch;
    uint256 deposit_count;

    bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes;

    constructor() public {
        // Compute hashes in empty sparse Merkle tree
        for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; height++)
            zero_hashes[height + 1] = sha256(abi.encodePacked(zero_hashes[height], zero_hashes[height]));
    }

The underlying data structure of the deposit contract is an incremental Merkle tree. This is a Merkle tree that supports only two operations, (1) appending a leaf, and (2) calculating the root. Constraining the data like this allows us to avoid storing the entire Merkle tree, which would be huge. Instead the contract stores only the last branch – a mere 32 nodes – which is all the information that's needed to calculate the Merkle root.

To gain this efficiency, we need an array of zero_hashes. At any given level of the tree, the zero hash is the value the node would have if all of the leaves under it were zero. Since we assign leaves sequentially, huge parts of the tree can be represented by the zero hashes.

The constructor() (which takes no arguments) only initialises the zero_hashes structure, taking advantage of the DVM's default that the uninitialised zero_hashes[0] storage value will be zero.

  function deposit(
        bytes calldata pubkey,
        bytes calldata withdrawal_credentials,
        bytes calldata signature,
        bytes32 deposit_data_root
    ) override external payable {
        // Extended ABI length checks since dynamic types are used.
        require(pubkey.length == 48, "DepositContract: invalid pubkey length");
        require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length");
        require(signature.length == 96, "DepositContract: invalid signature length");

        // Check deposit amount
        require(msg.value >= 1 dcoin, "DepositContract: deposit value too low");
        require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei");
        uint deposit_amount = msg.value / 1 gwei;
        require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");

This is the business part of the contract - where stakers' deposits are made.

A deposit comprises the following items.

  • The public key of the validator: pubkey is the 48 byte (compressed) BLS public key derived from the staker's secret signing key.

  • The withdrawal credentials: withdrawal_credentials is 32 bytes of either 0x00 BLS credentials or 0x01 credentials. Apart from their length, the withdrawal credentials are not validated anywhere in the contract, or even on the consensus layer.

  • The signature is a 96 Byte BLS signature. It is generated by signing the hash tree root of a DepositMessage object (public_key, withdrawal_credentials, and deposit_amount), with the validator's signing key.

  • The deposit_data_root is basically a form of checksum. See below for how it is verified.

    • at least one DCOIN,

    • a whole number of DCOIN, and

The very last condition is formally to avoid overflowing a consensus layer uint64, but seems kind of redundant in practice.

// Emit `DepositEvent` log
        bytes memory amount = to_little_endian_64(uint64(deposit_amount));
        emit DepositEvent(
            pubkey,
            withdrawal_credentials,
            amount,
            signature,
            to_little_endian_64(uint64(deposit_count))
        );

Deposit Receipts

The receipt has a single topic, which is the DepositEvent signature: 0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5, equal to keccak256("DepositEvent(bytes,bytes,bytes,bytes,bytes)").

The receipt's data is the 576 byte ABI encoding of pubkey, withdrawal_credentials, amount, signature, and deposit_count, converted to little-endian where required.

The first column is the hexadecimal byte position of the start of the data in the second column.

Example Receipt Data
# Pointer to pubkey: 0x0a0
000  00000000000000000000000000000000000000000000000000000000000000a0

# Pointer to withdrawal_credentials: 0x100
020  0000000000000000000000000000000000000000000000000000000000000100

# Pointer to amount: 0x140
040  0000000000000000000000000000000000000000000000000000000000000140

# Pointer to signature: 0x180
060  0000000000000000000000000000000000000000000000000000000000000180

# Pointer to deposit_count: 0x200
080  0000000000000000000000000000000000000000000000000000000000000200

# Length of pubkey: 48 bytes
0a0  0000000000000000000000000000000000000000000000000000000000000030

# Pubkey data, padded with 16 zero bytes
0c0  b73fe99acbf91f0032ae95c3ed0d663ea246d02332373e101ff5c7ed520ce098
0e0  652de3eab056a9889bb3d05d734be21400000000000000000000000000000000

# Length of withdrawal_credentials: 32 bytes
100  0000000000000000000000000000000000000000000000000000000000000020

# Withdrawal credentials (0x01 type)
120  010000000000000000000000e637a2acbc531531700fcb7d2ed7e6d96ed8bbe8

# Length of amount: 8 bytes
140  0000000000000000000000000000000000000000000000000000000000000008

# Amount, little-endian encoded. 0x0773594000 = 32,000,000,000
160  0040597307000000000000000000000000000000000000000000000000000000

# Length of signature: 96 bytes
180  0000000000000000000000000000000000000000000000000000000000000060

# Signature data
1a0  b4a7e1546b13be69d31849b4302d870a04867b9de73a973794f8be88c25dc71f
1c0  c3440141c33cf3fbf2dea328179c89550f4e19cad118dd962b07a7c40a3aa8ac
1e0  eaded660edb6e030df48074ddfbe70b26d0e9db1c3be28afc0b47096aab7a616

# Length of deposit_count: 8 bytes
200  0000000000000000000000000000000000000000000000000000000000000008

# Deposit count, little-endian. 0x0a7b1a = 686,874
220  1a7b0a00000000000000000000000000000000000000000000000000000000

Once the deposit contract is as satisfied as it can be that the deposit is valid, it issues (an EVM log event) containing the deposit data. This receipt will later be picked up by the consensus layer for processing.

The DEPOSIT_CONTRACT_TREE_DEPTH specifies the number of levels in the internal Merkle tree. With a depth of 32, it can have 2322^{32}232 leaves, allowing for up to 4.3 billion deposits (MAX_DEPOSIT_COUNT). A deposit is a minimum of DCOIN, so there's sufficient space for every DCOIN in existence to be deposited 35 times over.

Finally, a msg.value. The message value is the amount of DaVinci (denominated in Wei, which are 10−1810^{-18}10−18 DCOIN) that was sent with the transaction. This will normally be 32 DCOIN for a new validator, but can be more or less. It must be,

less than 2642^{64}264 Gwei 4^44 , which is 18.4 Billion DCOIN.

For every deposit accepted by the deposit contract it issues a receipt (also called a log or event), which is generated via an EVM LOG1 opcode.

A consensus client can request these receipts from its attached execution client via the standard RPC method, filtering by the deposit contract address, block numbers, and event topic. This is how the consensus layer becomes aware of the details of new deposits.

The use of event logs here is an optimisation. The deposit contract could instead store all the Merkle tree's leaves and make them available via an eth_call method. However, since logs are not stored in the chain's state, only in block history, it is much cheaper to use them than it would be to store the leaves in the contract's state. However, this places a constraint on the amount of history we must keep around - we cannot now discard block history from before the deployment of the deposit contract. A newly activated consensus client needs access to the full receipt history in order to rebuild its internal view of the Merkle tree, even if it is able to checkpoint sync its beacon state. For convenience, some clients now support starting from a of the Merkle tree that can be shared with other clients in much the same way as . This allows aggressive pruning of block history for those who want to do that.

a receipt
6
eth_getLogs
deposit snapshot
checkpoint states