Skip to content

Commit d97a830

Browse files
authored
Encode AccountId as AccountController enum with multisig sort (#5536)
Replace plain string encoding of AccountId with proper AccountController enum serialization (Single/Multisig discriminants). Sort multisig members by canonical sort key to match Rust ordering. Add bounds checks on all wire length reads in decode paths. Signed-off-by: Dmitry Selivanov <diselivanov@gmail.com>
1 parent 546f58b commit d97a830

File tree

2 files changed

+425
-25
lines changed

2 files changed

+425
-25
lines changed

java/iroha_android/src/main/java/org/hyperledger/iroha/android/norito/TransactionPayloadAdapter.java

Lines changed: 232 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.hyperledger.iroha.android.norito;
22

3+
import java.nio.charset.StandardCharsets;
34
import java.util.ArrayList;
5+
import java.util.Arrays;
46
import java.util.Collections;
57
import java.util.LinkedHashMap;
68
import 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

Comments
 (0)