:: [Libbitcoin] [PATCH] Implement a bi…
Forside
Slet denne besked
Besvar denne besked
Skribent: William Swanson
Dato:  
Til: libbitcoin
Emne: [Libbitcoin] [PATCH] Implement a bitcoin URI parser
This took quite a bit longer than I expected, but I have finally
finished my BIP 21 URI parser. I am attaching several patches for your
review. If these look good, I will submit a pull request.

The first patch, 0001-Implement-a-bitcoin-URI-parser, adds the parser
to libwallet. The other two patches tighten up the base58 decoding in
libbitcoin.

I have given my best effort towards following the libbitcoin style. My
background is in writing embedded C code for tiny micro-controllers,
but I have tried to leave those habits behind and code in an idiomatic
C++11 manner. Hopefully the code is clean enough for your standards.

-William
From edd0420c81991a7984adb317e46d0b9f11705b0a Mon Sep 17 00:00:00 2001
From: William Swanson <swansontec@???>
Date: Tue, 11 Mar 2014 00:52:16 -0700
Subject: [PATCH] Implement a bitcoin URI parser

This parser follows the BIP 21 standard as closely as possible.
---
 AUTHORS                    |   3 +
 examples/.gitignore        |   2 +
 examples/Makefile          |   9 +-
 examples/uri.cpp           | 105 ++++++++++++++++++++++
 include/wallet/Makefile.am |   3 +-
 include/wallet/uri.hpp     |  72 +++++++++++++++
 include/wallet/wallet.hpp  |   1 +
 src/Makefile.am            |   3 +-
 src/uri.cpp                | 212 +++++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 405 insertions(+), 5 deletions(-)
 create mode 100644 examples/.gitignore
 create mode 100644 examples/uri.cpp
 create mode 100644 include/wallet/uri.hpp
 create mode 100644 src/uri.cpp


diff --git a/AUTHORS b/AUTHORS
index e9fa421..c9ca425 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,6 +13,9 @@ Authors:
* Denis Roio (jaromil)
Hosting.

+* William Swanson
+ Development. Added URI code.
+
Thanks:

Kamil Domański (kdomanski)
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644
index 0000000..c91bfb6
--- /dev/null
+++ b/examples/.gitignore
@@ -0,0 +1,2 @@
+determ
+uri
diff --git a/examples/Makefile b/examples/Makefile
index 17c56de..2dcd229 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -3,14 +3,17 @@ LIBS=$(shell pkg-config --libs libbitcoin libwallet)

default: all

-determ.o: determ.cpp
+.cpp.o:
     $(CXX) -o $@ -c $< $(CXXFLAGS)


 determ: determ.o
     $(CXX) -o $@ $< $(LIBS)


-all: determ
+uri: uri.o
+    $(CXX) -o $@ $< $(LIBS)
+
+all: determ uri


 clean:
-    rm -f determ
+    rm -f determ uri
     rm -f *.o
