3
docs/src/proposals/README.md
Normal file
3
docs/src/proposals/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Accepted Design Proposals
|
||||
|
||||
The following architectural proposals have been accepted by the Solana team, but are not yet fully implemented. The proposals may be implemented as described, implemented differently as issues in the designs become evident, or not implemented at all. If implemented, the proposal will be moved to [Implemented Proposals](../implemented-proposals/README.md) and the details will be added to relevant sections of the docs.
|
90
docs/src/proposals/abi-management.md
Normal file
90
docs/src/proposals/abi-management.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Solana ABI management process
|
||||
|
||||
This document proposes the Solana ABI management process. The ABI management
|
||||
process is an engineering practice and a supporting technical framework to avoid
|
||||
introducing unintended incompatible ABI changes.
|
||||
|
||||
# Problem
|
||||
|
||||
The Solana ABI (binary interface to the cluster) is currently only defined
|
||||
implicitly by the implementation and requires a very careful eye to notice
|
||||
breaking changes. This makes it extremely difficult to upgrade the software
|
||||
on an existing cluster without rebooting the ledger.
|
||||
|
||||
# Requirements and objectives
|
||||
|
||||
- Unintended ABI changes can be detected as CI failures mechanically.
|
||||
- Newer implementation must be able to process the oldest data (since genesis)
|
||||
once we go mainnet.
|
||||
- The objective of this proposal is to protect the ABI while sustaining rather
|
||||
rapid development by opting for a mechanical process rather than a very long
|
||||
human-driven auditing process.
|
||||
- Once signed cryptographically, data blob must be identical, so no
|
||||
in-place data format update is possible regardless of inbound and outbound of
|
||||
the online system. Also, considering the sheer volume of transactions we're
|
||||
aiming to handle, retrospective in-place update is undesirable at best.
|
||||
|
||||
# Solution
|
||||
|
||||
Instead of natural human's eye due-diligence, which should be assumed to fail
|
||||
regularly, we need a systematic assurance of not breaking the cluster when
|
||||
changing the source code.
|
||||
|
||||
For that purpose, we introduce a mechanism of marking every ABI-related things
|
||||
in source code (`struct`s, `enum`s) with the new `#[frozen_abi]` attribute. This
|
||||
takes hard-coded digest value derived from types of its fields via
|
||||
`ser::Serialize`. And the attribute automatically generates a unit test to try
|
||||
to detect any unsanctioned changes to the marked ABI-related things.
|
||||
|
||||
However, the detection cannot be complete; no matter how hard we statically
|
||||
analyze the source code, it's still possible to break ABI. For example, this
|
||||
includes not-`derive`d hand-written `ser::Serialize`, underlying library's
|
||||
implementation changes (for example `bincode`), CPU architecture differences.
|
||||
The detection of these possible ABI incompatibilities is out-of-scope for this
|
||||
ABI management.
|
||||
|
||||
# Definitions
|
||||
|
||||
ABI item/type: various types to be used for serialization, which collectively
|
||||
comprises the whole ABI for any system components. For example, those types
|
||||
include `struct`s and `enum`s.
|
||||
|
||||
ABI item digest: Some fixed hash derived from type information of ABI item's
|
||||
fields.
|
||||
|
||||
# Example
|
||||
|
||||
```patch
|
||||
+#[frozen_abi(digest="1c6a53e9")]
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Vote {
|
||||
/// A stack of votes starting with the oldest vote
|
||||
pub slots: Vec<Slot>,
|
||||
/// signature of the bank's state at the last slot
|
||||
pub hash: Hash,
|
||||
}
|
||||
```
|
||||
|
||||
# Developer's workflow
|
||||
|
||||
To know the digest for new ABI items, developers can add `frozen_abi` with a
|
||||
random digest value and run the unit tests and replace it with the correct
|
||||
digest from the assertion test error message.
|
||||
|
||||
In general, once we add `frozen_abi` and its change is published in the stable
|
||||
release channel, its digest should never change. If such a change is needed, we
|
||||
should opt for defining a new struct like `FooV1`. And special release flow like
|
||||
hard forks should be approached.
|
||||
|
||||
# Implementation remarks
|
||||
|
||||
We use some degree of macro machinery to automatically generate unit tests
|
||||
and calculate a digest from ABI items. This is doable by clever use of
|
||||
`serde::Serialize` ([1]) and `any::typename` ([2]). For a precedent for similar
|
||||
implementation, `ink` from the Parity Technologies [3] could be informational.
|
||||
|
||||
# References
|
||||
|
||||
1. [(De)Serialization with type info · Issue #1095 · serde-rs/serde](https://github.com/serde-rs/serde/issues/1095#issuecomment-345483479)
|
||||
2. [`std::any::type_name` - Rust](https://doc.rust-lang.org/std/any/fn.type_name.html)
|
||||
3. [Parity's ink to write smart contracts](https://github.com/paritytech/ink)
|
55
docs/src/proposals/bankless-leader.md
Normal file
55
docs/src/proposals/bankless-leader.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Bankless Leader
|
||||
|
||||
A bankless leader does the minimum amount of work to produce a valid block. The leader is tasked with ingress transactions, sorting and filtering valid transactions, arranging them into entries, shredding the entries and broadcasting the shreds. While a validator only needs to reassemble the block and replay execution of well formed entries. The leader does 3x more memory operations before any bank execution than the validator per processed transaction.
|
||||
|
||||
## Rationale
|
||||
|
||||
Normal bank operation for a spend needs to do 2 loads and 2 stores. With this design leader just does 1 load. so 4x less account\_db work before generating the block. The store operations are likely to be more expensive than reads.
|
||||
|
||||
When replay stage starts processing the same transactions, it can assume that PoH is valid, and that all the entries are safe for parallel execution. The fee accounts that have been loaded to produce the block are likely to still be in memory, so the additional load should be warm and the cost is likely to be amortized.
|
||||
|
||||
## Fee Account
|
||||
|
||||
The [fee account](../terminology.md#fee_account) pays for the transaction to be included in the block. The leader only needs to validate that the fee account has the balance to pay for the fee.
|
||||
|
||||
## Balance Cache
|
||||
|
||||
For the duration of the leaders consecutive blocks, the leader maintains a temporary balance cache for all the processed fee accounts. The cache is a map of pubkeys to lamports.
|
||||
|
||||
At the start of the first block the balance cache is empty. At the end of the last block the cache is destroyed.
|
||||
|
||||
The balance cache lookups must reference the same base fork for the entire duration of the cache. At the block boundary, the cache can be reset along with the base fork after replay stage finishes verifying the previous block.
|
||||
|
||||
## Balance Check
|
||||
|
||||
Prior to the balance check, the leader validates all the signatures in the transaction.
|
||||
|
||||
1. Verify the accounts are not in use and BlockHash is valid.
|
||||
2. Check if the fee account is present in the cache, or load the account from accounts\_db and store the lamport balance in the cache.
|
||||
3. If the balance is less than the fee, drop the transaction.
|
||||
4. Subtract the fee from the balance.
|
||||
5. For all the keys in the transaction that are Credit-Debit and are referenced by an instruction, reduce their balance to 0 in the cache. The account fee is declared as Credit-Debit, but as long as it is not used in any instruction its balance will not be reduced to 0.
|
||||
|
||||
## Leader Replay
|
||||
|
||||
Leaders will need to replay their blocks as part of the standard replay stage operation.
|
||||
|
||||
## Leader Replay With Consecutive Blocks
|
||||
|
||||
A leader can be scheduled to produce multiple blocks in a row. In that scenario the leader is likely to be producing the next block while the replay stage for the first block is playing.
|
||||
|
||||
When the leader finishes the replay stage it can reset the balance cache by clearing it, and set a new fork as the base for the cache which can become active on the next block.
|
||||
|
||||
## Reseting the Balance Cache
|
||||
|
||||
1. At the start of the block, if the balance cache is uninitialized, set the base fork for the balance cache to be the parent of the block and create an empty cache.
|
||||
2. if the cache is initialized, check if block's parents has a new frozen bank that is newer than the current base fork for the balance cache.
|
||||
3. if a parent newer than the cache's base fork exist, reset the cache to the parent.
|
||||
|
||||
## Impact on Clients
|
||||
|
||||
The same fee account can be reused many times in the same block until it is used once as Credit-Debit by an instruction.
|
||||
|
||||
Clients that transmit a large number of transactions per second should use a dedicated fee account that is not used as Credit-Debit in any instruction.
|
||||
|
||||
Once an account fee is used as Credit-Debit, it will fail the balance check until the balance cache is reset.
|
83
docs/src/proposals/block-confirmation.md
Normal file
83
docs/src/proposals/block-confirmation.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Block Confirmation
|
||||
|
||||
A validator votes on a PoH hash for two purposes. First, the vote indicates it
|
||||
believes the ledger is valid up until that point in time. Second, since many
|
||||
valid forks may exist at a given height, the vote also indicates exclusive
|
||||
support for the fork. This document describes only the former. The latter is
|
||||
described in [Tower BFT](tower-bft.md).
|
||||
|
||||
## Current Design
|
||||
|
||||
To start voting, a validator first registers an account to which it will send
|
||||
its votes. It then sends votes to that account. The vote contains the tick
|
||||
height of the block it is voting on. The account stores the 32 highest heights.
|
||||
|
||||
### Problems
|
||||
|
||||
* Only the validator knows how to find its own votes directly.
|
||||
|
||||
Other components, such as the one that calculates confirmation time, needs to
|
||||
be baked into the validator code. The validator code queries the bank for all
|
||||
accounts owned by the vote program.
|
||||
|
||||
* Voting ballots do not contain a PoH hash. The validator is only voting that
|
||||
it has observed an arbitrary block at some height.
|
||||
|
||||
* Voting ballots do not contain a hash of the bank state. Without that hash,
|
||||
there is no evidence that the validator executed the transactions and
|
||||
verified there were no double spends.
|
||||
|
||||
## Proposed Design
|
||||
|
||||
### No Cross-block State Initially
|
||||
|
||||
At the moment a block is produced, the leader shall add a NewBlock transaction
|
||||
to the ledger with a number of tokens that represents the validation reward.
|
||||
It is effectively an incremental multisig transaction that sends tokens from
|
||||
the mining pool to the validators. The account should allocate just enough
|
||||
space to collect the votes required to achieve a supermajority. When a
|
||||
validator observes the NewBlock transaction, it has the option to submit a vote
|
||||
that includes a hash of its ledger state (the bank state). Once the account has
|
||||
sufficient votes, the vote program should disperse the tokens to the
|
||||
validators, which causes the account to be deleted.
|
||||
|
||||
#### Logging Confirmation Time
|
||||
|
||||
The bank will need to be aware of the vote program. After each transaction, it
|
||||
should check if it is a vote transaction and if so, check the state of that
|
||||
account. If the transaction caused the supermajority to be achieved, it should
|
||||
log the time since the NewBlock transaction was submitted.
|
||||
|
||||
### Finality and Payouts
|
||||
|
||||
[Tower BFT](tower-bft.md) is the proposed fork selection algorithm. It proposes
|
||||
that payment to miners be postponed until the *stack* of validator votes reaches
|
||||
a certain depth, at which point rollback is not economically feasible. The vote
|
||||
program may therefore implement Tower BFT. Vote instructions would need to
|
||||
reference a global Tower account so that it can track cross-block state.
|
||||
|
||||
## Challenges
|
||||
|
||||
### On-chain voting
|
||||
|
||||
Using programs and accounts to implement this is a bit tedious. The hardest
|
||||
part is figuring out how much space to allocate in NewBlock. The two variables
|
||||
are the *active set* and the stakes of those validators. If we calculate the
|
||||
active set at the time NewBlock is submitted, the number of validators to
|
||||
allocate space for is known upfront. If, however, we allow new validators to
|
||||
vote on old blocks, then we'd need a way to allocate space dynamically.
|
||||
|
||||
Similar in spirit, if the leader caches stakes at the time of NewBlock, the
|
||||
vote program doesn't need to interact with the bank when it processes votes. If
|
||||
we don't, then we have the option to allow stakes to float until a vote is
|
||||
submitted. A validator could conceivably reference its own staking account, but
|
||||
that'd be the current account value instead of the account value of the most
|
||||
recently finalized bank state. The bank currently doesn't offer a means to
|
||||
reference accounts from particular points in time.
|
||||
|
||||
### Voting Implications on Previous Blocks
|
||||
|
||||
Does a vote on one height imply a vote on all blocks of lower heights of
|
||||
that fork? If it does, we'll need a way to lookup the accounts of all
|
||||
blocks that haven't yet reached supermajority. If not, the validator could
|
||||
send votes to all blocks explicitly to get the block rewards.
|
102
docs/src/proposals/cluster-test-framework.md
Normal file
102
docs/src/proposals/cluster-test-framework.md
Normal file
@ -0,0 +1,102 @@
|
||||
# Cluster Test Framework
|
||||
|
||||
This document proposes the Cluster Test Framework \(CTF\). CTF is a test harness that allows tests to execute against a local, in-process cluster or a deployed cluster.
|
||||
|
||||
## Motivation
|
||||
|
||||
The goal of CTF is to provide a framework for writing tests independent of where and how the cluster is deployed. Regressions can be captured in these tests and the tests can be run against deployed clusters to verify the deployment. The focus of these tests should be on cluster stability, consensus, fault tolerance, API stability.
|
||||
|
||||
Tests should verify a single bug or scenario, and should be written with the least amount of internal plumbing exposed to the test.
|
||||
|
||||
## Design Overview
|
||||
|
||||
Tests are provided an entry point, which is a `contact_info::ContactInfo` structure, and a keypair that has already been funded.
|
||||
|
||||
Each node in the cluster is configured with a `validator::ValidatorConfig` at boot time. At boot time this configuration specifies any extra cluster configuration required for the test. The cluster should boot with the configuration when it is run in-process or in a data center.
|
||||
|
||||
Once booted, the test will discover the cluster through a gossip entry point and configure any runtime behaviors via validator RPC.
|
||||
|
||||
## Test Interface
|
||||
|
||||
Each CTF test starts with an opaque entry point and a funded keypair. The test should not depend on how the cluster is deployed, and should be able to exercise all the cluster functionality through the publicly available interfaces.
|
||||
|
||||
```text
|
||||
use crate::contact_info::ContactInfo;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
pub fn test_this_behavior(
|
||||
entry_point_info: &ContactInfo,
|
||||
funding_keypair: &Keypair,
|
||||
num_nodes: usize,
|
||||
)
|
||||
```
|
||||
|
||||
## Cluster Discovery
|
||||
|
||||
At test start, the cluster has already been established and is fully connected. The test can discover most of the available nodes over a few second.
|
||||
|
||||
```text
|
||||
use crate::gossip_service::discover_nodes;
|
||||
|
||||
// Discover the cluster over a few seconds.
|
||||
let cluster_nodes = discover_nodes(&entry_point_info, num_nodes);
|
||||
```
|
||||
|
||||
## Cluster Configuration
|
||||
|
||||
To enable specific scenarios, the cluster needs to be booted with special configurations. These configurations can be captured in `validator::ValidatorConfig`.
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
let mut validator_config = ValidatorConfig::default();
|
||||
validator_config.rpc_config.enable_validator_exit = true;
|
||||
let local = LocalCluster::new_with_config(
|
||||
num_nodes,
|
||||
10_000,
|
||||
100,
|
||||
&validator_config
|
||||
);
|
||||
```
|
||||
|
||||
## How to design a new test
|
||||
|
||||
For example, there is a bug that shows that the cluster fails when it is flooded with invalid advertised gossip nodes. Our gossip library and protocol may change, but the cluster still needs to stay resilient to floods of invalid advertised gossip nodes.
|
||||
|
||||
Configure the RPC service:
|
||||
|
||||
```text
|
||||
let mut validator_config = ValidatorConfig::default();
|
||||
validator_config.rpc_config.enable_rpc_gossip_push = true;
|
||||
validator_config.rpc_config.enable_rpc_gossip_refresh_active_set = true;
|
||||
```
|
||||
|
||||
Wire the RPCs and write a new test:
|
||||
|
||||
```text
|
||||
pub fn test_large_invalid_gossip_nodes(
|
||||
entry_point_info: &ContactInfo,
|
||||
funding_keypair: &Keypair,
|
||||
num_nodes: usize,
|
||||
) {
|
||||
let cluster = discover_nodes(&entry_point_info, num_nodes);
|
||||
|
||||
// Poison the cluster.
|
||||
let client = create_client(entry_point_info.client_facing_addr(), VALIDATOR_PORT_RANGE);
|
||||
for _ in 0..(num_nodes * 100) {
|
||||
client.gossip_push(
|
||||
cluster_info::invalid_contact_info()
|
||||
);
|
||||
}
|
||||
sleep(Durration::from_millis(1000));
|
||||
|
||||
// Force refresh of the active set.
|
||||
for node in &cluster {
|
||||
let client = create_client(node.client_facing_addr(), VALIDATOR_PORT_RANGE);
|
||||
client.gossip_refresh_active_set();
|
||||
}
|
||||
|
||||
// Verify that spends still work.
|
||||
verify_spends(&cluster);
|
||||
}
|
||||
```
|
||||
|
71
docs/src/proposals/cross-program-invocation.md
Normal file
71
docs/src/proposals/cross-program-invocation.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Cross-Program Invocation
|
||||
|
||||
## Problem
|
||||
|
||||
In today's implementation a client can create a transaction that modifies two accounts, each owned by a separate on-chain program:
|
||||
|
||||
```text
|
||||
let message = Message::new(vec![
|
||||
token_instruction::pay(&alice_pubkey),
|
||||
acme_instruction::launch_missiles(&bob_pubkey),
|
||||
]);
|
||||
client.send_message(&[&alice_keypair, &bob_keypair], &message);
|
||||
```
|
||||
|
||||
The current implementation does not, however, allow the `acme` program to conveniently invoke `token` instructions on the client's behalf:
|
||||
|
||||
```text
|
||||
let message = Message::new(vec![
|
||||
acme_instruction::pay_and_launch_missiles(&alice_pubkey, &bob_pubkey),
|
||||
]);
|
||||
client.send_message(&[&alice_keypair, &bob_keypair], &message);
|
||||
```
|
||||
|
||||
Currently, there is no way to create instruction `pay_and_launch_missiles` that executes `token_instruction::pay` from the `acme` program. The workaround is to extend the `acme` program with the implementation of the `token` program, and create `token` accounts with `ACME_PROGRAM_ID`, which the `acme` program is permitted to modify. With that workaround, `acme` can modify token-like accounts created by the `acme` program, but not token accounts created by the `token` program.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
The goal of this design is to modify Solana's runtime such that an on-chain program can invoke an instruction from another program.
|
||||
|
||||
Given two on-chain programs `token` and `acme`, each implementing instructions `pay()` and `launch_missiles()` respectively, we would ideally like to implement the `acme` module with a call to a function defined in the `token` module:
|
||||
|
||||
```text
|
||||
use token;
|
||||
|
||||
fn launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
|
||||
...
|
||||
}
|
||||
|
||||
fn pay_and_launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
|
||||
token::pay(&keyed_accounts[1..])?;
|
||||
|
||||
launch_missiles(keyed_accounts)?;
|
||||
}
|
||||
```
|
||||
|
||||
The above code would require that the `token` crate be dynamically linked, so that a custom linker could intercept calls and validate accesses to `keyed_accounts`. That is, even though the client intends to modify both `token` and `acme` accounts, only `token` program is permitted to modify the `token` account, and only the `acme` program is permitted to modify the `acme` account.
|
||||
|
||||
Backing off from that ideal cross-program call, a slightly more verbose solution is to expose token's existing `process_instruction()` entrypoint to the acme program:
|
||||
|
||||
```text
|
||||
use token_instruction;
|
||||
|
||||
fn launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
|
||||
...
|
||||
}
|
||||
|
||||
fn pay_and_launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
|
||||
let alice_pubkey = keyed_accounts[1].key;
|
||||
let instruction = token_instruction::pay(&alice_pubkey);
|
||||
process_instruction(&instruction)?;
|
||||
|
||||
launch_missiles(keyed_accounts)?;
|
||||
}
|
||||
```
|
||||
|
||||
where `process_instruction()` is built into Solana's runtime and responsible for routing the given instruction to the `token` program via the instruction's `program_id` field. Before invoking `pay()`, the runtime must also ensure that `acme` didn't modify any accounts owned by `token`. It does this by calling `runtime::verify_account_changes()` and then afterward updating all the `pre_*` variables to tentatively commit `acme`'s account modifications. After `pay()` completes, the runtime must again ensure that `token` didn't modify any accounts owned by `acme`. It should call `verify_account_changes()` again, but this time with the `token` program ID. Lastly, after `pay_and_launch_missiles()` completes, the runtime must call `verify_account_changes()` one more time, where it normally would, but using all updated `pre_*` variables. If executing `pay_and_launch_missiles()` up to `pay()` made no invalid account changes, `pay()` made no invalid changes, and executing from `pay()` until `pay_and_launch_missiles()` returns made no invalid changes, then the runtime can transitively assume `pay_and_launch_missiles()` as whole made no invalid account changes, and therefore commit all account modifications.
|
||||
|
||||
### Setting `KeyedAccount.is_signer`
|
||||
|
||||
When `process_instruction()` is invoked, the runtime must create a new `KeyedAccounts` parameter using the signatures from the _original_ transaction data. Since the `token` program is immutable and existed on-chain prior to the `acme` program, the runtime can safely treat the transaction signature as a signature of a transaction with a `token` instruction. When the runtime sees the given instruction references `alice_pubkey`, it looks up the key in the transaction to see if that key corresponds to a transaction signature. In this case it does and so sets `KeyedAccount.is_signer`, thereby authorizing the `token` program to modify Alice's account.
|
||||
|
104
docs/src/proposals/interchain-transaction-verification.md
Normal file
104
docs/src/proposals/interchain-transaction-verification.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Inter-chain Transaction Verification
|
||||
|
||||
## Problem
|
||||
|
||||
Inter-chain applications are not new to the digital asset ecosystem; in fact, even the smaller centralized exchanges still categorically dwarf all single chain applications put together in terms of users and volume. They command massive valuations and have spent years effectively optimizing their core products for a broad range of end users. However, their basic operations center around mechanisms that require their users to unilaterally trust them, typically with little to no recourse or protection from accidental loss. This has led to the broader digital asset ecosystem being fractured along network lines because interoperability solutions typically:
|
||||
|
||||
* Are technically complex to fully implement
|
||||
* Create unstable network scale incentive structures
|
||||
* Require consistent and high level cooperation between stakeholders
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Simple Payment Verification \(SPV\) is a generic term for a range of different methodologies used by light clients on most major blockchain networks to verify aspects of the network state without the burden of fully storing and maintaining the chain itself. In most cases, this means relying on a form of hash tree to supply a proof of the presence of a given transaction in a certain block by comparing against a root hash in that block’s header or equivalent. This allows a light client or wallet to reach a probabilistic level of certainty about on-chain events by itself with a minimum of trust required with regard to network nodes.
|
||||
|
||||
Traditionally the process of assembling and validating these proofs is carried out off chain by nodes, wallets, or other clients, but it also offers a potential mechanism for inter-chain state verification. However, by moving the capability to validate SPV proofs on-chain as a smart contract while leveraging the archival properties inherent to the blockchain, it is possible to construct a system for programmatically detecting and verifying transactions on other networks without the involvement of any type of trusted oracle or complex multi-stage consensus mechanism. This concept is broadly generalisable to any network with an SPV mechanism and can even be operated bilaterally on other smart contract platforms, opening up the possibility of cheap, fast, inter-chain transfer of value without relying on collateral, hashlocks, or trusted intermediaries.
|
||||
|
||||
Opting to take advantage of well established and developmentally stable mechanisms already common to all major blockchains allows SPV based interoperability solutions to be dramatically simpler than orchestrated multi-stage approaches. As part of this, they dispense with the need for widely agreed upon cross chain communication standards and the large multi-party organizations that write them in favor of a set of discrete contract-based services that can be easily utilized by caller contracts through a common abstraction format. This will set the groundwork for a broad range of applications and contracts able to interoperate across the variegated and every growing platform ecosystem.
|
||||
|
||||
## Terminology
|
||||
|
||||
SPV Program - Client-facing interface for the inter-chain SPV system, manages participant roles. SPV Engine - Validates transaction proofs, subset of the SPV Program. Client - The caller to the SPV Program, typically another solana contract. Prover - Party who generates proofs for transactions and submits them to the SPV Program. Transaction Proof - Created by Provers, contains a merkle proof, transaction, and blockheader reference. Merkle Proof - Basic SPV proof that validates the presence of a transaction in a certain block. Block Header - Represents the basic parameters and relative position of a given block. Proof Request - An order placed by a client for verification of transaction\(s\) by provers. Header Store - A data structure for storing and referencing ranges of block headers in proofs. Client Request - Transaction from the client to the SPV Program to trigger creation of a Proof Request. Sub-account - A Solana account owned by another contract account, without its own private key.
|
||||
|
||||
## Service
|
||||
|
||||
SPV Programs run as contracts deployed on the Solana network and maintain a type of public marketplace for SPV proofs that allows any party to submit both requests for proofs as well as proofs themselves for verification in response to requests. There will be multiple SPV Program instances active at any given time, at least one for each connected external network and potentially multiple instances per network. SPV program instances will be relatively consistent in their high level API and feature sets with some variation between currency platforms \(Bitcoin, Litecoin\) and smart contract platforms owing to the potential for verification of network state changes beyond simply transactions. In every case regardless of network, the SPV Program relies on an internal component called an SPV engine to provide stateless verification of the actual SPV proofs upon which the higher level client facing features and api are built. The SPV engine requires a network specific implementation, but allows easy extension of the larger inter-chain ecosystem by any team who chooses to carry out that implementation and drop it into the standard SPV program for deployment.
|
||||
|
||||
For purposes of Proof Requests, the requester is referred to as the program client, which in most if not all cases will be another Solana Contract. The client can choose to submit a request pertaining to a specific transaction or to include a broader filter that can apply to any of a range of parameters of a transaction including its inputs, outputs, and amount. For example, A client could submit a request for any transaction sent from a given address A to address B with the amount X after a certain time. This structure can be used in a range of applications, such as verifying a specific intended payment in the case of an atomic swap or detecting the movement of collateral assets for a loan.
|
||||
|
||||
Following submission of a Client Request, assuming that it is successfully validated, a proof request account is created by the SPV program to track the progress of the request. Provers use the account to specify the request they intend to fill in the proofs they submit for validation, at which point the SPV program validates those proofs and if successful, saves them to the account data of the request account. Clients can monitor the status of their requests and see any applicable transactions alongside their proofs by querying the account data of the request account. In future iterations when supported by Solana, this process will be simplified by contracts publishing events rather than requiring a polling style process as described.
|
||||
|
||||
## Implementation
|
||||
|
||||
The Solana Inter-chain SPV mechanism consists of the following components and participants:
|
||||
|
||||
### SPV engine
|
||||
|
||||
A contract deployed on Solana which statelessly verifies SPV proofs for the caller. It takes as arguments for validation:
|
||||
|
||||
* An SPV proof in the correct format of the blockchain associated with the program
|
||||
* Reference\(s\) to the relevant block headers to compare that proof against
|
||||
* The necessary parameters of the transaction to verify
|
||||
|
||||
If the proof in question is successfully validated, the SPV program saves proof
|
||||
|
||||
of that verification to the request account, which can be saved by the caller to
|
||||
|
||||
its account data or otherwise handled as necessary. SPV programs also expose
|
||||
|
||||
utilities and structs used for representation and validation of headers,
|
||||
|
||||
transactions, hashes, etc. on a chain by chain basis.
|
||||
|
||||
### SPV program
|
||||
|
||||
A contract deployed on Solana which coordinates and intermediates the interaction between Clients and Provers and manages the validation of requests, headers, proofs, etc. It is the primary point of access for Client contracts to access the inter-chain. SPV mechanism. It offers the following core features:
|
||||
|
||||
* Submit Proof Request - allows client to place a request for a specific proof or set of proofs
|
||||
* Cancel Proof Request - allows client to invalidate a pending request
|
||||
* Fill Proof Request - used by Provers to submit for validation a proof corresponding to a given Proof Request
|
||||
|
||||
The SPV program maintains a publicly available listing of valid pending Proof
|
||||
|
||||
Requests in its account data for the benefit of the Provers, who monitor it and
|
||||
|
||||
enclose references to target requests with their submitted proofs.
|
||||
|
||||
### Proof Request
|
||||
|
||||
A message sent by the Client to the SPV engine denoting a request for a proof of a specific transaction or set of transactions. Proof Requests can either manually specify a certain transaction by its hash or can elect to submit a filter that matches multiple transactions or classes of transactions. For example, a filter matching “any transaction from address xxx to address yyy” could be used to detect payment of a debt or settlement of an inter-chain swap. Likewise, a filter matching “any transaction from address xxx” could be used by a lending or synthetic token minting contract to monitor and react to changes in collateralization. Proof Requests are sent with a fee, which is disbursed by the SPV engine contract to the appropriate Prover once a proof matching that request is validated.
|
||||
|
||||
### Request Book
|
||||
|
||||
The public listing of valid, open Proof Requests available to provers to fill or for clients to cancel. Roughly analogous to an orderbook in an exchange, but with a single type of listing rather than two separate sides. It is stored in the account data of the SPV program.
|
||||
|
||||
### Proof
|
||||
|
||||
A proof of the presence of a given transaction in the blockchain in question. Proofs encompass both the actual merkle proof and reference\(s\) to a chain of valid sequential block headers. They are constructed and submitted by Provers in accordance with the specifications of the publicly available Proof Requests hosted on the request book by the SPV program. Upon Validation, they are saved to the account data of the relevant Proof Request, which can be used by the Client to monitor the state of the request.
|
||||
|
||||
### Client
|
||||
|
||||
The originator of a request for a transaction proof. Clients will most often be other contracts as parts of applications or specific financial products like loans, swaps, escrow, etc. The client in any given verification process cycle initially submits a ClientRequest which communicates the parameters and fee and if successfully validated, results in the creation of a Proof Request account by the SPV program. The Client may also submit a CancelRequest referencing an active Proof Request in order to denote it as invalid for purposes of proof submission.
|
||||
|
||||
### Prover
|
||||
|
||||
The submitter of a proof that fills a Proof Request. Provers monitor the request book of the SPV program for outstanding Proof Requests and generate matching proofs, which they submit to the SPV program for validation. If the proof is accepted, the fee associated with the Proof Request in question is disbursed to the Prover. Provers typically operate as Solana Blockstreamer nodes that also have access to a Bitcoin node, which they use for purposes of constructing proofs and accessing block headers.
|
||||
|
||||
### Header Store
|
||||
|
||||
An account-based data structure used to maintain block headers for the purpose of inclusion in submitted proofs by reference to the header store account. header stores can be maintained by independent entities, since header chain validation is a component of the SPV program proof validation mechanism. Fees that are paid out by Proof Requests to Provers are split between the submitter of the merkle proof itself and the header store that is referenced in the submitted proof. Due to the current inability to grow already allocated account data capacity, the use case necessitates a data structure that can grow indefinitely without rebalancing. Sub-accounts are accounts owned by the SPV program without their own private keys that are used for storage by allocating blockheaders to their account data. Multiple potential approaches to the implementation of the header store system are feasible:
|
||||
|
||||
Store Headers in program sub-accounts indexed by Public address:
|
||||
|
||||
* Each sub-account holds one header and has a public key matching the blockhash
|
||||
* Requires same number of account data lookups as confirmations per verification
|
||||
* Limit on number of confirmations \(15-20\) via max transaction data ceiling
|
||||
* No network-wide duplication of individual headers
|
||||
|
||||
Linked List of multiple sub-accounts storing headers:
|
||||
|
||||
* Maintain sequential index of storage accounts, many headers per storage account
|
||||
* Max 2 account data lookups for >99.9% of verifications \(1 for most\)
|
||||
* Compact sequential data address format allows any number of confirmations and fast lookups
|
||||
* Facilitates network-wide header duplication inefficiencies
|
||||
|
137
docs/src/proposals/ledger-replication-to-implement.md
Normal file
137
docs/src/proposals/ledger-replication-to-implement.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Ledger Replication
|
||||
|
||||
Replication behavior yet to be implemented.
|
||||
|
||||
## Storage epoch
|
||||
|
||||
The storage epoch should be the number of slots which results in around 100GB-1TB of ledger to be generated for archivers to store. Archivers will start storing ledger when a given fork has a high probability of not being rolled back.
|
||||
|
||||
## Validator behavior
|
||||
|
||||
1. Every NUM\_KEY\_ROTATION\_TICKS it also validates samples received from
|
||||
|
||||
archivers. It signs the PoH hash at that point and uses the following
|
||||
|
||||
algorithm with the signature as the input:
|
||||
|
||||
* The low 5 bits of the first byte of the signature creates an index into
|
||||
|
||||
another starting byte of the signature.
|
||||
|
||||
* The validator then looks at the set of storage proofs where the byte of
|
||||
|
||||
the proof's sha state vector starting from the low byte matches exactly
|
||||
|
||||
with the chosen byte\(s\) of the signature.
|
||||
|
||||
* If the set of proofs is larger than the validator can handle, then it
|
||||
|
||||
increases to matching 2 bytes in the signature.
|
||||
|
||||
* Validator continues to increase the number of matching bytes until a
|
||||
|
||||
workable set is found.
|
||||
|
||||
* It then creates a mask of valid proofs and fake proofs and sends it to
|
||||
|
||||
the leader. This is a storage proof confirmation transaction.
|
||||
|
||||
2. After a lockout period of NUM\_SECONDS\_STORAGE\_LOCKOUT seconds, the
|
||||
|
||||
validator then submits a storage proof claim transaction which then causes the
|
||||
|
||||
distribution of the storage reward if no challenges were seen for the proof to
|
||||
|
||||
the validators and archivers party to the proofs.
|
||||
|
||||
## Archiver behavior
|
||||
|
||||
1. The archiver then generates another set of offsets which it submits a fake
|
||||
|
||||
proof with an incorrect sha state. It can be proven to be fake by providing the
|
||||
|
||||
seed for the hash result.
|
||||
|
||||
* A fake proof should consist of an archiver hash of a signature of a PoH
|
||||
|
||||
value. That way when the archiver reveals the fake proof, it can be
|
||||
|
||||
verified on chain.
|
||||
|
||||
2. The archiver monitors the ledger, if it sees a fake proof integrated, it
|
||||
|
||||
creates a challenge transaction and submits it to the current leader. The
|
||||
|
||||
transacation proves the validator incorrectly validated a fake storage proof.
|
||||
|
||||
The archiver is rewarded and the validator's staking balance is slashed or
|
||||
|
||||
frozen.
|
||||
|
||||
## Storage proof contract logic
|
||||
|
||||
Each archiver and validator will have their own storage account. The validator's account would be separate from their gossip id similiar to their vote account. These should be implemented as two programs one which handles the validator as the keysigner and one for the archiver. In that way when the programs reference other accounts, they can check the program id to ensure it is a validator or archiver account they are referencing.
|
||||
|
||||
### SubmitMiningProof
|
||||
|
||||
```text
|
||||
SubmitMiningProof {
|
||||
slot: u64,
|
||||
sha_state: Hash,
|
||||
signature: Signature,
|
||||
};
|
||||
keys = [archiver_keypair]
|
||||
```
|
||||
|
||||
Archivers create these after mining their stored ledger data for a certain hash value. The slot is the end slot of the segment of ledger they are storing, the sha\_state the result of the archiver using the hash function to sample their encrypted ledger segment. The signature is the signature that was created when they signed a PoH value for the current storage epoch. The list of proofs from the current storage epoch should be saved in the account state, and then transfered to a list of proofs for the previous epoch when the epoch passes. In a given storage epoch a given archiver should only submit proofs for one segment.
|
||||
|
||||
The program should have a list of slots which are valid storage mining slots. This list should be maintained by keeping track of slots which are rooted slots in which a significant portion of the network has voted on with a high lockout value, maybe 32-votes old. Every SLOTS\_PER\_SEGMENT number of slots would be added to this set. The program should check that the slot is in this set. The set can be maintained by receiving a AdvertiseStorageRecentBlockHash and checking with its bank/Tower BFT state.
|
||||
|
||||
The program should do a signature verify check on the signature, public key from the transaction submitter and the message of the previous storage epoch PoH value.
|
||||
|
||||
### ProofValidation
|
||||
|
||||
```text
|
||||
ProofValidation {
|
||||
proof_mask: Vec<ProofStatus>,
|
||||
}
|
||||
keys = [validator_keypair, archiver_keypair(s) (unsigned)]
|
||||
```
|
||||
|
||||
A validator will submit this transaction to indicate that a set of proofs for a given segment are valid/not-valid or skipped where the validator did not look at it. The keypairs for the archivers that it looked at should be referenced in the keys so the program logic can go to those accounts and see that the proofs are generated in the previous epoch. The sampling of the storage proofs should be verified ensuring that the correct proofs are skipped by the validator according to the logic outlined in the validator behavior of sampling.
|
||||
|
||||
The included archiver keys will indicate the the storage samples which are being referenced; the length of the proof\_mask should be verified against the set of storage proofs in the referenced archiver account\(s\), and should match with the number of proofs submitted in the previous storage epoch in the state of said archiver account.
|
||||
|
||||
### ClaimStorageReward
|
||||
|
||||
```text
|
||||
ClaimStorageReward {
|
||||
}
|
||||
keys = [validator_keypair or archiver_keypair, validator/archiver_keypairs (unsigned)]
|
||||
```
|
||||
|
||||
Archivers and validators will use this transaction to get paid tokens from a program state where SubmitStorageProof, ProofValidation and ChallengeProofValidations are in a state where proofs have been submitted and validated and there are no ChallengeProofValidations referencing those proofs. For a validator, it should reference the archiver keypairs to which it has validated proofs in the relevant epoch. And for an archiver it should reference validator keypairs for which it has validated and wants to be rewarded.
|
||||
|
||||
### ChallengeProofValidation
|
||||
|
||||
```text
|
||||
ChallengeProofValidation {
|
||||
proof_index: u64,
|
||||
hash_seed_value: Vec<u8>,
|
||||
}
|
||||
keys = [archiver_keypair, validator_keypair]
|
||||
```
|
||||
|
||||
This transaction is for catching lazy validators who are not doing the work to validate proofs. An archiver will submit this transaction when it sees a validator has approved a fake SubmitMiningProof transaction. Since the archiver is a light client not looking at the full chain, it will have to ask a validator or some set of validators for this information maybe via RPC call to obtain all ProofValidations for a certain segment in the previous storage epoch. The program will look in the validator account state see that a ProofValidation is submitted in the previous storage epoch and hash the hash\_seed\_value and see that the hash matches the SubmitMiningProof transaction and that the validator marked it as valid. If so, then it will save the challenge to the list of challenges that it has in its state.
|
||||
|
||||
### AdvertiseStorageRecentBlockhash
|
||||
|
||||
```text
|
||||
AdvertiseStorageRecentBlockhash {
|
||||
hash: Hash,
|
||||
slot: u64,
|
||||
}
|
||||
```
|
||||
|
||||
Validators and archivers will submit this to indicate that a new storage epoch has passed and that the storage proofs which are current proofs should now be for the previous epoch. Other transactions should check to see that the epoch that they are referencing is accurate according to current chain state.
|
||||
|
155
docs/src/proposals/simple-payment-and-state-verification.md
Normal file
155
docs/src/proposals/simple-payment-and-state-verification.md
Normal file
@ -0,0 +1,155 @@
|
||||
# Simple Payment and State Verification
|
||||
|
||||
It is often useful to allow low resourced clients to participate in a Solana
|
||||
cluster. Be this participation economic or contract execution, verification
|
||||
that a client's activity has been accepted by the network is typically
|
||||
expensive. This proposal lays out a mechanism for such clients to confirm that
|
||||
their actions have been committed to the ledger state with minimal resource
|
||||
expenditure and third-party trust.
|
||||
|
||||
## A Naive Approach
|
||||
|
||||
Validators store the signatures of recently confirmed transactions for a short
|
||||
period of time to ensure that they are not processed more than once. Validators
|
||||
provide a JSON RPC endpoint, which clients can use to query the cluster if a
|
||||
transaction has been recently processed. Validators also provide a PubSub
|
||||
notification, whereby a client registers to be notified when a given signature
|
||||
is observed by the validator. While these two mechanisms allow a client to
|
||||
verify a payment, they are not a proof and rely on completely trusting a
|
||||
validator.
|
||||
|
||||
We will describe a way to minimize this trust using Merkle Proofs to anchor the
|
||||
validator's response in the ledger, allowing the client to confirm on their own
|
||||
that a sufficient number of their preferred validators have confirmed a
|
||||
transaction. Requiring multiple validator attestations further reduces trust in
|
||||
the validator, as it increases both the technical and economic difficulty of
|
||||
compromising several other network participants.
|
||||
|
||||
## Light Clients
|
||||
|
||||
A 'light client' is a cluster participant that does not itself run a validator.
|
||||
This light client would provide a level of security greater than trusting a
|
||||
remote validator, without requiring the light client to spend a lot of resources
|
||||
verifying the ledger.
|
||||
|
||||
Rather than providing transaction signatures directly to a light client, the
|
||||
validator instead generates a Merkle Proof from the transaction of interest to
|
||||
the root of a Merkle Tree of all transactions in the including block. This
|
||||
Merkle Root is stored in a ledger entry which is voted on by validators,
|
||||
providing it consensus legitimacy. The additional level of security for a light
|
||||
client depends on an initial canonical set of validators the light client
|
||||
considers to be the stakeholders of the cluster. As that set is changed, the
|
||||
client can update its internal set of known validators with
|
||||
[receipts](simple-payment-and-state-verification.md#receipts). This may become
|
||||
challenging with a large number of delegated stakes.
|
||||
|
||||
Validators themselves may want to use light client APIs for performance reasons.
|
||||
For example, during the initial launch of a validator, the validator may use a
|
||||
cluster provided checkpoint of the state and verify it with a receipt.
|
||||
|
||||
## Receipts
|
||||
|
||||
A receipt is a minimal proof that; a transaction has been included in a block,
|
||||
that the block has been voted on by the client's preferred set of validators
|
||||
and that the votes have reached the desired confirmation depth.
|
||||
|
||||
### Transaction Inclusion Proof
|
||||
|
||||
A transaction inclusion proof is a data structure that contains a Merkle Path
|
||||
from a transaction, through an Entry-Merkle to a Block-Merkle, which is included
|
||||
in a Bank-Hash with the required set of validator votes. A chain of PoH Entries
|
||||
containing subsequent validator votes, deriving from the Bank-Hash, is the proof
|
||||
of confirmation. Clients can examine this ledger data and compute finality using
|
||||
Solana's fork selection rules.
|
||||
|
||||
An Entry-Merkle is a Merkle Root including all transactions in a given entry,
|
||||
sorted by signature.
|
||||
|
||||
A Block-Merkle is the Merkle Root of all the Entry-Merkles sequenced in the block.
|
||||
|
||||

|
||||
|
||||
A Bank-Hash is the hash of the concatenation of the Block-Merkle and Accounts-Hash
|
||||
|
||||
<img alt="Bank Hash Diagram" src="img/spv-bank-hash.svg" class="center"/>
|
||||
|
||||
An Accounts-Hash is the hash of the concatentation of the state hashes of each
|
||||
account modified during the current slot.
|
||||
|
||||
Transaction status is necessary for the receipt because the state receipt is
|
||||
constructed for the block. Two transactions over the same state can appear in
|
||||
the block, and therefore, there is no way to infer from just the state whether
|
||||
a transaction that is committed to the ledger has succeeded or failed in
|
||||
modifying the intended state. It may not be necessary to encode the full status
|
||||
code, but a single status bit to indicate the transaction's success.
|
||||
|
||||
### Account State Verification
|
||||
|
||||
An account's state (balance or other data) can be verified by submitting a
|
||||
transaction with a ___TBD___ Instruction to the cluster. The client can then
|
||||
use a [Transaction Inclusion Proof](#transaction-inclusion-proof) to verify
|
||||
whether the cluster agrees that the acount has reached the expected state.
|
||||
|
||||
### Validator Votes
|
||||
|
||||
Leaders should coalesce the validator votes by stake weight into a single entry.
|
||||
This will reduce the number of entries necessary to create a receipt.
|
||||
|
||||
### Chain of Entries
|
||||
|
||||
A receipt has a PoH link from the payment or state Merkle Path root to a list
|
||||
of consecutive validation votes.
|
||||
|
||||
It contains the following:
|
||||
|
||||
* Transaction -> Entry-Merkle -> Block-Merkle -> Bank-Hash
|
||||
|
||||
And a vector of PoH entries:
|
||||
|
||||
* Validator vote entries
|
||||
* Ticks
|
||||
* Light entries
|
||||
|
||||
```text
|
||||
/// This Entry definition skips over the transactions and only contains the
|
||||
/// hash of the transactions used to modify PoH.
|
||||
LightEntry {
|
||||
/// The number of hashes since the previous Entry ID.
|
||||
pub num_hashes: u64,
|
||||
/// The SHA-256 hash `num_hashes` after the previous Entry ID.
|
||||
hash: Hash,
|
||||
/// The Merkle Root of the transactions encoded into the Entry.
|
||||
entry_hash: Hash,
|
||||
}
|
||||
```
|
||||
|
||||
The light entries are reconstructed from Entries and simply show the entry
|
||||
Merkle Root that was mixed in to the PoH hash, instead of the full transaction
|
||||
set.
|
||||
|
||||
Clients do not need the starting vote state. The
|
||||
[fork selection](../implemented-proposals/tower-bft.md) algorithm is defined
|
||||
such that only votes that appear after the transaction provide finality for the
|
||||
transaction, and finality is independent of the starting state.
|
||||
|
||||
### Verification
|
||||
|
||||
A light client that is aware of the supermajority set validators can verify a
|
||||
receipt by following the Merkle Path to the PoH chain. The Block-Merkle is the
|
||||
Merkle Root and will appear in votes included in an Entry. The light client can
|
||||
simulate [fork selection](../implemented-proposals/tower-bft.md) for the
|
||||
consecutive votes and verify that the receipt is confirmed at the desired
|
||||
lockout threshold.
|
||||
|
||||
### Synthetic State
|
||||
|
||||
Synthetic state should be computed into the Bank-Hash along with the bank
|
||||
generated state.
|
||||
|
||||
For example:
|
||||
|
||||
* Epoch validator accounts and their stakes and weights.
|
||||
* Computed fee rates
|
||||
|
||||
These values should have an entry in the Bank-Hash. They should live under known
|
||||
accounts, and therefore have an index into the hash concatenation.
|
58
docs/src/proposals/slashing.md
Normal file
58
docs/src/proposals/slashing.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Slashing rules
|
||||
|
||||
Unlike Proof of Work \(PoW\) where off-chain capital expenses are already
|
||||
deployed at the time of block construction/voting, PoS systems require
|
||||
capital-at-risk to prevent a logical/optimal strategy of multiple chain voting.
|
||||
We intend to implement slashing rules which, if broken, result some amount of
|
||||
the offending validator's deposited stake to be removed from circulation. Given
|
||||
the ordering properties of the PoH data structure, we believe we can simplify
|
||||
our slashing rules to the level of a voting lockout time assigned per vote.
|
||||
|
||||
I.e. Each vote has an associated lockout time \(PoH duration\) that represents
|
||||
a duration by any additional vote from that validator must be in a PoH that
|
||||
contains the original vote, or a portion of that validator's stake is
|
||||
slashable. This duration time is a function of the initial vote PoH count and
|
||||
all additional vote PoH counts. It will likely take the form:
|
||||
|
||||
```text
|
||||
Lockouti\(PoHi, PoHj\) = PoHj + K \* exp\(\(PoHj - PoHi\) / K\)
|
||||
```
|
||||
|
||||
Where PoHi is the height of the vote that the lockout is to be applied to and
|
||||
PoHj is the height of the current vote on the same fork. If the validator
|
||||
submits a vote on a different PoH fork on any PoHk where k > j > i and
|
||||
PoHk < Lockout\(PoHi, PoHj\), then a portion of that validator's stake is at
|
||||
risk of being slashed.
|
||||
|
||||
In addition to the functional form lockout described above, early
|
||||
implementation may be a numerical approximation based on a First In, First Out
|
||||
\(FIFO\) data structure and the following logic:
|
||||
|
||||
* FIFO queue holding 32 votes per active validator
|
||||
* new votes are pushed on top of queue \(`push_front`\)
|
||||
* expired votes are popped off top \(`pop_front`\)
|
||||
* as votes are pushed into the queue, the lockout of each queued vote doubles
|
||||
* votes are removed from back of queue if `queue.len() > 32`
|
||||
* the earliest and latest height that has been removed from the back of the
|
||||
queue should be stored
|
||||
|
||||
It is likely that a reward will be offered as a % of the slashed amount to any
|
||||
node that submits proof of this slashing condition being violated to the PoH.
|
||||
|
||||
### Partial Slashing
|
||||
|
||||
In the schema described so far, when a validator votes on a given PoH stream,
|
||||
they are committing themselves to that fork for a time determined by the vote
|
||||
lockout. An open question is whether validators will be hesitant to begin
|
||||
voting on an available fork if the penalties are perceived too harsh for an
|
||||
honest mistake or flipped bit.
|
||||
|
||||
One way to address this concern would be a partial slashing design that results
|
||||
in a slashable amount as a function of either:
|
||||
|
||||
1. the fraction of validators, out of the total validator pool, that were also
|
||||
slashed during the same time period \(ala Casper\)
|
||||
2. the amount of time since the vote was cast \(e.g. a linearly increasing % of
|
||||
total deposited as slashable amount over time\), or both.
|
||||
|
||||
This is an area currently under exploration.
|
9
docs/src/proposals/snapshot-verification.md
Normal file
9
docs/src/proposals/snapshot-verification.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Snapshot Verification
|
||||
|
||||
## Problem
|
||||
|
||||
Snapshot verification of the account states is implemented, but the bank hash of the snapshot which is used to verify is falsifiable.
|
||||
|
||||
## Solution
|
||||
|
||||
While a validator is processing transactions to catch up to the cluster from the snapshot, use incoming vote transactions and the commitment calculator to confirm that the cluster is indeed building on the snapshotted bank hash. Once a threshold commitment level is reached, accept the snapshot as valid and start voting.
|
69
docs/src/proposals/tick-verification.md
Normal file
69
docs/src/proposals/tick-verification.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Tick Verification
|
||||
|
||||
This design the criteria and validation of ticks in a slot. It also describes
|
||||
error handling and slashing conditions encompassing how the system handles
|
||||
transmissions that do not meet these requirements.
|
||||
|
||||
# Slot structure
|
||||
|
||||
Each slot must contain an expected `ticks_per_slot` number of ticks. The last
|
||||
shred in a slot must contain only the entirety of the last tick, and nothing
|
||||
else. The leader must also mark this shred containing the last tick with the
|
||||
`LAST_SHRED_IN_SLOT` flag. Between ticks, there must be `hashes_per_tick`
|
||||
number of hashes.
|
||||
|
||||
# Handling bad transmissions
|
||||
|
||||
Malicious transmissions `T` are handled in two ways:
|
||||
|
||||
1) If a leader can generate some erronenous transmission `T` and also some
|
||||
alternate transmission `T'` for the same slot without violating any slashing
|
||||
rules for duplicate transmissions (for instance if `T'` is a subset of `T`),
|
||||
then the cluster must handle the possibility of both transmissions being live.
|
||||
|
||||
Thus this means we cannot mark the erronenous transmission `T` as dead because
|
||||
the cluster may have reached consensus on `T'`. These cases necessitate a
|
||||
slashing proof to punish this bad behavior.
|
||||
|
||||
2) Otherwise, we can simply mark the slot as dead and not playable. A slashing
|
||||
proof may or may not be necessary depending on feasibility.
|
||||
|
||||
# Blockstore receiving shreds
|
||||
|
||||
When blockstore receives a new shred `s`, there are two cases:
|
||||
|
||||
1) `s` is marked as `LAST_SHRED_IN_SLOT`, then check if there exists a shred
|
||||
`s'` in blockstore for that slot where `s'.index > s.index` If so, together `s`
|
||||
and `s'` constitute a slashing proof.
|
||||
|
||||
2) Blockstore has already received a shred `s'` marked as `LAST_SHRED_IN_SLOT`
|
||||
with index `i`. If `s.index > i`, then together `s` and `s'`constitute a
|
||||
slashing proof. In this case, blockstore will also not insert `s`.
|
||||
|
||||
3) Duplicate shreds for the same index are ignored. Non-duplicate shreds for
|
||||
the same index are a slashable condition. Details for this case are covered
|
||||
in the `Leader Duplicate Block Slashing` section.
|
||||
|
||||
|
||||
# Replaying and validating ticks
|
||||
|
||||
1) Replay stage replays entries from blockstore, keeping track of the number of
|
||||
ticks it has seen per slot, and verifying there are `hashes_per_tick` number of
|
||||
hashes between ticcks. After the tick from this last shred has been played,
|
||||
replay stage then checks the total number of ticks.
|
||||
|
||||
Failure scenario 1: If ever there are two consecutive ticks between which the
|
||||
number of hashes is `!= hashes_per_tick`, mark this slot as dead.
|
||||
|
||||
Failure scenario 2: If the number of ticks != `ticks_per_slot`, mark slot as
|
||||
dead.
|
||||
|
||||
Failure scenario 3: If the number of ticks reaches `ticks_per_slot`, but we still
|
||||
haven't seen the `LAST_SHRED_IN_SLOT`, mark this slot as dead.
|
||||
|
||||
2) When ReplayStage reaches a shred marked as the last shred, it checks if this
|
||||
last shred is a tick.
|
||||
|
||||
Failure scenario: If the signed shred with the `LAST_SHRED_IN_SLOT` flag cannot
|
||||
be deserialized into a tick (either fails to deserialize or deserializes into
|
||||
an entry), mark this slot as dead.
|
52
docs/src/proposals/validator-proposal.md
Normal file
52
docs/src/proposals/validator-proposal.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Validator
|
||||
|
||||
## History
|
||||
|
||||
When we first started Solana, the goal was to de-risk our TPS claims. We knew
|
||||
that between optimistic concurrency control and sufficiently long leader slots,
|
||||
that PoS consensus was not the biggest risk to TPS. It was GPU-based signature
|
||||
verification, software pipelining and concurrent banking. Thus, the TPU was
|
||||
born. After topping 100k TPS, we split the team into one group working toward
|
||||
710k TPS and another to flesh out the validator pipeline. Hence, the TVU was
|
||||
born. The current architecture is a consequence of incremental development with
|
||||
that ordering and project priorities. It is not a reflection of what we ever
|
||||
believed was the most technically elegant cross-section of those technologies.
|
||||
In the context of leader rotation, the strong distinction between leading and
|
||||
validating is blurred.
|
||||
|
||||
## Difference between validating and leading
|
||||
|
||||
The fundamental difference between the pipelines is when the PoH is present. In
|
||||
a leader, we process transactions, removing bad ones, and then tag the result
|
||||
with a PoH hash. In the validator, we verify that hash, peel it off, and
|
||||
process the transactions in exactly the same way. The only difference is that
|
||||
if a validator sees a bad transaction, it can't simply remove it like the
|
||||
leader does, because that would cause the PoH hash to change. Instead, it
|
||||
rejects the whole block. The other difference between the pipelines is what
|
||||
happens _after_ banking. The leader broadcasts entries to downstream validators
|
||||
whereas the validator will have already done that in RetransmitStage, which is
|
||||
a confirmation time optimization. The validation pipeline, on the other hand,
|
||||
has one last step. Any time it finishes processing a block, it needs to weigh
|
||||
any forks it's observing, possibly cast a vote, and if so, reset its PoH hash
|
||||
to the block hash it just voted on.
|
||||
|
||||
## Proposed Design
|
||||
|
||||
We unwrap the many abstraction layers and build a single pipeline that can
|
||||
toggle leader mode on whenever the validator's ID shows up in the leader
|
||||
schedule.
|
||||
|
||||

