Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TradableKitty piece #171

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tuxedo-template-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ money = { default-features = false, path = "../wardrobe/money" }
poe = { default-features = false, path = "../wardrobe/poe" }
runtime-upgrade = { default-features = false, path = "../wardrobe/runtime_upgrade" }
timestamp = { default-features = false, path = "../wardrobe/timestamp" }
tradable-kitties = { default-features = false, path = "../wardrobe/tradable_kitties" }
tuxedo-core = { default-features = false, path = "../tuxedo-core" }

# Parachain related ones
Expand Down
5 changes: 5 additions & 0 deletions tuxedo-template-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub use money;
pub use poe;
pub use runtime_upgrade;
pub use timestamp;
pub use tradable_kitties;

/// Opaque types. These are used by the CLI to instantiate machinery that don't need to know
/// the specifics of the runtime. They can then be made to be agnostic over specific formats
Expand Down Expand Up @@ -170,6 +171,8 @@ pub enum OuterConstraintChecker {
Money(money::MoneyConstraintChecker<0>),
/// Checks Free Kitty transactions
FreeKittyConstraintChecker(kitties::FreeKittyConstraintChecker),
/// Checks tradable Kitty transactions
TradableKittyConstraintChecker(tradable_kitties::TradableKittyConstraintChecker<0>),
muraca marked this conversation as resolved.
Show resolved Hide resolved
/// Checks that an amoeba can split into two new amoebas
AmoebaMitosis(amoeba::AmoebaMitosis),
/// Checks that a single amoeba is simply removed from the state
Expand Down Expand Up @@ -205,6 +208,8 @@ pub enum OuterConstraintChecker {
Money(money::MoneyConstraintChecker<0>),
/// Checks Free Kitty transactions
FreeKittyConstraintChecker(kitties::FreeKittyConstraintChecker),
/// Checks Paid Kitty transactions
TradableKittyConstraintChecker(tradable_kitties::TradableKittyConstraintChecker<0>),
/// Checks that an amoeba can split into two new amoebas
AmoebaMitosis(amoeba::AmoebaMitosis),
/// Checks that a single amoeba is simply removed from the state
Expand Down
195 changes: 174 additions & 21 deletions wardrobe/kitties/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
//! An NFT game inspired by cryptokitties.
//! This is a game which allows for kitties to be bred based on a few factors
//! 1.) Mom and Tired have to be in a state where they are ready to breed
//! This is a game which allows for kitties to be create,bred and update name of kitty.
//!
//! ## Features
//!
//! - **Create:** Generate a new kitty.
//! To submit a valid transaction for creating a kitty, adhere to the following structure:
muraca marked this conversation as resolved.
Show resolved Hide resolved
//! 1. Input must be empty.
//! 2. Output must contain only the newly created kittities as a child.
//!
//! **Note 1:** Multiple kitties can be created at the same time in the same txn..
//!
//! - **Update Name:** Modify the name of a kitty.
//! To submit a valid transaction for updating a kitty's name, adhere to the following structure:
//! 1. Input must be the kitty to be updated.
//! 2. Output must contain the kitty with the updated name.
//!
//! **Note 1:** All other properties such as DNA, parents, free breedings, etc., must remain unaltered in the output.
//! **Note 2:** The input and output kitties must follow same order.
//!
//! - **Breed:** Breeds a new kitty using mom and dad based on below factors
//! 1.) Mom and Dad have to be in a state where they are ready to breed
//! 2.) Each Mom and Dad have some DNA and the child will have unique DNA combined from the both of them
//! Linkable back to the Mom and Dad
//! 3.) The game also allows Kitties to have a cooling off period inbetween breeding before they can be bred again.
//! 4.) A rest operation allows for a Mom Kitty and a Dad Kitty to be cooled off
//!
//! In order to submit a valid transaction you must strutucture it as follows:
//! In order to submit a valid transaction you must structure it as follows:
muraca marked this conversation as resolved.
Show resolved Hide resolved
//! 1.) Input must contain 1 mom and 1 dad
//! 2.) Output must contain Mom, Dad, and newly created Child
//! 3.) A child's DNA is calculated by:
Expand All @@ -25,6 +44,7 @@ use sp_runtime::{
traits::{BlakeTwo256, Hash as HashT},
transaction_validity::TransactionPriority,
};
use sp_std::collections::btree_set::BTreeSet; // For checking the uniqueness of input and output based on dna.
use sp_std::prelude::*;
use tuxedo_core::{
dynamic_typing::{DynamicallyTypedData, UtxoData},
Expand All @@ -36,6 +56,10 @@ use tuxedo_core::{
#[cfg(test)]
mod tests;

/// The main constraint checker for the kitty piece. Allows below :
/// Create : Allows creation of kitty without parents, Multiple kitties can be created in same txn.
/// UpdateKittyName : Allows updating the name of the kitty s, Multiple kitty name can be updated in same txn.
/// Breed : Allows breeding of kitty.
#[derive(
Serialize,
Deserialize,
Expand All @@ -50,8 +74,16 @@ mod tests;
Debug,
TypeInfo,
)]
pub struct FreeKittyConstraintChecker;
pub enum FreeKittyConstraintChecker {
/// Txn that creates kitty without parents.Multiple kitties can be created at the same time.
Create,
/// Txn that updates kitty Name. Multiple kitty names can be updated. input & output must follow the same order.
UpdateKittyName,
/// Txn where kitties are consumed and new family(Parents(mom,dad) and child) is created.
Breed,
}

/// Dad kitty status with respect to breeding.
#[derive(
Serialize,
Deserialize,
Expand All @@ -69,10 +101,13 @@ pub struct FreeKittyConstraintChecker;
)]
pub enum DadKittyStatus {
#[default]
/// Can breed.
RearinToGo,
/// Can't breed due to tired.
muraca marked this conversation as resolved.
Show resolved Hide resolved
Tired,
}

