10

I've been reading through the docs trying to figure out how to commit a transaction in QLDB, and in order to do so, a CommitDigest is required, and the docs describe it as:

Specifies the commit digest for the transaction to commit. For every active transaction, the commit digest must be passed. QLDB validates CommitDigest and rejects the commit with an error if the digest computed on the client does not match the digest computed by QLDB.

So CommitDigest must be computed, but I'm not quite sure what's required for its computation given this example:

// ** Start Session **
const startSessionResult = await qldbSession.sendCommand({
        StartSession: {
            LedgerName: ledgerName
        }
    }).promise(),
    sessionToken = startSessionResult.StartSession!.SessionToken!;

// ** Start Transaction **
const startTransactionResult = await qldbSession.sendCommand({
        StartTransaction: {},
        SessionToken: sessionToken
    }).promise(),
    transactionId = startTransactionResult.StartTransaction!.TransactionId!;

// ** Insert Document **
const executeStatementResult = await qldbSession.sendCommand({
        ExecuteStatement: {
            TransactionId: transactionId,
            Statement: `INSERT INTO sometable { 'id': 'abc123', 'userId': '123abc' }`
        },
        SessionToken: sessionToken
    }).promise(),
    documentId = getDocumentIdFromExecuteStateResult(executeStatementResult)

// ** Get Ledger Digest
const getDigestResult = await qldb.getDigest({
        Name: ledgerName
    }).promise(),
    ledgerDigest = getDigestResult.Digest;


// ** Commit Transaction **
// ** **The API call in question** **
const commitTransactionResult = await qldbSession.sendCommand({
    CommitTransaction: {
        TransactionId: transactionId,
        CommitDigest: `${commitDigest}` // <-- How to compute?
    },
    SessionToken: sessionToken
}).promise();
// *******************************


// ** End Session **
const endSession = await qldbSession.sendCommand({
    EndSession: {},
    SessionToken: sessionToken
}).promise();

What do I need to hash for CommitDigest in the CommitTransaction api call?

Mike Richards
  • 5,557
  • 3
  • 28
  • 34

2 Answers2

7

Update: the Node.js driver is now available. Take a look at https://github.com/awslabs/amazon-qldb-driver-nodejs/.

At the time of writing, the QLDB Node.js driver is still in development. It is going to be fairly difficult if you try and create one yourself, so I would caution against doing so. That said, I can explain both the purpose and algorithm behind CommitDigest.

The purpose is fairly straight-forward: to ensure that transactions are only committed iff the server has processed the exact set of statements sent by the client (all, in order, no duplicates). HTTP is request-response and it is therefore possible that requests may be dropped, processed out of order or duplicated. The QLDB drivers manage the communication with QLDB correctly, but having commit digest being in the protocol makes it impossible for an implementation to incorrectly retry requests and still commit the transaction. For example, consider incrementing a bank balance twice because a HTTP message is retried even though the first request succeeded.

The algorithm is also pretty straight-forward: a hash value is seeded with the transaction id and then updated with the QLDB ‘dot’ operator. Each update ‘dots’ in the statement hash (sha256 of the PartiQL string) as well as the IonHash of all of the bind values. The dot operator is the way QLDB merges hash values (this is the same operator used in the verification APIs) and is defined as the hash of the concatenation of the two hashes, ordered by the (signed, little-endian) byte-wise comparison between the two hashes. The client and server run this algorithm in lock-step and the server will only process the commit command if the value the client passes matches what the server computed. In this way, the server will never commit a transaction that isn’t exactly what the client requested.