diff --git a/examples/uri.cpp b/examples/uri.cpp
new file mode 100644
index 0000000..e5d881f
--- /dev/null
+++ b/examples/uri.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2011-2013 libwallet developers (see AUTHORS)
+ *
+ * This file is part of libwallet.
+ *
+ * libwallet is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License with
+ * additional permissions to the one published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. For more information see LICENSE.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+/*
+  Demonstration of URI utilities.
+*/
+#include <bitcoin/bitcoin.hpp>
+#include <wallet/wallet.hpp>
+#include <iostream>
+
+void test_uri_parse(std::string uri)
+{
+    std::cout << "parse URI: \"" << uri << "\"" << std::endl;
+    class parse_handler: public libwallet::uri_parse_handler
+    {
+        virtual void got_address(std::string address)
+        {
+            std::cout << "    got address: \"" << address << "\"" << std::endl;
+        }
+        virtual void got_param(std::string key, std::string value)
+        {
+            std::cout << "    got parameter: \"" << key << "\" = \"" <<
+                value << "\"" << std::endl;
+        }
+    } handler;
+    if (libwallet::uri_parse(uri, handler))
+        std::cout << "    ok" << std::endl;
+    else
+        std::cout << "    error" << std::endl;
+}
+
+void test_uri_decode(std::string uri)
+{
+    libwallet::decoded_uri out = libwallet::uri_decode(uri);
+    std::cout << "decode URI: \"" << uri << "\"" << std::endl;
+    if (!out.valid)
+        std::cout << "    invalid" << std::endl;
+    if (out.has_address)
+        std::cout << "    address: " << out.address.encoded() << std::endl;
+    if (out.has_amount)
+        std::cout << "    amount: " << out.amount << std::endl;
+    if (out.has_label)
+        std::cout << "    label: \"" << out.label << "\"" << std::endl;
+    if (out.has_message)
+        std::cout << "    message: \"" << out.message << "\"" << std::endl;
+    if (out.has_r)
+        std::cout << "    r: \"" << out.r << "\"" << std::endl;
+}
+
+int main()
+{
+    test_uri_parse("bitcoin:113Pfw4sFqN1T5kXUnKbqZHMJHN9oyjtgD?label=test");
+    test_uri_parse("bitcoin:");
+    test_uri_parse("bitcorn:");
+    test_uri_parse("BITCOIN:?");
+    test_uri_parse("Bitcoin:?&");
+    test_uri_parse("bitcOin:&");
+    test_uri_parse("bitcoin:?x=y");
+    test_uri_parse("bitcoin:?x=");
+    test_uri_parse("bitcoin:?=y");
+    test_uri_parse("bitcoin:?=");
+    test_uri_parse("bitcoin:?x");
+    test_uri_parse("bitcoin:19z88");
+    test_uri_parse("bitcoin:19l88");
+    test_uri_parse("bitcoin:19z88?x=http://www.example.com?purchase%3Dshoes");
+    test_uri_parse("bitcoin:19z88?name=%E3%83%95"); // UTF-8
+    test_uri_parse("bitcoin:19z88?name=%3");
+    test_uri_parse("bitcoin:19z88?name=%3G");
+    test_uri_parse("bitcoin:19z88?name=%3f");
+    test_uri_parse("bitcoin:%31");
+
+    std::cout << "================================" << std::endl;
+
+    test_uri_decode("bitcoin:113Pfw4sFqN1T5kXUnKbqZHMJHN9oyjtgD");
+    test_uri_decode("bitcoin:19z88");
+    test_uri_decode("bitcoin:?=");
+    test_uri_decode("bitcoin:?amount=4.2");
+    test_uri_decode("bitcoin:?amount=.");
+    test_uri_decode("bitcoin:?amount=4.2.4");
+    test_uri_decode("bitcoin:?amount=foo");
+    test_uri_decode("bitcoin:?label=Bob");
+    test_uri_decode("bitcoin:?message=Hi%20Alice");
+    test_uri_decode("bitcoin:?r=http://www.example.com?purchase%3Dshoes");
+    test_uri_decode("bitcoin:?foo=ignore");
+    test_uri_decode("bitcoin:?req-foo=die");
+
+    return 0;
+}
+
diff --git a/include/wallet/Makefile.am b/include/wallet/Makefile.am
index 748eed4..94eca86 100644
--- a/include/wallet/Makefile.am
+++ b/include/wallet/Makefile.am
@@ -4,5 +4,6 @@ wallet_include_HEADERS = \
     key_formats.hpp \
     transaction.hpp \
     deterministic_wallet.hpp \
-    mnemonic.hpp
+    mnemonic.hpp \
+    uri.hpp


