tendermint
Version:
A light client which talks to your Tendermint node over RPC
318 lines (270 loc) • 10.1 kB
JavaScript
;
var stringify = require('json-stable-stringify');
var ed25519 = require('supercop.js');
// TODO: try to load native ed25519 implementation, fall back to supercop.js
var _require = require('./hash.js'),
getBlockHash = _require.getBlockHash,
getValidatorSetHash = _require.getValidatorSetHash;
var _require2 = require('./types.js'),
VarBuffer = _require2.VarBuffer,
CanonicalVote = _require2.CanonicalVote;
var _require3 = require('./pubkey.js'),
getAddress = _require3.getAddress;
var _require4 = require('./common.js'),
safeParseInt = _require4.safeParseInt;
// gets the serialized representation of a vote, which is used
// in the commit signatures
function getVoteSignBytes(chainId, vote) {
var canonicalVote = Object.assign({}, vote);
canonicalVote.chain_id = chainId;
canonicalVote.height = safeParseInt(vote.height);
canonicalVote.round = safeParseInt(vote.round);
canonicalVote.block_id.parts.total = safeParseInt(vote.block_id.parts.total);
if (vote.validator_index) {
canonicalVote.validator_index = safeParseInt(vote.validator_index);
}
var encodedVote = CanonicalVote.encode(canonicalVote);
return VarBuffer.encode(encodedVote);
}
// verifies that a number is a positive integer, less than the
// maximum safe JS integer
function verifyPositiveInt(n) {
if (!Number.isInteger(n)) {
throw Error('Value must be an integer');
}
if (n > Number.MAX_SAFE_INTEGER) {
throw Error('Value must be < 2^53');
}
if (n < 0) {
throw Error('Value must be >= 0');
}
}
// verifies a commit signs the given header, with 2/3+ of
// the voting power from given validator set
//
// This is for Tendermint v0.33.0 and later
function verifyCommit(header, commit, validators) {
var blockHash = getBlockHash(header);
if (blockHash !== commit.block_id.hash) {
throw Error('Commit does not match block hash');
}
var countedValidators = new Set();
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = commit.signatures[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var signature = _step.value;
// ensure there are never multiple signatures from a single validator
var validator_address = signature.validator_address;
if (countedValidators.has(validator_address)) {
throw Error('Validator has multiple signatures');
}
countedValidators.add(signature.validator_address);
}
// ensure this signature references at least one validator
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var validator = validators.find(function (v) {
return countedValidators.has(v.address);
});
if (!validator) {
throw Error('No recognized validators have signatures');
}
verifyCommitSigs(header, commit, validators);
}
// verifies a commit is signed by at least 2/3+ of the voting
// power of the given validator set
//
// This is for Tendermint v0.33.0 and later
function verifyCommitSigs(header, commit, validators) {
var committedVotingPower = 0;
// index validators by address
var validatorsByAddress = new Map();
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = validators[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var validator = _step2.value;
validatorsByAddress.set(validator.address, validator);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
var PrecommitType = 2;
var BlockIDFlagAbsent = 1;
var BlockIDFlagCommit = 2;
var BlockIDFlagNil = 3;
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = commit.signatures[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var cs = _step3.value;
switch (cs.block_id_flag) {
case BlockIDFlagAbsent:
case BlockIDFlagCommit:
case BlockIDFlagNil:
break;
default:
throw Error('unknown block_id_flag: ' + cs.block_id_flag);
}
var _validator = validatorsByAddress.get(cs.validator_address);
// skip if this validator isn't in the set
// (we allow signatures from validators not in the set,
// because we sometimes check the commit against older
// validator sets)
if (!_validator) continue;
var signature = Buffer.from(cs.signature, 'base64');
var vote = {
type: PrecommitType,
timestamp: cs.timestamp,
block_id: commit.block_id,
height: commit.height,
round: commit.round
};
var signBytes = getVoteSignBytes(header.chain_id, vote);
// TODO: support secp256k1 signatures
var pubKey = Buffer.from(_validator.pub_key.value, 'base64');
if (!ed25519.verify(signature, signBytes, pubKey)) {
throw Error('Invalid signature');
}
// count this validator's voting power
committedVotingPower += safeParseInt(_validator.voting_power);
}
// sum all validators' voting power
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
var totalVotingPower = validators.reduce(function (sum, v) {
return sum + safeParseInt(v.voting_power);
}, 0);
// JS numbers have no loss of precision up to 2^53, but we
// error at over 2^52 since we have to do arithmetic. apps
// should be able to keep voting power lower than this anyway
if (totalVotingPower > 2 ** 52) {
throw Error('Total voting power must be less than 2^52');
}
// verify enough voting power signed
var twoThirds = Math.ceil(totalVotingPower * 2 / 3);
if (committedVotingPower < twoThirds) {
var error = Error('Not enough committed voting power');
error.insufficientVotingPower = true;
throw error;
}
}
// verifies that a validator set is in the correct format
// and hashes to the correct value
function verifyValidatorSet(validators, expectedHash) {
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = validators[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var validator = _step4.value;
if (getAddress(validator.pub_key) !== validator.address) {
throw Error('Validator address does not match pubkey');
}
validator.voting_power = safeParseInt(validator.voting_power);
verifyPositiveInt(validator.voting_power);
if (validator.voting_power === 0) {
throw Error('Validator voting power must be > 0');
}
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
var validatorSetHash = getValidatorSetHash(validators);
if (expectedHash != null && validatorSetHash !== expectedHash) {
throw Error('Validator set does not match what we expected');
}
}
// verifies transition from one block to a higher one, given
// each block's header, commit, and validator set
function verify(oldState, newState) {
var oldHeader = oldState.header;
var oldValidators = oldState.validators;
var newHeader = newState.header;
var newValidators = newState.validators;
if (newHeader.chain_id !== oldHeader.chain_id) {
throw Error('Chain IDs do not match');
}
if (newHeader.height <= oldHeader.height) {
throw Error('New state height must be higher than old state height');
}
var validatorSetChanged = newHeader.validators_hash !== oldHeader.validators_hash;
if (validatorSetChanged && newValidators == null) {
throw Error('Must specify new validator set');
}
// make sure new header has a valid commit
var validators = validatorSetChanged ? newValidators : oldValidators;
verifyCommit(newHeader, newState.commit, validators);
if (validatorSetChanged) {
// make sure new validator set is valid
// make sure new validator set has correct hash
verifyValidatorSet(newValidators, newHeader.validators_hash);
// if previous state's `next_validators_hash` matches the new validator
// set hash, then we already know it is valid
if (oldHeader.next_validators_hash !== newHeader.validators_hash) {
// otherwise, make sure new commit is signed by 2/3+ of old validator set.
// sometimes we will take this path to skip ahead, we don't need any
// headers between `oldState` and `newState` if this check passes
verifyCommitSigs(newHeader, newState.commit, oldValidators);
}
// TODO: also pass transition if +2/3 of old validator set is still represented in commit
}
}
module.exports = verify;
Object.assign(module.exports, {
verifyCommit: verifyCommit,
verifyCommitSigs: verifyCommitSigs,
verifyValidatorSet: verifyValidatorSet,
verify: verify,
getVoteSignBytes: getVoteSignBytes
});