• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanopub-registry / 22843263429

09 Mar 2026 07:41AM UTC coverage: 25.557% (+0.4%) from 25.137%
22843263429

push

github

web-flow
Merge pull request #78 from knowledgepixels/73-reject-future-timestamps

feat: reject nanopubs with future timestamps

131 of 592 branches covered (22.13%)

Branch coverage included in aggregate %.

477 of 1787 relevant lines covered (26.69%)

4.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.66
src/main/java/com/knowledgepixels/registry/RegistryDB.java
1
package com.knowledgepixels.registry;
2

3
import com.knowledgepixels.registry.db.IndexInitializer;
4
import com.mongodb.MongoClient;
5
import com.mongodb.MongoNamespace;
6
import com.mongodb.MongoWriteException;
7
import com.mongodb.ServerAddress;
8
import com.mongodb.client.ClientSession;
9
import com.mongodb.client.MongoCollection;
10
import com.mongodb.client.MongoCursor;
11
import com.mongodb.client.MongoDatabase;
12
import com.mongodb.client.model.CountOptions;
13
import com.mongodb.client.model.UpdateOptions;
14
import net.trustyuri.TrustyUriUtils;
15
import org.bson.Document;
16
import org.bson.conversions.Bson;
17
import org.bson.types.Binary;
18
import org.eclipse.rdf4j.common.exception.RDF4JException;
19
import org.eclipse.rdf4j.model.IRI;
20
import org.eclipse.rdf4j.rio.RDFFormat;
21
import org.nanopub.MalformedNanopubException;
22
import org.nanopub.Nanopub;
23

24
import java.util.Calendar;
25
import org.nanopub.NanopubUtils;
26
import org.nanopub.extra.security.MalformedCryptoElementException;
27
import org.nanopub.extra.security.NanopubSignatureElement;
28
import org.nanopub.extra.security.SignatureUtils;
29
import org.nanopub.jelly.JellyUtils;
30
import org.slf4j.Logger;
31
import org.slf4j.LoggerFactory;
32

33
import java.io.IOException;
34
import java.security.GeneralSecurityException;
35
import java.util.ArrayList;
36

37
import static com.mongodb.client.model.Indexes.ascending;
38

