/ src / addresses.py
addresses.py
  1  """
  2  Operations with addresses
  3  """
  4  # pylint: disable=inconsistent-return-statements
  5  
  6  import logging
  7  from binascii import hexlify, unhexlify
  8  from struct import pack, unpack
  9  
 10  try:
 11      from highlevelcrypto import double_sha512
 12  except ImportError:
 13      from highlevelcrypto import double_sha512
 14  
 15  
 16  logger = logging.getLogger('default')
 17  
 18  ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
 19  
 20  
 21  def encodeBase58(num):
 22      """Encode a number in Base X
 23  
 24      Args:
 25        num: The number to encode
 26        alphabet: The alphabet to use for encoding
 27      """
 28      if num < 0:
 29          return None
 30      if num == 0:
 31          return ALPHABET[0]
 32      arr = []
 33      base = len(ALPHABET)
 34      while num:
 35          num, rem = divmod(num, base)
 36          arr.append(ALPHABET[rem])
 37      arr.reverse()
 38      return ''.join(arr)
 39  
 40  
 41  def decodeBase58(string):
 42      """Decode a Base X encoded string into the number
 43  
 44      Args:
 45        string: The encoded string
 46        alphabet: The alphabet to use for encoding
 47      """
 48      base = len(ALPHABET)
 49      num = 0
 50  
 51      try:
 52          for char in string:
 53              num *= base
 54              num += ALPHABET.index(char)
 55      except ValueError:
 56          # character not found (like a space character or a 0)
 57          return 0
 58      return num
 59  
 60  
 61  class varintEncodeError(Exception):
 62      """Exception class for encoding varint"""
 63      pass
 64  
 65  
 66  class varintDecodeError(Exception):
 67      """Exception class for decoding varint data"""
 68      pass
 69  
 70  
 71  def encodeVarint(integer):
 72      """Convert integer into varint bytes"""
 73      if integer < 0:
 74          raise varintEncodeError('varint cannot be < 0')
 75      if integer < 253:
 76          return pack('>B', integer)
 77      if integer >= 253 and integer < 65536:
 78          return pack('>B', 253) + pack('>H', integer)
 79      if integer >= 65536 and integer < 4294967296:
 80          return pack('>B', 254) + pack('>I', integer)
 81      if integer >= 4294967296 and integer < 18446744073709551616:
 82          return pack('>B', 255) + pack('>Q', integer)
 83      if integer >= 18446744073709551616:
 84          raise varintEncodeError('varint cannot be >= 18446744073709551616')
 85  
 86  
 87  def decodeVarint(data):
 88      """
 89      Decodes an encoded varint to an integer and returns it.
 90      Per protocol v3, the encoded value must be encoded with
 91      the minimum amount of data possible or else it is malformed.
 92      Returns a tuple: (theEncodedValue, theSizeOfTheVarintInBytes)
 93      """
 94  
 95      if not data:
 96          return (0, 0)
 97      firstByte, = unpack('>B', data[0:1])
 98      if firstByte < 253:
 99          # encodes 0 to 252
