11package org .hyperledger .iroha .android .norito ;
22
3+ import java .nio .charset .StandardCharsets ;
34import java .util .ArrayList ;
5+ import java .util .Arrays ;
46import java .util .Collections ;
57import java .util .LinkedHashMap ;
68import java .util .List ;
@@ -146,10 +148,119 @@ public Executable decode(final NoritoDecoder decoder) {
146148 }
147149 }
148150
151+ /**
152+ * Encodes/decodes AccountId which is {@code #[norito(transparent)]} over AccountController.
153+ *
154+ * <p>Rust layout:
155+ *
156+ * <pre>
157+ * struct AccountId { controller: AccountController } // #[norito(transparent)]
158+ * enum AccountController {
159+ * Single(PublicKey), // discriminant 0
160+ * Multisig(MultisigPolicy), // discriminant 1
161+ * }
162+ * </pre>
163+ *
164+ * The transparent attribute means AccountId serializes directly as AccountController. The Java
165+ * side represents authority as the canonical I105 address string.
166+ */
149167 private static final class AccountIdAdapter implements TypeAdapter <String > {
168+ private static final long SINGLE_DISCRIMINANT = 0L ;
169+ private static final long MULTISIG_DISCRIMINANT = 1L ;
170+
150171 @ Override
151172 public void encode (final NoritoEncoder encoder , final String value ) {
152- STRING_ADAPTER .encode (encoder , normalizeAuthority (value ));
173+ final String i105 = normalizeAuthority (value );
174+ final AccountAddress address ;
175+ try {
176+ address = AccountAddress .parseEncoded (i105 , null ).address ;
177+ } catch (final AccountAddress .AccountAddressException ex ) {
178+ throw new IllegalArgumentException ("Failed to parse I105 address: " + i105 , ex );
179+ }
180+
181+ try {
182+ final Optional <AccountAddress .SingleKeyPayload > singleKey = address .singleKeyPayload ();
183+ if (singleKey .isPresent ()) {
184+ encodeSingle (encoder , singleKey .get ());
185+ return ;
186+ }
187+ } catch (final AccountAddress .AccountAddressException ex ) {
188+ throw new IllegalArgumentException ("Failed to extract controller from I105 address" , ex );
189+ }
190+
191+ try {
192+ final Optional <AccountAddress .MultisigPolicyPayload > multisig = address .multisigPolicyPayload ();
193+ if (multisig .isPresent ()) {
194+ encodeMultisig (encoder , multisig .get ());
195+ return ;
196+ }
197+ } catch (final AccountAddress .AccountAddressException ex ) {
198+ throw new IllegalArgumentException ("Failed to extract controller from I105 address" , ex );
199+ }
200+
201+ throw new IllegalArgumentException (
202+ "I105 address contains neither single-key nor multisig controller" );
203+ }
204+
205+ private static void encodeSingle (
206+ final NoritoEncoder encoder , final AccountAddress .SingleKeyPayload key ) {
207+ final boolean compact = (encoder .flags () & NoritoHeader .COMPACT_LEN ) != 0 ;
208+ ENUM_TAG_ADAPTER .encode (encoder , SINGLE_DISCRIMINANT );
209+ final String multihashHex =
210+ PublicKeyCodec .encodePublicKeyMultihash (key .curveId (), key .publicKey ());
211+ final NoritoEncoder child = encoder .childEncoder ();
212+ STRING_ADAPTER .encode (child , multihashHex );
213+ final byte [] payload = child .toByteArray ();
214+ encoder .writeLength (payload .length , compact );
215+ encoder .writeBytes (payload );
216+ }
217+
218+ private static void encodeMultisig (
219+ final NoritoEncoder encoder , final AccountAddress .MultisigPolicyPayload policy ) {
220+ final boolean compact = (encoder .flags () & NoritoHeader .COMPACT_LEN ) != 0 ;
221+ ENUM_TAG_ADAPTER .encode (encoder , MULTISIG_DISCRIMINANT );
222+
223+ final NoritoEncoder policyEncoder = encoder .childEncoder ();
224+ encodeSizedField (policyEncoder , UINT8_ADAPTER , (long ) policy .version ());
225+ encodeSizedField (policyEncoder , UINT16_ADAPTER , (long ) policy .threshold ());
226+ encodeMultisigMembers (policyEncoder , policy .members ());
227+
228+ final byte [] policyPayload = policyEncoder .toByteArray ();
229+ encoder .writeLength (policyPayload .length , compact );
230+ encoder .writeBytes (policyPayload );
231+ }
232+
233+ private static void encodeMultisigMembers (
234+ final NoritoEncoder encoder , final List <AccountAddress .MultisigMemberPayload > members ) {
235+ final List <AccountAddress .MultisigMemberPayload > sorted = new ArrayList <>(members );
236+ sorted .sort (
237+ (a , b ) -> {
238+ final byte [] keyA = canonicalSortKey (a );
239+ final byte [] keyB = canonicalSortKey (b );
240+ return compareUnsigned (keyA , keyB );
241+ });
242+ for (int i = 1 ; i < sorted .size (); i ++) {
243+ if (Arrays .equals (canonicalSortKey (sorted .get (i - 1 )), canonicalSortKey (sorted .get (i )))) {
244+ throw new IllegalArgumentException ("Duplicate multisig member" );
245+ }
246+ }
247+
248+ final boolean compact = (encoder .flags () & NoritoHeader .COMPACT_LEN ) != 0 ;
249+ final NoritoEncoder vecEncoder = encoder .childEncoder ();
250+ vecEncoder .writeLength (sorted .size (), false );
251+ for (final AccountAddress .MultisigMemberPayload member : sorted ) {
252+ final NoritoEncoder memberEncoder = vecEncoder .childEncoder ();
253+ final String memberMultihash =
254+ PublicKeyCodec .encodePublicKeyMultihash (member .curveId (), member .publicKey ());
255+ encodeSizedField (memberEncoder , STRING_ADAPTER , memberMultihash );
256+ encodeSizedField (memberEncoder , UINT16_ADAPTER , (long ) member .weight ());
257+ final byte [] memberPayload = memberEncoder .toByteArray ();
258+ vecEncoder .writeLength (memberPayload .length , compact );
259+ vecEncoder .writeBytes (memberPayload );
260+ }
261+ final byte [] vecPayload = vecEncoder .toByteArray ();
262+ encoder .writeLength (vecPayload .length , compact );
263+ encoder .writeBytes (vecPayload );
153264 }
154265
155266 @ Override
@@ -158,14 +269,128 @@ public String decode(final NoritoDecoder decoder) {
158269 return decodePayload (payload , decoder .flags (), decoder .flagsHint ());
159270 }
160271
161- private static String decodePayload (
272+ static String decodePayload (
162273 final byte [] payload , final int flags , final int flagsHint ) {
163- final NoritoDecoder stringDecoder = new NoritoDecoder (payload , flags , flagsHint );
164- final String literal = STRING_ADAPTER .decode (stringDecoder );
165- if (stringDecoder .remaining () != 0 ) {
166- throw new IllegalArgumentException ("Trailing bytes after authority payload" );
274+ final NoritoDecoder d = new NoritoDecoder (payload , flags , flagsHint );
275+ final long tag = ENUM_TAG_ADAPTER .decode (d );
276+ final long variantLen = d .readLength (d .compactLenActive ());
277+ if (variantLen > Integer .MAX_VALUE ) {
278+ throw new IllegalArgumentException ("AccountController variant payload too large" );
279+ }
280+ final byte [] variantPayload = d .readBytes ((int ) variantLen );
281+ if (d .remaining () != 0 ) {
282+ throw new IllegalArgumentException ("Trailing bytes after AccountController" );
283+ }
284+
285+ if (tag == SINGLE_DISCRIMINANT ) {
286+ return decodeSingleVariant (variantPayload , flags , flagsHint );
287+ }
288+ if (tag == MULTISIG_DISCRIMINANT ) {
289+ return decodeMultisigVariant (variantPayload , flags , flagsHint );
290+ }
291+ throw new IllegalArgumentException ("Unknown AccountController discriminant: " + tag );
292+ }
293+
294+ private static String decodeSingleVariant (
295+ final byte [] payload , final int flags , final int flagsHint ) {
296+ final NoritoDecoder d = new NoritoDecoder (payload , flags , flagsHint );
297+ final String multihashHex = STRING_ADAPTER .decode (d );
298+ if (d .remaining () != 0 ) {
299+ throw new IllegalArgumentException ("Trailing bytes after PublicKey" );
300+ }
301+ final PublicKeyCodec .PublicKeyPayload pk = PublicKeyCodec .decodePublicKeyLiteral (multihashHex );
302+ if (pk == null ) {
303+ throw new IllegalArgumentException ("Invalid public key multihash: " + multihashHex );
304+ }
305+ try {
306+ return AccountAddress .fromAccount (pk .keyBytes (), algorithmForCurveId (pk .curveId ()))
307+ .toI105 (AccountAddress .DEFAULT_I105_DISCRIMINANT );
308+ } catch (final AccountAddress .AccountAddressException ex ) {
309+ throw new IllegalArgumentException ("Failed to reconstruct I105 from public key" , ex );
310+ }
311+ }
312+
313+ private static String decodeMultisigVariant (
314+ final byte [] payload , final int flags , final int flagsHint ) {
315+ final NoritoDecoder d = new NoritoDecoder (payload , flags , flagsHint );
316+ final int version = Math .toIntExact (decodeSizedField (d , UINT8_ADAPTER ));
317+ final int threshold = Math .toIntExact (decodeSizedField (d , UINT16_ADAPTER ));
318+
319+ final long vecLen = d .readLength (d .compactLenActive ());
320+ if (vecLen > Integer .MAX_VALUE ) {
321+ throw new IllegalArgumentException ("MultisigPolicy vector payload too large" );
322+ }
323+ final byte [] vecPayload = d .readBytes ((int ) vecLen );
324+ if (d .remaining () != 0 ) {
325+ throw new IllegalArgumentException ("Trailing bytes after MultisigPolicy" );
326+ }
327+
328+ final NoritoDecoder vecDecoder = new NoritoDecoder (vecPayload , flags , flagsHint );
329+ final long count = vecDecoder .readLength (false );
330+ if (count > Integer .MAX_VALUE ) {
331+ throw new IllegalArgumentException ("MultisigMember count too large" );
332+ }
333+ final List <AccountAddress .MultisigMemberPayload > members = new ArrayList <>((int ) count );
334+ for (long i = 0 ; i < count ; i ++) {
335+ final long memberLen = vecDecoder .readLength (vecDecoder .compactLenActive ());
336+ if (memberLen > Integer .MAX_VALUE ) {
337+ throw new IllegalArgumentException ("MultisigMember payload too large" );
338+ }
339+ final byte [] memberPayload = vecDecoder .readBytes ((int ) memberLen );
340+ final NoritoDecoder memberDecoder = new NoritoDecoder (memberPayload , flags , flagsHint );
341+ final String memberMultihash = decodeSizedField (memberDecoder , STRING_ADAPTER );
342+ final int weight = Math .toIntExact (decodeSizedField (memberDecoder , UINT16_ADAPTER ));
343+ if (memberDecoder .remaining () != 0 ) {
344+ throw new IllegalArgumentException ("Trailing bytes after MultisigMember" );
345+ }
346+ final PublicKeyCodec .PublicKeyPayload pk =
347+ PublicKeyCodec .decodePublicKeyLiteral (memberMultihash );
348+ if (pk == null ) {
349+ throw new IllegalArgumentException ("Invalid member public key: " + memberMultihash );
350+ }
351+ members .add (AccountAddress .MultisigMemberPayload .of (pk .curveId (), weight , pk .keyBytes ()));
352+ }
353+ if (vecDecoder .remaining () != 0 ) {
354+ throw new IllegalArgumentException ("Trailing bytes after MultisigMember vector" );
355+ }
356+
357+ try {
358+ return AccountAddress .fromMultisigPolicy (
359+ AccountAddress .MultisigPolicyPayload .of (version , threshold , members ))
360+ .toI105 (AccountAddress .DEFAULT_I105_DISCRIMINANT );
361+ } catch (final AccountAddress .AccountAddressException ex ) {
362+ throw new IllegalArgumentException ("Failed to reconstruct I105 from multisig policy" , ex );
363+ }
364+ }
365+
366+ private static byte [] canonicalSortKey (final AccountAddress .MultisigMemberPayload member ) {
367+ final String algorithm = algorithmForCurveId (member .curveId ());
368+ final byte [] algorithmBytes = algorithm .getBytes (StandardCharsets .UTF_8 );
369+ final byte [] key = member .publicKey ();
370+ final byte [] sortKey = new byte [algorithmBytes .length + 1 + key .length ];
371+ System .arraycopy (algorithmBytes , 0 , sortKey , 0 , algorithmBytes .length );
372+ sortKey [algorithmBytes .length ] = 0 ;
373+ System .arraycopy (key , 0 , sortKey , algorithmBytes .length + 1 , key .length );
374+ return sortKey ;
375+ }
376+
377+ private static int compareUnsigned (final byte [] a , final byte [] b ) {
378+ final int len = Math .min (a .length , b .length );
379+ for (int i = 0 ; i < len ; i ++) {
380+ final int cmp = (a [i ] & 0xFF ) - (b [i ] & 0xFF );
381+ if (cmp != 0 ) {
382+ return cmp ;
383+ }
384+ }
385+ return Integer .compare (a .length , b .length );
386+ }
387+
388+ private static String algorithmForCurveId (final int curveId ) {
389+ final String algorithm = PublicKeyCodec .algorithmForCurveId (curveId );
390+ if (algorithm == null ) {
391+ throw new IllegalArgumentException ("Unknown curve id: " + curveId );
167392 }
168- return normalizeAuthority ( literal ) ;
393+ return algorithm ;
169394 }
170395
171396 private static String normalizeAuthority (final String authority ) {
0 commit comments