/// Mad kitty status with respect to breeding.
#[derive(
Serialize,
Deserialize,
Expand All @@ -90,10 +125,13 @@ pub enum DadKittyStatus {
)]
pub enum MomKittyStatus {
#[default]
/// Can breed.
RearinToGo,
/// Can't breed due to recent child kitty delivery.
HadBirthRecently,
}

/// Parent stuct contains 1 mom kitty and 1 dad kitty.
#[derive(
Serialize,
Deserialize,
Expand Down Expand Up @@ -146,6 +184,12 @@ impl Default for Parent {
)]
pub struct KittyDNA(pub H256);

/// Kitty data contains basic informationsuch as below :
/// parent: 1 mom kitty and 1 dad kitty.
/// free_breedings: Free breeding allowed on a kitty.
/// dna :Its a unique per kitty.
/// num_breedings: number of free breedings are remaining.
/// name: Name of kitty.
#[derive(
Serialize,
Deserialize,
Expand All @@ -165,6 +209,7 @@ pub struct KittyData {
pub free_breedings: u64, // Ignore in breed for money case
pub dna: KittyDNA,
pub num_breedings: u128,
pub name: [u8; 4],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think the fields should follow a more logical order, such as:

  • DNA
  • Name
  • Parents
  • free_breedings
  • num_breedings

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is generally better to add new members at the end of a struct. This practice aligns with the principle of maintaining backward compatibility. When we add a new member at the end of the struct, existing code that uses the struct won't be affected, as the layout of the existing members remains unchanged.

If you add a new member in the middle of a struct, it can break existing code that relies on the order and size of the struct members. This is because the memory layout of the struct may change, leading to potential issues with code that assumes a specific order or size.

By appending new members at the end, we follow a practice commonly referred to as "struct versioning" or "extensible struct pattern," where you ensure that new fields are added without affecting the existing layout. This helps in maintaining compatibility and minimizes the risk of introducing errors in the existing codebase.

As of now, I don't see any code which is relying on the layout of the structure.
If it is a strong request, I will update it. Otherwise, I want to keep it as it is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is generally better to add new members at the end of a struct. This practice aligns with the principle of maintaining backward compatibility.

Although you are not wrong, at this stage of the development we don't need to care about this, and we should prioritize doing stuff that makes sense and is clear and understandable.

And sometimes, you do want to break compatibility.

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I updated the struct as you suggested.

}

