Skip to content

Commit af132f4

Browse files
dejmekradimdghgit
authored andcommitted
ERS: build the sorted hash lists by collecting and sorting once rather than finding each insertion point by walking a LinkedList with get(index), incorporating github PR #2457, relates to github #2456.
1 parent cc6ba6c commit af132f4

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)