@@ -155,6 +155,110 @@ public void testDifferentCase() {
155155 }
156156
157157
158+ // TIP-854: after activation, validateMultiSign (H=5, I=5) must reject calldata
159+ // whose byte length is incompatible with the (words - 5) / 5 shape the per-call
160+ // energy formula already assumes, returning (false, empty).
161+ @ Test
162+ public void testTip854RejectsMalformedCalldata () {
163+ VMConfig .initAllowTvmOsaka (1 );
164+ try {
165+ // Bucket 1: 32-aligned head + sub-word trailing bytes (r=1, r=31).
166+ for (int r : new int []{1 , 31 }) {
167+ byte [] data = new byte [(5 + 5 ) * 32 + r ];
168+ Pair <Boolean , byte []> ret = contract .execute (data );
169+ Assert .assertFalse ("non-32-aligned len=" + data .length , ret .getLeft ());
170+ Assert .assertSame (ByteUtil .EMPTY_BYTE_ARRAY , ret .getRight ());
171+ }
172+ // Bucket 2: fewer than the static head's 5 words.
173+ for (int bytes : new int []{0 , 32 , 64 , 96 , 128 }) {
174+ Pair <Boolean , byte []> ret = contract .execute (new byte [bytes ]);
175+ Assert .assertFalse ("len=" + bytes + " < 5 words" , ret .getLeft ());
176+ Assert .assertSame (ByteUtil .EMPTY_BYTE_ARRAY , ret .getRight ());
177+ }
178+ // Bucket 3: 32-aligned but tail not a multiple of I=5 words (k = 1..4).
179+ for (int k = 1 ; k <= 4 ; k ++) {
180+ byte [] data = new byte [(5 + k ) * 32 ];
181+ Pair <Boolean , byte []> ret = contract .execute (data );
182+ Assert .assertFalse ("aligned bad-tail k=" + k , ret .getLeft ());
183+ Assert .assertSame (ByteUtil .EMPTY_BYTE_ARRAY , ret .getRight ());
184+ }
185+ // Null calldata: explicit spec clause.
186+ Pair <Boolean , byte []> ret = contract .execute (null );
187+ Assert .assertFalse ("null calldata" , ret .getLeft ());
188+ Assert .assertSame (ByteUtil .EMPTY_BYTE_ARRAY , ret .getRight ());
189+ } finally {
190+ VMConfig .initAllowTvmOsaka (0 );
191+ }
192+ }
193+
194+ // TIP-854 Compatibility: for canonically-shaped calldata (real 65-byte sigs,
195+ // total length == 5*32 + 5*32*N), behaviour must be identical pre- vs
196+ // post-activation — the guard is a no-op for well-formed inputs.
197+ @ Test
198+ public void testTip854CanonicalInputUnchanged () {
199+ ECKey key = new ECKey ();
200+ AccountCapsule toAccount = new AccountCapsule (ByteString .copyFrom (key .getAddress ()),
201+ Protocol .AccountType .Normal ,
202+ System .currentTimeMillis (), true , dbManager .getDynamicPropertiesStore ());
203+ ECKey key1 = new ECKey ();
204+ ECKey key2 = new ECKey ();
205+ Protocol .Permission activePermission =
206+ Protocol .Permission .newBuilder ()
207+ .setType (Protocol .Permission .PermissionType .Active )
208+ .setId (2 )
209+ .setPermissionName ("active" )
210+ .setThreshold (2 )
211+ .setOperations (ByteString .copyFrom (ByteArray
212+ .fromHexString ("0000000000000000000000000000000000000000000000000000000000000000" )))
213+ .addKeys (Protocol .Key .newBuilder ().setAddress (ByteString .copyFrom (key1 .getAddress ()))
214+ .setWeight (1 ).build ())
215+ .addKeys (Protocol .Key .newBuilder ().setAddress (ByteString .copyFrom (key2 .getAddress ()))
216+ .setWeight (1 ).build ())
217+ .build ();
218+ toAccount .updatePermissions (toAccount .getPermissionById (0 ), null ,
219+ Collections .singletonList (activePermission ));
220+ dbManager .getAccountStore ().put (key .getAddress (), toAccount );
221+
222+ byte [] data = Sha256Hash .hash (CommonParameter .getInstance ().isECKeyCryptoEngine (), longData );
223+ byte [] merged = ByteUtil .merge (key .getAddress (), ByteArray .fromInt (2 ), data );
224+ byte [] toSign = Sha256Hash .hash (CommonParameter .getInstance ().isECKeyCryptoEngine (), merged );
225+ List <Object > signs = new ArrayList <>();
226+ signs .add (Hex .toHexString (key1 .sign (toSign ).toByteArray ()));
227+ signs .add (Hex .toHexString (key2 .sign (toSign ).toByteArray ()));
228+
229+ VMConfig .initAllowTvmOsaka (0 );
230+ Pair <Boolean , byte []> pre =
231+ validateMultiSign (StringUtil .encode58Check (key .getAddress ()), 2 , data , signs );
232+ VMConfig .initAllowTvmOsaka (1 );
233+ try {
234+ Pair <Boolean , byte []> post =
235+ validateMultiSign (StringUtil .encode58Check (key .getAddress ()), 2 , data , signs );
236+ Assert .assertEquals (pre .getLeft (), post .getLeft ());
237+ Assert .assertArrayEquals (pre .getValue (), post .getValue ());
238+ Assert .assertArrayEquals (DataWord .ONE ().getData (), post .getValue ());
239+ } finally {
240+ VMConfig .initAllowTvmOsaka (0 );
241+ }
242+ }
243+
244+ // TIP-854: before activation, malformed calldata reaches the legacy decoder.
245+ // Assert the guard is not taken — this precompile has no outer catch, so a
246+ // too-short input raises inside the decoder; that is the documented
247+ // pre-activation failure mode the TIP explicitly preserves.
248+ @ Test
249+ public void testTip854PreActivationNoOp () {
250+ VMConfig .initAllowTvmOsaka (0 );
251+ contract .setRepository (RepositoryImpl .createRoot (StoreFactory .getInstance ()));
252+ try {
253+ Pair <Boolean , byte []> ret = contract .execute (new byte [(5 + 1 ) * 32 ]);
254+ // If the decoder happened to handle it without raising, we must not have
255+ // taken the post-activation reject path (false, empty).
256+ Assert .assertNotSame (ByteUtil .EMPTY_BYTE_ARRAY , ret .getRight ());
257+ } catch (RuntimeException expectedLegacyBehaviour ) {
258+ // Pre-activation: decoder may throw — this is the existing behaviour.
259+ }
260+ }
261+
158262 Pair <Boolean , byte []> validateMultiSign (String address , int permissionId , byte [] hash ,
159263 List <Object > signatures ) {
160264 List <Object > parameters = Arrays
0 commit comments