my ts rlp decoder seems to work

i feel like i would have more confidence in my code if i didn't test it
This commit is contained in:
2022-10-23 06:41:19 -06:00
parent 2254c82f77
commit 7751e71982
6 changed files with 12856 additions and 10 deletions
+99
View File
@@ -0,0 +1,99 @@
export {
decoded_data,
decode_result,
decode
}
type decoded_data
= Uint8Array
| Array<decoded_data>;
type decode_result
= {decoded_data : decoded_data,
remainder : Uint8Array};
function
decode(bytes: Uint8Array): decode_result {
// check the first byte
let first_byte: number = bytes[0];
let rest : Uint8Array = bytes.slice(1);
// if the first byte is between 0 and 127, that is the data
if
(first_byte <= 127) {
return dr(new Uint8Array([first_byte]), rest);
}
// if the first byte is between 128 and 183 = 128 + 55, it is a bytestring
// and the length is Byte - 128
else if
(first_byte <= 183) {
let payload_byte_length : number = first_byte - 128;
let payload : Uint8Array = rest.slice(0, payload_byte_length);
let rest2 : Uint8Array = rest.slice(payload_byte_length);
return dr(payload, rest2);
}
// if the first byte is between 184 = 183 + 1 and 191 = 183 + 8, it is a
// bytestring. the byte length of bytestring is FirstByte - 183. Then pull
// out the actual data
else if
(first_byte <= 191) {
let byte_length_of_byte_length : number = first_byte - 183;
let bytes_of_byte_length : Uint8Array = rest.slice(0, byte_length_of_byte_length);
let byte_length : number = bytes_to_number(bytes_of_byte_length);
let bytes : Uint8Array = rest.slice(byte_length_of_byte_length,
byte_length + byte_length_of_byte_length);
let rest2 : Uint8Array = rest.slice(byte_length + byte_length_of_byte_length);
return dr(bytes, rest2);
}
// If the first byte is between 192 and 247 = 192 + 55, it is a list. The
// byte length of the list-payload is FirstByte - 192. Then the list
// payload, which needs to be decoded on its own.
else if
(first_byte <= 247) {
let byte_length_of_list : number = first_byte - 192;
let list_payload : Uint8Array = rest.slice(0, byte_length_of_list);
let list : Array<decoded_data> = decode_list(list_payload);
let rest2 : Uint8Array = rest.slice(byte_length_of_list);
return dr(list, rest2);
}
// If the first byte is between 248 = 247 + 1 and 255 = 247 + 8, it is a
// list. The byte length of the byte length of the list-payload is
// FirstByte - 247. Then the byte length of the list. Then the list
// payload, which needs to be decoded on its own.
else {
let byte_length_of_byte_length : number = first_byte - 247;
let bytes_of_byte_length : Uint8Array = rest.slice(0, byte_length_of_byte_length);
let byte_length : number = bytes_to_number(bytes_of_byte_length);
let list_bytes : Uint8Array = rest.slice(byte_length_of_byte_length,
byte_length + byte_length_of_byte_length);
let list : Array<decoded_data> = decode_list(list_bytes);
let rest2 : Uint8Array = rest.slice(byte_length + byte_length_of_byte_length);
return dr(list, rest2);
}
}
function decode_list(bytes: Uint8Array): Array<decoded_data> {
let arr : Array<decoded_data> = [];
while (bytes.length > 0) {
// grab an item off the bytes
let {decoded_data, remainder} = decode(bytes);
// push it
arr.push(decoded_data);
// update bytes
bytes = remainder;
}
return arr;
}
// convert bytestring to number
function bytes_to_number(bytes: Uint8Array) {
let n : number = 0;
for (let b of bytes) {
n <<= 8;
n += b;
}
return n;
}
function dr(x : decoded_data, y : Uint8Array) {
return {decoded_data: x, remainder: y};
}
+7
View File
@@ -20,6 +20,13 @@
<h5>Failed case log:</h5> <h5>Failed case log:</h5>
<pre id="b64-assershins"></pre> <pre id="b64-assershins"></pre>
<h1>RLP Encode/Decode Tests</h1>
<p>Current case: <span id="rlp-current-case"></span></p>
<p>Total cases: <span id="rlp-total-cases"></span></p>
<button id="rlp-go">Go!</button>
<h5>Failed case log:</h5>
<pre id="rlp-assershins"></pre>
<script type="module" src="dist/test.js"></script> <script type="module" src="dist/test.js"></script>
</body> </body>
</html> </html>
+4
View File
@@ -1,3 +1,7 @@
import * as rlpcases from './rlpcases.js';
export const rlp = rlpcases.cases;
export const base58 = [ export const base58 = [
{encoded: "1", {encoded: "1",
decoded: new Uint8Array([0])}, decoded: new Uint8Array([0])},
File diff suppressed because it is too large Load Diff
+94 -7
View File
@@ -2,8 +2,52 @@ import * as cases from './cases.js';
import * as b64 from './jex_include/local-vanillae-0.1.0/dist/base64.js'; import * as b64 from './jex_include/local-vanillae-0.1.0/dist/base64.js';
import * as b58 from './jex_include/local-vanillae-0.1.0/dist/base58.js'; import * as b58 from './jex_include/local-vanillae-0.1.0/dist/base58.js';
import * as rlp from './jex_include/local-vanillae-0.1.0/dist/rlp.js';
function uint8arr_eq(a: Uint8Array, b: Uint8Array) {
function deepeq(dd1: rlp.decoded_data, dd2: rlp.decoded_data): boolean {
// if both are Uint8Arrays
if ((dd1 instanceof Uint8Array) && (dd2 instanceof Uint8Array)) {
return uint8arr_eq(dd1, dd2);
}
// if both are Arrays
else if ((dd1 instanceof Array) && (dd2 instanceof Array)) {
return arr_eq(dd1, dd2);
}
// otherwise
else {
return false;
}
}
function arr_eq(a: Array<rlp.decoded_data>, b: Array<rlp.decoded_data>): boolean {
// make sure they have the same length
if (a.length !== b.length) {
return false;
}
// they have the same length now
else {
let len : number = a.length;
// loop over the items
for (let i = 0;
i < len;
i++)
{
let a_elt : rlp.decoded_data = a[i];
let b_elt : rlp.decoded_data = b[i];
let eq : boolean = deepeq(a_elt, b_elt);
// if they are not equal, break and return false
if (!eq) {
return false;
}
}
// at the end, if we havent' proven ourselves wrong yet,
// the arrays are equal
return true;
}
}
function uint8arr_eq(a: Uint8Array, b: Uint8Array): boolean {
// first test if the length // first test if the length
if (a.length !== b.length) { if (a.length !== b.length) {
return false; return false;
@@ -108,15 +152,58 @@ function b58_tests(): void {
} }
} }
function rlp_tests(): void {
// @ts-ignore ts can't prove to itself that the element exists
let rlp_pre : HTMLElement = document.getElementById('rlp-assershins');
// @ts-ignore ts can't prove to itself that the element exists
let rlp_casen : HTMLElement = document.getElementById('rlp-current-case');
let case_n : number = 1;
for(let this_case of cases.rlp) {
// i love this type error
// can't set the inner html to a number
// but a string plus a number is totally cool
rlp_casen.innerHTML = '' + case_n;
case_n++;
let {encoded, decoded} = this_case;
//let my_encoded = rlp.encode(decoded);
let my_decoded = rlp.decode(encoded).decoded_data;
//let encodes_correctly = (encoded === my_encoded);
let decodes_correctly = deepeq(decoded, my_decoded);
//if (!encodes_correctly) {
// rlp_pre.innerHTML +=
// '===================================\n' +
// 'FAILED CASE: encode\n' +
// '===================================\n' +
// 'decoded : ' + decoded + '\n' +
// 'expected: ' + encoded + '\n' +
// 'actual : ' + my_encoded + '\n\n' ;
//}
if (!decodes_correctly) {
rlp_pre.innerHTML +=
'===================================\n' +
'FAILED CASE: decode\n' +
'===================================\n' +
'encoded : ' + encoded + '\n' +
'expected: ' + decoded + '\n' +
'actual : ' + my_decoded + '\n\n' ;
}
}
}
function main(): void { function main(): void {
// @ts-ignore ts can't prove to itself that the element exists document.getElementById('b64-total-cases')!.innerHTML = '' + cases.base64.length;
document.getElementById('b64-total-cases')!.innerHTML = cases.base64.length;
// @ts-ignore ts can't prove to itself that the element exists
document.getElementById('b64-go')!.onclick = b64_tests; document.getElementById('b64-go')!.onclick = b64_tests;
// @ts-ignore ts can't prove to itself that the element exists document.getElementById('b58-total-cases')!.innerHTML = '' + cases.base58.length;
document.getElementById('b58-total-cases')!.innerHTML = cases.base58.length;
// @ts-ignore ts can't prove to itself that the element exists
document.getElementById('b58-go')!.onclick = b58_tests; document.getElementById('b58-go')!.onclick = b58_tests;
document.getElementById('rlp-total-cases')!.innerHTML = '' + cases.rlp.length;
document.getElementById('rlp-go')!.onclick = rlp_tests;
} }
main(); main();
@@ -2,15 +2,83 @@
%-compile(export_all). %-compile(export_all).
-mode(compile). -mode(compile).
%-compile(nowarn_unused). -compile([nowarn_unused_function]).
main([]) -> main([]) ->
% Decoded cases % Decoded cases
DecodedCases = rand_decode_datas(100_000), DecodedCases = rand_decode_datas(10_000),
%io:format("~p~n", [DecodedCases]), %io:format("~p~n", [DecodedCases]),
io:format("~s~n", [format_cases_py(DecodedCases)]), io:format("~s~n", [format_cases_js(DecodedCases)]),
ok. ok.
%%%%%%%%%%%%%%%%%%%%%
%%% JS FORMATTING %%%
%%%%%%%%%%%%%%%%%%%%%
format_cases_js(Cases) ->
format_stringlist_js(lists:map(fun format_case_js/1, Cases)).
% format a list of strings into a python list
format_stringlist_js(List) ->
% stringlist commas are the same in python/js, so reusing python
["import * as rlp from './jex_include/local-vanillae-0.1.0/dist/rlp.js';\n"
"\n"
"type rlpcases = {decoded: rlp.decoded_data, encoded: Uint8Array};\n"
"\n"
"// @ts-ignore never mind; jesus christ\n"
"export const cases : Array<rlpcases> = [\n", slcommas(List, []), "];"].
% input: decoded_data
% format a case as
% {'decoded_data': <js term for rlist>,
% 'encoded_bytes': new Uint8Array([B1, B2, B3, ...])}
format_case_js(DecodedData_rlist) ->
% EncodedData_bytes = rlp:encode(
% DD_js = format_data_js(DecodedData_rlist),
EncodedData_bytes = rlp:encode(DecodedData_rlist),
EncodedBytes_py = format_bytes_js(EncodedData_bytes),
DecodedData_py = format_data_js(DecodedData_rlist),
[" {'decoded': ", DecodedData_py, ",\n",
" 'encoded': ", EncodedBytes_py, "}"].
format_data_js(List) when is_list(List) ->
format_list_js(List);
format_data_js(Bytes) when is_binary(Bytes) ->
format_bytes_js(Bytes).
format_list_js(List) ->
[$[, js_lcommas(List, []), $]].
% similar to commas/2 below but for a list
% cases
% - list is empty -> special case
% - exactly one element -> special case
% - two or more elements -> peel off one at a time until terminal case of exactly one
% initial input empty, so return empty
js_lcommas([], []) ->
[];
% one item left, do not add comma
js_lcommas([Item], Acc) ->
[Acc, format_data_js(Item)];
% two or more items left, add comma
js_lcommas([Item | Rest], Acc) ->
js_lcommas(Rest, [Acc, format_data_js(Item), ", "]).
% format a bytestring as "bytes([Byte1, Byte2, ...])"
format_bytes_js(Bytes) ->
% commas are the same as in python so reusing that
Commas = commas(Bytes, []),
["new Uint8Array([", Commas, "])"].
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% PYTHON FORMATTING %%%
%%%%%%%%%%%%%%%%%%%%%%%%%
format_cases_py(Cases) -> format_cases_py(Cases) ->
format_stringlist_py(lists:map(fun format_case_py/1, Cases)). format_stringlist_py(lists:map(fun format_case_py/1, Cases)).
@@ -99,6 +167,11 @@ commas(<<B, Rest/binary>>, Acc) ->
commas(Rest, [Acc, integer_to_list(B), ", "]). commas(Rest, [Acc, integer_to_list(B), ", "]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% INVERSE PROPERTY TEST %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% test that the inverse property is correct % test that the inverse property is correct
test_inverse() -> test_inverse() ->
@@ -121,6 +194,12 @@ check_inverse(DecodedData) ->
{DeEncodedData, <<>>} = rlp:decode(rlp:encode(DecodedData)), {DeEncodedData, <<>>} = rlp:decode(rlp:encode(DecodedData)),
DeEncodedData =:= DecodedData. DeEncodedData =:= DecodedData.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% RANDOM CASE GENERATION %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%mkcase(DecodedData) -> %mkcase(DecodedData) ->
% Encoded = rlp:encode(DecodedData), % Encoded = rlp:encode(DecodedData),
% {Deencoded, <<>>} = rlp:decode( % {Deencoded, <<>>} = rlp:decode(