Skip to content

Commit c63d0c0

Browse files
dejmekradimclaude
andcommitted
ERS: sort the hash lists once instead of inserting into a LinkedList by index
SortedHashList and SortedIndexedHashList kept their hashes in a LinkedList and found each insertion point by walking it with get(index). LinkedList.get(i) is O(i), so one add was O(n^2) and building a list of n hashes was O(n^3): 10,000 data objects took 347 s in generateTimeStampRequest, rising by about a factor of eight per doubling. Both lists are on the evidence record generation path - SortedIndexedHashList from ERSArchiveTimeStampGenerator.getPartialHashtrees(), SortedHashList from BinaryTreeRootCalculator.computeRootHash() - and neither can be replaced from outside the library, since getPartialHashtrees() is private. Both classes expose only size(), getFirst() and toList(), so collect into an ArrayList and sort once, lazily, in the accessors. The order is unchanged: the old add inserted each hash after every element comparing <= to it, which is where a stable sort of the insertion sequence puts it, and Collections.sort is stable. getFirst() still throws NoSuchElementException on an empty list. Tests: testSortedHashListOrder reproduces the original insertion algorithm and asserts the list matches it element for element over 1,200 pseudo-random values including duplicates and shared prefixes; testLargeDataObjectSet builds a reduced hash tree over 2,000 data objects, reaching both lists, and asserts the root does not depend on the order the objects were added in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137cntJ35EeSoLRzoBaUVEK
1 parent ab16374 commit c63d0c0

3 files changed

Lines changed: 220 additions & 62 deletions

File tree

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.bouncycastle.tsp.ers;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.Comparator;
5-
import java.util.LinkedList;
66
import java.util.List;
7+
import java.util.NoSuchElementException;
78

89
/**
910
* A sorting list - byte[] are sorted in ascending order.
@@ -12,47 +13,30 @@ public class SortedHashList
1213
{
1314
private static final Comparator<byte[]> hashComp = new ByteArrayComparator();
1415

15-
private final LinkedList<byte[]> baseList = new LinkedList<byte[]>();
16+
private final List<byte[]> baseList = new ArrayList<byte[]>();
17+
18+
private boolean isSorted = true;
1619

1720
public SortedHashList()
1821
{
1922
}
2023

2124
public byte[] getFirst()
2225
{
23-
return (byte[])baseList.getFirst();
26+
if (baseList.isEmpty())
27+
{
28+
throw new NoSuchElementException();
29+
}
30+
31+
sort();
32+
33+
return (byte[])baseList.get(0);
2434
}
2535

2636
public void add(byte[] hash)
2737
{
28-
if (baseList.size() == 0)
29-
{
30-
baseList.addFirst(hash);
31-
}
32-
else
33-
{
34-
if (hashComp.compare(hash, baseList.get(0)) < 0)
35-
{
36-
baseList.addFirst(hash);
37-
}
38-
else
39-
{
40-
int index = 1;
41-
while(index < baseList.size() && hashComp.compare(baseList.get(index), hash) <= 0)
42-
{
43-
index++;
44-
}
45-
46-
if (index == baseList.size())
47-
{
48-
baseList.add(hash);
49-
}
50-
else
51-
{
52-
baseList.add(index, hash);
53-
}
54-
}
55-
}
38+
baseList.add(hash);
39+
isSorted = false;
5640
}
5741

5842
public int size()
@@ -62,6 +46,25 @@ public int size()
6246

6347
public List<byte[]> toList()
6448
{
49+
sort();
50+
6551
return new ArrayList<byte[]>(baseList);
6652
}
53+
54+
/**
55+
* Sorting is deferred to the accessors. Inserting each hash on add() meant searching a
56+
* LinkedList for the insertion point with get(index), which is O(index), so a single add()
57+
* was O(n^2) and building a list of n hashes was O(n^3).
58+
* <p>
59+
* Collections.sort() is stable, so hashes comparing equal keep the order they were added
60+
* in - which is where inserting after the last equal element used to put them.
61+
*/
62+
private void sort()
63+
{
64+
if (!isSorted)
65+
{
66+
Collections.sort(baseList, hashComp);
67+
isSorted = true;
68+
}
69+
}
6770
}
Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.bouncycastle.tsp.ers;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.Comparator;
5-
import java.util.LinkedList;
66
import java.util.List;
7+
import java.util.NoSuchElementException;
78