diff --git a/include/wallet/uri.hpp b/include/wallet/uri.hpp
new file mode 100644
index 0000000..959d02b
--- /dev/null
+++ b/include/wallet/uri.hpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2011-2013 libwallet developers (see AUTHORS)
+ *
+ * This file is part of libwallet.
+ *
+ * libwallet is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License with
+ * additional permissions to the one published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. For more information see LICENSE.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBWALLET_URI_HPP
+#define LIBWALLET_URI_HPP
+
+#include <bitcoin/address.hpp>
+
+namespace libwallet {
+
+struct uri_parse_handler {
+    virtual void got_address(std::string address) = 0;
+    virtual void got_param(std::string key, std::string value) = 0;
+};
+
+bool uri_parse(const std::string& uri, uri_parse_handler& handler);
+bool uri_validate(const std::string& uri);
+
+/**
+ * A decoded bitcoin URI corresponding to BIP 21 and BIP 72.
+ * All string members are UTF-8.
+ */
+struct decoded_uri
+{
+    bool valid;
+    bool has_address;
+    bool has_amount;
+    bool has_label;
+    bool has_message;
+    bool has_r;
+
+    libbitcoin::payment_address address;
+    uint64_t amount;
+    std::string label;
+    std::string message;
+    std::string r;
+
+    decoded_uri()
+      : valid(true), has_address(false), has_amount(false),
+        has_label(false), has_message(false), has_r(false)
+    {}
+};
+
+decoded_uri uri_decode(const std::string& uri);
+
+/**
+ * Parses a bitcoin amount string.
+ * @return string value, in satoshis, or -1 for failure.
+ */
+uint64_t parse_amount(const std::string& amount);
+
+} // libwallet
+
+#endif
+
diff --git a/include/wallet/wallet.hpp b/include/wallet/wallet.hpp
index ba88126..5829532 100644
--- a/include/wallet/wallet.hpp
+++ b/include/wallet/wallet.hpp
@@ -25,6 +25,7 @@
 #include <wallet/mnemonic.hpp>
 #include <wallet/key_formats.hpp>
 #include <wallet/transaction.hpp>
+#include <wallet/uri.hpp>


#endif

diff --git a/src/Makefile.am b/src/Makefile.am
index 835513c..2225a9c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -6,7 +6,8 @@ libwallet_la_SOURCES = \
     deterministic_wallet.cpp \
     mnemonic.cpp \
     transaction.cpp \
-    key_formats.cpp
+    key_formats.cpp \
+    uri.cpp


libwallet_la_LIBADD = $(libbitcoin_LIBS)