39
public class RegistryDB {
40

41
    private RegistryDB() {
42
    }
43

44
    private static final String REGISTRY_DB_NAME = Utils.getEnv("REGISTRY_DB_NAME", "nanopubRegistry");
12✔
45

46
    private static final Logger logger = LoggerFactory.getLogger(RegistryDB.class);
9✔
47

48
    private static MongoClient mongoClient;
49
    private static MongoDatabase mongoDB;
50

51
    /**
52
     * Returns the MongoDB database instance.
53
     *
54
     * @return the MongoDatabase instance
55
     */
56
    public static MongoDatabase getDB() {
57
        return mongoDB;
6✔
58
    }
59

60
    /**
61
     * Returns the MongoDB client instance.
62
     *
63
     * @return the MongoClient instance
64
     */
65
    public static MongoClient getClient() {
66
        return mongoClient;
6✔
67
    }
68

69
    /**
70
     * Returns the specified collection from the MongoDB database.
71
     *
72
     * @param name the name of the collection
73
     * @return the MongoCollection instance
74
     */
75
    public static MongoCollection<Document> collection(String name) {
76
        return mongoDB.getCollection(name);
12✔
77
    }
78

79
    /**
80
     * Initializes the MongoDB connection and sets up collections and indexes if not already initialized.
81
     */
82
    public static void init() {
83
        if (mongoClient != null) {
6✔
84
            logger.info("RegistryDB already initialized");
9✔
85
            return;
3✔
86
        }
87
        final String REGISTRY_DB_HOST = Utils.getEnv("REGISTRY_DB_HOST", "mongodb");
12✔
88
        final int REGISTRY_DB_PORT = Integer.parseInt(Utils.getEnv("REGISTRY_DB_PORT", String.valueOf(ServerAddress.defaultPort())));
18✔
89
        logger.info("Initializing RegistryDB connection to database '{}' at {}:{}", REGISTRY_DB_NAME, REGISTRY_DB_HOST, REGISTRY_DB_PORT);
54✔
90
        mongoClient = new MongoClient(REGISTRY_DB_HOST, REGISTRY_DB_PORT);
18✔
91
        mongoDB = mongoClient.getDatabase(REGISTRY_DB_NAME);
12✔
92

93
        try (ClientSession mongoSession = mongoClient.startSession()) {
9✔
94
            if (isInitialized(mongoSession)) {
9!
95
                return;
×
96
            }
97
            IndexInitializer.initCollections(mongoSession);
6✔
98
        }
×
99
    }
3✔
100

101
    /**
102
     * Checks if the database has been initialized.
103
     *
104
     * @param mongoSession the MongoDB client session
105
     * @return true if initialized, false otherwise
106
     */
107
    public static boolean isInitialized(ClientSession mongoSession) {
108
        return getValue(mongoSession, Collection.SERVER_INFO.toString(), "setupId") != null;
24!
109
    }
110

111
    /**
112
     * Renames a collection in the database. If the new collection name already exists, it will be dropped first.
113
     *
114
     * @param oldCollectionName the current name of the collection
115
     * @param newCollectionName the new name for the collection
116
     */
117
    public static void rename(String oldCollectionName, String newCollectionName) {
118
        // Designed as idempotent operation: calling multiple times has same effect as calling once
119
        if (hasCollection(oldCollectionName)) {
9✔
120
            if (hasCollection(newCollectionName)) {
9✔
121
                collection(newCollectionName).drop();
9✔
122
            }
123
            collection(oldCollectionName).renameCollection(new MongoNamespace(REGISTRY_DB_NAME, newCollectionName));
24✔
124
        }
125
    }
3✔
126

127
    /**
128
     * Checks if a collection with the given name exists in the database.
129
     *
130
     * @param collectionName the name of the collection to check
131
     * @return true if the collection exists, false otherwise
132
     */
133
    public static boolean hasCollection(String collectionName) {
134
        return mongoDB.listCollectionNames().into(new ArrayList<>()).contains(collectionName);
30✔
135
    }
136

137
    /**
138
     * Increases the trust state counter in the server info collection.
139
     *
140
     * @param mongoSession the MongoDB client session
141
     */
142
    public static void increaseStateCounter(ClientSession mongoSession) {
143
        MongoCursor<Document> cursor = collection(Collection.SERVER_INFO.toString()).find(mongoSession, new Document("_id", "trustStateCounter")).cursor();
36✔
144
        if (cursor.hasNext()) {
9✔
145
            long counter = cursor.next().getLong("value");
21✔
146
            collection(Collection.SERVER_INFO.toString()).updateOne(mongoSession, new Document("_id", "trustStateCounter"), new Document("$set", new Document("value", counter + 1)));
69✔
147
        } else {
3✔
148
            collection(Collection.SERVER_INFO.toString()).insertOne(mongoSession, new Document("_id", "trustStateCounter").append("value", 0L));
42✔
149
        }
150
    }
3✔
151

152
    /**
153
     * Checks if an element with the given name exists in the specified collection.
154
     *
155
     * @param mongoSession the MongoDB client session
156
     * @param collection   the name of the collection
157
     * @param elementName  the name of the element used as the _id field
158
     * @return true if the element exists, false otherwise
159
     */
160
    public static boolean has(ClientSession mongoSession, String collection, String elementName) {
161
        return has(mongoSession, collection, new Document("_id", elementName));
27✔
162
    }
163

164
    private static final CountOptions hasCountOptions = new CountOptions().limit(1);
21✔
165

166
    /**
167
     * Checks if any document matching the given filter exists in the specified collection.
168
     *
169
     * @param mongoSession the MongoDB client session
170
     * @param collection   the name of the collection
171
     * @param find         the filter to match documents
172
     * @return true if at least one matching document exists, false otherwise
173
     */
174
    public static boolean has(ClientSession mongoSession, String collection, Bson find) {
175
        return collection(collection).countDocuments(mongoSession, find, hasCountOptions) > 0;
39✔
176
    }
177

178
    /**
179
     * Retrieves a cursor for documents matching the given filter in the specified collection.
180
     *
181
     * @param mongoSession the MongoDB client session
182
     * @param collection   the name of the collection
183
     * @param find         the filter to match documents
184
     * @return a MongoCursor for the matching documents
185
     */
186
    public static MongoCursor<Document> get(ClientSession mongoSession, String collection, Bson find) {
187
        return collection(collection).find(mongoSession, find).cursor();
21✔
188
    }
189

190
    /**
191
     * Retrieves the value of an element with the given name from the specified collection.
192
     *
193
     * @param mongoSession the MongoDB client session
194
     * @param collection   the name of the collection
195
     * @param elementName  the name of the element used as the _id field
196
     * @return the value of the element, or null if not found
197
     */
198
    public static Object getValue(ClientSession mongoSession, String collection, String elementName) {
199
        logger.info("Getting value of element '{}' from collection '{}'", elementName, collection);
15✔
200
        Document d = collection(collection).find(mongoSession, new Document("_id", elementName)).first();
36✔
201
        if (d == null) {
6✔
202
            return null;
6✔
203
        }
204
        return d.get("value");
12✔
205
    }
206

207
    /**
208
     * Retrieves the boolean value of an element with the given name from the specified collection.
209
     *
210
     * @param mongoSession the MongoDB client session
211
     * @param collection   the name of the collection
212
     * @param elementName  the name of the element used as the _id field
213
     * @return the value of the element, or null if not found
214
     */
215
    public static boolean isSet(ClientSession mongoSession, String collection, String elementName) {
216
        Document d = collection(collection).find(mongoSession, new Document("_id", elementName)).first();
36✔
217
        if (d == null) {
6✔
218
            return false;
6✔
219
        }
220
        return d.getBoolean("value");
15✔
221
    }
222

223
    /**
224
     * Retrieves a single document matching the given filter from the specified collection.
225
     *
226
     * @param mongoSession the MongoDB client session
227
     * @param collection   the name of the collection
228
     * @param find         the filter to match the document
229
     * @return the matching document, or null if not found
230
     */
231
    public static Document getOne(ClientSession mongoSession, String collection, Bson find) {
232
        return collection(collection).find(mongoSession, find).first();
24✔
233
    }
234

235
    /**
236
     * Retrieves the maximum value of a specified field from the documents in the given collection.
237
     *
238
     * @param mongoSession the MongoDB client session
239
     * @param collection   the name of the collection
240
     * @param fieldName    the field for which to find the maximum value
241
     * @return the maximum value of the specified field, or null if no documents exist
242
     */
243
    public static Object getMaxValue(ClientSession mongoSession, String collection, String fieldName) {
244
        Document doc = collection(collection).find(mongoSession).projection(new Document(fieldName, 1)).sort(new Document(fieldName, -1)).first();
63✔
245
        if (doc == null) {
6✔
246
            return null;
6✔
247
        }
248
        return doc.get(fieldName);
12✔
249
    }
250

251
    /**
252
     * Retrieves the document with the maximum value of a specified field from the documents matching the given filter in the specified collection.
253
     *
254
     * @param mongoSession the MongoDB client session
255
     * @param collection   the name of the collection
256
     * @param find         the filter to match documents
257
     * @param fieldName    the field for which to find the maximum value
258
     * @return the document with the maximum value of the specified field, or null if no matching documents exist
259
     */
260
    public static Document getMaxValueDocument(ClientSession mongoSession, String collection, Bson find, String fieldName) {
261
        return collection(collection).find(mongoSession, find).sort(new Document(fieldName, -1)).first();
45✔
262
    }
263

264
    /**
265
     * Sets or updates a document in the specified collection.
266
     *
267
     * @param mongoSession the MongoDB client session
268
     * @param collection   the name of the collection
269
     * @param update       the document to set or update (must contain an _id field)
270
     */
271
    public static void set(ClientSession mongoSession, String collection, Document update) {
272
        Bson find = new Document("_id", update.get("_id"));
24✔
273
        MongoCursor<Document> cursor = collection(collection).find(mongoSession, find).cursor();
21✔
274
        if (cursor.hasNext()) {
9✔
275
            collection(collection).updateOne(mongoSession, find, new Document("$set", update));
33✔
276
        }
277
    }
3✔
278

279
    /**
280
     * Inserts a document into the specified collection.
281
     *
282
     * @param mongoSession the MongoDB client session
283
     * @param collection   the name of the collection
284
     * @param doc          the document to insert
285
     */
286
    public static void insert(ClientSession mongoSession, String collection, Document doc) {
287
        collection(collection).insertOne(mongoSession, doc);
15✔
288
    }
3✔
289

290
    /**
291
     * Sets the value of an element with the given name in the specified collection.
292
     * If the element does not exist, it will be created.
293
     *
294
     * @param mongoSession the MongoDB client session
295
     * @param collection   the name of the collection
296
     * @param elementId    the name of the element used as the _id field
297
     * @param value        the value to set
298
     */
299
    public static void setValue(ClientSession mongoSession, String collection, String elementId, Object value) {
300
        collection(collection).updateOne(mongoSession, new Document("_id", elementId), new Document("$set", new Document("value", value)), new UpdateOptions().upsert(true));
72✔
301
    }
3✔
302

303
    /**
304
     * Records the hash of a given value in the "hashes" collection.
305
     * If the hash already exists, it will be ignored.
306
     *
307
     * @param mongoSession the MongoDB client session
308
     * @param value        the value to hash and record
309
     */
310
    public static void recordHash(ClientSession mongoSession, String value) {
311
        try {
312
            insert(mongoSession, "hashes", new Document("value", value).append("hash", Utils.getHash(value)));
36✔
313
        } catch (MongoWriteException e) {
×
314
            // Duplicate key error -- ignore it
315
            if (e.getError().getCode() != 11000) throw e;
×
316
        }
3✔
317
    }
3✔
318

319
    /**
320
     * Retrieves the original value corresponding to a given hash from the "hashes" collection.
321
     *
322
     * @param hash the hash to look up
323
     * @return the original value, or null if not found
324
     */
325
    public static String unhash(String hash) {
326
        try (var c = collection("hashes").find(new Document("hash", hash)).cursor()) {
30✔
327
            if (c.hasNext()) {
9✔
328
                return c.next().getString("value");
24✔
329
            }
330
            return null;
12✔
331
        }
12!
332
    }
333

334
    /**
335
     * Loads a nanopublication into the database.
336
     *
337
     * @param mongoSession the MongoDB client session
338
     * @param nanopub      the nanopublication to load
339
     */
340
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub) {
341
        return loadNanopub(mongoSession, nanopub, null);
21✔
342
    }
