Search code examples
blockchainhyperledgerhyperledger-sawtooth

TP not receiving transactions after block rejected due to state root hash mismatch Hyperledger Sawtooth


I have setup a Hyperlder Sawtooth Network from the Sawtooth Docs, you can find docker-compose.yaml I used to setup the network here:

https://sawtooth.hyperledger.org/docs/core/releases/1.0/app_developers_guide/sawtooth-default.yaml

Transaction processor code:

const { TransactionHandler } = require('sawtooth-sdk/processor/handler');
const { InvalidTransaction } = require('sawtooth-sdk/processor/exceptions');
const { TextEncoder, TextDecoder } = require('text-encoding/lib/encoding');
const crypto = require('crypto');

const _hash = (x) => {
    return crypto.createHash('sha512').update(x).digest('hex').toLowerCase();
}

const encoder = new TextEncoder('utf8');
const decoder = new TextDecoder('utf8');

const TP_FAMILY = 'grocery';
const TP_NAMESPACE = _hash(TP_FAMILY).substring(0, 6);

class GroceryHandler extends TransactionHandler {
    constructor() {
        super(TP_FAMILY, ['1.0.0'], [TP_NAMESPACE]);
        this.timeout = 500;
    }

    apply(request, context) {
        console.log('Transaction Processor Called!');
        this._context = context;
        this._request = request;

        const actions = ['createOrder'];

        try {
            let payload = JSON.parse(decoder.decode(request.payload));
            let action = payload.action

            if(!action || !actions.includes(action)) {
                throw new InvalidTransaction(`Upsupported action "${action}"!`);
            }

            try {
                return this[action](payload.data);
            } catch(e) {
                console.log(e);
            }
        } catch(e) {
            throw new InvalidTransaction('Pass a valid json string.');
        }
    }

    createOrder(payload) {
        console.log('Creating order!');
        let data = {
            id: payload.id,
            status: payload.status,
            created_at: Math.floor((new Date()).getTime() / 1000)
        };

        return this._setEntry(this._makeAddress(payload.id), data);
    }    

    _setEntry(address, payload) {
        let dataBytes = encoder.encode(JSON.stringify(payload));
        let entries = {
            [address]: dataBytes
        }
        return this._context.setState(entries);
    }

    _makeAddress(id) {
        return TP_NAMESPACE + _hash(id).substr(0,64);
    }
}

const transactionProcessor = new TransactionProcessor('tcp://validator:4004');
transactionProcessor.addHandler(new GroceryHandler());
transactionProcessor.start();

Client code:

const { createContext, CryptoFactory } = require('sawtooth-sdk/signing');
const { protobuf } = require('sawtooth-sdk');
const { TextEncoder } = require('text-encoding/lib/encoding');
const request = require('request');
const crypto = require('crypto');

const encoder = new TextEncoder('utf8');

const _hash = (x) => {
    return crypto.createHash('sha512').update(x).digest('hex').toLowerCase();
}

const TP_FAMILY = 'grocery';
const TP_NAMESPACE = _hash(TP_FAMILY).substr(0, 6);

const context = createContext('secp256k1');
const privateKey = context.newRandomPrivateKey();
const signer = new CryptoFactory(context).newSigner(privateKey);

let payload = {
    action: 'create_order',
    data: {
        id: '1'
    }
};

const address = TP_NAMESPACE + _hash(payload.id).substr(0, 64);
const payloadBytes = encoder.encode(JSON.stringify(payload));

const transactionHeaderBytes = protobuf.TransactionHeader.encode({
    familyName: TP_FAMILY,
    familyVersion: '1.0.0',
    inputs: [address],
    outputs: [address],
    signerPublicKey: signer.getPublicKey().asHex(),
    batcherPublicKey: signer.getPublicKey().asHex(),
    dependencies: [],
    payloadSha512: _hash(payloadBytes)
}).finish();

const transactionHeaderSignature = signer.sign(transactionHeaderBytes);

const transaction = protobuf.Transaction.create({
    header: transactionHeaderBytes,
    headerSignature: transactionHeaderSignature,
    payload: payloadBytes
});

const transactions = [transaction]

const batchHeaderBytes = protobuf.BatchHeader.encode({
    signerPublicKey: signer.getPublicKey().asHex(),
    transactionIds: transactions.map((txn) => txn.headerSignature),
}).finish();

const batchHeaderSignature = signer.sign(batchHeaderBytes)

const batch = protobuf.Batch.create({
    header: batchHeaderBytes,
    headerSignature: batchHeaderSignature,
    transactions: transactions
});

const batchListBytes = protobuf.BatchList.encode({
    batches: [batch]
}).finish();


request.post({
    url: 'http://localhost:8008/batches',
    body: batchListBytes,
    headers: { 'Content-Type': 'application/octet-stream' }
}, (err, response) => {
    if (err) {
        return console.log(err);
    }

    console.log(response.body);
});

Validator log: https://justpaste.it/74y5g

Transaction processor log: https://justpaste.it/5ayn6

> grocery-tp@1.0.0 start /processor
> node index.js tcp://validator:4004

Connected to tcp://validator:4004
Registration of [grocery 1.0.0] succeeded

Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!
Transaction Processor Called!
Creating order!

After the below entry in validator logs, I don't receive any transactions to the processor.

[2018-07-04 10:39:18.026 DEBUG    block_validator] Block(c9636780f4babea6b8103665bc1fb19a59ce0ba66289494fc61972e97423a3273dd1d41e93ddf90c933809dab5350a0a83b282aaf25ebdcc6619735e25d8b337 (block_num:75, state:00704f66a517e79dc064e63586b12d677a3b60ce25363a4654fa819a59e4132c, previous_block_id:32b07cd79093aee0b7833b8924c8fef01fce798f3d58560c83c9891b2c05c02f2a4b894de43503fdcb0f129e9f365cfbdc415b798877393f7e75598195ad3c94)) rejected due to state root hash mismatch: 00704f66a517e79dc064e63586b12d677a3b60ce25363a4654fa819a59e4132c != e52737049078b9e0f149bb58fc4938473a5e889fa427536b0e862c4728df5004

Solution

  • When sawtooth processes a transaction it will send it to your TP more than once and then compare the hash between the multiple invocations to ensure the same result is returned. If, within the TP, you are generating a different address or variation of data stored at an address it will fail the transaction.

    The famous saying in sawtooth is that the TP must be deterministic for each transaction, in other words it is similar to the rule in function programming: The same TP called with the same Transaction should produce the same result.

    Things to watch for:

    1. Be careful to not construct an address that incorporates timestamp elements, incremental counts or other random bits of information
    2. Be careful to not do the same for the data you are storing at an address