use std::{
array::TryFromSliceError,
collections::BTreeSet,
error::Error as StdError,
fmt::{self, Debug, Display, Formatter},
iter::FromIterator,
};
use datasize::DataSize;
use hex::FromHexError;
use itertools::Itertools;
#[cfg(test)]
use rand::{Rng, RngCore};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::warn;
use casper_execution_engine::core::engine_state::{
executable_deploy_item::ExecutableDeployItem, DeployItem,
};
use casper_types::bytesrepr::{self, FromBytes, ToBytes};
use super::{CryptoRngCore, Item, Tag, TimeDiff, Timestamp};
#[cfg(test)]
use crate::testing::TestRng;
use crate::{
components::storage::Value,
crypto::{
asymmetric_key::{self, PublicKey, SecretKey, Signature},
hash::{self, Digest},
Error as CryptoError,
},
utils::DisplayIter,
};
#[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,
},
}
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,
)]
pub struct DeployHash(Digest);
impl DeployHash {
pub fn new(hash: Digest) -> Self {
DeployHash(hash)
}
pub fn inner(&self) -> &Digest {
&self.0
}
}
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)]
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
}
}
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)]
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)
}
}
#[derive(Clone, DataSize, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug)]
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 dyn CryptoRngCore,
) -> 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 dyn CryptoRngCore) {
let signer = PublicKey::from(secret_key);
let signature = asymmetric_key::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 is_valid(&mut self) -> bool {
match self.is_valid {
None => {
let validity = validate_deploy(self);
self.is_valid = Some(validity);
validity
}
Some(validity) => validity,
}
}
#[cfg(test)]
pub fn random(rng: &mut TestRng) -> Self {
let timestamp = Timestamp::now();
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,
)
}
}
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) =
asymmetric_key::verify(&deploy.hash, &approval.signature, &approval.signer)
{
warn!(?deploy, "failed to verify approval {}: {}", index, error);
return false;
}
}
true
}
impl Value for Deploy {
type Id = DeployHash;
type Header = DeployHeader;
fn id(&self) -> &Self::Id {
self.id()
}
fn header(&self) -> &Self::Header {
self.header()
}
fn take_header(self) -> Self::Header {
self.take_header()
}
}
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]),
deploy.id().inner().to_array(),
)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::testing::TestRng;
#[test]
fn json_roundtrip() {
let mut rng = TestRng::new();
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 = TestRng::new();
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 = TestRng::new();
let hash = DeployHash(Digest::random(&mut rng));
bytesrepr::test_serialization_roundtrip(&hash);
let deploy = Deploy::random(&mut rng);
bytesrepr::test_serialization_roundtrip(deploy.header());
}
#[test]
fn is_valid() {
let mut rng = TestRng::new();
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: vec![],
args: vec![],
},
ExecutableDeployItem::Transfer { args: vec![] },
&SecretKey::generate_ed25519(),
&mut TestRng::new(),
);
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");
}
}