impl KittyData {
Expand All @@ -187,7 +232,7 @@ impl KittyData {
v,
)
.into()],
checker: FreeKittyConstraintChecker.into(),
checker: FreeKittyConstraintChecker::Create.into(),
}
}
}
Expand All @@ -199,6 +244,7 @@ impl Default for KittyData {
free_breedings: 2,
dna: KittyDNA(H256::from_slice(b"mom_kitty_1asdfasdfasdfasdfasdfa")),
num_breedings: 3,
name: *b"kity",
NadigerAmit marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -207,6 +253,7 @@ impl UtxoData for KittyData {
const TYPE_ID: [u8; 4] = *b"Kitt";
}

/// Reasons that kitty opertaion may go wrong.
#[derive(
Serialize,
Deserialize,
Expand Down Expand Up @@ -261,9 +308,27 @@ pub enum ConstraintCheckerError {
TooManyBreedingsForKitty,
/// Not enough free breedings available for these parents.
NotEnoughFreeBreedings,
/// The transaction attempts to create no Kitty.
CreatingNothing,
/// Inputs(Parents) not required for mint.
CreatingWithInputs,
/// No input for kitty Update.
InvalidNumberOfInputOutput,
/// Duplicate kitty found i.e based on the DNA.
DuplicateKittyFound,
/// Dna mismatch between input and output.
DnaMismatchBetweenInputAndOutput,
/// Name is not updated
KittyNameUnAltered,
/// Kitty FreeBreeding cannot be updated.
FreeBreedingCannotBeUpdated,
/// Kitty NumOfBreeding cannot be updated.
NumOfBreedingCannotBeUpdated,
/// Gender cannot be updated
KittyGenderCannotBeUpdated,
}

trait Breed {
pub trait Breed {
/// The Cost to breed a kitty if it is not free.
const COST: u128;
/// Number of free breedings a kitty will have.
Expand Down Expand Up @@ -500,28 +565,116 @@ impl TryFrom<&DynamicallyTypedData> for KittyData {

impl SimpleConstraintChecker for FreeKittyConstraintChecker {
type Error = ConstraintCheckerError;
/// Checks:
/// - `input_data` is of length 2
/// - `output_data` is of length 3
///

fn check(
&self,
input_data: &[DynamicallyTypedData],
_peeks: &[DynamicallyTypedData],
output_data: &[DynamicallyTypedData],
) -> Result<TransactionPriority, Self::Error> {
// Input must be a Mom and a Dad
ensure!(input_data.len() == 2, Self::Error::TwoParentsDoNotExist);

let mom = KittyData::try_from(&input_data[0])?;
let dad = KittyData::try_from(&input_data[1])?;
KittyHelpers::can_breed(&mom, &dad)?;

// Output must be Mom, Dad, Child
ensure!(output_data.len() == 3, Self::Error::NotEnoughFamilyMembers);
match &self {
Self::Create => {
// Make sure there are no inputs being consumed
ensure!(
input_data.is_empty(),
ConstraintCheckerError::CreatingWithInputs
);

// Make sure there is at least one output being minted
ensure!(
!output_data.is_empty(),
ConstraintCheckerError::CreatingNothing
);

// Make sure the outputs are the right type
for utxo in output_data {
let _utxo_kitty = utxo
.extract::<KittyData>()
.map_err(|_| ConstraintCheckerError::BadlyTyped)?;
}
Ok(0)
}
Self::Breed => {
// Check that we are consuming at least one input
ensure!(input_data.len() == 2, Self::Error::TwoParentsDoNotExist);

let mom = KittyData::try_from(&input_data[0])?;
let dad = KittyData::try_from(&input_data[1])?;
KittyHelpers::can_breed(&mom, &dad)?;
// Output must be Mom, Dad, Child
ensure!(output_data.len() == 3, Self::Error::NotEnoughFamilyMembers);
KittyHelpers::check_new_family(&mom, &dad, output_data)?;
Ok(0)
}
Self::UpdateKittyName => {
can_kitty_name_be_updated(input_data, output_data)?;
Ok(0)
}
}
}
}

KittyHelpers::check_new_family(&mom, &dad, output_data)?;
/// Checks:
/// - Input and output is of kittyType
/// - Only name is updated and ther basic properties are not updated.
/// - Order between input and output must be same.
pub fn can_kitty_name_be_updated(
input_data: &[DynamicallyTypedData],
output_data: &[DynamicallyTypedData],
) -> Result<TransactionPriority, ConstraintCheckerError> {
ensure!(
input_data.len() == output_data.len() && !input_data.is_empty(),
{ ConstraintCheckerError::InvalidNumberOfInputOutput }
);
let mut dna_to_kitty_set: BTreeSet<KittyDNA> = BTreeSet::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like you're spending a lot of effort making sure that no two kitties have the same DNA here. But isn't that already guaranteed (assuming polynomial time advarsaries) by the hash function itself?

I recommend you remove this check entirely. Here you should just make sure that the output kitties are the same as the input kitties except for the names. If you aren't convinced, I challenge you to think of a case where two kitties would have the same DNA.

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoshOrndorff, Thank you.

I can remove those checks but I thought of below cases:
Good wallets don't abuse the blockchain apis, but rouge wallets can abuse the blockchain API as below :

Case1:

  1. I have 1 kitty in my wallet

  2. I will send the same kitty 2 times as below :
    let inputs: Vec<Input> = vec![input_kitty_ref.clone(),input_kitty_ref]; // I am just sending same kitty 2 times

  3. Then I will create the 2 outputs as below in the wallet :

           let mut updated_kitty: KittyData = input_kitty_info.clone();  // This is valid kitty 
            updated_kitty.name = *kitty_name;
            let output = Output {
                payload: updated_kitty.into(),
                verifier: OuterVerifier::Sr25519Signature(Sr25519Signature {
                    owner_pubkey: args.owner,
                }),
            };
            let mut updated_kitty_fake: KittyData = input_kitty_info; // This is fake kitty 

            updated_kitty_fake.dna = KittyDNA(H256::from_slice(b"mom_kitty_1asdfasdfasdfasdfasdfz")); // Here I am updating the kitty DNA to some random value.
            
            updated_kitty_fake.name  = *fake_kitty_name; // name as lily
            let output1 = Output {
                payload: updated_kitty_fake.into(),
                verifier: OuterVerifier::Sr25519Signature(Sr25519Signature {
                    owner_pubkey: args.owner,
                }),
            };

            // Create the Transaction
            let transaction = Transaction {
                inputs: inputs,
                peeks: Vec::new(),
                outputs: vec![output,output1],  // Including the both valid and fake kitty 
                checker: FreeKittyConstraintChecker::UpdateKittyName.into(),
            };
            transaction
        }
  1. If I don't have DNA comparison between input and output this I think it is valid transaction:
    Below is o/p when I tested.

Blockchain logs: I truncated the logs manually for easy reading :
INFO tokio-runtime-worker tuxedo_template_runtime: runtime-> validate_transaction TransactionSource = TransactionSource::InBlock tx = Transaction { inputs: [Input { output_ref: OutputRef { tx_hash: 0x928ff2975393c8466ef76121f98692a3500f1a4579372799eaffb27deaf2b387, index: 0 }, redeemer: [184, ---] }, verifier: Sr25519Signature(Sr25519Signature { owner_pubkey: 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67 }) }, Output { payload: DynamicallyTypedData { data: [0, --- 116, 116] }, verifier: Sr25519Signature(Sr25519Signature { owner_pubkey: 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67 }) }], checker: FreeKitty(UpdateKittyName) } block_hash = 0x37040850b24d9dad724f1657ea572b1536df1c7fdf52670090dd54b4a0b6c537

  1. Below is the o/p of the wallet

You can see I could create 2 kitties with 1 kitty by abusing update name API .

amit@DESKTOP-TF687VE:~/OmBlockchain/OmTest$ ./target/release/tuxedo-template-wallet show-all-kitties
Show All Kitty Summary
==========================================
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("amit") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3), num_breedings: 3, name: [97, 109, 105, 116] } ->
--------------------------------------------------
=-===================================================
// Updating the kitty, From cmd line, updating only 1 kitty, but I have hacking code in the source code 
=-===================================================
amit@DESKTOP-TF687VE:~/OmBlockchain/OmTest$ ./target/release/tuxedo-template-wallet update-kitty-name --current-name "amit" --new-name "vina"
[2024-03-02T06:03:39Z INFO  tuxedo_template_wallet] cli from cmd args = Cli { endpoint: "http://localhost:9944", path: None, no_sync: false, tmp: false, dev: false, command: Some(UpdateKittyName(UpdateKittyNameArgs { current_name: "amit", new_name: "vina", owner: 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67 })) }
Node's response to spawn transaction: Ok("0xa2b0a4a144115e15df280ecd21bcfa7f13523998f10fadec856d3b64b65aad87")
Created "bd3f0a8bb0596f336c82122cd373b2743717d28d82bdf3ee48a0b86d458c0f7100000000" basic Kitty 0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3. owned by 0xd2bf…df67
Created "bd3f0a8bb0596f336c82122cd373b2743717d28d82bdf3ee48a0b86d458c0f7101000000" basic Kitty 0x6d6f6d5f6b697474795f3161736466617364666173646661736466617364667a. owned by 0xd2bf…df67
amit@DESKTOP-TF687VE:~/OmBlockchain/OmTest$ ./target/release/tuxedo-template-wallet show-all-kitties
Show All Kitty Summary
==========================================
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("vina") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3), num_breedings: 3, name: [118, 105, 110, 97] } ->
--------------------------------------------------
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("lily") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0x6d6f6d5f6b697474795f3161736466617364666173646661736466617364667a), num_breedings: 3, name: [108, 105, 108, 121] } ->

So thought of preventing that with the Line no 647 to 649 i.e below :
if utxo_input_kitty.dna != utxo_output_kitty.dna { return Err(ConstraintCheckerError::DnaMismatchBetweenInputAndOutput); }

With the above check the transaction will fail as below: I removed some logs for easy reading .
TransactionSource = TransactionSource::External tx = Transaction { inputs: [Input { output_ref: OutputRef { tx_hash: 0x45ce7924543b5e60490c93e25ff8b379589290d90a46df1725bf4e57d9325fb2, index: 0 }, redeemer: [---0, 108, 105, 108, 121], type_id: [75, 105, 116, 116] }, verifier: Sr25519Signature(Sr25519Signature { owner_pubkey: 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67 }) }], checker: FreeKitty(UpdateKittyName) } block_hash = 0x86ed44adb9742d6cebea1538bab2645812a00e3b11249dc44379ec957fcd1629 2024-03-02 11:22:04.243 WARN tokio-runtime-worker tuxedo-core: Tuxedo Transaction did not validate (in the pool): ConstraintCheckerError(FreeKitty(DnaMismatchBetweenInputAndOutput))

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case 2:
If I send 2 output kittyes with same DNA i.e without below line in wallet:

updated_kitty_fake.dna = KittyDNA(H256::from_slice(b"mom_kitty_1asdfasdfasdfasdfasdfz")); // Here I am updating the kitty DNA to some random value.

