From 3fe32636247d4e0a8334bdb23f06f9339a65f1f3 Mon Sep 17 00:00:00 2001 From: fonglh Date: Mon, 3 Apr 2023 15:15:09 +0000 Subject: [PATCH 1/3] Use Decimal type for amount calculations in Level 1. This prevents other rounding bugs and comparison errors caused by the inability of floats to store base 10 numbers precisely. Add a test to hack.py to demonstrate the rounding error from using float. --- Level-1/hack.py | 10 +++++++++- Level-1/solution.py | 23 ++++++++++++++++++++--- Level-1/tests.py | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Level-1/hack.py b/Level-1/hack.py index 731ddc0..900b929 100644 --- a/Level-1/hack.py +++ b/Level-1/hack.py @@ -11,5 +11,13 @@ def test_4(self): order_4 = c.Order(id='4', items=[payment, tv, reimbursement]) self.assertEqual(c.validorder(order_4), 'Order ID: 4 - Payment imbalance: $-1000.00') + # Valid payments that should add up correctly, but do not + def test_5(self): + small_item = c.Item(type='product', description='sweets', amount=3.3, quantity=1) + payment_1 = c.Item(type='payment', description='invoice_5_1', amount=1.1, quantity=1) + payment_2 = c.Item(type='payment', description='invoice_5_2', amount=2.2, quantity=1) + order_5 = c.Order(id='5', items=[small_item, payment_1, payment_2]) + self.assertEqual(c.validorder(order_5), 'Order ID: 5 - Full payment received!') + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/Level-1/solution.py b/Level-1/solution.py index 82f16f9..0f85bd5 100644 --- a/Level-1/solution.py +++ b/Level-1/solution.py @@ -1,4 +1,5 @@ from collections import namedtuple +from decimal import * Order = namedtuple('Order', 'id, items') Item = namedtuple('Item', 'type, description, amount, quantity') @@ -8,16 +9,16 @@ MAX_TOTAL = 1e6 # maximum total amount accepted for an order def validorder(order): - net = 0 + net = Decimal('0') for item in order.items: if item.type == 'payment': # sets a reasonable min & max value for the invoice amounts if item.amount > -1*MAX_ITEM_AMOUNT and item.amount < MAX_ITEM_AMOUNT: - net += item.amount + net += Decimal(str(item.amount)) elif item.type == 'product': if item.quantity > 0 and item.quantity <= MAX_QUANTITY and item.amount > 0 and item.amount <= MAX_ITEM_AMOUNT: - net -= item.amount * item.quantity + net -= Decimal(str(item.amount)) * item.quantity if net > MAX_TOTAL or net < -1*MAX_TOTAL: return("Total amount exceeded") else: @@ -41,4 +42,20 @@ def validorder(order): We also need to protect from a scenario where the attacker sends a huge number of items, resulting in a huge net. We can do this by limiting all variables to reasonable values. + +In addition, using floating-point data types for calculations involving financial values causes unexpected rounding and comparison +errors as it cannot represent decimal numbers with the precision we expect. +For example, running `0.1 + 0.2` in the Python interpreter gives `0.30000000000000004` instead of 0.3. + +The solution to this is to use the Decimal type for calculations that should work in the same way "as the arithmetic that people learn at school." +-- except from Python's documentation on Decimal (https://docs.python.org/3/library/decimal.html). + +It is also necessary to convert the floating point values to string first before passing it to the Decimal constructor. +If the floating point value is passed to the Decimal constructor, the rounded value is stored instead. + +Compare the following examples from the interpreter: +>>> Decimal(0.3) +Decimal('0.299999999999999988897769753748434595763683319091796875') +>>> Decimal('0.3') +Decimal('0.3') ''' diff --git a/Level-1/tests.py b/Level-1/tests.py index 126e3d6..0e057a6 100644 --- a/Level-1/tests.py +++ b/Level-1/tests.py @@ -25,4 +25,4 @@ def test_3(self): self.assertEqual(c.validorder(order_3), 'Order ID: 3 - Payment imbalance: $-1000.00') if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 069bb5b2ebaefb87e8ba6d32cfc7b13ef43a9a97 Mon Sep 17 00:00:00 2001 From: Joseph Katsioloudes Date: Mon, 3 Apr 2023 19:38:16 +0100 Subject: [PATCH 2/3] improving the example --- Level-1/hack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Level-1/hack.py b/Level-1/hack.py index 900b929..74146c8 100644 --- a/Level-1/hack.py +++ b/Level-1/hack.py @@ -13,7 +13,7 @@ def test_4(self): # Valid payments that should add up correctly, but do not def test_5(self): - small_item = c.Item(type='product', description='sweets', amount=3.3, quantity=1) + small_item = c.Item(type='product', description='Accessory', amount=3.3, quantity=1) payment_1 = c.Item(type='payment', description='invoice_5_1', amount=1.1, quantity=1) payment_2 = c.Item(type='payment', description='invoice_5_2', amount=2.2, quantity=1) order_5 = c.Order(id='5', items=[small_item, payment_1, payment_2]) From 8e0219475d3445fbc2c7d6e2ddf8b17acf17824c Mon Sep 17 00:00:00 2001 From: Joseph Katsioloudes Date: Mon, 3 Apr 2023 19:39:51 +0100 Subject: [PATCH 3/3] fixing styling issue --- Level-1/hack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Level-1/hack.py b/Level-1/hack.py index 74146c8..a9c7dcb 100644 --- a/Level-1/hack.py +++ b/Level-1/hack.py @@ -13,7 +13,7 @@ def test_4(self): # Valid payments that should add up correctly, but do not def test_5(self): - small_item = c.Item(type='product', description='Accessory', amount=3.3, quantity=1) + small_item = c.Item(type='product', description='accessory', amount=3.3, quantity=1) payment_1 = c.Item(type='payment', description='invoice_5_1', amount=1.1, quantity=1) payment_2 = c.Item(type='payment', description='invoice_5_2', amount=2.2, quantity=1) order_5 = c.Order(id='5', items=[small_item, payment_1, payment_2])