diff --git a/Cargo.lock b/Cargo.lock index e363adb80e..bd192215ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,9 +80,9 @@ checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "arbitrary" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e0a02cf12f1b1f48b14cb7f8217b876d09992b39c816ffb3b1ba64dd979a87" +checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60" [[package]] name = "arrayref" @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880" +checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" dependencies = [ "async-channel", "async-executor", @@ -159,6 +159,7 @@ dependencies = [ "async-lock", "blocking", "futures-lite", + "num_cpus", "once_cell", ] @@ -192,9 +193,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" @@ -300,9 +301,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] @@ -330,9 +331,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitvec" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ "funty", "radium", @@ -442,9 +443,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" [[package]] name = "cache-padded" @@ -485,9 +486,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" dependencies = [ "cfg-if", "cipher", @@ -497,9 +498,9 @@ dependencies = [ [[package]] name = "chacha20poly1305" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" dependencies = [ "aead", "chacha20", @@ -606,9 +607,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.5" +version = "3.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" +checksum = "44bbe24bbd31a185bc2c4f7c2abe80bea13a20d57ee4e55be70ac512bdc76417" dependencies = [ "atty", "bitflags", @@ -623,9 +624,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.5" +version = "3.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" +checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -636,9 +637,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -696,18 +697,18 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" dependencies = [ "cache-padded", ] [[package]] name = "const_format" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2906f2480cdc015e998deac388331a0f1c1cd88744948c749513020c83c370bc" +checksum = "939dc9e2eb9077e0679d2ce32de1ded8531779360b003b4a972a7a39ec263495" dependencies = [ "const_format_proc_macros", ] @@ -768,9 +769,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -778,9 +779,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -789,9 +790,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", @@ -803,9 +804,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -838,9 +839,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -1035,15 +1036,15 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "expect-test" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dced95c9dcd4e3241f95841aad395f9c8d7933a3b0b524bdeb2440885c72a271" +checksum = "1d4661aca38d826eb7c72fe128e4238220616de4c0cc00db7bfc38e2e1364dd3" dependencies = [ "dissimilar", "once_cell", @@ -1051,18 +1052,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] [[package]] name = "fixedbitset" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" @@ -1234,15 +1235,15 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" +checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" dependencies = [ "cc", "libc", "log", "rustversion", - "winapi 0.3.9", + "windows 0.32.0", ] [[package]] @@ -1302,9 +1303,9 @@ dependencies = [ [[package]] name = "gloo-net" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d37f728c2b2b8c568bd2efb34ce9087e347c182db68f101a969b4fe23054d5" +checksum = "351e6f94c76579cc9f9323a15f209086fc7bd428bff4288723d3a417851757b2" dependencies = [ "futures-channel", "futures-core", @@ -1334,9 +1335,9 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0bbef55e98d946adbd89f3c65a497cf9adb995a73b99573f30180e8813ab21" +checksum = "929c53c913bb7a88d75d9dc3e9705f963d8c2b9001510b25ddaf671b9fb7049d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1364,19 +1365,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" - [[package]] name = "heck" version = "0.3.3" @@ -1461,9 +1456,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.19" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -1522,9 +1517,9 @@ dependencies = [ [[package]] name = "if-watch" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774d59367a3d26965d21ac70a86fcb697d83405d1c4312d1d8a6855296af0cf7" +checksum = "015a7df1eb6dda30df37f34b63ada9b7b352984b0e84de2a20ed526345000791" dependencies = [ "async-io", "core-foundation", @@ -1535,7 +1530,7 @@ dependencies = [ "log", "rtnetlink", "system-configuration", - "windows", + "windows 0.34.0", ] [[package]] @@ -1551,12 +1546,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown 0.12.1", + "hashbrown", ] [[package]] @@ -1591,9 +1586,9 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -1939,9 +1934,9 @@ dependencies = [ [[package]] name = "libp2p-metrics" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4357140141ba9739eee71b20aa735351c0fc642635b2bffc7f57a6b5c1090" +checksum = "564a7e5284d7d9b3140fdfc3cb6567bc32555e86a21de5604c2ec85da05cf384" dependencies = [ "libp2p-core", "libp2p-gossipsub", @@ -2078,7 +2073,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc195aab5b803465bf614a5a4765741abce6c8d64e7d8ca57acd2923661fba9f" dependencies = [ - "clap 3.2.5", + "clap 3.2.15", "crossbeam-channel", "rayon", "termcolor", @@ -2126,11 +2121,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84e6fe5655adc6ce00787cf7dcaf8dc4f998a0565d23eafc207a8b08ca3349a" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" dependencies = [ - "hashbrown 0.11.2", + "hashbrown", ] [[package]] @@ -2173,6 +2168,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mempool" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "lazy_static", + "logging", + "mockall", + "parity-scale-codec", + "serialization", + "thiserror", + "utils", +] + [[package]] name = "merkletree" version = "0.21.0" @@ -2223,6 +2233,7 @@ dependencies = [ "common", "crypto", "logging", + "mempool", "p2p", "rpc", "script", @@ -2256,9 +2267,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5641e476bbaf592a3939a7485fa079f427b4db21407d5ebfd5bba4e07a1f6f4c" +checksum = "e2be9a9090bc1cac2930688fa9478092a64c6a92ddc6ae0692d46b37d9cab709" dependencies = [ "cfg-if", "downcast", @@ -2271,9 +2282,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262d56735932ee0240d515656e5a7667af3af2a5b0af4da558c4cff2b2aeb0c7" +checksum = "86d702a0530a0141cf4ed147cf5ec7be6f2c187d4e37fcbefc39cf34116bfe8f" dependencies = [ "cfg-if", "proc-macro2", @@ -2360,9 +2371,9 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733ea73609acfd7fa7ddadfb7bf709b0471668c456ad9513685af543a06342b2" +checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" dependencies = [ "anyhow", "bitflags", @@ -2386,23 +2397,24 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8785b8141e8432aa45fceb922a7e876d7da3fad37fa7e7ec702ace3aa0826b" +checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", + "thiserror", "tokio", ] [[package]] name = "netlink-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c9f9547a08241bee7b6558b9b98e1f290d187de8b7cfca2bbb4937bcaa8f8" +checksum = "92b654097027250401127914afb37cb1f311df6610a9891ff07a757e94199027" dependencies = [ "async-io", "bytes", @@ -2419,15 +2431,13 @@ checksum = "d36047f46c69ef97b60e7b069a26ce9a15cd8a7852eddb6991ea94a83ba36a78" [[package]] name = "nix" -version = "0.22.3" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" dependencies = [ "bitflags", - "cc", "cfg-if", "libc", - "memoffset", ] [[package]] @@ -2438,7 +2448,7 @@ dependencies = [ "assert_cmd", "chainstate", "chainstate-storage", - "clap 3.2.5", + "clap 3.2.15", "common", "directories", "jsonrpsee", @@ -2533,9 +2543,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", @@ -2564,9 +2574,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "opaque-debug" @@ -2582,9 +2592,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" [[package]] name = "owning_ref" @@ -2752,18 +2762,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ "proc-macro2", "quote", @@ -2911,9 +2921,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" dependencies = [ "unicode-ident", ] @@ -3199,9 +3209,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -3219,9 +3229,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -3239,9 +3249,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "remove_dir_all" @@ -3344,9 +3354,9 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f54290e54521dac3de4149d83ddf9f62a359b3cc93bcb494a794a41e6f4744b" +checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" dependencies = [ "async-global-executor", "futures", @@ -3384,7 +3394,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.10", + "semver 1.0.12", ] [[package]] @@ -3422,9 +3432,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" +checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" [[package]] name = "rusty-fork" @@ -3554,9 +3564,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" [[package]] name = "semver-parser" @@ -3575,9 +3585,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" dependencies = [ "serde_derive", ] @@ -3593,9 +3603,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", @@ -3604,9 +3614,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "itoa", "ryu", @@ -3615,9 +3625,9 @@ dependencies = [ [[package]] name = "serde_test" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe196827aea34242c314d2f0dd49ed00a129225e80dda71b0dbf65d54d25628d" +checksum = "38b0651a2f427e4a4d74d458947aa5ca36c62c1b503344d143763ec06216a975" dependencies = [ "serde", ] @@ -3784,9 +3794,12 @@ checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] [[package]] name = "slave-pool" @@ -3799,9 +3812,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "snow" @@ -3853,9 +3866,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sscanf" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311683a27f16025db4f8bcf0732662f5fecd1406f53f0aab9adbf6f396f1189" +checksum = "40f3891ecd1b4d716fd674bf238ee743d2cd948c9aeb3b42b1dd9d8f56d0154b" dependencies = [ "const_format", "lazy_static", @@ -3865,9 +3878,9 @@ dependencies = [ [[package]] name = "sscanf_macro" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d1d364d856d72a52b518c7669df864bdf177de242bcb5b45369ec33190c91c3" +checksum = "95d18335301db6e21d35b955cfb58327f3ede99f831a84cad5a07a59c6b1ec5d" dependencies = [ "proc-macro2", "quote", @@ -4191,10 +4204,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.19.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg", "bytes", "libc", "memchr", @@ -4286,9 +4300,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", @@ -4297,9 +4311,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" +checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" dependencies = [ "once_cell", "valuable", @@ -4318,13 +4332,13 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.11" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" dependencies = [ "ansi_term", - "lazy_static", "matchers", + "once_cell", "regex", "sharded-slab", "smallvec", @@ -4348,9 +4362,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" [[package]] name = "unicase" @@ -4369,15 +4383,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -4539,9 +4553,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "serde", @@ -4551,13 +4565,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -4566,9 +4580,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if", "js-sys", @@ -4578,9 +4592,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4588,9 +4602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -4601,9 +4615,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "wasm-timer" @@ -4622,9 +4636,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -4642,9 +4656,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" dependencies = [ "webpki", ] @@ -4712,6 +4726,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + [[package]] name = "windows" version = "0.34.0" @@ -4738,6 +4765,12 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + [[package]] name = "windows_aarch64_msvc" version = "0.34.0" @@ -4750,6 +4783,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + [[package]] name = "windows_i686_gnu" version = "0.34.0" @@ -4762,6 +4801,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + [[package]] name = "windows_i686_msvc" version = "0.34.0" @@ -4774,6 +4819,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" @@ -4786,6 +4837,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" @@ -4820,9 +4877,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.4.3" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 18f98b0450..c634b2c467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "chainstate", # code on chainstate of blocks and transactions "script", # bitcoin script and its interfaces "logging", # logging engine and its interfaces + "mempool", # mempool interface and implementation "p2p", # p2p communication interfaces and protocols "rpc", # rpc abstraction and implementation "serialization", # full featured serialization interfaces and implementations @@ -40,6 +41,7 @@ default-members = [ "chainstate", "script", "logging", + "mempool", "p2p", "rpc", "serialization", @@ -61,6 +63,7 @@ chainstate = { path = "chainstate"} chainstate-types = { path = "chainstate-types"} script = { path = "script"} logging = { path = "logging"} +mempool = { path = "mempool"} p2p = { path = "p2p"} rpc = { path = "rpc"} serialization = { path = "serialization"} diff --git a/common/src/chain/transaction.rs b/common/src/chain/transaction.rs index 234412f326..f3c0c768cc 100644 --- a/common/src/chain/transaction.rs +++ b/common/src/chain/transaction.rs @@ -17,6 +17,7 @@ use crate::primitives::{Id, Idable}; use serialization::{DirectDecode, DirectEncode, Encode}; +use thiserror::Error; use crate::chain::transaction::transaction_v1::TransactionV1; @@ -54,8 +55,9 @@ impl Idable for Transaction { } } -#[derive(Debug, Clone)] +#[derive(Error, Debug, Clone)] pub enum TransactionCreationError { + #[error("An unknown error has occurred")] Unknown, } diff --git a/common/src/chain/transaction/input.rs b/common/src/chain/transaction/input.rs index bee1185350..f2e20cb6b8 100644 --- a/common/src/chain/transaction/input.rs +++ b/common/src/chain/transaction/input.rs @@ -52,6 +52,15 @@ impl From> for OutPointSourceId { } } +impl OutPointSourceId { + pub fn get_tx_id(&self) -> Option<&Id> { + match self { + OutPointSourceId::Transaction(id) => Some(id), + _ => None, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct OutPoint { id: OutPointSourceId, diff --git a/common/src/primitives/amount.rs b/common/src/primitives/amount.rs index bc20b493ae..e2f5969ec1 100644 --- a/common/src/primitives/amount.rs +++ b/common/src/primitives/amount.rs @@ -317,6 +317,35 @@ mod tests { assert_eq!(Amount { val: IntType::MAX } + Amount { val: 1 }, None); } + #[test] + fn sum_some() { + let amounts = vec![Amount { val: 1 }, Amount { val: 2 }, Amount { val: 3 }]; + assert_eq!( + amounts.into_iter().sum::>(), + Some(Amount { val: 6 }) + ); + } + + #[test] + fn sum_overflow() { + let amounts = vec![ + Amount { val: 1 }, + Amount { val: 2 }, + Amount { + val: IntType::MAX - 2, + }, + ]; + assert_eq!(amounts.into_iter().sum::>(), None); + } + + #[test] + fn sum_empty() { + assert_eq!( + vec![].into_iter().sum::>(), + Some(Amount::from_atoms(0)) + ) + } + #[test] fn sub_underflow() { assert_eq!(Amount { val: IntType::MIN } - Amount { val: 1 }, None); diff --git a/deny.toml b/deny.toml index 5b73398da9..87d0df7700 100644 --- a/deny.toml +++ b/deny.toml @@ -44,6 +44,7 @@ skip = [ {name = "textwrap"}, {name = "wasi"}, {name = "winapi"}, + {name = "windows"}, {name = "windows_aarch64_msvc" }, {name = "windows_i686_gnu" }, {name = "windows_i686_msvc" }, @@ -70,6 +71,7 @@ allow = [ "LicenseRef-webpki", "WTFPL", "BSL-1.0", + "Unicode-DFS-2016", "Unlicense",#this is a specific license rather than no license at all ] #deny a license not in this set of licenses diff --git a/mempool/Cargo.toml b/mempool/Cargo.toml new file mode 100644 index 0000000000..ffca866f1f --- /dev/null +++ b/mempool/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mempool" +version = "0.1.0" +edition = "2021" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +parity-scale-codec = {version = "3.1", features = ["derive", "chain-error"]} +serialization = { path = '../serialization' } +common = { path = '../common' } +utils = { path = '../utils' } +logging = { path = '../logging' } +anyhow = "1.0" +thiserror = "1.0" +lazy_static = "1.4" +mockall = "0.11.0" diff --git a/mempool/src/error.rs b/mempool/src/error.rs new file mode 100644 index 0000000000..4a2d4ebb67 --- /dev/null +++ b/mempool/src/error.rs @@ -0,0 +1,71 @@ +use thiserror::Error; + +use common::chain::transaction::Transaction; +use common::chain::OutPoint; +use common::primitives::amount::Amount; +use common::primitives::Id; +use common::primitives::H256; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Mempool is full")] + MempoolFull, + #[error(transparent)] + TxValidationError(#[from] TxValidationError), +} + +#[derive(Debug, Error)] +pub enum TxValidationError { + #[error("Transaction has no inputs.")] + NoInputs, + #[error("Transaction has no outputs.")] + NoOutputs, + #[error("Transaction has duplicate inputs.")] + DuplicateInputs, + #[error("Outpoint not found: {outpoint:?}")] + OutPointNotFound { + outpoint: OutPoint, + tx_id: Id, + }, + #[error("Transaction exceeds the maximum block size.")] + ExceedsMaxBlockSize, + #[error("Transaction already exists in the mempool.")] + TransactionAlreadyInMempool, + #[error("Transaction conflicts with another, irreplaceable transaction.")] + ConflictWithIrreplaceableTransaction, + #[error("The sum of the transaction's inputs' values overflows.")] + InputValuesOverflow, + #[error("The sum of the transaction's outputs' values overflows.")] + OutputValuesOverflow, + #[error("The sum of the transaction's inputs is smaller than the sum of its outputs.")] + InputsBelowOutputs, + #[error("Replacement transaction has fee lower than the original. Replacement fee is {replacement_fee:?}, original fee {original_fee:?}")] + ReplacementFeeLowerThanOriginal { + replacement_tx: H256, + replacement_fee: Amount, + original_tx: H256, + original_fee: Amount, + }, + #[error("Transaction would require too many replacements.")] + TooManyPotentialReplacements, + #[error("Replacement transaction spends an unconfirmed input which was not spent by any of the original transactions.")] + SpendsNewUnconfirmedOutput, + #[error("The sum of the fees of this transaction's conflicts overflows.")] + ConflictsFeeOverflow, + #[error("Transaction pays a fee that is lower than the fee of its conflicts with their descendants.")] + TransactionFeeLowerThanConflictsWithDescendants, + #[error("Underflow in computing transaction's additional fees.")] + AdditionalFeesUnderflow, + #[error("Transaction does not pay sufficient fees to be relayed.")] + InsufficientFeesToRelay { tx_fee: Amount, relay_fee: Amount }, + #[error("Replacement transaction does not pay enough for its bandwidth.")] + InsufficientFeesToRelayRBF, + #[error("Rolling fee threshold not met.")] + RollingFeeThresholdNotMet { minimum_fee: Amount, tx_fee: Amount }, + #[error("Overflow encountered while updating ancestor fee.")] + AncestorFeeUpdateOverflow, + #[error("Transaction is a descendant of expired transaction.")] + DescendantOfExpiredTransaction, + #[error("Internal Error.")] + InternalError, +} diff --git a/mempool/src/feerate.rs b/mempool/src/feerate.rs new file mode 100644 index 0000000000..65faf42d69 --- /dev/null +++ b/mempool/src/feerate.rs @@ -0,0 +1,76 @@ +use common::primitives::amount::Amount; + +lazy_static::lazy_static! { + pub(crate) static ref INCREMENTAL_RELAY_FEE_RATE: FeeRate = FeeRate::new(Amount::from_atoms(1000)); + pub(crate) static ref INCREMENTAL_RELAY_THRESHOLD: FeeRate = FeeRate::new(Amount::from_atoms(500)); +} + +// TODO we should reconsider using Amount and define functions for the specific operations required +// on FeeRate. Previous attempts to do this did not pan out, but this should be revisited to avoid +// dangerous arithmetic operations +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct FeeRate { + atoms_per_kb: u128, +} + +impl FeeRate { + pub(crate) fn new(atoms_per_kb: Amount) -> Self { + Self { + atoms_per_kb: atoms_per_kb.into_atoms(), + } + } + + pub(crate) fn from_total_tx_fee(total_tx_fee: Amount, tx_size: usize) -> Self { + Self { + atoms_per_kb: Self::div_up(1000 * total_tx_fee.into_atoms(), tx_size), + } + } + + pub(crate) fn compute_fee(&self, size: usize) -> Amount { + let size = u128::try_from(size).expect("compute_fee conversion"); + // +999 for ceil operation + Amount::from_atoms((self.atoms_per_kb * size + 999) / 1000) + } + + pub(crate) fn atoms_per_kb(&self) -> u128 { + self.atoms_per_kb + } + + // TODO Use NonZeroUsize for divisor + fn div_up(dividend: u128, divisor: usize) -> u128 { + debug_assert!(divisor != 0); + let divisor = u128::try_from(divisor).expect("div_up conversion"); + (dividend + divisor - 1) / divisor + } +} + +impl std::ops::Add for FeeRate { + type Output = FeeRate; + fn add(self, other: Self) -> Self::Output { + let atoms_per_kb = self.atoms_per_kb + other.atoms_per_kb; + FeeRate { atoms_per_kb } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::num::NonZeroU128; + + impl std::ops::Div for FeeRate { + type Output = FeeRate; + fn div(self, rhs: NonZeroU128) -> Self::Output { + FeeRate { + atoms_per_kb: self.atoms_per_kb / rhs, + } + } + } + + #[test] + fn test_div_up() { + let fee = 7; + let tx_size = usize::MAX; + let rate = FeeRate::div_up(fee, tx_size); + assert_eq!(rate, 1); + } +} diff --git a/mempool/src/lib.rs b/mempool/src/lib.rs new file mode 100644 index 0000000000..05a34d101c --- /dev/null +++ b/mempool/src/lib.rs @@ -0,0 +1,7 @@ +#![deny(clippy::clone_on_ref_ptr)] + +pub mod error; +mod feerate; +pub mod pool; + +pub use error::Error as MempoolError; diff --git a/mempool/src/pool.rs b/mempool/src/pool.rs new file mode 100644 index 0000000000..abb88b6fc8 --- /dev/null +++ b/mempool/src/pool.rs @@ -0,0 +1,1131 @@ +use std::cell::Cell; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::HashMap; +use std::fmt::Debug; +use std::time::Duration; + +use mockall::*; +use serialization::Encode; + +use common::chain::transaction::Transaction; +use common::chain::transaction::TxInput; +use common::chain::OutPoint; +use common::primitives::amount::Amount; +use common::primitives::Id; +use common::primitives::Idable; +use common::primitives::H256; + +use logging::log; + +use utils::ensure; +use utils::newtype; + +use crate::error::Error; +use crate::error::TxValidationError; +use crate::feerate::FeeRate; +use crate::feerate::INCREMENTAL_RELAY_FEE_RATE; +use crate::feerate::INCREMENTAL_RELAY_THRESHOLD; + +const ROLLING_FEE_BASE_HALFLIFE: Time = Duration::new(60 * 60 * 12, 1); +// TODO this willbe defined elsewhere (some of limits.rs file) +const MAX_BLOCK_SIZE_BYTES: usize = 1_000_000; + +const MAX_BIP125_REPLACEMENT_CANDIDATES: usize = 100; + +// TODO this should really be taken from some global node settings +const RELAY_FEE_PER_BYTE: usize = 1; + +const MAX_MEMPOOL_SIZE_BYTES: usize = 300_000_000; + +const DEFAULT_MEMPOOL_EXPIRY: Duration = Duration::new(336 * 60 * 60, 0); + +const ROLLING_FEE_DECAY_INTERVAL: Time = Duration::new(10, 0); + +pub(crate) type MemoryUsage = usize; + +#[automock] +pub trait GetMemoryUsage { + fn get_memory_usage(&self) -> MemoryUsage; +} + +pub(crate) type Time = Duration; +pub trait GetTime { + fn get_time(&self) -> Time; +} + +impl TryGetFee for MempoolImpl +where + C: ChainState, + T: GetTime, + M: GetMemoryUsage, +{ + // TODO this calculation is already done in ChainState, reuse it + fn try_get_fee(&self, tx: &Transaction) -> Result { + let inputs = tx + .inputs() + .iter() + .map(|input| { + let outpoint = input.outpoint(); + self.chain_state + .get_outpoint_value(outpoint) + .or_else(|_| self.store.get_unconfirmed_outpoint_value(outpoint)) + }) + .collect::, _>>()?; + let sum_inputs = inputs + .iter() + .cloned() + .sum::>() + .ok_or(TxValidationError::InputValuesOverflow)?; + let sum_outputs = tx + .outputs() + .iter() + .map(|output| output.value()) + .sum::>() + .ok_or(TxValidationError::OutputValuesOverflow)?; + (sum_inputs - sum_outputs).ok_or(TxValidationError::InputsBelowOutputs) + } +} + +fn get_relay_fee(tx: &Transaction) -> Amount { + // TODO we should never reach the expect, but should this be an error anyway? + Amount::from_atoms(u128::try_from(tx.encoded_size() * RELAY_FEE_PER_BYTE).expect("Overflow")) +} + +pub trait Mempool { + fn create(chain_state: C, clock: T, memory_usage_estimator: M) -> Self; + fn add_transaction(&mut self, tx: Transaction) -> Result<(), Error>; + fn get_all(&self) -> Vec<&Transaction>; + + // Returns `true` if the mempool contains a transaction with the given id, `false` otherwise. + fn contains_transaction(&self, tx: &Id) -> bool; + + // Drops a transaction from the mempool, updating its in-mempool parents and children. This + // operation removes the transaction from all indices, as well as updating the state (fee, + // count with descendants) of the transaction's ancestors. In addition, outpoints spent by this + // transaction are no longer marked as spent + fn drop_transaction(&mut self, tx: &Id); + + // Add/remove transactions to/from the mempool according to a new tip + fn new_tip_set(&mut self, chain_state: C); +} + +pub trait ChainState: Debug { + fn contains_outpoint(&self, outpoint: &OutPoint) -> bool; + fn get_outpoint_value(&self, outpoint: &OutPoint) -> Result; +} + +trait TryGetFee { + fn try_get_fee(&self, tx: &Transaction) -> Result; +} + +newtype! { + #[derive(Debug)] + struct Ancestors(BTreeSet>); +} + +newtype! { + #[derive(Debug)] + struct Descendants(BTreeSet>); +} + +newtype! { + #[derive(Debug)] + struct Conflicts(BTreeSet>); +} + +#[derive(Debug, Eq, Clone)] +struct TxMempoolEntry { + tx: Transaction, + fee: Amount, + parents: BTreeSet>, + children: BTreeSet>, + count_with_descendants: usize, + fees_with_descendants: Amount, + creation_time: Time, +} + +impl TxMempoolEntry { + fn new( + tx: Transaction, + fee: Amount, + parents: BTreeSet>, + creation_time: Time, + ) -> TxMempoolEntry { + Self { + tx, + fee, + parents, + children: BTreeSet::default(), + count_with_descendants: 1, + creation_time, + fees_with_descendants: fee, + } + } + + fn count_with_descendants(&self) -> usize { + self.count_with_descendants + } + + fn tx_id(&self) -> Id { + self.tx.get_id() + } + + fn unconfirmed_parents(&self) -> impl Iterator> { + self.parents.iter() + } + + fn unconfirmed_children(&self) -> impl Iterator> { + self.children.iter() + } + + fn get_children_mut(&mut self) -> &mut BTreeSet> { + &mut self.children + } + + fn get_parents_mut(&mut self) -> &mut BTreeSet> { + &mut self.parents + } + + fn is_replaceable(&self, store: &MempoolStore) -> bool { + self.tx.is_replaceable() + || self + .unconfirmed_ancestors(store) + .0 + .iter() + .any(|ancestor| store.get_entry(ancestor).expect("entry").tx.is_replaceable()) + } + + fn unconfirmed_ancestors(&self, store: &MempoolStore) -> Ancestors { + let mut visited = Ancestors(BTreeSet::new()); + self.unconfirmed_ancestors_inner(&mut visited, store); + visited + } + + fn unconfirmed_ancestors_inner(&self, visited: &mut Ancestors, store: &MempoolStore) { + for parent in self.parents.iter() { + if visited.insert(*parent) { + store + .get_entry(parent) + .expect("entry") + .unconfirmed_ancestors_inner(visited, store); + } + } + } + + fn unconfirmed_descendants(&self, store: &MempoolStore) -> Descendants { + let mut visited = Descendants(BTreeSet::new()); + self.unconfirmed_descendants_inner(&mut visited, store); + visited + } + + fn unconfirmed_descendants_inner(&self, visited: &mut Descendants, store: &MempoolStore) { + for child in self.children.iter() { + if visited.insert(*child) { + store + .get_entry(child) + .expect("entry") + .unconfirmed_descendants_inner(visited, store); + } + } + } +} + +impl PartialOrd for TxMempoolEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(other.tx_id().cmp(&self.tx_id())) + } +} + +impl PartialEq for TxMempoolEntry { + fn eq(&self, other: &Self) -> bool { + self.tx_id() == other.tx_id() + } +} + +impl Ord for TxMempoolEntry { + fn cmp(&self, other: &Self) -> Ordering { + other.tx_id().cmp(&self.tx_id()) + } +} + +newtype! { + #[derive(Debug, PartialEq, Eq, Ord, PartialOrd)] + struct DescendantScore(Amount); +} + +#[derive(Clone, Copy, Debug)] +struct RollingFeeRate { + block_since_last_rolling_fee_bump: bool, + rolling_minimum_fee_rate: FeeRate, + last_rolling_fee_update: Time, +} + +impl RollingFeeRate { + #[allow(clippy::float_arithmetic)] + fn decay_fee(mut self, halflife: Time, current_time: Time) -> Self { + log::trace!( + "decay_fee: old fee rate: {:?}\nCurrent time: {:?}\nLast Rolling Fee Update: {:?}\nHalflife: {:?}", + self.rolling_minimum_fee_rate, + self.last_rolling_fee_update, + current_time, + halflife, + ); + + let divisor = ((current_time.as_secs() - self.last_rolling_fee_update.as_secs()) as f64 + / (halflife.as_secs() as f64)) + .exp2(); + self.rolling_minimum_fee_rate = FeeRate::new(Amount::from_atoms( + (self.rolling_minimum_fee_rate.atoms_per_kb() as f64 / divisor) as u128, + )); + + log::trace!( + "decay_fee: new fee rate: {:?}", + self.rolling_minimum_fee_rate + ); + self.last_rolling_fee_update = current_time; + self + } +} + +impl RollingFeeRate { + pub(crate) fn new(creation_time: Time) -> Self { + Self { + block_since_last_rolling_fee_bump: false, + rolling_minimum_fee_rate: FeeRate::new(Amount::from_atoms(0)), + last_rolling_fee_update: creation_time, + } + } +} + +#[derive(Debug)] +pub struct MempoolImpl { + store: MempoolStore, + rolling_fee_rate: Cell, + max_size: usize, + max_tx_age: Duration, + chain_state: C, + clock: T, + memory_usage_estimator: M, +} + +#[derive(Debug)] +struct MempoolStore { + // This is the "main" data structure storing Mempool entries. All other structures in the + // MempoolStore contain ids (hashes) of entries, sorted according to some order of interest. + txs_by_id: HashMap, + + // Mempool entries sorted by descendant score. + // We keep this index so that when the mempool grows full, we know which transactions are the + // most economically reasonable to evict. When an entry is removed from the mempool for + // fullness reasons, it must be removed together with all of its descendants (as these descendants + // would no longer be valid to mine). Entries with a lower descendant score will be evicted + // first. + // + // FIXME currently, the descendant score is the sum fee of the transaction to gether with all + // of its descendants. If we wish to follow Bitcoin Core, we should use: + // max(feerate(tx, tx_with_descendants)), + // Where feerate is computed as fee(tx)/size(tx) + // Note that if we wish to follow Bitcoin Bore, "size" is not simply the encoded size, but + // rather a value that takes into account witdess and sigop data (see CTxMemPoolEntry::GetTxSize). + txs_by_descendant_score: BTreeMap>>, + + // Entries that have remained in the mempool for a long time (see DEFAULT_MEMPOOL_EXPIRY) are + // evicted. To efficiently know which entries to evict, we store the mempool entries sorted by + // their creation time, from earliest to latest. + txs_by_creation_time: BTreeMap>>, + + // TODO add txs_by_ancestor_score index, which will be used by the block production subsystem + // to select the best transactions for the next block + // + // We keep the information of which outpoints are spent by entries currently in the mempool. + // This allows us to recognize conflicts (double-spends) and handle them + spender_txs: BTreeMap>, +} + +impl MempoolStore { + fn new() -> Self { + Self { + txs_by_descendant_score: BTreeMap::new(), + txs_by_id: HashMap::new(), + txs_by_creation_time: BTreeMap::new(), + spender_txs: BTreeMap::new(), + } + } + + fn is_empty(&self) -> bool { + self.txs_by_id.is_empty() + // TODO maybe add some asserts here + } + + // Checks whether the outpoint is to be created by an unconfirmed tx + fn contains_outpoint(&self, outpoint: &OutPoint) -> bool { + matches!(self.txs_by_id.get(&outpoint.tx_id().get_tx_id().expect("Not Coinbase").get()), + Some(entry) if entry.tx.outputs().len() > outpoint.output_index() as usize) + } + + fn get_unconfirmed_outpoint_value( + &self, + outpoint: &OutPoint, + ) -> Result { + let tx_id = *outpoint.tx_id().get_tx_id().expect("Not coinbase"); + let err = || TxValidationError::OutPointNotFound { + outpoint: outpoint.clone(), + tx_id, + }; + self.txs_by_id + .get(&tx_id.get()) + .ok_or_else(err) + .and_then(|entry| { + entry.tx.outputs().get(outpoint.output_index() as usize).ok_or_else(err) + }) + .map(|output| output.value()) + } + + fn get_entry(&self, id: &Id) -> Option<&TxMempoolEntry> { + self.txs_by_id.get(&id.get()) + } + + fn append_to_parents(&mut self, entry: &TxMempoolEntry) { + for parent in entry.unconfirmed_parents() { + self.txs_by_id + .get_mut(&parent.get()) + .expect("append_to_parents") + .get_children_mut() + .insert(entry.tx_id()); + } + } + + fn remove_from_parents(&mut self, entry: &TxMempoolEntry) { + for parent in entry.unconfirmed_parents() { + self.txs_by_id + .get_mut(&parent.get()) + .expect("remove_from_parents") + .get_children_mut() + .remove(&entry.tx_id()); + } + } + + fn remove_from_children(&mut self, entry: &TxMempoolEntry) { + for child in entry.unconfirmed_children() { + self.txs_by_id + .get_mut(&child.get()) + .expect("remove_from_children") + .get_parents_mut() + .remove(&entry.tx_id()); + } + } + + fn update_ancestor_state_for_add(&mut self, entry: &TxMempoolEntry) -> Result<(), Error> { + for ancestor in entry.unconfirmed_ancestors(self).0 { + let ancestor = self.txs_by_id.get_mut(&ancestor.get()).expect("ancestor"); + ancestor.fees_with_descendants = (ancestor.fees_with_descendants + entry.fee) + .ok_or(TxValidationError::AncestorFeeUpdateOverflow)?; + ancestor.count_with_descendants += 1; + } + Ok(()) + } + + fn update_ancestor_state_for_drop(&mut self, entry: &TxMempoolEntry) { + for ancestor in entry.unconfirmed_ancestors(self).0 { + let ancestor = self.txs_by_id.get_mut(&ancestor.get()).expect("ancestor"); + ancestor.fees_with_descendants = + (ancestor.fees_with_descendants - entry.fee).expect("fee with descendants"); + ancestor.count_with_descendants -= 1; + } + } + + fn mark_outpoints_as_spent(&mut self, entry: &TxMempoolEntry) { + let id = entry.tx_id(); + for outpoint in entry.tx.inputs().iter().map(|input| input.outpoint()) { + self.spender_txs.insert(outpoint.clone(), id); + } + } + + fn unspend_outpoints(&mut self, entry: &TxMempoolEntry) { + self.spender_txs.retain(|_, id| *id != entry.tx_id()) + } + + fn add_tx(&mut self, entry: TxMempoolEntry) -> Result<(), Error> { + self.append_to_parents(&entry); + self.update_ancestor_state_for_add(&entry)?; + self.mark_outpoints_as_spent(&entry); + + let creation_time = entry.creation_time; + let tx_id = entry.tx_id(); + + self.txs_by_id.insert(tx_id.get(), entry.clone()); + + self.add_to_descendant_score_index(&entry); + self.txs_by_creation_time.entry(creation_time).or_default().insert(tx_id); + Ok(()) + } + + fn add_to_descendant_score_index(&mut self, entry: &TxMempoolEntry) { + self.refresh_ancestors(entry); + self.txs_by_descendant_score + .entry(entry.fees_with_descendants.into()) + .or_default() + .insert(entry.tx_id()); + } + + fn refresh_ancestors(&mut self, entry: &TxMempoolEntry) { + // Since the ancestors of `entry` have had their descendant score modified, their ordering + // in txs_by_descendant_score may no longer be correct. We thus remove all ancestors and + // reinsert them, taking the new, updated fees into account + let ancestors = entry.unconfirmed_ancestors(self); + for entries in self.txs_by_descendant_score.values_mut() { + entries.retain(|id| !ancestors.contains(id)) + } + for ancestor_id in ancestors.0 { + let ancestor = + self.txs_by_id.get(&ancestor_id.get()).expect("Inconsistent mempool state"); + self.txs_by_descendant_score + .entry(ancestor.fees_with_descendants.into()) + .or_default() + .insert(ancestor_id); + } + + self.txs_by_descendant_score.retain(|_score, txs| !txs.is_empty()); + } + + fn remove_tx(&mut self, tx_id: &Id) { + log::info!("remove_tx: {}", tx_id.get()); + if let Some(entry) = self.txs_by_id.remove(&tx_id.get()) { + self.update_ancestor_state_for_drop(&entry); + self.drop_tx(&entry); + } else { + assert!(!self.txs_by_descendant_score.values().flatten().any(|id| *id == *tx_id)); + assert!(!self.spender_txs.iter().any(|(_, id)| *id == *tx_id)); + } + } + + fn update_for_drop(&mut self, entry: &TxMempoolEntry) { + self.remove_from_parents(entry); + self.remove_from_children(entry); + } + + fn drop_tx(&mut self, entry: &TxMempoolEntry) { + self.update_for_drop(entry); + self.remove_from_descendant_score_index(entry); + self.txs_by_creation_time.entry(entry.creation_time).and_modify(|entries| { + entries.remove(&entry.tx_id()).then(|| ()).expect("Inconsistent mempool store") + }); + self.unspend_outpoints(entry) + } + + fn remove_from_descendant_score_index(&mut self, entry: &TxMempoolEntry) { + self.refresh_ancestors(entry); + self.txs_by_descendant_score + .entry(entry.fees_with_descendants.into()) + .or_default() + .remove(&entry.tx_id()); + if self + .txs_by_descendant_score + .get(&entry.fees_with_descendants.into()) + .expect("key must exist") + .is_empty() + { + self.txs_by_descendant_score.remove(&entry.fees_with_descendants.into()); + } + } + + fn drop_conflicts(&mut self, conflicts: Conflicts) { + for conflict in conflicts.0 { + self.remove_tx(&conflict) + } + } + + fn drop_tx_and_descendants(&mut self, tx_id: Id) { + if let Some(entry) = self.txs_by_id.get(&tx_id.get()).cloned() { + let descendants = entry.unconfirmed_descendants(self); + log::trace!( + "Dropping tx {} which has {} descendants", + tx_id.get(), + descendants.len() + ); + self.remove_tx(&entry.tx.get_id()); + for descendant_id in descendants.0 { + // It may be that this descendant has several ancestors and has already been removed + if let Some(descendant) = self.txs_by_id.get(&descendant_id.get()).cloned() { + self.remove_tx(&descendant.tx.get_id()) + } + } + } + } + + fn find_conflicting_tx(&self, outpoint: &OutPoint) -> Option> { + self.spender_txs.get(outpoint).cloned() + } +} + +impl MempoolImpl +where + C: ChainState, + T: GetTime, + M: GetMemoryUsage, +{ + fn rolling_fee_halflife(&self) -> Time { + let mem_usage = self.get_memory_usage(); + if mem_usage < self.max_size / 4 { + ROLLING_FEE_BASE_HALFLIFE / 4 + } else if mem_usage < self.max_size / 2 { + ROLLING_FEE_BASE_HALFLIFE / 2 + } else { + ROLLING_FEE_BASE_HALFLIFE + } + } + + fn get_memory_usage(&self) -> usize { + self.memory_usage_estimator.get_memory_usage() + } + + pub(crate) fn update_min_fee_rate(&self, rate: FeeRate) { + let mut rolling_fee_rate = self.rolling_fee_rate.get(); + rolling_fee_rate.rolling_minimum_fee_rate = rate; + rolling_fee_rate.block_since_last_rolling_fee_bump = false; + self.rolling_fee_rate.set(rolling_fee_rate) + } + + pub(crate) fn get_update_min_fee_rate(&self) -> FeeRate { + let rolling_fee_rate = self.rolling_fee_rate.get(); + if !rolling_fee_rate.block_since_last_rolling_fee_bump + || rolling_fee_rate.rolling_minimum_fee_rate == FeeRate::new(Amount::from_atoms(0)) + { + return rolling_fee_rate.rolling_minimum_fee_rate; + } else if self.clock.get_time() + > rolling_fee_rate.last_rolling_fee_update + ROLLING_FEE_DECAY_INTERVAL + { + // Decay the rolling fee + self.decay_rolling_fee_rate(); + log::debug!( + "rolling fee rate after decay_rolling_fee_rate {:?}", + self.rolling_fee_rate + ); + + if self.rolling_fee_rate.get().rolling_minimum_fee_rate < *INCREMENTAL_RELAY_THRESHOLD { + log::trace!("rolling fee rate {:?} less than half of the incremental fee rate, dropping the fee", self.rolling_fee_rate.get().rolling_minimum_fee_rate); + self.drop_rolling_fee(); + return self.rolling_fee_rate.get().rolling_minimum_fee_rate; + } + } + + std::cmp::max( + self.rolling_fee_rate.get().rolling_minimum_fee_rate, + *INCREMENTAL_RELAY_FEE_RATE, + ) + } + + fn drop_rolling_fee(&self) { + let mut rolling_fee_rate = self.rolling_fee_rate.get(); + rolling_fee_rate.rolling_minimum_fee_rate = FeeRate::new(Amount::from_atoms(0)); + self.rolling_fee_rate.set(rolling_fee_rate) + } + + fn decay_rolling_fee_rate(&self) { + let halflife = self.rolling_fee_halflife(); + let time = self.clock.get_time(); + self.rolling_fee_rate.set(self.rolling_fee_rate.get().decay_fee(halflife, time)); + } + + fn verify_inputs_available(&self, tx: &Transaction) -> Result<(), TxValidationError> { + tx.inputs() + .iter() + .map(TxInput::outpoint) + .find(|outpoint| !self.outpoint_available(outpoint)) + .map_or_else( + || Ok(()), + |outpoint| { + Err(TxValidationError::OutPointNotFound { + outpoint: outpoint.clone(), + tx_id: tx.get_id(), + }) + }, + ) + } + + fn outpoint_available(&self, outpoint: &OutPoint) -> bool { + self.store.contains_outpoint(outpoint) || self.chain_state.contains_outpoint(outpoint) + } + + fn create_entry(&self, tx: Transaction) -> Result { + let parents = tx + .inputs() + .iter() + .map(|input| *input.outpoint().tx_id().get_tx_id().expect("Not coinbase")) + .filter_map(|id| self.store.txs_by_id.contains_key(&id.get()).then(|| id)) + .collect::>(); + + let fee = self.try_get_fee(&tx)?; + let time = self.clock.get_time(); + Ok(TxMempoolEntry::new(tx, fee, parents, time)) + } + + fn get_update_minimum_mempool_fee(&self, tx: &Transaction) -> Amount { + let minimum_fee_rate = self.get_update_min_fee_rate(); + log::debug!("minimum fee rate {:?}", minimum_fee_rate); + /*log::debug!( + "tx_size: {:?}, tx_fee {:?}", + tx.encoded_size(), + self.try_get_fee(tx)? + ); + */ + let res = minimum_fee_rate.compute_fee(tx.encoded_size()); + log::debug!("minimum_mempool_fee for tx: {:?}", res); + res + } + + fn validate_transaction(&self, tx: &Transaction) -> Result { + // This validation function is based on Bitcoin Core's MemPoolAccept::PreChecks. + // However, as of this stage it does not cover everything covered in Bitcoin Core + // + // Currently, the items we want covered which are NOT yet covered are: + // + // - Checking if a transaction is "standard" (see `IsStandardTx`, `AreInputsStandard` in Bitcoin Core). We have yet to decide on Mintlayer's + // definition of "standard" + // + // - Time locks: Briefly, the corresponding functions in Bitcoin Core are `CheckFinalTx` and + // `CheckSequenceLocks`. See mempool/src/time_lock_notes.txt for more details on our + // brainstorming on this topic thus far. + // + // - Bitcoin Core does not relay transactions smaller than 82 bytes (see + // MIN_STANDARD_TX_NONWITNESS_SIZE in Bitcoin Core's policy.h) + // + // - Checking that coinbase inputs have matured + // + // - Checking the signature operations cost (Bitcoin Core: `GetTransactionSigOpCost`) + // + // - We have yet to understand and implement this comment pertaining to chain limits and + // calculation of in-mempool ancestors: + // https://github.com/bitcoin/bitcoin/blob/7c08d81e119570792648fe95bbacddbb1d5f9ae2/src/validation.cpp#L829 + // + // - Bitcoin Core's `EntriesAndTxidsDisjoint` check + // + // - Bitcoin Core's `PolicyScriptChecks` + // + // - Bitcoin Core's `ConsensusScriptChecks` + + // Deviations from Bitcoin Core + // + // - In our FeeRate calculations, we use the `encoded_size`of a transaction. In contrast, + // see Bitcoin Core's `CTxMemPoolEntry::GetTxSize()`, which takes into account sigops cost + // and witness data. This deviation is not intentional, but rather the result of wanting to + // get a basic implementation working. TODO: weigh what notion of size/weight we wish to + // use and whether to follow Bitcoin Core in this regard + // + // We don't have a notion of MAX_MONEY like Bitcoin Core does, so we also don't have a + // `MoneyRange` check like the one found in Bitcoin Core's `CheckTransaction` + + if tx.inputs().is_empty() { + return Err(TxValidationError::NoInputs); + } + + if tx.outputs().is_empty() { + return Err(TxValidationError::NoOutputs); + } + + let outpoints = tx.inputs().iter().map(|input| input.outpoint()).cloned(); + + if has_duplicate_entry(outpoints) { + return Err(TxValidationError::DuplicateInputs); + } + + // TODO see this issue: + // https://github.com/mintlayer/mintlayer-core/issues/331 + if tx.encoded_size() > MAX_BLOCK_SIZE_BYTES { + return Err(TxValidationError::ExceedsMaxBlockSize); + } + + if self.contains_transaction(&tx.get_id()) { + return Err(TxValidationError::TransactionAlreadyInMempool); + } + + let conflicts = self.rbf_checks(tx)?; + + self.verify_inputs_available(tx)?; + + self.pays_minimum_relay_fees(tx)?; + + self.pays_minimum_mempool_fee(tx)?; + + Ok(conflicts) + } + + fn pays_minimum_mempool_fee(&self, tx: &Transaction) -> Result<(), TxValidationError> { + let tx_fee = self.try_get_fee(tx)?; + let minimum_fee = self.get_update_minimum_mempool_fee(tx); + ensure!( + tx_fee >= minimum_fee, + TxValidationError::RollingFeeThresholdNotMet { + minimum_fee, + tx_fee, + } + ); + Ok(()) + } + + fn pays_minimum_relay_fees(&self, tx: &Transaction) -> Result<(), TxValidationError> { + let tx_fee = self.try_get_fee(tx)?; + let relay_fee = get_relay_fee(tx); + log::debug!("tx_fee: {:?}, relay_fee: {:?}", tx_fee, relay_fee); + ensure!( + tx_fee >= relay_fee, + TxValidationError::InsufficientFeesToRelay { tx_fee, relay_fee } + ); + Ok(()) + } + + fn rbf_checks(&self, tx: &Transaction) -> Result { + let conflicts = tx + .inputs() + .iter() + .filter_map(|input| self.store.find_conflicting_tx(input.outpoint())) + .map(|id_conflict| self.store.get_entry(&id_conflict).expect("entry for id")) + .collect::>(); + + if conflicts.is_empty() { + Ok(Conflicts(BTreeSet::new())) + } else { + self.do_rbf_checks(tx, &conflicts) + } + } + + fn do_rbf_checks( + &self, + tx: &Transaction, + conflicts: &[&TxMempoolEntry], + ) -> Result { + for entry in conflicts { + // Enforce BIP125 Rule #1. + + ensure!( + entry.is_replaceable(&self.store), + TxValidationError::ConflictWithIrreplaceableTransaction + ); + } + // It's possible that the replacement pays more fees than its direct conflicts but not more + // than all conflicts (i.e. the direct conflicts have high-fee descendants). However, if the + // replacement doesn't pay more fees than its direct conflicts, then we can be sure it's not + // more economically rational to mine. Before we go digging through the mempool for all + // transactions that would need to be removed (direct conflicts and all descendants), check + // that the replacement transaction pays more than its direct conflicts. + self.pays_more_than_direct_conflicts(tx, conflicts)?; + // Enforce BIP125 Rule #2. + self.spends_no_new_unconfirmed_outputs(tx, conflicts)?; + // Enforce BIP125 Rule #5. + let conflicts_with_descendants = self.potential_replacements_within_limit(conflicts)?; + // Enforce BIP125 Rule #3. + let total_conflict_fees = + self.pays_more_than_conflicts_with_descendants(tx, &conflicts_with_descendants)?; + // Enforce BIP125 Rule #4. + self.pays_for_bandwidth(tx, total_conflict_fees)?; + Ok(Conflicts::from(conflicts_with_descendants)) + } + + fn pays_for_bandwidth( + &self, + tx: &Transaction, + total_conflict_fees: Amount, + ) -> Result<(), TxValidationError> { + log::debug!("pays_for_bandwidth: tx fee is {:?}", self.try_get_fee(tx)?); + let additional_fees = (self.try_get_fee(tx)? - total_conflict_fees) + .ok_or(TxValidationError::AdditionalFeesUnderflow)?; + let relay_fee = get_relay_fee(tx); + log::debug!( + "conflict fees: {:?}, additional fee: {:?}, relay_fee {:?}", + total_conflict_fees, + additional_fees, + relay_fee + ); + ensure!( + additional_fees >= relay_fee, + TxValidationError::InsufficientFeesToRelayRBF + ); + Ok(()) + } + + fn pays_more_than_conflicts_with_descendants( + &self, + tx: &Transaction, + conflicts_with_descendants: &BTreeSet>, + ) -> Result { + let conflicts_with_descendants = conflicts_with_descendants.iter().map(|conflict_id| { + self.store + .txs_by_id + .get(&conflict_id.get()) + .expect("tx should exist in mempool") + }); + + let total_conflict_fees = conflicts_with_descendants + .map(|conflict| conflict.fee) + .sum::>() + .ok_or(TxValidationError::ConflictsFeeOverflow)?; + + let replacement_fee = self.try_get_fee(tx)?; + ensure!( + replacement_fee > total_conflict_fees, + TxValidationError::TransactionFeeLowerThanConflictsWithDescendants + ); + Ok(total_conflict_fees) + } + + fn spends_no_new_unconfirmed_outputs( + &self, + tx: &Transaction, + conflicts: &[&TxMempoolEntry], + ) -> Result<(), TxValidationError> { + let outpoints_spent_by_conflicts = conflicts + .iter() + .flat_map(|conflict| conflict.tx.inputs().iter().map(|input| input.outpoint())) + .collect::>(); + + tx.inputs() + .iter() + .find(|input| { + // input spends an unconfirmed output + input.spends_unconfirmed(self) && + // this unconfirmed output is not spent by one of the conflicts + !outpoints_spent_by_conflicts.contains(&input.outpoint()) + }) + .map_or(Ok(()), |_| { + Err(TxValidationError::SpendsNewUnconfirmedOutput) + }) + } + + fn pays_more_than_direct_conflicts( + &self, + tx: &Transaction, + conflicts: &[&TxMempoolEntry], + ) -> Result<(), TxValidationError> { + let replacement_fee = self.try_get_fee(tx)?; + conflicts.iter().find(|conflict| conflict.fee >= replacement_fee).map_or_else( + || Ok(()), + |conflict| { + Err(TxValidationError::ReplacementFeeLowerThanOriginal { + replacement_tx: tx.get_id().get(), + replacement_fee, + original_fee: conflict.fee, + original_tx: conflict.tx_id().get(), + }) + }, + ) + } + + fn potential_replacements_within_limit( + &self, + conflicts: &[&TxMempoolEntry], + ) -> Result>, TxValidationError> { + let mut num_potential_replacements = 0; + for conflict in conflicts { + num_potential_replacements += conflict.count_with_descendants(); + if num_potential_replacements > MAX_BIP125_REPLACEMENT_CANDIDATES { + return Err(TxValidationError::TooManyPotentialReplacements); + } + } + let replacements_with_descendants = conflicts + .iter() + .flat_map(|conflict| conflict.unconfirmed_descendants(&self.store).0) + .chain(conflicts.iter().map(|conflict| conflict.tx_id())) + .collect(); + + Ok(replacements_with_descendants) + } + + fn finalize_tx(&mut self, tx: Transaction) -> Result<(), Error> { + let entry = self.create_entry(tx)?; + let id = entry.tx.get_id().get(); + self.store.add_tx(entry)?; + self.remove_expired_transactions(); + ensure!( + self.store.txs_by_id.contains_key(&id), + TxValidationError::DescendantOfExpiredTransaction + ); + + self.limit_mempool_size()?; + ensure!(self.store.txs_by_id.contains_key(&id), Error::MempoolFull); + Ok(()) + } + + fn limit_mempool_size(&mut self) -> Result<(), Error> { + let removed_fees = self.trim(); + if !removed_fees.is_empty() { + let new_minimum_fee_rate = + *removed_fees.iter().max().expect("removed_fees should not be empty") + + *INCREMENTAL_RELAY_FEE_RATE; + if new_minimum_fee_rate > self.rolling_fee_rate.get().rolling_minimum_fee_rate { + self.update_min_fee_rate(new_minimum_fee_rate) + } + } + + Ok(()) + } + + fn remove_expired_transactions(&mut self) { + let expired: Vec<_> = self + .store + .txs_by_creation_time + .values() + .flatten() + .map(|entry_id| self.store.txs_by_id.get(&entry_id.get()).expect("entry should exist")) + .filter(|entry| { + let now = self.clock.get_time(); + let expired = now.saturating_sub(entry.creation_time) > self.max_tx_age; + if expired { + log::trace!( + "Evicting tx {} which was created at {:?}. It is now {:?}", + entry.tx_id(), + entry.creation_time, + now + ); + true + } else { + false + } + }) + .cloned() + .collect(); + + for tx_id in expired.iter().map(|entry| entry.tx.get_id()) { + self.store.drop_tx_and_descendants(tx_id) + } + } + + fn trim(&mut self) -> Vec { + let mut removed_fees = Vec::new(); + while !self.store.is_empty() && self.get_memory_usage() > self.max_size { + // TODO sort by descendant score, not by fee + let removed_id = self + .store + .txs_by_descendant_score + .values() + .flatten() + .next() + .expect("pool not empty"); + let removed = self + .store + .txs_by_id + .get(&removed_id.get()) + .expect("tx with id should exist") + .clone(); + + log::debug!( + "Mempool trim: Evicting tx {} which has a descendant score of {:?} and has size {}", + removed.tx_id(), + removed.fees_with_descendants, + removed.tx.encoded_size() + ); + removed_fees.push(FeeRate::from_total_tx_fee( + removed.fee, + removed.tx.encoded_size(), + )); + self.store.drop_tx_and_descendants(removed.tx.get_id()); + } + removed_fees + } +} + +trait SpendsUnconfirmed +where + C: ChainState, + T: GetTime, + M: GetMemoryUsage, +{ + fn spends_unconfirmed(&self, mempool: &MempoolImpl) -> bool; +} + +impl SpendsUnconfirmed for TxInput +where + C: ChainState, + T: GetTime, + M: GetMemoryUsage, +{ + fn spends_unconfirmed(&self, mempool: &MempoolImpl) -> bool { + mempool.contains_transaction(self.outpoint().tx_id().get_tx_id().expect("Not coinbase")) + } +} + +#[derive(Clone)] +struct SystemClock; +impl GetTime for SystemClock { + fn get_time(&self) -> Duration { + common::primitives::time::get() + } +} + +#[derive(Clone)] +struct SystemUsageEstimator; +impl GetMemoryUsage for SystemUsageEstimator { + fn get_memory_usage(&self) -> MemoryUsage { + //TODO implement real usage estimation here + 0 + } +} + +impl Mempool for MempoolImpl +where + C: ChainState, + T: GetTime, + M: GetMemoryUsage, +{ + fn create(chain_state: C, clock: T, memory_usage_estimator: M) -> Self { + Self { + store: MempoolStore::new(), + chain_state, + max_size: MAX_MEMPOOL_SIZE_BYTES, + max_tx_age: DEFAULT_MEMPOOL_EXPIRY, + rolling_fee_rate: Cell::new(RollingFeeRate::new(clock.get_time())), + clock, + memory_usage_estimator, + } + } + + fn new_tip_set(&mut self, chain_state: C) { + self.chain_state = chain_state; + self.rolling_fee_rate.set({ + let mut rolling_fee_rate = self.rolling_fee_rate.get(); + // TODO Not sure we should set the flag to true when a block is disconnected/during a + // reorg + rolling_fee_rate.block_since_last_rolling_fee_bump = true; + rolling_fee_rate + }) + } + // + + fn add_transaction(&mut self, tx: Transaction) -> Result<(), Error> { + let conflicts = self.validate_transaction(&tx)?; + self.store.drop_conflicts(conflicts); + self.finalize_tx(tx)?; + Ok(()) + } + + fn get_all(&self) -> Vec<&Transaction> { + self.store + .txs_by_descendant_score + .values() + .flatten() + .map(|id| &self.store.get_entry(id).expect("entry").tx) + .collect() + } + + fn contains_transaction(&self, tx_id: &Id) -> bool { + self.store.txs_by_id.contains_key(&tx_id.get()) + } + + // TODO Consider returning an error + fn drop_transaction(&mut self, tx_id: &Id) { + self.store.remove_tx(tx_id); + } +} + +fn has_duplicate_entry(iter: T) -> bool +where + T: IntoIterator, + T::Item: Ord, +{ + let mut uniq = BTreeSet::new(); + iter.into_iter().any(move |x| !uniq.insert(x)) +} + +#[cfg(test)] +mod tests; diff --git a/mempool/src/pool/tests.rs b/mempool/src/pool/tests.rs new file mode 100644 index 0000000000..584d49b4fd --- /dev/null +++ b/mempool/src/pool/tests.rs @@ -0,0 +1,1745 @@ +use super::*; +use common::chain::signature::inputsig::InputWitness; +use common::chain::transaction::{Destination, TxInput, TxOutput}; +use common::chain::OutPointSourceId; +use common::chain::OutputPurpose; +use core::panic; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +mod utils; + +use self::utils::*; + +const DUMMY_WITNESS_MSG: &[u8] = b"dummy_witness_msg"; + +#[test] +fn dummy_size() { + logging::init_logging::<&str>(None); + log::debug!("1, 1: {}", estimate_tx_size(1, 1)); + log::debug!("1, 2: {}", estimate_tx_size(1, 2)); + log::debug!("1, 400: {}", estimate_tx_size(1, 400)); +} + +#[test] +fn real_size() -> anyhow::Result<()> { + let mempool = setup(); + let tx = TxGenerator::new() + .with_num_inputs(1) + .with_num_outputs(400) + .generate_tx(&mempool)?; + log::debug!("real size of tx {}", tx.encoded_size()); + Ok(()) +} + +fn valued_outpoint( + tx_id: &Id, + outpoint_index: u32, + output: &TxOutput, +) -> ValuedOutPoint { + let outpoint_source_id = OutPointSourceId::Transaction(*tx_id); + let outpoint = OutPoint::new(outpoint_source_id, outpoint_index); + let value = output.value(); + ValuedOutPoint { outpoint, value } +} + +pub(crate) fn create_genesis_tx() -> Transaction { + const TOTAL_SUPPLY: u128 = 10_000_000_000_000; + let genesis_message = b"".to_vec(); + let outpoint_source_id = OutPointSourceId::Transaction(Id::new(H256::zero())); + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(genesis_message)), + ); + let output = TxOutput::new( + Amount::from_atoms(TOTAL_SUPPLY), + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + ); + Transaction::new(0, vec![input], vec![output], 0) + .expect("Failed to create genesis coinbase transaction") +} + +impl TxMempoolEntry { + fn outpoints_created(&self) -> BTreeSet { + let id = self.tx.get_id(); + std::iter::repeat(id) + .zip(self.tx.outputs().iter().enumerate()) + .map(|(id, (index, output))| valued_outpoint(&id, index as u32, output)) + .collect() + } +} + +impl MempoolStore { + fn unconfirmed_outpoints(&self) -> BTreeSet { + self.txs_by_id + .values() + .cloned() + .flat_map(|entry| entry.outpoints_created()) + .collect() + } +} + +impl MempoolImpl +where + T: GetTime, + M: GetMemoryUsage, +{ + fn available_outpoints(&self, allow_double_spend: bool) -> BTreeSet { + let mut available = self + .store + .unconfirmed_outpoints() + .into_iter() + .chain(self.chain_state.confirmed_outpoints()) + .collect::>(); + if !allow_double_spend { + available.retain(|valued_outpoint| { + !self.store.spender_txs.contains_key(&valued_outpoint.outpoint) + }); + } + available + } + + fn get_input_value(&self, input: &TxInput) -> anyhow::Result { + let allow_double_spend = true; + self.available_outpoints(allow_double_spend) + .iter() + .find_map(|valued_outpoint| { + (valued_outpoint.outpoint == *input.outpoint()).then(|| valued_outpoint.value) + }) + .ok_or_else(|| anyhow::anyhow!("No such unconfirmed output")) + } + + fn get_minimum_rolling_fee(&self) -> FeeRate { + self.rolling_fee_rate.get().rolling_minimum_fee_rate + } + + fn process_block(&mut self, tx_id: &Id) -> anyhow::Result<()> { + let mut chain_state = self.chain_state.clone(); + chain_state.add_confirmed_tx( + self.store + .txs_by_id + .get(&tx_id.get()) + .cloned() + .ok_or_else(|| { + anyhow::anyhow!("process_block: tx {} not found in mempool", tx_id.get()) + })? + .tx, + ); + log::debug!("Setting tip to {:?}", chain_state); + self.new_tip_set(chain_state); + self.drop_transaction(tx_id); + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct ChainStateMock { + confirmed_txs: HashMap, + available_outpoints: BTreeSet, +} + +impl ChainStateMock { + pub(crate) fn new() -> Self { + let genesis_tx = create_genesis_tx(); + let outpoint_source_id = OutPointSourceId::Transaction(genesis_tx.get_id()); + let outpoints = genesis_tx + .outputs() + .iter() + .enumerate() + .map(|(index, _)| OutPoint::new(outpoint_source_id.clone(), index as u32)) + .collect(); + Self { + confirmed_txs: std::iter::once((genesis_tx.get_id().get(), genesis_tx)).collect(), + available_outpoints: outpoints, + } + } + + fn confirmed_txs(&self) -> &HashMap { + &self.confirmed_txs + } + + fn confirmed_outpoints(&self) -> BTreeSet { + self.available_outpoints + .iter() + .map(|outpoint| { + let tx_id = outpoint + .tx_id() + .get_tx_id() + .cloned() + .expect("Outpoints in these tests are created from TXs"); + let index = outpoint.output_index(); + let tx = self.confirmed_txs.get(&tx_id.get()).expect("Inconsistent Chain State"); + let output = tx + .outputs() + .get(index as usize) + .expect("Inconsistent Chain State: output not found"); + + valued_outpoint(&tx_id, index, output) + }) + .collect() + } + + fn add_confirmed_tx(&mut self, tx: Transaction) { + let outpoints_spent: BTreeSet<_> = + tx.inputs().iter().map(|input| input.outpoint()).collect(); + let outpoints_created: BTreeSet<_> = tx + .outputs() + .iter() + .enumerate() + .map(|(i, _)| OutPoint::new(OutPointSourceId::Transaction(tx.get_id()), i as u32)) + .collect(); + self.available_outpoints.extend(outpoints_created); + self.available_outpoints.retain(|outpoint| !outpoints_spent.contains(outpoint)); + self.confirmed_txs.insert(tx.get_id().get(), tx); + } +} + +impl ChainState for ChainStateMock { + fn contains_outpoint(&self, outpoint: &OutPoint) -> bool { + self.available_outpoints.iter().any(|value| *value == *outpoint) + } + + fn get_outpoint_value(&self, outpoint: &OutPoint) -> Result { + self.confirmed_txs + .get(&outpoint.tx_id().get_tx_id().expect("Not coinbase").get()) + .ok_or_else(|| anyhow::anyhow!("tx for outpoint sought in chain state, not found")) + .and_then(|tx| { + tx.outputs() + .get(outpoint.output_index() as usize) + .ok_or_else(|| anyhow::anyhow!("outpoint index out of bounds")) + .map(|output| output.value()) + }) + } +} + +/* FIXME The second call in the following flow sometimes returns TransactionAlreadyInMempool +let tx1 = TxGenerator::new(&mempool).generate_tx()?; +mempool.add_transaction(tx1)?; + +let tx2 = TxGenerator::new(&mempool).generate_tx()?; +mempool.add_transaction(tx2)?; +*/ +struct TxGenerator { + coin_pool: BTreeSet, + num_inputs: usize, + num_outputs: usize, + tx_fee: Option, + replaceable: bool, + allow_double_spend: bool, +} + +impl TxGenerator { + fn with_num_inputs(mut self, num_inputs: usize) -> Self { + self.num_inputs = num_inputs; + self + } + + fn with_num_outputs(mut self, num_outputs: usize) -> Self { + self.num_outputs = num_outputs; + self + } + + fn replaceable(mut self) -> Self { + self.replaceable = true; + self + } + + fn with_fee(mut self, fee: Amount) -> Self { + self.tx_fee = Some(fee); + self + } + + fn new() -> Self { + Self { + coin_pool: BTreeSet::new(), + num_inputs: 1, + num_outputs: 1, + tx_fee: None, + replaceable: false, + allow_double_spend: false, + } + } + + fn generate_tx( + &mut self, + mempool: &MempoolImpl, + ) -> anyhow::Result { + self.coin_pool = mempool.available_outpoints(self.allow_double_spend); + let fee = if let Some(tx_fee) = self.tx_fee { + tx_fee + } else { + Amount::from_atoms(get_relay_fee_from_tx_size(estimate_tx_size( + self.num_inputs, + self.num_outputs, + ))) + }; + log::debug!( + "Trying to build a tx with {} inputs, {} outputs, and a fee of {:?}", + self.num_inputs, + self.num_outputs, + fee + ); + let valued_inputs = self.generate_tx_inputs(fee)?; + let outputs = self.generate_tx_outputs(&valued_inputs, fee)?; + let locktime = 0; + let flags = if self.replaceable { 1 } else { 0 }; + let (inputs, _): (Vec, Vec) = valued_inputs.into_iter().unzip(); + let spent_outpoints = inputs.iter().map(|input| input.outpoint()).collect::>(); + self.coin_pool + .retain(|outpoint| !spent_outpoints.iter().any(|spent| **spent == outpoint.outpoint)); + let tx = Transaction::new(flags, inputs, outputs.clone(), locktime) + .map_err(anyhow::Error::from)?; + self.coin_pool.extend( + std::iter::repeat(tx.get_id()) + .zip(outputs.iter().enumerate()) + .map(|(id, (i, output))| valued_outpoint(&id, i as u32, output)), + ); + + Ok(tx) + } + + fn generate_tx_inputs(&mut self, fee: Amount) -> anyhow::Result> { + Ok(self + .get_unspent_outpoints(self.num_inputs, fee)? + .iter() + .map(|valued_outpoint| { + let ValuedOutPoint { outpoint, value } = valued_outpoint; + ( + TxInput::new( + outpoint.tx_id(), + outpoint.output_index(), + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + *value, + ) + }) + .collect()) + } + + fn generate_tx_outputs( + &self, + inputs: &[(TxInput, Amount)], + tx_fee: Amount, + ) -> anyhow::Result> { + if self.num_outputs == 0 { + return Ok(vec![]); + } + + let inputs: Vec<_> = inputs.to_owned(); + let (inputs, values): (Vec, Vec) = inputs.into_iter().unzip(); + if inputs.is_empty() { + return Ok(vec![]); + } + let sum_of_inputs = + values.into_iter().sum::>().expect("Overflow in sum of input values"); + + let total_to_spend = (sum_of_inputs - tx_fee).ok_or_else(||anyhow::anyhow!( + "generate_tx_outputs: underflow computing total_to_spend - sum_of_inputs = {:?}, fee = {:?}", sum_of_inputs, tx_fee + ))?; + + let value = (sum_of_inputs / u128::try_from(self.num_outputs).expect("conversion")) + .expect("not dividing by zero"); + + let mut left_to_spend = total_to_spend; + let mut outputs = Vec::new(); + + for _ in 0..self.num_outputs - 1 { + outputs.push(TxOutput::new( + value, + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + )); + left_to_spend = (left_to_spend - value).expect("subtraction failed"); + } + + outputs.push(TxOutput::new( + left_to_spend, + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + )); + Ok(outputs) + } + + fn get_unspent_outpoints( + &self, + num_outputs: usize, + fee: Amount, + ) -> anyhow::Result> { + log::debug!( + "get_unspent_outpoints: num_outputs: {}, fee: {:?}", + num_outputs, + fee + ); + let num_available_outpoints = self.coin_pool.len(); + let outpoints: Vec<_> = (num_available_outpoints >= num_outputs) + .then(|| self.coin_pool.iter().take(num_outputs).cloned().collect()) + .ok_or_else(|| anyhow::anyhow!("no outpoints left"))?; + let sum_of_outputs = outpoints + .iter() + .map(|valued_outpoint| valued_outpoint.value) + .sum::>() + .expect("sum error"); + if fee > sum_of_outputs { + Err(anyhow::Error::msg( + "get_unspent_outpoints:: fee is {:?} but sum of outputs is {:?}", + )) + } else { + Ok(outpoints) + } + } +} + +fn get_relay_fee_from_tx_size(tx_size: usize) -> u128 { + u128::try_from(tx_size * RELAY_FEE_PER_BYTE).expect("relay fee overflow") +} + +#[test] +fn add_single_tx() -> anyhow::Result<()> { + let mut mempool = setup(); + + let genesis_tx = mempool + .chain_state + .confirmed_txs() + .values() + .next() + .expect("genesis tx not found"); + + let outpoint_source_id = OutPointSourceId::Transaction(genesis_tx.get_id()); + + let flags = 0; + let locktime = 0; + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let relay_fee = Amount::from_atoms(get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE)); + let tx = tx_spend_input(&mempool, input, relay_fee, flags, locktime)?; + + let tx_clone = tx.clone(); + let tx_id = tx.get_id(); + mempool.add_transaction(tx)?; + assert!(mempool.contains_transaction(&tx_id)); + let all_txs = mempool.get_all(); + assert_eq!(all_txs, vec![&tx_clone]); + mempool.drop_transaction(&tx_id); + assert!(!mempool.contains_transaction(&tx_id)); + let all_txs = mempool.get_all(); + assert_eq!(all_txs, Vec::<&Transaction>::new()); + Ok(()) +} + +#[test] +fn txs_sorted() -> anyhow::Result<()> { + let mut mempool = setup(); + let mut tx_generator = TxGenerator::new(); + let target_txs = 10; + + for _ in 0..target_txs { + match tx_generator.generate_tx(&mempool) { + Ok(tx) => { + mempool.add_transaction(tx.clone())?; + } + _ => break, + } + } + + let fees = mempool + .get_all() + .iter() + .map(|tx| mempool.try_get_fee(tx)) + .collect::, _>>()?; + let mut fees_sorted = fees.clone(); + fees_sorted.sort_by(|a, b| b.cmp(a)); + assert_eq!(fees, fees_sorted); + Ok(()) +} + +#[test] +fn tx_no_inputs() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .with_num_inputs(0) + .with_fee(Amount::from_atoms(0)) + .generate_tx(&mempool) + .expect("generate_tx failed"); + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError(TxValidationError::NoInputs)) + )); + Ok(()) +} + +fn setup() -> MempoolImpl { + logging::init_logging::<&str>(None); + MempoolImpl::create( + ChainStateMock::new(), + SystemClock {}, + SystemUsageEstimator {}, + ) +} + +#[test] +fn tx_no_outputs() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .with_num_outputs(0) + .generate_tx(&mempool) + .expect("generate_tx failed"); + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError(TxValidationError::NoOutputs)) + )); + Ok(()) +} + +#[test] +fn tx_duplicate_inputs() -> anyhow::Result<()> { + let mut mempool = setup(); + + let genesis_tx = mempool + .chain_state + .confirmed_txs() + .values() + .next() + .expect("genesis tx not found"); + + let outpoint_source_id = OutPointSourceId::Transaction(genesis_tx.get_id()); + let input = TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let witness = b"attempted_double_spend".to_vec(); + let duplicate_input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(witness)), + ); + let flags = 0; + let locktime = 0; + let outputs = tx_spend_input(&mempool, input.clone(), None, flags, locktime)? + .outputs() + .clone(); + let inputs = vec![input, duplicate_input]; + let tx = Transaction::new(flags, inputs, outputs, locktime)?; + + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError(TxValidationError::DuplicateInputs)) + )); + Ok(()) +} + +#[test] +fn tx_already_in_mempool() -> anyhow::Result<()> { + let mut mempool = setup(); + + let genesis_tx = mempool + .chain_state + .confirmed_txs() + .values() + .next() + .expect("genesis tx not found"); + + let outpoint_source_id = OutPointSourceId::Transaction(genesis_tx.get_id()); + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + let flags = 0; + let locktime = 0; + let tx = tx_spend_input(&mempool, input, None, flags, locktime)?; + + mempool.add_transaction(tx.clone())?; + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError( + TxValidationError::TransactionAlreadyInMempool + )) + )); + Ok(()) +} + +#[test] +fn outpoint_not_found() -> anyhow::Result<()> { + let mut mempool = setup(); + + let genesis_tx = mempool + .chain_state + .confirmed_txs() + .values() + .next() + .expect("genesis tx not found"); + + let outpoint_source_id = OutPointSourceId::Transaction(genesis_tx.get_id()); + + let good_input = TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let flags = 0; + let locktime = 0; + let outputs = tx_spend_input(&mempool, good_input, None, flags, locktime)?.outputs().clone(); + + let bad_outpoint_index = 1; + let bad_input = TxInput::new( + outpoint_source_id, + bad_outpoint_index, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + let inputs = vec![bad_input]; + let tx = Transaction::new(flags, inputs, outputs, locktime)?; + + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError( + TxValidationError::OutPointNotFound { .. } + )) + )); + + Ok(()) +} + +#[test] +fn tx_too_big() -> anyhow::Result<()> { + let mut mempool = setup(); + let single_output_size = TxOutput::new( + Amount::from_atoms(100), + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + ) + .encoded_size(); + let too_many_outputs = MAX_BLOCK_SIZE_BYTES / single_output_size; + let tx = TxGenerator::new() + .with_num_outputs(too_many_outputs) + .generate_tx(&mempool) + .expect("generate_tx failed"); + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::TxValidationError( + TxValidationError::ExceedsMaxBlockSize + )) + )); + Ok(()) +} + +fn test_replace_tx(original_fee: Amount, replacement_fee: Amount) -> Result<(), Error> { + log::debug!( + "tx_replace_tx: original_fee: {:?}, replacement_fee {:?}", + original_fee, + replacement_fee + ); + let mut mempool = setup(); + let outpoint = mempool + .available_outpoints(true) + .iter() + .next() + .expect("there should be an outpoint since setup creates the genesis transaction") + .outpoint + .clone(); + + let outpoint_source_id = + OutPointSourceId::from(*outpoint.tx_id().get_tx_id().expect("Not Coinbase")); + + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let flags = 1; + let locktime = 0; + let original = tx_spend_input(&mempool, input.clone(), original_fee, flags, locktime) + .expect("should be able to spend here"); + let original_id = original.get_id(); + log::debug!("created a tx with fee {:?}", mempool.try_get_fee(&original)); + mempool.add_transaction(original)?; + + let flags = 0; + let replacement = tx_spend_input(&mempool, input, replacement_fee, flags, locktime) + .expect("should be able to spend here"); + log::debug!( + "created a replacement with fee {:?}", + mempool.try_get_fee(&replacement) + ); + mempool.add_transaction(replacement)?; + assert!(!mempool.contains_transaction(&original_id)); + + Ok(()) +} + +#[test] +fn try_replace_irreplaceable() -> anyhow::Result<()> { + let mut mempool = setup(); + let outpoint = mempool + .available_outpoints(true) + .iter() + .next() + .expect("there should be an outpoint since setup creates the genesis transaction") + .outpoint + .clone(); + + let outpoint_source_id = + OutPointSourceId::from(*outpoint.tx_id().get_tx_id().expect("Not Coinbase")); + + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let flags = 0; + let locktime = 0; + let original_fee = Amount::from_atoms(get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE)); + let original = tx_spend_input(&mempool, input.clone(), original_fee, flags, locktime) + .expect("should be able to spend here"); + let original_id = original.get_id(); + mempool.add_transaction(original)?; + + let flags = 0; + let replacement_fee = (original_fee + Amount::from_atoms(1000)).unwrap(); + let replacement = tx_spend_input(&mempool, input, replacement_fee, flags, locktime) + .expect("should be able to spend here"); + assert!(matches!( + mempool.add_transaction(replacement.clone()), + Err(Error::TxValidationError( + TxValidationError::ConflictWithIrreplaceableTransaction + )) + )); + + mempool.drop_transaction(&original_id); + mempool.add_transaction(replacement)?; + + Ok(()) +} + +#[test] +fn tx_replace() -> anyhow::Result<()> { + let relay_fee = get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE); + let replacement_fee = Amount::from_atoms(relay_fee + 100); + test_replace_tx(Amount::from_atoms(100), replacement_fee)?; + let res = test_replace_tx(Amount::from_atoms(300), replacement_fee); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::InsufficientFeesToRelayRBF + )) + )); + let res = test_replace_tx(Amount::from_atoms(100), Amount::from_atoms(100)); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::ReplacementFeeLowerThanOriginal { .. } + )) + )); + let res = test_replace_tx(Amount::from_atoms(100), Amount::from_atoms(90)); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::ReplacementFeeLowerThanOriginal { .. } + )) + )); + Ok(()) +} + +#[test] +fn tx_replace_child() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .replaceable() + .generate_tx(&mempool) + .expect("generate_replaceable_tx"); + mempool.add_transaction(tx.clone())?; + + let outpoint_source_id = OutPointSourceId::Transaction(tx.get_id()); + let child_tx_input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + // We want to test that even though child_tx doesn't signal replaceability directly, it is replaceable because its parent signalled replaceability + // replaced + let flags = 0; + let locktime = 0; + let child_tx = tx_spend_input( + &mempool, + child_tx_input.clone(), + Amount::from_atoms(100), + flags, + locktime, + )?; + mempool.add_transaction(child_tx)?; + + let relay_fee = get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE); + let replacement_fee = Amount::from_atoms(relay_fee + 100); + let replacement_tx = + tx_spend_input(&mempool, child_tx_input, replacement_fee, flags, locktime)?; + mempool.add_transaction(replacement_tx)?; + Ok(()) +} + +// To test our validation of BIP125 Rule#4 (replacement transaction pays for its own bandwidth), we need to know the necessary relay fee before creating the transaction. The relay fee depends on the size of the transaction. The usual way to get the size of a transaction is to call `tx.encoded_size` but we cannot do this until we have created the transaction itself. To get around this cycle, we have precomputed the size of all transaction created by `tx_spend_input`. This value will be the same for all transactions created by this function. +const TX_SPEND_INPUT_SIZE: usize = 213; + +fn tx_spend_input( + mempool: &MempoolImpl, + input: TxInput, + fee: impl Into>, + flags: u32, + locktime: u32, +) -> anyhow::Result { + let fee = fee.into().map_or_else( + || Amount::from_atoms(get_relay_fee_from_tx_size(estimate_tx_size(1, 2))), + std::convert::identity, + ); + tx_spend_several_inputs(mempool, &[input], fee, flags, locktime) +} + +fn tx_spend_several_inputs( + mempool: &MempoolImpl, + inputs: &[TxInput], + fee: Amount, + flags: u32, + locktime: u32, +) -> anyhow::Result { + let input_value = inputs + .iter() + .map(|input| mempool.get_input_value(input)) + .collect::, _>>()? + .into_iter() + .sum::>() + .ok_or_else(|| { + let msg = String::from("tx_spend_input: overflow"); + log::error!("{}", msg); + anyhow::Error::msg(msg) + })?; + + let available_for_spending = (input_value - fee).ok_or_else(|| { + let msg = format!( + "tx_spend_several_inputs: input_value ({:?}) lower than fee ({:?})", + input_value, fee + ); + log::error!("{}", msg); + anyhow::Error::msg(msg) + })?; + let spent = (available_for_spending / 2).expect("division error"); + + let change = (available_for_spending - spent).ok_or_else(|| { + let msg = String::from("Error computing change"); + anyhow::Error::msg(msg) + })?; + + Transaction::new( + flags, + inputs.to_owned(), + vec![ + TxOutput::new(spent, OutputPurpose::Transfer(Destination::AnyoneCanSpend)), + TxOutput::new(change, OutputPurpose::Transfer(Destination::AnyoneCanSpend)), + ], + locktime, + ) + .map_err(Into::into) +} + +#[test] +fn one_ancestor_replaceability_signal_is_enough() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .with_num_outputs(2) + .generate_tx(&mempool) + .expect("generate_replaceable_tx"); + + mempool.add_transaction(tx.clone())?; + + let flags_replaceable = 1; + let flags_irreplaceable = 0; + let locktime = 0; + + let outpoint_source_id = OutPointSourceId::Transaction(tx.get_id()); + let ancestor_with_signal = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags_replaceable, + locktime, + )?; + + let ancestor_without_signal = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id, + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags_irreplaceable, + locktime, + )?; + + mempool.add_transaction(ancestor_with_signal.clone())?; + mempool.add_transaction(ancestor_without_signal.clone())?; + + let input_with_replaceable_parent = TxInput::new( + OutPointSourceId::Transaction(ancestor_with_signal.get_id()), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + let input_with_irreplaceable_parent = TxInput::new( + OutPointSourceId::Transaction(ancestor_without_signal.get_id()), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + // TODO compute minimum necessary relay fee instead of just overestimating it + let original_fee = Amount::from_atoms(200); + let dummy_output = TxOutput::new( + original_fee, + OutputPurpose::Transfer(Destination::AnyoneCanSpend), + ); + let replaced_tx = tx_spend_several_inputs( + &mempool, + &[input_with_irreplaceable_parent.clone(), input_with_replaceable_parent], + original_fee, + flags_irreplaceable, + locktime, + )?; + let replaced_tx_id = replaced_tx.get_id(); + + mempool.add_transaction(replaced_tx)?; + + let replacing_tx = Transaction::new( + flags_irreplaceable, + vec![input_with_irreplaceable_parent], + vec![dummy_output], + locktime, + )?; + + mempool.add_transaction(replacing_tx)?; + assert!(!mempool.contains_transaction(&replaced_tx_id)); + + Ok(()) +} + +#[test] +fn tx_mempool_entry() -> anyhow::Result<()> { + use common::primitives::time; + let mut mempool = setup(); + // Input different flag values just to make the hashes of these dummy transactions + // different + let txs = (1..=6) + .into_iter() + .map(|i| Transaction::new(i, vec![], vec![], 0).unwrap_or_else(|_| panic!("tx {}", i))) + .collect::>(); + let fee = Amount::from_atoms(0); + + // Generation 1 + let tx1_parents = BTreeSet::default(); + let entry1 = TxMempoolEntry::new(txs.get(0).unwrap().clone(), fee, tx1_parents, time::get()); + let tx2_parents = BTreeSet::default(); + let entry2 = TxMempoolEntry::new(txs.get(1).unwrap().clone(), fee, tx2_parents, time::get()); + + // Generation 2 + let tx3_parents = vec![entry1.tx_id(), entry2.tx_id()].into_iter().collect(); + let entry3 = TxMempoolEntry::new(txs.get(2).unwrap().clone(), fee, tx3_parents, time::get()); + + // Generation 3 + let tx4_parents = vec![entry3.tx_id()].into_iter().collect(); + let tx5_parents = vec![entry3.tx_id()].into_iter().collect(); + let entry4 = TxMempoolEntry::new(txs.get(3).unwrap().clone(), fee, tx4_parents, time::get()); + let entry5 = TxMempoolEntry::new(txs.get(4).unwrap().clone(), fee, tx5_parents, time::get()); + + // Generation 4 + let tx6_parents = vec![entry3.tx_id(), entry4.tx_id(), entry5.tx_id()].into_iter().collect(); + let entry6 = TxMempoolEntry::new(txs.get(5).unwrap().clone(), fee, tx6_parents, time::get()); + + let entries = vec![entry1, entry2, entry3, entry4, entry5, entry6]; + let ids = entries.clone().into_iter().map(|entry| entry.tx_id()).collect::>(); + + for entry in entries.into_iter() { + mempool.store.add_tx(entry)?; + } + + let entry1 = mempool.store.get_entry(ids.get(0).expect("index")).expect("entry"); + let entry2 = mempool.store.get_entry(ids.get(1).expect("index")).expect("entry"); + let entry3 = mempool.store.get_entry(ids.get(2).expect("index")).expect("entry"); + let entry4 = mempool.store.get_entry(ids.get(3).expect("index")).expect("entry"); + let entry5 = mempool.store.get_entry(ids.get(4).expect("index")).expect("entry"); + let entry6 = mempool.store.get_entry(ids.get(5).expect("index")).expect("entry"); + assert_eq!(entry1.unconfirmed_ancestors(&mempool.store).0.len(), 0); + assert_eq!(entry2.unconfirmed_ancestors(&mempool.store).0.len(), 0); + assert_eq!(entry3.unconfirmed_ancestors(&mempool.store).0.len(), 2); + assert_eq!(entry4.unconfirmed_ancestors(&mempool.store).0.len(), 3); + assert_eq!(entry5.unconfirmed_ancestors(&mempool.store).0.len(), 3); + assert_eq!(entry6.unconfirmed_ancestors(&mempool.store).0.len(), 5); + + assert_eq!(entry1.count_with_descendants(), 5); + assert_eq!(entry2.count_with_descendants(), 5); + assert_eq!(entry3.count_with_descendants(), 4); + assert_eq!(entry4.count_with_descendants(), 2); + assert_eq!(entry5.count_with_descendants(), 2); + assert_eq!(entry6.count_with_descendants(), 1); + + Ok(()) +} + +fn test_bip125_max_replacements( + mempool: &mut MempoolImpl, + num_potential_replacements: usize, +) -> anyhow::Result<()> { + let tx = TxGenerator::new() + .with_num_outputs(num_potential_replacements - 1) + .replaceable() + .generate_tx(mempool) + .expect("generate_tx failed"); + let input = tx.inputs().first().expect("one input").clone(); + let outputs = tx.outputs().clone(); + let tx_id = tx.get_id(); + mempool.add_transaction(tx)?; + + let flags = 0; + let locktime = 0; + let outpoint_source_id = OutPointSourceId::Transaction(tx_id); + let fee = get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE); + for (index, _) in outputs.iter().enumerate() { + let input = TxInput::new( + outpoint_source_id.clone(), + index.try_into().unwrap(), + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let tx = tx_spend_input(mempool, input, Amount::from_atoms(fee), flags, locktime)?; + mempool.add_transaction(tx)?; + } + let mempool_size_before_replacement = mempool.store.txs_by_id.len(); + + let replacement_fee = Amount::from_atoms(1000) * fee; + let replacement_tx = tx_spend_input(mempool, input, replacement_fee, flags, locktime)?; + mempool.add_transaction(replacement_tx).map_err(anyhow::Error::from)?; + let mempool_size_after_replacement = mempool.store.txs_by_id.len(); + + assert_eq!( + mempool_size_after_replacement, + mempool_size_before_replacement - num_potential_replacements + 1 + ); + Ok(()) +} + +#[test] +fn too_many_conflicts() -> anyhow::Result<()> { + let mut mempool = setup(); + let num_potential_replacements = MAX_BIP125_REPLACEMENT_CANDIDATES + 1; + let err = test_bip125_max_replacements(&mut mempool, num_potential_replacements) + .expect_err("expected error TooManyPotentialReplacements") + .downcast() + .expect("failed to downcast"); + assert!(matches!( + err, + Error::TxValidationError(TxValidationError::TooManyPotentialReplacements) + )); + Ok(()) +} + +#[test] +fn not_too_many_conflicts() -> anyhow::Result<()> { + let mut mempool = setup(); + let num_potential_replacements = MAX_BIP125_REPLACEMENT_CANDIDATES; + test_bip125_max_replacements(&mut mempool, num_potential_replacements) +} + +#[test] +fn spends_new_unconfirmed() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .with_num_outputs(2) + .replaceable() + .generate_tx(&mempool) + .expect("generate_replaceable_tx"); + let outpoint_source_id = OutPointSourceId::Transaction(tx.get_id()); + mempool.add_transaction(tx)?; + + let input1 = TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + let input2 = TxInput::new( + outpoint_source_id, + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + let locktime = 0; + let flags = 0; + let original_fee = Amount::from_atoms(100); + let replaced_tx = tx_spend_input(&mempool, input1.clone(), original_fee, flags, locktime)?; + mempool.add_transaction(replaced_tx)?; + let relay_fee = get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE); + let replacement_fee = Amount::from_atoms(100 + relay_fee); + let incoming_tx = tx_spend_several_inputs( + &mempool, + &[input1, input2], + replacement_fee, + flags, + locktime, + )?; + + let res = mempool.add_transaction(incoming_tx); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::SpendsNewUnconfirmedOutput + )) + )); + Ok(()) +} + +#[test] +fn pays_more_than_conflicts_with_descendants() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new().generate_tx(&mempool).expect("generate_replaceable_tx"); + let tx_id = tx.get_id(); + mempool.add_transaction(tx)?; + + let outpoint_source_id = OutPointSourceId::Transaction(tx_id); + let input = TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ); + + let locktime = 0; + let rbf = 1; + let no_rbf = 0; + + // Create transaction that we will attempt to replace + let original_fee = Amount::from_atoms(100); + let replaced_tx = tx_spend_input(&mempool, input.clone(), original_fee, rbf, locktime)?; + let replaced_tx_fee = mempool.try_get_fee(&replaced_tx)?; + let replaced_id = replaced_tx.get_id(); + mempool.add_transaction(replaced_tx)?; + + // Create some children for this transaction + let descendant_outpoint_source_id = OutPointSourceId::Transaction(replaced_id); + + let descendant1_fee = Amount::from_atoms(100); + let descendant1 = tx_spend_input( + &mempool, + TxInput::new( + descendant_outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + descendant1_fee, + no_rbf, + locktime, + )?; + let descendant1_id = descendant1.get_id(); + mempool.add_transaction(descendant1)?; + + let descendant2_fee = Amount::from_atoms(100); + let descendant2 = tx_spend_input( + &mempool, + TxInput::new( + descendant_outpoint_source_id, + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + descendant2_fee, + no_rbf, + locktime, + )?; + let descendant2_id = descendant2.get_id(); + mempool.add_transaction(descendant2)?; + + //Create a new incoming transaction that conflicts with `replaced_tx` because it spends + //`input`. It will be rejected because its fee exactly equals (so is not greater than) the + //sum of the fees of the conflict together with its descendants + let insufficient_rbf_fee = [replaced_tx_fee, descendant1_fee, descendant2_fee] + .into_iter() + .sum::>() + .unwrap(); + let incoming_tx = tx_spend_input( + &mempool, + input.clone(), + insufficient_rbf_fee, + no_rbf, + locktime, + )?; + + assert!(matches!( + mempool.add_transaction(incoming_tx), + Err(Error::TxValidationError( + TxValidationError::TransactionFeeLowerThanConflictsWithDescendants + )) + )); + + let relay_fee = get_relay_fee_from_tx_size(TX_SPEND_INPUT_SIZE); + let sufficient_rbf_fee = insufficient_rbf_fee + Amount::from_atoms(relay_fee); + let incoming_tx = tx_spend_input(&mempool, input, sufficient_rbf_fee, no_rbf, locktime)?; + mempool.add_transaction(incoming_tx)?; + + assert!(!mempool.contains_transaction(&replaced_id)); + assert!(!mempool.contains_transaction(&descendant1_id)); + assert!(!mempool.contains_transaction(&descendant2_id)); + Ok(()) +} + +#[derive(Clone)] +struct MockClock { + time: Arc, +} + +impl MockClock { + fn new() -> Self { + Self { + time: Arc::new(AtomicU64::new(0)), + } + } + + fn set(&self, time: Time) { + self.time.store(time.as_secs(), Ordering::SeqCst) + } + + fn increment(&self, inc: Time) { + self.time.store( + self.time.load(Ordering::SeqCst) + inc.as_secs(), + Ordering::SeqCst, + ) + } +} + +impl GetTime for MockClock { + fn get_time(&self) -> Time { + Duration::new(self.time.load(Ordering::SeqCst), 0) + } +} + +#[test] +fn only_expired_entries_removed() -> anyhow::Result<()> { + let mock_clock = MockClock::new(); + + let mut mempool = MempoolImpl::create( + ChainStateMock::new(), + mock_clock.clone(), + SystemUsageEstimator {}, + ); + + let num_inputs = 1; + let num_outputs = 2; + let big_fee = get_relay_fee_from_tx_size(estimate_tx_size(num_inputs, num_outputs)) + 100; + let parent = TxGenerator::new() + .with_num_inputs(num_inputs) + .with_num_outputs(num_outputs) + .with_fee(Amount::from_atoms(big_fee)) + .generate_tx(&mempool)?; + let parent_id = parent.get_id(); + mempool.add_transaction(parent)?; + + let flags = 0; + let locktime = 0; + let outpoint_source_id = OutPointSourceId::Transaction(parent_id); + let child_0 = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags, + locktime, + )?; + + let child_1 = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id, + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags, + locktime, + )?; + let child_1_id = child_1.get_id(); + + let expired_tx_id = child_0.get_id(); + mempool.add_transaction(child_0)?; + + // Simulate the parent being added to a block + // We have to do this because if we leave this parent in the mempool then it will be + // expired, and so removed along with both its children, and thus the addition of child_1 to + // the mempool will fail + mempool.process_block(&parent_id)?; + mock_clock.set(DEFAULT_MEMPOOL_EXPIRY + Duration::new(1, 0)); + + mempool.add_transaction(child_1)?; + assert!(!mempool.contains_transaction(&expired_tx_id)); + assert!(mempool.contains_transaction(&child_1_id)); + Ok(()) +} + +#[test] +fn rolling_fee() -> anyhow::Result<()> { + logging::init_logging::<&str>(None); + let mock_clock = MockClock::new(); + let mut mock_usage = MockGetMemoryUsage::new(); + // Add parent + // Add first child + mock_usage.expect_get_memory_usage().times(2).return_const(0usize); + // Add second child, triggering the trimming process + mock_usage + .expect_get_memory_usage() + .times(1) + .return_const(MAX_MEMPOOL_SIZE_BYTES + 1); + // After removing one entry, cause the code to exit the loop by showing a small usage + mock_usage.expect_get_memory_usage().return_const(0usize); + + let chain_state = ChainStateMock::new(); + let mut mempool = MempoolImpl::create(chain_state, mock_clock.clone(), mock_usage); + + let num_inputs = 1; + let num_outputs = 3; + + // Use a higher than default fee because we don't want this transction to be evicted during + // the trimming process + let parent = TxGenerator::new() + .with_num_inputs(num_inputs) + .with_num_outputs(num_outputs) + .generate_tx(&mempool)?; + let parent_id = parent.get_id(); + log::debug!("parent_id: {}", parent_id.get()); + log::debug!("before adding parent"); + mempool.add_transaction(parent)?; + log::debug!("after adding parent"); + + let flags = 0; + let locktime = 0; + let outpoint_source_id = OutPointSourceId::Transaction(parent_id); + + // child_0 has the lower fee so it will be evicted when memory usage is too high + let child_0 = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags, + locktime, + )?; + let child_0_id = child_0.get_id(); + log::debug!("child_0_id {}", child_0_id.get()); + + let big_fee = Amount::from_atoms( + get_relay_fee_from_tx_size(estimate_tx_size(num_inputs, num_outputs)) + 100, + ); + let child_1 = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + big_fee, + flags, + locktime, + )?; + let child_1_id = child_1.get_id(); + log::debug!("child_1_id {}", child_1_id.get()); + mempool.add_transaction(child_0.clone())?; + log::debug!("added child_0"); + mempool.add_transaction(child_1)?; + log::debug!("added child_1"); + + assert_eq!(mempool.store.txs_by_id.len(), 2); + assert!(mempool.contains_transaction(&child_1_id)); + assert!(!mempool.contains_transaction(&child_0_id)); + let rolling_fee = mempool.get_minimum_rolling_fee(); + let child_0_fee = mempool.try_get_fee(&child_0)?; + log::debug!("FeeRate of child_0 {:?}", child_0_fee); + assert_eq!( + rolling_fee, + *INCREMENTAL_RELAY_FEE_RATE + + FeeRate::from_total_tx_fee(child_0_fee, child_0.encoded_size()) + ); + assert_eq!(rolling_fee, FeeRate::new(Amount::from_atoms(3582))); + log::debug!( + "minimum rolling fee after child_0's eviction {:?}", + rolling_fee + ); + assert_eq!( + rolling_fee, + FeeRate::from_total_tx_fee(mempool.try_get_fee(&child_0)?, child_0.encoded_size()) + + *INCREMENTAL_RELAY_FEE_RATE + ); + + // Now that the minimum rolling fee has been bumped up, a low-fee tx will not pass + // validation + let child_2 = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 2, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags, + locktime, + )?; + log::debug!( + "before child2: fee = {:?}, size = {}, minimum fee rate = {:?}", + mempool.try_get_fee(&child_2)?, + child_2.encoded_size(), + mempool.get_minimum_rolling_fee() + ); + let res = mempool.add_transaction(child_2); + log::debug!("result of adding child2 {:?}", res); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::RollingFeeThresholdNotMet { .. } + )) + )); + + // We provide a sufficient fee for the tx to pass the minimum rolling fee requirement + let child_2_high_fee = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id, + 2, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + mempool.get_minimum_rolling_fee().compute_fee(estimate_tx_size(1, 1)), + flags, + locktime, + )?; + log::debug!("before child2_high_fee"); + mempool.add_transaction(child_2_high_fee)?; + + // We simulate a block being accepted so the rolling fee will begin to decay + mempool.process_block(&parent_id)?; + + // Because the rolling fee is only updated when we attempt to add a tx to the mempool + // we need to submit a "dummy" tx to trigger these updates. + + // Since memory usage is now zero, it is less than 1/4 of the max size + // and ROLLING_FEE_BASE_HALFLIFE / 4 is the time it will take for the fee to halve + // We are going to submit dummy txs to the mempool incrementing time by this halflife + // between txs. Finally, when the fee rate falls under INCREMENTAL_RELAY_THRESHOLD, we + // observer that it is set to zero + let halflife = ROLLING_FEE_BASE_HALFLIFE / 4; + mock_clock.increment(halflife); + let dummy_tx = TxGenerator::new().with_fee(Amount::from_atoms(100)).generate_tx(&mempool)?; + log::debug!( + "First attempt to add dummy which pays a fee of {:?}", + mempool.try_get_fee(&dummy_tx)? + ); + let res = mempool.add_transaction(dummy_tx.clone()); + + log::debug!("Result of first attempt to add dummy: {:?}", res); + assert!(matches!( + res, + Err(Error::TxValidationError( + TxValidationError::RollingFeeThresholdNotMet { .. } + )) + )); + log::debug!( + "minimum rolling fee after first attempt to add dummy: {:?}", + mempool.get_minimum_rolling_fee() + ); + assert_eq!( + mempool.get_minimum_rolling_fee(), + rolling_fee / std::num::NonZeroU128::new(2).expect("nonzero") + ); + + mock_clock.increment(halflife); + log::debug!("Second attempt to add dummy"); + mempool.add_transaction(dummy_tx)?; + log::debug!( + "minimum rolling fee after first second to add dummy: {:?}", + mempool.get_minimum_rolling_fee() + ); + assert_eq!( + mempool.get_minimum_rolling_fee(), + rolling_fee / std::num::NonZeroU128::new(4).expect("nonzero") + ); + log::debug!( + "After successful addition of dummy, rolling fee rate is {:?}", + mempool.get_minimum_rolling_fee() + ); + + // Add another dummmy until rolling feerate drops to zero + mock_clock.increment(halflife); + let another_dummy = + TxGenerator::new().with_fee(Amount::from_atoms(100)).generate_tx(&mempool)?; + mempool.add_transaction(another_dummy)?; + assert_eq!( + mempool.get_minimum_rolling_fee(), + FeeRate::new(Amount::from_atoms(0)) + ); + + Ok(()) +} + +#[test] +fn different_size_txs() -> anyhow::Result<()> { + let mut mempool = setup(); + let initial_tx = TxGenerator::new() + .with_num_inputs(1) + .with_num_outputs(10_000) + .generate_tx(&mempool)?; + mempool.add_transaction(initial_tx)?; + + let target_txs = 100; + for i in 0..target_txs { + let num_inputs = i + 1; + let num_outputs = i + 1; + let tx = TxGenerator::new() + .with_num_inputs(num_inputs) + .with_num_outputs(num_outputs) + .generate_tx(&mempool)?; + mempool.add_transaction(tx)?; + } + + Ok(()) +} + +#[test] +fn descendant_score() -> anyhow::Result<()> { + let mut mempool = setup(); + let tx = TxGenerator::new() + .with_num_outputs(2) + .generate_tx(&mempool) + .expect("generate_replaceable_tx"); + let tx_id = tx.get_id(); + mempool.add_transaction(tx)?; + + let outpoint_source_id = OutPointSourceId::Transaction(tx_id); + + let flags = 0; + let locktime = 0; + + let tx_b_fee = Amount::from_atoms(get_relay_fee_from_tx_size(estimate_tx_size(1, 2))); + let tx_a_fee = (tx_b_fee + Amount::from_atoms(1000)).unwrap(); + let tx_c_fee = (tx_a_fee + Amount::from_atoms(1000)).unwrap(); + let tx_a = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + tx_a_fee, + flags, + locktime, + )?; + let tx_a_id = tx_a.get_id(); + log::debug!("tx_a_id : {}", tx_a_id.get()); + log::debug!("tx_a fee : {:?}", mempool.try_get_fee(&tx_a)?); + mempool.add_transaction(tx_a)?; + + let tx_b = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id, + 1, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + tx_b_fee, + flags, + locktime, + )?; + let tx_b_id = tx_b.get_id(); + log::debug!("tx_b_id : {}", tx_b_id.get()); + log::debug!("tx_b fee : {:?}", mempool.try_get_fee(&tx_b)?); + mempool.add_transaction(tx_b)?; + + let tx_c = tx_spend_input( + &mempool, + TxInput::new( + OutPointSourceId::Transaction(tx_b_id), + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + tx_c_fee, + flags, + locktime, + )?; + let tx_c_id = tx_c.get_id(); + log::debug!("tx_c_id : {}", tx_c_id.get()); + log::debug!("tx_c fee : {:?}", mempool.try_get_fee(&tx_c)?); + mempool.add_transaction(tx_c)?; + + let entry_a = mempool.store.txs_by_id.get(&tx_a_id.get()).expect("tx_a"); + log::debug!("entry a has score {:?}", entry_a.fees_with_descendants); + let entry_b = mempool.store.txs_by_id.get(&tx_b_id.get()).expect("tx_b"); + log::debug!("entry b has score {:?}", entry_b.fees_with_descendants); + let entry_c = mempool.store.txs_by_id.get(&tx_c_id.get()).expect("tx_c").clone(); + log::debug!("entry c has score {:?}", entry_c.fees_with_descendants); + assert_eq!(entry_a.fee, entry_a.fees_with_descendants); + assert_eq!( + entry_b.fees_with_descendants, + (entry_b.fee + entry_c.fee).unwrap() + ); + assert!(!mempool.store.txs_by_descendant_score.contains_key(&tx_b_fee.into())); + log::debug!( + "raw_txs_by_descendant_score {:?}", + mempool.store.txs_by_descendant_score + ); + check_txs_sorted_by_descendant_sore(&mempool); + + mempool.drop_transaction(&entry_c.tx.get_id()); + assert!(!mempool.store.txs_by_descendant_score.contains_key(&tx_c_fee.into())); + let entry_b = mempool.store.txs_by_id.get(&tx_b_id.get()).expect("tx_b"); + assert_eq!(entry_b.fees_with_descendants, entry_b.fee); + + check_txs_sorted_by_descendant_sore(&mempool); + + Ok(()) +} + +fn check_txs_sorted_by_descendant_sore( + mempool: &MempoolImpl, +) { + let txs_by_descendant_score = + mempool.store.txs_by_descendant_score.values().flatten().collect::>(); + for i in 0..(txs_by_descendant_score.len() - 1) { + log::debug!("i = {}", i); + let tx_id = txs_by_descendant_score.get(i).unwrap(); + let next_tx_id = txs_by_descendant_score.get(i + 1).unwrap(); + let entry_score = mempool.store.txs_by_id.get(&tx_id.get()).unwrap().fees_with_descendants; + let next_entry_score = + mempool.store.txs_by_id.get(&next_tx_id.get()).unwrap().fees_with_descendants; + log::debug!("entry_score: {:?}", entry_score); + log::debug!("next_entry_score: {:?}", next_entry_score); + assert!(entry_score <= next_entry_score) + } +} + +#[test] +fn descendant_of_expired_entry() -> anyhow::Result<()> { + let mock_clock = MockClock::new(); + logging::init_logging::<&str>(None); + + let mut mempool = MempoolImpl::create( + ChainStateMock::new(), + mock_clock.clone(), + SystemUsageEstimator {}, + ); + + let num_inputs = 1; + let num_outputs = 2; + let fee = get_relay_fee_from_tx_size(estimate_tx_size(num_inputs, num_outputs)); + let parent = TxGenerator::new() + .with_num_inputs(num_inputs) + .with_num_outputs(num_outputs) + .with_fee(Amount::from_atoms(fee)) + .generate_tx(&mempool)?; + let parent_id = parent.get_id(); + mempool.add_transaction(parent)?; + + let flags = 0; + let locktime = 0; + let outpoint_source_id = OutPointSourceId::Transaction(parent_id); + let child = tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id, + 0, + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + None, + flags, + locktime, + )?; + let child_id = child.get_id(); + mock_clock.set(DEFAULT_MEMPOOL_EXPIRY + Duration::new(1, 0)); + + assert!(matches!( + mempool.add_transaction(child), + Err(Error::TxValidationError( + TxValidationError::DescendantOfExpiredTransaction + )) + )); + + assert!(!mempool.contains_transaction(&parent_id)); + assert!(!mempool.contains_transaction(&child_id)); + Ok(()) +} + +#[test] +fn mempool_full() -> anyhow::Result<()> { + logging::init_logging::<&str>(None); + let mut mock_usage = MockGetMemoryUsage::new(); + mock_usage + .expect_get_memory_usage() + .times(1) + .return_const(MAX_MEMPOOL_SIZE_BYTES + 1); + + let chain_state = ChainStateMock::new(); + let mut mempool = MempoolImpl::create(chain_state, SystemClock, mock_usage); + + let tx = TxGenerator::new().generate_tx(&mempool)?; + log::debug!("mempool_full: tx has is {}", tx.get_id().get()); + assert!(matches!( + mempool.add_transaction(tx), + Err(Error::MempoolFull) + )); + Ok(()) +} + +#[test] +fn no_empty_bags_in_descendant_score_index() -> anyhow::Result<()> { + let mut mempool = setup(); + + let num_inputs = 1; + let num_outputs = 100; + let fee = get_relay_fee_from_tx_size(estimate_tx_size(num_inputs, num_outputs)); + let parent = TxGenerator::new() + .with_num_inputs(num_inputs) + .with_num_outputs(num_outputs) + .with_fee(Amount::from_atoms(fee)) + .generate_tx(&mempool)?; + let parent_id = parent.get_id(); + + let outpoint_source_id = OutPointSourceId::Transaction(parent.get_id()); + mempool.add_transaction(parent)?; + let num_child_txs = num_outputs; + let flags = 0; + let locktime = 0; + let txs = (0..num_child_txs) + .into_iter() + .map(|i| { + tx_spend_input( + &mempool, + TxInput::new( + outpoint_source_id.clone(), + u32::try_from(i).unwrap(), + InputWitness::NoSignature(Some(DUMMY_WITNESS_MSG.to_vec())), + ), + Amount::from_atoms(fee + u128::try_from(i).unwrap()), + flags, + locktime, + ) + }) + .collect::, _>>()?; + let ids = txs.iter().map(|tx| tx.get_id()).collect::>(); + + for tx in txs { + mempool.add_transaction(tx)?; + } + + mempool.drop_transaction(&parent_id); + for id in ids { + mempool.drop_transaction(&id) + } + assert!(mempool.store.txs_by_descendant_score.is_empty()); + Ok(()) +} diff --git a/mempool/src/pool/tests/utils.rs b/mempool/src/pool/tests/utils.rs new file mode 100644 index 0000000000..47bcdac2e1 --- /dev/null +++ b/mempool/src/pool/tests/utils.rs @@ -0,0 +1,55 @@ +use super::*; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub(in crate::pool::tests) struct ValuedOutPoint { + pub(in crate::pool::tests) outpoint: OutPoint, + pub(in crate::pool::tests) value: Amount, +} + +impl std::cmp::PartialOrd for ValuedOutPoint { + fn partial_cmp(&self, other: &Self) -> Option { + other.value.partial_cmp(&self.value) + } +} + +impl std::cmp::Ord for ValuedOutPoint { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other.value.cmp(&self.value) + } +} + +fn dummy_input() -> TxInput { + let outpoint_source_id = OutPointSourceId::Transaction(Id::new(H256::zero())); + let output_index = 0; + let witness = DUMMY_WITNESS_MSG.to_vec(); + TxInput::new( + outpoint_source_id, + output_index, + InputWitness::NoSignature(Some(witness)), + ) +} + +fn dummy_output() -> TxOutput { + let value = Amount::from_atoms(0); + let purpose = OutputPurpose::Transfer(Destination::AnyoneCanSpend); + TxOutput::new(value, purpose) +} + +pub(in crate::pool::tests) fn estimate_tx_size(num_inputs: usize, num_outputs: usize) -> usize { + let inputs = (0..num_inputs).into_iter().map(|_| dummy_input()).collect(); + let outputs = (0..num_outputs).into_iter().map(|_| dummy_output()).collect(); + let flags = 0; + let locktime = 0; + let size = Transaction::new(flags, inputs, outputs, locktime).unwrap().encoded_size(); + // Take twice the encoded size of the dummy tx.Real Txs are larger than these dummy ones, + // but taking 3 times the size seems to ensure our txs won't fail the minimum relay fee + // validation (see the function `pays_minimum_relay_fees`) + let result = 3 * size; + log::debug!( + "estimated size for tx with {} inputs and {} outputs: {}", + num_inputs, + num_outputs, + result + ); + result +} diff --git a/notes/time_lock_notes.txt b/notes/time_lock_notes.txt new file mode 100644 index 0000000000..f37fcc9560 --- /dev/null +++ b/notes/time_lock_notes.txt @@ -0,0 +1,35 @@ +NOTE: This file was written before we had any implementation of timelocks. We should handle timelocks in the mempool, and then also remove this file. + +TIMELOCK NOTES + +1. There are two kinds of timelocks, absolute and relative. Absolute timelocks prevent transmitting a tx over the network or spending a utxo before a specific point in time (measured either in Unix Epoch Time or block height). Relative timelocks prevent spending an outputs before it has aged by a certain amount of time. + +2. Absolute timelocks: + +* Transaction-level timelocks +In Bitcoin Core transactions have the nLocktime field. When a transaction is validated, if nLocktime is less than the current time, it won't be relayed to the network. +Notes: + In this way Alice can create a transaction in which she sends "postdated" funds to Bob, send Bob that transaction, and now Bob can submit the transaction when enough time has passed (before that it will just be rejected). + +* UTXO-level timelocks +There is an opcode, CHECKLOCKTIMEVERIFY. When a transaction is submitted, for every output being spent a script is run (locking, unlocking). CLTV can appear in the script and make it fail if the locktime parameter in the script is greater than the locktime of the transaction. +So CLTV actually RELIES ON nLocktime. + +3. Relative Timelocks +- the nSequence field was originally intended for something else +NOTE: need to check the whole deal about it being used as a way to signal if CLTV applied. +- Transaction validation fails unless all inputs have sufficiently aged (each according to its timelock type? need to check). +- CHECKSEQUENCEVERIFY opcode. The use of this opcode makes reference to nSequence, need to see how they work together. + +The transaction structure will use an +enum Locktime { + BlockHeight(RangedInt<0, 500,000,000>), + TimeStamp(UnixEpoch(Ranged)) +} + +We will need some conversions. +QUESTION: need to map out the conversions. + +QUESTIONS: + Do we want to implement absolute tx level timelocks the exact same way Bitcoin Core implements them? + Do we want to do things differently with relative timelocks since nSequence is kind of a mess? diff --git a/utils/src/newtype.rs b/utils/src/newtype.rs index 59b2962b7b..a3515dd741 100644 --- a/utils/src/newtype.rs +++ b/utils/src/newtype.rs @@ -15,7 +15,7 @@ #[macro_export] macro_rules! newtype { - ($(#[$meta:meta])* $vis:vis struct $name:ident($wrapped:ty)) => { + ($(#[$meta:meta])* $vis:vis struct $name:ident($wrapped:ty);) => { $(#[$meta])* $vis struct $name($wrapped); @@ -65,9 +65,10 @@ mod tests { } } - newtype!( + newtype! { #[derive(Clone, Debug)] - struct NewInt(OldInt)); + struct NewInt(OldInt); + } #[test] fn test_new_type() {