I did the below steps 
1.  The code is the same as 1st case except for the DNA update of updated_kitty_fake i.e I didn't do a DNA update.
2. Update the kitty name from vina to Tina.
3. Then I have a new kitty lily which has the same DNA as the updated kitty tina . Then also transaction validation is passed without those checks;

Below are the wallet logs

amit@DESKTOP-TF687VE:~/OmBlockchain/OmTest$ ./target/release/tuxedo-template-wallet update-kitty-name --current-name "vina" --new-name "tina"
Node's response to spawn transaction: Ok("0x9289fc86d74f341a624c0cb5df161a583b513ac53efdcf8f271f844de200f454")
Created "12c23c992f25e080e4954b76ed0c6dfb02449e92e1c28eab686c42a8072fe72100000000" basic Kitty 0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3. owned by 0xd2bf…df67
Created "12c23c992f25e080e4954b76ed0c6dfb02449e92e1c28eab686c42a8072fe72101000000" basic Kitty 0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3. owned by 0xd2bf…df67
amit@DESKTOP-TF687VE:~/OmBlockchain/OmTest$ ./target/release/tuxedo-template-wallet show-all-kitties
Show All Kitty Summary
==========================================
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("tina") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3), num_breedings: 3, name: [116, 105, 110, 97] } ->
--------------------------------------------------
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("lily") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0xe23edbf55798fbda8f91204651446243fc675e63e10fe126fdff073755547dc3), num_breedings: 3, name: [108, 105, 108, 121] } ->
--------------------------------------------------
Owner -> 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67
Some("lily") => KittyData { parent: Mom(RearinToGo), free_breedings: 2, dna: KittyDNA(0x6d6f6d5f6b697474795f3161736466617364666173646661736466617364667a), num_breedings: 3, name: [108, 105, 108, 121] } 

