Repository: mintlayer/mintlayer-core
Pull request #767 (2023-03-16 15:21:33)

Add token bucket based rate limiter
This should prevent malicious peers from overloading the network with bogus addresses.


Patch:
diff --git a/dns_server/src/crawler_p2p/crawler/mod.rs b/dns_server/src/crawler_p2p/crawler/mod.rs
index 7ffd2359c..434ed6cdb 100644
--- a/dns_server/src/crawler_p2p/crawler/mod.rs
+++ b/dns_server/src/crawler_p2p/crawler/mod.rs
@@ -37,7 +37,13 @@ use std::{
 use common::chain::ChainConfig;
 use crypto::random::{seq::IteratorRandom, Rng};
 use logging::log;
-use p2p::{error::P2pError, net::types::PeerInfo, types::peer_id::PeerId};
+use p2p::{
+    error::P2pError,
+    net::types::PeerInfo,
+    peer_manager::{ADDR_RATE_BUCKET_SIZE, ADDR_RATE_INITIAL_SIZE, MAX_ADDR_RATE_PER_SECOND},
+    types::peer_id::PeerId,
+    utils::rate_limiter::RateLimiter,
+};
 
 use crate::crawler_p2p::crawler::address_data::AddressStateTransitionTo;
 
@@ -66,12 +72,17 @@ pub struct Crawler<A> {
     /// Map of all currently connected outbound peers that we successfully
     /// reached and are still connected to (generally speaking,
     /// we don't have to stay connected to those peers, but this is an implementation detail)
-    outbound_peers: BTreeMap<PeerId, A>,
+    outbound_peers: BTreeMap<PeerId, Peer<A>>,
+}
+
+struct Peer<A> {
+    address: A,
+    address_rate_limiter: RateLimiter,
 }
 
 pub enum CrawlerEvent<A> {
     Timer { period: Duration },
-    NewAddress { address: A },
+    NewAddress { address: A, sender: PeerId },
     Connected { address: A, peer_info: PeerInfo },
     Disconnected { peer_id: PeerId },
     ConnectionError { address: A, error: P2pError },
@@ -165,7 +176,12 @@ impl<A: Ord + Clone + ToString> Crawler<A> {
         self.remove_outbound_peer(peer_id, callback);
     }
 
-    fn handle_new_address(&mut self, address: A) {
+    fn handle_new_address(&mut self, address: A, sender: PeerId) {
+        let peer = self.outbound_peers.get_mut(&sender).expect("must be connected peer");
+        if !peer.address_rate_limiter.accept(self.now) {
+            log::debug!("address announcement is rate limited from peer {sender}");
+            return;
+        }
         if let Entry::Vacant(vacant) = self.addresses.entry(address.clone()) {
             log::debug!("new address {} added", address.to_string());
             vacant.insert(AddressData {
@@ -214,7 +230,19 @@ impl<A: Ord + Clone + ToString> Crawler<A> {
         peer_info: PeerInfo,
         callback: &mut impl FnMut(CrawlerCommand<A>),
     ) {
-        let old_peer = self.outbound_peers.insert(peer_id, address.clone());
+        let address_rate_limiter = RateLimiter::new(
+            self.now,
+            MAX_ADDR_RATE_PER_SECOND,
+            ADDR_RATE_INITIAL_SIZE,
+            ADDR_RATE_BUCKET_SIZE,
+        );
+
+        let peer = Peer {
+            address: address.clone(),
+            address_rate_limiter,
+        };
+
+        let old_peer = self.outbound_peers.insert(peer_id, peer);
         assert!(old_peer.is_none());
 
         let is_compatible = peer_info.is_compatible(&self.chain_config);
@@ -260,19 +288,19 @@ impl<A: Ord + Clone + ToString> Crawler<A> {
     ) {
         log::debug!("outbound peer removed, peer_id: {}", peer_id);
 
-        let address = self
+        let peer = self
             .outbound_peers
             .remove(&peer_id)
             .expect("peer must be known (remove_outbound_peer)");
 
         let address_data = self
             .addresses
-            .get_mut(&address)
+            .get_mut(&peer.address)
             .expect("address must be known (remove_outbound_peer)");
 
         Self::change_address_state(
             self.now,
-            &address,
+            &peer.address,
             address_data,
             AddressStateTransitionTo::Disconnected,
             callback,
@@ -324,8 +352,8 @@ impl<A: Ord + Clone + ToString> Crawler<A> {
 
                 self.heartbeat(callback, rng);
             }
-            CrawlerEvent::NewAddress { address } => {
-                self.handle_new_address(address);
+            CrawlerEvent::NewAddress { address, sender } => {
+                self.handle_new_address(address, sender);
             }
             CrawlerEvent::Connected { peer_info, address } => {
                 self.handle_connected(address, peer_info, callback);
diff --git a/dns_server/src/crawler_p2p/crawler_manager/mod.rs b/dns_server/src/crawler_p2p/crawler_manager/mod.rs
index c4ae9fdd2..f84f7c338 100644
--- a/dns_server/src/crawler_p2p/crawler_manager/mod.rs
+++ b/dns_server/src/crawler_p2p/crawler_manager/mod.rs
@@ -159,11 +159,11 @@ where
                 // Ignored
             }
             PeerManagerMessage::AnnounceAddrRequest(AnnounceAddrRequest { address }) => {
-                // TODO: Rate limit `AnnounceAddrRequest` requests from a specific peer to prevent DoS attack,
-                // when too many invalid addresses are announced, preventing the server from discovering new addresses.
-                // For example, Bitcoin Core allows 0.1 address/sec.
                 if let Some(address) = TransportAddress::from_peer_address(&address, false) {
-                    self.send_crawler_event(CrawlerEvent::NewAddress { address });
+                    self.send_crawler_event(CrawlerEvent::NewAddress {
+                        address,
+                        sender: peer_id,
+                    });
                 }
             }
             PeerManagerMessage::PingRequest(PingRequest { nonce }) => {
diff --git a/p2p/src/peer_manager/mod.rs b/p2p/src/peer_manager/mod.rs
index 526c686f4..7fb29b24e 100644
--- a/p2p/src/peer_manager/mod.rs
+++ b/p2p/src/peer_manager/mod.rs
@@ -56,7 +56,7 @@ use crate::{
         peer_address::{PeerAddress, PeerAddressIp4, PeerAddressIp6},
         peer_id::PeerId,
     },
-    utils::oneshot_nofail,
+    utils::{oneshot_nofail, rate_limiter::RateLimiter},
 };
 
 use self::{
@@ -76,6 +76,14 @@ const PEER_MGR_HEARTBEAT_INTERVAL_MAX: Duration = Duration::from_secs(30);
 /// How many addresses are allowed to be sent
 const MAX_ADDRESS_COUNT: usize = 1000;
 
+/// The maximum rate of address announcements the node will process from a peer (value as in Bitcoin Core).
+pub const MAX_ADDR_RATE_PER_SECOND: f64 = 0.1;
+/// Bucket size used to rate limit address announcements from a peer.
+/// Use 1 to allow peers to send one own address immediately after connecting.
+pub const ADDR_RATE_INITIAL_SIZE: u32 = 1;
+/// Bucket size used to rate limit address announcements from a peer.
+pub const ADDR_RATE_BUCKET_SIZE: u32 = 10;
+
 /// To how many peers resend received address
 const PEER_ADDRESS_RESEND_COUNT: usize = 2;
 
@@ -213,7 +221,9 @@ where
             })
             .collect::<Vec<_>>();
 
-        for address in discovered_own_addresses {
+        // Send only one address because of the rate limiter (see `ADDR_RATE_INITIAL_SIZE`).
+        // Select a random address to give all addresses a chance to be discovered by the network.
+        if let Some(address) = discovered_own_addresses.into_iter().choose(&mut make_pseudo_rng()) {
             self.announce_address(peer_id, address);
         }
     }
@@ -456,6 +466,13 @@ where
             );
         }
 
+        let address_rate_limiter = RateLimiter::new(
+            self.time_getter.get_time(),
+            MAX_ADDR_RATE_PER_SECOND,
+            ADDR_RATE_INITIAL_SIZE,
+            ADDR_RATE_BUCKET_SIZE,
+        );
+
         let old_value = self.peers.insert(
             peer_id,
             PeerContext {
@@ -468,6 +485,7 @@ where
                 ping_min: None,
                 expect_addr_list_response,
                 announced_addresses: HashSet::new(),
+                address_rate_limiter,
             },
         );
         assert!(old_value.is_none());
@@ -697,18 +715,21 @@ where
         }
     }
 
-    fn handle_announce_addr_request(&mut self, peer: PeerId, address: PeerAddress) {
-        // TODO: Rate limit announce address requests to prevent DoS attacks.
-        // For example it's 0.1 req/sec in Bitcoin Core.
+    fn handle_announce_addr_request(&mut self, peer_id: PeerId, address: PeerAddress) {
         if let Some(address) = <T::Address as TransportAddress>::from_peer_address(
             &address,
             *self.p2p_config.allow_discover_private_ips,
         ) {
-            self.peers
-                .get_mut(&peer)
-                .expect("peer sending AnnounceAddrRequest must be known")
-                .announced_addresses
-                .insert(address.clone());
+            let peer = self
+                .peers
+                .get_mut(&peer_id)
+                .expect("peer sending AnnounceAddrRequest must be known");
+            if !peer.address_rate_limiter.accept(self.time_getter.get_time()) {
+                log::debug!("address announcement is rate limited from peer {peer_id}");
+                return;
+            }
+
+            peer.announced_addresses.insert(address.clone());
 
             self.peerdb.peer_discovered(address.clone());
 
diff --git a/p2p/src/peer_manager/peer_context.rs b/p2p/src/peer_manager/peer_context.rs
index a15d735e8..6a6ff02ea 100644
--- a/p2p/src/peer_manager/peer_context.rs
+++ b/p2p/src/peer_manager/peer_context.rs
@@ -15,7 +15,10 @@
 
 use std::{collections::HashSet, time::Duration};
 
-use crate::net::types::{self, Role};
+use crate::{
+    net::types::{self, Role},
+    utils::rate_limiter::RateLimiter,
+};
 
 #[derive(Debug)]
 pub struct SentPing {
@@ -51,8 +54,10 @@ pub struct PeerContext<A> {
     /// It is used to score peers that send unsolicited address list responses.
     pub expect_addr_list_response: bool,
 
-    /// All addresses that were announced to or from some peer.
+    /// All addresses that were announced to or from this peer.
     /// Used to prevent infinity loops while broadcasting addresses.
     // TODO: Use bloom filter (like it's done in Bitcoin Core).
     pub announced_addresses: HashSet<A>,
+
+    pub address_rate_limiter: RateLimiter,
 }
diff --git a/p2p/src/utils/mod.rs b/p2p/src/utils/mod.rs
index 0e6a3a94b..862241fa4 100644
--- a/p2p/src/utils/mod.rs
+++ b/p2p/src/utils/mod.rs
@@ -14,3 +14,4 @@
 // limitations under the License.
 
 pub mod oneshot_nofail;
+pub mod rate_limiter;
diff --git a/p2p/src/utils/rate_limiter/mod.rs b/p2p/src/utils/rate_limiter/mod.rs
new file mode 100644
index 000000000..edbff718d
--- /dev/null
+++ b/p2p/src/utils/rate_limiter/mod.rs
@@ -0,0 +1,70 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![allow(clippy::float_arithmetic)]
+
+use std::time::Duration;
+
+/// Token bucket based rate limiter
+///
+/// See https://en.wikipedia.org/wiki/Token_bucket
+#[derive(Debug)]
+pub struct RateLimiter {
+    rate: f64,
+    tokens: f64,
+    bucket: u32,
+    last_time: Duration,
+}
+
+impl RateLimiter {
+    /// Construct new RateLimiter
+    ///
+    /// # Arguments
+    /// `now` - Current time
+    /// `rate` - Tokens per second
+    /// `initial_tokens` - Token count after the start
+    /// `bucket` - Bucket size
+    pub fn new(now: Duration, rate: f64, initial_tokens: u32, bucket: u32) -> Self {
+        assert!(rate >= 0.0);
+        assert!(initial_tokens <= bucket);
+        assert!(bucket >= 1);
+        RateLimiter {
+            rate,
+            tokens: initial_tokens.into(),
+            bucket,
+            last_time: now,
+        }
+    }
+
+    /// Check if the new request is within the allowed rate at the current time (updating the state)
+    ///
+    /// # Arguments
+    /// `now` - Current time
+    pub fn accept(&mut self, now: Duration) -> bool {
+        let seconds = now.checked_sub(self.last_time).unwrap_or_default().as_secs_f64();
+        self.last_time = now;
+        self.tokens = f64::min(self.tokens + self.rate * seconds, self.bucket.into());
+        // Use a value slightly less than 1.0 to account for f64 rounding errors (makes unit testing easier)
+        if self.tokens >= 0.99999 {
+            self.tokens -= 1.0;
+            true
+        } else {
+            false
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests;


Test patch:
diff --git a/dns_server/src/crawler_p2p/crawler/tests/mock_crawler.rs b/dns_server/src/crawler_p2p/crawler/tests/mock_crawler.rs
index b9c854dc8..91d771a43 100644
--- a/dns_server/src/crawler_p2p/crawler/tests/mock_crawler.rs
+++ b/dns_server/src/crawler_p2p/crawler/tests/mock_crawler.rs
@@ -89,7 +89,10 @@ impl MockCrawler {
     pub fn step(&mut self, event: CrawlerEvent<SocketAddr>, rng: &mut impl Rng) {
         match &event {
             CrawlerEvent::Timer { period: _ } => {}
-            CrawlerEvent::NewAddress { address: _ } => {}
+            CrawlerEvent::NewAddress {
+                address: _,
+                sender: _,
+            } => {}
             CrawlerEvent::Connected { peer_info, address } => {
                 let removed = self.pending_connects.remove(address);
                 assert!(removed);
diff --git a/dns_server/src/crawler_p2p/crawler/tests/mod.rs b/dns_server/src/crawler_p2p/crawler/tests/mod.rs
index 22c311601..de6f20fe7 100644
--- a/dns_server/src/crawler_p2p/crawler/tests/mod.rs
+++ b/dns_server/src/crawler_p2p/crawler/tests/mod.rs
@@ -73,7 +73,13 @@ fn basic(#[case] seed: Seed) {
     let node2: SocketAddr = "4.3.2.1:12345".parse().unwrap();
     let peer2 = PeerId::new();
 
-    crawler.step(CrawlerEvent::NewAddress { address: node2 }, &mut rng);
+    crawler.step(
+        CrawlerEvent::NewAddress {
+            address: node2,
+            sender: peer1,
+        },
+        &mut rng,
+    );
 
     crawler.timer(Duration::from_secs(100), &mut rng);
     assert!(crawler.pending_connects.contains(&node2));
@@ -198,7 +204,8 @@ fn randomized(#[case] seed: Seed) {
         // Randomly send an address announcement request
         if !crawler.peers.is_empty() && rng.gen_bool(0.01) {
             let address = nodes.iter().choose(&mut rng).cloned().unwrap();
-            crawler.step(CrawlerEvent::NewAddress { address }, &mut rng)
+            let sender = crawler.peers.keys().choose(&mut rng).cloned().unwrap();
+            crawler.step(CrawlerEvent::NewAddress { address, sender }, &mut rng)
         }
 
         if !crawler.peers.is_empty() && rng.gen_bool(0.001) {
diff --git a/p2p/src/peer_manager/tests/addresses.rs b/p2p/src/peer_manager/tests/addresses.rs
new file mode 100644
index 000000000..8dbb1a146
--- /dev/null
+++ b/p2p/src/peer_manager/tests/addresses.rs
@@ -0,0 +1,105 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::{collections::BTreeSet, sync::Arc, time::Duration};
+
+use common::{chain::config, primitives::user_agent::mintlayer_core_user_agent};
+
+use crate::{
+    net::{
+        default_backend::{
+            transport::{MpscChannelTransport, TransportAddress},
+            DefaultNetworkingService,
+        },
+        types::{PeerInfo, Role},
+        ConnectivityService, NetworkingService,
+    },
+    peer_manager::tests::make_peer_manager_custom,
+    testing_utils::{
+        test_p2p_config, P2pBasicTestTimeGetter, RandomAddressMaker, TestTcpAddressMaker,
+        TestTransportChannel, TestTransportMaker,
+    },
+    types::peer_id::PeerId,
+};
+
+async fn test_address_rate_limiter<A, T, B>()
+where
+    A: TestTransportMaker<Transport = T::Transport, Address = T::Address>,
+    T: NetworkingService + 'static + std::fmt::Debug,
+    T::ConnectivityHandle: ConnectivityService<T>,
+    B: RandomAddressMaker<Address = T::Address>,
+{
+    let addr = A::make_address();
+    let config = Arc::new(config::create_mainnet());
+    let p2p_config = Arc::new(test_p2p_config());
+    let time_getter = P2pBasicTestTimeGetter::new();
+    let (mut pm, _tx) = make_peer_manager_custom::<T>(
+        A::make_transport(),
+        addr,
+        Arc::clone(&config),
+        p2p_config,
+        time_getter.get_time_getter(),
+    )
+    .await;
+
+    let address = B::new();
+    let peer_id = PeerId::new();
+    let peer_info = PeerInfo {
+        peer_id,
+        network: *config.magic_bytes(),
+        version: *config.version(),
+        user_agent: mintlayer_core_user_agent(),
+        subscriptions: BTreeSet::new(),
+    };
+    pm.accept_connection(address, Role::Inbound, peer_info, None);
+    assert_eq!(pm.peers.len(), 1);
+
+    let get_new_public_address = || loop {
+        let address = B::new().as_peer_address();
+        if T::Address::from_peer_address(&address, false).is_some() {
+            return address;
+        }
+    };
+
+    // Check that nodes are allowed to send own address immediately after connecting
+    let address = get_new_public_address();
+    pm.handle_announce_addr_request(peer_id, address);
+    let accepted_count = pm.peerdb.known_addresses().count();
+    assert_eq!(accepted_count, 1);
+
+    for _ in 0..120 {
+        time_getter.advance_time(Duration::from_secs(1));
+        for _ in 0..100 {
+            pm.handle_announce_addr_request(peer_id, B::new().as_peer_address());
+        }
+    }
+    let accepted_count = pm.peerdb.known_addresses().count();
+    // The average expected count is 13 (1 + 120 * 0.1), but the exact number is not very important
+    assert!(
+        (5..25).contains(&accepted_count),
+        "Unexpected accepted address count: {accepted_count}"
+    );
+}
+
+// Test only TestTransportChannel because actual networking is not used
+#[tokio::test]
+async fn test_address_rate_limiter_channels() {
+    test_address_rate_limiter::<
+        TestTransportChannel,
+        DefaultNetworkingService<MpscChannelTransport>,
+        TestTcpAddressMaker,
+    >()
+    .await;
+}
diff --git a/p2p/src/peer_manager/tests/connections.rs b/p2p/src/peer_manager/tests/connections.rs
index 1184ce1e7..299f9d400 100644
--- a/p2p/src/peer_manager/tests/connections.rs
+++ b/p2p/src/peer_manager/tests/connections.rs
@@ -27,8 +27,8 @@ use crate::{
     peer_manager::tests::{get_connected_peers, run_peer_manager},
     testing_utils::{
         connect_and_accept_services, connect_services, get_connectivity_event,
-        peerdb_inmemory_store, test_p2p_config, P2pTokioTestTimeGetter, TestTransportChannel,
-        TestTransportMaker, TestTransportNoise, TestTransportTcp,
+        peerdb_inmemory_store, test_p2p_config, P2pBasicTestTimeGetter, P2pTokioTestTimeGetter,
+        TestTransportChannel, TestTransportMaker, TestTransportNoise, TestTransportTcp,
     },
     types::peer_id::PeerId,
     utils::oneshot_nofail,
@@ -776,7 +776,7 @@ where
 {
     let chain_config = Arc::new(config::create_mainnet());
 
-    let time_getter = P2pTokioTestTimeGetter::new();
+    let time_getter = P2pBasicTestTimeGetter::new();
 
     // Start the first peer manager
     let p2p_config_1 = Arc::new(P2pConfig {
@@ -888,13 +888,11 @@ where
             break;
         }
 
-        // Without this noise handshake does complete in time for some reasons
-        tokio::time::sleep(Duration::from_millis(10)).await;
-
-        time_getter.advance_time(Duration::from_millis(1000)).await;
+        tokio::time::sleep(Duration::from_millis(100)).await;
+        time_getter.advance_time(Duration::from_millis(1000));
 
         assert!(
-            Instant::now().duration_since(started_at) < Duration::from_secs(10),
+            Instant::now().duration_since(started_at) < Duration::from_secs(60),
             "Unexpected peer counts: {connected_peers_1}, {connected_peers_2}, {connected_peers_3}, expected: {EXPECTED_COUNT}"
         );
     }
diff --git a/p2p/src/peer_manager/tests/mod.rs b/p2p/src/peer_manager/tests/mod.rs
index c24666bbe..d039d9526 100644
--- a/p2p/src/peer_manager/tests/mod.rs
+++ b/p2p/src/peer_manager/tests/mod.rs
@@ -13,6 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+mod addresses;
 mod ban;
 mod connections;
 mod ping;
diff --git a/p2p/src/utils/rate_limiter/tests.rs b/p2p/src/utils/rate_limiter/tests.rs
new file mode 100644
index 000000000..3c8a32edc
--- /dev/null
+++ b/p2p/src/utils/rate_limiter/tests.rs
@@ -0,0 +1,49 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::*;
+
+fn run_test(seconds: u32, rate: f64, initial_tokens: u32, bucket: u32, expected: u32) {
+    let now = Duration::from_secs(0);
+    let mut rate_limiter = RateLimiter::new(now, rate, initial_tokens, bucket);
+    let mut actual = 0;
+    for i in 0..=seconds {
+        let now = Duration::from_secs(i.into());
+        while rate_limiter.accept(now) {
+            actual += 1;
+        }
+        // Check that RateLimiter is consistent
+        assert!(!rate_limiter.accept(now));
+    }
+    assert_eq!(
+        actual, expected,
+        "test failed, seconds: {seconds}, rate: {rate}, initial_tokens: {initial_tokens}, bucket: {bucket}, actual: {actual}, expected: {expected}"
+    );
+}
+
+#[test]
+fn rate_limiter_basic() {
+    // expected = seconds * rate + initial_bucket (normal fill rate)
+    run_test(120, 0.1, 0, 10, 12);
+    run_test(120, 1.0, 0, 10, 120);
+    run_test(120, 0.1, 10, 10, 22);
+    run_test(120, 1.0, 20, 20, 140);
+    run_test(120, 3.0, 0, 10, 360);
+    run_test(120, 10.0, 40, 40, 1240);
+
+    // expected = seconds * bucket (rate is limited by the bucket size)
+    run_test(60, 10.0, 0, 3, 180);
+    run_test(120, 100.0, 0, 5, 600);
+}