100          return (firstByte, 1)  # the 1 is the length of the varint
101      if firstByte == 253:
102          # encodes 253 to 65535
103          if len(data) < 3:
104              raise varintDecodeError(
105                  'The first byte of this varint as an integer is %s'
106                  ' but the total length is only %s. It needs to be'
107                  ' at least 3.' % (firstByte, len(data)))
108          encodedValue, = unpack('>H', data[1:3])
109          if encodedValue < 253:
110              raise varintDecodeError(
111                  'This varint does not encode the value with the lowest'
112                  ' possible number of bytes.')
113          return (encodedValue, 3)
114      if firstByte == 254:
115          # encodes 65536 to 4294967295
116          if len(data) < 5:
117              raise varintDecodeError(
118                  'The first byte of this varint as an integer is %s'
119                  ' but the total length is only %s. It needs to be'
120                  ' at least 5.' % (firstByte, len(data)))
121          encodedValue, = unpack('>I', data[1:5])
122          if encodedValue < 65536:
123              raise varintDecodeError(
124                  'This varint does not encode the value with the lowest'
125                  ' possible number of bytes.')
126          return (encodedValue, 5)
127      if firstByte == 255:
128          # encodes 4294967296 to 18446744073709551615
129          if len(data) < 9:
130              raise varintDecodeError(
131                  'The first byte of this varint as an integer is %s'
132                  ' but the total length is only %s. It needs to be'
133                  ' at least 9.' % (firstByte, len(data)))
134          encodedValue, = unpack('>Q', data[1:9])
135          if encodedValue < 4294967296:
136              raise varintDecodeError(
137                  'This varint does not encode the value with the lowest'
138                  ' possible number of bytes.')
139          return (encodedValue, 9)
140  
141  
142  def encodeAddress(version, stream, ripe):
143      """Convert ripe to address"""
144      if version >= 2 and version < 4:
145          if len(ripe) != 20:
146              raise Exception(
147                  'Programming error in encodeAddress: The length of'
148                  ' a given ripe hash was not 20.'
149              )
150  
151          if ripe[:2] == b'\x00\x00':
152              ripe = ripe[2:]
153          elif ripe[:1] == b'\x00':
154              ripe = ripe[1:]
155      elif version == 4:
156          if len(ripe) != 20:
157              raise Exception(
158                  'Programming error in encodeAddress: The length of'
159                  ' a given ripe hash was not 20.')
160          ripe = ripe.lstrip(b'\x00')
161  
162      storedBinaryData = encodeVarint(version) + encodeVarint(stream) + ripe
163  
164      # Generate the checksum
165      checksum = double_sha512(storedBinaryData)[0:4]
166  
167      # FIXME: encodeBase58 should take binary data, to reduce conversions
168      # encodeBase58(storedBinaryData + checksum)
169      asInt = int(hexlify(storedBinaryData) + hexlify(checksum), 16)
170      # should it be str? If yes, it should be everywhere in the code
171      return 'BM-' + encodeBase58(asInt)
172  
173  
174  def decodeAddress(address):
175      """
176      returns (status, address version number, stream number,
177      data (almost certainly a ripe hash))
178      """
179      # pylint: disable=too-many-return-statements,too-many-statements
180      # pylint: disable=too-many-branches
181  
182      address = str(address).strip()
183  
184      if address[:3] == 'BM-':
185          integer = decodeBase58(address[3:])
186      else:
187          integer = decodeBase58(address)
188      if integer == 0:
189          status = 'invalidcharacters'
190          return status, 0, 0, ''
191      # after converting to hex, the string will be prepended
192      # with a 0x and appended with a L in python2
193      hexdata = hex(integer)[2:].rstrip('L')
194  
195      if len(hexdata) % 2 != 0:
196          hexdata = '0' + hexdata
197  
198      data = unhexlify(hexdata)
199      checksum = data[-4:]
200  
201      if checksum != double_sha512(data[:-4])[0:4]:
202          status = 'checksumfailed'
203          return status, 0, 0, ''
204  
205      try:
206          addressVersionNumber, bytesUsedByVersionNumber = decodeVarint(data[:9])
207      except varintDecodeError as e:
208          logger.error(str(e))
209          status = 'varintmalformed'
210          return status, 0, 0, ''
211  
212      if addressVersionNumber > 4:
213          logger.error('cannot decode address version numbers this high')
214          status = 'versiontoohigh'
215          return status, 0, 0, ''
216      elif addressVersionNumber == 0:
217          logger.error('cannot decode address version numbers of zero.')
218          status = 'versiontoohigh'
219          return status, 0, 0, ''
220  
221      try:
222          streamNumber, bytesUsedByStreamNumber = \
223              decodeVarint(data[bytesUsedByVersionNumber:])
224      except varintDecodeError as e:
225          logger.error(str(e))
226          status = 'varintmalformed'
227          return status, 0, 0, ''
228  
229      status = 'success'
230      if addressVersionNumber == 1:
231          return status, addressVersionNumber, streamNumber, data[-24:-4]
232      elif addressVersionNumber == 2 or addressVersionNumber == 3:
233          embeddedRipeData = \
234              data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4]
235          if len(embeddedRipeData) == 19:
236              return status, addressVersionNumber, streamNumber, \
237                  b'\x00' + embeddedRipeData
238          elif len(embeddedRipeData) == 20:
239              return status, addressVersionNumber, streamNumber, \
240                  embeddedRipeData
241          elif len(embeddedRipeData) == 18:
242              return status, addressVersionNumber, streamNumber, \
243                  b'\x00\x00' + embeddedRipeData
244          elif len(embeddedRipeData) < 18:
245              return 'ripetooshort', 0, 0, ''
246          elif len(embeddedRipeData) > 20:
247              return 'ripetoolong', 0, 0, ''
248          return 'otherproblem', 0, 0, ''
249      elif addressVersionNumber == 4:
250          embeddedRipeData = \
251              data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4]
252          if embeddedRipeData[0:1] == b'\x00':
253              # In order to enforce address non-malleability, encoded
254              # RIPE data must have NULL bytes removed from the front
255              return 'encodingproblem', 0, 0, ''
256          elif len(embeddedRipeData) > 20:
257              return 'ripetoolong', 0, 0, ''
258          elif len(embeddedRipeData) < 4:
259              return 'ripetooshort', 0, 0, ''
260          x00string = b'\x00' * (20 - len(embeddedRipeData))
261          return status, addressVersionNumber, streamNumber, \
262              x00string + embeddedRipeData
263  
264  
265  def addBMIfNotPresent(address):
266      """Prepend BM- to an address if it doesn't already have it"""
267      address = str(address).strip()
268      return address if address[:3] == 'BM-' else 'BM-' + address