Blockchain log:
2024-03-02 11:38:54.004 INFO tokio-runtime-worker tuxedo_template_runtime: runtime-> apply_extrinsic Transaction { inputs: [Input { output_ref: OutputRef { tx_hash: 0xbd3f0a8bb0596f336c82122cd373b2743717d28d82bdf3ee48a0b86d458c0f71, index: 0 }, redeemer: [56, 5, 230, 157, 51, 235, 0, 59, 119, 144, 251, 58, 29, 247, 215, 194, 194, 241, 15, 174, 193, 227, 100, 185, 51, 92, 123, 148, 120, 6, 81, 31, 243, 141, 56, 37, 230, 43, 233, 254, 225, 60, 25, 119, 206, 8, 84, 229, 145, 92, 46, 128, 123, 90, 234, 63, 48, 98, 198, 238, 7, 178, 77, 129] }, Input { output_ref: OutputRef { tx_hash: 0xbd3f0a8bb0596f336c82122cd373b2743717d28d82bdf3ee48a0b86d458c0f71, index: 0 }, redeemer: [172, 171, 253, 111, 104, 241, 143, 143, 59, 122, 103, 39, 113, 118, 69, 21, 56, 136, 207, 121, 143, 84, 229, 76, 146, 230, 46, 106, 240, 146, 252, 69, 149, 254, 28, 98, 163, 118, 28, 100, 185, 176, 226, 204, 235, 182, 67, 31, 83, 211, 10, 230, 200, 86, 142, 38, 176, 239, 255, 252, 170, 164, 128, 141] }], peeks: [], outputs: [Output { payload: DynamicallyTypedData { data: [0, -- 121], type_id: [75, 105, 116, 116] }, verifier: Sr25519Signature(Sr25519Signature { owner_pubkey: 0xd2bf4b844dfefd6772a8843e669f943408966a977e3ae2af1dd78e0f55f4df67 }) }], checker: FreeKitty(UpdateKittyName) } 2024-03-02 11:38:54.006 INFO tokio-runtime-worker tuxedo_template_runtime: runtime-> finalize_block

