consensus/state: Clean up native wasm deploy and verification API.

This commit is contained in:
parazyd
2022-11-24 12:55:27 +01:00
parent d672e1e9c0
commit 60e6f94edb
5 changed files with 246 additions and 96 deletions

View File

@@ -38,7 +38,7 @@ zkas: $(BINDEPS)
contracts: zkas contracts: zkas
$(MAKE) -C src/contract/money $(MAKE) -C src/contract/money
$(PROOFS_BIN): $(PROOFS) zkas $(PROOFS_BIN): $(PROOFS)
./zkas $(basename $@) -o $@ ./zkas $(basename $@) -o $@
token_lists: token_lists:

View File

@@ -16,16 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{io::Cursor, time::Duration}; use std::{collections::HashMap, io::Cursor, time::Duration};
use async_std::sync::{Arc, RwLock}; use async_std::sync::{Arc, RwLock};
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use darkfi_sdk::crypto::{ use darkfi_sdk::{
constants::MERKLE_DEPTH, crypto::{
schnorr::{SchnorrPublic, SchnorrSecret}, constants::MERKLE_DEPTH,
ContractId, MerkleNode, PublicKey, schnorr::{SchnorrPublic, SchnorrSecret},
ContractId, MerkleNode, PublicKey,
},
db::ZKAS_DB_NAME,
};
use darkfi_serial::{
deserialize, serialize, Decodable, Encodable, SerialDecodable, SerialEncodable, WriteExt,
}; };
use darkfi_serial::{serialize, Decodable, Encodable, SerialDecodable, SerialEncodable, WriteExt};
use incrementalmerkletree::{bridgetree::BridgeTree, Tree}; use incrementalmerkletree::{bridgetree::BridgeTree, Tree};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use pasta_curves::{ use pasta_curves::{
@@ -130,6 +135,8 @@ pub struct ValidatorState {
pub blockchain: Blockchain, pub blockchain: Blockchain,
/// Pending transactions /// Pending transactions
pub unconfirmed_txs: Vec<Transaction>, pub unconfirmed_txs: Vec<Transaction>,
/// ZK proof verifying keys for smart contract calls
pub verifying_keys: Arc<RwLock<HashMap<[u8; 32], Vec<(String, VerifyingKey)>>>>,
/// Participating start slot /// Participating start slot
pub participating: Option<u64>, pub participating: Option<u64>,
/// Wallet interface /// Wallet interface
@@ -189,34 +196,68 @@ impl ValidatorState {
let unconfirmed_txs = vec![]; let unconfirmed_txs = vec![];
let participating = None; let participating = None;
// -----BEGIN ARTIFACT: WASM INTEGRATION----- // -----NATIVE WASM CONTRACTS-----
// This is the current place where this stuff is being done, and very loosely. // This is the current place where native contracts are being deployed.
// We initialize and "deploy" _native_ contracts here - currently the money contract. // When the `Blockchain` object is created, it doesn't care whether it
// Eventually, the crypsinous consensus should be a native contract like payments are. // already has the contract data or not. If there's existing data, it
// This means the previously existing Blockchain state will be a bit different and is // will just open the necessary db and trees, and give back what it has.
// going to have to be changed. // This means that on subsequent runs our native contracts will already
// When the `Blockchain` object is created, it doesn't care whether it already has // be in a deployed state, so what we actually do here is a redeployment.
// data or not. If there's existing data it will just open the necessary db and trees, // This kind of operation should only modify the contract's state in case
// and give back what it has. This means, on subsequent runs our native contracts will // it wasn't deployed before (meaning the initial run). Otherwise, it
// already be in a deployed state. So what we do here is a "re-deployment". This kind // shouldn't touch anything, or just potentially update the db schemas or
// of operation should only modify the contract's state in case it wasn't deployed // whatever is necessary. This logic should be handled in the init function
// before (meaning the initial run). Otherwise, it shouldn't touch anything, or just // of the actual contract, so make sure the native contracts handle this well.
// potentially update the database schemas or whatever is necessary. Here it's
// transparent and generic, and the entire logic for this db protection is supposed to // FIXME: This ID should be something that does not solve the pallas curve equation,
// be in the `init` function of the contract, so look there for a reference of the // and/or just hardcoded and forbidden in non-native contract deployment.
// databases and the state. let money_contract_id = ContractId::from(pallas::Base::from(u64::MAX - 420));
info!("ValidatorState::new(): Deploying \"money_contract.wasm\""); // The faucet pubkeys are pubkeys which are allowed to create clear inputs
let money_contract_wasm_bincode = include_bytes!("../contract/money/money_contract.wasm"); // in the money contract.
// XXX: FIXME: This ID should be something that does not solve the pallas curve equation, let money_contract_deploy_payload = serialize(&faucet_pubkeys);
// and/or just hardcoded and forbidden in non-native contract deployment.
let cid = ContractId::from(pallas::Base::from(u64::MAX - 420)); // In this hashmap, we keep references to ZK proof verifying keys needed
let mut runtime = Runtime::new(&money_contract_wasm_bincode[..], blockchain.clone(), cid)?; // for the circuits our native contracts provide.
// The faucet pubkeys are pubkeys which are allowed to create clear inputs in the let mut verifying_keys = HashMap::new();
// money contract.
let payload = serialize(&faucet_pubkeys); let native_contracts = vec![(
runtime.deploy(&payload)?; "Money Contract",
info!("Deployed Money Contract with ID: {}", cid); money_contract_id,
// -----END ARTIFACT----- include_bytes!("../contract/money/money_contract.wasm"),
money_contract_deploy_payload,
)];
info!("Deploying native wasm contracts");
for nc in native_contracts {
info!("Deploying {} with ContractID {}", nc.0, nc.1);
let mut runtime = Runtime::new(&nc.2[..], blockchain.clone(), nc.1)?;
runtime.deploy(&nc.3)?;
info!("Successfully deployed {}", nc.0);
// When deployed, we can do a lookup for the zkas circuits and
// initialize verifying keys for them.
info!("Creating ZK verifying keys for {} zkas circuits", nc.0);
debug!("Looking up zkas db for {} (ContractID: {})", nc.0, nc.1);
let zkas_db = blockchain.contracts.lookup(&blockchain.sled_db, &nc.1, ZKAS_DB_NAME)?;
let mut vks = vec![];
for i in zkas_db.iter() {
let (zkas_ns, zkas_bincode) = i?;
let zkas_ns: String = deserialize(&zkas_ns)?;
let zkas_bincode: Vec<u8> = deserialize(&zkas_bincode)?;
info!("Creating VerifyingKey for zkas circuit with namespace {}", zkas_ns);
let zkbin = ZkBinary::decode(&zkas_bincode)?;
let circuit = ZkCircuit::new(empty_witnesses(&zkbin), zkbin);
// FIXME: This k=13 man...
let vk = VerifyingKey::build(13, &circuit);
vks.push((zkas_ns, vk));
}
info!("Finished creating VerifyingKey objects for {} (ContractID: {})", nc.0, nc.1);
verifying_keys.insert(nc.1.to_bytes(), vks);
}
info!("Finished deployment of native wasm contracts");
// -----NATIVE WASM CONTRACTS-----
let zero = Float10::from_str_native("0").unwrap().with_precision(RADIX_BITS).value(); let zero = Float10::from_str_native("0").unwrap().with_precision(RADIX_BITS).value();
let one = Float10::from_str_native("1").unwrap().with_precision(RADIX_BITS).value(); let one = Float10::from_str_native("1").unwrap().with_precision(RADIX_BITS).value();
@@ -229,6 +270,7 @@ impl ValidatorState {
consensus, consensus,
blockchain, blockchain,
unconfirmed_txs, unconfirmed_txs,
verifying_keys: Arc::new(RwLock::new(verifying_keys)),
participating, participating,
wallet, wallet,
nullifiers: vec![], nullifiers: vec![],
@@ -261,7 +303,7 @@ impl ValidatorState {
} }
debug!("append_tx(): Starting state transition validation"); debug!("append_tx(): Starting state transition validation");
if let Err(e) = self.verify_transactions(&[tx.clone()], false) { if let Err(e) = self.verify_transactions(&[tx.clone()], false).await {
error!("append_tx(): Failed to verify transaction: {}", e); error!("append_tx(): Failed to verify transaction: {}", e);
return false return false
}; };
@@ -740,7 +782,7 @@ impl ValidatorState {
// Validate state transition against canonical state // Validate state transition against canonical state
// TODO: This should be validated against fork state // TODO: This should be validated against fork state
debug!("receive_proposal(): Starting state transition validation"); debug!("receive_proposal(): Starting state transition validation");
if let Err(e) = self.verify_transactions(&proposal.block.txs, false) { if let Err(e) = self.verify_transactions(&proposal.block.txs, false).await {
error!("receive_proposal(): Transaction verifications failed: {}", e); error!("receive_proposal(): Transaction verifications failed: {}", e);
return Err(e.into()) return Err(e.into())
}; };
@@ -908,7 +950,7 @@ impl ValidatorState {
// TODO: FIXME: The state transitions have already been written, they have to be in memory // TODO: FIXME: The state transitions have already been written, they have to be in memory
// until this point. // until this point.
debug!(target: "consensus", "Applying state transition for finalized block"); debug!(target: "consensus", "Applying state transition for finalized block");
if let Err(e) = self.verify_transactions(&proposal.txs, true) { if let Err(e) = self.verify_transactions(&proposal.txs, true).await {
error!(target: "consensus", "Finalized block transaction verifications failed: {}", e); error!(target: "consensus", "Finalized block transaction verifications failed: {}", e);
return Err(e) return Err(e)
} }
@@ -949,7 +991,7 @@ impl ValidatorState {
// Verify state transitions for all blocks and their respective transactions. // Verify state transitions for all blocks and their respective transactions.
debug!("receive_blocks(): Starting state transition validations"); debug!("receive_blocks(): Starting state transition validations");
for block in blocks { for block in blocks {
if let Err(e) = self.verify_transactions(&block.txs, false) { if let Err(e) = self.verify_transactions(&block.txs, false).await {
error!("receive_blocks(): Transaction verifications failed: {}", e); error!("receive_blocks(): Transaction verifications failed: {}", e);
return Err(e) return Err(e)
} }
@@ -1026,87 +1068,182 @@ impl ValidatorState {
/// the state transitions to the database. /// the state transitions to the database.
// TODO: This should be paralellized as if even one tx in the batch fails to verify, // TODO: This should be paralellized as if even one tx in the batch fails to verify,
// we can drop everything. // we can drop everything.
pub fn verify_transactions(&self, txs: &[Transaction], write: bool) -> Result<()> { pub async fn verify_transactions(&self, txs: &[Transaction], write: bool) -> Result<()> {
debug!("Verifying {} transaction(s)", txs.len()); debug!("Verifying {} transaction(s)", txs.len());
for tx in txs { for tx in txs {
let tx_hash = blake3::hash(&serialize(tx));
debug!("Verifying transaction {}", tx_hash);
// Table of public inputs used for ZK proof verification // Table of public inputs used for ZK proof verification
let mut zkp_table = vec![]; let mut zkp_table = vec![];
// Table of public keys used for signature verification // Table of public keys used for signature verification
let mut sig_table = vec![]; let mut sig_table = vec![];
// State updates produced by contract execution // State updates produced by contract execcution
let mut updates = vec![]; let mut updates = vec![];
// ZK circuit verifying keys (FIXME: These should be in a more global scope)
let mut verifying_keys = vec![];
// Iterate over all calls to get the metadata // Iterate over all calls to get the metadata
for (idx, call) in tx.calls.iter().enumerate() { for (idx, call) in tx.calls.iter().enumerate() {
debug!("Verifying contract call {}", idx); debug!("Executing contract call {}", idx);
// Check if the called contract exist as bincode. let wasm = match self.blockchain.wasm_bincode.get(call.contract_id) {
let bincode = self.blockchain.wasm_bincode.get(call.contract_id)?; Ok(v) => {
debug!("Found wasm bincode for {}", call.contract_id); debug!("Found wasm bincode for {}", call.contract_id);
v
}
Err(e) => {
error!(
"Could not find wasm bincode for contract {}: {}",
call.contract_id, e
);
return Err(Error::ContractNotFound(call.contract_id.to_string()))
}
};
// Write the actual payload data // Write the actual payload data
let mut payload = vec![]; let mut payload = vec![];
payload.write_u32(idx as u32)?; // Call index payload.write_u32(idx as u32)?; // Call index
tx.calls.encode(&mut payload)?; // Actual call_data tx.calls.encode(&mut payload)?; // Actual call data
// Instantiate the wasm runtime // Instantiate the wasm runtime
// TODO: Sum up the gas fees of these calls and instantiations
let mut runtime = let mut runtime =
Runtime::new(&bincode, self.blockchain.clone(), call.contract_id)?; match Runtime::new(&wasm, self.blockchain.clone(), call.contract_id) {
Ok(v) => v,
Err(e) => {
error!(
"Failed to instantiate WASM runtime for contract {}",
call.contract_id
);
return Err(e.into())
}
};
// Perform the execution to fetch verification metadata
debug!("Executing \"metadata\" call"); debug!("Executing \"metadata\" call");
let metadata = runtime.metadata(&payload)?; let metadata = match runtime.metadata(&payload) {
Ok(v) => v,
Err(e) => {
error!("Failed to execute \"metadata\" call: {}", e);
return Err(e.into())
}
};
// Decode the metadata retrieved from the execution
let mut decoder = Cursor::new(&metadata); let mut decoder = Cursor::new(&metadata);
let zkp_pub: Vec<(String, Vec<pallas::Base>)> = Decodable::decode(&mut decoder)?; let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
let sig_pub: Vec<PublicKey> = Decodable::decode(&mut decoder)?; match Decodable::decode(&mut decoder) {
// TODO: Make sure we've read all the data above Ok(v) => v,
Err(e) => {
error!("Failed to decode ZK public inputs from metadata: {}", e);
return Err(e.into())
}
};
let sig_pub: Vec<PublicKey> = match Decodable::decode(&mut decoder) {
Ok(v) => v,
Err(e) => {
error!("Failed to decode signature pubkeys from metadata: {}", e);
return Err(e.into())
}
};
// TODO: Make sure we've read all the bytes above.
debug!("Successfully executed \"metadata\" call");
zkp_table.push(zkp_pub); zkp_table.push(zkp_pub);
sig_table.push(sig_pub); sig_table.push(sig_pub);
debug!("Successfully executed \"metadata\" call");
// Execute the contract call // After getting the metadata, we run the "exec" function with the same
// runtime and the same payload.
debug!("Executing \"exec\" call"); debug!("Executing \"exec\" call");
let update = runtime.exec(&payload)?; match runtime.exec(&payload) {
updates.push(update); Ok(v) => {
debug!("Successfully executed \"exec\" call"); debug!("Successfully executed \"exec\" call");
updates.push(v);
}
Err(e) => {
error!(
"Failed to execute \"exec\" call for contract id {}: {}",
call.contract_id, e
);
return Err(e.into())
}
};
// At this point we're done with the call and move on to the next one.
} }
// Verify the Schnorr signatures with the public keys given to us from // When we're done looping and executing over the tx's contract calls, we
// the metadata call. // move on with verification. First we verify the signatures as that's
debug!("Verifying transaction signatures"); // cheaper, and then finally we verify the ZK proofs.
tx.verify_sigs(sig_table)?; debug!("Verifying signatures for transaction {}", tx_hash);
debug!("Signatures verified successfully!"); match tx.verify_sigs(sig_table) {
Ok(()) => debug!("Signatures verification for tx {} successful", tx_hash),
Err(e) => {
error!("Signature verification for tx {} failed: {}", tx_hash, e);
return Err(e.into())
}
};
// Finally, verify the ZK proofs // NOTE: When it comes to the ZK proofs, we first do a lookup of the
debug!("Verifying transaction ZK proofs"); // verifying keys, but if we do not find them, we'll generate them
tx.verify_zkps(&verifying_keys, zkp_table)?; // inside of this function. This can be kinda expensive, so open to
debug!("Transaction ZK proofs verified successfully!"); // alternatives.
debug!("Verifying ZK proofs for transaction {}", tx_hash);
match tx.verify_zkps(self.verifying_keys.clone(), zkp_table).await {
Ok(()) => debug!("ZK proof verification for tx {} successful", tx_hash),
Err(e) => {
error!("ZK proof verrification for tx {} failed: {}", tx_hash, e);
return Err(e.into())
}
};
// When the verification stage has passed, just apply all the changes. // After the verifications stage passes, if we're told to write, we
// TODO: FIXME: This writes directly to the database. Instead it should live // apply the state updates.
// in memory until things get finalized. (Search #finalization assert!(tx.calls.len() == updates.len());
// for additional notes).
// TODO: We instantiate new runtimes here, so pick up the gas fees from
// the previous runs and sum them all together.
if write { if write {
debug!("Performing state updates"); debug!("Performing state updates");
assert!(tx.calls.len() == updates.len());
for (call, update) in tx.calls.iter().zip(updates.iter()) { for (call, update) in tx.calls.iter().zip(updates.iter()) {
// Do the bincode lookups again // For this we instantiate the runtimes again.
let bincode = self.blockchain.wasm_bincode.get(call.contract_id)?; // TODO: Optimize this
debug!("Found wasm bincode for {}", call.contract_id); // TODO: Sum up the gas costs of previous calls during execution
// and verification and these.
let wasm = match self.blockchain.wasm_bincode.get(call.contract_id) {
Ok(v) => {
debug!("Found wasm bincode for {}", call.contract_id);
v
}
Err(e) => {
error!(
"Could not find wasm bincode for contract {}: {}",
call.contract_id, e
);
return Err(Error::ContractNotFound(call.contract_id.to_string()))
}
};
let mut runtime = let mut runtime =
Runtime::new(&bincode, self.blockchain.clone(), call.contract_id)?; match Runtime::new(&wasm, self.blockchain.clone(), call.contract_id) {
Ok(v) => v,
Err(e) => {
error!(
"Failed to instantiate WASM runtime for contract {}",
call.contract_id
);
return Err(e.into())
}
};
debug!("Executing \"apply\" call"); debug!("Executing \"apply\" call");
runtime.apply(&update)?; match runtime.apply(&update) {
// TODO: FIXME: This should be done in an atomic tx/batch
Ok(()) => debug!("State update applied successfully"),
Err(e) => {
error!("Failed to apply state update: {}", e);
return Err(e.into())
}
};
} }
} else { } else {
debug!("Skipping state updates because write=false"); debug!("Skipping apply of state updates because write=false");
} }
debug!("Transaction {} verified successfully", tx_hash);
} }
Ok(()) Ok(())

View File

@@ -50,6 +50,11 @@ impl ContractId {
} }
} }
/// Convert a `ContractId` object to its byte representation
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_repr()
}
/// `blake3(self || tree_name)` is used in datbases to have a /// `blake3(self || tree_name)` is used in datbases to have a
/// fixed-size name for a contract's state db. /// fixed-size name for a contract's state db.
pub fn hash_state_id(&self, tree_name: &str) -> [u8; 32] { pub fn hash_state_id(&self, tree_name: &str) -> [u8; 32] {

View File

@@ -24,6 +24,9 @@ use super::{
util::{get_object_bytes, get_object_size}, util::{get_object_bytes, get_object_size},
}; };
// This might not be the right place for this constant...
pub const ZKAS_DB_NAME: &str = "_zkas";
pub type DbHandle = u32; pub type DbHandle = u32;
pub const DB_SUCCESS: i32 = 0; pub const DB_SUCCESS: i32 = 0;

View File

@@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::collections::HashMap;
use async_std::sync::{Arc, RwLock};
use darkfi_sdk::{ use darkfi_sdk::{
crypto::{ crypto::{
schnorr::{SchnorrPublic, SchnorrSecret, Signature}, schnorr::{SchnorrPublic, SchnorrSecret, Signature},
@@ -54,9 +57,9 @@ pub struct Transaction {
impl Transaction { impl Transaction {
/// Verify ZK proofs for the entire transaction. /// Verify ZK proofs for the entire transaction.
pub fn verify_zkps( pub async fn verify_zkps(
&self, &self,
verifying_keys: &[(String, VerifyingKey)], verifying_keys: Arc<RwLock<HashMap<[u8; 32], Vec<(String, VerifyingKey)>>>>,
zkp_table: Vec<Vec<(String, Vec<pallas::Base>)>>, zkp_table: Vec<Vec<(String, Vec<pallas::Base>)>>,
) -> Result<()> { ) -> Result<()> {
// TODO: Are we sure we should assert here? // TODO: Are we sure we should assert here?
@@ -68,20 +71,22 @@ impl Transaction {
for (i, (proof, (zk_ns, public_vals))) in proofs.iter().zip(pubvals.iter()).enumerate() for (i, (proof, (zk_ns, public_vals))) in proofs.iter().zip(pubvals.iter()).enumerate()
{ {
if let Some(vk) = verifying_keys.iter().find(|x| &x.0 == zk_ns) { if let Some(vks) = verifying_keys.read().await.get(&call.contract_id.to_bytes()) {
// We have a verifying key for this if let Some(vk) = vks.iter().find(|x| &x.0 == zk_ns) {
debug!("public inputs: {:#?}", public_vals); // We have a verifying key for this
if let Err(e) = proof.verify(&vk.1, public_vals) { debug!("public inputs: {:#?}", public_vals);
error!("Failed verifying zk proof: {}", e); if let Err(e) = proof.verify(&vk.1, public_vals) {
return Err(VerifyFailed::ProofVerifyFailed(e.to_string()).into()) error!("Failed verifying ZK proof: {:#?}", e);
return Err(VerifyFailed::ProofVerifyFailed(e.to_string()).into())
}
debug!("Successfully verified {}:{} ZK proof", call.contract_id, zk_ns);
continue
} }
} else {
return Err(VerifyFailed::ProofVerifyFailed(format!(
"Verifying key for {} circuit does not exist",
zk_ns
))
.into())
} }
let e = format!("{}:{} circuit VK nonexistent", call.contract_id, zk_ns);
error!("{}", e);
return Err(VerifyFailed::ProofVerifyFailed(e).into())
} }
} }