Marc
  • 928
  • 5
  • 8
  • 1
    Thanks @Marc, are you on the AWS QLDB team? Completely understand the purpose of the hash. I've tried a few different implementations to calculate the hash using your description and examples, but with no luck, yet. Is there a timeline for when the QLDB Node.js driver will be able to do this? – Mike Richards Sep 20 '19 at 16:15
  • @MikeRichards I don't think you'll be able to compute the value without having access to an IonHash implementation in Node.js. In terms of timelines, I'm not able to provide one other than to say it's in progress. – Marc Sep 23 '19 at 02:43
  • 1
    See @FroiD's answer on IonHash for Javascript which is under active development. However, in his comment he takes the IonHash of the statement-with-literal, which is not the algorithm I described. The correct implementation is to take the sha256 of any statement (with any PartiQL literals that may be in there) and then the IonHash of each IonValue passed in as a parameter. So, in his example, IonHash is not used at all (only sha256 is) while if his statement was "INSERT INTO Vehicle ?" then you'd also need to dot in the IonHash of the vehicle Value. – Marc Sep 25 '19 at 16:56
  • thanks for expanding on this. To simplify things for a sec, ignoring IonHash of IonValues for the moment, if I have a single statement in my crude example, then pseudocode of the calculation of the commit digest should be: `dot(sha256(transactionId), sha256("INSERT INTO Vehicle { 'VIN': '123' }"))`, right? And `dot` should return `sha256(combine(byteSort(hash1, hash2)))`? – Mike Richards Sep 25 '19 at 18:35
  • To clarify, `dot` = `(hash1, hash2) => { if byteCompare(hash1, hash2) < 0 return sha256(concat(hash1, hash2)) else return sha256(concat(hash2, hash1)) }` – Mike Richards Sep 25 '19 at 18:52
  • 1
    @MikeRichards I'm sorry I should have been more specific. All of the hashing is done through IonHash using sha256. The transaction id and the PartiQL statements are turned into IonValues and then fed through the hasher. The IonHash-with-sha256 for an IonString is not the same as the sha256 of the UTF8 bytes of that string. Let me know if that makes sense. – Marc Sep 30 '19 at 20:48
  • Ah ok, I see, makes sense. I'll play around more with the `ion-hash-js` package – Mike Richards Sep 30 '19 at 22:03
  • Are there any news on the progress of node.js libs for QLDB? I also want to be able to commit transactions to QLDB and am stuck with properly calculating digest. Can we get a raw example how to achieve this for a simple INSERT statement? – Alko Oct 07 '19 at 08:37
  • @Alko no news I can share other than to say it's something we're working on. In terms of examples, we just released a preview version of the Python driver to GitHub: https://github.com/awslabs/amazon-qldb-driver-python. If you're familiar with Python you should be able to derive a solution for Node.js. That said, I would still discourage building your own driver. We're working to get one to you as quickly as we can. – Marc Oct 29 '19 at 20:28
  • And today we have a preview version of the Node.js driver. Details in https://twitter.com/marcbowes/status/1192969400506171392. – Marc Nov 09 '19 at 02:07
  • @Marc any news on a PHP driver? – Petah Aug 19 '20 at 05:06
  • None to share yet. – Marc Aug 24 '20 at 20:09
  • @Petah here is the Getting Started guide for the Python driver https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.python.html – emilebaizel Nov 23 '20 at 17:51
0

I have not enough reputation to add a comment, but I found this library here might help: https://github.com/amzn/ion-hash-js

I'm now here:

const ionHashJS = require("ion-hash-js/dist/commonjs/es5/src/IonHash");
const ionStr =
  "INSERT INTO Vehicle { 'VIN': '12345', 'Type': 'Semi', 'Year': '2020', 'Make': 'Frank', 'Model': '313373', 'Color': 'Blue'  }";
const hashReader = ionHashJS.makeHashReader(
  ionJs.makeReader(ionStr),
  ionHashJS.cryptoIonHasherProvider("sha256")
);
hashReader.next();
hashReader.next();
const digest = hashReader.digest();
FroiD
  • 49
  • 7
  • Have you by chance figured out the rest? I have tried your solution and also tried to concate it with transactionId like @Marc explained and still get a wrong digest. – Alko Oct 07 '19 at 08:26