If I have the below line I can avoid this situation as inputs are checked.
if dna_to_kitty_set.contains(&utxo_input_kitty.dna) { return Err(ConstraintCheckerError::DuplicateKittyFound);

I checked the kitties via verifykitty which will get the kitties from the blockchain db, Then I also could fine 2 kitties with the same DNA.

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am doing something wrong in the above tests, Please suggest.

Copy link
Contributor Author

@NadigerAmit NadigerAmit Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoshOrndorff

Hi Joshy,

I have delved deeper into the issues and identified the root cause of why duplicate DNA can occur in kitties.
But basically I think the issue is common to any UTXOs.

Issue 1: Duplicate UTXO Checks
Presently, our UTXO validation primarily focuses on the existing UTXOs within the blockchain database i.e 'TransparentUtxoSet', detecting any duplicate output UTXOs from the transaction output sets.
If a duplicate UTXO is detected, we trigger the error 'PreExistingOutput.' However, it seems we do not perform checks for duplicity within the outputs of the transaction itself. I think it may be valid in the case of coin/money.

for index in 0..transaction.outputs.len() {
let output_ref = OutputRef {
tx_hash,
index: index as u32,
};
debug!(
target: LOG_TARGET,
"Checking for pre-existing output {:?}", output_ref
);
ensure!(
TransparentUtxoSet::<V>::peek_utxo(&output_ref).is_none(),
UtxoError::PreExistingOutput
);
}

Perhaps originally, the design intended to have a single output of the same type.
Due to this, I can create identical kitties by cloning in the 'create_kitty' transaction, and all the outputs are validated and stored in the database as valid UTXOs.

Possible Solutions:
Solution 1: Consider supporting only one output of the same type at a time or per transaction to address the issue of allowing multiple cloned outputs as valid transactions.
Solution 2: Implement a mechanism in each piece to verify uniqueness within the output UTXO, preventing the inclusion of duplicate outputs in a transaction.

Issue 2: DNA Calculation and Uniqueness

This issue predominantly concerns kitties, and it remains unclear if it also applies to other UTXOs. Currently,
we calculate DNA based on the 'dna_preimage' supplied by the client. While we can identify duplicate DNA if the 'dna_preimage' is the same and the other information is identical, considering the hash of the UTXO as a whole, this becomes challenging when parameters like Parents (gender, name, etc.) can be updated, altering the hash of the UTXO and compromising DNA uniqueness.
This issues is exploited in any of the transactions like updateName , UpdatePrice, listKittyForSAle,etc

Possible Solution:
To address this, we can reconsider the DNA calculation process it self or check the uniqueness of DNA within the Kitty piece, as we are currently doing.

I think it is better to create new issues for this. I will do it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replied in your issue #190. I think you are not distinguishing between the concepts of two kitties with the same DNA vs a single kitty. You cannot input the same kitty twice because tuxedo core already checks it.

The question here is how would you ever get two identical kitties to begin with. The only way is if you mint them directly. Up to you whether that is valid or not. If it is not, use a universal creator to ensure they are always unique. If it is valid to mint kitties with identical dna, then it should be no surprise that their offspring can also have identical DNA.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @JoshOrndorff, I removed the DNA check.


for i in 0..input_data.len() {
let utxo_input_kitty = input_data[i]
.clone()
NadigerAmit marked this conversation as resolved.
Show resolved Hide resolved
.extract::<KittyData>()
.map_err(|_| ConstraintCheckerError::BadlyTyped)?;

if dna_to_kitty_set.contains(&utxo_input_kitty.dna) {
return Err(ConstraintCheckerError::DuplicateKittyFound);
} else {
dna_to_kitty_set.insert(utxo_input_kitty.clone().dna);
}

Ok(0)
let utxo_output_kitty = output_data[i]
.clone()
.extract::<KittyData>()
.map_err(|_| ConstraintCheckerError::BadlyTyped)?;
if utxo_input_kitty.dna != utxo_output_kitty.dna {
return Err(ConstraintCheckerError::DnaMismatchBetweenInputAndOutput);
}
check_kitty_name_update(&utxo_input_kitty, &utxo_output_kitty)?;
}
return Ok(0);
}

/// Checks:
/// - Private function used by can_kitty_name_be_updated.
/// - Only name is updated and ther basic properties are not updated.
///
fn check_kitty_name_update(
original_kitty: &KittyData,
updated_kitty: &KittyData,
) -> Result<TransactionPriority, ConstraintCheckerError> {
ensure!(
original_kitty != updated_kitty,
ConstraintCheckerError::KittyNameUnAltered
);
ensure!(
original_kitty.free_breedings == updated_kitty.free_breedings,
ConstraintCheckerError::FreeBreedingCannotBeUpdated
);
ensure!(
original_kitty.num_breedings == updated_kitty.num_breedings,
ConstraintCheckerError::NumOfBreedingCannotBeUpdated
);
ensure!(
original_kitty.parent == updated_kitty.parent,
ConstraintCheckerError::KittyGenderCannotBeUpdated
);
return Ok(0);
}
Loading
Loading