#![allow(clippy::field_reassign_with_default)]
use std::{
array::TryFromSliceError,
collections::{BTreeSet, HashMap},
error::Error as StdError,
fmt::{self, Debug, Display, Formatter},
iter::FromIterator,
};
use datasize::DataSize;
use hex::FromHexError;
use itertools::Itertools;
use num_traits::Zero;
use once_cell::sync::Lazy;
#[cfg(test)]
use rand::{Rng, RngCore};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::warn;
use casper_execution_engine::{
core::engine_state::{executable_deploy_item::ExecutableDeployItem, DeployItem},
shared::motes::Motes,
};
use casper_types::{
bytesrepr::{self, FromBytes, ToBytes},
standard_payment::ARG_AMOUNT,
AsymmetricType, ExecutionResult, PublicKey, SecretKey, Signature, U512,
};
use super::{BlockHash, Item, Tag, TimeDiff, Timestamp};
#[cfg(test)]
use crate::testing::TestRng;
use crate::{
components::{block_proposer::DeployType, chainspec_loader::DeployConfig},
crypto,
crypto::{
hash::{self, Digest},
AsymmetricKeyExt, Error as CryptoError,
},
rpcs::docs::DocExample,
utils::DisplayIter,
NodeRng,
};
static DEPLOY: Lazy<Deploy> = Lazy::new(|| {
let payment = ExecutableDeployItem::StoredContractByName {
name: String::from("casper-example"),
entry_point: String::from("example-entry-point"),
args: vec![1, 1].into(),
};
let session = ExecutableDeployItem::Transfer {
args: vec![2, 2].into(),
};
let serialized_body = serialize_body(&payment, &session);
let body_hash = hash::hash(&serialized_body);
let secret_key = SecretKey::doc_example();
let header = DeployHeader {
account: PublicKey::from(secret_key),
timestamp: *Timestamp::doc_example(),
ttl: TimeDiff::from(3_600_000),
gas_price: 1,
body_hash,
dependencies: vec![DeployHash::new(Digest::from([1u8; Digest::LENGTH]))],
chain_name: String::from("casper-example"),
};
let serialized_header = serialize_header(&header);
let hash = DeployHash::new(hash::hash(&serialized_header));
let signature = Signature::from_hex(
"012dbf03817a51794a8e19e0724884075e6d1fbec326b766ecfa6658b41f81290da85e23b24e88b1c8d976\
1185c961daee1adab0649912a6477bcd2e69bd91bd08"
.as_bytes(),
)
.unwrap();
let approval = Approval {
signer: PublicKey::from(secret_key),
signature,
};
Deploy {
hash,
header,
payment,
session,
approvals: vec![approval],
is_valid: None,
}
});
#[derive(Debug, Error)]
pub enum Error {
#[error("encoding to JSON: {0}")]
EncodeToJson(#[from] serde_json::Error),
#[error("decoding from JSON: {0}")]
DecodeFromJson(Box<dyn StdError>),
#[error("approval at index {0} does not exist")]
NoSuchApproval(usize),
#[error("failed to verify approval {index}: {error}")]
FailedVerification {
index: usize,
error: CryptoError,
},
#[error("invalid payment: missing \"amount\" arg")]
InvalidPayment,
}
impl From<FromHexError> for Error {
fn from(error: FromHexError) -> Self {
Error::DecodeFromJson(Box::new(error))
}
}
impl From<TryFromSliceError> for Error {
fn from(error: TryFromSliceError) -> Self {
Error::DecodeFromJson(Box::new(error))
}
}
#[derive(
Copy,
Clone,
DataSize,
Ord,
PartialOrd,
Eq,
PartialEq,
Hash,
Serialize,
Deserialize,
Debug,
Default,
JsonSchema,
)]
#[serde(deny_unknown_fields)]
#[schemars(with = "String", description = "Hex-encoded deploy hash.")]
pub struct DeployHash(#[schemars(skip)] Digest);
impl DeployHash {
pub fn new(hash: Digest) -> Self {
DeployHash(hash)
}
pub fn inner(&self) -> &Digest {
&self.0
}
#[cfg(test)]
pub fn random(rng: &mut TestRng) -> Self {
let hash = Digest::random(rng);
DeployHash(hash)
}
}
impl Display for DeployHash {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "deploy-hash({})", self.0,)
}
}
impl From<Digest> for DeployHash {
fn from(digest: Digest) -> Self {
Self(digest)
}
}
impl AsRef<[u8]> for DeployHash {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl ToBytes for DeployHash {
fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
self.0.to_bytes()
}
fn serialized_length(&self) -> usize {
self.0.serialized_length()
}
}
impl FromBytes for DeployHash {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
Digest::from_bytes(bytes).map(|(inner, remainder)| (DeployHash(inner), remainder))
}
}
#[derive(
Clone, DataSize, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug, JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct DeployHeader {
account: PublicKey,
timestamp: Timestamp,
ttl: TimeDiff,
gas_price: u64,
body_hash: Digest,
dependencies: Vec<DeployHash>,
chain_name: String,
}
impl DeployHeader {
pub fn account(&self) -> &PublicKey {
&self.account
}
pub fn timestamp(&self) -> Timestamp {
self.timestamp
}
pub fn ttl(&self) -> TimeDiff {
self.ttl
}
pub fn expired(&self, current_instant: Timestamp) -> bool {
let lifespan = self.timestamp + self.ttl;
lifespan < current_instant
}
pub fn gas_price(&self) -> u64 {
self.gas_price
}
pub fn body_hash(&self) -> &Digest {
&self.body_hash
}
pub fn dependencies(&self) -> &Vec<DeployHash> {
&self.dependencies
}
pub fn chain_name(&self) -> &str {
&self.chain_name
}
pub fn is_valid(&self, deploy_config: &DeployConfig, current_timestamp: Timestamp) -> bool {
let ttl_valid = self.ttl() <= deploy_config.max_ttl;
let timestamp_valid = self.timestamp() <= current_timestamp;
let not_expired = !self.expired(current_timestamp);
let num_deps_valid = self.dependencies().len() <= deploy_config.max_dependencies as usize;
ttl_valid && timestamp_valid && not_expired && num_deps_valid
}
}
impl DeployHeader {
pub fn expires(&self) -> Timestamp {
self.timestamp + self.ttl
}
}
impl ToBytes for DeployHeader {
fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
let mut buffer = bytesrepr::allocate_buffer(self)?;
buffer.extend(self.account.to_bytes()?);
buffer.extend(self.timestamp.to_bytes()?);
buffer.extend(self.ttl.to_bytes()?);
buffer.extend(self.gas_price.to_bytes()?);
buffer.extend(self.body_hash.to_bytes()?);
buffer.extend(self.dependencies.to_bytes()?);
buffer.extend(self.chain_name.to_bytes()?);
Ok(buffer)
}
fn serialized_length(&self) -> usize {
self.account.serialized_length()
+ self.timestamp.serialized_length()
+ self.ttl.serialized_length()
+ self.gas_price.serialized_length()
+ self.body_hash.serialized_length()
+ self.dependencies.serialized_length()
+ self.chain_name.serialized_length()
}
}
impl FromBytes for DeployHeader {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
let (account, remainder) = PublicKey::from_bytes(bytes)?;
let (timestamp, remainder) = Timestamp::from_bytes(remainder)?;
let (ttl, remainder) = TimeDiff::from_bytes(remainder)?;
let (gas_price, remainder) = u64::from_bytes(remainder)?;
let (body_hash, remainder) = Digest::from_bytes(remainder)?;
let (dependencies, remainder) = Vec::<DeployHash>::from_bytes(remainder)?;
let (chain_name, remainder) = String::from_bytes(remainder)?;
let deploy_header = DeployHeader {
account,
timestamp,
ttl,
gas_price,
body_hash,
dependencies,
chain_name,
};
Ok((deploy_header, remainder))
}
}
impl Display for DeployHeader {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(
formatter,
"deploy-header[account: {}, timestamp: {}, ttl: {}, gas_price: {}, body_hash: {}, dependencies: [{}], chain_name: {}]",
self.account,
self.timestamp,
self.ttl,
self.gas_price,
self.body_hash,
DisplayIter::new(self.dependencies.iter()),
self.chain_name,
)
}
}
#[derive(
Clone, DataSize, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug, JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Approval {
signer: PublicKey,
signature: Signature,
}
impl Approval {
pub fn signer(&self) -> &PublicKey {
&self.signer
}
pub fn signature(&self) -> &Signature {
&self.signature
}
}
impl Display for Approval {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "approval({})", self.signer)
}
}
impl ToBytes for Approval {
fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
let mut buffer = bytesrepr::allocate_buffer(self)?;
buffer.extend(self.signer.to_bytes()?);
buffer.extend(self.signature.to_bytes()?);
Ok(buffer)
}
fn serialized_length(&self) -> usize {
self.signer.serialized_length() + self.signature.serialized_length()
}
}
impl FromBytes for Approval {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
let (signer, remainder) = PublicKey::from_bytes(bytes)?;
let (signature, remainder) = Signature::from_bytes(remainder)?;
let approval = Approval { signer, signature };
Ok((approval, remainder))
}
}
#[derive(
Clone, DataSize, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug, JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Deploy {
hash: DeployHash,
header: DeployHeader,
payment: ExecutableDeployItem,
session: ExecutableDeployItem,
approvals: Vec<Approval>,
#[serde(skip)]
is_valid: Option<bool>,
}
impl Deploy {
#[allow(clippy::too_many_arguments)]
pub fn new(
timestamp: Timestamp,
ttl: TimeDiff,
gas_price: u64,
dependencies: Vec<DeployHash>,
chain_name: String,
payment: ExecutableDeployItem,
session: ExecutableDeployItem,
secret_key: &SecretKey,
rng: &mut NodeRng,
) -> Deploy {
let serialized_body = serialize_body(&payment, &session);
let body_hash = hash::hash(&serialized_body);
let account = PublicKey::from(secret_key);
let dependencies = dependencies.into_iter().unique().collect();
let header = DeployHeader {
account,
timestamp,
ttl,
gas_price,
body_hash,
dependencies,
chain_name,
};
let serialized_header = serialize_header(&header);
let hash = DeployHash::new(hash::hash(&serialized_header));
let mut deploy = Deploy {
hash,
header,
payment,
session,
approvals: vec![],
is_valid: None,
};
deploy.sign(secret_key, rng);
deploy
}
pub fn sign(&mut self, secret_key: &SecretKey, rng: &mut NodeRng) {
let signer = PublicKey::from(secret_key);
let signature = crypto::sign(&self.hash, secret_key, &signer, rng);
let approval = Approval { signer, signature };
self.approvals.push(approval);
}
pub fn id(&self) -> &DeployHash {
&self.hash
}
pub fn header(&self) -> &DeployHeader {
&self.header
}
pub fn take_header(self) -> DeployHeader {
self.header
}
pub fn payment(&self) -> &ExecutableDeployItem {
&self.payment
}
pub fn session(&self) -> &ExecutableDeployItem {
&self.session
}
pub fn approvals(&self) -> &[Approval] {
&self.approvals
}
pub fn deploy_type(&self) -> Result<DeployType, Error> {
let header = self.header().clone();
let size = self.serialized_length();
if self.session().is_transfer() {
let payment_amount = Motes::zero();
Ok(DeployType::Transfer {
header,
payment_amount,
size,
})
} else {
let payment_item = self.payment().clone();
let payment_amount = {
let args = payment_item
.into_runtime_args()
.map_err(|_| Error::InvalidPayment)?;
let value = args.get(ARG_AMOUNT).ok_or(Error::InvalidPayment)?;
let value = value
.clone()
.into_t::<U512>()
.map_err(|_| Error::InvalidPayment)?;
Motes::new(value)
};
Ok(DeployType::Other {
header,
payment_amount,
size,
})
}
}
pub fn is_valid(&mut self) -> bool {
match self.is_valid {
None => {
let validity = validate_deploy(self);
self.is_valid = Some(validity);
validity
}
Some(validity) => validity,
}
}
pub fn is_acceptable(&mut self, chain_name: String, config: DeployConfig) -> bool {
let header = self.header();
if header.chain_name() != chain_name {
warn!(
deploy_hash = %self.id(),
deploy_header = %header,
chain_name = %header.chain_name(),
"invalid chain identifier"
);
return false;
}
if header.dependencies().len() > config.max_dependencies as usize {
warn!(
deploy_hash = %self.id(),
deploy_header = %header,
max_dependencies = %config.max_dependencies,
"deploy dependency ceiling exceeded"
);
return false;
}
if header.ttl() > config.max_ttl {
warn!(
deploy_hash = %self.id(),
deploy_header = %header,
max_ttl = %config.max_ttl,
"deploy ttl excessive"
);
return false;
}
self.is_valid()
}
#[cfg(test)]
pub fn random(rng: &mut TestRng) -> Self {
let timestamp = Timestamp::random(rng);
let ttl = TimeDiff::from(rng.gen_range(60_000, 3_600_000));
let gas_price = rng.gen_range(1, 100);
let dependencies = vec![
DeployHash::new(hash::hash(rng.next_u64().to_le_bytes())),
DeployHash::new(hash::hash(rng.next_u64().to_le_bytes())),
DeployHash::new(hash::hash(rng.next_u64().to_le_bytes())),
];
let chain_name = String::from("casper-example");
let payment = rng.gen();
let session = rng.gen();
let secret_key = SecretKey::random(rng);
Deploy::new(
timestamp,
ttl,
gas_price,
dependencies,
chain_name,
payment,
session,
&secret_key,
rng,
)
}
}
impl DocExample for Deploy {
fn doc_example() -> &'static Self {
&*DEPLOY
}
}
fn serialize_header(header: &DeployHeader) -> Vec<u8> {
header
.to_bytes()
.unwrap_or_else(|error| panic!("should serialize deploy header: {}", error))
}
fn serialize_body(payment: &ExecutableDeployItem, session: &ExecutableDeployItem) -> Vec<u8> {
let mut buffer = payment
.to_bytes()
.unwrap_or_else(|error| panic!("should serialize payment code: {}", error));
buffer.extend(
session
.to_bytes()
.unwrap_or_else(|error| panic!("should serialize session code: {}", error)),
);
buffer
}
fn validate_deploy(deploy: &Deploy) -> bool {
let serialized_body = serialize_body(&deploy.payment, &deploy.session);
let body_hash = hash::hash(&serialized_body);
if body_hash != deploy.header.body_hash {
warn!(?deploy, ?body_hash, "invalid deploy body hash");
return false;
}
let serialized_header = serialize_header(&deploy.header);
let hash = DeployHash::new(hash::hash(&serialized_header));
if hash != deploy.hash {
warn!(?deploy, ?hash, "invalid deploy hash");
return false;
}
for (index, approval) in deploy.approvals.iter().enumerate() {
if let Err(error) = crypto::verify(&deploy.hash, &approval.signature, &approval.signer) {
warn!(?deploy, "failed to verify approval {}: {}", index, error);
return false;
}
}
true
}
impl Item for Deploy {
type Id = DeployHash;
const TAG: Tag = Tag::Deploy;
const ID_IS_COMPLETE_ITEM: bool = false;
fn id(&self) -> Self::Id {
*self.id()
}
}
impl Display for Deploy {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(
formatter,
"deploy[{}, {}, payment_code: {}, session_code: {}, approvals: {}]",
self.hash,
self.header,
self.payment,
self.session,
DisplayIter::new(self.approvals.iter())
)
}
}
impl From<Deploy> for DeployItem {
fn from(deploy: Deploy) -> Self {
let account_hash = deploy.header().account().to_account_hash();
DeployItem::new(
account_hash,
deploy.session().clone(),
deploy.payment().clone(),
deploy.header().gas_price(),
BTreeSet::from_iter(vec![account_hash]),
casper_types::DeployHash::new(deploy.id().inner().to_array()),
)
}
}
#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
pub struct DeployMetadata {
pub execution_results: HashMap<BlockHash, ExecutionResult>,
}
impl ToBytes for Deploy {
fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
let mut buffer = bytesrepr::allocate_buffer(self)?;
buffer.extend(self.header.to_bytes()?);
buffer.extend(self.hash.to_bytes()?);
buffer.extend(self.payment.to_bytes()?);
buffer.extend(self.session.to_bytes()?);
buffer.extend(self.approvals.to_bytes()?);
Ok(buffer)
}
fn serialized_length(&self) -> usize {
self.header.serialized_length()
+ self.hash.serialized_length()
+ self.payment.serialized_length()
+ self.session.serialized_length()
+ self.approvals.serialized_length()
}
}
impl FromBytes for Deploy {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
let (header, remainder) = DeployHeader::from_bytes(bytes)?;
let (hash, remainder) = DeployHash::from_bytes(remainder)?;
let (payment, remainder) = ExecutableDeployItem::from_bytes(remainder)?;
let (session, remainder) = ExecutableDeployItem::from_bytes(remainder)?;
let (approvals, remainder) = Vec::<Approval>::from_bytes(remainder)?;
let maybe_valid_deploy = Deploy {
header,
hash,
payment,
session,
approvals,
is_valid: None,
};
Ok((maybe_valid_deploy, remainder))
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use casper_types::bytesrepr::Bytes;
use super::*;
use crate::crypto::AsymmetricKeyExt;
#[test]
fn json_roundtrip() {
let mut rng = crate::new_rng();
let deploy = Deploy::random(&mut rng);
let json_string = serde_json::to_string_pretty(&deploy).unwrap();
let decoded = serde_json::from_str(&json_string).unwrap();
assert_eq!(deploy, decoded);
}
#[test]
fn bincode_roundtrip() {
let mut rng = crate::new_rng();
let deploy = Deploy::random(&mut rng);
let serialized = bincode::serialize(&deploy).unwrap();
let deserialized = bincode::deserialize(&serialized).unwrap();
assert_eq!(deploy, deserialized);
}
#[test]
fn bytesrepr_roundtrip() {
let mut rng = crate::new_rng();
let hash = DeployHash(Digest::random(&mut rng));
bytesrepr::test_serialization_roundtrip(&hash);
let deploy = Deploy::random(&mut rng);
bytesrepr::test_serialization_roundtrip(deploy.header());
bytesrepr::test_serialization_roundtrip(&deploy);
}
#[test]
fn is_valid() {
let mut rng = crate::new_rng();
let mut deploy = Deploy::random(&mut rng);
assert_eq!(deploy.is_valid, None, "is valid should initially be None");
assert!(deploy.is_valid());
assert_eq!(deploy.is_valid, Some(true), "is valid should be true");
}
#[test]
fn is_not_valid() {
let mut deploy = Deploy::new(
Timestamp::zero(),
TimeDiff::from(Duration::default()),
0,
vec![],
String::default(),
ExecutableDeployItem::ModuleBytes {
module_bytes: Bytes::new(),
args: Bytes::new(),
},
ExecutableDeployItem::Transfer { args: Bytes::new() },
&SecretKey::generate_ed25519().unwrap(),
&mut crate::new_rng(),
);
deploy.header.gas_price = 1;
assert_eq!(deploy.is_valid, None, "is valid should initially be None");
assert!(!deploy.is_valid(), "should not be valid");
assert_eq!(deploy.is_valid, Some(false), "is valid should be false");
}
}