Transactions
(Temporary document, to be integrated into other docs)
Transaction behaviour
In our network context, we have two types of nodes.
- Miner (
M) - Spectator (
S)
S acts as a relayer for transactions in order to help out
that transactions reach M.
To avoid spam attacks, S should keep in their mempool for some
period of time, and then prune it.
Ideal simulation with instant confirmation
The lifetime of a transaction that passes verification and whose state transition can be applied on top of the canonical (confirmed) chain:
- User creates a transaction
- User broadcasts to
S Svalidates state transition- enters
Smempool Sbroadcasts toMMvalidates state transition- enters
Mmempool Mvalidates all transactions in itsmempoolin sequenceMproposes a block confirmation containingMwrites the state transition update of to their chainMremoves from theirmempoolMbroadcasts the confirmed proposalSreceives the proposal and validates transactionsSwrites the state updates to their chainSremoves from theirmempool
Real-world simulation with non-instant confirmation
The lifetime of a transaction that passes verification and whose state transition is pending to be applied on top of the canonical (confirmed) chain:
- User creates a transaction
- User broadcasts to
S Svalidates state transition- enters
Smempool Sbroadcasts toMMvalidates state transition- enters
Mmempool Mproposes a block proposal containingMproposes more block proposals- When proposals can be confirmed,
Mvalidates all their transactions in sequence Mwrites the state transition update of to their chainMremoves from theirmempoolMbroadcasts the confirmed proposals sequenceSreceives the proposals sequence and validates transactionsSwrites the state updates to their chainSremoves from theirmempool
Real-world simulation with non-instant confirmation, forks and multiple CP nodes
The lifetime of a transaction that passes verifications and whose state transition is pending to be applied on top of the canonical (confirmed) chain:
- User creates a transaction
- User broadcasts to
S Svalidates state transition against canonical chain state- enters
Smempool Sbroadcasts toPMvalidates state transition against all known fork states- enters
Mmempool Mbroadcasts to restMnodes- Block producer
SMfinds which fork to extend SMvalidates all unproposed transactions in itsmempoolin sequence, extended fork state, discarding invalidSMcreates a block proposal containing extending the forkMreceives block proposal and validates its transactions against the extended fork stateSMproposes more block proposals extending a fork state- When a fork can be confirmed,
Mvalidates all its proposals transactions in sequence, against canonical state Mwrites the state transition update of to their chainMremoves from theirmempoolMdrop rest forks and keeps only the confirmed oneMbroadcasts the confirmed proposals sequenceSreceives the proposals sequence and validates transactionsSwrites the state updates to their chainSremoves from theirmempool
M will keep in its mempool as long as it is a valid state
transition for any fork(including canonical) or it get confirmed.
Unproposed transactions refers to all not included in a proposal of any fork.
If a fork that can be confirmed fails to validate all its transactions(14), it should be dropped.
The Transaction object
pub struct ContractCall {
/// The contract ID to which the payload is fed to
pub contract_id: ContractId,
/// Arbitrary payload for the contract call
pub payload: Vec<u8>,
}
pub struct Transaction {
/// Calls executed in this transaction
pub calls: Vec<ContractCall>,
/// Attached ZK proofs
pub proofs: Vec<Vec<Proof>>,
/// Attached Schnorr signatures
pub signatures: Vec<Vec<Signature>>,
}
A generic DarkFi transaction object is simply an array of smart contract calls, along with attached ZK proofs and signatures needed to properly verify the contracts' execution. A transaction can have any number of calls, and proofs, provided it does not exhaust a set gas limit.
In DarkFi, every operation is a smart contract. This includes payments, which we'll explain in the following section.
Payments
For A -> B payments in DarkFi we use the Sapling scheme that originates from zcash. A payment transaction has a number of inputs (which are coins being burned/spent), and a number of outputs (which are coins being minted/created). An explanation for the ZK proofs for this scheme can be found here under the Zkas section of this book.
In code, the structs we use are the following:
pub struct MoneyTransferParams {
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
}
pub struct Input {
/// Pedersen commitment for the input's value
pub value_commit: ValueCommit,
/// Pedersen commitment for the input's token ID
pub token_commit: ValueCommit,
/// Revealed nullifier
pub nullifier: Nullifier,
/// Revealed Merkle root
pub merkle_root: MerkleNode,
/// Public key for the Schnorr signature
pub signature_public: PublicKey,
}
pub struct Output {
/// Pedersen commitment for the output's value
pub value_commit: ValueCommit,
/// Pedersen commitment for the output's token ID
pub token_commit: ValueCommit,
/// Minted coin: poseidon_hash(pubkey, value, token, serial, blind)
pub coin: Coin,
/// The encrypted note ciphertext
pub encrypted_note: EncryptedNote,
}
pub struct EncryptedNote {
pub ciphertext: Vec<u8>,
pub ephemeral_key: PublicKey,
}
pub struct Note {
/// Serial number of the coin, used to derive the nullifier
pub serial: pallas::Base,
/// Value of the coin
pub value: u64,
/// Token ID of the coin
pub token_id: TokenId,
/// Blinding factor for the value Pedersen commitment
pub value_blind: ValueBlind,
/// Blinding factor for the token ID Pedersen commitment
pub token_blind: ValueBlind,
/// Attached memo (arbitrary data)
pub memo: Vec<u8>,
}
In the blockchain state, every minted coin must be added into a Merkle tree of all existing coins. Once added, the new tree root is used to prove existence of this coin when it's being spent.
Let's imagine a scenario where Alice has 100 ALICE tokens and wants to
send them to Bob. Alice would create an Input object using the info
she has of her coin. She has to derive a nullifier given her secret
key and the serial number of the coin, hash the coin bulla so she can
create a merkle path proof, and derive the value and token commitments
using the blinds.
let nullifier = poseidon_hash([alice_secret_key, serial]);
let signature_public = alice_secret_key * Generator;
let coin = poseidon_hash([signature_public, value, token_id, blind]);
let merkle_root = calculate_merkle_root(coin);
let value_commit = pedersen_commitment(value, value_blind);
let token_commit = pedersen_commitment(token_id, token_blind);
The values above, except coin become the public inputs for the Burn
ZK proof. If everything is correct, this allows Alice to spend her coin.
In DarkFi, the changes have to be atomic, so any payment transaction
that is burning some coins, has to mint new coins at the same time, and
no value must be lost, nor can the token ID change. We enforce this by
using Pedersen commitments.
Now that Alice has a valid Burn proof and can spend her coin, she can
mint a new coin for Bob.
let blind = pallas::Base::random();
let value_blind = ValueBlind::random();
let token_blind = ValueBlind::random();
let coin = poseidon_hash([bob_public, value, token_id, blind]);
let value_commit = pedersen_commitment(value, value_blind);
let token_commit = pedersen_commitment(token, token_blind);
coin, value_commit, and token_commit become the public inputs
for the Mint ZK proof. If this proof is valid, it creates a new coin
for Bob with the given parameters. Additionally, Alice would put the
values and blinds in a Note which is encrypted with Bob's public key
so only Bob is able to decrypt it. This Note has the necessary info
for him to further spend the coin he received.
At this point Alice should have 1 input and 1 output. The input is the
coin she burned, and the output is the coin she minted for Bob. Along
with this, she has two ZK proofs that prove creation of the input and
output. Now she can build a transaction object, and then use her secret
key she derived in the Burn proof to sign the transaction and publish
it to the blockchain.
The blockchain will execute the smart contract with the given payload and verify that the Pedersen commitments match, that the nullifier has not been published before, and also that the merkle authentication path is valid and therefore the coin existed in a previous state. Outside of the VM, the validator will also verify the signature(s) and the ZK proofs. If this is valid, then Alice's coin is now burned and cannot be used anymore. And since Alice also created an output for Bob, this new coin is now added to the Merkle tree and is able to be spent by him. Effectively this means that Alice has sent her tokens to Bob.