Added "identifier" property to arbitrary transactions

Until now we have been limited to one data resource per name/service combination. This meant that each name could only have a single website, git repo, image, video, etc, and adding another would overwrite the previous data. The identifier property now allows an optional string to be supplied with each resource, therefore allowing an unlimited amount of resources per name/service combination.

Some examples of what this will allow us to do:

- Create a video library app which holds multiple videos per name
- Same as above but for photos
- Store multiple images against each name, such as an avatar, website thumbnails, video thumbnails, etc. This will be necessary for many "system level" features.
- Attach multiple websites to each name. The default website (with blank/null identifier) would remain the entry point, but other websites could be hosted essentially as subdomains, and then linked from the default site. This also provides a means to go beyond the 500MB website size limit.

Not all of these features will exist initially, but having this identifier included in the protocol layer allows them to be added at any time.
This commit is contained in:
CalDescent
2021-11-07 18:39:43 +00:00
parent a0fe1a85f1
commit 991125034e
11 changed files with 88 additions and 49 deletions

View File

@@ -353,11 +353,12 @@ public class ArbitraryResource {
)
public String post(@PathParam("service") String serviceString,
@PathParam("name") String name,
@QueryParam("identifier") String identifier,
String path) {
Security.checkApiCallAllowed(request);
// TODO: automatic PUT/PATCH
return this.upload(Method.PUT, Service.valueOf(serviceString), name, path);
return this.upload(Method.PUT, Service.valueOf(serviceString), name, identifier, path);
}
@PUT
@@ -388,10 +389,11 @@ public class ArbitraryResource {
)
public String put(@PathParam("service") String serviceString,
@PathParam("name") String name,
@QueryParam("identifier") String identifier,
String path) {
Security.checkApiCallAllowed(request);
return this.upload(Method.PUT, Service.valueOf(serviceString), name, path);
return this.upload(Method.PUT, Service.valueOf(serviceString), name, identifier, path);
}
@PATCH
@@ -422,14 +424,15 @@ public class ArbitraryResource {
}
)
public String patch(@PathParam("service") String serviceString,
@PathParam("name") String name,
@PathParam("name") String name,
@QueryParam("identifier") String identifier,
String path) {
Security.checkApiCallAllowed(request);
return this.upload(Method.PATCH, Service.valueOf(serviceString), name, path);
return this.upload(Method.PATCH, Service.valueOf(serviceString), name, identifier, path);
}
private String upload(Method method, Service service, String name, String path) {
private String upload(Method method, Service service, String name, String identifier, String path) {
// It's too dangerous to allow user-supplied file paths in weaker security contexts
if (Settings.getInstance().isApiRestricted()) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
@@ -452,7 +455,7 @@ public class ArbitraryResource {
try {
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
publicKey58, Paths.get(path), name, method, service
publicKey58, Paths.get(path), name, method, service, identifier
);
ArbitraryTransactionData transactionData = transactionBuilder.build();

View File

@@ -36,13 +36,16 @@ public class ArbitraryDataTransactionBuilder {
private String name;
private Method method;
private Service service;
private String identifier;
public ArbitraryDataTransactionBuilder(String publicKey58, Path path, String name, Method method, Service service) {
public ArbitraryDataTransactionBuilder(String publicKey58, Path path, String name,
Method method, Service service, String identifier) {
this.publicKey58 = publicKey58;
this.path = path;
this.name = name;
this.method = method;
this.service = service;
this.identifier = identifier;
}
public ArbitraryTransactionData build() throws DataException {
@@ -97,7 +100,7 @@ public class ArbitraryDataTransactionBuilder {
final List<PaymentData> payments = new ArrayList<>();
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, service, nonce, size, name, method,
version, service, nonce, size, name, identifier, method,
secret, compression, digest, dataType, chunkHashes, payments);
ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData);

View File

@@ -100,6 +100,7 @@ public class ArbitraryTransactionData extends TransactionData {
private int size;
private String name;
private String identifier;
private Method method;
private byte[] secret;
private Compression compression;
@@ -125,7 +126,7 @@ public class ArbitraryTransactionData extends TransactionData {
public ArbitraryTransactionData(BaseTransactionData baseTransactionData,
int version, Service service, int nonce, int size,
String name, Method method, byte[] secret, Compression compression,
String name, String identifier, Method method, byte[] secret, Compression compression,
byte[] data, DataType dataType, byte[] chunkHashes, List<PaymentData> payments) {
super(TransactionType.ARBITRARY, baseTransactionData);
@@ -135,6 +136,7 @@ public class ArbitraryTransactionData extends TransactionData {
this.nonce = nonce;
this.size = size;
this.name = name;
this.identifier = identifier;
this.method = method;
this.secret = secret;
this.compression = compression;
@@ -174,6 +176,10 @@ public class ArbitraryTransactionData extends TransactionData {
return this.name;
}
public String getIdentifier() {
return this.identifier;
}
public Method getMethod() {
return this.method;
}

View File

@@ -157,7 +157,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
String sql = "SELECT type, reference, signature, creator, created_when, fee, " +
"tx_group_id, block_height, approval_status, approval_height, " +
"version, nonce, service, size, is_data_raw, data, chunk_hashes, " +
"name, update_method, secret, compression FROM ArbitraryTransactions " +
"name, identifier, update_method, secret, compression FROM ArbitraryTransactions " +
"JOIN Transactions USING (signature) " +
"WHERE lower(name) = ? AND service = ? AND created_when >= ? " +
"ORDER BY created_when ASC";
@@ -201,14 +201,15 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
byte[] data = resultSet.getBytes(16);
byte[] chunkHashes = resultSet.getBytes(17);
String nameResult = resultSet.getString(18);
Method method = Method.valueOf(resultSet.getInt(19));
byte[] secret = resultSet.getBytes(20);
Compression compression = Compression.valueOf(resultSet.getInt(21));
String identifierResult = resultSet.getString(19);
Method method = Method.valueOf(resultSet.getInt(20));
byte[] secret = resultSet.getBytes(21);
Compression compression = Compression.valueOf(resultSet.getInt(22));
List<PaymentData> payments = new ArrayList<>(); // TODO: this.getPaymentsFromSignature(baseTransactionData.getSignature());
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, serviceResult, nonce, size, nameResult, method, secret, compression, data,
dataType, chunkHashes, payments);
version, serviceResult, nonce, size, nameResult, identifierResult, method, secret,
compression, data, dataType, chunkHashes, payments);
arbitraryTransactionData.add(transactionData);
} while (resultSet.next());
@@ -226,7 +227,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
sql.append("SELECT type, reference, signature, creator, created_when, fee, " +
"tx_group_id, block_height, approval_status, approval_height, " +
"version, nonce, service, size, is_data_raw, data, chunk_hashes, " +
"name, update_method, secret, compression FROM ArbitraryTransactions " +
"name, identifier, update_method, secret, compression FROM ArbitraryTransactions " +
"JOIN Transactions USING (signature) " +
"WHERE lower(name) = ? AND service = ?");
@@ -274,14 +275,15 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
byte[] data = resultSet.getBytes(16);
byte[] chunkHashes = resultSet.getBytes(17);
String nameResult = resultSet.getString(18);
Method methodResult = Method.valueOf(resultSet.getInt(19));
byte[] secret = resultSet.getBytes(20);
Compression compression = Compression.valueOf(resultSet.getInt(21));
String identifierResult = resultSet.getString(19);
Method methodResult = Method.valueOf(resultSet.getInt(20));
byte[] secret = resultSet.getBytes(21);
Compression compression = Compression.valueOf(resultSet.getInt(22));
List<PaymentData> payments = new ArrayList<>(); // TODO: this.getPaymentsFromSignature(baseTransactionData.getSignature());
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, serviceResult, nonce, size, nameResult, methodResult, secret, compression, data,
dataType, chunkHashes, payments);
version, serviceResult, nonce, size, nameResult, identifierResult, methodResult, secret,
compression, data, dataType, chunkHashes, payments);
return transactionData;
} catch (SQLException e) {

View File

@@ -920,6 +920,8 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE ArbitraryTransactions ADD secret VARBINARY(32)");
// We want to support compressed and uncompressed data, as well as different compression algorithms
stmt.execute("ALTER TABLE ArbitraryTransactions ADD compression INTEGER NOT NULL DEFAULT 0");
// An optional identifier string can be used to allow more than one resource per user/service combo
stmt.execute("ALTER TABLE ArbitraryTransactions ADD identifier VARCHAR(64)");
// For finding transactions by registered name
stmt.execute("CREATE INDEX ArbitraryNameIndex ON ArbitraryTransactions (name)");
break;

View File

@@ -21,7 +21,8 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT version, nonce, service, size, is_data_raw, data, chunk_hashes, " +
"name, update_method, secret, compression from ArbitraryTransactions WHERE signature = ?";
"name, identifier, update_method, secret, compression from ArbitraryTransactions " +
"WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
@@ -36,13 +37,14 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
byte[] data = resultSet.getBytes(6);
byte[] chunkHashes = resultSet.getBytes(7);
String name = resultSet.getString(8);
ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.valueOf(resultSet.getInt(9));
byte[] secret = resultSet.getBytes(10);
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.valueOf(resultSet.getInt(11));
String identifier = resultSet.getString(9);
ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.valueOf(resultSet.getInt(10));
byte[] secret = resultSet.getBytes(11);
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.valueOf(resultSet.getInt(12));
List<PaymentData> payments = this.getPaymentsFromSignature(baseTransactionData.getSignature());
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name, method,
secret, compression, data, dataType, chunkHashes, payments);
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name,
identifier, method, secret, compression, data, dataType, chunkHashes, payments);
} catch (SQLException e) {
throw new DataException("Unable to fetch arbitrary transaction from repository", e);
}
@@ -63,8 +65,8 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
.bind("nonce", arbitraryTransactionData.getNonce()).bind("size", arbitraryTransactionData.getSize())
.bind("is_data_raw", arbitraryTransactionData.getDataType() == DataType.RAW_DATA).bind("data", arbitraryTransactionData.getData())
.bind("chunk_hashes", arbitraryTransactionData.getChunkHashes()).bind("name", arbitraryTransactionData.getName())
.bind("update_method", arbitraryTransactionData.getMethod().value).bind("secret", arbitraryTransactionData.getSecret())
.bind("compression", arbitraryTransactionData.getCompression().value);
.bind("identifier", arbitraryTransactionData.getIdentifier()).bind("update_method", arbitraryTransactionData.getMethod().value)
.bind("secret", arbitraryTransactionData.getSecret()).bind("compression", arbitraryTransactionData.getCompression().value);
try {
saveHelper.execute(this.repository);

View File

@@ -35,6 +35,7 @@ public class ArbitraryTransaction extends Transaction {
public static final int POW_MIN_DIFFICULTY = 12; // leading zero bits
public static final int POW_MAX_DIFFICULTY = 19; // leading zero bits
public static final long MAX_FILE_SIZE = ArbitraryDataFile.MAX_FILE_SIZE;
public static final int MAX_IDENTIFIER_LENGTH = 64;
// Constructors

View File

@@ -35,14 +35,15 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
private static final int CHUNKS_SIZE_LENGTH = INT_LENGTH;
private static final int NUMBER_PAYMENTS_LENGTH = INT_LENGTH;
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
private static final int IDENTIFIER_SIZE_LENGTH = INT_LENGTH;
private static final int COMPRESSION_LENGTH = INT_LENGTH;
private static final int METHOD_LENGTH = INT_LENGTH;
private static final int SECRET_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = SERVICE_LENGTH + DATA_TYPE_LENGTH + DATA_SIZE_LENGTH;
private static final int EXTRAS_V5_LENGTH = NONCE_LENGTH + NAME_SIZE_LENGTH + METHOD_LENGTH + SECRET_LENGTH +
COMPRESSION_LENGTH + RAW_DATA_SIZE_LENGTH + CHUNKS_SIZE_LENGTH;
private static final int EXTRAS_V5_LENGTH = NONCE_LENGTH + NAME_SIZE_LENGTH + IDENTIFIER_SIZE_LENGTH +
METHOD_LENGTH + SECRET_LENGTH + COMPRESSION_LENGTH + RAW_DATA_SIZE_LENGTH + CHUNKS_SIZE_LENGTH;
protected static final TransactionLayout layout;
@@ -57,6 +58,8 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
layout.add("name length", TransformationType.INT); // Version 5+
layout.add("name", TransformationType.DATA); // Version 5+
layout.add("identifier length", TransformationType.INT); // Version 5+
layout.add("identifier", TransformationType.DATA); // Version 5+
layout.add("method", TransformationType.INT); // Version 5+
layout.add("secret length", TransformationType.INT); // Version 5+
layout.add("secret", TransformationType.DATA); // Version 5+
@@ -94,6 +97,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
int nonce = 0;
String name = null;
String identifier = null;
ArbitraryTransactionData.Method method = null;
byte[] secret = null;
ArbitraryTransactionData.Compression compression = null;
@@ -103,6 +107,8 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE);
identifier = Serialization.deserializeSizedString(byteBuffer, ArbitraryTransaction.MAX_IDENTIFIER_LENGTH);
method = ArbitraryTransactionData.Method.valueOf(byteBuffer.getInt());
int secretLength = byteBuffer.getInt();
@@ -160,18 +166,20 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name, method, secret, compression, data, dataType, chunkHashes, payments);
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name, identifier,
method, secret, compression, data, dataType, chunkHashes, payments);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
int nameLength = (arbitraryTransactionData.getName() != null) ? Utf8.encodedLength(arbitraryTransactionData.getName()) : 0;
int identifierLength = (arbitraryTransactionData.getIdentifier() != null) ? Utf8.encodedLength(arbitraryTransactionData.getIdentifier()) : 0;
int secretLength = (arbitraryTransactionData.getSecret() != null) ? arbitraryTransactionData.getSecret().length : 0;
int dataLength = (arbitraryTransactionData.getData() != null) ? arbitraryTransactionData.getData().length : 0;
int chunkHashesLength = (arbitraryTransactionData.getChunkHashes() != null) ? arbitraryTransactionData.getChunkHashes().length : 0;
int length = getBaseLength(transactionData) + EXTRAS_LENGTH + nameLength + secretLength + dataLength + chunkHashesLength;
int length = getBaseLength(transactionData) + EXTRAS_LENGTH + nameLength + identifierLength + secretLength + dataLength + chunkHashesLength;
if (arbitraryTransactionData.getVersion() >= 5) {
length += EXTRAS_V5_LENGTH;
@@ -196,6 +204,8 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
Serialization.serializeSizedString(bytes, arbitraryTransactionData.getName());
Serialization.serializeSizedString(bytes, arbitraryTransactionData.getIdentifier());
bytes.write(Ints.toByteArray(arbitraryTransactionData.getMethod().value));
byte[] secret = arbitraryTransactionData.getSecret();
@@ -265,6 +275,8 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
Serialization.serializeSizedString(bytes, arbitraryTransactionData.getName());
Serialization.serializeSizedString(bytes, arbitraryTransactionData.getIdentifier());
bytes.write(Ints.toByteArray(arbitraryTransactionData.getMethod().value));
byte[] secret = arbitraryTransactionData.getSecret();