diff --git a/src/uri.cpp b/src/uri.cpp
new file mode 100644
index 0000000..3abce6b
--- /dev/null
+++ b/src/uri.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2011-2013 libwallet developers (see AUTHORS)
+ *
+ * This file is part of libwallet.
+ *
+ * libwallet is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License with
+ * additional permissions to the one published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. For more information see LICENSE.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <wallet/uri.hpp>
+#include <bitcoin/utility/base58.hpp>
+#include <sstream>
+#include <stdlib.h>
+
+namespace libwallet {
+
+static bool is_digit(char c)
+{
+    return '0' <= c && c <= '9';
+}
+static bool is_hex(char c)
+{
+    return is_digit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f');
+}
+static bool is_qchar(char c)
+{
+    return
+        ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || is_digit(c) ||
+        '-' == c || '.' == c || '_' == c || '~' == c || // unreserved
+        '!' == c || '$' == c || '\'' == c || '(' == c || ')' == c ||
+        '*' == c || '+' == c || ',' == c || ';' == c || // sub-delims
+        ':' == c || '@' == c || // pchar
+        '/' == c || '?' == c;   // query
+}
+
+static unsigned from_hex(char c)
+{
+    return
+        'A' <= c && c <= 'F' ? 10 + c - 'A' :
+        'a' <= c && c <= 'f' ? 10 + c - 'a' :
+        c - '0';
+}
+
+/**
+ * Unescapes a URI-encoded string while advancing the iterator.
+ * @param i set to one-past the last-read character on return.
+ */
+typedef std::string::const_iterator sci;
+static std::string unescape(sci& i, sci end, bool (*is_valid)(char))
+{
+    auto j = i;
+    size_t count = 0;
+    while (end != i && (is_valid(*i) ||
+        ('%' == *i && 2 < end - i && is_hex(i[1]) && is_hex(i[2]))))
+    {
+        ++count;
+        i += ('%' == *i ? 3 : 1);
+    }
+    std::string out;
+    out.reserve(count);
+    while (j != i)
+    {
+        out.push_back('%' == *j ? from_hex(j[1]) << 4 | from_hex(j[2]) : *j);
+        j += ('%' == *j ? 3 : 1);
+    }
+    return out;
+}
+
+bool uri_parse(const std::string& uri, uri_parse_handler& handler)
+{
+    auto i = uri.begin();
+
+    // URI scheme:
+    const char* lower = "bitcoin:";
+    const char* upper = "BITCOIN:";
+    while (*lower)
+    {
+        if (uri.end() == i || (*lower != *i && *upper != *i))
+            return false;
+        ++lower; ++upper; ++i;
+    }
+
+    // Payment address:
+    std::string address = unescape(i, uri.end(), libbitcoin::is_base58);
+    if (uri.end() != i && '?' != *i)
+        return false;
+    if (!address.empty())
+        handler.got_address(address);
+
+    // Parameters:
+    while (uri.end() != i)
+    {
+        ++i; // Consume '?' or '&'
+        std::string key = unescape(i, uri.end(), is_qchar);
+        std::string value;
+        if (uri.end() != i && '=' == *i)
+        {
+            ++i; // Consume '='
+            if (key.empty())
+                return false;
+            value = unescape(i, uri.end(), is_qchar);
+        }
+        if (uri.end() != i && '&' != *i)
+            return false;
+        if (!key.empty())
+            handler.got_param(key, value);
+    }
+    return true;
+}
+
+bool uri_validate(const std::string& uri)
+{
+    class parse_handler: public uri_parse_handler
+    {
+        virtual void got_address(std::string address)
+        {
+            (void)address;
+        }
+        virtual void got_param(std::string key, std::string value)
+        {
+            (void)key;
+            (void)value;
+        }
+    } handler;
+    return uri_parse(uri, handler);
+}
+
+decoded_uri uri_decode(const std::string& uri)
+{
+    class parse_handler: public uri_parse_handler
+    {
+    public:
+        decoded_uri wip_;
+        virtual void got_address(std::string address)
+        {
+            if (wip_.address.set_encoded(address))
+                wip_.has_address = true;
+            else
+                wip_.valid = false;
+        }
+        virtual void got_param(std::string key, std::string value)
+        {
+            if ("amount" == key)
+            {
+                wip_.amount = parse_amount(value);
+                if (static_cast<uint64_t>(-1) != wip_.amount)
+                    wip_.has_amount = true;
+                else
+                    wip_.valid = false;
+            }
+            else if ("label" == key)
+            {
+                wip_.label = value;
+                wip_.has_label = true;
+            }
+            else if ("message" == key)
+            {
+                wip_.message = value;
+                wip_.has_message = true;
+            }
+            else if ("r" == key)
+            {
+                wip_.r = value;
+                wip_.has_r = true;
+            }
+            else if (!key.compare(0, 4, "req-"))
+            {
+                wip_.valid = false;
+            }
+        }
+    } handler;
+    if (!uri_parse(uri, handler))
+        handler.wip_.valid = false;
+    return handler.wip_;
+}
+
+/**
+ * Validates an amount string according to the BIP 21 grammar.
+ */
+static bool check_amount(const std::string& amount)
+{
+    auto i = amount.begin();
+    while (amount.end() != i && is_digit(*i))
+        ++i;
+    if (amount.end() != i && '.' == *i)
+    {
+        ++i;
+        while (amount.end() != i && is_digit(*i))
+            ++i;
+    }
+    return amount.end() == i;
+}
+
+uint64_t parse_amount(const std::string& amount)
+{
+    if (!check_amount(amount))
+        return static_cast<uint64_t>(-1);
+    // This code might have numerical problems:
+    return static_cast<uint64_t>(100000000*strtod(amount.c_str(), nullptr));
+}
+
+} // namespace libwallet
-- 
1.9.0


