|
| 1 | +%%% @doc A battery of test vectors exercising the `bundle' / `hint-device' |
| 2 | +%%% machinery of the `message@1.0' device across a three-level message tree. |
| 3 | +%%% |
| 4 | +%%% The tree is built bottom-up; each level is a committed (signed) message |
| 5 | +%%% holding the level below it as a sub-message: |
| 6 | +%%% |
| 7 | +%%% <pre> |
| 8 | +%%% L1 (root) -- l2 --> L2 (middle) -- l3 --> L3 (leaf) -- inner --> #{} |
| 9 | +%%% </pre> |
| 10 | +%%% |
| 11 | +%%% Each level is committed with its own `bundle' choice -- `true', `false' |
| 12 | +%%% or `none' (committed with no `bundle' flag at all). The flag decides |
| 13 | +%%% whether that level's sub-message is held inline (loaded) or as a link |
| 14 | +%%% (offloaded) in the level's signed TABM form: |
| 15 | +%%% |
| 16 | +%%% - L1's flag controls `l2', L2's flag controls `l3', and L3's flag |
| 17 | +%%% controls L3's plain sub-map `inner'. |
| 18 | +%%% |
| 19 | +%%% `none' is observably identical to `false': committing with no flag |
| 20 | +%%% offloads children exactly as `false' does. |
| 21 | +%%% |
| 22 | +%%% For every 3x3x3 permutation of build flags the suite checks: |
| 23 | +%%% |
| 24 | +%%% - verify/3 with no forced bundle: the reliable path -- every level |
| 25 | +%%% verifies in the state it was committed in. |
| 26 | +%%% - verify/3 with a forced bundle (`true'|`false'): the edge case -- a |
| 27 | +%%% `bundle' on the verify request is harmless. verify builds its |
| 28 | +%%% source spec like commit/3 (mirroring the request `bundle' but also |
| 29 | +%%% setting `hint-device'), so the per-node hints override the forced |
| 30 | +%%% value and the tree still verifies. Tested for completeness. |
| 31 | +%%% - id/3: the root's id equals its sole commitment's key. |
| 32 | +%%% - convert/4: the tree round-trips through the `ans104@1.0' codec -- |
| 33 | +%%% the standard structured<->codec path -- and still verifies at every |
| 34 | +%%% level. A `bundle' on the request is per-node-overridden, so the |
| 35 | +%%% committed shape survives the round-trip. |
| 36 | +-module(dev_message_bundle_test_vectors). |
| 37 | +-include_lib("eunit/include/eunit.hrl"). |
| 38 | +-include("include/hb.hrl"). |
| 39 | + |
| 40 | +%% @doc Fresh, isolated options for a single vector: a new wallet and a new |
| 41 | +%% in-memory store, so vectors cannot interfere with one another. |
| 42 | +fresh_opts() -> |
| 43 | + #{ |
| 44 | + <<"priv-wallet">> => hb:wallet(), |
| 45 | + <<"store">> => hb_test_utils:test_store() |
| 46 | + }. |
| 47 | + |
| 48 | +%% @doc Commit a message with the `ans104@1.0' codec. `Bundle' is `true', |
| 49 | +%% `false', or `none' to commit with no `bundle' flag at all. |
| 50 | +commit(Msg, none, Opts) -> |
| 51 | + hb_message:commit(Msg, Opts, #{ <<"device">> => <<"ans104@1.0">> }); |
| 52 | +commit(Msg, Bundle, Opts) -> |
| 53 | + hb_message:commit( |
| 54 | + Msg, |
| 55 | + Opts, |
| 56 | + #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => Bundle } |
| 57 | + ). |
| 58 | + |
| 59 | +%% @doc Build a signed three-level tree with the given per-level flags. |
| 60 | +build_tree(B1, B2, B3, Opts) -> |
| 61 | + L3 = |
| 62 | + commit( |
| 63 | + #{ |
| 64 | + <<"l3-tag">> => <<"l3-value">>, |
| 65 | + <<"inner">> => #{ <<"deep">> => <<"deep-value">> } |
| 66 | + }, |
| 67 | + B3, |
| 68 | + Opts |
| 69 | + ), |
| 70 | + L2 = commit(#{ <<"l2-tag">> => <<"l2-value">>, <<"l3">> => L3 }, B2, Opts), |
| 71 | + commit(#{ <<"l1-tag">> => <<"l1-value">>, <<"l2">> => L2 }, B1, Opts). |
| 72 | + |
| 73 | +%%% Test vector generator. |
| 74 | + |
| 75 | +%% @doc The {API, RequestBundle} operations run against every tree shape. |
| 76 | +operations() -> |
| 77 | + [ |
| 78 | + {verify, none}, |
| 79 | + {verify, true}, |
| 80 | + {verify, false}, |
| 81 | + {id, none}, |
| 82 | + {convert, none}, |
| 83 | + {convert, true}, |
| 84 | + {convert, false} |
| 85 | + ]. |
| 86 | + |
| 87 | +%% @doc Generate the full grid: 3x3x3 tree shapes x the operation list. |
| 88 | +bundle_vectors_test_() -> |
| 89 | + {timeout, 240, |
| 90 | + [ |
| 91 | + { |
| 92 | + test_label(B1, B2, B3, Api, ReqBundle), |
| 93 | + fun() -> run(B1, B2, B3, Api, ReqBundle) end |
| 94 | + } |
| 95 | + || |
| 96 | + B1 <- [true, false, none], |
| 97 | + B2 <- [true, false, none], |
| 98 | + B3 <- [true, false, none], |
| 99 | + {Api, ReqBundle} <- operations() |
| 100 | + ] |
| 101 | + }. |
| 102 | + |
| 103 | +test_label(B1, B2, B3, Api, ReqBundle) -> |
| 104 | + lists:flatten( |
| 105 | + io_lib:format( |
| 106 | + "L1=~p L2=~p L3=~p ~p req-bundle=~p", |
| 107 | + [B1, B2, B3, Api, ReqBundle] |
| 108 | + ) |
| 109 | + ). |
| 110 | + |
| 111 | +%% @doc Build the tree and exercise the chosen API. |
| 112 | +run(B1, B2, B3, Api, ReqBundle) -> |
| 113 | + Opts = fresh_opts(), |
| 114 | + Tree = build_tree(B1, B2, B3, Opts), |
| 115 | + % Every freshly built tree must verify via the reliable per-node path, |
| 116 | + % whatever per-level bundle permutation it was signed with. |
| 117 | + ?assert(hb_message:verify(Tree, all, Opts)), |
| 118 | + exercise(Api, ReqBundle, B1, B2, B3, Tree, Opts). |
| 119 | + |
| 120 | +%%% Per-API exercises. |
| 121 | + |
| 122 | +%% `verify': verification always uses the per-node path -- each subtree is |
| 123 | +%% checked in the bundle state it was committed in. A `bundle' on the |
| 124 | +%% request is mirrored as commit/3 does, but `hint-device' is set too, so |
| 125 | +%% the per-node hints override it. A validly-built tree therefore always |
| 126 | +%% verifies at every level, with or without a forced request bundle. |
| 127 | +exercise(verify, ReqBundle, _B1, _B2, _B3, Tree, Opts) -> |
| 128 | + Spec = verify_spec(ReqBundle), |
| 129 | + ?assert(hb_message:verify(Tree, Spec, Opts)), |
| 130 | + L2 = hb_maps:get(<<"l2">>, Tree, undefined, Opts), |
| 131 | + ?assert(hb_message:verify(L2, Spec, Opts)), |
| 132 | + L3 = hb_maps:get(<<"l3">>, L2, undefined, Opts), |
| 133 | + ?assert(hb_message:verify(L3, Spec, Opts)); |
| 134 | + |
| 135 | +%% `id': the root was committed exactly once, so `id/3' with `all' |
| 136 | +%% committers accumulates to that single commitment -- the id must equal |
| 137 | +%% the key under which it is stored in the root's commitments map. |
| 138 | +exercise(id, _ReqBundle, _B1, _B2, _B3, Tree, Opts) -> |
| 139 | + Id = hb_message:id(Tree, all, Opts), |
| 140 | + Commitments = hb_maps:get(<<"commitments">>, Tree, #{}, Opts), |
| 141 | + ?assertEqual([Id], maps:keys(Commitments)); |
| 142 | + |
| 143 | +%% `convert': round-trip the tree through the `ans104@1.0' codec -- the |
| 144 | +%% standard structured<->codec path. Each subtree converts in the state its |
| 145 | +%% own commitment dictates (per-node), so a `bundle' flag on the request is |
| 146 | +%% overridden and the committed shape is preserved. The round-tripped tree |
| 147 | +%% must therefore still verify at every level. |
| 148 | +exercise(convert, ReqBundle, _B1, _B2, _B3, Tree, Opts) -> |
| 149 | + Encoded = hb_message:convert(Tree, convert_target(ReqBundle), Opts), |
| 150 | + Restored = |
| 151 | + hb_message:convert( |
| 152 | + Encoded, |
| 153 | + <<"structured@1.0">>, |
| 154 | + <<"ans104@1.0">>, |
| 155 | + Opts |
| 156 | + ), |
| 157 | + ?assert(hb_message:verify(Restored, all, Opts)), |
| 158 | + L2 = hb_maps:get(<<"l2">>, Restored, undefined, Opts), |
| 159 | + ?assert(hb_message:verify(L2, all, Opts)), |
| 160 | + L3 = hb_maps:get(<<"l3">>, L2, undefined, Opts), |
| 161 | + ?assert(hb_message:verify(L3, all, Opts)). |
| 162 | + |
| 163 | +%% @doc The verify spec for a request-bundle value: `all' committers, plus |
| 164 | +%% the forced `bundle' flag when one is given. |
| 165 | +verify_spec(none) -> |
| 166 | + all; |
| 167 | +verify_spec(ReqBundle) -> |
| 168 | + #{ <<"committers">> => <<"all">>, <<"bundle">> => ReqBundle }. |
| 169 | + |
| 170 | +%% @doc The convert target for a request-bundle value: the bare `ans104@1.0' |
| 171 | +%% codec, plus a forced `bundle' flag when one is given. |
| 172 | +convert_target(none) -> |
| 173 | + <<"ans104@1.0">>; |
| 174 | +convert_target(ReqBundle) -> |
| 175 | + #{ <<"device">> => <<"ans104@1.0">>, <<"bundle">> => ReqBundle }. |
0 commit comments