343

344
    /**
345
     * Loads a nanopublication into the database, optionally filtering by public key hash and types.
346
     *
347
     * @param mongoSession the MongoDB client session
348
     * @param nanopub      the nanopublication to load
349
     * @param pubkeyHash   the public key hash to filter by (can be null)
350
     * @param types        the types to filter by (can be empty)
351
     * @return true if the nanopublication was loaded, false otherwise
352
     */
353
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String... types) {
354
        if (nanopub.getTripleCount() > 1200) {
12!
355
            logger.info("Nanopub has too many triples ({}): {}", nanopub.getTripleCount(), nanopub.getUri());
×
356
            return false;
×
357
        }
358
        if (nanopub.getByteCount() > 1000000) {
15!
359
            logger.info("Nanopub is too large ({}): {}", nanopub.getByteCount(), nanopub.getUri());
×
360
            return false;
×
361
        }
362
        Calendar creationTime = nanopub.getCreationTime();
9✔
363
        if (creationTime != null && creationTime.getTimeInMillis() > System.currentTimeMillis() + 60000) {
27✔
364
            logger.info("Nanopub has a future timestamp: {}", nanopub.getUri());
15✔
365
            return false;
6✔
366
        }
367
        String pubkey = getPubkey(nanopub);
9✔
368
        if (pubkey == null) {
6✔
369
            logger.info("Ignoring invalid nanopub: {}", nanopub.getUri());
15✔
370
            return false;
6✔
371
        }
372
        String ph = Utils.getHash(pubkey);
9✔
373
        if (pubkeyHash != null && !pubkeyHash.equals(ph)) {
6!
374
            logger.info("Ignoring nanopub with non-matching pubkey: {}", nanopub.getUri());
×
375
            return false;
×
376
        }
377
        recordHash(mongoSession, pubkey);
9✔
378

379
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
15✔
380
        if (ac == null) {
6!
381
            // I don't think this ever happens, but checking here to be sure
382
            logger.info("ERROR. Unexpected Trusty URI: {}", nanopub.getUri());
×
383
            return false;
×
384
        }
385
        if (has(mongoSession, Collection.NANOPUBS.toString(), ac)) {
18!
386
            logger.info("Already loaded: {}", nanopub.getUri());
×
387
        } else {
388
            Long counter = (Long) getMaxValue(mongoSession, Collection.NANOPUBS.toString(), "counter");
21✔
389
            if (counter == null) counter = 0l;
15!
390
            String nanopubString;
391
            byte[] jellyContent;
392
            try {
393
                nanopubString = NanopubUtils.writeToString(nanopub, RDFFormat.TRIG);
12✔
394
                // Save the same thing in the Jelly format for faster loading
395
                jellyContent = JellyUtils.writeNanopubForDB(nanopub);
9✔
396
            } catch (IOException ex) {
×
397
                throw new RuntimeException(ex);
×
398
            }
3✔
399
            collection(Collection.NANOPUBS.toString()).insertOne(mongoSession, new Document("_id", ac).append("fullId", nanopub.getUri().stringValue()).append("counter", counter + 1).append("pubkey", ph).append("content", nanopubString).append("jelly", new Binary(jellyContent)));
102✔
400

401
            for (IRI invalidatedId : Utils.getInvalidatedNanopubIds(nanopub)) {
33✔
402
                String invalidatedAc = TrustyUriUtils.getArtifactCode(invalidatedId.stringValue());
12✔
403
                if (invalidatedAc == null) continue;  // This should never happen; checking here just to be sure
6!
404

405
                // Add this nanopub also to all lists of invalidated nanopubs:
406
                collection("invalidations").insertOne(mongoSession, new Document("invalidatingNp", ac).append("invalidatingPubkey", ph).append("invalidatedNp", invalidatedAc));
45✔
407
                MongoCursor<Document> invalidatedEntries = collection("listEntries").find(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph)).cursor();
42✔
408
                while (invalidatedEntries.hasNext()) {
9!
409
                    Document invalidatedEntry = invalidatedEntries.next();
×
410
                    addToList(mongoSession, nanopub, ph, invalidatedEntry.getString("type"));
×
411
                }
×
412

413
                collection("listEntries").updateMany(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
69✔
414
                collection("trustEdges").updateMany(mongoSession, new Document("source", invalidatedAc), new Document("$set", new Document("invalidated", true)));
60✔
415
            }
3✔
416
        }
417

418
        if (pubkeyHash != null) {
6!
419
            for (String type : types) {
×
420
                // TODO Check if nanopub really has the type?
421
                addToList(mongoSession, nanopub, pubkeyHash, Utils.getTypeHash(mongoSession, type));
×
422
                if (type.equals("$")) {
×
423
                    for (IRI t : NanopubUtils.getTypes(nanopub)) {
×
424
                        addToList(mongoSession, nanopub, pubkeyHash, Utils.getTypeHash(mongoSession, t));
×
425
                    }
×
426
                }
427
            }
428
        }
429

430
        // Add the invalidating nanopubs also to the lists of this nanopub:
431
        try (MongoCursor<Document> invalidations = collection("invalidations").find(mongoSession, new Document("invalidatedNp", ac).append("invalidatingPubkey", ph)).cursor()) {
42✔
432
            if (invalidations.hasNext()) {
9!
433
                collection("listEntries").updateMany(mongoSession, new Document("np", ac).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
×
434
                collection("trustEdges").updateMany(mongoSession, new Document("source", ac), new Document("$set", new Document("invalidated", true)));
×
435
            }
436
            while (invalidations.hasNext()) {
9!
437
                String iac = invalidations.next().getString("invalidatingNp");
×
438
                try {
439
                    Document npDoc = collection(Collection.NANOPUBS.toString()).find(mongoSession, new Document("_id", iac)).projection(new Document("jelly", 1)).first();
×
440
                    Nanopub inp = JellyUtils.readFromDB(npDoc.get("jelly", Binary.class).getData());
×
441
                    for (IRI type : NanopubUtils.getTypes(inp)) {
×
442
                        addToList(mongoSession, inp, ph, Utils.getTypeHash(mongoSession, type));
×
443
                    }
×
444
                } catch (RDF4JException | MalformedNanopubException ex) {
×
445
                    ex.printStackTrace();
×
446
                }
×
447
            }
×
448

449
        }
450

451
        return true;
6✔
452
    }
453

454
    private static void addToList(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String typeHash) {
455
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
×
456
        try {
457
            insert(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", typeHash));
×
458
        } catch (MongoWriteException e) {
×
459
            // Duplicate key error -- ignore it
460
            if (e.getError().getCode() != 11000) throw e;
×
461
        }
×
462

463
        if (has(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash).append("np", ac))) {
×
464
            logger.info("Already listed: {}", nanopub.getUri());
×
465
        } else {
466

467
            Document doc = getMaxValueDocument(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash), "position");
×
468
            long position;
469
            String checksum;
470
            if (doc == null) {
×
471
                position = 0l;
×
472
                checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), NanopubUtils.INIT_CHECKSUM);
×
473
            } else {
474
                position = doc.getLong("position") + 1;
×
475
                checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), doc.getString("checksum"));
