Browse Source

PIP-0027: final amendments

Herman Schoenfeld 6 years ago
parent
commit
433beeb553
1 changed files with 242 additions and 21 deletions
  1. 242 21
      PIP/PIP-0027.md

+ 242 - 21
PIP/PIP-0027.md

@@ -11,7 +11,7 @@
 
 ## Summary
 
-This PIP proposes a backwards compatible addressing scheme that enables an infinite address-space within PascalCoin. The usage of these extension addresses can be employed within existing infrastructure such as wallets and exchanges now as well as future Layer-2 dapps.
+This PIP proposes a backwards compatible addressing scheme that enables an infinite address-space within PascalCoin. The usage of these extension addresses can be employed immediately by existing infrastructure such as wallets and exchanges and in future Layer-2 dapps.
 
 ## Motivation
 
@@ -19,7 +19,7 @@ PascalCoin currently allows users to send/receive operations between accounts us
 
 These account numbers are a limited (and commoditized) resource which form a finite address-space (note: this is fundamental to SafeBox design and it's infinite-scaling capability).
 
-PascalCoin will provide an infinite address-space (similar toother other crypto-currencies) via "decentralized custodial accounts" which are Layer-2 dapps governed via a Layer-2 Proof-of-Stake overlay network.
+PascalCoin will provide an infinite address-space (similar to other other crypto-currencies) via "decentralized custodial accounts" which are Layer-2 dapps governed via a Layer-2 Proof-of-Stake overlay network.
 
 Before rolling out this Layer-2 infrastructure, PascalCoin first needs to establish an addressing-scheme for this infinite address-space.
 
@@ -42,12 +42,12 @@ An E-PASA has the following unique characteristics:
 An Extended PASA is defined by the below EBNF grammar:
 
 ```
-    EPASA              = PASA, [ ExtendedAddress ], [ ':', EPASAChecksum ] ;
+    EPASA              = PASA, [ ExtendedAddress ], [ ':', ExtendedChecksum ] ;
     PASA               = ( AccountName | AccountNumber ) ;
     AccountName        = Pascal64String ;
     AccountNumber      = Integer, "-", Checksum ;
     Checksum           = Digit, Digit ;
-    EPASAChecksum      = HexByte, HexByte ;
+    ExtendedChecksum   = HexByte, HexByte ;
     ExtendedAddress    = ( PublicPayload | ReceiverEncPayload | SenderEncPayload | PasswordEncPayload ) ;
     PublicPayload      = "[", [ Payload ], "]" ; 
     ReceiverEncPayload = "(", [ Payload ], ")" ;
@@ -56,24 +56,31 @@ An Extended PASA is defined by the below EBNF grammar:
     Payload            = ( """, SafeAnsiString, """ | "0", "x", HexString | Base58String ) ;
     Password           = SafeAnsiString
     SafeAnsiString     = SafeAnsiChar, { SafeAnsiChar } ;
-    SafeAnsiChar       = (" " | "!" | EscapeChar """ | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "[" | EscapeChar "\" | "]" | "^" | "_" | "`" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "{" | "|" | EscapeChar "}" | "~") ;
+    SafeAnsiChar       = (" " | "!" | EscapeChar, """ | "#" | "$" | "%" | "&" | "'" | EscapeChar, "(" | EscapeChar, ")" | "*" | "+" | "," | "-" | "." | "/" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | EscapeChar, ":" | ";" | EscapeChar, "<" | "=" | EscapeChar, ">" | "?" | "@" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | EscapeChar, "[" | EscapeChar, "\" | EscapeChar, "]" | "^" | "_" | "`" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | EscapeChar, "{" | "|" | EscapeChar, "}" | "~") ;
     Pascal64String     = SafePascal64Char, { Pascal64Char } ;
     Pascal64Char       = (Digit | SafePascal64Char)
-    SafePascal64Char   = ( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "+", "{", "}", "[", "]", "_", ":", "`", "|", "<", ">", ",", ".", "?", "/", "~", ")" "-", "+", "{", "}", "[", "]", "_", ":", "`", "|", "<", ">", ",", ".", "?", "/", "~" ) ; 
+    SafePascal64Char   = ( "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | EscapeChar, "(" | EscapeChar, ")" | "-" | "+" | EscapeChar, "{" | EscapeChar, "}" | EscapeChar, "[" | EscapeChar, "]" | "_" | EscapeChar, ":" | "`" | "|" | EscapeChar, "<" | EscapeChar, ">" | "," | "." | "?" | "/" | "~" ) ; 
     HexString          = HexByte { HexByte } ;
     HexByte            = HexNibble, HexNibble ;
     HexNibble          = ( Digit | "a" | "b" | "c" | "d" | "e" | "f" ) ;       (* no uppercase hex allowed *)
     Base58String       = Base58Char, { Base58Char } ;
     Base58Char         = ( NaturalDigit | Base58UpperChar | Base58LowerChar ) ; 
-    Base58UpperChar    = ( "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ) ;     (* missing I, O *)
-    Base58LowerChar    = ( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" ) ; (* missing l *)
+    Base58UpperChar    = ( "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "J" | "K" | "L" | "M" | "N" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" ) ;  (* missing I, O *)
+    Base58LowerChar    = ( "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" ) ; (* missing l *)
     Integer            = NaturalDigit, { Digit } ;
     Digit              = ( "0" | NaturalDigit ) ;
     NaturalDigit       = ( "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ) ;
     EscapeChar         = "\" ;
-```
 
-| Rule               | Explanation                                                                                                   |
+**NOTES**: 
+ * Text payload and passwords are restricted to ANSI charset subset range 32..126
+ * The following characters are escaped in Pascal64 and SafeAnsi strings): **:**, **\\**, **"**, **[**, **]**, **(**, **), **<**, **>**,**{**, **}**
+ * Escape character is always: **\\**
+
+The above rules can be interpreted as follows:
+
+```
+| Rule               | Interpretation                                                                                                   |
 | -----------------: | :------------------------------------------------------------------------------------------------------------ |
 | EPASA              | This is a layer-2 address, fully backwards compatible as Layer-1 address                                      |
 | PASA               | This is the standard layer-1 address of the receiver account (account number or account name)                 |
@@ -84,7 +91,7 @@ An Extended PASA is defined by the below EBNF grammar:
 | SenderEncPayload   | A payload which is ECIES encrypted using the senders public key (only sender can decrypt EPASA)               |
 | PasswordEncPayload | A payload which is AES256 encrypted using the specified password                                              |
 | Payload            | The actual payload data, specified it an well-defined encoding                                                |
-| PayloadChecksum    | An UINT16 specified by two hexbytes (4 hexnibbles) that denotes a checksum of payload, used to ensure payload consistency (prevents typo/copy-paste errors)  |
+| ExtendedChecksum   | A checksum of all the preceding text in the E-PASA (necessary to prevent typo-errors)                         |
 | Password           | The password used in PasswordEndPayload. Must be specified as a SafeAnsiString (chars 32..126)                |
 | Pascal64String     | An ANSI string involving a limited subset used for account names (cannot start with a digit)                  |
 | SafeAnsiString     | An ANSI string involvolving subset characters 32..126                                                         |
@@ -92,11 +99,6 @@ An Extended PASA is defined by the below EBNF grammar:
 | HexString          | A hexadecimal-encoded string prefixed with a 0x. Every byte specified by two hexdigits, lower-case            |
 
 
-**NOTES**: 
- * Text payload and passwords are restricted to ANSI charset subset range 32..126
- * The following characters **\\**, **"**, and **}** must be escaped in ASCII payloads/AES passwords via preceding **\\**
- * Payload content is 
-
 #### Validation Rules
 
 #### AccountNumber Checksum
@@ -117,9 +119,9 @@ These strings are used to denote an account names and conform to the following r
 - By definition, they must **not** start with a digit. 
 - String length must between 3..64 inclusive.
 
-#### E-PASA Checksum
+#### Extended Checksum
 
-In order to avoid data entry errors, the EPASA is checksummed. In short, the E-PASA checksum is simply the 16-bit MurMur3 hash of all preceding text. 
+In order to avoid data entry errors, the EPASA is checksummed via an Extended Checksum. In short, the Extended Checksum is simply the 16-bit MurMur3 hash of all preceding text. 
 
 Formally, the EPASA checksum is defined as follows:
 
@@ -133,7 +135,7 @@ Formally, the EPASA checksum is defined as follows:
        ToHexStringLE   = converts the 16bit unsigned integer argument into 4 hexadecimal characters in little-endian
 ```
 
-**IMPORTANT**: Whilst the E-PASA grammar allows optional E-PASA checksums, this is purely for convenience. Implementations are expected to automatically fill-in the checksum if not specified in the input.
+**IMPORTANT**: Whilst the E-PASA grammar allows optional Extended Checksums, this is purely for convenience. Implementations are expected to automatically fill-in the checksum if not specified in the input.
 
 #### Payload Lengths
 
@@ -165,6 +167,61 @@ This capability is fundamental for using E-PASA as an *address-space* in Layer-2
 
 #### Payload Type Specification:
 
+```csharp
+	[Flags]
+	public enum PayloadType {
+
+		/// <summary>
+		/// Payload encoding method not specified.
+		/// </summary>
+		NonDeterministic = 0x00000000,
+
+		/// <summary>
+		/// Not encrypted public payload.
+		/// </summary>
+		Public = 0x00000001,
+
+		/// <summary>
+		/// Encrypted using recipient accounts public key.
+		/// </summary>		
+		RecipientKeyEncrypted = 0x00000010,
+
+		/// <summary>
+		/// Encrypted using sender accounts public key.
+		/// </summary>
+		SenderKeyEncrypted = 0x00000100,
+
+		/// <summary>
+		/// Encrypted data using pwd param
+		/// </summary>
+		AESEncrypted = 0x00001000,
+
+		/// <summary>
+		/// Payload data is ASCII
+		/// </summary>
+		AsciiFormatted = 0x0001000,
+
+		/// <summary>
+		/// Payload data is HEX
+		/// </summary>
+		HexFormatted = 0x0010000,
+
+		/// <summary>
+		/// Payload data is Base58
+		/// </summary>
+		Base58Formatted = 0x0100000,
+
+		/// <summary>
+		/// E-Pasa encoding uses account name
+		/// </summary>
+		AddressedByName = 0x1000000,
+
+	}
+```
+
+The values are interpreted as follows:
+
+
 | Value    | Interpretation                                                                  |
 | -------: | :------------------------------------------------------------------------------ |
 | 00000000 | Non-deterministic (requires manual decoding by receiver and **not** an E-PASA)  |
@@ -315,8 +372,172 @@ For Layer-2 applications the ability for a receiver to auto-decode the E-PASA vi
  
 ## Reference Implementation
 
-WIP
+The following regex parses an e-pasa:
+```
+((?<AccountNumber>[1-9]\d+)(?:(?<ChecksumDelim>-)(?<Checksum>\d{2}))?|(?<AccountName>(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|!|@|#|\$|%|\^|&|\*|\\\(|\\\)|-|\+|\\\{|\\\}|\\\[|\\]|_|\\:|`|\||\\<|\\>|,|\.|\?|/|~)(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9|!|@|#|\$|%|\^|&|\*|\\\(|\\\)|-|\+|\\\{|\\\}|\\\[|\\]|_|\\:|`|\||\\<|\\>|,|\.|\?|/|~){2,63}))(?:(?<PayloadStartChar>[\[\(<\{])(?<PayloadContent>"( |!|\\"|#|\$|%|&|'|\\\(|\\\)|\*|\+|,|-|\.|/|0|1|2|3|4|5|6|7|8|9|\\:|;|\\<|=|\\>|\?|@|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|\\\[|\\\\|\\]|\^|_|`|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|\\\{|\||\\\}|~)+"|0x(?:[0-9a-f]{2})+|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)?(?:(?<PayloadPasswordDelim>:){1}(?<PayloadPassword>( |!|\\"|#|\$|%|&|'|\\\(|\\\)|\*|\+|,|-|\.|/|0|1|2|3|4|5|6|7|8|9|\\:|;|\\<|=|\\>|\?|@|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|\\\[|\\\\|\\]|\^|_|`|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|\\\{|\||\\\}|~)+)?)?(?<PayloadEndChar>[]\)>\}]))?(?:(?<ExtendedChecksumDelim>:)(?<ExtendedChecksum>[0-9a-f]{2}[0-9a-f]{2}))?
+```
+
+After matching with the above regex, the named groups need to be extracted and validated as the below snippet shows
+
+```csharp
+
+	public bool TryParse(string epasaText, out EPasa epasa, out EPasaErrorCode errorCode) {
+		errorCode = EPasaErrorCode.Success;
+		epasa = new EPasa();
+
+		if (string.IsNullOrEmpty(epasaText)) {
+			errorCode = EPasaErrorCode.BadFormat;
+			return false;
+		}
+
+		var match = _epasaRegex.Match(epasaText);
+		var checksumDelim = match.Groups["ChecksumDelim"].Success ? match.Groups["ChecksumDelim"].Value : null;
+		var accountNumber = match.Groups["AccountNumber"].Success ? match.Groups["AccountNumber"].Value : null;
+		var accountChecksum = match.Groups["AccountChecksum"].Success ? match.Groups["AccountChecksum"].Value : null;
+		var accountName = match.Groups["AccountName"].Success ? match.Groups["AccountName"].Value : null;
+		var payloadStartChar = match.Groups["PayloadStartChar"].Success ? match.Groups["PayloadStartChar"].Value : null;
+		var payloadEndChar = match.Groups["PayloadEndChar"].Success ? match.Groups["PayloadEndChar"].Value : null;
+		var payloadContent = match.Groups["PayloadContent"].Success ? match.Groups["PayloadContent"].Value : null;
+		var payloadPasswordDelim = match.Groups["PayloadPasswordDelim"].Success ? match.Groups["PayloadPasswordDelim"].Value : null;
+		var payloadPassword = match.Groups["PayloadPassword"].Success ? match.Groups["PayloadPassword"].Value : null;
+		var extendedChecksumDelim = match.Groups["ExtendedChecksumDelim"].Success ? match.Groups["ExtendedChecksumDelim"].Value : null;
+		var extendedChecksum = match.Groups["ExtendedChecksum"].Success ? match.Groups["ExtendedChecksum"].Value : null;
+
+		// Check parsed completely
+		if (epasaText != match.Value) {
+			errorCode = EPasaErrorCode.BadFormat;
+			return false;
+		}
+
+		if (accountName != null) {
+			// Account Name
+			if (string.IsNullOrEmpty(accountName)) {
+				errorCode = EPasaErrorCode.BadFormat;
+				return false;
+			}
+			epasa.PayloadType = epasa.PayloadType | PayloadType.AddressedByName;
+			epasa.AccountName = accountName;
+		} else {
+			// Account Number
+			if (!uint.TryParse(accountNumber, out var accNo)) {
+				errorCode = EPasaErrorCode.AccountNumberTooLong;
+				return false;
+			}
+			epasa.PayloadType = epasa.PayloadType ^ PayloadType.AddressedByName;
+			epasa.Account = accNo;
+
+			if (checksumDelim != null) {
+				if (!uint.TryParse(accountChecksum, out var accChecksum)) {
+					errorCode = EPasaErrorCode.AccountChecksumInvalid;
+					return false;
+				}
+				if (!AccountHelper.IsValidAccountChecksum(epasa.Account.Value, accChecksum)) {
+					errorCode = EPasaErrorCode.BadChecksum;
+					return false;
+				}
+				epasa.AccountChecksum = accChecksum;
+			}
+		}
+
+		// Encryption type			
+		switch (payloadStartChar) {
+			case null:
+				break;
+			case "[":
+				if (payloadEndChar != "]") {
+					errorCode = EPasaErrorCode.MismatchedPayloadEncoding;
+					return false;
+				}
+				epasa.PayloadType = epasa.PayloadType | PayloadType.Public;
+				break;
+			case "(":
+				if (payloadEndChar != ")") {
+					errorCode = EPasaErrorCode.MismatchedPayloadEncoding;
+					return false;
+				}
+				epasa.PayloadType = epasa.PayloadType | PayloadType.RecipientKeyEncrypted;
+				break;
+			case "<":
+				if (payloadEndChar != ">") {
+					errorCode = EPasaErrorCode.MismatchedPayloadEncoding;
+					return false;
+				}
+				epasa.PayloadType = epasa.PayloadType | PayloadType.SenderKeyEncrypted;
+				break;
+			case "{":
+				if (payloadEndChar != "}") {
+					errorCode = EPasaErrorCode.MismatchedPayloadEncoding;
+					return false;
+				}
+				epasa.PayloadType = epasa.PayloadType | PayloadType.AESEncrypted;
+				break;
+			default:
+				throw new NotSupportedException($"Unrecognized start character '{payloadStartChar}'");
+
+		}
+ 
+		// Password
+		if (epasa.PayloadType.HasFlag(PayloadType.AESEncrypted)) {
+			if (payloadPasswordDelim == null) {
+				errorCode = EPasaErrorCode.MissingPassword;
+				return false;
+			}
+			epasa.Password = payloadPassword ?? "";
+		} else if (payloadPasswordDelim != null) {
+			errorCode = EPasaErrorCode.UnusedPassword;
+			return false;
+		}
+
+		// Payload 
+		if (payloadStartChar != null) {
+			if (payloadContent == null) {
+				epasa.Payload = string.Empty;
+			} else if (payloadContent.StartsWith("\"")) {
+				epasa.PayloadType = epasa.PayloadType | PayloadType.AsciiFormatted;
+				epasa.Payload = payloadContent.Trim('"');					
+			} else if (payloadContent.StartsWith("0x")) {
+				epasa.PayloadType = epasa.PayloadType | PayloadType.HexFormatted;
+				epasa.Payload = payloadContent.Substring(2);
+			} else  {
+				epasa.PayloadType = epasa.PayloadType | PayloadType.Base58Formatted;
+				epasa.Payload = payloadContent;
+			} 
+			epasa.Payload = payloadContent;
+		}
+
+		// Payload Lengths
+		if (!EPasaHelper.IsValidPayloadLength(epasa.PayloadType, epasa.Payload)) {
+			errorCode = EPasaErrorCode.PayloadTooLarge;
+			return false;
+		}
+
+		// Extended Checksum
+		if (extendedChecksumDelim != null) {
+			if (checksumDelim == null) {
+				errorCode = EPasaErrorCode.MissingAccountChecksum;
+				return false;
+			}
+			if (!EPasaHelper.IsValidExtendedChecksum(epasa.ToString(true), epasa.ExtendedChecksum)) {
+				errorCode = EPasaErrorCode.BadExtendedChecksum;
+				return false;
+			}
+			epasa.ExtendedChecksum = extendedChecksum;
+		}
+		return true;
+	}
+
+```
+
+Full source-code for the above is available [here][1]. 
+
+A recursive-descent implementation can be found [here][2].
+
 
 ## Links
 
-None
+1. [C# Regex Parser][1]
+2. [C# Recursive-Descent Parser][2]
+
+[1]: https://github.com/Sphere10/NPascalCoin/blob/master/src/NPascalCoin/Common/Parsing/RegexEPasaParser.cs
+[2]: https://github.com/Sphere10/NPascalCoin/blob/master/src/NPascalCoin/Common/Parsing/RecursiveDescentEPasaParser.cs
+