From 1973ed73de5684ecc078a674c6170bc3ca9df77e Mon Sep 17 00:00:00 2001
From: William Swanson <swansontec@???>
Date: Wed, 5 Mar 2014 16:38:59 -0800
Subject: [PATCH 1/2] Detect wrongly-encoded bitcoin addresses

---
 include/bitcoin/utility/base58.hpp |  3 +++
 src/address.cpp                    |  2 ++
 src/utility/base58.cpp             | 13 ++++++++++++-
 3 files changed, 17 insertions(+), 1 deletion(-)


diff --git a/include/bitcoin/utility/base58.hpp b/include/bitcoin/utility/base58.hpp
index 63e67c9..75ac9cd 100644
--- a/include/bitcoin/utility/base58.hpp
+++ b/include/bitcoin/utility/base58.hpp
@@ -24,6 +24,9 @@

namespace libbitcoin {

+bool is_base58(char c);
+bool is_base58(std::string const& text);
+
std::string encode_base58(const data_chunk& unencoded_data);

data_chunk decode_base58(std::string encoded_data);
diff --git a/src/address.cpp b/src/address.cpp
index f71607c..ab52049 100644
--- a/src/address.cpp
+++ b/src/address.cpp
@@ -59,6 +59,8 @@ const short_hash& payment_address::hash() const

 bool payment_address::set_encoded(const std::string& encoded_address)
 {
+    if (!is_base58(encoded_address))
+        return false;
     const data_chunk decoded_address = decode_base58(encoded_address);
     // version + 20 bytes short hash + 4 bytes checksum
     if (decoded_address.size() != 25)
diff --git a/src/utility/base58.cpp b/src/utility/base58.cpp
index c587e52..f31ae63 100644
--- a/src/utility/base58.cpp
+++ b/src/utility/base58.cpp
@@ -26,7 +26,18 @@ namespace libbitcoin {


// Thanks for all the wonderful bitcoin hackers

-const char* base58_chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+const char base58_chars[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+
+bool is_base58(char c)
+{
+    // This works because the base58 characters happen to be in sorted order
+    return std::binary_search(base58_chars, std::end(base58_chars) - 1, c);
+}
+bool is_base58(std::string const& text)
+{
+    return std::all_of(text.begin(), text.end(),
+        [](char c) { return is_base58(c); });
+}


 std::string encode_base58(const data_chunk& unencoded_data)
 {                                                                                
-- 
1.9.0


From 10d3c7e76bd3d9f9eae577f0acb4062f0c0ac7fe Mon Sep 17 00:00:00 2001
From: William Swanson <swansontec@???>
Date: Thu, 6 Mar 2014 16:31:38 -0800
Subject: [PATCH 2/2] Optimize the base58 decoder

The existing version was not using a binary search,
and was potentially allocating memory on each iteration.
---
src/utility/base58.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/utility/base58.cpp b/src/utility/base58.cpp
index f31ae63..a089fb9 100644
--- a/src/utility/base58.cpp
+++ b/src/utility/base58.cpp
@@ -87,8 +87,9 @@ data_chunk decode_base58(std::string encoded_data)
     for (const uint8_t current_char: encoded_data)
     {                                                                            
         bn *= 58;
-        bn += std::string(base58_chars).find(current_char);
-    }                                                                            
+        bn += std::lower_bound(base58_chars, std::end(base58_chars) - 1,
+            current_char) - base58_chars;
+    }


     // Get bignum as little endian data                                          
     data_chunk temp_data = bn.data();       
-- 
1.9.0