diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo index f097358455..f1f9766ad1 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo +++ b/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo @@ -1,3 +1,4 @@ +use dojo::model::ModelStorage; use core::starknet::ContractAddress; use dojo::world::IWorldDispatcher; @@ -54,6 +55,33 @@ struct FooModelMemberAdded { pub b: u128, } +#[derive(Introspect, Copy, Drop, Serde)] +enum MyEnum { + X: u8, +} + +#[derive(Introspect, Copy, Drop, Serde)] +#[dojo::model] +struct FooModelMemberChanged { + #[key] + pub caller: ContractAddress, + pub a: MyEnum, + pub b: u128, +} + +#[derive(Introspect, Copy, Drop, Serde)] +enum AnotherEnum { + X: u8, +} + +#[derive(Introspect, Copy, Drop, Serde)] +#[dojo::model] +struct FooModelMemberIllegalChange { + #[key] + pub caller: ContractAddress, + pub a: AnotherEnum, + pub b: u128, +} pub fn deploy_world_for_model_upgrades() -> IWorldDispatcher { let namespace_def = NamespaceDef { @@ -66,8 +94,20 @@ pub fn deploy_world_for_model_upgrades() -> IWorldDispatcher { ), TestResource::Model(m_FooModelMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()), TestResource::Model(m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(m_FooModelMemberChanged::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(m_FooModelMemberIllegalChange::TEST_CLASS_HASH.try_into().unwrap()), ] .span(), }; - spawn_test_world([namespace_def].span()).dispatcher + let world = spawn_test_world([namespace_def].span()).dispatcher; + + // write some model values to be able to check if after a successfully upgrade, these values + // remain the same + let mut world_storage = dojo::world::WorldStorageTrait::new(world, @"dojo"); + let caller = starknet::contract_address_const::<0xb0b>(); + + world_storage.write_model(@FooModelMemberAdded { caller, a: 123, b: 456 }); + world_storage.write_model(@FooModelMemberChanged { caller, a: MyEnum::X(42), b: 456 }); + + world } diff --git a/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo b/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo index 996111d135..0eae64670b 100644 --- a/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo +++ b/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo @@ -1,5 +1,6 @@ -use dojo::meta::introspect::Introspect; +use dojo::meta::introspect::{Introspect, Struct, Enum, Member, Ty, TyCompareTrait}; use dojo::meta::{Layout, FieldLayout}; +use crate::utils::GasCounterTrait; #[derive(Drop, Introspect)] struct Base { @@ -308,3 +309,456 @@ fn test_layout_of_inner_packed_enum() { fn test_layout_of_not_packed_inner_enum() { let _ = Introspect::::layout(); } + +#[test] +fn test_introspect_upgrade() { + let p = Ty::Primitive('u8'); + let s = Ty::Struct(Struct { name: 's', attrs: [].span(), children: [].span() }); + let e = Ty::Enum(Enum { name: 'e', attrs: [].span(), children: [].span() }); + let t = Ty::Tuple([Ty::Primitive('u8')].span()); + let a = Ty::Array([Ty::Primitive('u8')].span()); + let b = Ty::ByteArray; + + assert!(p.is_an_upgrade_of(@p)); + assert!(!p.is_an_upgrade_of(@s)); + assert!(!p.is_an_upgrade_of(@e)); + assert!(!p.is_an_upgrade_of(@t)); + assert!(!p.is_an_upgrade_of(@a)); + assert!(!p.is_an_upgrade_of(@b)); + + assert!(!s.is_an_upgrade_of(@p)); + assert!(s.is_an_upgrade_of(@s)); + assert!(!s.is_an_upgrade_of(@e)); + assert!(!s.is_an_upgrade_of(@t)); + assert!(!s.is_an_upgrade_of(@a)); + assert!(!s.is_an_upgrade_of(@b)); + + assert!(!e.is_an_upgrade_of(@p)); + assert!(!e.is_an_upgrade_of(@s)); + assert!(e.is_an_upgrade_of(@e)); + assert!(!e.is_an_upgrade_of(@t)); + assert!(!e.is_an_upgrade_of(@a)); + assert!(!e.is_an_upgrade_of(@b)); + + assert!(!t.is_an_upgrade_of(@p)); + assert!(!t.is_an_upgrade_of(@s)); + assert!(!t.is_an_upgrade_of(@e)); + assert!(t.is_an_upgrade_of(@t)); + assert!(!t.is_an_upgrade_of(@a)); + assert!(!t.is_an_upgrade_of(@b)); + + assert!(!a.is_an_upgrade_of(@p)); + assert!(!a.is_an_upgrade_of(@s)); + assert!(!a.is_an_upgrade_of(@e)); + assert!(!a.is_an_upgrade_of(@t)); + assert!(a.is_an_upgrade_of(@a)); + assert!(!a.is_an_upgrade_of(@b)); + + assert!(!b.is_an_upgrade_of(@p)); + assert!(!b.is_an_upgrade_of(@s)); + assert!(!b.is_an_upgrade_of(@e)); + assert!(!b.is_an_upgrade_of(@t)); + assert!(!b.is_an_upgrade_of(@a)); + assert!(b.is_an_upgrade_of(@b)); +} + +#[test] +fn test_primitive_upgrade() { + let primitives = [ + 'bool', 'u8', 'u16', 'u32', 'usize', 'u64', 'u128', 'u256', 'i8', 'i16', 'i32', 'i64', + 'i128', 'felt252', 'ClassHash', 'ContractAddress', + ] + .span(); + + let mut allowed_upgrades: Span<(felt252, Span)> = [ + ('bool', [].span()), ('u8', ['u16', 'u32', 'usize', 'u64', 'u128', 'felt252'].span()), + ('u16', ['u32', 'usize', 'u64', 'u128', 'felt252'].span()), + ('u32', ['usize', 'u64', 'u128', 'felt252'].span()), + ('usize', ['u32', 'u64', 'u128', 'felt252'].span()), ('u64', ['u128', 'felt252'].span()), + ('u128', ['felt252'].span()), ('u256', [].span()), + ('i8', ['i16', 'i32', 'i64', 'i128', 'felt252'].span()), + ('i16', ['i32', 'i64', 'i128', 'felt252'].span()), + ('i32', ['i64', 'i128', 'felt252'].span()), ('i64', ['i128', 'felt252'].span()), + ('i128', ['felt252'].span()), ('felt252', ['ClassHash', 'ContractAddress'].span()), + ('ClassHash', ['felt252', 'ContractAddress'].span()), + ('ContractAddress', ['felt252', 'ClassHash'].span()), + ] + .span(); + + loop { + match allowed_upgrades.pop_front() { + Option::Some(( + src, allowed, + )) => { + for dest in primitives { + let expected = if src == dest { + true + } else { + let allowed = *allowed; + let mut i = 0; + + loop { + if i >= allowed.len() { + break false; + } + + if *allowed.at(i) == *dest { + break true; + } + + i += 1; + } + }; + + assert_eq!( + Ty::Primitive(*dest).is_an_upgrade_of(@Ty::Primitive(*src)), + expected, + "src: {} dest: {}", + *src, + *dest, + ); + } + }, + Option::None => { break; }, + }; + } +} + +#[test] +fn test_struct_upgrade() { + let s = Struct { + name: 's', + attrs: ['one'].span(), + children: [ + Member { name: 'x', attrs: ['two'].span(), ty: Ty::Primitive('u8') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u16') }, + ] + .span(), + }; + + // different name + let mut upgraded = s; + upgraded.name = 'upgraded'; + assert!(!upgraded.is_an_upgrade_of(@s), "different name"); + + // different attributes + let mut upgraded = s; + upgraded.attrs = [].span(); + assert!(!upgraded.is_an_upgrade_of(@s), "different attributes"); + + // member name changed + let mut upgraded = s; + upgraded + .children = + [ + Member { name: 'new', attrs: ['two'].span(), ty: Ty::Primitive('u8') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u16') }, + ] + .span(); + assert!(!upgraded.is_an_upgrade_of(@s), "member name changed"); + + // member attr changed + let mut upgraded = s; + upgraded + .children = + [ + Member { name: 'x', attrs: [].span(), ty: Ty::Primitive('u8') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u16') }, + ] + .span(); + assert!(!upgraded.is_an_upgrade_of(@s), "member attr changed"); + + // allowed member change + let mut upgraded = s; + upgraded + .children = + [ + Member { name: 'x', attrs: ['two'].span(), ty: Ty::Primitive('u16') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u16') }, + ] + .span(); + assert!(upgraded.is_an_upgrade_of(@s), "allowed member change"); + + // wrong member change + let mut upgraded = s; + upgraded + .children = + [ + Member { name: 'x', attrs: ['two'].span(), ty: Ty::Primitive('u8') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u8') }, + ] + .span(); + assert!(!upgraded.is_an_upgrade_of(@s), "wrong member change"); + + // new member + let mut upgraded = s; + upgraded + .children = + [ + Member { name: 'x', attrs: ['two'].span(), ty: Ty::Primitive('u8') }, + Member { name: 'y', attrs: ['three'].span(), ty: Ty::Primitive('u16') }, + Member { name: 'z', attrs: ['four'].span(), ty: Ty::Primitive('u32') }, + ] + .span(); + assert!(upgraded.is_an_upgrade_of(@s), "new member"); +} + +#[test] +fn test_enum_upgrade() { + let e = Enum { + name: 'e', + attrs: ['one'].span(), + children: [('x', Ty::Primitive('u8')), ('y', Ty::Primitive('u16'))].span(), + }; + + // different name + let mut upgraded = e; + upgraded.name = 'upgraded'; + assert!(!upgraded.is_an_upgrade_of(@e), "different name"); + + // different attributes + let mut upgraded = e; + upgraded.attrs = [].span(); + assert!(!upgraded.is_an_upgrade_of(@e), "different attrs"); + + // variant name changed + let mut upgraded = e; + upgraded.children = [('new', Ty::Primitive('u8')), ('y', Ty::Primitive('u16'))].span(); + assert!(!upgraded.is_an_upgrade_of(@e), "variant name changed"); + + // allowed variant change + let mut upgraded = e; + upgraded.children = [('x', Ty::Primitive('u16')), ('y', Ty::Primitive('u16'))].span(); + assert!(upgraded.is_an_upgrade_of(@e), "allowed variant change"); + + // wrong variant change + let mut upgraded = e; + upgraded.children = [('x', Ty::Primitive('u8')), ('y', Ty::Primitive('u8'))].span(); + assert!(!upgraded.is_an_upgrade_of(@e), "wrong variant change"); + + // new member + let mut upgraded = e; + upgraded + .children = + [('x', Ty::Primitive('u8')), ('y', Ty::Primitive('u16')), ('z', Ty::Primitive('u32'))] + .span(); + assert!(upgraded.is_an_upgrade_of(@e), "new member"); + + let e = Enum { + name: 'e', + attrs: [].span(), + children: [('x', Ty::Tuple([].span())), ('y', Ty::Tuple([].span()))].span(), + }; + + // A variant without data (empty tuple / unit type) cannot be upgraded with data + let mut upgraded = e; + upgraded.children = [('x', Ty::Primitive('u8')), ('y', Ty::Tuple([].span()))].span(); + + assert!(!upgraded.is_an_upgrade_of(@e), "variant without data"); +} + +#[test] +fn test_tuple_upgrade() { + let t = Ty::Tuple([Ty::Primitive('u8'), Ty::Primitive('u16')].span()); + + // tuple item is upgradable + let upgraded = Ty::Tuple([Ty::Primitive('u16'), Ty::Primitive('u16')].span()); + assert!(upgraded.is_an_upgrade_of(@t)); + + // tuple item is not upgradable + let upgraded = Ty::Tuple([Ty::Primitive('bool'), Ty::Primitive('u16')].span()); + assert!(!upgraded.is_an_upgrade_of(@t)); + + // tuple length changed + let upgraded = Ty::Tuple( + [Ty::Primitive('u8'), Ty::Primitive('u16'), Ty::Primitive('u32')].span(), + ); + assert!(!upgraded.is_an_upgrade_of(@t)); +} + +#[test] +fn test_array_upgrade() { + let a = Ty::Array([Ty::Primitive('u8')].span()); + + // array item is upgradable + let upgraded = Ty::Array([Ty::Primitive('u16')].span()); + assert!(upgraded.is_an_upgrade_of(@a)); + + // array item is not upgradable + let upgraded = Ty::Array([Ty::Primitive('bool')].span()); + assert!(!upgraded.is_an_upgrade_of(@a)); +} + +#[test] +#[available_gas(300000)] +fn test_primitive_upgrade_performance() { + let gas = GasCounterTrait::start(); + let _ = Ty::Primitive('ClassHash').is_an_upgrade_of(@Ty::Primitive('ContractAddress')); + gas.end("Upgrade from ContractAddress to ClassHash"); +} + +#[test] +fn test_key_member_upgrade() { + let s = Struct { + name: 's', + attrs: [].span(), + children: [ + Member { name: 'x', attrs: ['key'].span(), ty: Ty::Primitive('u8') }, + Member { + name: 'y', + attrs: ['key'].span(), + ty: Ty::Enum( + Enum { + name: 'e', + attrs: [].span(), + children: [('A', Ty::Primitive('u8')), ('B', Ty::Primitive('u16'))].span(), + }, + ), + }, + ] + .span(), + }; + + // primitive type + let mut upgraded = s; + upgraded + .children = + [Member { name: 'x', attrs: ['key'].span(), ty: Ty::Primitive('u128') }, *s.children[1]] + .span(); + + assert!(upgraded.is_an_upgrade_of(@s), "key primitive type upgrade"); + + // enum type / new variant + let mut upgraded = s; + upgraded + .children = + [ + *s.children[0], + Member { + name: 'y', + attrs: ['key'].span(), + ty: Ty::Enum( + Enum { + name: 'e', + attrs: [].span(), + children: [ + ('A', Ty::Primitive('u8')), ('B', Ty::Primitive('u16')), + ('C', Ty::Primitive('u32')), + ] + .span(), + }, + ), + }, + ] + .span(); + + assert!(upgraded.is_an_upgrade_of(@s), "key enum type upgrade (variant added)"); + + // enum type / variant data upgrade (not allowed) + let mut upgraded = s; + upgraded + .children = + [ + *s.children[0], + Member { + name: 'y', + attrs: ['key'].span(), + ty: Ty::Enum( + Enum { + name: 'e', + attrs: [].span(), + children: [('A', Ty::Primitive('u8')), ('B', Ty::Primitive('u32'))] + .span(), + }, + ), + }, + ] + .span(); + + assert!(!upgraded.is_an_upgrade_of(@s), "key enum type upgrade (variant data upgraded)"); + + // struct type (not allowed) + let s = Struct { + name: 's', + attrs: [].span(), + children: [ + Member { + name: 'x', + attrs: ['key'].span(), + ty: Ty::Struct(Struct { name: 'n', attrs: [].span(), children: [].span() }), + }, + ] + .span(), + }; + + let mut upgraded = s; + upgraded + .children = + [ + Member { + name: 'x', + attrs: ['key'].span(), + ty: Ty::Struct( + Struct { + name: 'n', + attrs: [].span(), + children: [ + Member { name: 'y', attrs: [].span(), ty: Ty::Primitive('u16') }, + ] + .span(), + }, + ), + }, + ] + .span(); + + assert!(!upgraded.is_an_upgrade_of(@s), "key struct type upgrade"); + + // array type (not allowed) + let s = Struct { + name: 's', + attrs: [].span(), + children: [ + Member { + name: 'x', attrs: ['key'].span(), ty: Ty::Array([Ty::Primitive('u8')].span()), + }, + ] + .span(), + }; + + let mut upgraded = s; + upgraded + .children = + [ + Member { + name: 'x', attrs: ['key'].span(), ty: Ty::Array([Ty::Primitive('u16')].span()), + }, + ] + .span(); + + assert!(!upgraded.is_an_upgrade_of(@s), "key array type upgrade"); + + // tuple type (not allowed) + let s = Struct { + name: 's', + attrs: [].span(), + children: [ + Member { + name: 'x', attrs: ['key'].span(), ty: Ty::Tuple([Ty::Primitive('u8')].span()), + }, + ] + .span(), + }; + + let mut upgraded = s; + upgraded + .children = + [ + Member { + name: 'x', attrs: ['key'].span(), ty: Ty::Tuple([Ty::Primitive('u16')].span()), + }, + ] + .span(); + + assert!(!upgraded.is_an_upgrade_of(@s), "key tuple type upgrade"); +} diff --git a/crates/dojo/core-cairo-test/src/tests/world/model.cairo b/crates/dojo/core-cairo-test/src/tests/world/model.cairo index 717d1d0afe..0fa17c0ec7 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/model.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/model.cairo @@ -1,3 +1,4 @@ +use dojo::model::ModelStorage; use core::starknet::ContractAddress; use crate::tests::helpers::{ @@ -55,6 +56,35 @@ pub struct FooModelMemberAdded { pub c: u256, } +#[derive(Introspect, Copy, Drop, Serde, PartialEq)] +enum MyEnum { + X: u8, + Y: u16, +} + +#[derive(Introspect, Copy, Drop, Serde)] +#[dojo::model] +struct FooModelMemberChanged { + #[key] + pub caller: ContractAddress, + pub a: MyEnum, + pub b: u128, +} + +#[derive(Introspect, Copy, Drop, Serde)] +enum AnotherEnum { + X: bool, +} + +#[derive(Introspect, Copy, Drop, Serde)] +#[dojo::model] +struct FooModelMemberIllegalChange { + #[key] + pub caller: ContractAddress, + pub a: MyEnum, + pub b: u128, +} + #[test] fn test_register_model_for_namespace_owner() { let bob = starknet::contract_address_const::<0xb0b>(); @@ -166,7 +196,10 @@ fn test_upgrade_model_from_model_owner() { #[test] fn test_upgrade_model() { + let caller = starknet::contract_address_const::<0xb0b>(); + let world = deploy_world_for_model_upgrades(); + let mut world_storage = dojo::world::WorldStorageTrait::new(world, @"dojo"); drop_all_events(world.contract_address); @@ -191,6 +224,47 @@ fn test_upgrade_model() { } else { core::panic_with_felt252('no ModelUpgraded event'); } + + // values previously set in deploy_world_for_model_upgrades + let read: FooModelMemberAdded = world_storage.read_model(caller); + assert!(read.a == 123); + assert!(read.b == 456); + assert!(read.c == 0); +} + +fn test_upgrade_model_with_member_changed() { + let caller = starknet::contract_address_const::<0xb0b>(); + let world = deploy_world_for_model_upgrades(); + let mut world_storage = dojo::world::WorldStorageTrait::new(world, @"dojo"); + + drop_all_events(world.contract_address); + + world.upgrade_model("dojo", m_FooModelMemberChanged::TEST_CLASS_HASH.try_into().unwrap()); + + let event = starknet::testing::pop_log::(world.contract_address); + assert(event.is_some(), 'no event)'); + + if let world::Event::ModelUpgraded(event) = event.unwrap() { + assert( + event.selector == Model::::selector(DOJO_NSH), + 'bad model selector', + ); + assert( + event.class_hash == m_FooModelMemberChanged::TEST_CLASS_HASH.try_into().unwrap(), + 'bad model class_hash', + ); + assert( + event.address != core::num::traits::Zero::::zero(), + 'bad model address', + ); + } else { + core::panic_with_felt252('no ModelUpgraded event'); + } + + // values previously set in deploy_world_for_model_upgrades + let read: FooModelMemberChanged = world_storage.read_model(caller); + assert!(read.a == MyEnum::X(42)); + assert!(read.b == 456); } #[test] @@ -244,6 +318,18 @@ fn test_upgrade_model_with_member_moved() { world.upgrade_model("dojo", m_FooModelMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()); } +#[test] +#[should_panic( + expected: ( + "Invalid new schema to upgrade the resource `dojo-FooModelMemberIllegalChange`", + 'ENTRYPOINT_FAILED', + ), +)] +fn test_upgrade_model_with_member_illegal_change() { + let world = deploy_world_for_model_upgrades(); + world.upgrade_model("dojo", m_FooModelMemberIllegalChange::TEST_CLASS_HASH.try_into().unwrap()); +} + #[test] #[should_panic( expected: ( diff --git a/crates/dojo/core/src/lib.cairo b/crates/dojo/core/src/lib.cairo index 67b2dc9290..2d086a518a 100644 --- a/crates/dojo/core/src/lib.cairo +++ b/crates/dojo/core/src/lib.cairo @@ -29,7 +29,7 @@ pub mod meta { }; pub mod introspect; - pub use introspect::{Introspect, Ty, StructCompareTrait}; + pub use introspect::{Introspect, Ty, TyCompareTrait}; pub mod layout; pub use layout::{Layout, FieldLayout, LayoutCompareTrait}; diff --git a/crates/dojo/core/src/meta/introspect.cairo b/crates/dojo/core/src/meta/introspect.cairo index 6e6f8ee80e..9c4cca1ed3 100644 --- a/crates/dojo/core/src/meta/introspect.cairo +++ b/crates/dojo/core/src/meta/introspect.cairo @@ -1,6 +1,151 @@ use dojo::meta::Layout; use dojo::storage::packing; +// Each index matches with a primitive types in both arrays (main and nested): +// 'bool': 0 +// 'u8': 1 +// 'u16': 2 +// 'u32' / 'usize': 3 +// 'u64': 4 +// 'u128': 5 +// 'u256': 6 +// 'i8': 7 +// 'i16': 8 +// 'i32': 9 +// 'i64': 10 +// 'i128': 11 +// 'felt252': 12 +// 'ClassHash': 13 +// 'ContractAddress': 14 +const ALLOWED_PRIMITIVE_UPGRADES: [[bool; 15]; 15] = [ + // bool + [ + false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, + ], + // u8 + [ + false, true, true, true, true, true, false, false, false, false, false, false, true, false, + false, + ], + // u16 + [ + false, false, true, true, true, true, false, false, false, false, false, false, true, false, + false, + ], + // u32 + [ + false, false, false, true, true, true, false, false, false, false, false, false, true, + false, false, + ], + // u64 + [ + false, false, false, false, true, true, false, false, false, false, false, false, true, + false, false, + ], + // u128 + [ + false, false, false, false, false, true, false, false, false, false, false, false, true, + false, false, + ], + // u256 + [ + false, false, false, false, false, false, true, false, false, false, false, false, false, + false, false, + ], + // i8 + [ + false, false, false, false, false, false, false, true, true, true, true, true, true, false, + false, + ], + // i16 + [ + false, false, false, false, false, false, false, false, true, true, true, true, true, false, + false, + ], + // i32 + [ + false, false, false, false, false, false, false, false, false, true, true, true, true, + false, false, + ], + // i64 + [ + false, false, false, false, false, false, false, false, false, false, true, true, true, + false, false, + ], + // i128 + [ + false, false, false, false, false, false, false, false, false, false, false, true, true, + false, false, + ], + // felt252 + [ + false, false, false, false, false, false, false, false, false, false, false, false, true, + true, true, + ], + // ClassHash + [ + false, false, false, false, false, false, false, false, false, false, false, false, true, + true, true, + ], + // ContractAddress + [ + false, false, false, false, false, false, false, false, false, false, false, false, true, + true, true, + ], +]; + +#[inline(always)] +fn primitive_to_index(primitive: felt252) -> u32 { + if primitive == 'bool' { + return 0; + } + if primitive == 'u8' { + return 1; + } + if primitive == 'u16' { + return 2; + } + if primitive == 'u32' || primitive == 'usize' { + return 3; + } + if primitive == 'u64' { + return 4; + } + if primitive == 'u128' { + return 5; + } + if primitive == 'u256' { + return 6; + } + if primitive == 'i8' { + return 7; + } + if primitive == 'i16' { + return 8; + } + if primitive == 'i32' { + return 9; + } + if primitive == 'i64' { + return 10; + } + if primitive == 'i128' { + return 11; + } + if primitive == 'felt252' { + return 12; + } + if primitive == 'ClassHash' { + return 13; + } + if primitive == 'ContractAddress' { + return 14; + } + + return 0xFFFFFFFF; +} + #[derive(Copy, Drop, Serde, Debug, PartialEq)] pub enum Ty { Primitive: felt252, @@ -35,8 +180,93 @@ pub struct Member { pub ty: Ty, } -#[generate_trait] -pub impl StructCompareImpl of StructCompareTrait { +pub trait TyCompareTrait { + fn is_an_upgrade_of(self: @T, old: @T) -> bool; +} + +impl PrimitiveCompareImpl of TyCompareTrait { + fn is_an_upgrade_of(self: @felt252, old: @felt252) -> bool { + if self == old { + return true; + } + + let new_index = primitive_to_index(*self); + let old_index = primitive_to_index(*old); + + let allowed_upgrades = ALLOWED_PRIMITIVE_UPGRADES.span(); + let allowed_upgrades = allowed_upgrades[old_index].span(); + *allowed_upgrades[new_index] + } +} + +impl TyCompareImpl of TyCompareTrait { + fn is_an_upgrade_of(self: @Ty, old: @Ty) -> bool { + match (self, old) { + (Ty::Primitive(n), Ty::Primitive(o)) => n.is_an_upgrade_of(o), + (Ty::Struct(n), Ty::Struct(o)) => n.is_an_upgrade_of(o), + (Ty::Array(n), Ty::Array(o)) => { (*n).at(0).is_an_upgrade_of((*o).at(0)) }, + ( + Ty::Tuple(n), Ty::Tuple(o), + ) => { + let n = *n; + let o = *o; + + if n.len() != o.len() { + return false; + } + + let mut i = 0; + loop { + if i >= n.len() { + break true; + } + if !n.at(i).is_an_upgrade_of(o.at(i)) { + break false; + } + i += 1; + } + }, + (Ty::ByteArray, Ty::ByteArray) => true, + (Ty::Enum(n), Ty::Enum(o)) => n.is_an_upgrade_of(o), + _ => false, + } + } +} + +impl EnumCompareImpl of TyCompareTrait { + fn is_an_upgrade_of(self: @Enum, old: @Enum) -> bool { + if self.name != old.name + || self.attrs != old.attrs + || (*self.children).len() < (*old.children).len() { + return false; + } + + let mut i = 0; + + loop { + if i >= (*old.children).len() { + break true; + } + + let (old_name, old_ty) = *old.children[i]; + let (new_name, new_ty) = *self.children[i]; + + // renaming is not allowed as checking if variants have not been reordered + // could be quite challenging + if new_name != old_name { + break false; + } + + if !new_ty.is_an_upgrade_of(@old_ty) { + break false; + } + + i += 1; + } + } +} + +impl StructCompareImpl of TyCompareTrait { fn is_an_upgrade_of(self: @Struct, old: @Struct) -> bool { if self.name != old.name || self.attrs != old.attrs @@ -51,11 +281,79 @@ pub impl StructCompareImpl of StructCompareTrait { break true; } - if *old.children[i] != *self.children[i] { + if !self.children[i].is_an_upgrade_of(old.children[i]) { + break false; + } + + i += 1; + } + } +} + +impl MemberCompareImpl of TyCompareTrait { + fn is_an_upgrade_of(self: @Member, old: @Member) -> bool { + if self.name != old.name || self.attrs != old.attrs { + return false; + } + + let mut i = 0; + let is_key = loop { + if i >= (*self).attrs.len() { break false; } + if *self.attrs[i] == 'key' { + break true; + } + i += 1; + }; + + if is_key { + match (self.ty, old.ty) { + (Ty::Primitive(n), Ty::Primitive(o)) => n.is_an_upgrade_of(o), + ( + Ty::Enum(n), Ty::Enum(o), + ) => { + if n == o { + return true; + } + + let n = *n; + let o = *o; + + if n.name != o.name + || n.attrs != o.attrs + || n.children.len() < o.children.len() { + return false; + } + + // only new variants are allowed so existing variants must remain + // the same. + let mut i = 0; + loop { + if i >= o.children.len() { + break true; + } + + let (new_name, new_ty) = n.children[i]; + let (old_name, old_ty) = o.children[i]; + + if new_name != old_name || new_ty != old_ty { + break false; + } + + i += 1; + } + }, + (Ty::Struct(n), Ty::Struct(o)) => n == o, + (Ty::Array(n), Ty::Array(o)) => n == o, + (Ty::Tuple(n), Ty::Tuple(o)) => n == o, + (Ty::ByteArray, Ty::ByteArray) => true, + _ => false, + } + } else { + self.ty.is_an_upgrade_of(old.ty) } } } diff --git a/crates/dojo/core/src/world/errors.cairo b/crates/dojo/core/src/world/errors.cairo index a0cc3e4f0f..dc2eb762a2 100644 --- a/crates/dojo/core/src/world/errors.cairo +++ b/crates/dojo/core/src/world/errors.cairo @@ -82,6 +82,10 @@ pub fn invalid_resource_schema_upgrade(namespace: @ByteArray, name: @ByteArray) format!("Invalid new schema to upgrade the resource `{}-{}`", namespace, name) } +pub fn packed_layout_cannot_be_upgraded(namespace: @ByteArray, name: @ByteArray) -> ByteArray { + format!("Packed layout cannot be upgraded `{}-{}`", namespace, name) +} + pub fn invalid_resource_layout_upgrade(namespace: @ByteArray, name: @ByteArray) -> ByteArray { format!("Invalid new layout to upgrade the resource `{}-{}`", namespace, name) } diff --git a/crates/dojo/core/src/world/world_contract.cairo b/crates/dojo/core/src/world/world_contract.cairo index f3654b37e3..495412b0d1 100644 --- a/crates/dojo/core/src/world/world_contract.cairo +++ b/crates/dojo/core/src/world/world_contract.cairo @@ -42,7 +42,7 @@ pub mod world { use dojo::meta::{ Layout, IStoredResourceDispatcher, IStoredResourceDispatcherTrait, IDeployedResourceDispatcher, IDeployedResourceDispatcherTrait, LayoutCompareTrait, - StructCompareTrait, + TyCompareTrait, }; use dojo::model::{Model, ResourceMetadata, metadata, ModelIndex}; use dojo::storage; @@ -1100,6 +1100,10 @@ pub mod world { panic_with_byte_array(@errors::invalid_resource_layout_upgrade(namespace, name)); } + if let Layout::Fixed(_) = new_layout { + panic_with_byte_array(@errors::packed_layout_cannot_be_upgraded(namespace, name)); + } + if !new_schema.is_an_upgrade_of(@old_schema) { panic_with_byte_array(@errors::invalid_resource_schema_upgrade(namespace, name)); } diff --git a/examples/spawn-and-move/dojo_dev.toml b/examples/spawn-and-move/dojo_dev.toml index a1e96bb9e2..e717a9dac0 100644 --- a/examples/spawn-and-move/dojo_dev.toml +++ b/examples/spawn-and-move/dojo_dev.toml @@ -31,7 +31,7 @@ rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba" private_key = "0x1800000000300000180000000000030000000000003006001800006600" -world_address = "0x736c456dec44741fad1940876e62342a626ca73fd4ac574a94eb3ded2e25d8d" +world_address = "0x5d2547a660f585d31c9e8b53fc916ab22fc97a6d1c4d8fcd8fb12d1c20f9952" ipfs_config.url = "https://ipfs.infura.io:5001" ipfs_config.username = "2EBrzr7ZASQZKH32sl2xWauXPSA" ipfs_config.password = "12290b883db9138a8ae3363b6739d220" diff --git a/spawn-and-move-db.tar.gz b/spawn-and-move-db.tar.gz index 20dcfbeb51..04ec395d9d 100644 Binary files a/spawn-and-move-db.tar.gz and b/spawn-and-move-db.tar.gz differ diff --git a/types-test-db.tar.gz b/types-test-db.tar.gz index c5c1b3f889..ce1ab2c401 100644 Binary files a/types-test-db.tar.gz and b/types-test-db.tar.gz differ