🧾

E-PASA

Summary

A backwards compatible addressing scheme that enables an infinite address-space within account-based blockchains that use ordinal account numbers (such as PascalCoin). The usage of these extension addresses can transform a bounded finite address space into an unbounded infinite address space.

Motivation

PascalCoin currently allows users to send/receive operations between accounts using their account numbers (PASA). 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 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.

This PIP provides one such scheme. Additionally, this scheme can also be immediately employed at the presentation-layer to greatly simplify exchange integrations and payload-based payments.

Specification

Layer-2 addresses will be herein referred to as Extended PASA, or E-PASA for short.

An E-PASA has the following unique characteristics:

  • Each E-PASA is a unique identifier.
  • An E-PASA is encoded into the raw network payload of operations.
  • There is a 1-1 mapping between E-PASA and their raw payload form.
    • They are deterministically dehydrated to into raw payloads.
    • They are deterministically hydrated from raw payloads (note: requires V5 protocol).
  • No two fully check-summed E-PASA's can refer to the same logical address.

Extended PASA format (E-PASA)

An Extended PASA is defined by the below EBNF grammar:


    EPASA              = PASA, [ ExtendedAddress ], [ ':', ExtendedChecksum ] ;
    PASA               = ( AccountName | AccountNumber ) ;
    AccountName        = Pascal64String ;
    AccountNumber      = Integer, "-", Checksum ;
    Checksum           = Digit, Digit ;
    ExtendedChecksum   = HexByte, HexByte ;
    ExtendedAddress    = ( PublicPayload | ReceiverEncPayload | SenderEncPayload | PasswordEncPayload ) ;
    PublicPayload      = "[", [ Payload ], "]" ; 
    ReceiverEncPayload = "(", [ Payload ], ")" ;
    SenderEncPayload   = "<", [ Payload ], ">" ;
    PasswordEncPayload = "{", [ Payload ], ":", [ Password ], "}" ;
    Payload            = ( """, PascalAsciiString, """ | "0", "x", HexString | Base58String ) ;
    Password           = PascalAsciiString
    PascalAsciiString  = PascalAsciiChar, { PascalAsciiChar } ;
    PascalAsciiChar    = (" " | "!" | 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     = Pascal64StartChar, { Pascal64Char } ;
    Pascal64Char       = (Digit | Pascal64StartChar)
    Pascal64StartChar  = ( "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, "<" | 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 *)
    Integer            = NaturalDigit, { Digit } ;
    Digit              = ( "0" | NaturalDigit ) ;
    NaturalDigit       = ( "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ) ;
    EscapeChar         = "\" ;

NOTES:

  • Text payload and passwords are restricted to ANSI charset subset range 32..126
  • The following characters are escaped in Pascal64 encoding: ( ( ) { } [ ] : " < >
  • The following characters are escaped in PascalAscii encoding: " ( ) : < > [ \ ] { }
  • 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)
Checksum This is the standard layer-1 address checksum
ExtendedAddress The optional extra text that forms part of layer-2 address (payload specification)
PublicPayload A payload which is not encrypted and publicly visible
ReceiverEncPayload A payload which is ECIES encrypted using receivers public key (the PASA portion specifies receiver)
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
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 PascalAsciiString (chars 32..126)
Pascal64String An ANSI string involving a limited subset used for account names (cannot start with a digit)
PascalAsciiString An ANSI string involvolving subset characters 32..126
Base58String A Base58-encoded string. This is used for specifying public keys, and hashes of public keys
HexString A hexadecimal-encoded string prefixed with a 0x. Every byte specified by two hexdigits, lower-case

Validation Rules

AccountNumber Checksum

Layer-1 account checkum must be the following number:


  Checksum = ((AccountNumber*101) MOD 89) + 10

ℹ️ AccountNumber above denotes the integer portion of the string.
⚠️ Whilst the E-PASA grammar allows optional PASA checksum, this is purely for convenience. Implementations are expected to automatically fill-in the checksum if not specified in the input.

Pascal64String

These strings are used to denote an account names and conform to the following rules:

  1. By definition, they must not start with a digit.

Account Name Validation

Account names in PascalCoin must be 3..64 characters in length. This validation rule is applied at the processing-tier level and not enforced by the EPASA grammar where a Pascal64 string is unbounded in size.

New Account Validation

EPASA is used to encode addresses for new accounts. An E-PASA of the form @[1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2] is used to denote a "Pay to Key" style transaction where payments are sent to a "new account" containing the key encoded in the payload.

Extended Checksum

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:


    PayloadChecksum = ToHexStringLE ( CastToUINT16( MurMur3( ToAsciiBytes ( PASA ++ ExtendedAddress ) ) MOD 65536 ) )

    where
       ToAsciiBytes    = converts ASCII string argument into raw byte form, character by character (no endianness concerns here)                
       MurMur3         = performs 32bit MurMur3 hash of the byte array argument                
       CastToUINT16    = casts the integer argument into to a 16bit unsigned integer (should never overflow due to modulo 65536)
       ToHexStringLE   = converts the 16bit unsigned integer argument into 4 hexadecimal characters in little-endian

⚠️ Whilst the E-PASA grammar allows optional PASA checksum, this is purely for convenience. Implementations are expected to automatically fill-in the checksum if not specified in the input.

Payload Lengths

The following validation rules must be applied to Payload lengths:

Payload Type Encryption Mode Byte-form Length E-PASA string-form length
ASCII None (Public) 255 255
ASCII ECIES 144 144
ASCII AES 223 223
Hexadecimal None (Public) 255 510+2
Hexadecimal ECIES 144 288+2
Hexadecimal AES 223 446+2
Base58 None (Public) 255 348
Base58 ECIES 144 196
Base58 AES 223 304
ℹ️ NOTE: +2 accounts for "0x" prefix for hexadecimal strings

Payload Type

In order for a recipient of an operation to automatically and deterministically decode an E-PASA from a raw network payload, the PascalCoin protocol needs additional data to describe the Payload.

This PIP proposes pre-fixing all Payloads in operations with a single-byte value called the PayloadType. The PayloadType describes the encryption and encoding of the Payload.

PayloadType will allow E-PASA's to be deterministically decoded by the recipient and prevent ambiguous decoding. This capability is fundamental for using E-PASA as an address-space since uniqueness requires a 1-1 mapping. With this feature, Layer-2 apps can safely use E-PASA as Layer-2 addresses.

Payload Type Specification:


	[Flags]
	public enum PayloadType {

		/// <summary>
		/// Payload encryption and encoding method not specified.
		/// </summary>
		NonDeterministic = 0b00000000,

		/// <summary>
		/// Unencrypted, public payload.
		/// </summary>
		Public = 0b00000001,

		/// <summary>
		/// ECIES encrypted using recipient accounts public key.
		/// </summary>		
		RecipientKeyEncrypted = 0b00000010,

		/// <summary>
		/// ECIES encrypted using sender accounts public key.
		/// </summary>
		SenderKeyEncrypted = 0b00000100,

		/// <summary>
		/// AES encrypted using pwd param
		/// </summary>
		PasswordEncrypted = 0b00001000,

		/// <summary>
		/// Payload data encoded in ASCII
		/// </summary>
		AsciiFormatted = 0b00010000,

		/// <summary>
		/// Payload data encoded in HEX
		/// </summary>
		HexFormatted = 0b00100000,

		/// <summary>
		/// Payload data encoded in Base58
		/// </summary>
		Base58Formatted = 0b01000000,

		/// <summary>
		/// E-PASA addressed by account name (not number).
		/// </summary>
		AddressedByName = 0b10000000,

	}

The values are interpreted as follows:

Value Interpretation
00000000 Non-deterministic (requires manual decoding by receiver and not an E-PASA)
00000001 Public payload (E-PASA shows empty payload)
00000010 ECIES-Sender encrypted payload (E-PASA shows empty payload)
00000100 ECIES-Receiver encrypted payload (E-PASA shows empty payload)
00001000 AES encrypted payload (E-PASA shows empty payload)
00010001 Public payload in ASCII encoding
00010010 ECIES-Sender encrypted payload in ASCII encoding
00010100 ECIES-Receiver encrypted payload in ASCII encoding
00011000 AES encrypted payload in ASCII encoding
00100001 Public payload in hexadecimal encoding
00100010 ECIES-Sender encrypted payload in hexadecimal encoding
00100100 ECIES-Receiver encrypted payload in hexadecimal encoding
00101000 AES encrypted payload in hexadecimal encoding
01000001 Public payload in Base58 encoding
01000010 ECIES-Sender encrypted payload in Base58 encoding
01000100 ECIES-Receiver encrypted payload in Base58 encoding
01001000 AES encrypted payload in Base58 encoding
10000000 Non-deterministic (E-PASA shows Account Name)
10000001 Public payload (E-PASA shows Account Name and empty payload)
10000010 ECIES-Sender encrypted payload (E-PASA shows AccountName and empty payload)
10000100 ECIES-Receiver encrypted payload (E-PASA shows AccountName and empty payload)
10001000 AES encrypted payload (E-PASA uses AccountName and empty payload)
10010001 Public payload in ASCII encoding (E-PASA shows Account Name)
10010010 ECIES-Sender encrypted payload in ASCII encoding (E-PASA shows Account Name)
10010100 ECIES-Receiver encrypted payload in ASCII encoding (E-PASA shows Account Name)
10011000 AES encrypted payload in ASCII encoding (E-PASA shows AccountName)
10100001 Public payload in hexadecimal encoding (E-PASA shows AccountName)
10100010 ECIES-Sender encrypted payload in hexadecimal encoding (E-PASA shows AccountName)
10100100 ECIES-Receiver encrypted payload in hexadecimal encoding (E-PASA shows AccountName)
10101000 AES encrypted payload in hexadecimal encoding (E-PASA shows AccountName)
11000001 Public payload in Base58 encoding (E-PASA shows AccountName)
11000010 ECIES-Sender encrypted payload in Base58 encoding (E-PASA shows AccountName)
11000100 ECIES-Receiver encrypted payload in Base58 encoding (E-PASA shows AccountName)
11001000 AES encrypted payload in Base58 encoding (E-PASA shows AccountName)

E-PASA Examples

The below cases are only example E-PASA without valid checksums.

TODO: replace with real E-Pasa after implementation QA.

Base Cases

E-PASA Description
123456-77 Account 123456-77 (backwards compatible with current addresses)
pascalcoin-foundation Account with name 'pascalcoin-foundation' no payload
my-favorite-exchange("[email protected]") An account called "my-favorite-exchange" with a recipient-encrypted payload (e.g. an exchange deposit address where only exchange can see payload, used as a user ID)

With ASCII payloads

E-PASA Description
123456-77 Account 123456-77 (backwards compatible)
123456-77["Hello World!"] With public ASCII payload "Hello World!" without checksum protection
123456-77["Hello World!"]:10cb Checksum protected
123456-77("Hello World!"):7ba2 ECIES encrypted using recipients public key
123456-77<"Hello World!">:b51f ECIES encrypted using senders public key
123456-77{"Hello World!":!43lp|-d|a%@#!} AES256 encrypted payload using password !43lp|-d|a%@#!

With Hexadecimal payloads

7-44[0x416c70686124] Account 77-44 with unencrypted (public) hexadecimal payload without protection
77-44[0x416c70686124]:10cb Checksum protected
77-44(0x416c70686124):7ba2 ECIES encrypted using recipients public key (and checksum protected)
77-44<0x416c70686124>:b51f ECIES encrypted using senders public key (and checksum protected)
77-44{0x416c70686124:!43lp-da%@#!} AES encrypted using password !43lp-da%@#!

With Base58 payloads

SEYstWetqTFn5Au4m4GFg7xJaNVN2] Pay To Key style transaction with Base58 encoded public key without checksum protection
@[1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2]:a054 Pay To Key style transaction with Base58 encoded public key with checksum protection
77-44[1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2] Account 77-44 with unencrypted (public) Base58 payload (bitcoin address) without checksum protection
77-44[1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2]:10cb Checksum protected
77-44(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2):7ba2 ECIES encrypted using recipients public key
77-44<1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2>:b51f ECIES encrypted using senders public key
77-44{1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2:!43lp-da%@#!} AES encrypted using password !43lp-da%@#!

Special Cases

-72["Message with all escaped chars \\\"\} here"] Public ANSI string Message with all escaped chars \"} here
999-72 Non-deterministic, raw network-level payload bytes ignored
999-72[] Empty Public payload, raw network-level payload bytes ignored
999-72() Empty ECIES payload, raw network-level payload bytes encrypted with receivers key but ignored
999-72<> Empty ECIES payload, raw network-level payload bytes encrypted with senders key but ignored
999-72{:} Empty AES payload, raw network-level payload bytes AES encryped but ignored
999-72{:pwd} Empty AES payload, raw network-level payload bytes AES encryped with password pwd but ignoredpwd but ignored
my-account-name Non-deterministic, raw network-level payload bytes ignored
my-account-name[] Empty public payload, raw network-level payload bytes ignored
my-account-name() Empty ECIES payload, raw network-level payload bytes encrypted with receivers key but ignored
my-account-name<> Empty ECIES payload, raw network-level payload bytes encrypted with senders key but ignored
my-account-name{:} Empty AES payload, raw network-level payload bytes encrypted with empty password but ignored
my-account-name{:pwd} Empty AES payload, raw network-level payload bytes encrypted with password pwd but ignored
999-72{"Hello":Funny\"Pwd} AES encrypted using escaped password Funny"Pwd
999-72{"Hello":\\\"\}} AES encrypted using escaped password \"}

Rationale

The design approach was to remain backwards compatible so that EPASA can replace "account" in existing JSON APIs. The caller need not specify Payloads anymore since the EPASA can contain the Payload.

Backwards Compatibility

This PIP is generally backwards compatible and does not require a hard-fork activation for using E-PASA as a convention.

For Layer-2 applications the ability for a receiver to auto-decode the E-PASA via PayloadType does require a hard-fork except although this does not prevent usage of E-PASA for other purposes.

Acknowledgements

  • Ugochukwu Mmaduekwe for assistance developing payload length validation rules
  • Benjamin Ansbach for regular feedback, assistance and insightful suggestions
  • UrbanCohort for elegancy-improving suggestion

Reference Implementation

The following regex parses an E-PASA:


((?<AccountNumber>(0|[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|!|@|#|\$|%|\^|&|\*|\\\(|\\\)|-|\+|\\\{|\\\}|\\\[|\\]|_|\\:|\\"|`|\||\\<|\\>|,|\.|\?|/|~)*))(?:(?<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


public override 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["Checksum"].Success ? match.Groups["Checksum"].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 = Pascal64Encoding.Unescape(accountName);
		epasa.Account = epasa.AccountChecksum = null;
	} else {
		// Account Number
		if (!uint.TryParse(accountNumber, out var accNo)) {
			errorCode = EPasaErrorCode.InvalidAccountNumber;
			return false;
		}
		epasa.Account = accNo;
		var actualAccountChecksum = AccountHelper.CalculateAccountChecksum(accNo);

		if (checksumDelim != null) {
			// validate account checksum
			if (!uint.TryParse(accountChecksum, out var accChecksum)) {
				errorCode = EPasaErrorCode.AccountChecksumInvalid;
				return false;
			}
			if (accChecksum != actualAccountChecksum) {
				errorCode = EPasaErrorCode.BadChecksum;
				return false;
			}
		}
		epasa.AccountChecksum = actualAccountChecksum;

	}

	// 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.PasswordEncrypted;
			break;
		default:
			throw new NotSupportedException($"Unrecognized start character '{payloadStartChar}'");

	}
 
	// Password
	if (epasa.PayloadType.HasFlag(PayloadType.PasswordEncrypted)) {
		if (payloadPasswordDelim == null) {
			errorCode = EPasaErrorCode.MissingPassword;
			return false;
		}
		epasa.Password = PascalAsciiEncoding.Unescape(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 = PascalAsciiEncoding.Unescape(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;
		} 
	}

	// Payload Lengths
	if (!EPasaHelper.IsValidPayloadLength(epasa.PayloadType, epasa.Payload)) {
		errorCode = EPasaErrorCode.PayloadTooLarge;
		return false;
	}

	// Extended Checksum
	var actualChecksum = EPasaHelper.ComputeExtendedChecksum(epasa.ToString(true));
	if (extendedChecksumDelim != null) {
		if (extendedChecksum != actualChecksum) {
			errorCode = EPasaErrorCode.BadExtendedChecksum;
			return false;
		}
	}
	epasa.ExtendedChecksum = actualChecksum;
	return true;
}

Some of the referenced methods can be found below:


public static byte CalculateAccountChecksum(uint accountNo) {
	var overflowSafeAccountNo = (ulong) accountNo;
	return (byte)(overflowSafeAccountNo * 101 % 89 + 10);
}

public static string ComputeExtendedChecksum(string text) {
	if (text == null)
		throw new ArgumentNullException(nameof(text));
	var checksum = (ushort) (Hashers.MURMUR3_32(Encoding.ASCII.GetBytes(text), ExtendedChecksumMurMur3Seed) % 65536);
	return EndianBitConverter.Little.GetBytes(checksum).ToHexString(true);
}


public const int MaxPublicAsciiContentLength = 255;
public const int MaxECIESAsciiContentLength = 144;
public const int MaxAESAsciiContentLength = 223;
public const int MaxPublicHexContentLength = 510 + 2;
public const int MaxECIESHexContentLength = 288 + 2;
public const int MaxAESHexContentLength = 446 + 2;
public const int MaxPublicBase58ContentLength = 348;
public const int MaxECIESBase58ContentLength = 196;
public const int MaxAESBase58ContentLength = 304;
public const uint ExtendedChecksumMurMur3Seed = 0;

public static bool IsValidPayloadLength(PayloadType payloadType, string payloadContent) {
	if (string.IsNullOrEmpty(payloadContent))
		return true;

	if (payloadType.HasFlag(PayloadType.Public)) {
		if (payloadType.HasFlag(PayloadType.AsciiFormatted)) {
			return PascalAsciiEncoding.Unescape(payloadContent).Length <= MaxPublicAsciiContentLength;
		}

		if (payloadType.HasFlag(PayloadType.HexFormatted)) {
			return payloadContent.Length <= MaxPublicHexContentLength;
		}

		if (payloadType.HasFlag(PayloadType.Base58Formatted)) {
			return payloadContent.Length <= MaxPublicBase58ContentLength;
		}

		// unknown encoding format
		return false;
	}

	if (payloadType.HasFlag(PayloadType.SenderKeyEncrypted) || payloadType.HasFlag(PayloadType.RecipientKeyEncrypted)) {
		if (payloadType.HasFlag(PayloadType.AsciiFormatted)) {
			return PascalAsciiEncoding.Unescape(payloadContent).Length <= MaxECIESAsciiContentLength;
		}

		if (payloadType.HasFlag(PayloadType.HexFormatted)) {
			return payloadContent.Length <= MaxECIESHexContentLength;
		}

		if (payloadType.HasFlag(PayloadType.Base58Formatted)) {
			return payloadContent.Length <= MaxECIESBase58ContentLength;
		}

		// unknown encoding format
		return false;
	}

	if (payloadType.HasFlag(PayloadType.PasswordEncrypted)) {
		if (payloadType.HasFlag(PayloadType.AsciiFormatted)) {
			return PascalAsciiEncoding.Unescape(payloadContent).Length <= MaxAESAsciiContentLength;
		}

		if (payloadType.HasFlag(PayloadType.HexFormatted)) {
			return payloadContent.Length <= MaxAESHexContentLength;
		}

		if (payloadType.HasFlag(PayloadType.Base58Formatted)) {
			return payloadContent.Length <= MaxAESBase58ContentLength;
		}

		// unknown encoding format
		return false;
	}

	// unknown encryption format
	return false;
}

Full source-code for the above is available here.

A recursive-descent implementation can be found here.

Links

  1. C# Regex Parser
  2. C# Recursive-Descent Parser
Built with our Notion CMS magic. Reach out to recreate and experience this website excellence!