89
/**
910
* A sorting list - byte[] are sorted in ascending order.
@@ -12,47 +13,38 @@ public class SortedIndexedHashList
1213
{
1314
private static final Comparator<byte[]> hashComp = new ByteArrayComparator();
1415

15-
private final LinkedList<IndexedHash> baseList = new LinkedList<IndexedHash>();
16+
private static final Comparator<IndexedHash> digestComp = new Comparator<IndexedHash>()
17+
{
18+
public int compare(IndexedHash l, IndexedHash r)
19+
{
20+
return hashComp.compare(l.digest, r.digest);
21+
}
22+
};
23+
24+
private final List<IndexedHash> baseList = new ArrayList<IndexedHash>();
25+
26+
private boolean isSorted = true;
1627

1728
public SortedIndexedHashList()
1829
{
1930
}
2031

2132
public IndexedHash getFirst()
2233
{
23-
return (IndexedHash)baseList.getFirst();
34+
if (baseList.isEmpty())
35+
{
36+
throw new NoSuchElementException();
37+
}
38+
39+
sort();
40+
41+
return (IndexedHash)baseList.get(0);
2442
}
2543

2644
public void add(IndexedHash hash)
2745
{
28-
if (baseList.size() == 0)
29-
{
30-
baseList.addFirst(hash);
31-
}
32-
else
33-
{
34-
if (hashComp.compare(hash.digest, ((IndexedHash)baseList.get(0)).digest) < 0)
35-
{
36-
baseList.addFirst(hash);
37-
}
38-
else
39-
{
40-
int index = 1;
41-
while(index < baseList.size() && hashComp.compare(((IndexedHash)baseList.get(index)).digest, hash.digest) <= 0)
42-
{
43-
index++;
44-
}
45-
46-
if (index == baseList.size())
47-
{
48-
baseList.add(hash);
49-
}
50-
else
51-
{
52-
baseList.add(index, hash);
53-
}
54-
}
55-
}
46+
baseList.add(hash);
47+
isSorted = false;
5648
}
5749

5850
public int size()
@@ -62,6 +54,22 @@ public int size()
6254

6355
public List<IndexedHash> toList()
6456
{
57+
sort();
58+
6559
return new ArrayList<IndexedHash>(baseList);
6660
}
61+
62+
/**
63+
* Sorting is deferred to the accessors, for the reason given on SortedHashList.sort():
64+
* finding the insertion point with LinkedList.get(index) made building a list of n hashes
65+
* O(n^3). Collections.sort() is stable, so hashes comparing equal keep ascending order.
66+
*/
67+
private void sort()
68+
{
69+
if (!isSorted)
70+
{
71+
Collections.sort(baseList, digestComp);
72+
isSorted = true;
73+
}
74+
}
6775
}

‎pkix/src/test/java/org/bouncycastle/tsp/test/ERSTest.java‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import java.util.Collections;
2020
import java.util.Date;
2121
import java.util.HashSet;
22+
import java.util.LinkedList;
2223
import java.util.List;
24+
import java.util.Random;
2325

2426
import junit.framework.TestCase;
2527
import org.bouncycastle.asn1.ASN1EncodableVector;
@@ -68,6 +70,7 @@
6870
import org.bouncycastle.tsp.ers.ERSException;
6971
import org.bouncycastle.tsp.ers.ERSFileData;
7072
import org.bouncycastle.tsp.ers.ERSInputStreamData;
73+
import org.bouncycastle.tsp.ers.SortedHashList;
7174
import org.bouncycastle.util.Arrays;
7275
import org.bouncycastle.util.Store;
7376
import org.bouncycastle.util.Strings;
@@ -1355,6 +1358,150 @@ private int compare(byte[] a, byte[] b)
13551358
return new BigInteger(1, a).compareTo(new BigInteger(1, b));
13561359
}
13571360