×
476
            }
477
            collection("listEntries").insertOne(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("position", position).append("np", ac).append("checksum", checksum).append("invalidated", false));
×
478
        }
479
    }
×
480

481
    /**
482
     * Returns the public key string of the Nanopub's signature, or null if not available or invalid.
483
     *
484
     * @param nanopub the nanopub to extract the public key from
485
     * @return The public key string, or null if not available or invalid.
486
     */
487
    public static String getPubkey(Nanopub nanopub) {
488
        // TODO shouldn't this be moved to a utility class in nanopub-java? there is a similar method in NanopubElement class of nanodash
489
        NanopubSignatureElement el;
490
        try {
491
            el = SignatureUtils.getSignatureElement(nanopub);
9✔
492
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
24!
493
                return el.getPublicKeyString();
9✔
494
            }
495
        } catch (MalformedCryptoElementException | GeneralSecurityException ex) {
×
496
            logger.error("Error in checking the signature of the nanopub {}", nanopub.getUri());
×
497
        }
3✔
498
        return null;
6✔
499
    }
500

501
    /**
502
     * Calculates a hash representing the current state of the trust paths in the loading collection.
503
     *
504
     * @param mongoSession the MongoDB client session
505
     * @return the calculated trust state hash
506
     */
507
    public static String calculateTrustStateHash(ClientSession mongoSession) {
508
        MongoCursor<Document> tp = collection("trustPaths_loading").find(mongoSession).sort(ascending("_id")).cursor();
×
509
        // TODO Improve this so we don't create the full string just for calculating its hash:
510
        String s = "";
×
511
        while (tp.hasNext()) {
×
512
            Document d = tp.next();
×
513
            s += d.getString("_id") + " (" + d.getString("type") + ")\n";
×
514
        }
×
515
        return Utils.getHash(s);
×
516
    }
517

518
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc