1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
//! This file represents a simple Proof of Existence application, identical in behavior
//! to the tutorial https://docs.substrate.io/tutorials/work-with-pallets/use-macros-in-a-custom-pallet/
//! Of course, this implementation is based on UTXOs and works with Tuxedo rather than FRAME.
//!
//! The application allows users to claim the existence of a preimage for a particular hash with a
//! transaction. Thus, the blockchain network acts as a decentralized notary service. Claims are
//! stored in the state, and can be "revoked" from the state later, although the redeemer to the original
//! claim will always remain in the history of the blockchain.
//!
//! The main design deviation from the FRAME PoE pallet is the means by which redundant claims are settled.
//! In FRAME, the exact storage location of each claim is known globally, whereas in the UTXO model, all state
//! is local. This means that when a new claim is registered, it is not possible to efficiently check that the
//! same claim has not already been registered. Instead there is a constraint checker
//! to boot subsequent redundant claims when they are discovered. This difference is analogous to
//! the difference between recorded and registered land
//! https://cannerlaw.com/blog/the-difference-of-recorded-and-registered-land/
#![cfg_attr(not(feature = "std"), no_std)]
use core::marker::PhantomData;
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_core::H256;
use sp_runtime::transaction_validity::TransactionPriority;
use sp_std::fmt::Debug;
use tuxedo_core::{
dynamic_typing::{DynamicallyTypedData, UtxoData},
ensure,
support_macros::{CloneNoBound, DebugNoBound, DefaultNoBound},
SimpleConstraintChecker,
};
#[cfg(test)]
mod tests;
// Notice this type doesn't have to be public. Cool.
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
struct ClaimData {
/// The hash of the data whose existence is being proven.
claim: H256,
/// The time (in block height) at which the claim becomes valid.
effective_height: u32,
}
impl UtxoData for ClaimData {
const TYPE_ID: [u8; 4] = *b"poe_";
}
/// Errors that can occur when checking PoE Transactions
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum ConstraintCheckerError {
// Ughhh again with these common errors.
/// Wrong number of inputs were provided to the constraint checker.
WrongNumberInputs,
/// Wrong number of outputs were provided to the constraint checker.
WrongNumberOutputs,
/// An input data has the wrong type.
BadlyTypedInput,
/// An output data has the wrong type.
BadlyTypedOutput,
// Now we get on to the actual amoeba-specific errors
/// The effective height of this claim is in the past,
/// So the claim cannot be created.
EffectiveHeightInPast,
/// Claims under dispute do not have the same hash, but they must.
DisputingMismatchedClaims,
/// The winner of a dispute must be the oldest claim (the lowest block number)
IncorrectDisputeWinner,
}
/// Configuration items for the Proof of Existence piece when it is
/// instantiated in a concrete runtime.
pub trait PoeConfig {
/// A means of getting the current block height.
/// Probably this will be the Tuxedo Executive
fn block_height() -> u32;
}
/// A constraint checker to create claims.
///
/// This constraint checker allows the creation of many claims in a single operation
/// It also allows the creation of zero claims, although such a transaction is useless and is simply a
/// waste of caller fees.
#[derive(
Serialize,
Deserialize,
Encode,
Decode,
DebugNoBound,
DefaultNoBound,
CloneNoBound,
PartialEq,
Eq,
TypeInfo,
)]
pub struct PoeClaim<T>(PhantomData<T>);
impl<T: PoeConfig> SimpleConstraintChecker for PoeClaim<T> {
type Error = ConstraintCheckerError;
fn check(
&self,
input_data: &[DynamicallyTypedData],
evicted_input_data: &[DynamicallyTypedData],
_peeks: &[DynamicallyTypedData],
output_data: &[DynamicallyTypedData],
) -> Result<TransactionPriority, Self::Error> {
// Make sure there are no inputs or evictions
ensure!(
input_data.is_empty(),
ConstraintCheckerError::WrongNumberInputs
);
ensure!(
evicted_input_data.is_empty(),
ConstraintCheckerError::WrongNumberInputs
);
// For each output, make sure the claimed block height is >= the current block height.
// If we required exact equality, this would mean that transactors needed to get their transactions
// in exactly the next block which is challenging in times of network congestion. Relaxing the
// requirement allows the caller to make a somewhat weaker claim with the advantage that they have a longer
// period of time during which their transaction is valid.
for untyped_output in output_data {
let output = untyped_output
.extract::<ClaimData>()
.map_err(|_| ConstraintCheckerError::BadlyTypedOutput)?;
ensure!(
output.effective_height >= T::block_height(),
ConstraintCheckerError::EffectiveHeightInPast
);
}
Ok(0)
}
}
/// A constraint checker to revoke claims.
///
/// Like the creation constraint checker, this allows batch revocation.
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
pub struct PoeRevoke;
impl SimpleConstraintChecker for PoeRevoke {
type Error = ConstraintCheckerError;
fn check(
&self,
input_data: &[DynamicallyTypedData],
evicted_input_data: &[DynamicallyTypedData],
_peeks: &[DynamicallyTypedData],
output_data: &[DynamicallyTypedData],
) -> Result<TransactionPriority, Self::Error> {
// Can't evict anything
ensure!(
evicted_input_data.is_empty(),
ConstraintCheckerError::WrongNumberInputs
);
// Make sure there are no outputs
ensure!(
output_data.is_empty(),
ConstraintCheckerError::WrongNumberOutputs
);
// Make sure the inputs are properly typed. We don't need to check anything else about them.
for untyped_input in input_data {
let _ = untyped_input
.extract::<ClaimData>()
.map_err(|_| ConstraintCheckerError::BadlyTypedInput)?;
}
Ok(0)
}
}
/// A constraint checker that resolves claim disputes by keeping whichever claim came first.
///
/// Any user may submit a transaction reporting conflicting claims, and the oldest one will be kept.
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
pub struct PoeDispute;
impl SimpleConstraintChecker for PoeDispute {
type Error = ConstraintCheckerError;
fn check(
&self,
input_data: &[DynamicallyTypedData],
evicted_input_data: &[DynamicallyTypedData],
peek_data: &[DynamicallyTypedData],
output_data: &[DynamicallyTypedData],
) -> Result<TransactionPriority, Self::Error> {
// Make sure there are no normal inputs or outputs
ensure!(
input_data.is_empty(),
ConstraintCheckerError::WrongNumberInputs
);
ensure!(
output_data.is_empty(),
ConstraintCheckerError::WrongNumberOutputs
);
// Make sure there is exactly one peek (the oldest, winning claim)
let winner = peek_data
.first()
.ok_or(ConstraintCheckerError::WrongNumberInputs)?
.extract::<ClaimData>()
.map_err(|_| ConstraintCheckerError::BadlyTypedInput)?;
// Make sure that all evicted inputs, claim the same hash as the winner
// and have block heights strictly greater than the winner.
for untyped_loser in evicted_input_data {
let loser = untyped_loser
.extract::<ClaimData>()
.map_err(|_| ConstraintCheckerError::BadlyTypedInput)?;
ensure!(
winner.claim == loser.claim,
Self::Error::DisputingMismatchedClaims
);
ensure!(
winner.effective_height < loser.effective_height,
Self::Error::IncorrectDisputeWinner
);
}
Ok(0)
}
}