diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14c97ee0..0cbc7941 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ on: env: JS_PACKAGES: "['clients-js']" - RUST_PACKAGES: "['clients-rust', 'interface', 'program']" + RUST_PACKAGES: "['clients-rust', 'interface', 'program', 'pinocchio-interface']" SBPF_PROGRAM_PACKAGES: "['program']" WASM_PACKAGES: "['interface', 'program']" IDL_PACKAGES: "['interface']" diff --git a/Cargo.lock b/Cargo.lock index d3ec163c..387015f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,7 +302,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -544,7 +544,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -692,7 +692,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -764,7 +764,7 @@ checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -860,7 +860,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -923,7 +923,7 @@ dependencies = [ "derive_more", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -935,7 +935,7 @@ dependencies = [ "cargo_toml", "proc-macro2", "serde_json", - "syn 2.0.98", + "syn 2.0.111", "thiserror 2.0.17", ] @@ -963,7 +963,7 @@ dependencies = [ "codama-nodes", "codama-syn-helpers", "serde_json", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -979,7 +979,7 @@ dependencies = [ "codama-syn-helpers", "derive_more", "proc-macro2", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -994,7 +994,7 @@ dependencies = [ "codama-stores", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1021,7 +1021,7 @@ dependencies = [ "derive_more", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1033,7 +1033,7 @@ dependencies = [ "cargo_toml", "codama-errors", "proc-macro2", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1046,7 +1046,7 @@ dependencies = [ "derive_more", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1267,7 +1267,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1291,7 +1291,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1302,7 +1302,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1385,7 +1385,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1405,7 +1405,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1452,7 +1452,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1475,7 +1475,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1630,7 +1630,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1643,7 +1643,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -1908,7 +1908,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -2397,7 +2397,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -2584,7 +2584,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3115,7 +3115,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3188,7 +3188,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3250,7 +3250,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3300,6 +3300,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "p-stake-interface" +version = "0.1.0" +dependencies = [ + "bincode", + "proptest", + "solana-pubkey", + "solana-stake-interface 2.0.2", + "test-case", + "wincode", +] + [[package]] name = "parking" version = "2.2.1" @@ -3385,7 +3397,7 @@ checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3520,9 +3532,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3563,7 +3575,7 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -3646,9 +3658,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3826,7 +3838,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -4294,7 +4306,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -4359,7 +4371,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -4384,7 +4396,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -5351,7 +5363,7 @@ checksum = "38e4bb0f5232668866a7f6f5cbc3222bbd9eebbff6afe392e3394498e8c9b1fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -6340,7 +6352,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7477,7 +7489,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7505,9 +7517,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -7543,7 +7555,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7629,7 +7641,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7640,7 +7652,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", "test-case-core", ] @@ -7670,7 +7682,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7681,7 +7693,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7778,7 +7790,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -7970,7 +7982,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -8241,7 +8253,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -8276,7 +8288,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8384,6 +8396,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "wincode" +version = "0.2.5" +source = "git+https://github.com/anza-xyz/wincode.git?rev=fa70c7c7c13885085f743e4f01deb1a4de0b64fb#fa70c7c7c13885085f743e4f01deb1a4de0b64fb" +dependencies = [ + "proc-macro2", + "quote", + "thiserror 2.0.17", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.2.3" +source = "git+https://github.com/anza-xyz/wincode.git?rev=fa70c7c7c13885085f743e4f01deb1a4de0b64fb#fa70c7c7c13885085f743e4f01deb1a4de0b64fb" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -8709,7 +8743,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", "synstructure 0.13.1", ] @@ -8731,7 +8765,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -8751,7 +8785,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", "synstructure 0.13.1", ] @@ -8772,7 +8806,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] @@ -8794,7 +8828,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ac4fc77c..a62d54a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "clients/rust", "interface", "program", + "pinocchio/interface", ] [workspace.package] diff --git a/pinocchio/interface/Cargo.lock b/pinocchio/interface/Cargo.lock new file mode 100644 index 00000000..a14e0be1 --- /dev/null +++ b/pinocchio/interface/Cargo.lock @@ -0,0 +1,866 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "solana-account-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" +dependencies = [ + "solana-address 2.0.0", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-address" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37320fd2945c5d654b2c6210624a52d66c3f1f73b653ed211ab91a703b35bdd" +dependencies = [ + "borsh", + "five8", + "five8_const", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a933ff1e50aff72d02173cfcd7511bd8540b027ee720b75f353f594f834216d0" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb62e9381182459a4520b5fe7fb22d423cae736239a6427fc398a88743d0ed59" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction", + "solana-program-error", + "solana-pubkey 4.0.0", + "solana-stable-layout", +] + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-epoch-rewards" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b319a4ed70390af911090c020571f0ff1f4ec432522d05ab89f5c08080381995" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5481e72cc4d52c169db73e4c0cd16de8bc943078aac587ec4817a75cc6388f" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a73cc03ca4bed871ca174558108835f8323e85917bb38b9c81c7af2ab853efe" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" +dependencies = [ + "solana-hash 4.0.1", +] + +[[package]] +name = "solana-hash" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5d48a6ee7b91fc7b998944ab026ed7b3e2fc8ee3bc58452644a86c2648152f" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", +] + +[[package]] +name = "solana-instruction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" +dependencies = [ + "bincode", + "borsh", + "serde", + "serde_derive", + "solana-define-syscall 4.0.1", + "solana-instruction-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04259e03c05faf38a8c24217b5cfe4c90572ae6184ab49cddb1584fdd756d3f" +dependencies = [ + "num-traits", + "solana-program-error", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-msg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +dependencies = [ + "solana-define-syscall 3.0.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f7104d456b58e1418c21a8581e89810278d1190f70f27ece7fc0b2c9282a57" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-rent" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b702d8c43711e3c8a9284a4f1bbc6a3de2553deb25b0c8142f9a44ef0ce5ddc1" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6430000e97083460b71d9fbadc52a2ab2f88f53b3a4c5e58c5ae3640a0e8c00" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2", + "solana-define-syscall 4.0.1", + "solana-hash 4.0.1", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a293f952293281443c04f4d96afd9d547721923d596e92b4377ed2360f1746" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1da74507795b6e8fb60b7c7306c0c36e2c315805d16eaaf479452661234685ac" +dependencies = [ + "solana-instruction", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-stake-interface" +version = "2.0.1" +dependencies = [ + "borsh", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", +] + +[[package]] +name = "solana-stake-interface-pinocchio" +version = "0.1.0" +dependencies = [ + "bincode", + "borsh", + "solana-clock", + "solana-pubkey 3.0.0", + "solana-stake-interface", +] + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3205cc7db64a0f1a20b7eb2405773fa64e45f7fe0fc7a73e50e90eca6b2b0be7" +dependencies = [ + "base64", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash 4.0.1", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.0.0", + "solana-sdk-ids", +] + + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] diff --git a/pinocchio/interface/Cargo.toml b/pinocchio/interface/Cargo.toml new file mode 100644 index 00000000..6342dfe2 --- /dev/null +++ b/pinocchio/interface/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "p-stake-interface" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } + +[dependencies] +# TODO: Waiting on 0.3.0 to be released +wincode = { git = "https://github.com/anza-xyz/wincode.git", rev = "fa70c7c7c13885085f743e4f01deb1a4de0b64fb", default-features = false, features = ["derive"] } + +[dev-dependencies] +bincode = "1.3.3" +proptest = "1.9.0" +solana-pubkey = "3.0.0" +solana-stake-interface = { path = "../../interface", features = ["bincode"] } +test-case = "3.3.1" diff --git a/pinocchio/interface/src/error.rs b/pinocchio/interface/src/error.rs new file mode 100644 index 00000000..104c43e7 --- /dev/null +++ b/pinocchio/interface/src/error.rs @@ -0,0 +1,30 @@ +use crate::state::StakeStateV2Tag; + +#[derive(Debug)] +pub enum StakeStateError { + /// The discriminant tag is not a valid variant (must be 0-3). + InvalidTag(u32), + /// An invalid state transition was attempted. + InvalidTransition { + from: StakeStateV2Tag, + to: StakeStateV2Tag, + }, + /// Pass-through for wincode read errors when borrowing layout structs. + Read(wincode::ReadError), + /// Input buffer is shorter than 200 bytes. + UnexpectedEof, + /// Pass-through for wincode write errors when serializing layout structs. + Write(wincode::WriteError), +} + +impl From for StakeStateError { + fn from(e: wincode::ReadError) -> Self { + StakeStateError::Read(e) + } +} + +impl From for StakeStateError { + fn from(e: wincode::WriteError) -> Self { + StakeStateError::Write(e) + } +} diff --git a/pinocchio/interface/src/lib.rs b/pinocchio/interface/src/lib.rs new file mode 100644 index 00000000..486f68ce --- /dev/null +++ b/pinocchio/interface/src/lib.rs @@ -0,0 +1,2 @@ +pub mod error; +pub mod state; diff --git a/pinocchio/interface/src/state/entrypoint.rs b/pinocchio/interface/src/state/entrypoint.rs new file mode 100644 index 00000000..31c92cb1 --- /dev/null +++ b/pinocchio/interface/src/state/entrypoint.rs @@ -0,0 +1,20 @@ +//! High-level entrypoint for reading and writing stake account state. +use { + super::{view::StakeStateV2View, writer::StakeStateV2Writer}, + crate::error::StakeStateError, +}; + +/// Main entrypoint for parsing stake account data. Provides zero-copy access to the stake account state. +pub struct StakeStateV2; + +impl StakeStateV2 { + /// Parse stake account data into a read-only view. + pub fn from_bytes(data: &[u8]) -> Result, StakeStateError> { + StakeStateV2View::from_bytes(data) + } + + /// Parse stake account data into a mutable writer. + pub fn from_bytes_mut(data: &mut [u8]) -> Result, StakeStateError> { + StakeStateV2Writer::from_bytes_mut(data) + } +} diff --git a/pinocchio/interface/src/state/layout.rs b/pinocchio/interface/src/state/layout.rs new file mode 100644 index 00000000..535179fb --- /dev/null +++ b/pinocchio/interface/src/state/layout.rs @@ -0,0 +1,145 @@ +use { + super::pod::{PodAddress, PodI64, PodU32, PodU64}, + crate::error::StakeStateError, + core::mem::size_of, + wincode::{Deserialize, ReadError, SchemaRead, SchemaWrite}, +}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] +#[wincode(assert_zero_copy)] +pub struct Authorized { + pub staker: PodAddress, + pub withdrawer: PodAddress, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] +#[wincode(assert_zero_copy)] +pub struct Lockup { + /// `UnixTimestamp` at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub unix_timestamp: PodI64, + /// epoch height at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub epoch: PodU64, + /// custodian signature on a transaction exempts the operation from + /// lockup constraints + pub custodian: PodAddress, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead, Default)] +#[wincode(assert_zero_copy)] +pub struct Meta { + pub rent_exempt_reserve: PodU64, + pub authorized: Authorized, + pub lockup: Lockup, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] +#[wincode(assert_zero_copy)] +pub struct Delegation { + /// to whom the stake is delegated + pub voter_pubkey: PodAddress, + /// activated stake amount, set at delegate() time + pub stake: PodU64, + /// epoch at which this stake was activated, `std::u64::MAX` if is a bootstrap stake + pub activation_epoch: PodU64, + /// epoch the stake was deactivated, `std::u64::MAX` if not deactivated + pub deactivation_epoch: PodU64, + /// Legacy bytes from legacy warmup/cooldown rate encoding (deprecated). + pub _reserved: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead, Default)] +#[wincode(assert_zero_copy)] +pub struct Stake { + pub delegation: Delegation, + /// credits observed is credits from vote account state when delegated or redeemed + pub credits_observed: PodU64, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] +#[wincode(tag_encoding = "u32")] +pub enum StakeStateV2Tag { + #[wincode(tag = 0)] + Uninitialized = 0, + #[wincode(tag = 1)] + Initialized = 1, + #[wincode(tag = 2)] + Stake = 2, + #[wincode(tag = 3)] + RewardsPool = 3, +} + +impl StakeStateV2Tag { + pub const TAG_LEN: usize = size_of::(); + + pub fn from_u32(v: u32) -> Result { + match v { + 0 => Ok(Self::Uninitialized), + 1 => Ok(Self::Initialized), + 2 => Ok(Self::Stake), + 3 => Ok(Self::RewardsPool), + other => Err(StakeStateError::InvalidTag(other)), + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < Self::TAG_LEN { + return Err(StakeStateError::UnexpectedEof); + } + + let tag_bytes = &bytes[..Self::TAG_LEN]; + StakeStateV2Tag::deserialize(tag_bytes).map_err(|e| match e { + ReadError::InvalidTagEncoding(tag) => StakeStateError::InvalidTag(tag as u32), + other => StakeStateError::Read(other), + }) + } +} + +/// Raw 200-byte stake account data with this structure: +/// +/// ```text +/// ┌────────┬──────┬────────────┐ +/// │ Offset │ Size │ Field │ +/// ├────────┼──────┼────────────┤ +/// │ 0 │ 4 │ Tag │ +/// │ 4 │ 120 │ Meta │ +/// │ 124 │ 72 │ Stake │ +/// │ 196 │ 1 │ StakeFlags │ +/// │ 197 │ 3 │ Padding │ +/// └────────┴──────┴────────────┘ +/// ``` +/// +/// All structs have alignment 1 for safe zero-copy from unaligned `&[u8]`. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] +#[wincode(assert_zero_copy)] +pub struct StakeStateV2Layout { + pub tag: PodU32, + pub meta: Meta, + pub stake: Stake, + pub stake_flags: u8, + pub padding: [u8; 3], +} + +// ======= Compile-time size guards ======= +const _: () = assert!(size_of::() == 200); +const _: () = assert!(size_of::() == 4); +const _: () = assert!(size_of::() == 4); +const _: () = assert!(size_of::() == 120); +const _: () = assert!(size_of::() == 72); +const _: () = assert!(size_of::() == 64); +const _: () = assert!(size_of::() == 48); +const _: () = assert!(size_of::() == 64); + +// ======= Compile-time alignment guards ======= +const _: () = assert!(align_of::() == 1); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(align_of::() == 1); diff --git a/pinocchio/interface/src/state/mod.rs b/pinocchio/interface/src/state/mod.rs new file mode 100644 index 00000000..0ed2fa1f --- /dev/null +++ b/pinocchio/interface/src/state/mod.rs @@ -0,0 +1,15 @@ +//! Zero-copy stake state types. + +mod entrypoint; +mod layout; +mod pod; +mod view; +mod writer; + +pub use { + entrypoint::StakeStateV2, + layout::*, + pod::*, + view::*, + writer::{StakeStateV2ViewMut, StakeStateV2Writer}, +}; diff --git a/pinocchio/interface/src/state/pod.rs b/pinocchio/interface/src/state/pod.rs new file mode 100644 index 00000000..92f75346 --- /dev/null +++ b/pinocchio/interface/src/state/pod.rs @@ -0,0 +1,79 @@ +//! Alignment-1 ("pod") primitives for zero-copy deserialization. +//! +//! Solana account data is a raw `&[u8]` with no alignment guarantees. +//! Standard Rust primitives like `u64` require 8-byte alignment, so you can't +//! safely cast `&[u8]` to `&u64` without risking undefined behavior. +//! +//! These "pod" (plain old data) types wrap byte arrays and provide safe +//! get/set methods that handle little-endian conversion. They have alignment 1, +//! so they can be safely referenced from any byte offset. +use wincode::{SchemaRead, SchemaWrite}; + +/// Macro to define an alignment-1 little-endian integer wrapper. +#[macro_export] +macro_rules! impl_pod_int { + ( + $(#[$meta:meta])* + $name:ident, $prim:ty, $n:expr + ) => { + $(#[$meta])* + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] + #[wincode(assert_zero_copy)] + pub struct $name(pub [u8; $n]); + + impl $name { + pub const fn from_primitive(v: $prim) -> Self { + Self(v.to_le_bytes()) + } + + pub fn get(self) -> $prim { + <$prim>::from_le_bytes(self.0) + } + + pub fn set(&mut self, v: $prim) { + self.0 = v.to_le_bytes(); + } + + pub fn as_bytes(&self) -> &[u8; $n] { + &self.0 + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + &mut self.0 + } + } + + impl From<$prim> for $name { + fn from(v: $prim) -> Self { + Self::from_primitive(v) + } + } + + impl From<$name> for $prim { + fn from(v: $name) -> Self { + v.get() + } + } + }; +} + +impl_pod_int!(PodU32, u32, 4); +impl_pod_int!(PodU64, u64, 8); +impl_pod_int!(PodI64, i64, 8); + +/// An `Address` stored as 32 bytes with alignment 1. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] +#[wincode(assert_zero_copy)] +pub struct PodAddress(pub [u8; 32]); + +impl PodAddress { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} diff --git a/pinocchio/interface/src/state/view.rs b/pinocchio/interface/src/state/view.rs new file mode 100644 index 00000000..afa4aa54 --- /dev/null +++ b/pinocchio/interface/src/state/view.rs @@ -0,0 +1,36 @@ +//! Read-only zero-copy view into stake account data. + +use { + super::layout::{Meta, Stake, StakeStateV2Layout, StakeStateV2Tag}, + crate::error::StakeStateError, + core::mem::size_of, + wincode::ZeroCopy, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StakeStateV2View<'a> { + Uninitialized, + Initialized(&'a Meta), + Stake { meta: &'a Meta, stake: &'a Stake }, + RewardsPool, +} + +impl<'a> StakeStateV2View<'a> { + pub(super) fn from_bytes(data: &'a [u8]) -> Result { + if data.len() < size_of::() { + return Err(StakeStateError::UnexpectedEof); + } + let layout = StakeStateV2Layout::from_bytes(data)?; + let tag = StakeStateV2Tag::from_u32(layout.tag.get())?; + + match tag { + StakeStateV2Tag::Uninitialized => Ok(Self::Uninitialized), + StakeStateV2Tag::RewardsPool => Ok(Self::RewardsPool), + StakeStateV2Tag::Initialized => Ok(Self::Initialized(&layout.meta)), + StakeStateV2Tag::Stake => Ok(Self::Stake { + meta: &layout.meta, + stake: &layout.stake, + }), + } + } +} diff --git a/pinocchio/interface/src/state/writer.rs b/pinocchio/interface/src/state/writer.rs new file mode 100644 index 00000000..2f9d856e --- /dev/null +++ b/pinocchio/interface/src/state/writer.rs @@ -0,0 +1,125 @@ +//! Mutable handle and view for stake account data. + +use { + super::{ + layout::{Meta, Stake, StakeStateV2Layout, StakeStateV2Tag}, + pod::PodU32, + view::StakeStateV2View, + }, + crate::error::StakeStateError, + core::mem::size_of, + wincode::ZeroCopy, +}; + +/// Mutable handle for stake account state transitions. +#[derive(Debug)] +pub struct StakeStateV2Writer<'a> { + data: &'a mut [u8], +} + +impl<'a> StakeStateV2Writer<'a> { + pub(super) fn from_bytes_mut(data: &'a mut [u8]) -> Result { + if data.len() < size_of::() { + return Err(StakeStateError::UnexpectedEof); + } + + let layout = StakeStateV2Layout::from_bytes_mut(data)?; + StakeStateV2Tag::from_u32(layout.tag.get())?; + + Ok(Self { data }) + } + + pub fn view(&self) -> Result, StakeStateError> { + StakeStateV2View::from_bytes(self.data) + } + + pub fn view_mut(&mut self) -> Result, StakeStateError> { + StakeStateV2ViewMut::from_bytes_mut(self.data) + } + + /// Transition to `Initialized` state. Only valid from `Uninitialized` state. + pub fn into_initialized(self, meta: Meta) -> Result { + self.check_transition(StakeStateV2Tag::Initialized)?; + + let layout = StakeStateV2Layout::from_bytes_mut(self.data)?; + + // Clear stake and tail regions + layout.stake = Stake::default(); + layout.stake_flags = 0; + layout.padding.fill(0); + + // Set meta and tag + layout.meta = meta; + layout.tag.set(StakeStateV2Tag::Initialized as u32); + + Ok(Self { data: self.data }) + } + + /// Transition to Stake state. Only valid from `Initialized` or `Stake` state. + /// When transitioning from `Initialized`, clears `stake_flags` to 0. + /// When staying in `Stake`, preserves existing `stake_flags`. + pub fn into_stake(self, meta: Meta, stake: Stake) -> Result { + self.check_transition(StakeStateV2Tag::Stake)?; + let from_initialized = self.tag()? == StakeStateV2Tag::Initialized; + + let layout = StakeStateV2Layout::from_bytes_mut(self.data)?; + + // Only clear tail region on Initialized -> Stake + if from_initialized { + layout.stake_flags = 0; + layout.padding.fill(0); + } + + layout.meta = meta; + layout.stake = stake; + layout.tag.set(StakeStateV2Tag::Stake as u32); + + Ok(Self { data: self.data }) + } + + fn tag(&self) -> Result { + StakeStateV2Tag::from_bytes(&self.data[..size_of::()]) + } + + fn check_transition(&self, to: StakeStateV2Tag) -> Result<(), StakeStateError> { + let from = self.tag()?; + match (from, to) { + (StakeStateV2Tag::Uninitialized, StakeStateV2Tag::Initialized) + | (StakeStateV2Tag::Initialized, StakeStateV2Tag::Stake) + | (StakeStateV2Tag::Stake, StakeStateV2Tag::Stake) => Ok(()), + _ => Err(StakeStateError::InvalidTransition { from, to }), + } + } +} + +/// Mutable view into stake account data for in-place field mutations. +#[derive(Debug, PartialEq, Eq)] +pub enum StakeStateV2ViewMut<'a> { + Uninitialized, + Initialized(&'a mut Meta), + Stake { + meta: &'a mut Meta, + stake: &'a mut Stake, + }, + RewardsPool, +} + +impl<'a> StakeStateV2ViewMut<'a> { + pub(super) fn from_bytes_mut(data: &'a mut [u8]) -> Result { + if data.len() < size_of::() { + return Err(StakeStateError::UnexpectedEof); + } + let layout = StakeStateV2Layout::from_bytes_mut(data)?; + let tag = StakeStateV2Tag::from_u32(layout.tag.get())?; + + match tag { + StakeStateV2Tag::Uninitialized => Ok(Self::Uninitialized), + StakeStateV2Tag::RewardsPool => Ok(Self::RewardsPool), + StakeStateV2Tag::Initialized => Ok(Self::Initialized(&mut layout.meta)), + StakeStateV2Tag::Stake => Ok(Self::Stake { + meta: &mut layout.meta, + stake: &mut layout.stake, + }), + } + } +} diff --git a/pinocchio/interface/tests/abi.rs b/pinocchio/interface/tests/abi.rs new file mode 100644 index 00000000..6b120b4d --- /dev/null +++ b/pinocchio/interface/tests/abi.rs @@ -0,0 +1,68 @@ +mod helpers; + +use { + bincode::Options, + helpers::*, + p_stake_interface::state::{StakeStateV2, StakeStateV2Layout, StakeStateV2View}, + proptest::prelude::*, + solana_stake_interface::state::StakeStateV2 as LegacyStakeStateV2, + wincode::ZeroCopy, +}; + +fn assert_legacy_and_view_agree(bytes: &[u8]) { + let legacy: LegacyStakeStateV2 = bincode_opts().deserialize(bytes).unwrap(); + let view = StakeStateV2::from_bytes(bytes).unwrap(); + + match (legacy, view) { + (LegacyStakeStateV2::Uninitialized, StakeStateV2View::Uninitialized) => {} + (LegacyStakeStateV2::RewardsPool, StakeStateV2View::RewardsPool) => {} + (LegacyStakeStateV2::Initialized(legacy_meta), StakeStateV2View::Initialized(meta)) => { + assert_meta_compat(meta, &legacy_meta); + } + ( + LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, legacy_flags), + StakeStateV2View::Stake { meta, stake }, + ) => { + assert_meta_compat(meta, &legacy_meta); + assert_stake_compat(stake, &legacy_stake); + + // ABI: stake_flags byte must match legacy exactly. + let layout = StakeStateV2Layout::from_bytes(bytes).unwrap(); + assert_eq!(layout.stake_flags, stake_flags_byte(&legacy_flags)); + } + + (o, v) => panic!("variant mismatch legacy={o:?} new={v:?}"), + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + // legacy bincode == new layout wincode bytes + #[test] + fn prop_wincode_roundtrips_legacy_bytes(legacy in arb_legacy_state()) { + let expected = serialize_legacy(&legacy); + prop_assert_eq!(expected.len(), LAYOUT_LEN); + + let new_layout = StakeStateV2Layout::from_bytes(&expected[..]).unwrap(); + let mut actual = [0u8; 200]; + wincode::serialize_into(&mut actual.as_mut_slice(), new_layout).unwrap(); + + prop_assert_eq!(expected.as_slice(), &actual); + } + + // both the legacy decoder and zero-copy view interpret trailing bytes the same + #[test] + fn prop_unpadded_legacy_prefix_is_compatible(legacy in arb_legacy_state(), mut tail in any::<[u8; 200]>()) { + let prefix = serialize_legacy_unpadded(&legacy); + tail[..prefix.len()].copy_from_slice(&prefix); + assert_legacy_and_view_agree(&tail[..]); + } + + // arbitrary 200-byte blobs with a valid tag must parse identically in legacy bincode and the zero-copy view + #[test] + fn prop_any_200_bytes_with_valid_tag_legacy_and_new_agree(mut bytes in any::<[u8; 200]>(), tag in 0u32..=3u32) { + bytes[..4].copy_from_slice(&tag.to_le_bytes()); + assert_legacy_and_view_agree(&bytes[..]); + } +} diff --git a/pinocchio/interface/tests/helpers/arb.rs b/pinocchio/interface/tests/helpers/arb.rs new file mode 100644 index 00000000..08c7cbbc --- /dev/null +++ b/pinocchio/interface/tests/helpers/arb.rs @@ -0,0 +1,83 @@ +#![allow(deprecated)] +use { + p_stake_interface::state::StakeStateV2Tag, + proptest::prelude::*, + solana_pubkey::Pubkey, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{ + Authorized as LegacyAuthorized, Delegation as LegacyDelegation, Lockup as LegacyLockup, + Meta as LegacyMeta, Stake as LegacyStake, StakeStateV2 as LegacyStakeStateV2, + }, + }, +}; + +fn arb_pubkey() -> impl Strategy { + any::<[u8; 32]>().prop_map(Pubkey::new_from_array) +} + +prop_compose! { + pub fn arb_legacy_meta()( + rent in any::(), + staker in arb_pubkey(), + withdrawer in arb_pubkey(), + unix in any::(), + epoch in any::(), + custodian in arb_pubkey(), + ) -> LegacyMeta { + LegacyMeta { + rent_exempt_reserve: rent, + authorized: LegacyAuthorized { staker, withdrawer }, + lockup: LegacyLockup { unix_timestamp: unix, epoch, custodian }, + } + } +} + +prop_compose! { + pub fn arb_legacy_stake()( + voter in arb_pubkey(), + stake_amount in any::(), + activation_epoch in any::(), + deactivation_epoch in any::(), + reserved_bytes in any::<[u8; 8]>(), + credits_observed in any::(), + ) -> LegacyStake { + let delegation = LegacyDelegation { + voter_pubkey: voter, + stake: stake_amount, + activation_epoch, + deactivation_epoch, + warmup_cooldown_rate: f64::from_bits(u64::from_le_bytes(reserved_bytes)), + }; + LegacyStake { delegation, credits_observed } + } +} + +prop_compose! { + fn arb_legacy_flags()(flag_set in any::()) -> LegacyStakeFlags { + let mut f = LegacyStakeFlags::empty(); + if flag_set { + f.set(LegacyStakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED); + } + f + } +} + +pub fn arb_legacy_state() -> impl Strategy { + prop_oneof![ + Just(LegacyStakeStateV2::Uninitialized), + Just(LegacyStakeStateV2::RewardsPool), + arb_legacy_meta().prop_map(LegacyStakeStateV2::Initialized), + (arb_legacy_meta(), arb_legacy_stake(), arb_legacy_flags()) + .prop_map(|(m, s, f)| LegacyStakeStateV2::Stake(m, s, f)), + ] +} + +pub fn arb_valid_tag() -> impl Strategy { + prop_oneof![ + Just(StakeStateV2Tag::Uninitialized), + Just(StakeStateV2Tag::Initialized), + Just(StakeStateV2Tag::Stake), + Just(StakeStateV2Tag::RewardsPool), + ] +} diff --git a/pinocchio/interface/tests/helpers/layout.rs b/pinocchio/interface/tests/helpers/layout.rs new file mode 100644 index 00000000..12009c5c --- /dev/null +++ b/pinocchio/interface/tests/helpers/layout.rs @@ -0,0 +1,94 @@ +#![allow(deprecated)] +use { + super::legacy::bincode_opts, + bincode::Options, + core::mem::size_of, + p_stake_interface::state::{ + Authorized, Delegation, Lockup, Meta, PodAddress, PodI64, PodU64, Stake, + StakeStateV2Layout, StakeStateV2Tag, + }, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{Meta as LegacyMeta, Stake as LegacyStake}, + }, +}; + +pub const TAG_LEN: usize = StakeStateV2Tag::TAG_LEN; +pub const LAYOUT_LEN: usize = size_of::(); + +pub const META_OFF: usize = TAG_LEN; +pub const STAKE_OFF: usize = TAG_LEN + size_of::(); +pub const FLAGS_OFF: usize = TAG_LEN + size_of::() + size_of::(); +pub const PADDING_OFF: usize = FLAGS_OFF + 1; + +pub fn write_tag(bytes: &mut [u8], tag: StakeStateV2Tag) { + let mut slice = &mut bytes[..TAG_LEN]; + wincode::serialize_into(&mut slice, &tag).unwrap(); +} + +pub fn empty_state_bytes(tag: StakeStateV2Tag) -> [u8; 200] { + let mut data = [0u8; size_of::()]; + let mut slice = &mut data[..StakeStateV2Tag::TAG_LEN]; + wincode::serialize_into(&mut slice, &tag).unwrap(); + data +} + +pub fn stake_flags_byte(legacy_flags: &LegacyStakeFlags) -> u8 { + let bs = bincode_opts().serialize(legacy_flags).unwrap(); + assert_eq!(bs.len(), 1); + bs[0] +} + +pub fn warmup_reserved_bytes_from_legacy_rate(legacy_rate: f64) -> [u8; 8] { + legacy_rate.to_bits().to_le_bytes() +} + +pub fn meta_from_legacy(legacy: &LegacyMeta) -> Meta { + Meta { + rent_exempt_reserve: PodU64::from_primitive(legacy.rent_exempt_reserve), + authorized: Authorized { + staker: PodAddress::from_bytes(legacy.authorized.staker.to_bytes()), + withdrawer: PodAddress::from_bytes(legacy.authorized.withdrawer.to_bytes()), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(legacy.lockup.unix_timestamp), + epoch: PodU64::from_primitive(legacy.lockup.epoch), + custodian: PodAddress::from_bytes(legacy.lockup.custodian.to_bytes()), + }, + } +} + +pub fn assert_meta_compat(new: &Meta, legacy: &LegacyMeta) { + assert_eq!(new.rent_exempt_reserve.get(), legacy.rent_exempt_reserve); + assert_eq!(new.authorized.staker.0, legacy.authorized.staker.to_bytes()); + assert_eq!( + new.authorized.withdrawer.0, + legacy.authorized.withdrawer.to_bytes() + ); + assert_eq!( + new.lockup.unix_timestamp.get(), + legacy.lockup.unix_timestamp + ); + assert_eq!(new.lockup.epoch.get(), legacy.lockup.epoch); + assert_eq!(new.lockup.custodian.0, legacy.lockup.custodian.to_bytes()); +} + +pub fn assert_stake_compat(new: &Stake, legacy: &LegacyStake) { + assert_eq!( + new.delegation.voter_pubkey.0, + legacy.delegation.voter_pubkey.to_bytes() + ); + assert_eq!(new.delegation.stake.get(), legacy.delegation.stake); + assert_eq!( + new.delegation.activation_epoch.get(), + legacy.delegation.activation_epoch + ); + assert_eq!( + new.delegation.deactivation_epoch.get(), + legacy.delegation.deactivation_epoch + ); + let expected_reserved = + warmup_reserved_bytes_from_legacy_rate(legacy.delegation.warmup_cooldown_rate); + assert_eq!(new.delegation._reserved, expected_reserved); + assert_eq!(new.credits_observed.get(), legacy.credits_observed); +} diff --git a/pinocchio/interface/tests/helpers/legacy.rs b/pinocchio/interface/tests/helpers/legacy.rs new file mode 100644 index 00000000..91fa8971 --- /dev/null +++ b/pinocchio/interface/tests/helpers/legacy.rs @@ -0,0 +1,26 @@ +use {bincode::Options, solana_stake_interface::state::StakeStateV2 as LegacyStakeStateV2}; + +const LEGACY_LAYOUT_LEN: usize = 200; + +pub fn bincode_opts() -> impl Options { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .allow_trailing_bytes() +} + +pub fn serialize_legacy(state: &LegacyStakeStateV2) -> Vec { + let mut data = bincode_opts().serialize(state).unwrap(); + assert!(data.len() <= LEGACY_LAYOUT_LEN); + data.resize(LEGACY_LAYOUT_LEN, 0); + data +} + +pub fn serialize_legacy_unpadded(state: &LegacyStakeStateV2) -> Vec { + let data = bincode_opts().serialize(state).unwrap(); + assert!(data.len() <= LEGACY_LAYOUT_LEN); + data +} + +pub fn deserialize_legacy(data: &[u8]) -> LegacyStakeStateV2 { + bincode_opts().deserialize(data).unwrap() +} diff --git a/pinocchio/interface/tests/helpers/mod.rs b/pinocchio/interface/tests/helpers/mod.rs new file mode 100644 index 00000000..a909c926 --- /dev/null +++ b/pinocchio/interface/tests/helpers/mod.rs @@ -0,0 +1,7 @@ +#![allow(dead_code, unused_imports)] + +pub mod arb; +pub mod layout; +pub mod legacy; + +pub use {arb::*, layout::*, legacy::*}; diff --git a/pinocchio/interface/tests/pod.rs b/pinocchio/interface/tests/pod.rs new file mode 100644 index 00000000..4e43d577 --- /dev/null +++ b/pinocchio/interface/tests/pod.rs @@ -0,0 +1,136 @@ +use p_stake_interface::state::{PodAddress, PodI64, PodU32, PodU64}; + +macro_rules! pod_int_tests { + ($pod_ty:ty, $prim_ty:ty, $size:expr, $test_value:expr, $mod_name:ident) => { + mod $mod_name { + use { + super::*, + core::mem::{align_of, needs_drop, size_of}, + }; + + #[test] + fn from_primitive_and_get_roundtrip() { + let pod = <$pod_ty>::from_primitive($test_value); + assert_eq!(pod.get(), $test_value); + } + + #[test] + fn bytes_are_little_endian() { + let pod = <$pod_ty>::from_primitive($test_value); + assert_eq!(pod.0, ($test_value as $prim_ty).to_le_bytes()); + assert_eq!(pod.as_bytes(), &($test_value as $prim_ty).to_le_bytes()); + } + + #[test] + fn from_trait_impls() { + let pod: $pod_ty = ($test_value as $prim_ty).into(); + let value: $prim_ty = pod.into(); + assert_eq!(value, $test_value); + } + + #[test] + fn set_updates_value() { + let mut pod = <$pod_ty>::from_primitive(0 as $prim_ty); + pod.set($test_value); + assert_eq!(pod.get(), $test_value); + } + + #[test] + fn default_is_zero() { + let pod = <$pod_ty>::default(); + assert_eq!(pod.get(), 0 as $prim_ty); + } + + #[test] + fn boundary_values() { + assert_eq!( + <$pod_ty>::from_primitive(<$prim_ty>::MIN).get(), + <$prim_ty>::MIN + ); + assert_eq!( + <$pod_ty>::from_primitive(<$prim_ty>::MAX).get(), + <$prim_ty>::MAX + ); + } + + #[test] + fn layout_properties() { + assert_eq!(align_of::<$pod_ty>(), 1); + assert_eq!(size_of::<$pod_ty>(), $size); + assert!(!needs_drop::<$pod_ty>()); + } + + #[test] + fn can_be_read_from_misaligned_offset() { + // Build a buffer with 1 leading byte so the value starts at an odd address. + let mut buf = [0u8; 1 + $size]; + buf[1..1 + $size].copy_from_slice(&($test_value as $prim_ty).to_le_bytes()); + + let ptr = unsafe { buf.as_ptr().add(1) as *const $pod_ty }; + let pod = unsafe { *ptr }; + assert_eq!(pod.get(), $test_value); + } + + #[test] + fn as_mut_slice_respects_little_endian_low_byte() { + let mut pod = <$pod_ty>::from_primitive(0 as $prim_ty); + let s = pod.as_mut_slice(); + assert_eq!(s.len(), $size); + + // Set only the least significant byte. + s.fill(0); + s[0] = 1; + assert_eq!(pod.get(), 1 as $prim_ty); + } + + #[test] + fn const_from_primitive_compiles() { + const _POD: $pod_ty = <$pod_ty>::from_primitive(0 as $prim_ty); + } + } + }; +} + +pod_int_tests!(PodU32, u32, 4, 123456789u32, pod_u32); +pod_int_tests!(PodU64, u64, 8, 9876543210u64, pod_u64); +pod_int_tests!(PodI64, i64, 8, -1234567890i64, pod_i64); + +// ============================================================================= +// PodAddress tests +// ============================================================================= + +#[test] +fn from_bytes_and_as_bytes_roundtrip() { + let bytes = [42; 32]; + let addr = PodAddress::from_bytes(bytes); + assert_eq!(addr.as_bytes(), &bytes); + assert_eq!(addr.0, bytes); +} + +#[test] +fn default_is_zero() { + let addr = PodAddress::default(); + assert_eq!(addr.as_bytes(), &[0u8; 32]); +} + +#[test] +fn equality() { + let a = PodAddress::from_bytes([1; 32]); + let b = PodAddress::from_bytes([1; 32]); + let c = PodAddress::from_bytes([2; 32]); + assert_eq!(a, b); + assert_ne!(a, c); +} + +#[test] +fn layout_properties() { + use core::mem::{align_of, needs_drop, size_of}; + assert_eq!(align_of::(), 1); + assert_eq!(size_of::(), 32); + assert!(!needs_drop::()); +} + +#[test] +fn const_from_bytes_compiles() { + const _A: PodAddress = PodAddress::from_bytes([0u8; 32]); +} diff --git a/pinocchio/interface/tests/tag.rs b/pinocchio/interface/tests/tag.rs new file mode 100644 index 00000000..585cdf7d --- /dev/null +++ b/pinocchio/interface/tests/tag.rs @@ -0,0 +1,149 @@ +#![allow(deprecated)] +mod helpers; + +use { + helpers::*, + p_stake_interface::{error::StakeStateError, state::StakeStateV2Tag}, + solana_pubkey::Pubkey, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{ + Authorized as LegacyAuthorized, Delegation as LegacyDelegation, Lockup as LegacyLockup, + Meta as LegacyMeta, Stake as LegacyStake, StakeStateV2 as LegacyStakeStateV2, + }, + }, + test_case::test_case, +}; + +fn serialize_tag(tag: StakeStateV2Tag) -> [u8; 4] { + let mut buf = [0u8; 4]; + wincode::serialize_into(&mut buf.as_mut_slice(), &tag).unwrap(); + buf +} + +fn legacy_state_for_tag(tag: StakeStateV2Tag) -> LegacyStakeStateV2 { + match tag { + StakeStateV2Tag::Uninitialized => LegacyStakeStateV2::Uninitialized, + StakeStateV2Tag::RewardsPool => LegacyStakeStateV2::RewardsPool, + StakeStateV2Tag::Initialized => LegacyStakeStateV2::Initialized(LegacyMeta { + rent_exempt_reserve: u64::MAX, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([17; 32]), + withdrawer: Pubkey::new_from_array([34; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: 123, + epoch: u64::MAX, + custodian: Pubkey::new_from_array([51; 32]), + }, + }), + StakeStateV2Tag::Stake => { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 1, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([68; 32]), + withdrawer: Pubkey::new_from_array([85; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -1, + epoch: 1, + custodian: Pubkey::new_from_array([102; 32]), + }, + }; + + let legacy_delegation = LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([119; 32]), + stake: u64::MAX, + activation_epoch: 0, + deactivation_epoch: u64::MAX, + warmup_cooldown_rate: f64::from_le_bytes([170; 8]), + }; + + let legacy_stake = LegacyStake { + delegation: legacy_delegation, + credits_observed: u64::MAX - 1, + }; + + let mut legacy_flags = LegacyStakeFlags::empty(); + legacy_flags + .set(LegacyStakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED); + + LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, legacy_flags) + } + } +} + +#[test] +fn tag_len_is_4_bytes() { + assert_eq!(StakeStateV2Tag::TAG_LEN, 4); +} + +#[test_case(StakeStateV2Tag::Uninitialized, 0)] +#[test_case(StakeStateV2Tag::Initialized, 1)] +#[test_case(StakeStateV2Tag::Stake, 2)] +#[test_case(StakeStateV2Tag::RewardsPool, 3)] +fn tag_serializes_to_expected_u32_le_discriminant(tag: StakeStateV2Tag, expected: u32) { + let buf = serialize_tag(tag); + assert_eq!(buf, expected.to_le_bytes()); +} + +#[test] +fn from_u32_errors_on_invalid_values() { + let err = StakeStateV2Tag::from_u32(4).unwrap_err(); + assert!(matches!(err, StakeStateError::InvalidTag(4))); + + let err = StakeStateV2Tag::from_u32(u32::MAX).unwrap_err(); + assert!(matches!(err, StakeStateError::InvalidTag(u32::MAX))); +} + +#[test_case(0u32, StakeStateV2Tag::Uninitialized)] +#[test_case(1u32, StakeStateV2Tag::Initialized)] +#[test_case(2u32, StakeStateV2Tag::Stake)] +#[test_case(3u32, StakeStateV2Tag::RewardsPool)] +fn from_bytes_decodes_all_valid_tags(expected: u32, tag: StakeStateV2Tag) { + let bytes = expected.to_le_bytes(); + let decoded = StakeStateV2Tag::from_bytes(&bytes[..]).unwrap(); + assert_eq!(decoded, tag); +} + +#[test] +fn from_bytes_ignores_trailing_data() { + // from_bytes() should only look at the first 4 bytes and ignore any trailing data. + let mut bytes = [0u8; 9]; + bytes[..4].copy_from_slice(&2u32.to_le_bytes()); + bytes[4..].copy_from_slice(&[171; 5]); + + let decoded = StakeStateV2Tag::from_bytes(&bytes[..]).unwrap(); + assert_eq!(decoded, StakeStateV2Tag::Stake); +} + +#[test] +fn from_bytes_rejects_invalid_discriminant() { + let bytes = 999u32.to_le_bytes(); + let err = StakeStateV2Tag::from_bytes(&bytes[..]).unwrap_err(); + assert!(matches!(err, StakeStateError::InvalidTag(999))); +} + +#[test] +fn from_bytes_rejects_short_buffer() { + let bytes = [0u8; 3]; + let err = StakeStateV2Tag::from_bytes(&bytes[..]).unwrap_err(); + assert!(matches!(err, StakeStateError::UnexpectedEof)); +} + +#[test_case(StakeStateV2Tag::Uninitialized)] +#[test_case(StakeStateV2Tag::Initialized)] +#[test_case(StakeStateV2Tag::Stake)] +#[test_case(StakeStateV2Tag::RewardsPool)] +fn tag_encoding_matches_legacy_bincode_discriminant(tag: StakeStateV2Tag) { + // This enforces the compatibility requirement: legacy StakeStateV2 (bincode with our opts) + // must have the same 4-byte LE u32 discriminant as the wincode-encoded StakeStateV2Tag. + let legacy_state = legacy_state_for_tag(tag); + let legacy_bytes = serialize_legacy(&legacy_state); + assert!(legacy_bytes.len() >= StakeStateV2Tag::TAG_LEN); + + let legacy_tag = &legacy_bytes[..StakeStateV2Tag::TAG_LEN]; + let wincode_tag = serialize_tag(tag); + + assert_eq!(legacy_tag, &wincode_tag[..]); +} diff --git a/pinocchio/interface/tests/view.rs b/pinocchio/interface/tests/view.rs new file mode 100644 index 00000000..d7e12375 --- /dev/null +++ b/pinocchio/interface/tests/view.rs @@ -0,0 +1,404 @@ +#![allow(clippy::arithmetic_side_effects)] +#![allow(deprecated)] + +mod helpers; + +use { + bincode::Options, + helpers::*, + p_stake_interface::{ + error::StakeStateError, + state::{StakeStateV2, StakeStateV2Tag, StakeStateV2View}, + }, + proptest::prelude::*, + solana_pubkey::Pubkey, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{ + Authorized as LegacyAuthorized, Delegation as LegacyDelegation, Lockup as LegacyLockup, + Meta as LegacyMeta, Stake as LegacyStake, StakeStateV2 as LegacyStakeStateV2, + }, + }, + test_case::test_case, +}; + +// Verifies that the deserialized view is a true zero-copy borrow into the original byte slice. +fn assert_borrows_at(borrow: &T, bytes: &[u8], offset: usize) { + let ptr = borrow as *const T; + let expected = unsafe { bytes.as_ptr().add(offset) }; + assert_eq!(ptr as *const u8, expected); +} + +fn overwrite_tail(bytes: &mut [u8], stake_flags: u8, padding: [u8; 3]) { + bytes[FLAGS_OFF] = stake_flags; + bytes[PADDING_OFF..LAYOUT_LEN].copy_from_slice(&padding); +} + +#[test] +fn view_short_buffer_returns_unexpected_eof() { + let data = vec![0u8; LAYOUT_LEN - 1]; + let err = StakeStateV2::from_bytes(&data).unwrap_err(); + assert!(matches!(err, StakeStateError::UnexpectedEof)); +} + +#[test] +fn view_buffer_with_trailing_bytes_is_ok() { + let mut data = empty_state_bytes(StakeStateV2Tag::Uninitialized).to_vec(); + data.extend_from_slice(&[171; 64]); + let view = StakeStateV2::from_bytes(&data).unwrap(); + assert!(matches!(view, StakeStateV2View::Uninitialized)); +} + +#[test] +fn view_invalid_tag_returns_error() { + let mut data = [0u8; LAYOUT_LEN]; + data[..4].copy_from_slice(&999u32.to_le_bytes()); + let err = StakeStateV2::from_bytes(&data).unwrap_err(); + assert!(matches!(err, StakeStateError::InvalidTag(999))); +} + +#[test_case(StakeStateV2Tag::Uninitialized)] +#[test_case(StakeStateV2Tag::Initialized)] +#[test_case(StakeStateV2Tag::Stake)] +#[test_case(StakeStateV2Tag::RewardsPool)] +fn view_variants_match_tag_for_empty_layout_bytes(tag: StakeStateV2Tag) { + let data = empty_state_bytes(tag); + let bytes = &data; + + let view = StakeStateV2::from_bytes(bytes).unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2View::Uninitialized) => {} + (StakeStateV2Tag::RewardsPool, StakeStateV2View::RewardsPool) => {} + + (StakeStateV2Tag::Initialized, StakeStateV2View::Initialized(meta)) => { + assert_borrows_at(meta, bytes, META_OFF); + } + + (StakeStateV2Tag::Stake, StakeStateV2View::Stake { meta, stake }) => { + assert_borrows_at(meta, bytes, META_OFF); + assert_borrows_at(stake, bytes, STAKE_OFF); + } + + _ => panic!("unexpected variant for tag {tag:?}"), + } +} + +#[test] +fn view_initialized_legacy_bytes_borrows_correctly() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 1234605616436508552, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([1u8; 32]), + withdrawer: Pubkey::new_from_array([2u8; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -123, + epoch: 456, + custodian: Pubkey::new_from_array([3u8; 32]), + }, + }; + let legacy_state = LegacyStakeStateV2::Initialized(legacy_meta); + let data = serialize_legacy(&legacy_state); + + let view = StakeStateV2::from_bytes(&data).unwrap(); + let StakeStateV2View::Initialized(meta) = view else { + panic!("expected Initialized"); + }; + + assert_borrows_at(meta, &data, META_OFF); + assert_meta_compat(meta, &legacy_meta); + + // Legacy encoding defaults for these bytes in Initialized. + assert_eq!(data[FLAGS_OFF], 0); + assert_eq!(&data[PADDING_OFF..LAYOUT_LEN], &[0u8; 3]); +} + +#[test] +fn view_unaligned_initialized_legacy_bytes_borrows_correctly() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 42, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([17; 32]), + withdrawer: Pubkey::new_from_array([34; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: i64::MIN + 7, + epoch: u64::MAX - 9, + custodian: Pubkey::new_from_array([51; 32]), + }, + }; + let legacy_state = LegacyStakeStateV2::Initialized(legacy_meta); + let aligned = serialize_legacy(&legacy_state); + + let mut buffer = vec![0u8; LAYOUT_LEN + 1]; + buffer[1..1 + LAYOUT_LEN].copy_from_slice(&aligned); + let unaligned = &buffer[1..1 + LAYOUT_LEN]; + + let view = StakeStateV2::from_bytes(unaligned).unwrap(); + let StakeStateV2View::Initialized(meta) = view else { + panic!("expected Initialized"); + }; + + assert_borrows_at(meta, unaligned, META_OFF); + assert_meta_compat(meta, &legacy_meta); +} + +#[test] +fn view_initialized_legacy_bytes_ignores_tail_bytes() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 1, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([9u8; 32]), + withdrawer: Pubkey::new_from_array([8u8; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -7, + epoch: 9, + custodian: Pubkey::new_from_array([7u8; 32]), + }, + }; + let legacy_state = LegacyStakeStateV2::Initialized(legacy_meta); + let mut data = serialize_legacy(&legacy_state); + + overwrite_tail(&mut data, 0xDE, [0xAD, 0xBE, 0xEF]); + + let view = StakeStateV2::from_bytes(&data).unwrap(); + let StakeStateV2View::Initialized(meta) = view else { + panic!("expected Initialized"); + }; + + assert_meta_compat(meta, &legacy_meta); + assert_eq!(data[FLAGS_OFF], 0xDE); + assert_eq!(&data[PADDING_OFF..LAYOUT_LEN], &[0xAD, 0xBE, 0xEF]); +} + +#[test] +fn view_stake_legacy_bytes_borrows_correctly() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 1, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([68; 32]), + withdrawer: Pubkey::new_from_array([85; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -1, + epoch: 1, + custodian: Pubkey::new_from_array([102; 32]), + }, + }; + + let reserved_bytes = [170u8; 8]; + let legacy_delegation = LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([119; 32]), + stake: u64::MAX, + activation_epoch: 0, + deactivation_epoch: u64::MAX, + warmup_cooldown_rate: f64::from_le_bytes(reserved_bytes), + }; + + let legacy_stake = LegacyStake { + delegation: legacy_delegation, + credits_observed: u64::MAX - 1, + }; + + let mut legacy_flags = LegacyStakeFlags::empty(); + legacy_flags.set(LegacyStakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED); + + let expected_flags_byte = bincode_opts().serialize(&legacy_flags).unwrap()[0]; + + let legacy_state = LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, legacy_flags); + let data = serialize_legacy(&legacy_state); + + let view = StakeStateV2::from_bytes(&data).unwrap(); + let StakeStateV2View::Stake { meta, stake } = view else { + panic!("expected Stake"); + }; + + assert_borrows_at(meta, &data, META_OFF); + assert_borrows_at(stake, &data, STAKE_OFF); + + assert_meta_compat(meta, &legacy_meta); + assert_stake_compat(stake, &legacy_stake); + + assert_eq!(data[FLAGS_OFF], expected_flags_byte); +} + +#[test] +fn view_unaligned_stake_legacy_bytes_borrows_correctly() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 9, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([1; 32]), + withdrawer: Pubkey::new_from_array([2; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: 3, + epoch: 4, + custodian: Pubkey::new_from_array([5; 32]), + }, + }; + + let reserved_bytes = [94u8; 8]; + let legacy_delegation = LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([6; 32]), + stake: 7, + activation_epoch: 8, + deactivation_epoch: 9, + warmup_cooldown_rate: f64::from_le_bytes(reserved_bytes), + }; + let legacy_stake = LegacyStake { + delegation: legacy_delegation, + credits_observed: 10, + }; + + let legacy_state = + LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, LegacyStakeFlags::empty()); + let aligned = serialize_legacy(&legacy_state); + + let mut buffer = vec![0u8; LAYOUT_LEN + 1]; + buffer[1..1 + LAYOUT_LEN].copy_from_slice(&aligned); + let unaligned = &buffer[1..1 + LAYOUT_LEN]; + + let view = StakeStateV2::from_bytes(unaligned).unwrap(); + let StakeStateV2View::Stake { meta, stake } = view else { + panic!("expected Stake"); + }; + + assert_borrows_at(meta, unaligned, META_OFF); + assert_borrows_at(stake, unaligned, STAKE_OFF); + + assert_meta_compat(meta, &legacy_meta); + assert_stake_compat(stake, &legacy_stake); + + assert_eq!(aligned[FLAGS_OFF], 0); +} + +#[test] +fn view_stake_legacy_bytes_ignores_tail_bytes() { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 111, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([17; 32]), + withdrawer: Pubkey::new_from_array([34; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -7, + epoch: 9, + custodian: Pubkey::new_from_array([51; 32]), + }, + }; + + let reserved_bytes = [19u8; 8]; + let legacy_delegation = LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([6; 32]), + stake: 7, + activation_epoch: 8, + deactivation_epoch: 9, + warmup_cooldown_rate: f64::from_le_bytes(reserved_bytes), + }; + let legacy_stake = LegacyStake { + delegation: legacy_delegation, + credits_observed: 10, + }; + + let legacy_state = + LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, LegacyStakeFlags::empty()); + let mut data = serialize_legacy(&legacy_state); + + overwrite_tail(&mut data, 0xEE, [0xFA, 0xCE, 0xB0]); + + let view = StakeStateV2::from_bytes(&data).unwrap(); + let StakeStateV2View::Stake { meta, stake } = view else { + panic!("expected Stake"); + }; + + assert_meta_compat(meta, &legacy_meta); + assert_stake_compat(stake, &legacy_stake); + + assert_eq!(data[FLAGS_OFF], 0xEE); + assert_eq!(&data[PADDING_OFF..LAYOUT_LEN], &[0xFA, 0xCE, 0xB0]); +} + +// ----------------------------- property tests -------------------------------- + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + #[test] + fn prop_short_buffer_returns_unexpected_eof(data in proptest::collection::vec(any::(), 0..LAYOUT_LEN)) { + let err = StakeStateV2::from_bytes(&data).unwrap_err(); + prop_assert!(matches!(err, StakeStateError::UnexpectedEof)); + } + + #[test] + fn prop_invalid_tag_when_view_then_invalid_tag( + mut bytes in any::<[u8; 200]>(), + invalid in 4u32..=u32::MAX, + ) { + bytes[..4].copy_from_slice(&invalid.to_le_bytes()); + let err = StakeStateV2::from_bytes(&bytes).unwrap_err(); + prop_assert!(matches!(err, StakeStateError::InvalidTag(t) if t == invalid)); + } + + #[test] + fn prop_any_200_bytes_with_valid_tag_when_view_then_variant_matches( + mut bytes in any::<[u8; 200]>(), + tag in arb_valid_tag(), + ) { + write_tag(&mut bytes, tag); + + let view = StakeStateV2::from_bytes(&bytes).unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2View::Uninitialized) => {} + (StakeStateV2Tag::Initialized, StakeStateV2View::Initialized(_)) => {} + (StakeStateV2Tag::Stake, StakeStateV2View::Stake { .. }) => {} + (StakeStateV2Tag::RewardsPool, StakeStateV2View::RewardsPool) => {} + _ => prop_assert!(false, "tag/view mismatch"), + } + } + + #[test] + fn prop_any_200_bytes_with_valid_tag_when_view_then_variant_matches_on_unaligned_slice( + mut buffer in any::<[u8; 201]>(), + tag in arb_valid_tag(), + ) { + // Make an unaligned 200-byte window + let unaligned = &mut buffer[1..1 + LAYOUT_LEN]; + write_tag(unaligned, tag); + + let view = StakeStateV2::from_bytes(unaligned).unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2View::Uninitialized) => {} + (StakeStateV2Tag::Initialized, StakeStateV2View::Initialized(meta)) => { + assert_borrows_at(meta, unaligned, META_OFF); + } + (StakeStateV2Tag::Stake, StakeStateV2View::Stake { meta, stake }) => { + assert_borrows_at(meta, unaligned, META_OFF); + assert_borrows_at(stake, unaligned, STAKE_OFF); + } + (StakeStateV2Tag::RewardsPool, StakeStateV2View::RewardsPool) => {} + _ => prop_assert!(false, "tag/view mismatch (unaligned)"), + } + } + + #[test] + fn prop_random_legacy_state_when_view_then_matches_expected(legacy in arb_legacy_state()) { + let data = serialize_legacy(&legacy); + prop_assert_eq!(data.len(), 200); + + let view = StakeStateV2::from_bytes(&data).unwrap(); + match (legacy, view) { + (LegacyStakeStateV2::Uninitialized, StakeStateV2View::Uninitialized) => {} + (LegacyStakeStateV2::RewardsPool, StakeStateV2View::RewardsPool) => {} + (LegacyStakeStateV2::Initialized(legacy_meta), StakeStateV2View::Initialized(meta)) => { + assert_meta_compat(meta, &legacy_meta); + } + (LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, flags), StakeStateV2View::Stake { meta, stake }) => { + assert_meta_compat(meta, &legacy_meta); + assert_stake_compat(stake, &legacy_stake); + prop_assert_eq!(data[FLAGS_OFF], stake_flags_byte(&flags)); + } + (o, v) => prop_assert!(false, "variant mismatch legacy={o:?} new={v:?}"), + } + } +} diff --git a/pinocchio/interface/tests/view_mut.rs b/pinocchio/interface/tests/view_mut.rs new file mode 100644 index 00000000..d4121fed --- /dev/null +++ b/pinocchio/interface/tests/view_mut.rs @@ -0,0 +1,300 @@ +#![allow(clippy::arithmetic_side_effects)] +#![allow(deprecated)] + +mod helpers; + +use { + core::mem::size_of, + helpers::*, + p_stake_interface::state::{ + Delegation, StakeStateV2, StakeStateV2Tag, StakeStateV2View, StakeStateV2ViewMut, + }, + proptest::prelude::*, + solana_pubkey::Pubkey, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{ + Authorized as LegacyAuthorized, Lockup as LegacyLockup, Meta as LegacyMeta, + StakeStateV2 as LegacyStakeStateV2, + }, + }, + test_case::test_case, +}; + +// Verifies that the deserialized mutable view is a true zero-copy borrow into the original +// byte slice at the given offset. +fn assert_mut_borrows_at(borrow: &mut T, base_ptr: *mut u8, offset: usize) { + let ptr = borrow as *mut T as *mut u8; + let expected = unsafe { base_ptr.add(offset) }; + assert_eq!(ptr, expected); +} + +fn overwrite_tail(bytes: &mut [u8], stake_flags: u8, padding: [u8; 3]) -> [u8; 4] { + bytes[FLAGS_OFF] = stake_flags; + bytes[PADDING_OFF..LAYOUT_LEN].copy_from_slice(&padding); + [stake_flags, padding[0], padding[1], padding[2]] +} + +#[test_case(StakeStateV2Tag::Uninitialized)] +#[test_case(StakeStateV2Tag::Initialized)] +#[test_case(StakeStateV2Tag::Stake)] +#[test_case(StakeStateV2Tag::RewardsPool)] +fn variants_match_tag(tag: StakeStateV2Tag) { + let mut data = empty_state_bytes(tag); + let base_ptr = data.as_mut_ptr(); + + let mut writer = StakeStateV2::from_bytes_mut(&mut data).unwrap(); + let view = writer.view_mut().unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2ViewMut::Uninitialized) => {} + (StakeStateV2Tag::RewardsPool, StakeStateV2ViewMut::RewardsPool) => {} + (StakeStateV2Tag::Initialized, StakeStateV2ViewMut::Initialized(meta)) => { + assert_mut_borrows_at(meta, base_ptr, META_OFF); + } + (StakeStateV2Tag::Stake, StakeStateV2ViewMut::Stake { meta, stake }) => { + assert_mut_borrows_at(meta, base_ptr, META_OFF); + assert_mut_borrows_at(stake, base_ptr, STAKE_OFF); + } + _ => panic!("unexpected view_mut variant for tag {tag:?}"), + } +} + +#[test_case(false; "aligned")] +#[test_case(true; "unaligned")] +fn initialized_updates_preserve_tail(is_unaligned: bool) { + let legacy_meta = LegacyMeta { + rent_exempt_reserve: 1, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([1u8; 32]), + withdrawer: Pubkey::new_from_array([2u8; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: 3, + epoch: 4, + custodian: Pubkey::new_from_array([5u8; 32]), + }, + }; + let legacy_state = LegacyStakeStateV2::Initialized(legacy_meta); + let aligned = serialize_legacy(&legacy_state); + + let new_rent: u64 = 12302652060662169617; + let new_custodian = [66u8; 32]; + + // Test both aligned and unaligned memory access to ensure POD types handle misalignment + let offset = if is_unaligned { 1 } else { 0 }; + let mut buffer = vec![0u8; offset + LAYOUT_LEN]; + buffer[offset..offset + LAYOUT_LEN].copy_from_slice(&aligned); + let tail_before = overwrite_tail( + &mut buffer[offset..offset + LAYOUT_LEN], + 222, + [173, 190, 239], + ); + + let slice = &mut buffer[offset..offset + LAYOUT_LEN]; + let base_ptr = slice.as_mut_ptr(); + + let mut writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + let view = writer.view_mut().unwrap(); + let StakeStateV2ViewMut::Initialized(meta) = view else { + panic!("expected Initialized"); + }; + + // Verify mutable view borrows directly into the buffer + assert_mut_borrows_at(meta, base_ptr, META_OFF); + + // Mutate fields through the view + meta.rent_exempt_reserve.set(new_rent); + meta.lockup.custodian.0 = new_custodian; + + // Tail bytes (stake_flags + padding) must be untouched by view_mut operations + let layout_bytes = &buffer[offset..offset + LAYOUT_LEN]; + assert_eq!(&layout_bytes[FLAGS_OFF..LAYOUT_LEN], &tail_before); + + // Read-only view validates the updates + let view = StakeStateV2::from_bytes(layout_bytes).unwrap(); + let StakeStateV2View::Initialized(meta) = view else { + panic!("expected Initialized"); + }; + assert_eq!(meta.rent_exempt_reserve.get(), new_rent); + assert_eq!(meta.lockup.custodian.as_bytes(), &new_custodian); + + // Legacy bincode decode still works + let decoded = deserialize_legacy(layout_bytes); + let LegacyStakeStateV2::Initialized(decoded_meta) = decoded else { + panic!("expected legacy Initialized"); + }; + assert_eq!(decoded_meta.rent_exempt_reserve, new_rent); + assert_eq!( + decoded_meta.lockup.custodian, + Pubkey::new_from_array(new_custodian) + ); +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + #[test] + fn prop_unaligned_variant_matches_and_borrows( + mut buffer in any::<[u8; 201]>(), + tag in arb_valid_tag(), + ) { + // Make an unaligned 200-byte window + let unaligned = &mut buffer[1..1 + LAYOUT_LEN]; + write_tag(unaligned, tag); + let base_ptr = unaligned.as_mut_ptr(); + + let mut writer = StakeStateV2::from_bytes_mut(unaligned).unwrap(); + let view = writer.view_mut().unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2ViewMut::Uninitialized) => {} + (StakeStateV2Tag::RewardsPool, StakeStateV2ViewMut::RewardsPool) => {} + (StakeStateV2Tag::Initialized, StakeStateV2ViewMut::Initialized(meta)) => { + assert_mut_borrows_at(meta, base_ptr, META_OFF); + } + (StakeStateV2Tag::Stake, StakeStateV2ViewMut::Stake { meta, stake }) => { + assert_mut_borrows_at(meta, base_ptr, META_OFF); + assert_mut_borrows_at(stake, base_ptr, STAKE_OFF); + } + _ => prop_assert!(false, "tag/view mismatch (unaligned)"), + } + } + + #[test] + fn prop_borrowless_variants_noop( + mut base in any::<[u8; 200]>(), + is_rewards_pool in any::(), + unaligned in any::(), + trailing_len in 0usize..64usize, + trailing_byte in any::(), + ) { + let tag = if is_rewards_pool { StakeStateV2Tag::RewardsPool } else { StakeStateV2Tag::Uninitialized }; + + // Ensure the tag is valid for parsing. + write_tag(&mut base, tag); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(trailing_byte); + + let expected = buffer.clone(); + + { + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let mut writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + let view = writer.view_mut().unwrap(); + match (tag, view) { + (StakeStateV2Tag::Uninitialized, StakeStateV2ViewMut::Uninitialized) => {} + (StakeStateV2Tag::RewardsPool, StakeStateV2ViewMut::RewardsPool) => {} + _ => prop_assert!(false, "unexpected view_mut variant for tag {tag:?}"), + } + } + + prop_assert_eq!(buffer, expected); + } + + #[test] + fn prop_stake_updates_preserve_untouched_bytes( + legacy_meta in arb_legacy_meta(), + legacy_stake in arb_legacy_stake(), + raw_flags in any::(), + raw_padding in any::<[u8; 3]>(), + new_rent_exempt_reserve in any::(), + new_credits_observed in any::(), + new_stake_amount in any::(), + unaligned in any::(), + trailing_len in 0usize..64usize, + ) { + let reserved_bytes = warmup_reserved_bytes_from_legacy_rate(legacy_stake.delegation.warmup_cooldown_rate); + + let legacy_state = LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, LegacyStakeFlags::empty()); + let base = serialize_legacy(&legacy_state); + prop_assert_eq!(base.len(), LAYOUT_LEN); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![0u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..].fill(126); + + // Make tail arbitrary and ensure we preserve it + buffer[start + FLAGS_OFF] = raw_flags; + buffer[start + PADDING_OFF..start + LAYOUT_LEN].copy_from_slice(&raw_padding); + + let before_layout = buffer[start..start + LAYOUT_LEN].to_vec(); + let trailing_before = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let mut writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + let view = writer.view_mut().unwrap(); + let StakeStateV2ViewMut::Stake { meta, stake } = view else { + prop_assert!(false, "expected Stake"); + return Ok(()); + }; + + // Reserved bytes must not change. + prop_assert_eq!(stake.delegation._reserved, reserved_bytes); + + meta.rent_exempt_reserve.set(new_rent_exempt_reserve); + stake.credits_observed.set(new_credits_observed); + stake.delegation.stake.set(new_stake_amount); + + prop_assert_eq!(stake.delegation._reserved, reserved_bytes); + + // Trailing bytes beyond the 200-byte layout must not be modified + prop_assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + trailing_before.as_slice() + ); + // Tail bytes must remain untouched + prop_assert_eq!(buffer[start + FLAGS_OFF], before_layout[FLAGS_OFF]); + prop_assert_eq!( + &buffer[start + PADDING_OFF..start + LAYOUT_LEN], + &before_layout[PADDING_OFF..LAYOUT_LEN] + ); + + // Only specific byte ranges should have changed + let allowed_ranges = [ + (META_OFF, META_OFF + 8), + (STAKE_OFF + 32, STAKE_OFF + 32 + 8), + (STAKE_OFF + size_of::(), STAKE_OFF + size_of::() + 8), + ]; + + let after_layout = &buffer[start..start + LAYOUT_LEN]; + + for i in 0..LAYOUT_LEN { + if allowed_ranges + .iter() + .any(|(start, end)| i >= *start && i < *end) + { + continue; + } + prop_assert_eq!(after_layout[i], before_layout[i]); + } + + // Read-only view sees the updates + let view = StakeStateV2::from_bytes(after_layout).unwrap(); + let StakeStateV2View::Stake { meta, stake } = view else { + prop_assert!(false, "expected Stake"); + return Ok(()); + }; + prop_assert_eq!(meta.rent_exempt_reserve.get(), new_rent_exempt_reserve); + prop_assert_eq!(stake.credits_observed.get(), new_credits_observed); + prop_assert_eq!(stake.delegation.stake.get(), new_stake_amount); + + // Legacy decode sees the updates and flags/padding are preserved + let decoded_after = deserialize_legacy(after_layout); + let decoded_before = deserialize_legacy(&before_layout); + + let (LegacyStakeStateV2::Stake(_, stake_a, flags_a), + LegacyStakeStateV2::Stake(_, stake_b, flags_b)) = (decoded_after, decoded_before) + else { + prop_assert!(false, "expected legacy Stake"); + return Ok(()); + }; + + prop_assert_eq!(flags_a, flags_b); + let warmup_a = warmup_reserved_bytes_from_legacy_rate(stake_a.delegation.warmup_cooldown_rate); + let warmup_b = warmup_reserved_bytes_from_legacy_rate(stake_b.delegation.warmup_cooldown_rate); + prop_assert_eq!(warmup_a, warmup_b); + } +} diff --git a/pinocchio/interface/tests/writer.rs b/pinocchio/interface/tests/writer.rs new file mode 100644 index 00000000..a56fc07b --- /dev/null +++ b/pinocchio/interface/tests/writer.rs @@ -0,0 +1,862 @@ +#![allow(clippy::arithmetic_side_effects)] +#![allow(deprecated)] + +mod helpers; + +use { + helpers::*, + p_stake_interface::{ + error::StakeStateError, + state::{ + Authorized, Delegation, Lockup, Meta, PodAddress, PodI64, PodU64, Stake, StakeStateV2, + StakeStateV2Layout, StakeStateV2Tag, StakeStateV2View, StakeStateV2ViewMut, + }, + }, + proptest::prelude::*, + solana_pubkey::Pubkey, + solana_stake_interface::{ + stake_flags::StakeFlags as LegacyStakeFlags, + state::{ + Authorized as LegacyAuthorized, Delegation as LegacyDelegation, Lockup as LegacyLockup, + Meta as LegacyMeta, Stake as LegacyStake, StakeStateV2 as LegacyStakeStateV2, + }, + }, + test_case::test_case, + wincode::ZeroCopy, +}; + +fn assert_tail_zeroed(layout_bytes: &[u8]) { + assert_eq!(layout_bytes[FLAGS_OFF], 0); + assert_eq!(&layout_bytes[PADDING_OFF..LAYOUT_LEN], &[0u8; 3]); +} + +fn assert_tail(layout_bytes: &[u8], flags: u8, padding: [u8; 3]) { + assert_eq!(layout_bytes[FLAGS_OFF], flags); + assert_eq!(&layout_bytes[PADDING_OFF..LAYOUT_LEN], &padding); +} + +#[test] +fn short_buffer_eof() { + let mut data = vec![0u8; LAYOUT_LEN - 1]; + let err = StakeStateV2::from_bytes_mut(&mut data).unwrap_err(); + assert!(matches!(err, StakeStateError::UnexpectedEof)); +} + +#[test] +fn invalid_tag_err() { + let mut data = [0u8; LAYOUT_LEN]; + data[0..4].copy_from_slice(&999u32.to_le_bytes()); + let err = StakeStateV2::from_bytes_mut(&mut data).unwrap_err(); + assert!(matches!(err, StakeStateError::InvalidTag(999))); +} + +#[test] +fn trailing_bytes_untouched() { + let mut data = empty_state_bytes(StakeStateV2Tag::Uninitialized).to_vec(); + data.extend_from_slice(&[171; 64]); + + let expected = data.clone(); + + let writer = StakeStateV2::from_bytes_mut(&mut data).unwrap(); + let view = writer.view().unwrap(); + assert!(matches!(view, StakeStateV2View::Uninitialized)); + assert_eq!(data, expected); +} + +#[test] +fn unaligned_slice_noop() { + let mut buffer = vec![238u8; LAYOUT_LEN + 1]; + write_tag(&mut buffer[1..1 + TAG_LEN], StakeStateV2Tag::Uninitialized); + + let expected = buffer.clone(); + let unaligned = &mut buffer[1..1 + LAYOUT_LEN]; + + let writer = StakeStateV2::from_bytes_mut(unaligned).unwrap(); + let view = writer.view().unwrap(); + assert!(matches!(view, StakeStateV2View::Uninitialized)); + assert_eq!(buffer, expected); +} + +#[test] +fn legacy_bytes_unchanged() { + let legacy_initialized = LegacyStakeStateV2::Initialized(LegacyMeta { + rent_exempt_reserve: u64::MAX, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([17; 32]), + withdrawer: Pubkey::new_from_array([34; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: i64::MIN + 1, + epoch: u64::MAX, + custodian: Pubkey::new_from_array([51; 32]), + }, + }); + + let legacy_flags = LegacyStakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + let legacy_stake = LegacyStakeStateV2::Stake( + LegacyMeta { + rent_exempt_reserve: 7, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([68; 32]), + withdrawer: Pubkey::new_from_array([85; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -9, + epoch: 123, + custodian: Pubkey::new_from_array([102; 32]), + }, + }, + LegacyStake { + delegation: LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([119; 32]), + stake: 999, + activation_epoch: 1, + deactivation_epoch: 2, + warmup_cooldown_rate: 0.5, + }, + credits_observed: 88, + }, + legacy_flags, + ); + + let variants = [ + LegacyStakeStateV2::Uninitialized, + legacy_initialized, + legacy_stake, + LegacyStakeStateV2::RewardsPool, + ]; + + for legacy_state in variants { + let mut data = serialize_legacy(&legacy_state); + + data[FLAGS_OFF] = 165; + data[PADDING_OFF..LAYOUT_LEN].copy_from_slice(&[222, 173, 190]); + data[STAKE_OFF..STAKE_OFF + 8].copy_from_slice(&[204; 8]); + + let expected = data.clone(); + StakeStateV2::from_bytes_mut(&mut data).unwrap(); + assert_eq!(data, expected); + } +} + +#[test_case(false, 0; "aligned_no_trailing")] +#[test_case(false, 64; "aligned_trailing")] +#[test_case(true, 0; "unaligned_no_trailing")] +#[test_case(true, 64; "unaligned_trailing")] +fn invalid_transitions_err(unaligned: bool, trailing_len: usize) { + #[derive(Clone, Copy)] + enum Op { + IntoInitialized, + IntoStake, + } + + let tags = [ + StakeStateV2Tag::Uninitialized, + StakeStateV2Tag::Initialized, + StakeStateV2Tag::Stake, + StakeStateV2Tag::RewardsPool, + ]; + + let start = if unaligned { 1 } else { 0 }; + + for from in tags { + for &op in &[Op::IntoInitialized, Op::IntoStake] { + let mut base = empty_state_bytes(from); + base[FLAGS_OFF..LAYOUT_LEN].copy_from_slice(&[250, 251, 252, 253]); + + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(123); + + let expected_layout = buffer[start..start + LAYOUT_LEN].to_vec(); + let expected_trailing = + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let result = { + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + match op { + Op::IntoInitialized => writer.into_initialized(Meta::default()).map(|_| ()), + Op::IntoStake => writer + .into_stake(Meta::default(), Stake::default()) + .map(|_| ()), + } + }; + + let allowed = matches!( + (from, op), + (StakeStateV2Tag::Uninitialized, Op::IntoInitialized) + | (StakeStateV2Tag::Initialized, Op::IntoStake) + | (StakeStateV2Tag::Stake, Op::IntoStake) + ); + + if allowed { + assert!(result.is_ok()); + } else { + let expected_to = match op { + Op::IntoInitialized => StakeStateV2Tag::Initialized, + Op::IntoStake => StakeStateV2Tag::Stake, + }; + let err = result.unwrap_err(); + assert!(matches!( + err, + StakeStateError::InvalidTransition { from: f, to: t } + if f == from && t == expected_to + )); + assert_eq!( + &buffer[start..start + LAYOUT_LEN], + expected_layout.as_slice() + ); + assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + expected_trailing.as_slice() + ); + } + } + } +} + +#[test_case(false, 0; "aligned_no_trailing")] +#[test_case(false, 64; "aligned_trailing")] +#[test_case(true, 0; "unaligned_no_trailing")] +#[test_case(true, 64; "unaligned_trailing")] +fn uninitialized_to_initialized(unaligned: bool, trailing_len: usize) { + let mut base = [170u8; LAYOUT_LEN]; + write_tag(&mut base, StakeStateV2Tag::Uninitialized); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(124); + + let expected_trailing = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let meta = Meta { + rent_exempt_reserve: PodU64::from_primitive(42), + authorized: Authorized { + staker: PodAddress::from_bytes([1; 32]), + withdrawer: PodAddress::from_bytes([2; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(-3), + epoch: PodU64::from_primitive(9), + custodian: PodAddress::from_bytes([3; 32]), + }, + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_initialized(meta).unwrap(); + + assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + expected_trailing.as_slice() + ); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + + assert_eq!( + StakeStateV2Tag::from_bytes(layout_bytes).unwrap(), + StakeStateV2Tag::Initialized + ); + assert!(layout_bytes[STAKE_OFF..FLAGS_OFF].iter().all(|b| *b == 0)); + + assert_tail_zeroed(layout_bytes); + + let view = StakeStateV2::from_bytes(layout_bytes).unwrap(); + let StakeStateV2View::Initialized(view_meta) = view else { + panic!("expected Initialized"); + }; + + let expected_legacy = LegacyMeta { + rent_exempt_reserve: 42, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([1; 32]), + withdrawer: Pubkey::new_from_array([2; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -3, + epoch: 9, + custodian: Pubkey::new_from_array([3; 32]), + }, + }; + assert_meta_compat(view_meta, &expected_legacy); + + let old = deserialize_legacy(layout_bytes); + let LegacyStakeStateV2::Initialized(legacy_meta) = old else { + panic!("expected legacy Initialized"); + }; + assert_eq!(legacy_meta, expected_legacy); +} + +#[test_case(false, 0; "aligned_no_trailing")] +#[test_case(false, 64; "aligned_trailing")] +#[test_case(true, 0; "unaligned_no_trailing")] +#[test_case(true, 64; "unaligned_trailing")] +fn initialized_to_stake(unaligned: bool, trailing_len: usize) { + let mut base = empty_state_bytes(StakeStateV2Tag::Initialized); + base[FLAGS_OFF..LAYOUT_LEN].copy_from_slice(&[170, 187, 204, 221]); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(125); + + let expected_trailing = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let meta = Meta { + rent_exempt_reserve: PodU64::from_primitive(7), + authorized: Authorized { + staker: PodAddress::from_bytes([4; 32]), + withdrawer: PodAddress::from_bytes([5; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(6), + epoch: PodU64::from_primitive(8), + custodian: PodAddress::from_bytes([6; 32]), + }, + }; + let warmup_rate: f64 = 1.0; + let stake = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes([7; 32]), + stake: PodU64::from_primitive(123), + activation_epoch: PodU64::from_primitive(2), + deactivation_epoch: PodU64::from_primitive(3), + _reserved: warmup_rate.to_bits().to_le_bytes(), + }, + credits_observed: PodU64::from_primitive(44), + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_stake(meta, stake).unwrap(); + + assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + expected_trailing.as_slice() + ); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + assert_eq!( + StakeStateV2Tag::from_bytes(layout_bytes).unwrap(), + StakeStateV2Tag::Stake + ); + + assert_tail_zeroed(layout_bytes); + + let view = StakeStateV2::from_bytes(layout_bytes).unwrap(); + let StakeStateV2View::Stake { meta, stake, .. } = view else { + panic!("expected Stake"); + }; + + // Expected legacy values matching inputs above + let expected_legacy = LegacyMeta { + rent_exempt_reserve: 7, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([4; 32]), + withdrawer: Pubkey::new_from_array([5; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: 6, + epoch: 8, + custodian: Pubkey::new_from_array([6; 32]), + }, + }; + let expected_legacy_stake = LegacyStake { + delegation: LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([7; 32]), + stake: 123, + activation_epoch: 2, + deactivation_epoch: 3, + warmup_cooldown_rate: warmup_rate, + }, + credits_observed: 44, + }; + + assert_meta_compat(meta, &expected_legacy); + assert_stake_compat(stake, &expected_legacy_stake); + + let old = deserialize_legacy(layout_bytes); + let LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, legacy_flags) = old else { + panic!("expected legacy Stake"); + }; + assert_eq!(legacy_meta, expected_legacy); + assert_eq!(legacy_stake, expected_legacy_stake); + assert_eq!(legacy_flags, LegacyStakeFlags::empty()); +} + +#[test_case(false, 0; "aligned_no_trailing")] +#[test_case(false, 64; "aligned_trailing")] +#[test_case(true, 0; "unaligned_no_trailing")] +#[test_case(true, 64; "unaligned_trailing")] +fn initialized_to_stake_view_mut_works(unaligned: bool, trailing_len: usize) { + // Start from Initialized with a nonzero tail to ensure the transition zeroes it, + // and then ensure subsequent view_mut does not modify it + let mut base = empty_state_bytes(StakeStateV2Tag::Initialized); + base[FLAGS_OFF..LAYOUT_LEN].copy_from_slice(&[170, 187, 204, 221]); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(99); + + let trailing_before = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let meta = Meta { + rent_exempt_reserve: PodU64::from_primitive(7), + authorized: Authorized { + staker: PodAddress::from_bytes([4; 32]), + withdrawer: PodAddress::from_bytes([5; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(6), + epoch: PodU64::from_primitive(8), + custodian: PodAddress::from_bytes([6; 32]), + }, + }; + let warmup_rate: f64 = 1.0; + let stake = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes([7; 32]), + stake: PodU64::from_primitive(123), + activation_epoch: PodU64::from_primitive(2), + deactivation_epoch: PodU64::from_primitive(3), + _reserved: warmup_rate.to_bits().to_le_bytes(), + }, + credits_observed: PodU64::from_primitive(44), + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + let mut writer = writer.into_stake(meta, stake).unwrap(); + + // Mutate via the returned writer's view_mut + let view = writer.view_mut().unwrap(); + let StakeStateV2ViewMut::Stake { meta, stake } = view else { + panic!("expected Stake"); + }; + meta.rent_exempt_reserve.set(424242); + stake.credits_observed.set(7777); + + // Trailing bytes must remain untouched + assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + trailing_before.as_slice() + ); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + + // Tail should have been zeroed by Initialized -> Stake and view_mut should not change it + assert_tail_zeroed(layout_bytes); + + let view = StakeStateV2::from_bytes(layout_bytes).unwrap(); + let StakeStateV2View::Stake { + meta: view_meta, + stake: view_stake, + } = view + else { + panic!("expected Stake"); + }; + assert_eq!(view_meta.rent_exempt_reserve.get(), 424242); + assert_eq!(view_stake.credits_observed.get(), 7777); +} + +#[test] +fn chained_transitions_uninitialized_to_initialized_to_stake() { + let meta1 = Meta { + rent_exempt_reserve: PodU64::from_primitive(4), + authorized: Authorized { + staker: PodAddress::from_bytes([1; 32]), + withdrawer: PodAddress::from_bytes([2; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(-5), + epoch: PodU64::from_primitive(6), + custodian: PodAddress::from_bytes([3; 32]), + }, + }; + let meta2 = Meta { + rent_exempt_reserve: PodU64::from_primitive(10), + authorized: Authorized { + staker: PodAddress::from_bytes([7; 32]), + withdrawer: PodAddress::from_bytes([8; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(-11), + epoch: PodU64::from_primitive(12), + custodian: PodAddress::from_bytes([9; 32]), + }, + }; + let warmup_rate: f64 = 0.75; + let stake2 = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes([13; 32]), + stake: PodU64::from_primitive(14), + activation_epoch: PodU64::from_primitive(15), + deactivation_epoch: PodU64::from_primitive(16), + _reserved: warmup_rate.to_bits().to_le_bytes(), + }, + credits_observed: PodU64::from_primitive(17), + }; + + let start = 1; + let trailing_len = 32usize; + let end = start + LAYOUT_LEN + trailing_len; + let mut buffer = vec![0xAB; start + LAYOUT_LEN + trailing_len]; + buffer[start + STAKE_OFF..start + FLAGS_OFF].fill(0xCD); + buffer[start + FLAGS_OFF..start + LAYOUT_LEN].copy_from_slice(&[1, 2, 3, 4]); + + write_tag(&mut buffer[start..end], StakeStateV2Tag::Uninitialized); + + buffer[start + LAYOUT_LEN..end].fill(0x7E); + let trailing_before = buffer[start + LAYOUT_LEN..end].to_vec(); + + let slice = &mut buffer[start..end]; + let writer = StakeStateV2::from_bytes_mut(slice) + .unwrap() + .into_initialized(meta1) + .unwrap() + .into_stake(meta2, stake2) + .unwrap(); + + let view = writer.view().unwrap(); + let StakeStateV2View::Stake { meta, stake } = view else { + panic!("expected Stake"); + }; + + let expected_legacy_meta2 = LegacyMeta { + rent_exempt_reserve: 10, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([7; 32]), + withdrawer: Pubkey::new_from_array([8; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: -11, + epoch: 12, + custodian: Pubkey::new_from_array([9; 32]), + }, + }; + let expected_legacy_stake2 = LegacyStake { + delegation: LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([13; 32]), + stake: 14, + activation_epoch: 15, + deactivation_epoch: 16, + warmup_cooldown_rate: warmup_rate, + }, + credits_observed: 17, + }; + assert_meta_compat(meta, &expected_legacy_meta2); + assert_stake_compat(stake, &expected_legacy_stake2); + + assert_eq!(&buffer[start + LAYOUT_LEN..end], trailing_before.as_slice()); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + assert_tail_zeroed(layout_bytes); + + let layout = StakeStateV2Layout::from_bytes(layout_bytes).unwrap(); + assert_eq!(layout.tag.get(), StakeStateV2Tag::Stake as u32); + assert_eq!(layout.meta, meta2); + assert_eq!(layout.stake, stake2); + assert_eq!(layout.stake_flags, 0); + assert_eq!(layout.padding, [0u8; 3]); +} + +#[test_case(false, 0; "aligned_no_trailing")] +#[test_case(false, 64; "aligned_trailing")] +#[test_case(true, 0; "unaligned_no_trailing")] +#[test_case(true, 64; "unaligned_trailing")] +fn stake_to_stake_preserves_tail(unaligned: bool, trailing_len: usize) { + let preserved_flags = 1; + let preserved_padding = [222, 173, 190]; + + let mut base = empty_state_bytes(StakeStateV2Tag::Stake); + base[FLAGS_OFF] = preserved_flags; + base[PADDING_OFF..LAYOUT_LEN].copy_from_slice(&preserved_padding); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(126); + + let expected_trailing = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let meta = Meta { + rent_exempt_reserve: PodU64::from_primitive(1), + authorized: Authorized { + staker: PodAddress::from_bytes([9; 32]), + withdrawer: PodAddress::from_bytes([8; 32]), + }, + lockup: Lockup { + unix_timestamp: PodI64::from_primitive(7), + epoch: PodU64::from_primitive(11), + custodian: PodAddress::from_bytes([7; 32]), + }, + }; + let warmup_rate: f64 = 0.5; + let stake = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes([6; 32]), + stake: PodU64::from_primitive(55), + activation_epoch: PodU64::from_primitive(1), + deactivation_epoch: PodU64::from_primitive(9), + _reserved: warmup_rate.to_bits().to_le_bytes(), + }, + credits_observed: PodU64::from_primitive(99), + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_stake(meta, stake).unwrap(); + + assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + expected_trailing.as_slice() + ); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + assert_eq!( + StakeStateV2Tag::from_bytes(layout_bytes).unwrap(), + StakeStateV2Tag::Stake + ); + + assert_tail(layout_bytes, preserved_flags, preserved_padding); + + let old = deserialize_legacy(layout_bytes); + let LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, legacy_flags) = old else { + panic!("expected legacy Stake"); + }; + assert_eq!( + legacy_meta, + LegacyMeta { + rent_exempt_reserve: 1, + authorized: LegacyAuthorized { + staker: Pubkey::new_from_array([9; 32]), + withdrawer: Pubkey::new_from_array([8; 32]), + }, + lockup: LegacyLockup { + unix_timestamp: 7, + epoch: 11, + custodian: Pubkey::new_from_array([7; 32]), + }, + } + ); + assert_eq!( + legacy_stake, + LegacyStake { + delegation: LegacyDelegation { + voter_pubkey: Pubkey::new_from_array([6; 32]), + stake: 55, + activation_epoch: 1, + deactivation_epoch: 9, + warmup_cooldown_rate: warmup_rate, + }, + credits_observed: 99, + } + ); + let expected_flags = LegacyStakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + assert_eq!(legacy_flags, expected_flags); +} + +// ----------------------------- property tests -------------------------------- + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + #[test] + fn prop_uninitialized_to_initialized_zeroes_stake_and_tail( + legacy_meta in arb_legacy_meta(), + unaligned in any::(), + trailing_len in 0usize..64usize, + fill in any::(), + ) { + let meta = meta_from_legacy(&legacy_meta); + + let start = if unaligned { 1 } else { 0 }; + let end = start + LAYOUT_LEN + trailing_len; + let mut buffer = vec![fill; start + LAYOUT_LEN + trailing_len]; + buffer[start + STAKE_OFF..start + FLAGS_OFF].fill(0xAB); + buffer[start + FLAGS_OFF..start + LAYOUT_LEN].copy_from_slice(&[1, 2, 3, 4]); + + write_tag(&mut buffer[start..end], StakeStateV2Tag::Uninitialized); + + buffer[start + LAYOUT_LEN..end].fill(0x7E); + let trailing_before = buffer[start + LAYOUT_LEN..end].to_vec(); + + let slice = &mut buffer[start..end]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_initialized(meta).unwrap(); + + prop_assert_eq!(&buffer[start + LAYOUT_LEN..end], trailing_before.as_slice()); + + let layout_bytes = &buffer[start..start + LAYOUT_LEN]; + prop_assert_eq!( + StakeStateV2Tag::from_bytes(layout_bytes).unwrap(), + StakeStateV2Tag::Initialized + ); + prop_assert!(layout_bytes[STAKE_OFF..FLAGS_OFF].iter().all(|b| *b == 0)); + prop_assert_eq!(layout_bytes[FLAGS_OFF], 0); + prop_assert_eq!(&layout_bytes[PADDING_OFF..LAYOUT_LEN], &[0u8; 3]); + + let layout = StakeStateV2Layout::from_bytes(layout_bytes).unwrap(); + prop_assert_eq!(layout.tag.get(), StakeStateV2Tag::Initialized as u32); + prop_assert_eq!(layout.meta, meta); + prop_assert_eq!(layout.stake, Stake::default()); + prop_assert_eq!(layout.stake_flags, 0); + prop_assert_eq!(layout.padding, [0u8; 3]); + } + + // Stake -> Stake transition must preserve arbitrary stake_flags+padding AND preserve trailing bytes beyond 200 + #[test] + fn prop_stake_to_stake_preserves_flags( + legacy_meta in arb_legacy_meta(), + legacy_stake in arb_legacy_stake(), + new_meta in arb_legacy_meta(), + new_legacy_stake in arb_legacy_stake(), + arbitrary_flags in any::(), + arbitrary_padding in any::<[u8; 3]>(), + unaligned in any::(), + trailing_len in 0usize..64usize, + ) { + let new_reserved = warmup_reserved_bytes_from_legacy_rate(new_legacy_stake.delegation.warmup_cooldown_rate); + + let legacy_state = LegacyStakeStateV2::Stake(legacy_meta, legacy_stake, LegacyStakeFlags::empty()); + let base = serialize_legacy(&legacy_state); + prop_assert_eq!(base.len(), 200); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].fill(126); + + buffer[start + FLAGS_OFF] = arbitrary_flags; + buffer[start + PADDING_OFF..start + LAYOUT_LEN].copy_from_slice(&arbitrary_padding); + + let trailing_before = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let meta = meta_from_legacy(&new_meta); + let stake = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes(new_legacy_stake.delegation.voter_pubkey.to_bytes()), + stake: PodU64::from_primitive(new_legacy_stake.delegation.stake), + activation_epoch: PodU64::from_primitive(new_legacy_stake.delegation.activation_epoch), + deactivation_epoch: PodU64::from_primitive(new_legacy_stake.delegation.deactivation_epoch), + _reserved: new_reserved, + }, + credits_observed: PodU64::from_primitive(new_legacy_stake.credits_observed), + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_stake(meta, stake).unwrap(); + + prop_assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + trailing_before.as_slice() + ); + + prop_assert_eq!(buffer[start + FLAGS_OFF], arbitrary_flags); + prop_assert_eq!( + &buffer[start + PADDING_OFF..start + LAYOUT_LEN], + arbitrary_padding.as_slice() + ); + + let view = StakeStateV2::from_bytes(&buffer[start..start + LAYOUT_LEN]).unwrap(); + let StakeStateV2View::Stake { meta: view_meta, stake: view_stake } = view else { + prop_assert!(false, "expected Stake after into_stake"); + return Ok(()); + }; + + // Verify that the written fields match the inputs + assert_meta_compat(view_meta, &new_meta); + assert_stake_compat(view_stake, &new_legacy_stake); + + let decoded = deserialize_legacy(&buffer[start..start + LAYOUT_LEN]); + let LegacyStakeStateV2::Stake(decoded_meta, decoded_stake, decoded_flags) = decoded else { + prop_assert!(false, "expected legacy Stake after into_stake"); + return Ok(()); + }; + + // Verify legacy decode matches inputs + prop_assert_eq!(decoded_meta, new_meta); + + prop_assert_eq!(decoded_stake.credits_observed, new_legacy_stake.credits_observed); + prop_assert_eq!(decoded_stake.delegation.voter_pubkey, new_legacy_stake.delegation.voter_pubkey); + prop_assert_eq!(decoded_stake.delegation.stake, new_legacy_stake.delegation.stake); + prop_assert_eq!(decoded_stake.delegation.activation_epoch, new_legacy_stake.delegation.activation_epoch); + prop_assert_eq!(decoded_stake.delegation.deactivation_epoch, new_legacy_stake.delegation.deactivation_epoch); + + let decoded_bits = decoded_stake.delegation.warmup_cooldown_rate.to_bits(); + let expected_bits = new_legacy_stake.delegation.warmup_cooldown_rate.to_bits(); + prop_assert_eq!(decoded_bits, expected_bits); + + prop_assert_eq!(stake_flags_byte(&decoded_flags), arbitrary_flags); + } + + // Initialized -> Stake transition must always zero out flag/padding bytes + #[test] + fn prop_initialized_to_stake_zeroes_tail( + legacy_meta in arb_legacy_meta(), + new_meta in arb_legacy_meta(), + new_legacy_stake in arb_legacy_stake(), + mut dirty_tail in any::<[u8; 4]>(), + unaligned in any::(), + trailing_len in 0usize..64usize, + ) { + let legacy_state = LegacyStakeStateV2::Initialized(legacy_meta); + let base = serialize_legacy(&legacy_state); + prop_assert_eq!(base.len(), 200); + + let start = if unaligned { 1 } else { 0 }; + let mut buffer = vec![238u8; start + LAYOUT_LEN + trailing_len]; + buffer[start..start + LAYOUT_LEN].copy_from_slice(&base); + buffer[start + LAYOUT_LEN..].fill(126); + + // Corrupt the tail region (flags + padding) to ensure it gets cleared + // Make sure it's actually non-zero for the test to be meaningful + if dirty_tail == [0, 0, 0, 0] { + dirty_tail = [1, 2, 3, 4]; + } + buffer[start + FLAGS_OFF..start + LAYOUT_LEN].copy_from_slice(&dirty_tail); + + let trailing_before = buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len].to_vec(); + + let new_reserved = warmup_reserved_bytes_from_legacy_rate(new_legacy_stake.delegation.warmup_cooldown_rate); + + let meta = meta_from_legacy(&new_meta); + let stake = Stake { + delegation: Delegation { + voter_pubkey: PodAddress::from_bytes(new_legacy_stake.delegation.voter_pubkey.to_bytes()), + stake: PodU64::from_primitive(new_legacy_stake.delegation.stake), + activation_epoch: PodU64::from_primitive(new_legacy_stake.delegation.activation_epoch), + deactivation_epoch: PodU64::from_primitive(new_legacy_stake.delegation.deactivation_epoch), + _reserved: new_reserved, + }, + credits_observed: PodU64::from_primitive(new_legacy_stake.credits_observed), + }; + + let slice = &mut buffer[start..start + LAYOUT_LEN + trailing_len]; + let writer = StakeStateV2::from_bytes_mut(slice).unwrap(); + writer.into_stake(meta, stake).unwrap(); + + prop_assert_eq!( + &buffer[start + LAYOUT_LEN..start + LAYOUT_LEN + trailing_len], + trailing_before.as_slice() + ); + + // Verify tail is zeroed + prop_assert_eq!(buffer[start + FLAGS_OFF], 0); + prop_assert_eq!(&buffer[start + PADDING_OFF..start + LAYOUT_LEN], &[0u8; 3]); + + // Verify the rest of the data is correct + let view = StakeStateV2::from_bytes(&buffer[start..start + LAYOUT_LEN]).unwrap(); + let StakeStateV2View::Stake { meta: view_meta, stake: view_stake } = view else { + prop_assert!(false, "expected Stake after into_stake"); + return Ok(()); + }; + + assert_meta_compat(view_meta, &new_meta); + assert_stake_compat(view_stake, &new_legacy_stake); + } +} diff --git a/scripts/solana.dic b/scripts/solana.dic index 6c0dd4f2..9f63eb50 100644 --- a/scripts/solana.dic +++ b/scripts/solana.dic @@ -5,6 +5,7 @@ codama config cooldown deallocated +deserialization entrypoint fn IDL @@ -16,8 +17,10 @@ redelegate redelegated redelegation rpc +solana staker struct +structs sysvar sysvars undelegate @@ -25,4 +28,5 @@ undelegated unstake unstaked warmup +wincode withdrawer