|
||||
|
||||
## Notable changes
|
||||
|
||||
* Hoist FetchStage and BroadcastStage out of TPU
|
||||
* BankForks renamed to Banktree
|
||||
* TPU moves to new socket-free crate called solana-tpu.
|
||||
* TPU's BankingStage absorbs ReplayStage
|
||||
* TVU goes away
|
||||
* New RepairStage absorbs Shred Fetch Stage and repair requests
|
||||
* JSON RPC Service is optional - used for debugging. It should instead be part
|
||||
of a separate `solana-blockstreamer` executable.
|
||||
* New MulticastStage absorbs retransmit part of RetransmitStage
|
||||
* MulticastStage downstream of Blockstore
|
111
docs/src/proposals/vote-signing-to-implement.md
Normal file
111
docs/src/proposals/vote-signing-to-implement.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Secure Vote Signing
|
||||
|
||||
## Secure Vote Signing
|
||||
|
||||
This design describes additional vote signing behavior that will make the process more secure.
|
||||
|
||||
Currently, Solana implements a vote-signing service that evaluates each vote to ensure it does not violate a slashing condition. The service could potentially have different variations, depending on the hardware platform capabilities. In particular, it could be used in conjunction with a secure enclave \(such as SGX\). The enclave could generate an asymmetric key, exposing an API for user \(untrusted\) code to sign the vote transactions, while keeping the vote-signing private key in its protected memory.
|
||||
|
||||
The following sections outline how this architecture would work:
|
||||
|
||||
### Message Flow
|
||||
|
||||
1. The node initializes the enclave at startup
|
||||
* The enclave generates an asymmetric key and returns the public key to the
|
||||
|
||||
node
|
||||
|
||||
* The keypair is ephemeral. A new keypair is generated on node bootup. A
|
||||
|
||||
new keypair might also be generated at runtime based on some to be determined
|
||||
|
||||
criteria.
|
||||
|
||||
* The enclave returns its attestation report to the node
|
||||
2. The node performs attestation of the enclave \(e.g using Intel's IAS APIs\)
|
||||
* The node ensures that the Secure Enclave is running on a TPM and is
|
||||
|
||||
signed by a trusted party
|
||||
3. The stakeholder of the node grants ephemeral key permission to use its stake.
|
||||
|
||||
This process is to be determined.
|
||||
|
||||
4. The node's untrusted, non-enclave software calls trusted enclave software
|
||||
|
||||
using its interface to sign transactions and other data.
|
||||
|
||||
* In case of vote signing, the node needs to verify the PoH. The PoH
|
||||
|
||||
verification is an integral part of signing. The enclave would be
|
||||
|
||||
presented with some verifiable data to check before signing the vote.
|
||||
|
||||
* The process of generating the verifiable data in untrusted space is to be determined
|
||||
|
||||
### PoH Verification
|
||||
|
||||
1. When the node votes on an en entry `X`, there's a lockout period `N`, for
|
||||
|
||||
which it cannot vote on a fork that does not contain `X` in its history.
|
||||
|
||||
2. Every time the node votes on the derivative of `X`, say `X+y`, the lockout
|
||||
|
||||
period for `X` increases by a factor `F` \(i.e. the duration node cannot vote on
|
||||
|
||||
a fork that does not contain `X` increases\).
|
||||
|
||||
* The lockout period for `X+y` is still `N` until the node votes again.
|
||||
|
||||
3. The lockout period increment is capped \(e.g. factor `F` applies maximum 32
|
||||
|
||||
times\).
|
||||
|
||||
4. The signing enclave must not sign a vote that violates this policy. This
|
||||
|
||||
means
|
||||
|
||||
* Enclave is initialized with `N`, `F` and `Factor cap`
|
||||
* Enclave stores `Factor cap` number of entry IDs on which the node had
|
||||
|
||||
previously voted
|
||||
|
||||
* The sign request contains the entry ID for the new vote
|
||||
* Enclave verifies that new vote's entry ID is on the correct fork
|
||||
|
||||
\(following the rules \#1 and \#2 above\)
|
||||
|
||||
### Ancestor Verification
|
||||
|
||||
This is alternate, albeit, less certain approach to verifying voting fork. 1. The validator maintains an active set of nodes in the cluster 2. It observes the votes from the active set in the last voting period 3. It stores the ancestor/last\_tick at which each node voted 4. It sends new vote request to vote-signing service
|
||||
|
||||
* It includes previous votes from nodes in the active set, and their
|
||||
|
||||
corresponding ancestors
|
||||
|
||||
1. The signer checks if the previous votes contains a vote from the validator,
|
||||
|
||||
and the vote ancestor matches with majority of the nodes
|
||||
|
||||
* It signs the new vote if the check is successful
|
||||
* It asserts \(raises an alarm of some sort\) if the check is unsuccessful
|
||||
|
||||
The premise is that the validator can be spoofed at most once to vote on incorrect data. If someone hijacks the validator and submits a vote request for bogus data, that vote will not be included in the PoH \(as it'll be rejected by the cluster\). The next time the validator sends a request to sign the vote, the signing service will detect that validator's last vote is missing \(as part of
|
||||
|
||||
## 5 above\).
|
||||
|
||||
### Fork determination
|
||||
|
||||
Due to the fact that the enclave cannot process PoH, it has no direct knowledge of fork history of a submitted validator vote. Each enclave should be initiated with the current _active set_ of public keys. A validator should submit its current vote along with the votes of the active set \(including itself\) that it observed in the slot of its previous vote. In this way, the enclave can surmise the votes accompanying the validator's previous vote and thus the fork being voted on. This is not possible for the validator's initial submitted vote, as it will not have a 'previous' slot to reference. To account for this, a short voting freeze should apply until the second vote is submitted containing the votes within the active set, along with it's own vote, at the height of the initial vote.
|
||||
|
||||
### Enclave configuration
|
||||
|
||||
A staking client should be configurable to prevent voting on inactive forks. This mechanism should use the client's known active set `N_active` along with a threshold vote `N_vote` and a threshold depth `N_depth` to determine whether or not to continue voting on a submitted fork. This configuration should take the form of a rule such that the client will only vote on a fork if it observes more than `N_vote` at `N_depth`. Practically, this represents the client from confirming that it has observed some probability of economic finality of the submitted fork at a depth where an additional vote would create a lockout for an undesirable amount of time if that fork turns out not to be live.
|
||||
|
||||
### Challenges
|
||||
|
||||
1. Generation of verifiable data in untrusted space for PoH verification in the
|
||||
|
||||
enclave.
|
||||
|
||||
2. Need infrastructure for granting stake to an ephemeral key.
|
||||
|
Reference in New Issue
Block a user