|
| 1 | +package ibc.ics08.tendermint; |
| 2 | + |
| 3 | +import static ibc.ics08.tendermint.TendermintHelper.*; |
| 4 | + |
| 5 | +import java.math.BigInteger; |
| 6 | +import java.util.Arrays; |
| 7 | + |
| 8 | +import ibc.icon.score.util.Proto; |
| 9 | +import tendermint.types.*; |
| 10 | +import google.protobuf.*; |
| 11 | +import ibc.lightclients.tendermint.v1.*; |
| 12 | +import score.Context; |
| 13 | + |
| 14 | +public abstract class Tendermint { |
| 15 | + protected boolean verify( |
| 16 | + Duration trustingPeriod, |
| 17 | + Duration maxClockDrift, |
| 18 | + Fraction trustLevel, |
| 19 | + SignedHeader trustedHeader, |
| 20 | + ValidatorSet trustedVals, |
| 21 | + SignedHeader untrustedHeader, |
| 22 | + ValidatorSet untrustedVals, |
| 23 | + Timestamp currentTime) { |
| 24 | + verifyNewHeaderAndVals(untrustedHeader, untrustedVals, trustedHeader, currentTime, maxClockDrift); |
| 25 | + |
| 26 | + boolean isAdjacent = untrustedHeader.getHeader().getHeight() |
| 27 | + .equals(trustedHeader.getHeader().getHeight().add(BigInteger.ONE)); |
| 28 | + if (isAdjacent) { |
| 29 | + return verifyAdjacent(trustedHeader, untrustedHeader, untrustedVals, trustingPeriod, currentTime, |
| 30 | + maxClockDrift); |
| 31 | + } |
| 32 | + |
| 33 | + return verifyNonAdjacent( |
| 34 | + trustedHeader, |
| 35 | + trustedVals, |
| 36 | + untrustedHeader, |
| 37 | + untrustedVals, |
| 38 | + trustingPeriod, |
| 39 | + currentTime, |
| 40 | + maxClockDrift, |
| 41 | + trustLevel); |
| 42 | + |
| 43 | + } |
| 44 | + |
| 45 | + protected boolean verifyAdjacent( |
| 46 | + SignedHeader trustedHeader, |
| 47 | + SignedHeader untrustedHeader, |
| 48 | + ValidatorSet untrustedVals, |
| 49 | + Duration trustingPeriod, |
| 50 | + Timestamp currentTime, |
| 51 | + Duration maxClockDrift) { |
| 52 | + |
| 53 | + // Check the validator hashes are the same |
| 54 | + Context.require( |
| 55 | + Arrays.equals(untrustedHeader.getHeader().getValidatorsHash(), |
| 56 | + trustedHeader.getHeader().getNextValidatorsHash()), |
| 57 | + "expected old header next validators to match those from new header"); |
| 58 | + |
| 59 | + // Ensure that +2/3 of new validators signed correctly. |
| 60 | + return verifyCommitLight( |
| 61 | + untrustedVals, |
| 62 | + trustedHeader.getHeader().getChainId(), |
| 63 | + untrustedHeader.getCommit().getBlockId(), |
| 64 | + untrustedHeader.getHeader().getHeight(), |
| 65 | + untrustedHeader.getCommit()); |
| 66 | + |
| 67 | + } |
| 68 | + |
| 69 | + protected boolean verifyNonAdjacent( |
| 70 | + SignedHeader trustedHeader, |
| 71 | + ValidatorSet trustedVals, |
| 72 | + SignedHeader untrustedHeader, |
| 73 | + ValidatorSet untrustedVals, |
| 74 | + Duration trustingPeriod, |
| 75 | + Timestamp currentTime, |
| 76 | + Duration maxClockDrift, |
| 77 | + Fraction trustLevel) { |
| 78 | + |
| 79 | + // assert that trustedVals is NextValidators of last trusted header |
| 80 | + // to do this, we check that trustedVals.Hash() == consState.NextValidatorsHash |
| 81 | + Context.require(Arrays.equals(hash(trustedVals), trustedHeader.getHeader().getNextValidatorsHash()), |
| 82 | + "LC: headers trusted validators does not hash to latest trusted validators"); |
| 83 | + |
| 84 | + // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators |
| 85 | + // signed correctly. |
| 86 | + verifyCommitLightTrusting(trustedVals, trustedHeader.getHeader().getChainId(), untrustedHeader.getCommit(), |
| 87 | + trustLevel); |
| 88 | + |
| 89 | + // Ensure that +2/3 of new validators signed correctly. |
| 90 | + return verifyCommitLight( |
| 91 | + untrustedVals, |
| 92 | + trustedHeader.getHeader().getChainId(), |
| 93 | + untrustedHeader.getCommit().getBlockId(), |
| 94 | + untrustedHeader.getHeader().getHeight(), |
| 95 | + untrustedHeader.getCommit()); |
| 96 | + |
| 97 | + } |
| 98 | + |
| 99 | + protected void verifyNewHeaderAndVals( |
| 100 | + SignedHeader untrustedHeader, |
| 101 | + ValidatorSet untrustedVals, |
| 102 | + SignedHeader trustedHeader, |
| 103 | + Timestamp currentTime, |
| 104 | + Duration maxClockDrift) { |
| 105 | + // SignedHeader validate basic |
| 106 | + Context.require(untrustedHeader.getHeader().getChainId().equals(trustedHeader.getHeader().getChainId()), |
| 107 | + "header belongs to another chain"); |
| 108 | + Context.require(untrustedHeader.getCommit().getHeight().equals(untrustedHeader.getHeader().getHeight()), |
| 109 | + "header and commit height mismatch"); |
| 110 | + |
| 111 | + byte[] untrustedHeaderBlockHash = hash(untrustedHeader.getHeader()); |
| 112 | + Context.require(Arrays.equals(untrustedHeaderBlockHash, untrustedHeader.getCommit().getBlockId().getHash()), |
| 113 | + "commit signs signs block failed"); |
| 114 | + |
| 115 | + Context.require(untrustedHeader.getHeader().getHeight().compareTo(trustedHeader.getHeader().getHeight()) > 0, |
| 116 | + "expected new header height to be greater than one of old header"); |
| 117 | + Context.require( |
| 118 | + gt(untrustedHeader.getHeader().getTime(), trustedHeader.getHeader().getTime()), |
| 119 | + "expected new header time to be after old header time"); |
| 120 | + |
| 121 | + Timestamp curentTimestamp = new Timestamp(); |
| 122 | + curentTimestamp.setSeconds(currentTime.getSeconds().add(maxClockDrift.getSeconds())); |
| 123 | + curentTimestamp.setNanos(currentTime.getNanos().add(maxClockDrift.getNanos())); |
| 124 | + Context.require(gt(curentTimestamp, untrustedHeader.getHeader().getTime()), |
| 125 | + "new header has time from the future"); |
| 126 | + |
| 127 | + byte[] validatorsHash = hash(untrustedVals); |
| 128 | + Context.require(Arrays.equals(untrustedHeader.getHeader().getValidatorsHash(), validatorsHash), |
| 129 | + "expected new header validators to match those that were supplied at height XX"); |
| 130 | + } |
| 131 | + |
| 132 | + protected boolean verifyCommitLightTrusting( |
| 133 | + ValidatorSet trustedVals, |
| 134 | + String chainID, |
| 135 | + Commit commit, |
| 136 | + Fraction trustLevel) { |
| 137 | + BigInteger talliedVotingPower = BigInteger.ZERO; |
| 138 | + boolean[] seenVals = new boolean[trustedVals.getValidators().size()]; |
| 139 | + |
| 140 | + CommitSig commitSig; |
| 141 | + BigInteger totalVotingPowerMulByNumerator = getTotalVotingPower(trustedVals) |
| 142 | + .multiply(trustLevel.getNumerator()); |
| 143 | + BigInteger votingPowerNeeded = totalVotingPowerMulByNumerator.divide(trustLevel.getDenominator()); |
| 144 | + |
| 145 | + int signaturesLength = commit.getSignatures().size(); |
| 146 | + for (int idx = 0; idx < signaturesLength; idx++) { |
| 147 | + commitSig = commit.getSignatures().get(idx); |
| 148 | + |
| 149 | + // no need to verify absent or nil votes. |
| 150 | + if (commitSig.getBlockIdFlag() != BlockIDFlag.BLOCK_ID_FLAG_COMMIT) { |
| 151 | + continue; |
| 152 | + } |
| 153 | + |
| 154 | + // We don't know the validators that committed this block, so we have to |
| 155 | + // check for each vote if its validator is already known. |
| 156 | + int valIdx = getByAddress(trustedVals, commitSig.getValidatorAddress()); |
| 157 | + if (valIdx == -1) { |
| 158 | + continue; |
| 159 | + } |
| 160 | + |
| 161 | + // check for double vote of validator on the same commit |
| 162 | + Context.require(!seenVals[valIdx], "double vote of validator on the same commit"); |
| 163 | + seenVals[valIdx] = true; |
| 164 | + |
| 165 | + Validator val = trustedVals.getValidators().get(valIdx); |
| 166 | + |
| 167 | + // validate signature |
| 168 | + byte[] message = voteSignBytesDelim(commit, chainID, idx); |
| 169 | + byte[] sig = commitSig.getSignature(); |
| 170 | + |
| 171 | + if (!verifySig(val, message, sig)) { |
| 172 | + return false; |
| 173 | + } |
| 174 | + |
| 175 | + talliedVotingPower = talliedVotingPower.add(val.getVotingPower()); |
| 176 | + |
| 177 | + if (talliedVotingPower.compareTo(votingPowerNeeded) > 0) { |
| 178 | + return true; |
| 179 | + } |
| 180 | + |
| 181 | + } |
| 182 | + |
| 183 | + return false; |
| 184 | + } |
| 185 | + |
| 186 | + // VerifyCommitLight verifies +2/3 of the set had signed the given commit. |
| 187 | + // |
| 188 | + // This method is primarily used by the light client and does not check all the |
| 189 | + // signatures. |
| 190 | + protected boolean verifyCommitLight( |
| 191 | + ValidatorSet validators, |
| 192 | + String chainID, |
| 193 | + BlockID blockID, |
| 194 | + BigInteger height, |
| 195 | + Commit commit) { |
| 196 | + Context.require(validators.getValidators().size() == commit.getSignatures().size(), |
| 197 | + "invalid commmit signatures"); |
| 198 | + |
| 199 | + Context.require(height.equals(commit.getHeight()), "invalid commit height"); |
| 200 | + |
| 201 | + Context.require(commit.getBlockId().equals(blockID), "invalid commit -- wrong block ID"); |
| 202 | + Validator val; |
| 203 | + CommitSig commitSig; |
| 204 | + |
| 205 | + BigInteger talliedVotingPower = BigInteger.ZERO; |
| 206 | + BigInteger votingPowerNeeded = getTotalVotingPower(validators).multiply(BigInteger.TWO) |
| 207 | + .divide(BigInteger.valueOf(3)); |
| 208 | + |
| 209 | + int signaturesLength = commit.getSignatures().size(); |
| 210 | + for (int i = 0; i < signaturesLength; i++) { |
| 211 | + commitSig = commit.getSignatures().get(i); |
| 212 | + |
| 213 | + // no need to verify absent or nil votes. |
| 214 | + if (commitSig.getBlockIdFlag() != BlockIDFlag.BLOCK_ID_FLAG_COMMIT) { |
| 215 | + continue; |
| 216 | + } |
| 217 | + |
| 218 | + val = validators.getValidators().get(i); |
| 219 | + |
| 220 | + byte[] message = voteSignBytesDelim(commit, chainID, i); |
| 221 | + byte[] sig = commitSig.getSignature(); |
| 222 | + |
| 223 | + if (!verifySig(val, message, sig)) { |
| 224 | + return false; |
| 225 | + } |
| 226 | + |
| 227 | + talliedVotingPower = talliedVotingPower.add(val.getVotingPower()); |
| 228 | + |
| 229 | + if (talliedVotingPower.compareTo(votingPowerNeeded) > 0) { |
| 230 | + return true; |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + return false; |
| 235 | + } |
| 236 | + |
| 237 | + public boolean verifySig( |
| 238 | + Validator val, |
| 239 | + byte[] message, |
| 240 | + byte[] sig) { |
| 241 | + if (val.getPubKey().getEd25519() != null) { |
| 242 | + return verifySig("ed25519", message, sig, val.getPubKey().getEd25519()); |
| 243 | + } else { |
| 244 | + return verifySig("ecdsa-secp256k1", message, sig, val.getPubKey().getSecp256k1()); |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + public boolean verifySig( |
| 249 | + String alg, |
| 250 | + byte[] message, |
| 251 | + byte[] sig, |
| 252 | + byte[] pubKey) { |
| 253 | + return Context.verifySignature(alg, message, sig, pubKey); |
| 254 | + } |
| 255 | + |
| 256 | + protected byte[] voteSignBytes( |
| 257 | + Commit commit, |
| 258 | + String chainID, |
| 259 | + int idx) { |
| 260 | + |
| 261 | + return toCanonicalVote(commit, idx, chainID); |
| 262 | + } |
| 263 | + |
| 264 | + protected byte[] voteSignBytesDelim( |
| 265 | + Commit commit, |
| 266 | + String chainID, |
| 267 | + int idx) { |
| 268 | + return Proto.encodeDelim(voteSignBytes(commit, chainID, idx)); |
| 269 | + } |
| 270 | +} |
0 commit comments