1361+
/**
1362+
* SortedHashList used to find each hash's insertion point by walking a LinkedList with
1363+
* get(index). The order it produced is the one recorded in every existing evidence record,
1364+
* so it is reproduced here from the original algorithm and compared against the list's
1365+
* output, over pseudo-random input including duplicates and arrays of differing lengths.
1366+
*/
1367+
public void testSortedHashListOrder()
1368+
{
1369+
Random random = new Random(0x5eed);
1370+
List<byte[]> input = new ArrayList<byte[]>();
1371+
1372+
for (int i = 0; i != 1000; i++)
1373+
{
1374+
byte[] value = new byte[random.nextInt(33)];
1375+
random.nextBytes(value);
1376+
input.add(value);
1377+
}
1378+
// duplicates, and values sharing a prefix with a longer one
1379+
for (int i = 0; i != 100; i++)
1380+
{
1381+
input.add((byte[])input.get(i));
1382+
input.add(Arrays.copyOfRange((byte[])input.get(i + 100), 0, ((byte[])input.get(i + 100)).length / 2));
1383+
}
1384+
1385+
SortedHashList list = new SortedHashList();
1386+
for (int i = 0; i != input.size(); i++)
1387+
{
1388+
list.add((byte[])input.get(i));
1389+
}
1390+
1391+
List<byte[]> expected = insertionSorted(input);
1392+
List<byte[]> actual = list.toList();
1393+
1394+
assertEquals(input.size(), list.size());
1395+
assertEquals(expected.size(), actual.size());
1396+
for (int i = 0; i != expected.size(); i++)
1397+
{
1398+
assertTrue("differs at " + i, Arrays.areEqual((byte[])expected.get(i), (byte[])actual.get(i)));
1399+
}
1400+
assertTrue(Arrays.areEqual((byte[])expected.get(0), list.getFirst()));
1401+
}
1402+
1403+
/**
1404+
* The order SortedHashList produced before sorting was deferred to the accessors.
1405+
*/
1406+
private List<byte[]> insertionSorted(List<byte[]> hashes)
1407+
{
1408+
LinkedList<byte[]> baseList = new LinkedList<byte[]>();
1409+
1410+
for (int h = 0; h != hashes.size(); h++)
1411+
{
1412+
byte[] hash = (byte[])hashes.get(h);
1413+
1414+
if (baseList.size() == 0)
1415+
{
1416+
baseList.addFirst(hash);
1417+
}
1418+
else if (compareUnsigned(hash, (byte[])baseList.get(0)) < 0)
1419+
{
1420+
baseList.addFirst(hash);
1421+
}
1422+
else
1423+
{
1424+
int index = 1;
1425+
while (index < baseList.size() && compareUnsigned((byte[])baseList.get(index), hash) <= 0)
1426+
{
1427+
index++;
1428+
}
1429+
1430+
if (index == baseList.size())
1431+
{
1432+
baseList.add(hash);
1433+
}
1434+
else
1435+
{
1436+
baseList.add(index, hash);
1437+
}
1438+
}
1439+
}
1440+
1441+
return baseList;
1442+
}
1443+
1444+
private int compareUnsigned(byte[] left, byte[] right)
1445+
{
1446+
for (int i = 0; i < left.length && i < right.length; i++)
1447+
{
1448+
int a = (left[i] & 0xff);
1449+
int b = (right[i] & 0xff);
1450+
1451+
if (a != b)
1452+
{
1453+
return a - b;
1454+
}
1455+
}
1456+
return left.length - right.length;
1457+
}
1458+
1459+
/**
1460+
* A reduced hash tree over a large number of data objects. This reaches both sorted lists -
1461+
* SortedIndexedHashList from ERSArchiveTimeStampGenerator.getPartialHashtrees(), and
1462+
* SortedHashList from BinaryTreeRootCalculator.computeRootHash() - and took about 2.5
1463+
* seconds for these 2,000 objects when the insertion point was found by walking a
1464+
* LinkedList, rising by roughly a factor of eight per doubling (10,000 objects took 347
1465+
* seconds). The root is also checked to be independent of the order the objects were added
1466+
* in, which is what the sorting is there for.
1467+
*/
1468+
public void testLargeDataObjectSet()
1469+
throws Exception
1470+
{
1471+
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
1472+
1473+
List<ERSData> dataObjects = new ArrayList<ERSData>();
1474+
for (int i = 0; i != 2000; i++)
1475+
{
1476+
dataObjects.add(new ERSByteData(Strings.toByteArray("document " + i)));
1477+
}
1478+
1479+
byte[] ascending = rootOf(dataObjects, digestCalculatorProvider);
1480+
1481+
List<ERSData> reversed = new ArrayList<ERSData>(dataObjects);
1482+
Collections.reverse(reversed);
1483+
1484+
assertTrue(Arrays.areEqual(ascending, rootOf(reversed, digestCalculatorProvider)));
1485+
}
1486+
1487+
private byte[] rootOf(List<ERSData> dataObjects, DigestCalculatorProvider digestCalculatorProvider)
1488+
throws Exception
1489+
{
1490+
ERSArchiveTimeStampGenerator ersGen = new ERSArchiveTimeStampGenerator(
1491+
digestCalculatorProvider.get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)));
1492+
1493+
for (int i = 0; i != dataObjects.size(); i++)
1494+
{
1495+
ersGen.addData((ERSData)dataObjects.get(i));
1496+
}
1497+
1498+
TimeStampRequestGenerator tspReqGen = new TimeStampRequestGenerator();
1499+
1500+
tspReqGen.setCertReq(true);
1501+
1502+
return ersGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest();
1503+
}
1504+
13581505
public void testReducedHashTrees()
13591506
throws Exception
13601507
{

0 commit comments

Comments
 (0)