diff --git a/.coderabbit.yaml b/.coderabbit.yaml index a3295218..b755b0a3 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -10,10 +10,14 @@ reviews: poem: false request_changes_workflow: true + + auto_review: - enabled: true + enabled: true # Disables automatic reviews on push drafts: true - auto_incremental_review: true + base_branches: + - ".*" + auto_incremental_review: true # Never pause after repeated pushes. auto_pause_after_reviewed_commits: 0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8ec98ed8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.rs linguist-language=Rust +Cargo.toml linguist-language=Rust +Cargo.lock linguist-language=Rust diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ffd1f751..c289ec1f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,9 @@ + + ## What changed diff --git a/.gitignore b/.gitignore index 1798e8ba..8b202a78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -Cargo.lock AGENTS.md +CLAUDE.md diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..2acc2061 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,865 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[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 = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solana-account" +version = "4.3.1" +dependencies = [ + "bincode", + "bitflags", + "serde", + "serde_bytes", + "solana-account", + "solana-account-info", + "solana-clock", + "solana-instruction-error", + "solana-pubkey", + "solana-sdk-ids", + "solana-sysvar", + "thiserror", + "wincode 0.5.5", +] + +[[package]] +name = "solana-account-info" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" +dependencies = [ + "solana-address", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01332a01c0a3098404d55a724c8d9a92aed4a50fe40a7dd0c7a51e29274c14de" +dependencies = [ + "borsh", + "five8", + "five8_const", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-define-syscall 5.2.0", + "solana-program-error", + "solana-sanitize", + "wincode 0.6.1", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085db4906d89324cef2a30840d59eaecf3d4231c560ec7c9f6614a93c652f501" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7708bdb262fd9c257c3a56d4289297b10a3e821b8cba3f7cf42b49c40201ab9" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-define-syscall" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8209ece2bd9f1450e672858ffc0e5c8c786ff6916d2a862b126dd0128f380f" + +[[package]] +name = "solana-epoch-rewards" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0788d74ee15778deecaa15ed1a1e37727ba954f86cbc35225450a1f2b5012969" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1633cfd10cde127f2caf8f12021b4f8e9a425e7e4eea4326e428c422376d6fd" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-program-error", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4bf4f4afb4704212ef1d994ee65bef7293441721639dfd16f0e60996f37be51" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address", + "solana-define-syscall 5.2.0", + "solana-program-error", +] + +[[package]] +name = "solana-hash" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df9b01495ed31100aca97a7f5862d5e19ab1636d60d1a9f02391408dd9dec84" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-instruction" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70cf6ece1070a66d25e68274f338f29664794669c175223940a298645ef3495" +dependencies = [ + "solana-define-syscall 5.2.0", + "solana-instruction-error", + "solana-pubkey", +] + +[[package]] +name = "solana-instruction-error" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e7db78c122d02189619490b1fef04a2a042c389662920617c0e6381fdf8fdd" +dependencies = [ + "num-traits", + "solana-program-error", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5099e736c3c06c451b307a60637d69eb65b3144143ebeed899e2fd1134f4fc35" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[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", +] + +[[package]] +name = "solana-program-error" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" + +[[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 = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f71b7a414de2ced1071f015834e9be581592d013432adbd06d02e4af11eba9" +dependencies = [ + "solana-address", +] + +[[package]] +name = "solana-rent" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc016b348926395ba01f8288cdf5da25fc30f5a0806028b32d5e5b3147b10bf9" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "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", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d6bc909599eea325c9d09f7ff16ef6166c134876ff47a6a122d693d058dad" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4560fde841a6f2001aedc33b74fe99840ee3d56a71fe9b98ee332303b8597900" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stake-history" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c814b4e2570f8c343eb1d4f968ff981bdf518afc3643d070d8e5a28158e85a4b" +dependencies = [ + "serde", + "serde_derive", + "solana-clock", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0214d668b0aab7a64b49e43b7947307985fca4e03d3ef880cd8ee43f52f13a34" +dependencies = [ + "base64", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 5.2.0", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-get-sysvar", + "solana-hash", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-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", + "solana-sdk-ids", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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 = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wincode" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "wincode" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc6339f1ba427bf7ad7c42403b28e524832ba2ddb5eef1bb2cc3b85db6b7b75" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d582d9fc9d813264407a9e16bfa16846ccf15ef032f9bbcd700741bdc00255" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 85ad1f11..16afd214 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +members = ["solana/account"] resolver = "3" [workspace.package] @@ -10,6 +11,45 @@ repository = "https://github.com/magicblock-labs/engine" rust-version = "1.94.1" version = "0.1.0" +[workspace.dependencies] +magic-root-interface = { path = "programs/magic-root-interface" } +magic-root-program = { path = "programs/magic-root-program" } +solana-account = { path = "solana/account" } + +ahash = "0.8.12" +arc-swap = "1.9.1" +bincode = "1.3.3" +bitflags = "2.11.1" +blake3 = "1.8.5" +qualifier_attr = "0.2.2" +rand = "0.8.5" +rustix = { version = "1.1.4" } +serde = "1.0.228" +serde_bytes = "0.11.19" +serde_with = { version = "3.21.0", default-features = false } +snedfile = "0.1" +tar = "0.4.45" +tempfile = "3" +thiserror = "2.0.17" +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] } +wincode = "0.5.1" +zstd = { version = "0.13.3", default-features = false } + +agave-feature-set = { version = "3.1.14", features = ["agave-unstable-api"] } +agave-transaction-view = { version = "3.1.13", features = ["agave-unstable-api"] } +solana-account-info = "3.1.1" +solana-clock = "3.1.0" +solana-compute-budget-instruction = "=4.1.1" +solana-cpi = "3.1.0" +solana-hash = "4.3.0" +solana-instruction-error = "2.3.0" +solana-program-error = "3.0.1" +solana-pubkey = "4.2.0" +solana-sdk-ids = "3.1.0" +solana-sysvar = "4.0.0" +solana-transaction-error = "3.2.0" + + [workspace.lints.rust] missing_docs = "deny" rust_2018_idioms = { level = "warn", priority = -1 } diff --git a/clippy.toml b/clippy.toml index db02f67e..19eebeb6 100644 --- a/clippy.toml +++ b/clippy.toml @@ -19,7 +19,6 @@ max-struct-bools = 2 check-private-items = true disallowed-methods = [ - { path = "std::option::Option::unwrap", reason = "Use expect with context or handle the None case explicitly" }, - { path = "std::result::Result::unwrap", reason = "Use expect with context or propagate the error explicitly" }, - { path = "std::thread::sleep", reason = "Avoid timing-based flakiness in workspace code; isolate retries/backoff behind abstractions" } + { path = "std::thread::sleep", reason = "Avoid timing-based flakiness in workspace code; use event driven logic" }, + { path = "tokio::time::sleep", reason = "Avoid timing-based flakiness in workspace code; use event driven logic" } ] diff --git a/solana/account/Cargo.toml b/solana/account/Cargo.toml index 4e0c0cdf..9bf0208e 100644 --- a/solana/account/Cargo.toml +++ b/solana/account/Cargo.toml @@ -1,40 +1,35 @@ [package] name = "solana-account" -description = "Solana Account type" -documentation = "https://docs.rs/solana-account" -version = "4.3.1" + authors = { workspace = true } -repository = { workspace = true } +description = "Solana Account type" +edition = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = { workspace = true } - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] -all-features = true -rustdoc-args = ["--cfg=docsrs"] +readme = "README.md" +repository = { workspace = true } +version = "4.3.1" [features] bincode = ["dep:bincode", "dep:solana-sysvar", "serde"] -wincode = ["dep:wincode", "solana-pubkey/wincode"] -dev-context-only-utils = ["bincode", "dep:qualifier_attr"] -frozen-abi = [ - "dep:solana-frozen-abi", - "dep:solana-frozen-abi-macro", - "solana-pubkey/frozen-abi", +serde = [ + "bitflags/serde", + "dep:serde", + "dep:serde_bytes", + "serde/derive", + "serde/rc", + "solana-pubkey/serde" ] -serde = ["dep:serde", "dep:serde_bytes", "dep:serde_derive", "solana-pubkey/serde"] +testkit = ["bincode"] +wincode = ["bincode", "dep:wincode", "solana-pubkey/wincode"] [dependencies] bincode = { workspace = true, optional = true } -qualifier_attr = { workspace = true, optional = true } +bitflags = { workspace = true } serde = { workspace = true, optional = true } serde_bytes = { workspace = true, optional = true } -serde_derive = { workspace = true, optional = true } solana-account-info = { workspace = true } solana-clock = { workspace = true } -solana-frozen-abi = { workspace = true, optional = true, features = ["frozen-abi"] } -solana-frozen-abi-macro = { workspace = true, optional = true } solana-instruction-error = { workspace = true } solana-pubkey = { workspace = true } solana-sdk-ids = { workspace = true } @@ -43,5 +38,8 @@ thiserror = { workspace = true } wincode = { workspace = true, features = ["alloc"], optional = true } [dev-dependencies] -solana-account = { path = ".", features = ["dev-context-only-utils"] } +solana-account = { path = ".", features = ["testkit"] } solana-pubkey = { workspace = true, features = ["std"] } + +[lints] +workspace = true diff --git a/solana/account/README.md b/solana/account/README.md new file mode 100644 index 00000000..2c8a76bf --- /dev/null +++ b/solana/account/README.md @@ -0,0 +1,49 @@ +# `solana-account` + +This fork defines the engine's account representation. `Account` is the +fully-owned compatibility form. `AccountSharedData` uses either a heap-owned +`Arc>` or a borrowed view into aligned external storage and records +field-level dirty markers. + +Equality compares core state and data bytes, ignoring storage form and dirty +markers. + +`AccountMode::mutable()` identifies modes intrinsically writable by user +programs. `AccountSharedData::mutable()` also accepts transient and closed +accounts when its mode dirty marker records the lifecycle transition in the +current transaction. +`AccountMode::authoritative()` separately identifies delegated, ephemeral, and +transient state that the engine owns and higher layers retain in persistent +storage. +`AccountSharedData::set_mode()` is the authoritative lifecycle transition +check: read-only and placeholder accounts may enter any mode except transient, +delegated accounts may enter transient, and transient accounts may resolve to +read-only. Ephemeral accounts may close. Reapplying the current mode is a clean +no-op; invalid mode and slot transitions return `AccountPatchError` with their +source and target context without changing the account. + +Slot patches must advance the stored slot. An equal slot is accepted only after +the mode genuinely changed in the same transaction. + +Full-account patch sequences cover non-flag fields, establish the exact data +length, and then write data in bounded chunks. MagicRoot finalization installs +the caller-supplied complete flag value without changing lamports. `StateFlags` +currently contains only `EXECUTABLE`; replacement freshness is enforced by the +caller rather than an account flag. + +## Borrowed layout + +| Part | Position | Contents | +| --- | --- | --- | +| header | start | sequence and image size | +| pubkey | after header | shared account pubkey | +| image A | after pubkey | core state and data | +| image B | after image A | core state and data | + +Borrowed buffers must be 8-byte aligned, match this layout, remain live, and have +unique mutable access for the duration of the borrow. The source may be an mmap, +arena, or test buffer. + +The sequence counter selects the active image. Mutation translates active state +into the shadow image; commit advances the sequence to publish it. Writes that +exceed borrowed capacity promote the account to owned storage. diff --git a/solana/account/src/account.rs b/solana/account/src/account.rs new file mode 100644 index 00000000..38fb014e --- /dev/null +++ b/solana/account/src/account.rs @@ -0,0 +1,260 @@ +use { + crate::{AccountSharedData, ReadableAccount, traits::debug_fmt}, + solana_account_info::AccountInfo, + solana_clock::Epoch, + solana_pubkey::Pubkey, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + }, + std::{cell::RefCell, fmt, rc::Rc}, +}; + +/// An on-chain account with owned data and an explicit rent epoch. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +#[derive(PartialEq, Eq, Clone, Default)] +pub struct Account { + /// Lamports in the account. + pub lamports: u64, + /// Data held in the account. + #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] + pub data: Vec, + /// The program that owns this account. + pub owner: Pubkey, + /// Whether the account contains executable program data. + pub executable: bool, + /// The epoch at which this account next owes rent. + pub rent_epoch: Epoch, +} + +#[cfg(feature = "serde")] +mod account_serialize { + use { + crate::ReadableAccount, + serde::{Serialize, ser::Serializer}, + solana_clock::Epoch, + solana_pubkey::Pubkey, + }; + + #[repr(C)] + #[derive(serde::Serialize)] + #[serde(rename_all = "camelCase")] + /// Serialization shape shared by `Account` and `AccountSharedData`. + struct Account<'a> { + lamports: u64, + #[serde(with = "serde_bytes")] + data: &'a [u8], + owner: &'a Pubkey, + executable: bool, + rent_epoch: Epoch, + } + + /// Serializes any readable account using the canonical `Account` layout. + pub(crate) fn serialize_account( + account: &impl ReadableAccount, + serializer: S, + ) -> Result + where + S: Serializer, + { + let account = Account { + lamports: account.lamports(), + data: account.data(), + owner: account.owner(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + }; + account.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl serde::ser::Serialize for Account { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + account_serialize::serialize_account(self, serializer) + } +} + +#[cfg(feature = "serde")] +impl serde::ser::Serialize for AccountSharedData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + account_serialize::serialize_account(self, serializer) + } +} + +impl From for Account { + fn from(other: AccountSharedData) -> Self { + Self { + lamports: other.lamports(), + data: other.data().to_vec(), + owner: *other.owner(), + executable: other.executable(), + rent_epoch: other.rent_epoch(), + } + } +} + +impl fmt::Debug for Account { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f, |_| {}) + } +} + +impl fmt::Debug for AccountSharedData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f, |f| { + f.field("slot", &self.slot()) + .field("mode", &self.mode) + .field("flags", self.flags()) + .field("dirty", self.markers()); + }) + } +} + +impl Account { + /// Builds an account from its exact field set. + /// + /// Used by the constructors to keep the owned layout in one place. + fn from_parts( + lamports: u64, + data: Vec, + owner: Pubkey, + executable: bool, + rent_epoch: Epoch, + ) -> Self { + Self { + lamports, + data, + owner, + executable, + rent_epoch, + } + } + + /// Creates a new account with zero-filled data. + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + Self::new_rent_epoch(lamports, space, owner, Epoch::default()) + } + + /// Creates a new account wrapped in a `RefCell`. + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(Self::new(lamports, space, owner))) + } + + /// Creates a new account whose data is the serialized state. + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Self::from_parts( + lamports, + data, + *owner, + false, + Epoch::default(), + )) + } + + /// Creates a new serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data(lamports, state, owner).map(RefCell::new) + } + + /// Creates a new account with fixed space and serialized state. + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Self::new(lamports, space, owner); + crate::codec::serialize_data(&mut account, state)?; + Ok(account) + } + + /// Creates a new fixed-size serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + + /// Creates a new account with an explicit rent epoch. + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + Self::from_parts(lamports, vec![0; space], *owner, false, rent_epoch) + } + + /// Deserializes the account data as `T`. + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + crate::codec::deserialize_data(self) + } + + /// Serializes `state` into the existing account data buffer. + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + crate::codec::serialize_data(self, state) + } +} + +impl solana_account_info::Account for Account { + fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool) { + ( + &mut self.lamports, + &mut self.data, + &self.owner, + self.executable, + ) + } +} + +/// Builds `AccountInfo` values for accounts and signer bits. +/// +/// The returned infos borrow the provided accounts directly. +pub fn create_is_signer_account_infos<'a>( + accounts: &'a mut [(&'a Pubkey, bool, &'a mut Account)], +) -> Vec> { + accounts + .iter_mut() + .map(|(key, is_signer, account)| { + AccountInfo::new( + key, + *is_signer, + false, + &mut account.lamports, + &mut account.data, + &account.owner, + account.executable, + ) + }) + .collect() +} + +/// Owners that imply the account contains a loaded program. +pub const PROGRAM_OWNERS: &[Pubkey] = &[ + native_loader::id(), + bpf_loader_upgradeable::id(), + bpf_loader::id(), + bpf_loader_deprecated::id(), + loader_v4::id(), +]; diff --git a/solana/account/src/codec.rs b/solana/account/src/codec.rs new file mode 100644 index 00000000..c8aa14cd --- /dev/null +++ b/solana/account/src/codec.rs @@ -0,0 +1,23 @@ +//! Shared bincode helpers for account data. + +use serde::{Serialize, de::DeserializeOwned}; + +use crate::{ReadableAccount, WritableAccount}; + +/// Deserializes typed state from an account data slice. +pub(crate) fn deserialize_data( + account: &U, +) -> Result { + bincode::deserialize(account.data()) +} + +/// Serializes typed state into an existing account data buffer. +pub(crate) fn serialize_data( + account: &mut U, + state: &T, +) -> Result<(), bincode::Error> { + if bincode::serialized_size(state)? > account.data().len() as u64 { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + bincode::serialize_into(account.data_as_mut_slice(), state) +} diff --git a/solana/account/src/cow/borrowed.rs b/solana/account/src/cow/borrowed.rs new file mode 100644 index 00000000..e41fb510 --- /dev/null +++ b/solana/account/src/cow/borrowed.rs @@ -0,0 +1,324 @@ +//! Raw layout used by the borrowed zero-copy account view. +//! +//! The buffer is 8-byte aligned and contains a header followed by two images. +//! `AccountHeader::sequence` selects the active image; `translate` copies it to the shadow +//! image, `reset` repoints the view to the active image, `commit` publishes the shadow image, +//! and `rollback` undoes that publication by decrementing the sequence counter. + +#![allow(unsafe_op_in_unsafe_fn)] + +use std::{ + ops::{Deref, DerefMut}, + ptr::NonNull, + slice, + sync::atomic::{AtomicU32, Ordering::*}, +}; + +use solana_pubkey::Pubkey; + +use super::owned::OwnedAccount; +use super::{ALIGNMENT, AccountCore, STORAGE_UNIT, StorageUnit}; + +/// Fixed bytes in one image after the shared pubkey prefix: core and data header. +pub(super) const STATIC_SIZE: usize = size_of::() + size_of::(); +/// Storage-unit offset from the header to the first image payload, including the pubkey prefix. +pub(super) const IMAGE_OFFSET: usize = + (size_of::() + size_of::()) / STORAGE_UNIT; + +/// Header that prefixes a double-allocation borrowed account buffer. +#[repr(C, align(8))] +pub(crate) struct AccountHeader { + /// Sequence counter; parity selects the active image. + pub(crate) sequence: AtomicU32, + /// Image size measured in `AccountHeader` units. + pub(crate) space: u32, +} + +impl AccountHeader { + /// Creates a header for one image size in storage units. + pub(crate) fn new(space: u32) -> Self { + // `space` stays in storage units so the active + // image can be indexed with one multiply. + Self { sequence: 0.into(), space } + } +} + +/// Pointer arithmetic relies on these size and alignment invariants. +const _: () = assert!(size_of::() == ALIGNMENT); +const _: () = assert!(size_of::() == STORAGE_UNIT); +const _: () = assert!((size_of::() + STORAGE_UNIT) / ALIGNMENT == IMAGE_OFFSET); + +/// Borrowed zero-copy account view into an aligned external buffer. +#[derive(Eq, PartialEq)] +pub struct BorrowedAccount { + /// Header pointer for the borrowed buffer. + pub(crate) header: NonNull, + /// Pointer to the active image's account core. + pub(crate) core: NonNull, + /// Borrowed data bytes for the active image. + pub(crate) data: DataSlice, + /// Sequence used to select this view's image. + pub(crate) version: u32, +} + +/// Returns the byte offset for the active or shadow image. +#[inline] +fn offset(space: u32, sequence: u32, active: bool) -> usize { + // Even sequence => image A is active, odd sequence => image B is active. + let even = sequence.is_multiple_of(2); + // Flip to the shadow image when `active` does not match the current parity. + let step = (active ^ even) as u32; + (step * space) as usize + IMAGE_OFFSET +} + +impl BorrowedAccount { + /// Returns the sequence value that selects the active image. + pub(crate) fn sequence(&self) -> u32 { + // SAFETY: borrowed account headers live for the account view. + unsafe { self.header.as_ref() }.sequence.load(Acquire) + } + /// Returns the total borrowed span in `StorageUnit`s. + /// + /// # Safety + /// + /// `ptr` must point to a valid borrowed buffer created by + /// [`OwnedAccount::serialize`]. + pub unsafe fn span(ptr: NonNull) -> u32 { + let space = ptr.cast::().as_ref().space; + space * 2 + IMAGE_OFFSET as u32 + } + + /// Reads the account's pubkey stored in the image prefix. + /// + /// # Safety + /// + /// `ptr` must point to a valid borrowed buffer created by + /// [`OwnedAccount::serialize`]. + pub unsafe fn pubkey(ptr: NonNull) -> Pubkey { + *ptr.add(1).cast().as_ref() + } + + /// Builds a borrowed account view from an aligned account buffer. + /// + /// # Safety + /// + /// `buffer` must be 8-byte aligned and point to a valid borrowed account + /// buffer whose first bytes are the account header, followed by two + /// image-sized payloads. The active image is selected from the header + /// sequence parity. + pub unsafe fn init(buffer: NonNull) -> Self { + let header = buffer.cast::(); + let version = header.as_ref().sequence.load(Acquire); + let offset = offset(header.as_ref().space, version, true); + + let core = header.add(offset).cast(); + let data = DataSlice::init(core.add(1).cast()); + + Self { header, core, data, version } + } + + /// Copies the active image into the shadow image and switches to it. + /// + /// # Safety + /// + /// The borrowed image must still be the one selected by `init`. + pub unsafe fn translate(&mut self) { + let offset = offset(self.header.as_ref().space, self.version, false); + + // Copy bytes in bulk from active image to the shadow + let dst = self.header.add(offset).cast(); + let src = self.core.cast::(); + if src == dst { + return; + } + let count = self.header.as_ref().space as usize; + dst.copy_from_nonoverlapping(src, count); + // Switch the pointers to the shadow view + self.core = dst.cast(); + self.data = DataSlice::init(self.core.add(1).cast()); + } + + /// Publishes the shadow image if it was prepared against the current sequence. + pub fn commit(&self) { + // SAFETY: the header is part of the borrowed buffer for the lifetime of `self`. + let header = unsafe { self.header.as_ref() }; + let shadow = unsafe { + self.header.add(offset(header.space, self.version, false)).cast::() + }; + if self.core != shadow { + return; + } + let next = self.version.wrapping_add(1); + let _ = header.sequence.compare_exchange(self.version, next, Release, Relaxed); + } + + /// Repoints this view to the currently active image without copying data. + /// + /// # Safety + /// + /// The header must remain live, and `self` must be a view previously produced + /// by [`Self::init`] or [`Self::translate`] for that borrowed buffer. + pub unsafe fn reset(&mut self) { + self.version = self.header.as_ref().sequence.load(Acquire); + let offset = offset(self.header.as_ref().space, self.version, true); + self.core = self.header.add(offset).cast(); + self.data = DataSlice::init(self.core.add(1).cast()); + } + + /// Undoes the latest commit, by adjusting the sequence counter + /// + /// # Safety + /// + /// Call this only after `commit` to avoid data corruption; + pub unsafe fn rollback(&self) { + // SAFETY: the header is part of the borrowed buffer for the lifetime of `self`. + unsafe { self.header.as_ref().sequence.fetch_sub(1, Release) }; + } + + /// Returns the owner pubkey from the active image. + pub fn owner(&self) -> Pubkey { + // SAFETY: `core` points at a live `AccountCore` inside the borrowed buffer. + unsafe { self.core.as_ref() }.owner + } + + /// Returns the serialized active image bytes that define account state. + /// + /// The slice starts at `AccountCore`, includes the `DataHeader`, and stops + /// after initialized data. It excludes the shared header, pubkey prefix, + /// inactive shadow image, and spare data capacity. + pub fn storage(&self) -> &[u8] { + let len = STATIC_SIZE + self.data.len(); + // SAFETY: `core` points at the active image and `len` only covers its + // initialized state bytes: core, data header, and initialized data. + unsafe { slice::from_raw_parts(self.core.as_ptr().cast(), len) } + } +} + +impl From<&BorrowedAccount> for OwnedAccount { + fn from(value: &BorrowedAccount) -> Self { + Self { + // SAFETY: `BorrowedAccount` guarantees `core` points at a live account + // header inside the borrowed buffer for the lifetime of the borrow. + core: *unsafe { value.core.as_ref() }, + data: value.data.deref().to_vec().into(), + } + } +} + +/// Mutable byte slice backed by a borrowed account buffer. +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct DataSlice { + /// Header carrying length and capacity. + header: NonNull, + /// Pointer to the first data byte. + ptr: NonNull, +} + +/// Data header stored immediately before the raw byte slice. +#[repr(C)] +pub(crate) struct DataHeader { + /// Initialized data length. + len: u32, + /// Total writable capacity. + cap: u32, +} + +impl DataHeader { + /// Creates a data header for one image. + pub(crate) fn new(len: u32, allocation: u32) -> Self { + // `cap` is the writable tail after `AccountCore` and `DataHeader`. + let cap = (allocation as usize * STORAGE_UNIT - STATIC_SIZE) as u32; + Self { len, cap } + } +} + +impl DataSlice { + /// Builds a borrowed slice from a data header. + /// + /// # Safety + /// + /// `header` must point at a valid `DataHeader` followed by initialized data. + unsafe fn init(header: NonNull) -> Self { + let ptr = header.add(1).cast(); + Self { header, ptr } + } + + /// Returns the initialized byte length. + pub(crate) fn len(&self) -> usize { + // SAFETY: `header` points at the live data header for this borrowed slice. + let header = unsafe { self.header.as_ref() }; + header.len.min(header.cap) as usize + } + + /// Returns the total writable capacity. + pub(crate) fn capacity(&self) -> usize { + // SAFETY: `header` points at the live data header for this borrowed slice. + let header = unsafe { self.header.as_ref() }; + header.cap as usize + } + + /// Returns the remaining writable capacity. + pub(crate) fn spare(&self) -> usize { + self.capacity() - self.len() + } + + /// Resizes the initialized range in place. + /// + /// # Safety + /// + /// `len` must not exceed the borrowed capacity. + pub(crate) unsafe fn resize(&mut self, len: usize, val: u8) { + let prev = self.len(); + debug_assert!(prev <= self.capacity()); + debug_assert!(len <= self.capacity()); + let delta = len.saturating_sub(prev); + if delta > 0 { + self.ptr.as_ptr().add(prev).write_bytes(val, delta); + } + self.header.as_mut().len = len as u32; + } + + /// Appends bytes in place. + /// + /// # Safety + /// + /// `data` must fit in the remaining borrowed capacity and not overlap. + pub(crate) unsafe fn extend(&mut self, data: &[u8]) { + let len = self.len(); + let dst = self.ptr.as_ptr().add(len); + dst.copy_from_nonoverlapping(data.as_ptr(), data.len()); + self.header.as_mut().len += data.len() as u32; + } + + /// Replaces the initialized bytes in place. + /// + /// # Safety + /// + /// `data` must fit in the borrowed capacity and not overlap. + pub(crate) unsafe fn set(&mut self, data: &[u8]) { + self.ptr.as_ptr().copy_from_nonoverlapping(data.as_ptr(), data.len()); + self.header.as_mut().len = data.len() as u32; + } +} + +impl Deref for DataSlice { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + // SAFETY: `len` bytes from `ptr` are initialized account data owned by + // the borrowed buffer described by this `DataSlice`. + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len()) } + } +} + +impl DerefMut for DataSlice { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: the borrowed buffer grants unique mutable access through this borrow. + unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len()) } + } +} + +// SAFETY: `BorrowedAccount` points into external storage and only exposes +// shared reads unless the caller holds `&mut self`; moving the view to another +// thread does not weaken the buffer lifetime and aliasing requirements. +unsafe impl Send for BorrowedAccount {} diff --git a/solana/account/src/cow/mod.rs b/solana/account/src/cow/mod.rs new file mode 100644 index 00000000..2162c6fa --- /dev/null +++ b/solana/account/src/cow/mod.rs @@ -0,0 +1,708 @@ +//! Copy-on-write account data with zero-copy access to aligned external storage. +//! +//! `borrowed` defines the raw buffer layout and `owned` holds the heap-backed form. +#![allow(unsafe_op_in_unsafe_fn)] + +mod borrowed; +mod owned; + +pub use borrowed::BorrowedAccount; +pub use owned::{AccountBuilder, OwnedAccount}; + +use crate::{Account, ReadableAccount, WritableAccount, patch::AccountPatchError}; +use solana_clock::{Epoch, Slot}; +use solana_pubkey::Pubkey; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, + sync::Arc, +}; + +use CoWAccount::*; + +/// Borrowed buffers must be aligned to this many bytes. +pub const ALIGNMENT: usize = 8; +/// Bytes in one storage unit. +pub const STORAGE_UNIT: usize = size_of::(); +/// Minimum addressable storage unit for borrowed account images. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct StorageUnit(pub u64); + +/// Shared account data that borrows directly from an aligned external buffer +/// until a write requires promotion to owned heap storage. +/// +/// Higher layers use `mutable()` to enforce transaction write permissions. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(from = "Account"))] +#[derive(Clone, Default)] +pub struct AccountSharedData { + /// Backing storage, borrowed until promotion or direct construction. + pub(crate) cow: CoWAccount, + /// Fields changed through the writable APIs. + pub(crate) dirty: DirtyMarkers, +} + +/// Core account state shared by the borrowed and owned representations. +#[repr(C)] +#[derive(Clone, Copy, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct AccountCore { + /// Lamport balance. + pub(crate) lamports: u64, + /// Account owner. + pub(crate) owner: Pubkey, + /// On-chain slot, at which the account was cloned. + pub(crate) slot: Slot, + /// Mutually exclusive mode of existence for the account. + pub(crate) mode: AccountMode, + /// Account state modifier flags. + pub(crate) flags: StateFlags, + /// Reserved bytes that make the serialized representation deterministic. + _padding: [u8; 6], +} + +impl Deref for AccountSharedData { + type Target = AccountCore; + + fn deref(&self) -> &Self::Target { + match &self.cow { + Borrowed(account) => { + // SAFETY: `BorrowedAccount` owns the invariant that `core` points at + // a live `AccountCore` inside the borrowed buffer. + unsafe { account.core.as_ref() } + } + Owned(account) => &account.core, + } + } +} + +impl DerefMut for AccountSharedData { + fn deref_mut(&mut self) -> &mut Self::Target { + match &mut self.cow { + Borrowed(account) => { + // SAFETY: `&mut self` guarantees unique access to the borrowed image. + unsafe { account.core.as_mut() } + } + Owned(account) => &mut account.core, + } + } +} + +impl PartialEq for AccountSharedData { + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() && self.cow.data() == other.cow.data() + } +} + +impl Eq for AccountSharedData {} + +impl AccountSharedData { + /// Returns a reference to the inner copy-on-write representation. + pub fn cow(&self) -> &CoWAccount { + &self.cow + } + + /// Returns mutable access to the inner copy-on-write representation. + pub fn cow_mut(&mut self) -> &mut CoWAccount { + &mut self.cow + } + + /// Returns the account's on-chain slot. + pub fn slot(&self) -> Slot { + self.slot + } + + /// Copies a clean borrowed image into the shadow buffer before mutation. + pub fn translate(&mut self) { + if self.dirty() { + return; + } + if let Borrowed(ref mut acc) = self.cow { + // SAFETY: this runs before the first dirty marker, so the borrowed + // view still points at the active image selected by `init`. + unsafe { acc.translate() }; + } + } + + /// Returns an owned copy of the current account state. + pub fn owned(&self) -> OwnedAccount { + match self.cow() { + Borrowed(a) => a.into(), + Owned(a) => a.clone(), + } + } + + /// Returns whether the current transaction may leave the account modified. + /// + /// Mutable modes are always accepted. `Transient` and `Closed` are accepted + /// only when this transaction performed the corresponding mode transition. + pub fn mutable(&self) -> bool { + self.mode.mutable() + || matches!(self.mode, AccountMode::Transient | AccountMode::Closed) + && self.dirty.contains(DirtyMarkers::MODE) + } + + /// Returns the account's exact lifecycle mode. + pub fn mode(&self) -> AccountMode { + self.mode + } + + /// Returns `true` when the account is in `mode`. + pub fn is(&self, mode: AccountMode) -> bool { + self.mode == mode + } + + /// Returns the account modifier flags. + pub fn flags(&self) -> &StateFlags { + &self.flags + } + + /// Returns the dirty-field markers. + pub fn markers(&self) -> &DirtyMarkers { + &self.dirty + } + + /// Marks the data buffer as modified. + pub(crate) fn mark_data_dirty(&mut self) { + self.dirty.insert(DirtyMarkers::DATA); + } + + /// Returns `true` when the owned buffer has more than one strong reference. + pub fn is_shared(&self) -> bool { + self.cow.is_shared() + } + + /// Returns `true` if any field has been modified. + pub fn dirty(&self) -> bool { + self.dirty.intersects(DirtyMarkers::all()) + } + + /// Returns the current data capacity. + pub fn capacity(&self) -> usize { + self.cow.capacity() + } + + /// Returns a shared owned copy of the current data bytes. + pub fn data_clone(&self) -> Arc> { + self.cow.data_clone() + } + + /// Resizes the account data. + pub fn resize(&mut self, len: usize, val: u8) { + self.translate(); + self.mark_data_dirty(); + self.cow.resize(len, val); + } + + /// Appends bytes to the account data. + pub fn extend_from_slice(&mut self, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + self.cow.extend_from_slice(data); + } + + /// Replaces the account data with the provided bytes. + pub fn set_data_from_slice(&mut self, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + self.cow.set_data_from_slice(data); + } + + /// Sets a legal account mode transition and marks the mode dirty. + /// + /// Reapplying the current mode is a no-op so callers can distinguish a + /// genuine lifecycle transition from an unchanged account. Invalid + /// transitions leave the account and its dirty markers unchanged. + pub fn set_mode(&mut self, to: AccountMode) -> Result<(), AccountPatchError> { + use AccountMode::*; + let from = self.mode; + + if from == to { + return Ok(()); + } + let allowed = match (from, to) { + (ReadOnly | Placeholder, to) => to != Transient, + (Delegated, Transient) | (Transient, ReadOnly) | (Ephemeral, Closed) => true, + _ => false, + }; + if !allowed { + Err(AccountPatchError::InvalidModeTransition { from, to })?; + } + self.translate(); + self.dirty.set(DirtyMarkers::MODE, true); + self.mode = to; + Ok(()) + } + + /// Writes bytes at `offset`, extending and zero-filling as needed. + pub(crate) fn set_data_at(&mut self, offset: usize, data: &[u8]) { + self.translate(); + self.mark_data_dirty(); + let len = self.data().len(); + if offset > len { + // Grow to `offset`, zero-filling the gap; the write below then + // appends `data` past it via `extend_from_slice`. + self.resize(offset, 0); + } + + // Write the overlap in place, then append any remaining tail. This + // keeps borrowed buffers on the fast path when the write fits. + let n = self.data().len().saturating_sub(offset).min(data.len()); + self.data_as_mut_slice()[offset..offset + n].copy_from_slice(&data[..n]); + self.extend_from_slice(&data[n..]); + } + + /// Sets a non-regressing account slot. + /// + /// Reapplying the current slot is accepted only after a genuine mode change + /// in the same transaction. Rejected transitions leave the account and its + /// dirty markers unchanged. + pub(crate) fn set_slot(&mut self, to: Slot) -> Result<(), AccountPatchError> { + let from = self.slot; + let mode_changed = self.dirty.contains(DirtyMarkers::MODE); + if to < from || (to == from && !mode_changed) { + Err(AccountPatchError::InvalidSlotTransition { from, to })?; + } + self.translate(); + self.dirty.set(DirtyMarkers::SLOT, true); + self.slot = to; + Ok(()) + } + + /// Replaces all state flags and marks them dirty when the value changes. + pub fn set_flags(&mut self, flags: StateFlags) { + if self.flags == flags { + return; + } + self.translate(); + self.dirty.set(DirtyMarkers::FLAGS, true); + self.flags = flags; + } + + /// Creates a new owned shared-data account with zero-filled data. + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + AccountBuilder::default() + .lamports(lamports) + .data(vec![0; space]) + .owner(*owner) + .build() + } + /// Creates a new shared-data account wrapped in a `RefCell`. + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(Self::new(lamports, space, owner))) + } + + /// Creates a new account with serialized data. + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Self::create_from_existing_shared_data( + lamports, + Arc::new(data), + *owner, + false, + Epoch::default(), + )) + } + + /// Creates a new serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data(lamports, state, owner).map(RefCell::new) + } + + /// Creates a new fixed-size account with serialized data. + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Self::new(lamports, space, owner); + crate::codec::serialize_data(&mut account, state)?; + Ok(account) + } + + /// Creates a new fixed-size serialized account wrapped in a `RefCell`. + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Self::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + + /// Creates a new shared-data account. + /// + /// `rent_epoch` is ignored because this type does not store it. + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, _: Epoch) -> Self { + Self::new(lamports, space, owner) + } + + /// Deserializes the account data as `T`. + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + crate::codec::deserialize_data(self) + } + + /// Serializes `state` into the existing account data buffer. + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + crate::codec::serialize_data(self, state) + } + + /// Creates an owned shared-data account from existing shared bytes. + /// + /// `rent_epoch` is ignored because this type does not store it. + pub fn create_from_existing_shared_data( + lamports: u64, + data: Arc>, + owner: Pubkey, + executable: bool, + _: Epoch, + ) -> Self { + AccountBuilder::default() + .lamports(lamports) + .data(data) + .owner(owner) + .executable(executable) + .build() + } +} + +bitflags::bitflags! { + /// Account state modifier flags. + #[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] + pub struct StateFlags: u8 { + /// Executable account data. + const EXECUTABLE = 1 << 0; + } + + /// Bits that record which fields changed through `AccountSharedData`. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct DirtyMarkers: u8 { + /// Owner changed. + const OWNER = 1 << 0; + /// Lamports changed. + const LAMPORTS = 1 << 1; + /// Mode changed. + const MODE = 1 << 2; + /// State flags changed. + const FLAGS = 1 << 3; + /// Slot changed. + const SLOT = 1 << 4; + /// Data bytes changed. + const DATA = 1 << 5; + } +} + +/// `wincode` codec for `StateFlags`, which is a `bitflags!` newtype and so +/// cannot use the derives. Routed through `bincode`/`serde`, which encodes the +/// single bits byte identically to a plain `u8`. +#[cfg(feature = "wincode")] +const _: () = { + use core::mem::MaybeUninit; + use wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteError, WriteResult, + config::ConfigCore, + io::{Reader, Writer}, + }; + + // SAFETY: encodes exactly one byte; matches `TYPE_META` / `size_of`. + unsafe impl SchemaWrite for StateFlags { + type Src = StateFlags; + const TYPE_META: TypeMeta = TypeMeta::Static { size: 1, zero_copy: false }; + + fn size_of(_: &Self::Src) -> WriteResult { + Ok(1) + } + + fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> { + let bytes = bincode::serialize(src).map_err(|_| WriteError::Custom("StateFlags"))?; + writer.write(&bytes)?; + Ok(()) + } + } + + // SAFETY: consumes exactly one byte; matches `TYPE_META`. + unsafe impl<'de, C: ConfigCore> SchemaRead<'de, C> for StateFlags { + type Dst = StateFlags; + const TYPE_META: TypeMeta = TypeMeta::Static { size: 1, zero_copy: false }; + + fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let bytes = reader.take_array::<1>()?; + dst.write(bincode::deserialize(&bytes).map_err(|_| ReadError::Custom("StateFlags"))?); + Ok(()) + } + } +}; + +/// Backing storage for `AccountSharedData`. +#[derive(PartialEq, Eq)] +pub enum CoWAccount { + /// Borrowed image, a view into static backing buffer. + Borrowed(BorrowedAccount), + /// Heap-owned image. + Owned(OwnedAccount), +} + +impl Clone for CoWAccount { + fn clone(&self) -> Self { + match self { + Borrowed(acc) => Self::Owned(acc.into()), + Owned(acc) => Self::Owned(acc.clone()), + } + } +} + +impl CoWAccount { + /// Promotes borrowed storage to the owned form. + pub(crate) fn promote(&mut self) { + let Self::Borrowed(account) = self else { + return; + }; + *self = Self::Owned(account.deref().into()); + } + + /// Returns the current data slice. + pub(crate) fn data(&self) -> &[u8] { + match self { + Self::Borrowed(account) => &account.data, + Self::Owned(account) => &account.data, + } + } + + /// Returns `true` when the heap buffer has multiple owners. + pub(crate) fn is_shared(&self) -> bool { + match self { + Self::Borrowed(_) => false, + Self::Owned(account) => Arc::strong_count(&account.data) > 1, + } + } + + /// Returns the current data capacity. + pub(crate) fn capacity(&self) -> usize { + match self { + Self::Borrowed(account) => account.data.capacity(), + Self::Owned(account) => account.data.capacity(), + } + } + + /// Returns a shared owned copy of the current data bytes. + pub(crate) fn data_clone(&self) -> Arc> { + match self { + Self::Borrowed(account) => Arc::new(account.data.to_vec()), + Self::Owned(account) => Arc::clone(&account.data), + } + } + + /// Returns mutable data, promoting borrowed storage only when needed. + pub(crate) fn data_mut(&mut self) -> &mut [u8] { + match self { + Self::Borrowed(account) => &mut account.data, + Self::Owned(account) => Arc::>::make_mut(&mut account.data).as_mut_slice(), + } + } + + /// Reserves additional space for the account data. + pub fn reserve(&mut self, additional: usize) { + if let Self::Borrowed(a) = self + && a.data.spare() >= additional + { + return; + } + self.promote(); + if let Self::Owned(account) = self { + Arc::make_mut(&mut account.data).reserve(additional); + } + } + + /// Resizes the account data. + pub(crate) fn resize(&mut self, len: usize, val: u8) { + if let Self::Borrowed(a) = self + && len <= a.data.capacity() + { + // SAFETY: this stays in the borrowed image only while the resized + // range fits within the borrowed capacity. + unsafe { a.data.resize(len, val) }; + return; + } + + self.promote(); + if let Self::Owned(account) = self { + Arc::make_mut(&mut account.data).resize(len, val); + } + } + + /// Appends bytes to the account data. + pub(crate) fn extend_from_slice(&mut self, data: &[u8]) { + self.reserve(data.len()); + + match self { + Self::Borrowed(account) => { + // SAFETY: `reserve` keeps the borrowed image only when the appended + // bytes fit in the remaining borrowed capacity. + unsafe { account.data.extend(data) }; + } + Self::Owned(account) => Arc::make_mut(&mut account.data).extend_from_slice(data), + } + } + + /// Replaces the account data with the provided bytes. + pub(crate) fn set_data_from_slice(&mut self, data: &[u8]) { + let additional = data.len().saturating_sub(self.data().len()); + self.reserve(additional); + + match self { + Self::Borrowed(account) => { + // SAFETY: `reserve` keeps the borrowed image only when the + // replacement bytes fit in the borrowed capacity. + unsafe { account.data.set(data) }; + } + Self::Owned(account) => { + let data_buf = Arc::make_mut(&mut account.data); + data_buf.clear(); + data_buf.extend_from_slice(data); + } + } + } +} + +/// Mutually exclusive modes an account can occupy in the ephemeral rollup (ER). +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +#[repr(u8)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +pub enum AccountMode { + /// Empty account (not found on chain) used to avoid frequent chain syncs. + #[default] + Placeholder = 0, + /// Not writable by users (exists on chain, but not delegated) + ReadOnly = 1, + /// Internal account used for sysvars, features, and precompiles. + System, + /// Account delegated to the current ER node instance. + Delegated, + /// Account that exists only inside the ER. + Ephemeral, + /// Temporary state during mode transitions (e.g. delegated -> readonly). + Transient, + /// Closed account that should be removed from storage. + Closed = 255, +} + +impl AccountMode { + /// Returns `true` for modes that may be mutated by user programs. + pub fn mutable(&self) -> bool { + use AccountMode::*; + matches!(self, Delegated | Ephemeral) + } + + /// Returns `true` for modes whose state is authoritative in this engine. + pub fn authoritative(&self) -> bool { + use AccountMode::*; + matches!(self, Delegated | Ephemeral | Transient) + } +} + +/// Read wrapper that retries borrowed account reads when a concurrent publish +/// changes the backing image. +pub struct AccountSeqLock { + account: AccountSharedData, + sequence: Option, +} + +impl AccountSeqLock { + /// Creates a read lock with the sequence that matches the current account view. + pub fn new(account: AccountSharedData) -> Self { + let mut sequence = None; + if let Borrowed(ref acc) = account.cow { + sequence.replace(acc.version); + } + Self { account, sequence } + } + + /// Runs `reader` against a stable account image. + /// + /// For borrowed accounts, the sequence is checked after the read. If a + /// writer published a new image meanwhile, the account view is reset to that + /// active image and the read is retried. + pub fn read(&mut self, reader: F) -> R + where + F: Fn(&AccountSharedData) -> R, + { + loop { + // sequence is always present for borrowed accounts + let pre = self.sequence.unwrap_or_default(); + let result = reader(&self.account); + match self.account.cow_mut() { + Borrowed(acc) => { + let post = acc.sequence(); + if pre == post { + return result; + } + // SAFETY: a changed sequence means the active image may have + // moved, so the borrowed view must be repointed before retrying. + unsafe { acc.reset() }; + self.sequence = Some(acc.version); + } + Owned(_) => return result, + } + } + } +} + +impl Default for CoWAccount { + fn default() -> Self { + Self::Owned(OwnedAccount::default()) + } +} + +/// Wraps an owned account in `AccountSharedData`. +impl From for AccountSharedData { + fn from(value: OwnedAccount) -> Self { + Self { + cow: Owned(value), + dirty: DirtyMarkers::default(), + } + } +} + +/// Wraps a borrowed account in `AccountSharedData`. +impl From for AccountSharedData { + fn from(value: BorrowedAccount) -> Self { + Self { + cow: Borrowed(value), + dirty: DirtyMarkers::default(), + } + } +} + +/// Converts a plain `Account` into shared data. +impl From for AccountSharedData { + fn from(value: Account) -> Self { + AccountBuilder::default() + .lamports(value.lamports) + .data(value.data) + .owner(value.owner) + .executable(value.executable) + .build() + } +} + +/// We only access AccountSharedData via transaction lock in the +/// execution layer or with a SeqLock semantics outside of execution +unsafe impl Sync for AccountSharedData {} diff --git a/solana/account/src/cow/owned.rs b/solana/account/src/cow/owned.rs new file mode 100644 index 00000000..5243fa8e --- /dev/null +++ b/solana/account/src/cow/owned.rs @@ -0,0 +1,177 @@ +use super::borrowed::{AccountHeader, DataHeader, STATIC_SIZE}; +use super::{ALIGNMENT, StateFlags}; +use crate::cow::AccountCore; +use crate::cow::borrowed::IMAGE_OFFSET; +use crate::{Account, AccountMode, AccountSharedData, StorageUnit}; +use solana_clock::Slot; +use solana_pubkey::Pubkey; +use std::{ptr::NonNull, sync::Arc}; + +/// Heap-backed account, used after promotion from borrowed or direct construction. +#[derive(Clone, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct OwnedAccount { + /// Core account fields. + pub(crate) core: AccountCore, + /// Heap-owned data buffer. + pub(crate) data: Arc>, +} + +impl OwnedAccount { + /// Returns the exact storage units needed to serialize this account. + pub fn units(&self) -> u32 { + self.allocation() * 2 + IMAGE_OFFSET as u32 + } + + /// Returns the storage units needed for one image, rounded up to alignment. + fn allocation(&self) -> u32 { + (STATIC_SIZE + self.data.len()).div_ceil(ALIGNMENT) as u32 + } + + /// Writes the account into a buffer sized by `units`. + /// + /// # Safety + /// + /// `buf` must be exactly `units()` storage units long. + /// `pubkey` is written into the image prefix so borrowed iteration can + /// recover the full account key without consulting the index. + pub unsafe fn serialize(&self, buf: &mut [StorageUnit], pubkey: &Pubkey) { + let ptr = NonNull::new_unchecked(buf.as_mut_ptr()); + debug_assert_eq!(self.units() as usize, buf.len()); + + fn write(ptr: NonNull, v: T) -> NonNull { + // SAFETY: `serialize` requires a buffer sized for the full layout. + unsafe { + ptr.cast().write(v); + ptr.cast().add(1) + } + } + + let allocation = self.allocation(); + let ptr = write(ptr, AccountHeader::new(allocation)); + // The image prefix stores the account pubkey for later iteration. + let ptr = write(ptr, *pubkey); + let ptr = write(ptr, self.core); + let len = self.data.len(); + let ptr = write(ptr, DataHeader::new(len as u32, allocation)).cast(); + self.data.as_ptr().copy_to_nonoverlapping(ptr.as_ptr(), len); + } + + /// Tests the exact account mode without grouping modes by mutability. + pub fn is(&self, mode: AccountMode) -> bool { + self.core.mode == mode + } + + /// Returns the owner pubkey. + pub fn owner(&self) -> Pubkey { + self.core.owner + } + + /// Returns the lamport balance. + pub fn lamports(&self) -> u64 { + self.core.lamports + } + + /// Returns the account's exact lifecycle mode. + pub fn mode(&self) -> AccountMode { + self.core.mode + } + + /// Returns the account's on-chain slot. + pub fn slot(&self) -> u64 { + self.core.slot + } + + /// Returns the account modifier flags. + pub fn flags(&self) -> StateFlags { + self.core.flags + } + + /// Returns the account data. + pub fn data(&self) -> &[u8] { + &self.data + } +} + +/// Builder for an owned account representation. +/// +/// Use this when the account does not start from a borrowed external buffer. +#[derive(Default, Clone)] +pub struct AccountBuilder(OwnedAccount); + +impl AccountBuilder { + /// Sets the lamport balance. + pub fn lamports(mut self, lamports: u64) -> Self { + self.0.core.lamports = lamports; + self + } + + /// Sets the data buffer. + pub fn data(mut self, data: impl Into>>) -> Self { + self.0.data = data.into(); + self + } + + /// Sets the owner. + pub fn owner(mut self, owner: Pubkey) -> Self { + self.0.core.owner = owner; + self + } + + /// Sets the account persistence mode of the account + pub fn mode(mut self, mode: AccountMode) -> Self { + self.0.core.mode = mode; + self + } + + /// Sets the executable flag. + pub fn executable(mut self, executable: bool) -> Self { + self.0.core.flags.set(StateFlags::EXECUTABLE, executable); + self + } + + /// Sets the on chain slot. + pub fn slot(mut self, slot: Slot) -> Self { + self.0.core.slot = slot; + self + } + + /// Borrows the account under construction. + pub fn read(&self) -> &OwnedAccount { + &self.0 + } + + /// Finishes building the owned account. + pub fn build>(self) -> A { + self.0.into() + } +} + +impl From for OwnedAccount { + fn from(value: Account) -> Self { + AccountBuilder::default() + .lamports(value.lamports) + .data(value.data) + .owner(value.owner) + .executable(value.executable) + .build() + } +} + +impl From for OwnedAccount { + fn from(value: AccountBuilder) -> Self { + value.0 + } +} + +impl From for AccountBuilder { + fn from(value: Account) -> Self { + Self(value.into()) + } +} + +impl From for AccountBuilder { + fn from(value: AccountSharedData) -> Self { + Self(value.owned()) + } +} diff --git a/solana/account/src/cow/tests.rs b/solana/account/src/cow/tests.rs new file mode 100644 index 00000000..89ad2b65 --- /dev/null +++ b/solana/account/src/cow/tests.rs @@ -0,0 +1,83 @@ +use super::borrowed::BorrowedAccount; +use super::{StorageUnit, init, serialize_buf}; +use crate::AccountBuilder; +use solana_pubkey::Pubkey; +use std::sync::atomic::Ordering::Acquire; + +const BORROWED_LAMPORTS: u64 = 5; +const ACTIVE_DATA: &[u8] = &[1, 2, 3]; +const COMMIT_DATA: &[u8] = &[4, 5, 6]; +const COMMITTED_DATA: &[u8] = &[9, 2, 3]; +const ACTIVE_WRITE: u8 = 9; +const ROLLBACK_WRITE: u8 = 8; +const INITIAL_SEQUENCE: u32 = 0; +const COMMITTED_SEQUENCE: u32 = 1; + +// Serializes an owned account into a borrowed buffer image. +fn make_buf(data: &[u8]) -> Vec { + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(BORROWED_LAMPORTS) + .data(data.to_vec()) + .owner(owner) + .build(); + serialize_buf(&owned) +} + +// Reads the active sequence counter. +fn seq(acc: &BorrowedAccount) -> u32 { + // SAFETY: test helpers only call this on a live borrowed buffer. + unsafe { acc.header.as_ref().sequence.load(Acquire) } +} + +// Returns the active image bytes for direct assertions. +fn data(acc: &BorrowedAccount) -> &[u8] { + &acc.data +} + +#[test] +// `init` should read the active image without changing the sequence. +fn test_init_reads_active_image() { + let mut buf = make_buf(ACTIVE_DATA); + let borrowed = init(&mut buf); + + assert_eq!(data(&borrowed), ACTIVE_DATA); + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); +} + +#[test] +// `translate` should copy the active image into the shadow view, and `commit` should publish it. +fn test_translate_commit_publishes_shadow_image() { + let mut buf = make_buf(ACTIVE_DATA); + let mut borrowed = init(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); + + borrowed.data[0] = ACTIVE_WRITE; + borrowed.commit(); + assert_eq!(seq(&borrowed), COMMITTED_SEQUENCE); + + let borrowed = init(&mut buf); + assert_eq!(data(&borrowed), COMMITTED_DATA); +} + +#[test] +// `rollback` should discard shadow writes and restore the active view. +fn test_translate_rollback_discards_shadow_writes() { + let mut buf = make_buf(COMMIT_DATA); + let mut borrowed = init(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + borrowed.data[0] = ROLLBACK_WRITE; + + // SAFETY: `reset` is paired with the preceding `translate`. + unsafe { borrowed.reset() }; + assert_eq!(seq(&borrowed), INITIAL_SEQUENCE); + assert_eq!(data(&borrowed), COMMIT_DATA); + + let borrowed = init(&mut buf); + assert_eq!(data(&borrowed), COMMIT_DATA); +} diff --git a/solana/account/src/lib.rs b/solana/account/src/lib.rs index 4c0ea4c6..1d2cfc9c 100644 --- a/solana/account/src/lib.rs +++ b/solana/account/src/lib.rs @@ -1,1111 +1,33 @@ -#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] #![cfg_attr(docsrs, feature(doc_cfg))] -//! The Solana [`Account`] type. +#![doc = include_str!("../README.md")] -#[cfg(feature = "dev-context-only-utils")] -use qualifier_attr::qualifiers; -#[cfg(feature = "serde")] -use serde::ser::{Serialize, Serializer}; -#[cfg(feature = "frozen-abi")] -use solana_frozen_abi_macro::{frozen_abi, AbiExample, StableAbi, StableAbiSample}; +mod account; #[cfg(feature = "bincode")] -use solana_sysvar::SysvarSerialize; -use { - solana_account_info::{debug_account_data::*, AccountInfo}, - solana_clock::{Epoch, INITIAL_RENT_EPOCH}, - solana_instruction_error::LamportsError, - solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, - std::{cell::RefCell, fmt, mem::MaybeUninit, ops::Deref, ptr, rc::Rc, sync::Arc}, -}; +mod codec; +mod cow; +mod patch; #[cfg(feature = "bincode")] pub mod state_traits; - -/// An Account with data that is stored on chain -#[repr(C)] -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample, StableAbi, StableAbiSample), - frozen_abi( - api_digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME", - abi_digest = "G4phLpfhujMpk4wS1WswCe4HqnQjCBPWjrXjvDZ6iUw8" - ) -)] -#[cfg_attr( - feature = "serde", - derive(serde_derive::Deserialize), - serde(rename_all = "camelCase") -)] -#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] -#[derive(PartialEq, Eq, Clone, Default)] -pub struct Account { - /// lamports in the account - pub lamports: u64, - /// data held in this account - #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] - #[cfg_attr( - feature = "frozen-abi", - stable_abi_sample( - with = "(0..rng.random_range(0..=1000)).map(|_| rng.random()).collect()" - ) - )] - pub data: Vec, - /// the program that owns this account. If executable, the program that loads this account. - pub owner: Pubkey, - /// this account's data contains a loaded program (and is now read-only) - pub executable: bool, - /// the epoch at which this account will next owe rent - pub rent_epoch: Epoch, -} - -// mod because we need 'Account' below to have the name 'Account' to match expected serialization -#[cfg(feature = "serde")] -mod account_serialize { - #[cfg(feature = "frozen-abi")] - use solana_frozen_abi_macro::{frozen_abi, AbiExample}; - use { - crate::ReadableAccount, - serde::{ser::Serializer, Serialize}, - solana_clock::Epoch, - solana_pubkey::Pubkey, - }; - #[repr(C)] - #[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME") - )] - #[derive(serde_derive::Serialize)] - #[serde(rename_all = "camelCase")] - struct Account<'a> { - lamports: u64, - #[serde(with = "serde_bytes")] - // a slice so we don't have to make a copy just to serialize this - data: &'a [u8], - owner: &'a Pubkey, - executable: bool, - rent_epoch: Epoch, - } - - /// allows us to implement serialize on AccountSharedData that is equivalent to Account::serialize without making a copy of the Vec - pub fn serialize_account( - account: &impl ReadableAccount, - serializer: S, - ) -> Result - where - S: Serializer, - { - let temp = Account { - lamports: account.lamports(), - data: account.data(), - owner: account.owner(), - executable: account.executable(), - rent_epoch: account.rent_epoch(), - }; - temp.serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Account { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - crate::account_serialize::serialize_account(self, serializer) - } -} - -#[cfg(feature = "serde")] -impl Serialize for AccountSharedData { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - crate::account_serialize::serialize_account(self, serializer) - } -} - -/// An Account with data that is stored on chain -/// This will be the in-memory representation of the 'Account' struct data. -/// The existing 'Account' structure cannot easily change due to downstream projects. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] -#[cfg_attr( - feature = "serde", - derive(serde_derive::Deserialize), - serde(from = "Account") -)] -#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] -#[derive(PartialEq, Eq, Clone, Default)] -pub struct AccountSharedData { - /// lamports in the account - lamports: u64, - /// data held in this account - data: Arc>, - /// the program that owns this account. If executable, the program that loads this account. - owner: Pubkey, - /// this account's data contains a loaded program (and is now read-only) - executable: bool, - /// the epoch at which this account will next owe rent - rent_epoch: Epoch, -} - -/// Compares two ReadableAccounts -/// -/// Returns true if accounts are essentially equivalent as in all fields are equivalent. -pub fn accounts_equal(me: &T, other: &U) -> bool { - me.lamports() == other.lamports() - && me.executable() == other.executable() - && me.rent_epoch() == other.rent_epoch() - && me.owner() == other.owner() - && me.data() == other.data() -} - -impl From for Account { - fn from(mut other: AccountSharedData) -> Self { - let account_data = Arc::make_mut(&mut other.data); - Self { - lamports: other.lamports, - data: std::mem::take(account_data), - owner: other.owner, - executable: other.executable, - rent_epoch: other.rent_epoch, - } - } -} - -impl From for AccountSharedData { - fn from(other: Account) -> Self { - Self { - lamports: other.lamports, - data: Arc::new(other.data), - owner: other.owner, - executable: other.executable, - rent_epoch: other.rent_epoch, - } - } -} - -pub trait WritableAccount: ReadableAccount { - fn set_lamports(&mut self, lamports: u64); - fn checked_add_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { - self.set_lamports( - self.lamports() - .checked_add(lamports) - .ok_or(LamportsError::ArithmeticOverflow)?, - ); - Ok(()) - } - fn checked_sub_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { - self.set_lamports( - self.lamports() - .checked_sub(lamports) - .ok_or(LamportsError::ArithmeticUnderflow)?, - ); - Ok(()) - } - fn saturating_add_lamports(&mut self, lamports: u64) { - self.set_lamports(self.lamports().saturating_add(lamports)) - } - fn saturating_sub_lamports(&mut self, lamports: u64) { - self.set_lamports(self.lamports().saturating_sub(lamports)) - } - fn data_as_mut_slice(&mut self) -> &mut [u8]; - fn set_owner(&mut self, owner: Pubkey); - fn copy_into_owner_from_slice(&mut self, source: &[u8]); - fn set_executable(&mut self, executable: bool); - fn set_rent_epoch(&mut self, epoch: Epoch); -} - -pub trait ReadableAccount: Sized { - fn lamports(&self) -> u64; - fn data(&self) -> &[u8]; - fn owner(&self) -> &Pubkey; - fn executable(&self) -> bool; - fn rent_epoch(&self) -> Epoch; -} - -impl ReadableAccount for T -where - T: Deref, - T::Target: ReadableAccount, -{ - fn lamports(&self) -> u64 { - self.deref().lamports() - } - fn data(&self) -> &[u8] { - self.deref().data() - } - fn owner(&self) -> &Pubkey { - self.deref().owner() - } - fn executable(&self) -> bool { - self.deref().executable() - } - fn rent_epoch(&self) -> Epoch { - self.deref().rent_epoch() - } -} - -impl ReadableAccount for Account { - fn lamports(&self) -> u64 { - self.lamports - } - fn data(&self) -> &[u8] { - &self.data - } - fn owner(&self) -> &Pubkey { - &self.owner - } - fn executable(&self) -> bool { - self.executable - } - fn rent_epoch(&self) -> Epoch { - self.rent_epoch - } -} - -impl WritableAccount for Account { - fn set_lamports(&mut self, lamports: u64) { - self.lamports = lamports; - } - fn data_as_mut_slice(&mut self) -> &mut [u8] { - &mut self.data - } - fn set_owner(&mut self, owner: Pubkey) { - self.owner = owner; - } - fn copy_into_owner_from_slice(&mut self, source: &[u8]) { - self.owner.as_mut().copy_from_slice(source); - } - fn set_executable(&mut self, executable: bool) { - self.executable = executable; - } - fn set_rent_epoch(&mut self, epoch: Epoch) { - self.rent_epoch = epoch; - } -} - -impl WritableAccount for AccountSharedData { - fn set_lamports(&mut self, lamports: u64) { - self.lamports = lamports; - } - fn data_as_mut_slice(&mut self) -> &mut [u8] { - &mut self.data_mut()[..] - } - fn set_owner(&mut self, owner: Pubkey) { - self.owner = owner; - } - fn copy_into_owner_from_slice(&mut self, source: &[u8]) { - self.owner.as_mut().copy_from_slice(source); - } - fn set_executable(&mut self, executable: bool) { - self.executable = executable; - } - fn set_rent_epoch(&mut self, epoch: Epoch) { - self.rent_epoch = epoch; - } -} - -impl ReadableAccount for AccountSharedData { - fn lamports(&self) -> u64 { - self.lamports - } - fn data(&self) -> &[u8] { - &self.data - } - fn owner(&self) -> &Pubkey { - &self.owner - } - fn executable(&self) -> bool { - self.executable - } - fn rent_epoch(&self) -> Epoch { - self.rent_epoch - } -} - -fn debug_fmt(item: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut f = f.debug_struct("Account"); - - f.field("lamports", &item.lamports()) - .field("data.len", &item.data().len()) - .field("owner", &item.owner()) - .field("executable", &item.executable()) - .field("rent_epoch", &item.rent_epoch()); - debug_account_data(item.data(), &mut f); - - f.finish() -} - -impl fmt::Debug for Account { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - debug_fmt(self, f) - } -} - -impl fmt::Debug for AccountSharedData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - debug_fmt(self, f) - } -} - -#[cfg(feature = "bincode")] -fn shared_deserialize_data( - account: &U, -) -> Result { - bincode::deserialize(account.data()) -} - #[cfg(feature = "bincode")] -fn shared_serialize_data( - account: &mut U, - state: &T, -) -> Result<(), bincode::Error> { - if bincode::serialized_size(state)? > account.data().len() as u64 { - return Err(Box::new(bincode::ErrorKind::SizeLimit)); - } - bincode::serialize_into(account.data_as_mut_slice(), state) -} - -impl Account { - pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { - Account { - lamports, - data: vec![0; space], - owner: *owner, - executable: false, - rent_epoch: Epoch::default(), - } - } - pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { - Rc::new(RefCell::new(Account::new(lamports, space, owner))) - } - #[cfg(feature = "bincode")] - pub fn new_data( - lamports: u64, - state: &T, - owner: &Pubkey, - ) -> Result { - let data = bincode::serialize(state)?; - Ok(Account { - lamports, - data, - owner: *owner, - executable: false, - rent_epoch: Epoch::default(), - }) - } - #[cfg(feature = "bincode")] - pub fn new_ref_data( - lamports: u64, - state: &T, - owner: &Pubkey, - ) -> Result, bincode::Error> { - Account::new_data(lamports, state, owner).map(RefCell::new) - } - #[cfg(feature = "bincode")] - pub fn new_data_with_space( - lamports: u64, - state: &T, - space: usize, - owner: &Pubkey, - ) -> Result { - let mut account = Account::new(lamports, space, owner); - shared_serialize_data(&mut account, state)?; - Ok(account) - } - #[cfg(feature = "bincode")] - pub fn new_ref_data_with_space( - lamports: u64, - state: &T, - space: usize, - owner: &Pubkey, - ) -> Result, bincode::Error> { - Account::new_data_with_space(lamports, state, space, owner).map(RefCell::new) - } - pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { - Account { - lamports, - data: vec![0; space], - owner: *owner, - executable: false, - rent_epoch, - } - } - #[cfg(feature = "bincode")] - pub fn deserialize_data(&self) -> Result { - shared_deserialize_data(self) - } - #[cfg(feature = "bincode")] - pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { - shared_serialize_data(self, state) - } -} - -impl AccountSharedData { - pub fn is_shared(&self) -> bool { - Arc::strong_count(&self.data) > 1 - } - - pub fn reserve(&mut self, additional: usize) { - if let Some(data) = Arc::get_mut(&mut self.data) { - data.reserve(additional) - } else { - let mut data = Vec::with_capacity(self.data.len().saturating_add(additional)); - data.extend_from_slice(&self.data); - self.data = Arc::new(data); - } - } - - pub fn capacity(&self) -> usize { - self.data.capacity() - } - - pub fn data_clone(&self) -> Arc> { - Arc::clone(&self.data) - } - - fn data_mut(&mut self) -> &mut Vec { - Arc::make_mut(&mut self.data) - } - - pub fn resize(&mut self, new_len: usize, value: u8) { - self.data_mut().resize(new_len, value) - } - - pub fn extend_from_slice(&mut self, data: &[u8]) { - self.data_mut().extend_from_slice(data) - } - - pub fn set_data_from_slice(&mut self, new_data: &[u8]) { - // If the buffer isn't shared, we're going to memcpy in place. - let Some(data) = Arc::get_mut(&mut self.data) else { - // If the buffer is shared, the cheapest thing to do is to clone the - // incoming slice and replace the buffer. - return self.set_data(new_data.to_vec()); - }; - - let new_len = new_data.len(); - - // Reserve additional capacity if needed. Here we make the assumption - // that growing the current buffer is cheaper than doing a whole new - // allocation to make `new_data` owned. - // - // This assumption holds true during CPI, especially when the account - // size doesn't change but the account is only changed in place. And - // it's also true when the account is grown by a small margin (the - // realloc limit is quite low), in which case the allocator can just - // update the allocation metadata without moving. - // - // Shrinking and copying in place is always faster than making - // `new_data` owned, since shrinking boils down to updating the Vec's - // length. - - data.reserve(new_len.saturating_sub(data.len())); - - // Safety: - // We just reserved enough capacity. We set data::len to 0 to avoid - // possible UB on panic (dropping uninitialized elements), do the copy, - // finally set the new length once everything is initialized. - #[allow(clippy::uninit_vec)] - // this is a false positive, the lint doesn't currently special case set_len(0) - unsafe { - data.set_len(0); - ptr::copy_nonoverlapping(new_data.as_ptr(), data.as_mut_ptr(), new_len); - data.set_len(new_len); - }; - } - - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - fn set_data(&mut self, data: Vec) { - self.data = Arc::new(data); - } - - pub fn spare_data_capacity_mut(&mut self) -> &mut [MaybeUninit] { - self.data_mut().spare_capacity_mut() - } - - pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { - AccountSharedData { - lamports, - data: Arc::new(vec![0u8; space]), - owner: *owner, - executable: false, - rent_epoch: Epoch::default(), - } - } - pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { - Rc::new(RefCell::new(AccountSharedData::new(lamports, space, owner))) - } - #[cfg(feature = "bincode")] - pub fn new_data( - lamports: u64, - state: &T, - owner: &Pubkey, - ) -> Result { - let data = bincode::serialize(state)?; - Ok(Self::create_from_existing_shared_data( - lamports, - Arc::new(data), - *owner, - false, - Epoch::default(), - )) - } - #[cfg(feature = "bincode")] - pub fn new_ref_data( - lamports: u64, - state: &T, - owner: &Pubkey, - ) -> Result, bincode::Error> { - AccountSharedData::new_data(lamports, state, owner).map(RefCell::new) - } - #[cfg(feature = "bincode")] - pub fn new_data_with_space( - lamports: u64, - state: &T, - space: usize, - owner: &Pubkey, - ) -> Result { - let mut account = AccountSharedData::new(lamports, space, owner); - shared_serialize_data(&mut account, state)?; - Ok(account) - } - #[cfg(feature = "bincode")] - pub fn new_ref_data_with_space( - lamports: u64, - state: &T, - space: usize, - owner: &Pubkey, - ) -> Result, bincode::Error> { - AccountSharedData::new_data_with_space(lamports, state, space, owner).map(RefCell::new) - } - pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { - AccountSharedData { - lamports, - data: Arc::new(vec![0; space]), - owner: *owner, - executable: false, - rent_epoch, - } - } - #[cfg(feature = "bincode")] - pub fn deserialize_data(&self) -> Result { - shared_deserialize_data(self) - } - #[cfg(feature = "bincode")] - pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { - shared_serialize_data(self, state) - } - - pub fn create_from_existing_shared_data( - lamports: u64, - data: Arc>, - owner: Pubkey, - executable: bool, - rent_epoch: Epoch, - ) -> AccountSharedData { - AccountSharedData { - lamports, - data, - owner, - executable, - rent_epoch, - } - } -} - -pub type InheritableAccountFields = (u64, Epoch); -pub const DUMMY_INHERITABLE_ACCOUNT_FIELDS: InheritableAccountFields = (1, INITIAL_RENT_EPOCH); - -#[cfg(feature = "bincode")] -pub fn create_account_with_fields( - sysvar: &S, - (lamports, rent_epoch): InheritableAccountFields, -) -> Account { - let data_len = S::size_of().max(bincode::serialized_size(sysvar).unwrap() as usize); - let mut account = Account::new(lamports, data_len, &solana_sdk_ids::sysvar::id()); - to_account::(sysvar, &mut account).unwrap(); - account.rent_epoch = rent_epoch; - account -} - -#[cfg(feature = "bincode")] -pub fn create_account_for_test(sysvar: &S) -> Account { - create_account_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) -} - -#[cfg(feature = "bincode")] -/// Create an `Account` from a `Sysvar`. -pub fn create_account_shared_data_with_fields( - sysvar: &S, - fields: InheritableAccountFields, -) -> AccountSharedData { - AccountSharedData::from(create_account_with_fields(sysvar, fields)) -} - -#[cfg(feature = "bincode")] -pub fn create_account_shared_data_for_test(sysvar: &S) -> AccountSharedData { - AccountSharedData::from(create_account_with_fields( - sysvar, - DUMMY_INHERITABLE_ACCOUNT_FIELDS, - )) -} - -#[cfg(feature = "bincode")] -/// Create a `Sysvar` from an `Account`'s data. -pub fn from_account(account: &T) -> Option { - bincode::deserialize(account.data()).ok() -} +mod sysvar; +/// Test-only helpers for borrowed account buffers. +#[cfg(feature = "testkit")] +pub mod testkit; +mod traits; +pub use account::{Account, PROGRAM_OWNERS, create_is_signer_account_infos}; +pub use cow::{ + ALIGNMENT, AccountBuilder, AccountMode, AccountSeqLock, AccountSharedData, BorrowedAccount, + CoWAccount, DirtyMarkers, OwnedAccount, STORAGE_UNIT, StateFlags, StorageUnit, +}; +pub use patch::{AccountFieldPatch, AccountPatchError}; #[cfg(feature = "bincode")] -/// Serialize a `Sysvar` into an `Account`'s data. -pub fn to_account( - sysvar: &S, - account: &mut T, -) -> Option<()> { - bincode::serialize_into(account.data_as_mut_slice(), sysvar).ok() -} - -/// Return the information required to construct an `AccountInfo`. Used by the -/// `AccountInfo` conversion implementations. -impl solana_account_info::Account for Account { - fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool) { - ( - &mut self.lamports, - &mut self.data, - &self.owner, - self.executable, - ) - } -} - -/// Create `AccountInfo`s -pub fn create_is_signer_account_infos<'a>( - accounts: &'a mut [(&'a Pubkey, bool, &'a mut Account)], -) -> Vec> { - accounts - .iter_mut() - .map(|(key, is_signer, account)| { - AccountInfo::new( - key, - *is_signer, - false, - &mut account.lamports, - &mut account.data, - &account.owner, - account.executable, - ) - }) - .collect() -} - -/// Replacement for the executable flag: An account being owned by one of these contains a program. -#[deprecated(since = "4.3.0", note = "no longer available as a constant")] -pub const PROGRAM_OWNERS: &[Pubkey] = &[ - bpf_loader_upgradeable::id(), - bpf_loader::id(), - bpf_loader_deprecated::id(), - loader_v4::id(), -]; +pub use sysvar::{ + DUMMY_INHERITABLE_ACCOUNT_FIELDS, InheritableAccountFields, create_account_for_test, + create_account_shared_data_for_test, create_account_shared_data_with_fields, + create_account_with_fields, from_account, to_account, +}; +pub use traits::{ReadableAccount, WritableAccount, accounts_equal}; #[cfg(test)] -pub mod tests { - use super::*; - - fn make_two_accounts(key: &Pubkey) -> (Account, AccountSharedData) { - let mut account1 = Account::new(1, 2, key); - account1.executable = true; - account1.rent_epoch = 4; - let mut account2 = AccountSharedData::new(1, 2, key); - account2.executable = true; - account2.rent_epoch = 4; - assert!(accounts_equal(&account1, &account2)); - (account1, account2) - } - - #[test] - fn test_account_data_copy_as_slice() { - let key = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let (mut account1, mut account2) = make_two_accounts(&key); - account1.copy_into_owner_from_slice(key2.as_ref()); - account2.copy_into_owner_from_slice(key2.as_ref()); - assert!(accounts_equal(&account1, &account2)); - assert_eq!(account1.owner(), &key2); - } - - #[test] - fn test_account_set_data_from_slice() { - let key = Pubkey::new_unique(); - let (_, mut account) = make_two_accounts(&key); - assert_eq!(account.data(), &vec![0, 0]); - account.set_data_from_slice(&[1, 2]); - assert_eq!(account.data(), &vec![1, 2]); - account.set_data_from_slice(&[1, 2, 3]); - assert_eq!(account.data(), &vec![1, 2, 3]); - account.set_data_from_slice(&[4, 5, 6]); - assert_eq!(account.data(), &vec![4, 5, 6]); - account.set_data_from_slice(&[4, 5, 6, 0]); - assert_eq!(account.data(), &vec![4, 5, 6, 0]); - account.set_data_from_slice(&[]); - assert_eq!(account.data().len(), 0); - account.set_data_from_slice(&[44]); - assert_eq!(account.data(), &vec![44]); - account.set_data_from_slice(&[44]); - assert_eq!(account.data(), &vec![44]); - } - - #[test] - fn test_account_data_set_data() { - let key = Pubkey::new_unique(); - let (_, mut account) = make_two_accounts(&key); - assert_eq!(account.data(), &vec![0, 0]); - account.set_data(vec![1, 2]); - assert_eq!(account.data(), &vec![1, 2]); - account.set_data(vec![]); - assert_eq!(account.data().len(), 0); - } - - #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: Io(Kind(UnexpectedEof))" - )] - fn test_account_deserialize() { - let key = Pubkey::new_unique(); - let (account1, _account2) = make_two_accounts(&key); - account1.deserialize_data::().unwrap(); - } - - #[test] - #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: SizeLimit")] - fn test_account_serialize() { - let key = Pubkey::new_unique(); - let (mut account1, _account2) = make_two_accounts(&key); - account1.serialize_data(&"hello world").unwrap(); - } - - #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: Io(Kind(UnexpectedEof))" - )] - fn test_account_shared_data_deserialize() { - let key = Pubkey::new_unique(); - let (_account1, account2) = make_two_accounts(&key); - account2.deserialize_data::().unwrap(); - } - - #[test] - #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: SizeLimit")] - fn test_account_shared_data_serialize() { - let key = Pubkey::new_unique(); - let (_account1, mut account2) = make_two_accounts(&key); - account2.serialize_data(&"hello world").unwrap(); - } - - #[test] - fn test_account_shared_data() { - let key = Pubkey::new_unique(); - let (account1, account2) = make_two_accounts(&key); - assert!(accounts_equal(&account1, &account2)); - let account = account1; - assert_eq!(account.lamports, 1); - assert_eq!(account.lamports(), 1); - assert_eq!(account.data.len(), 2); - assert_eq!(account.data().len(), 2); - assert_eq!(account.owner, key); - assert_eq!(account.owner(), &key); - assert!(account.executable); - assert!(account.executable()); - assert_eq!(account.rent_epoch, 4); - assert_eq!(account.rent_epoch(), 4); - let account = account2; - assert_eq!(account.lamports, 1); - assert_eq!(account.lamports(), 1); - assert_eq!(account.data.len(), 2); - assert_eq!(account.data().len(), 2); - assert_eq!(account.owner, key); - assert_eq!(account.owner(), &key); - assert!(account.executable); - assert!(account.executable()); - assert_eq!(account.rent_epoch, 4); - assert_eq!(account.rent_epoch(), 4); - } - - // test clone and from for both types against expected - fn test_equal( - should_be_equal: bool, - account1: &Account, - account2: &AccountSharedData, - account_expected: &Account, - ) { - assert_eq!(should_be_equal, accounts_equal(account1, account2)); - if should_be_equal { - assert!(accounts_equal(account_expected, account2)); - } - assert_eq!( - accounts_equal(account_expected, account1), - accounts_equal(account_expected, &account1.clone()) - ); - assert_eq!( - accounts_equal(account_expected, account2), - accounts_equal(account_expected, &account2.clone()) - ); - assert_eq!( - accounts_equal(account_expected, account1), - accounts_equal(account_expected, &AccountSharedData::from(account1.clone())) - ); - assert_eq!( - accounts_equal(account_expected, account2), - accounts_equal(account_expected, &Account::from(account2.clone())) - ); - } - - #[test] - fn test_account_add_sub_lamports() { - let key = Pubkey::new_unique(); - let (mut account1, mut account2) = make_two_accounts(&key); - assert!(accounts_equal(&account1, &account2)); - account1.checked_add_lamports(1).unwrap(); - account2.checked_add_lamports(1).unwrap(); - assert!(accounts_equal(&account1, &account2)); - assert_eq!(account1.lamports(), 2); - account1.checked_sub_lamports(2).unwrap(); - account2.checked_sub_lamports(2).unwrap(); - assert!(accounts_equal(&account1, &account2)); - assert_eq!(account1.lamports(), 0); - } - - #[test] - #[should_panic(expected = "Overflow")] - fn test_account_checked_add_lamports_overflow() { - let key = Pubkey::new_unique(); - let (mut account1, _account2) = make_two_accounts(&key); - account1.checked_add_lamports(u64::MAX).unwrap(); - } - - #[test] - #[should_panic(expected = "Underflow")] - fn test_account_checked_sub_lamports_underflow() { - let key = Pubkey::new_unique(); - let (mut account1, _account2) = make_two_accounts(&key); - account1.checked_sub_lamports(u64::MAX).unwrap(); - } - - #[test] - #[should_panic(expected = "Overflow")] - fn test_account_checked_add_lamports_overflow2() { - let key = Pubkey::new_unique(); - let (_account1, mut account2) = make_two_accounts(&key); - account2.checked_add_lamports(u64::MAX).unwrap(); - } - - #[test] - #[should_panic(expected = "Underflow")] - fn test_account_checked_sub_lamports_underflow2() { - let key = Pubkey::new_unique(); - let (_account1, mut account2) = make_two_accounts(&key); - account2.checked_sub_lamports(u64::MAX).unwrap(); - } - - #[test] - fn test_account_saturating_add_lamports() { - let key = Pubkey::new_unique(); - let (mut account, _) = make_two_accounts(&key); - - let remaining = 22; - account.set_lamports(u64::MAX - remaining); - account.saturating_add_lamports(remaining * 2); - assert_eq!(account.lamports(), u64::MAX); - } - - #[test] - fn test_account_saturating_sub_lamports() { - let key = Pubkey::new_unique(); - let (mut account, _) = make_two_accounts(&key); - - let remaining = 33; - account.set_lamports(remaining); - account.saturating_sub_lamports(remaining * 2); - assert_eq!(account.lamports(), 0); - } - - #[test] - fn test_account_shared_data_all_fields() { - let key = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let key3 = Pubkey::new_unique(); - let (mut account1, mut account2) = make_two_accounts(&key); - assert!(accounts_equal(&account1, &account2)); - - let mut account_expected = account1.clone(); - assert!(accounts_equal(&account1, &account_expected)); - assert!(accounts_equal(&account1, &account2.clone())); // test the clone here - - for field_index in 0..5 { - for pass in 0..4 { - if field_index == 0 { - if pass == 0 { - account1.checked_add_lamports(1).unwrap(); - } else if pass == 1 { - account_expected.checked_add_lamports(1).unwrap(); - account2.set_lamports(account2.lamports + 1); - } else if pass == 2 { - account1.set_lamports(account1.lamports + 1); - } else if pass == 3 { - account_expected.checked_add_lamports(1).unwrap(); - account2.checked_add_lamports(1).unwrap(); - } - } else if field_index == 1 { - if pass == 0 { - account1.data[0] += 1; - } else if pass == 1 { - account_expected.data[0] += 1; - account2.data_as_mut_slice()[0] = account2.data[0] + 1; - } else if pass == 2 { - account1.data_as_mut_slice()[0] = account1.data[0] + 1; - } else if pass == 3 { - account_expected.data[0] += 1; - account2.data_as_mut_slice()[0] += 1; - } - } else if field_index == 2 { - if pass == 0 { - account1.owner = key2; - } else if pass == 1 { - account_expected.owner = key2; - account2.set_owner(key2); - } else if pass == 2 { - account1.set_owner(key3); - } else if pass == 3 { - account_expected.owner = key3; - account2.owner = key3; - } - } else if field_index == 3 { - if pass == 0 { - account1.executable = !account1.executable; - } else if pass == 1 { - account_expected.executable = !account_expected.executable; - account2.set_executable(!account2.executable); - } else if pass == 2 { - account1.set_executable(!account1.executable); - } else if pass == 3 { - account_expected.executable = !account_expected.executable; - account2.executable = !account2.executable; - } - } else if field_index == 4 { - if pass == 0 { - account1.rent_epoch += 1; - } else if pass == 1 { - account_expected.rent_epoch += 1; - account2.set_rent_epoch(account2.rent_epoch + 1); - } else if pass == 2 { - account1.set_rent_epoch(account1.rent_epoch + 1); - } else if pass == 3 { - account_expected.rent_epoch += 1; - account2.rent_epoch += 1; - } - } - - let should_be_equal = pass == 1 || pass == 3; - test_equal(should_be_equal, &account1, &account2, &account_expected); - - // test new_ref - if should_be_equal { - assert!(accounts_equal( - &Account::new_ref( - account_expected.lamports(), - account_expected.data().len(), - account_expected.owner() - ) - .borrow(), - &AccountSharedData::new_ref( - account_expected.lamports(), - account_expected.data().len(), - account_expected.owner() - ) - .borrow() - )); - - { - // test new_data - let account1_with_data = Account::new_data( - account_expected.lamports(), - &account_expected.data()[0], - account_expected.owner(), - ) - .unwrap(); - let account2_with_data = AccountSharedData::new_data( - account_expected.lamports(), - &account_expected.data()[0], - account_expected.owner(), - ) - .unwrap(); - - assert!(accounts_equal(&account1_with_data, &account2_with_data)); - assert_eq!( - account1_with_data.deserialize_data::().unwrap(), - account2_with_data.deserialize_data::().unwrap() - ); - } - - // test new_data_with_space - assert!(accounts_equal( - &Account::new_data_with_space( - account_expected.lamports(), - &account_expected.data()[0], - 1, - account_expected.owner() - ) - .unwrap(), - &AccountSharedData::new_data_with_space( - account_expected.lamports(), - &account_expected.data()[0], - 1, - account_expected.owner() - ) - .unwrap() - )); - - // test new_ref_data - assert!(accounts_equal( - &Account::new_ref_data( - account_expected.lamports(), - &account_expected.data()[0], - account_expected.owner() - ) - .unwrap() - .borrow(), - &AccountSharedData::new_ref_data( - account_expected.lamports(), - &account_expected.data()[0], - account_expected.owner() - ) - .unwrap() - .borrow() - )); - - //new_ref_data_with_space - assert!(accounts_equal( - &Account::new_ref_data_with_space( - account_expected.lamports(), - &account_expected.data()[0], - 1, - account_expected.owner() - ) - .unwrap() - .borrow(), - &AccountSharedData::new_ref_data_with_space( - account_expected.lamports(), - &account_expected.data()[0], - 1, - account_expected.owner() - ) - .unwrap() - .borrow() - )); - } - } - } - } -} +mod tests; diff --git a/solana/account/src/patch.rs b/solana/account/src/patch.rs new file mode 100644 index 00000000..60a33579 --- /dev/null +++ b/solana/account/src/patch.rs @@ -0,0 +1,107 @@ +use core::fmt; + +use solana_clock::Slot; +use solana_pubkey::Pubkey; + +use crate::{AccountMode, AccountSharedData, OwnedAccount, WritableAccount}; + +const MAX_DATA_CHUNK_SIZE: usize = (u16::MAX - 256) as usize; + +/// Failure to apply an account lifecycle or ordering patch. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +pub enum AccountPatchError { + /// The requested account mode transition is not part of the lifecycle. + #[error("invalid account mode transition: {from:?} -> {to:?}")] + InvalidModeTransition { + /// Current account mode. + from: AccountMode, + /// Requested account mode. + to: AccountMode, + }, + /// The requested slot neither advances nor accompanies a mode transition. + #[error("invalid account slot transition: {from} -> {to}")] + InvalidSlotTransition { + /// Current account slot. + from: Slot, + /// Requested account slot. + to: Slot, + }, +} + +/// A single-field account patch. +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +pub enum AccountFieldPatch { + /// Replaces the lamport balance. + Lamports(u64), + /// Replaces the owner. + Owner(Pubkey), + /// Writes bytes starting at `offset`, extending the account data if needed. + DataAt { + /// Byte offset into the current data buffer. + offset: usize, + /// Bytes to write. + data: Vec, + }, + /// Replaces the slot. + Slot(Slot), + /// Replaces the account mode. + Mode(AccountMode), + /// Resizes the data buffer to an exact length, zero-filling when growing. + DataLen(usize), +} + +impl AccountFieldPatch { + /// Applies this patch to `account`. + /// + /// The account methods mark dirtiness and preserve the writable invariants. + /// Invalid mode and slot transitions leave the account unchanged and return + /// their transition context. + pub fn apply(self, account: &mut AccountSharedData) -> Result<(), AccountPatchError> { + match self { + Self::Lamports(v) => account.set_lamports(v), + Self::Slot(v) => return account.set_slot(v), + Self::Owner(v) => account.set_owner(v), + Self::Mode(v) => return account.set_mode(v), + Self::DataAt { offset, data } => account.set_data_at(offset, &data), + Self::DataLen(len) => account.resize(len, 0), + } + Ok(()) + } + + /// Decomposes an owned account into the ordered sequence of patches that + /// reconstruct its non-flag fields: lamports, mode, slot, owner, the exact + /// data length, then the data in `MAX_DATA_CHUNK_SIZE`-sized chunks. + /// + /// Mode precedes slot so consumers can distinguish an equal-slot mode + /// transition from a duplicate replacement by inspecting the mode dirty + /// marker when they apply the slot patch. + pub fn sequence(account: OwnedAccount) -> Vec { + let mut sequence = Vec::with_capacity(6); + sequence.push(Self::Lamports(account.core.lamports)); + sequence.push(Self::Mode(account.core.mode)); + sequence.push(Self::Slot(account.core.slot)); + sequence.push(Self::Owner(account.core.owner)); + sequence.push(Self::DataLen(account.data.len())); + let mut offset = 0; + for data in account.data.chunks(MAX_DATA_CHUNK_SIZE) { + sequence.push(Self::DataAt { offset, data: data.into() }); + offset += data.len(); + } + sequence + } +} + +/// Concise, log-friendly rendering: scalar fields show their value, `DataAt` +/// shows only `offset+len` (never the raw bytes). +impl fmt::Debug for AccountFieldPatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Lamports(v) => write!(f, "lamports={v}"), + Self::Owner(v) => write!(f, "owner={v}"), + Self::Slot(v) => write!(f, "slot={v}"), + Self::Mode(v) => write!(f, "mode={v:?}"), + Self::DataAt { offset, data } => write!(f, "data@{offset}+{}", data.len()), + Self::DataLen(len) => write!(f, "data_len={len}"), + } + } +} diff --git a/solana/account/src/state_traits.rs b/solana/account/src/state_traits.rs index a7852a9a..4441f445 100644 --- a/solana/account/src/state_traits.rs +++ b/solana/account/src/state_traits.rs @@ -1,51 +1,54 @@ -//! Useful extras for `Account` state. +//! Typed bincode access to account data. use { - crate::{Account, AccountSharedData}, + crate::{AccountSharedData, ReadableAccount, WritableAccount}, bincode::ErrorKind, solana_instruction_error::InstructionError, std::cell::Ref, }; -/// Convenience trait to covert bincode errors to instruction errors. +/// Reads and writes typed account state through a mutable account handle. pub trait StateMut { + /// Deserializes the account data as `T`. fn state(&self) -> Result; + + /// Serializes `state` into the existing account data buffer. fn set_state(&mut self, state: &T) -> Result<(), InstructionError>; } -pub trait State { - fn state(&self) -> Result; - fn set_state(&self, state: &T) -> Result<(), InstructionError>; + +/// Deserializes typed state from account data. +/// +/// Invalid bytes map to `InstructionError::InvalidAccountData`. +fn state(account: &impl ReadableAccount) -> Result +where + T: serde::de::DeserializeOwned, +{ + crate::codec::deserialize_data(account).map_err(|_| InstructionError::InvalidAccountData) } -impl StateMut for Account +/// Serializes typed state into an existing account buffer. +/// +/// Oversized payloads map to `AccountDataTooSmall`; all other failures map to `GenericError`. +fn set_state(account: &mut impl WritableAccount, state: &T) -> Result<(), InstructionError> where - T: serde::Serialize + serde::de::DeserializeOwned, + T: serde::Serialize, { - fn state(&self) -> Result { - self.deserialize_data() - .map_err(|_| InstructionError::InvalidAccountData) - } - fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { - self.serialize_data(state).map_err(|err| match *err { - ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, - _ => InstructionError::GenericError, - }) - } + crate::codec::serialize_data(account, state).map_err(|err| match *err { + ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) } -impl StateMut for AccountSharedData +impl StateMut for A where + A: ReadableAccount + WritableAccount, T: serde::Serialize + serde::de::DeserializeOwned, { fn state(&self) -> Result { - self.deserialize_data() - .map_err(|_| InstructionError::InvalidAccountData) + state(self) } fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { - self.serialize_data(state).map_err(|err| match *err { - ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, - _ => InstructionError::GenericError, - }) + set_state(self, state) } } @@ -54,30 +57,9 @@ where T: serde::Serialize + serde::de::DeserializeOwned, { fn state(&self) -> Result { - self.deserialize_data() - .map_err(|_| InstructionError::InvalidAccountData) + state(&**self) } fn set_state(&mut self, _state: &T) -> Result<(), InstructionError> { - panic!("illegal"); - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_pubkey::Pubkey}; - - #[test] - fn test_account_state() { - let state = 42u64; - - assert!(AccountSharedData::default().set_state(&state).is_err()); - let res = AccountSharedData::default().state() as Result; - assert!(res.is_err()); - - let mut account = AccountSharedData::new(0, std::mem::size_of::(), &Pubkey::default()); - - assert!(account.set_state(&state).is_ok()); - let stored_state: u64 = account.state().unwrap(); - assert_eq!(stored_state, state); + Err(InstructionError::ReadonlyDataModified) } } diff --git a/solana/account/src/sysvar.rs b/solana/account/src/sysvar.rs new file mode 100644 index 00000000..ad108e3a --- /dev/null +++ b/solana/account/src/sysvar.rs @@ -0,0 +1,70 @@ +use { + crate::{Account, AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::{Epoch, INITIAL_RENT_EPOCH}, + solana_sysvar::SysvarSerialize, +}; + +/// Fields copied into test sysvar accounts. +pub type InheritableAccountFields = (u64, Epoch); + +/// Default lamports and rent epoch used by test sysvar account helpers. +pub const DUMMY_INHERITABLE_ACCOUNT_FIELDS: InheritableAccountFields = (1, INITIAL_RENT_EPOCH); + +/// Serializes a sysvar into account data and pads it to the declared size. +/// +/// Serialization failure falls back to zeroed bytes sized to `S::size_of()`. +/// That keeps the helper infallible while preserving the advertised layout. +fn account_data(sysvar: &S) -> Vec { + let mut data = bincode::serialize(sysvar).unwrap_or_default(); + data.resize(data.len().max(S::size_of()), 0); + data +} + +/// Creates an [`Account`] that contains a serialized sysvar value. +pub fn create_account_with_fields( + sysvar: &S, + (lamports, rent_epoch): InheritableAccountFields, +) -> Account { + Account { + lamports, + data: account_data(sysvar), + owner: solana_sdk_ids::sysvar::id(), + executable: false, + rent_epoch, + } +} + +/// Creates a test sysvar [`Account`]. +pub fn create_account_for_test(sysvar: &S) -> Account { + create_account_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) +} + +/// Creates an [`AccountSharedData`] that contains a serialized sysvar value. +pub fn create_account_shared_data_with_fields( + sysvar: &S, + fields: InheritableAccountFields, +) -> AccountSharedData { + AccountSharedData::from(create_account_with_fields(sysvar, fields)) +} + +/// Creates a test sysvar [`AccountSharedData`]. +pub fn create_account_shared_data_for_test(sysvar: &S) -> AccountSharedData { + create_account_shared_data_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) +} + +/// Deserializes a sysvar value from account data. +/// +/// Returns `None` on decode failure. +pub fn from_account(account: &T) -> Option { + bincode::deserialize(account.data()).ok() +} + +/// Serializes a sysvar value into account data. +/// +/// Returns `None` on encode failure. +pub fn to_account( + sysvar: &S, + account: &mut T, +) -> Option<()> { + bincode::serialize_into(account.data_as_mut_slice(), sysvar).ok() +} diff --git a/solana/account/src/testkit.rs b/solana/account/src/testkit.rs new file mode 100644 index 00000000..f78ba8de --- /dev/null +++ b/solana/account/src/testkit.rs @@ -0,0 +1,47 @@ +#![allow(clippy::expect_used)] + +use { + crate::{ + AccountBuilder, AccountSharedData, BorrowedAccount, OwnedAccount, ReadableAccount, + StorageUnit, + }, + solana_pubkey::Pubkey, + std::ptr::NonNull, +}; + +/// Builds a borrowed account image backed by serialized owned state. +pub fn borrowed_account_buffer(data: Vec, owner: Pubkey) -> Vec { + let pubkey = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(1) + .data(data) + .owner(owner) + .build::(); + serialize_account_buffer(&owned, &pubkey) +} + +/// Serializes an owned account into the borrowed layout used by tests. +pub fn serialize_account_buffer(owned: &OwnedAccount, pubkey: &Pubkey) -> Vec { + let mut buf = vec![Default::default(); owned.units() as usize]; + // SAFETY: `buf` is sized from `units` and allocated with the alignment + // required by the borrowed account layout. + unsafe { owned.serialize(&mut buf, pubkey) }; + buf +} + +/// Builds a borrowed account view over a test buffer. +pub fn init_borrowed_account(buf: &mut [StorageUnit]) -> BorrowedAccount { + let ptr = NonNull::from(&mut buf[..]).cast(); + // SAFETY: test buffers are created with the borrowed account layout. + unsafe { BorrowedAccount::init(ptr) } +} + +/// Wraps a borrowed account test buffer in `AccountSharedData`. +pub fn borrowed_shared_data(buf: &mut [StorageUnit]) -> AccountSharedData { + AccountSharedData::from(init_borrowed_account(buf)) +} + +/// Reinitializes a borrowed test buffer and returns its active data image. +pub fn active_borrowed_data(buf: &mut [StorageUnit]) -> Vec { + borrowed_shared_data(buf).data().to_vec() +} diff --git a/solana/account/src/tests/account.rs b/solana/account/src/tests/account.rs new file mode 100644 index 00000000..4cf2e602 --- /dev/null +++ b/solana/account/src/tests/account.rs @@ -0,0 +1,504 @@ +use crate::{ + Account, AccountBuilder, AccountFieldPatch, AccountMode, AccountPatchError, AccountSeqLock, + AccountSharedData, BorrowedAccount, CoWAccount, DirtyMarkers, OwnedAccount, ReadableAccount, + StorageUnit, WritableAccount, accounts_equal, + testkit::{init_borrowed_account, serialize_account_buffer}, +}; +use bincode::ErrorKind; +use solana_clock::Epoch; +use solana_instruction_error::LamportsError; +use solana_pubkey::Pubkey; +use std::{ + cell::{Cell, RefCell}, + ptr::NonNull, +}; + +// Builds matching owned and shared accounts for baseline assertions. +fn make_two_accounts() -> (Pubkey, Account, AccountSharedData) { + let key = Pubkey::new_unique(); + let mut account = Account::new(1, 2, &key); + account.executable = true; + account.rent_epoch = Epoch::MAX; + + let mut shared = AccountSharedData::new(1, 2, &key); + shared.set_executable(true); + shared.set_rent_epoch(4); + + assert!(accounts_equal(&account, &shared)); + (key, account, shared) +} + +// Builds a borrowed account image backed by serialized owned state. +fn make_borrowed(data: Vec) -> (Vec, AccountSharedData) { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(data).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + (buf, AccountSharedData::from(borrowed)) +} + +fn assert_add_err(mut account: T) { + assert!(matches!( + account.checked_add_lamports(u64::MAX), + Err(LamportsError::ArithmeticOverflow) + )); +} + +fn assert_sub_err(mut account: T) { + assert!(matches!( + account.checked_sub_lamports(u64::MAX), + Err(LamportsError::ArithmeticUnderflow) + )); +} + +fn assert_saturating_add( + mut account: T, + start: u64, + add: u64, + expected: u64, +) { + account.set_lamports(start); + account.saturating_add_lamports(add); + assert_eq!(account.lamports(), expected); +} + +fn assert_saturating_sub( + mut account: T, + start: u64, + sub: u64, + expected: u64, +) { + account.set_lamports(start); + account.saturating_sub_lamports(sub); + assert_eq!(account.lamports(), expected); +} + +#[test] +// Owner bytes should copy into both account representations identically. +fn test_account_data_copy_as_slice() { + let key2 = Pubkey::new_unique(); + let (_, mut account1, mut account2) = make_two_accounts(); + account1.copy_into_owner_from_slice(key2.as_ref()); + account2.copy_into_owner_from_slice(key2.as_ref()); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.owner(), &key2); +} + +#[test] +// set_data_from_slice should overwrite, grow, shrink, and preserve contents. +fn test_account_set_data_from_slice() { + let (_, _, mut account) = make_two_accounts(); + assert_eq!(account.data(), &[0, 0]); + account.set_data_from_slice(&[1, 2]); + assert_eq!(account.data(), &[1, 2]); + account.set_data_from_slice(&[1, 2, 3]); + assert_eq!(account.data(), &[1, 2, 3]); + account.set_data_from_slice(&[4, 5, 6]); + assert_eq!(account.data(), &[4, 5, 6]); + account.set_data_from_slice(&[4, 5, 6, 0]); + assert_eq!(account.data(), &[4, 5, 6, 0]); + account.set_data_from_slice(&[]); + assert_eq!(account.data(), &[]); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &[44]); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &[44]); +} + +#[test] +// set_data_at only writes and extends, so an empty write leaves the buffer as is. +fn test_account_set_data_at_never_truncates() { + let (_, _, mut account) = make_two_accounts(); + assert_eq!(account.data(), &[0, 0]); + account.set_data_at(0, &[1, 2]); + assert_eq!(account.data(), &[1, 2]); + account.set_data_at(0, &[]); + assert_eq!(account.data(), &[1, 2]); +} + +#[test] +// Data patches should write in place and extend when needed. +fn test_account_field_patch_data_at() { + let owner = Pubkey::new_unique(); + let mut account = AccountSharedData::new(1, 2, &owner); + account.set_data_from_slice(&[1, 2, 3, 4]); + + AccountFieldPatch::DataAt { + offset: 1, + data: vec![9, 8, 7, 6], + } + .apply(&mut account) + .unwrap(); + assert_eq!(account.data(), &[1, 9, 8, 7, 6]); + + AccountFieldPatch::DataAt { offset: 6, data: vec![5, 4] } + .apply(&mut account) + .unwrap(); + assert_eq!(account.data(), &[1, 9, 8, 7, 6, 0, 5, 4]); +} + +#[test] +fn test_account_patch_transition_errors() { + let mut account = AccountBuilder::default() + .mode(AccountMode::Delegated) + .slot(10) + .build::(); + + assert_eq!( + account.set_mode(AccountMode::ReadOnly), + Err(AccountPatchError::InvalidModeTransition { + from: AccountMode::Delegated, + to: AccountMode::ReadOnly, + }) + ); + assert!(account.is(AccountMode::Delegated)); + assert!(account.markers().is_empty()); + + assert_eq!( + AccountFieldPatch::Slot(10).apply(&mut account), + Err(AccountPatchError::InvalidSlotTransition { from: 10, to: 10 }) + ); + assert_eq!(account.slot(), 10); + assert!(account.markers().is_empty()); + + account.set_mode(AccountMode::Transient).unwrap(); + AccountFieldPatch::Slot(10).apply(&mut account).unwrap(); + assert!(account.markers().contains(DirtyMarkers::MODE)); + assert!(account.markers().contains(DirtyMarkers::SLOT)); + + let markers = *account.markers(); + assert_eq!( + AccountFieldPatch::Slot(9).apply(&mut account), + Err(AccountPatchError::InvalidSlotTransition { from: 10, to: 9 }) + ); + assert_eq!(account.slot(), 10); + assert_eq!(*account.markers(), markers); + + let mut ephemeral = AccountBuilder::default() + .mode(AccountMode::Ephemeral) + .build::(); + ephemeral.set_mode(AccountMode::Closed).unwrap(); + assert!(ephemeral.is(AccountMode::Closed)); +} + +#[test] +// Deserialization should fail on a non-bincode payload. +fn test_account_deserialize() { + let (_, account1, _) = make_two_accounts(); + assert!(account1.deserialize_data::().is_err()); +} + +#[test] +// Serialization should reject values larger than the data buffer. +fn test_account_serialize() { + let (_, mut account1, _) = make_two_accounts(); + let err = account1.serialize_data(&"hello world").unwrap_err(); + assert!(matches!(*err, ErrorKind::SizeLimit)); +} + +#[test] +// Shared accounts should fail deserialization on the same invalid payload. +fn test_account_cow_deserialize() { + let (_, _, account2) = make_two_accounts(); + assert!(account2.deserialize_data::().is_err()); +} + +#[test] +// Shared accounts should reject oversized serialization too. +fn test_account_cow_serialize() { + let (_, _, mut account2) = make_two_accounts(); + let err = account2.serialize_data(&"hello world").unwrap_err(); + assert!(matches!(*err, ErrorKind::SizeLimit)); +} + +#[test] +// Account and AccountSharedData should expose the same visible state. +fn test_account_cow() { + let (key, account1, account2) = make_two_accounts(); + assert!(accounts_equal(&account1, &account2)); + + assert_eq!(account1.lamports, 1); + assert_eq!(account1.lamports(), 1); + assert_eq!(account1.data.len(), 2); + assert_eq!(account1.data().len(), 2); + assert_eq!(account1.owner, key); + assert_eq!(account1.owner(), &key); + assert!(account1.executable); + assert!(account1.executable()); + assert_eq!(account1.rent_epoch, Epoch::MAX); + assert_eq!(account1.rent_epoch(), Epoch::MAX); + + assert_eq!(account2.lamports(), 1); + assert_eq!(account2.data().len(), 2); + assert_eq!(account2.owner(), &key); + assert!(account2.executable()); + assert_eq!(account2.rent_epoch(), Epoch::MAX); +} + +#[test] +// Checked lamport mutation should keep both account forms in sync. +fn test_account_add_sub_lamports() { + let (_, mut account1, mut account2) = make_two_accounts(); + assert!(accounts_equal(&account1, &account2)); + assert!(matches!(account1.checked_add_lamports(1), Ok(()))); + assert!(matches!(account2.checked_add_lamports(1), Ok(()))); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 2); + assert!(matches!(account1.checked_sub_lamports(2), Ok(()))); + assert!(matches!(account2.checked_sub_lamports(2), Ok(()))); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 0); +} + +#[test] +// Checked lamport arithmetic should report overflow and underflow. +fn test_account_checked_lamport_errors() { + let (_, account1, account2) = make_two_accounts(); + + assert_add_err(account1.clone()); + assert_sub_err(account1); + assert_add_err(account2.clone()); + assert_sub_err(account2); +} + +#[test] +// Saturating lamport arithmetic should clamp on both account forms. +fn test_account_saturating_lamports() { + let (_, account1, account2) = make_two_accounts(); + + assert_saturating_add(account1.clone(), u64::MAX - 22, 44, u64::MAX); + assert_saturating_add(account2.clone(), u64::MAX - 22, 44, u64::MAX); + assert_saturating_sub(account1, 33, 66, 0); + assert_saturating_sub(account2, 33, 66, 0); +} + +#[test] +// Shrinking data should replace the contents and allow regrowth. +fn test_account_cow_set_data_from_slice_shrinks() { + let owner = Pubkey::new_unique(); + let mut shared = AccountSharedData::new(1, 4, &owner); + + shared.set_data_from_slice(&[1, 2, 3, 4]); + assert_eq!(shared.data(), &[1, 2, 3, 4]); + + shared.set_data_from_slice(&[]); + assert_eq!(shared.data(), &[]); + + shared.set_data_from_slice(&[9]); + assert_eq!(shared.data(), &[9]); +} + +#[test] +// Cloning should share storage until a write forces promotion. +fn test_account_cow_is_copy_on_write() { + let owner = Pubkey::new_unique(); + let mut shared = AccountSharedData::new(1, 2, &owner); + shared.set_data_from_slice(&[1, 2]); + + let cloned = shared.clone(); + assert!(shared.is_shared()); + assert!(cloned.is_shared()); + + shared.extend_from_slice(&[3]); + assert_eq!(shared.data(), &[1, 2, 3]); + assert_eq!(cloned.data(), &[1, 2]); +} + +#[test] +// Borrowed serialization should round-trip back to shared state. +fn test_account_cow_borrowed_round_trip() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default() + .lamports(5) + .data(vec![7, 8]) + .owner(owner) + .executable(true) + .build::(); + let expected: AccountSharedData = owned.clone().into(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + let shared = AccountSharedData::from(borrowed); + + assert_eq!(shared, expected); +} + +#[test] +// Writing past borrowed capacity should promote to owned storage. +fn test_account_cow_borrowed_extend_promotes() { + let (_buf, mut shared) = make_borrowed(vec![7, 8]); + let len = shared.data().len(); + let extra = vec![9; shared.capacity() - len + 1]; + + shared.extend_from_slice(&extra); + + assert_eq!(&shared.data()[..len], &[7, 8]); + assert_eq!(&shared.data()[len..], extra.as_slice()); + assert!(matches!(shared.cow(), CoWAccount::Owned(_))); +} + +#[test] +// Exact-capacity borrowed writes should keep the existing bytes intact. +fn test_account_cow_borrowed_exact_capacity_writes() { + let (_buf, mut shared) = make_borrowed(vec![1, 2]); + let snap = shared.data_clone(); + let cap = shared.capacity(); + + assert!(cap > shared.data().len()); + + shared.resize(cap, 0x55); + assert_eq!(shared.data().len(), cap); + assert_eq!(&shared.data()[..2], &[1, 2]); + assert!(shared.data()[2..].iter().all(|&b| b == 0x55)); + + let repl = vec![0x9a; cap]; + shared.set_data_from_slice(&repl); + assert_eq!(shared.data(), repl.as_slice()); + assert_eq!(snap.as_ref(), &[1, 2]); +} + +#[test] +// Borrowed resize must write the shadow image before commit publishes it. +fn test_account_cow_borrowed_resize_survives_commit() { + let (mut buf, mut shared) = make_borrowed(vec![1, 2]); + let cap = shared.capacity(); + + shared.resize(cap, 0x55); + let CoWAccount::Borrowed(borrowed) = shared.cow() else { + panic!("resize within borrowed capacity should not promote"); + }; + borrowed.commit(); + drop(shared); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(borrowed.data.len(), cap); + assert_eq!(&borrowed.data[..2], &[1, 2]); + assert!(borrowed.data[2..].iter().all(|&b| b == 0x55)); +} + +#[test] +// Overflowing borrowed writes should preserve existing bytes through promotion. +fn test_account_cow_borrowed_overflow_promotes_without_corruption() { + let (_buf, mut shared) = make_borrowed(vec![3, 4]); + let snap = shared.data_clone(); + let cap = shared.capacity(); + let extra = vec![0xab; cap - shared.data().len() + 1]; + let mut exp = vec![3, 4]; + exp.extend_from_slice(&extra); + + shared.extend_from_slice(&extra); + + assert_eq!(shared.data(), exp.as_slice()); + assert_eq!(snap.as_ref(), &[3, 4]); +} + +#[test] +fn test_cow_set_data_at_borrowed_promotes_once() { + // In-place overlap: `offset < len` writes entirely through `data_as_mut_slice`. + let (_buf, mut shared) = make_borrowed(vec![1, 2, 3, 4]); + shared.set_data_at(1, &[9, 9]); + assert_eq!(shared.data(), &[1, 9, 9, 4]); + assert!(matches!(shared.cow(), CoWAccount::Borrowed(_))); + + // An overlapping write with a tail beyond capacity must promote once. + let (_buf, mut shared) = make_borrowed(vec![1, 2, 3]); + let offset = 2; + let data = vec![7; shared.capacity() - offset + 1]; + shared.set_data_at(offset, &data); + assert_eq!(&shared.data()[..offset], &[1, 2]); + assert_eq!(&shared.data()[offset..], data.as_slice()); + assert!(matches!(shared.cow(), CoWAccount::Owned(_))); +} + +#[test] +// `init` should read the active image without changing the sequence. +fn test_cow_init_reads_active_image() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let borrowed = init_borrowed_account(&mut buf); + + assert_eq!(&*borrowed.data, &[1, 2, 3]); + assert_eq!(borrowed.sequence(), 0); +} + +#[test] +// `translate` should copy the active image into the shadow view, and `commit` should publish it. +fn test_cow_translate_commit_publishes_shadow_image() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let mut borrowed = init_borrowed_account(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + assert_eq!(borrowed.sequence(), 0); + + borrowed.data[0] = 9; + borrowed.commit(); + assert_eq!(borrowed.sequence(), 1); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(&*borrowed.data, &[9, 2, 3]); +} + +#[test] +// Reset should discard shadow writes and re-read the active image. +fn test_cow_translate_rollback_discards_shadow_writes() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![4, 5, 6]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let mut borrowed = init_borrowed_account(&mut buf); + + // SAFETY: `borrowed` still points at the live borrowed image selected by `init`. + unsafe { borrowed.translate() }; + borrowed.data[0] = 8; + + // SAFETY: `reset` only repoints this view back to the active image. + unsafe { borrowed.reset() }; + assert_eq!(borrowed.sequence(), 0); + assert_eq!(&*borrowed.data, &[4, 5, 6]); + + let borrowed = init_borrowed_account(&mut buf); + assert_eq!(&*borrowed.data, &[4, 5, 6]); +} + +#[test] +// AccountSeqLock should retry against the newly published borrowed image. +fn test_account_seq_lock_read_retries_after_borrowed_publish() { + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let owned = AccountBuilder::default().lamports(5).data(vec![1, 2, 3]).owner(owner).build(); + let mut buf = serialize_account_buffer(&owned, &pubkey); + let ptr = NonNull::from(buf.as_mut_slice()).cast(); + // SAFETY: `ptr` names the live serialized buffer for the duration of both + // views. Their intentionally aliased access is sequenced by the test: the + // reader is idle while the writer publishes, then resets before retrying. + let borrowed = unsafe { BorrowedAccount::init(ptr) }; + // SAFETY: same buffer and access protocol as the reader view above. + let writer = RefCell::new(unsafe { BorrowedAccount::init(ptr) }); + let mut lock = AccountSeqLock::new(AccountSharedData::from(borrowed)); + let calls = Cell::new(0); + + let data = lock.read(|account| { + let call = calls.get(); + calls.set(call + 1); + + if call == 0 { + let mut writer = writer.borrow_mut(); + // SAFETY: the writer still points at the image selected by `init`. + unsafe { writer.translate() }; + writer.data[0] = 9; + writer.commit(); + } + + account.data().to_vec() + }); + + assert_eq!(calls.get(), 2); + assert_eq!(data, vec![9, 2, 3]); +} diff --git a/solana/account/src/tests/mod.rs b/solana/account/src/tests/mod.rs new file mode 100644 index 00000000..5a5acba7 --- /dev/null +++ b/solana/account/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod account; +mod state_traits; +#[cfg(feature = "bincode")] +mod sysvar; diff --git a/solana/account/src/tests/state_traits.rs b/solana/account/src/tests/state_traits.rs new file mode 100644 index 00000000..1490cea8 --- /dev/null +++ b/solana/account/src/tests/state_traits.rs @@ -0,0 +1,18 @@ +use { + crate::{AccountSharedData, state_traits::StateMut}, + solana_instruction_error::InstructionError, + solana_pubkey::Pubkey, +}; + +#[test] +fn test_account_state() { + let state = 42; + assert!(AccountSharedData::default().set_state(&state).is_err()); + let res = AccountSharedData::default().state() as Result; + assert!(res.is_err()); + + let mut account = AccountSharedData::new(0, size_of::(), &Pubkey::default()); + + assert!(account.set_state(&state).is_ok()); + assert_eq!(account.state(), Ok(state)); +} diff --git a/solana/account/src/tests/sysvar.rs b/solana/account/src/tests/sysvar.rs new file mode 100644 index 00000000..c5b539ca --- /dev/null +++ b/solana/account/src/tests/sysvar.rs @@ -0,0 +1,15 @@ +use { + crate::{create_account_with_fields, from_account}, + solana_clock::{Clock, Epoch}, +}; + +#[test] +fn test_create_account_with_fields_round_trips_sysvar() { + let clock = Clock { epoch: 7, ..Clock::default() }; + + let account = create_account_with_fields(&clock, (3, Epoch::MAX)); + + assert_eq!(account.lamports, 3); + assert_eq!(account.rent_epoch, Epoch::MAX); + assert_eq!(from_account::(&account), Some(clock)); +} diff --git a/solana/account/src/traits.rs b/solana/account/src/traits.rs new file mode 100644 index 00000000..3078689b --- /dev/null +++ b/solana/account/src/traits.rs @@ -0,0 +1,246 @@ +use { + crate::{ + Account, AccountSharedData, + cow::{DirtyMarkers, StateFlags}, + }, + solana_account_info::debug_account_data::debug_account_data, + solana_clock::Epoch, + solana_instruction_error::LamportsError, + solana_pubkey::Pubkey, + std::{fmt, ops::Deref}, +}; + +/// Read-only access to account state. +pub trait ReadableAccount: Sized { + /// Returns the lamport balance. + fn lamports(&self) -> u64; + + /// Returns the account data. + fn data(&self) -> &[u8]; + + /// Returns the account owner. + fn owner(&self) -> &Pubkey; + + /// Returns whether the account is executable. + fn executable(&self) -> bool; + + /// Returns the rent epoch view for this account. + fn rent_epoch(&self) -> Epoch; +} + +/// Writable access to account state. +pub trait WritableAccount: ReadableAccount { + /// Replaces the lamport balance. + fn set_lamports(&mut self, lamports: u64); + + /// Adds lamports or returns an overflow error. + fn checked_add_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports().checked_add(lamports).ok_or(LamportsError::ArithmeticOverflow)?, + ); + Ok(()) + } + + /// Subtracts lamports or returns an underflow error. + fn checked_sub_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports() + .checked_sub(lamports) + .ok_or(LamportsError::ArithmeticUnderflow)?, + ); + Ok(()) + } + + /// Adds lamports and saturates on overflow. + fn saturating_add_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_add(lamports)) + } + + /// Subtracts lamports and saturates on underflow. + fn saturating_sub_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_sub(lamports)) + } + + /// Returns mutable access to the account data. + fn data_as_mut_slice(&mut self) -> &mut [u8]; + + /// Replaces the owner. + fn set_owner(&mut self, owner: Pubkey); + + /// Copies 32 raw bytes into the owner pubkey. + fn copy_into_owner_from_slice(&mut self, source: &[u8]); + + /// Sets the executable flag. + fn set_executable(&mut self, executable: bool); + + /// Sets the rent epoch view if the implementation stores one. + /// + /// Implementations that do not store rent epoch may ignore this. + fn set_rent_epoch(&mut self, epoch: Epoch); +} + +/// Returns `true` when the readable account fields match. +/// +/// This ignores storage form and any non-readable metadata. +pub fn accounts_equal(me: &T, other: &U) -> bool { + me.lamports() == other.lamports() + && me.executable() == other.executable() + && me.rent_epoch() == other.rent_epoch() + && me.owner() == other.owner() + && me.data() == other.data() +} + +/// Formats readable accounts with the same debug shape as `Account`. +pub(crate) fn debug_fmt( + item: &T, + f: &mut fmt::Formatter<'_>, + add: impl FnOnce(&mut fmt::DebugStruct<'_, '_>), +) -> fmt::Result { + let mut f = f.debug_struct("Account"); + + f.field("lamports", &item.lamports()) + .field("data.len", &item.data().len()) + .field("owner", &item.owner()) + .field("executable", &item.executable()) + .field("rent_epoch", &item.rent_epoch()); + add(&mut f); + debug_account_data(item.data(), &mut f); + + f.finish() +} + +impl ReadableAccount for T +where + T: Deref, + T::Target: ReadableAccount, +{ + fn lamports(&self) -> u64 { + self.deref().lamports() + } + + fn data(&self) -> &[u8] { + self.deref().data() + } + + fn owner(&self) -> &Pubkey { + self.deref().owner() + } + + fn executable(&self) -> bool { + self.deref().executable() + } + + fn rent_epoch(&self) -> Epoch { + self.deref().rent_epoch() + } +} + +impl ReadableAccount for Account { + fn lamports(&self) -> u64 { + self.lamports + } + + fn data(&self) -> &[u8] { + &self.data + } + + fn owner(&self) -> &Pubkey { + &self.owner + } + + fn executable(&self) -> bool { + self.executable + } + + fn rent_epoch(&self) -> Epoch { + self.rent_epoch + } +} + +impl WritableAccount for Account { + fn set_lamports(&mut self, lamports: u64) { + self.lamports = lamports; + } + + fn data_as_mut_slice(&mut self) -> &mut [u8] { + &mut self.data + } + + fn set_owner(&mut self, owner: Pubkey) { + self.owner = owner; + } + + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.owner.as_mut().copy_from_slice(source); + } + + fn set_executable(&mut self, executable: bool) { + self.executable = executable; + } + + fn set_rent_epoch(&mut self, epoch: Epoch) { + self.rent_epoch = epoch; + } +} + +impl ReadableAccount for AccountSharedData { + fn lamports(&self) -> u64 { + self.lamports + } + + fn data(&self) -> &[u8] { + self.cow.data() + } + + fn owner(&self) -> &Pubkey { + &self.owner + } + + fn executable(&self) -> bool { + self.flags.contains(StateFlags::EXECUTABLE) + } + + fn rent_epoch(&self) -> Epoch { + Epoch::MAX + } +} + +impl WritableAccount for AccountSharedData { + fn set_lamports(&mut self, lamports: u64) { + if self.lamports == lamports { + return; + } + self.translate(); + self.dirty.insert(DirtyMarkers::LAMPORTS); + self.lamports = lamports; + } + + fn data_as_mut_slice(&mut self) -> &mut [u8] { + self.translate(); + self.mark_data_dirty(); + self.cow.data_mut() + } + + fn set_owner(&mut self, owner: Pubkey) { + if self.owner == owner { + return; + } + self.translate(); + self.dirty.insert(DirtyMarkers::OWNER); + self.owner = owner; + } + + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.translate(); + self.dirty.insert(DirtyMarkers::OWNER); + self.owner.as_mut().copy_from_slice(source); + } + + fn set_executable(&mut self, executable: bool) { + let mut flags = self.flags; + flags.set(StateFlags::EXECUTABLE, executable); + self.set_flags(flags); + } + + fn set_rent_epoch(&mut self, _: Epoch) {} +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8b137891..00000000 --- a/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -