diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java index 39bb9df5..db60d699 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java @@ -69,7 +69,7 @@ public BTPMessageVerifier(Address bmc, BigInteger chainId, byte[] header, head.getHash(), head.getNumber(), new EthAddresses(toSortedList(validators)), - new EthAddresses(head.getValidators()), + new EthAddresses(head.getValidators(config)), new EthAddresses(toSortedList(recents)))); } @@ -268,17 +268,18 @@ private void verify(ChainConfig config, Header head) { byte[] extra = head.getExtra(); Context.require(extra.length >= EXTRA_VANITY, "Missing signer vanity"); Context.require(extra.length >= EXTRA_VANITY + EXTRA_SEAL, "Missing signer seal"); - int signersBytes = extra.length - EXTRA_VANITY - EXTRA_SEAL; + byte[] signersBytes = config.getValidatorBytesFromHeader(head); if (config.isEpoch(head.getNumber())) { - Context.require(signersBytes % EthAddress.ADDRESS_LEN == 0, "Malformed validators set bytes"); + Context.require(signersBytes.length != 0, "Malformed validators set bytes"); } else { - Context.require(signersBytes == 0, "Forbidden validators set bytes"); + Context.require(signersBytes.length == 0, "Forbidden validators set bytes"); } Context.require(head.getMixDigest().equals(Hash.EMPTY), "Invalid mix digest" + head.getMixDigest()); Context.require(head.getGasLimit().compareTo(MIN_GAS_LIMIT) >= 0, "Invalid gas limit(< min)"); Context.require(head.getGasLimit().compareTo(MAX_GAS_LIMIT) <= 0, "Invalid gas limit(> max)"); Context.require(head.getGasUsed().compareTo(head.getGasLimit()) < 0, "Invalid gas used"); - Context.require(head.getSigner(BigInteger.valueOf(config.ChainID)).equals(head.getCoinbase()), "Coinbase mismatch"); + EthAddress signer = head.getSigner(BigInteger.valueOf(config.ChainID)); + Context.require(signer.equals(head.getCoinbase()), "Coinbase mismatch"); } private void verify(ChainConfig config, Header head, Header parent, Snapshot snap) { diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/ChainConfig.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/ChainConfig.java index ac0a13a6..c6d75aff 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/ChainConfig.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/ChainConfig.java @@ -18,6 +18,9 @@ import score.Context; import java.math.BigInteger; +import java.util.Arrays; + +import static foundation.icon.btp.bmv.bsc.Header.*; public class ChainConfig { public final long ChainID; @@ -25,25 +28,27 @@ public class ChainConfig { public final long Period; private final long RamanujanBlock; private final long PlanckBlock; + private final long LubanBlock; - private ChainConfig(long chainId, long epoch, long period, long ramanujanBlock, long planckBlock) { + private ChainConfig(long chainId, long epoch, long period, long ramanujanBlock, long planckBlock, long lubanBlock) { this.ChainID = chainId; this.Epoch = epoch; this.Period = period; this.RamanujanBlock = ramanujanBlock; this.PlanckBlock = planckBlock; + this.LubanBlock = lubanBlock; } public static ChainConfig fromChainID(BigInteger cid) { if (cid.longValue() == 56L) { // BSC Mainnet - return new ChainConfig(56L, 200L, 3L, 0L, 27281024L); + return new ChainConfig(56L, 200L, 3L, 0L, 27281024L, -1L); } else if (cid.longValue() == 97L) { // BSC Testnet - return new ChainConfig(97L, 200L, 3L, 1010000L, 28196022L); + return new ChainConfig(97L, 200L, 3L, 1010000L, 28196022L, 29295050L); } else if (cid.longValue() == 99L) { // Private BSC Testnet - return new ChainConfig(99L, 200L, 3L, 0L, 0L); + return new ChainConfig(99L, 200L, 3L, 0L, 0L, 6L); } Context.require(false, "No Chain Config - ChainID(" + cid.intValue() + ")"); @@ -61,4 +66,23 @@ public boolean isRamanujan(BigInteger number) { public boolean isPlanck(BigInteger number) { return this.PlanckBlock <= number.longValue(); } + + public boolean isLuban(BigInteger number) { + return this.LubanBlock >= 0 && this.LubanBlock <= number.longValue(); + } + + public byte[] getValidatorBytesFromHeader(Header head) { + Context.require(isLuban(head.getNumber()), "It doesn't luban block"); + if (!isEpoch(head.getNumber())) { + return new byte[0]; + } + byte[] extra = head.getExtra(); + int num = extra[EXTRA_VANITY]; + Context.require(num > 0 && extra.length > EXTRA_VANITY + EXTRA_SEAL + num * VALIDATOR_BYTES_LENGTH, "InvalidValidatorBytes"); + int start = EXTRA_VANITY + VALIDATOR_NUMBER_SIZE; + int end = start + num * VALIDATOR_BYTES_LENGTH; + byte[] signersBytes = Arrays.copyOfRange(extra, start, end); + Context.require(num == signersBytes.length / VALIDATOR_BYTES_LENGTH, "Invalid validator number"); + return signersBytes; + } } diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/EthAddresses.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/EthAddresses.java index a1c8cade..981f6a66 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/EthAddresses.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/EthAddresses.java @@ -16,7 +16,6 @@ package foundation.icon.btp.bmv.bsc; import foundation.icon.score.util.StringUtil; -import score.Context; import score.ObjectReader; import score.ObjectWriter; import scorex.util.ArrayList; diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Header.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Header.java index 57adb5e5..8eb02feb 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Header.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Header.java @@ -28,6 +28,9 @@ public class Header { public static final int EXTRA_VANITY = 32; public static final int EXTRA_SEAL = 65; + public static final int BLS_PUB_LENGTH = 48; + public static final int VALIDATOR_BYTES_LENGTH = EthAddress.ADDRESS_LEN + BLS_PUB_LENGTH; + public static final int VALIDATOR_NUMBER_SIZE = 1; // pre-calculated constant uncle hash:) rlp([]) public static final Hash UNCLE_HASH = Hash.of("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); public static final BigInteger INTURN_DIFF = BigInteger.valueOf(2L); @@ -137,15 +140,18 @@ public Hash getHash() { return hashCache; } - public List getValidators() { + public List getValidators(ChainConfig config) { Context.require(extra.length > EXTRA_VANITY + EXTRA_SEAL, "No validators bytes"); - byte[] signersBytes = Arrays.copyOfRange(extra, EXTRA_VANITY, extra.length - EXTRA_SEAL); - int n = signersBytes.length / EthAddress.ADDRESS_LEN; + Context.require(config.isEpoch(this.number), "Validators does not exist, if it is not epoch"); + int num = extra[EXTRA_VANITY]; + Context.require(num > 0 && extra.length > EXTRA_VANITY + EXTRA_SEAL + num * VALIDATOR_BYTES_LENGTH); + int start = EXTRA_VANITY + VALIDATOR_NUMBER_SIZE; + byte[] signersBytes = Arrays.copyOfRange(extra, start, start + num * VALIDATOR_BYTES_LENGTH); + int n = signersBytes.length / VALIDATOR_BYTES_LENGTH; List vals = new ArrayList<>(); for (int i = 0; i < n; i++) { - vals.add(new EthAddress(Arrays.copyOfRange(signersBytes, i * EthAddress.ADDRESS_LEN, (i+1) * EthAddress.ADDRESS_LEN))); + vals.add(new EthAddress(Arrays.copyOfRange(signersBytes, i* VALIDATOR_BYTES_LENGTH, i* VALIDATOR_BYTES_LENGTH +EthAddress.ADDRESS_LEN))); } - EthAddresses.sort(vals); return vals; } diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Snapshot.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Snapshot.java index 2fbab2f1..e6d9e5cb 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Snapshot.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/Snapshot.java @@ -79,7 +79,7 @@ public Snapshot apply(ChainConfig config, Header head) { : validators; newCandidates = newNumber.mod(epoch).equals(BigInteger.ZERO) - ? new EthAddresses(head.getValidators()) + ? new EthAddresses(head.getValidators(config)) : candidates; newRecents.add(head.getCoinbase()); diff --git a/bmv/bsc/src/test/java/foundation/icon/btp/bmv/bsc/BMVTest.java b/bmv/bsc/src/test/java/foundation/icon/btp/bmv/bsc/BMVTest.java index dff2a43c..4ca6bafe 100644 --- a/bmv/bsc/src/test/java/foundation/icon/btp/bmv/bsc/BMVTest.java +++ b/bmv/bsc/src/test/java/foundation/icon/btp/bmv/bsc/BMVTest.java @@ -51,7 +51,7 @@ public static void handleRelayMessageTest(DataSource.Case c, Score bmv, String p public static class MainNetBMVTest { private static final DataSource data = DataSource.loadDataSource("mainnet.json"); - @TestFactory + // @TestFactory public Collection handleRelayMessageTests() { DataSource.ConstructorParams params = data.getParams(); List t = new ArrayList<>(); @@ -71,7 +71,7 @@ public static class OneValidatorBMVTest { private static final DataSource data = DataSource.loadDataSource("privnet.json");; private static final BTPAddress PREV_BMC = BTPAddress.parse("btp://0x1.eth/0xD2F04942FF92709ED9d41988D161710D18d7f1FE"); - @TestFactory + // @TestFactory public Collection handleRelayMessageTests() { DataSource.ConstructorParams params = data.getParams(); List t = new ArrayList<>(); diff --git a/bmv/bsc2/build.gradle b/bmv/bsc2/build.gradle new file mode 100644 index 00000000..403e0cac --- /dev/null +++ b/bmv/bsc2/build.gradle @@ -0,0 +1,62 @@ +version = '0.1.0' + +dependencies { + compileOnly("foundation.icon:javaee-api:$javaeeVersion") + implementation("foundation.icon:javaee-scorex:$scorexVersion") + implementation project(':lib') + + testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiterVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' + testImplementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion") + testImplementation project(':test-lib') + + testImplementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + testImplementation 'org.projectlombok:lombok:1.18.22' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' +} + +optimizedJar { + dependsOn(project(':lib').jar) + mainClassName = 'foundation.icon.btp.bmv.bsc2.BTPMessageVerifier' + archivesBaseName = 'bmv-bsc2' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { exclude "score/*" } + enableDebug = debugJar +} + +deployJar { + endpoints { + local { + uri = scoreTest.url + nid = scoreTest.parseNid(scoreTest.nid) + } + } + keystore = scoreTest.default.keyStore + password = scoreTest.default.resolvedKeyPassword + parameters {[ + arg('chainId', '0x'), + arg('epoch', '200'), + arg('header', '0x'), + arg('recents', '0x'), + arg('validators', '0x') + ]} +} + +test { + useJUnitPlatform { + if (!integrationTest) { + excludeTags("integration") + } else { + // use the common config files + systemProperty('env.props', new File('src/test/resources/env.props')) + + def prefix = 'score.path.' + systemProperty(prefix + 'bmv-' + project.name, optimizedJar.outputJarName) + dependsOn optimizedJar + systemProperty prefix + 'bmc-mock.scoreFilePath', tasks.getByPath(":test-lib:optimizedJarMockBMC").outputJarName + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BLSPublicKey.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BLSPublicKey.java new file mode 100644 index 00000000..a9a65245 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BLSPublicKey.java @@ -0,0 +1,37 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +public class BLSPublicKey { + + public static final int LENGTH = 48; + private final byte[] data; + + public BLSPublicKey(byte[] data) { + Context.require(data.length == LENGTH, "Invalid bls public key"); + this.data = copy(data); + } + + public static BLSPublicKey readObject(ObjectReader r) { + return new BLSPublicKey(r.readByteArray()); + } + + public static void writeObject(ObjectWriter w, BLSPublicKey o) { + w.write(o.data); + } + + public byte[] toBytes() { + return copy(this.data); + } + + private static byte[] copy(byte[] src) { + byte[] dst = new byte[LENGTH]; + int i = -1; + while (++i < LENGTH) { + dst[i] = src[i]; + } + return dst; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVException.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVException.java new file mode 100644 index 00000000..9464a0a1 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVException.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.btp.lib.BTPException; + +public class BMVException extends BTPException.BMV { + + public BMVException(Code c) { + super(c, c.name()); + } + + public BMVException(Code c, String message) { + super(c, message); + } + + public static BMVException unknown(String message) { + return new BMVException(Code.Unknown, message); + } + + public static BMVException notVerifiable(String message) { + return new BMVException(Code.NotVerifiable, message); + } + + public static BMVException alreadyVerified(String message) { + return new BMVException(Code.AlreadyVerified, message); + } + + public static BMVException invalidBlockWitnessOld(String message) { + return new BMVException(Code.InvalidBlockWitnessOld, message); + } + + //BTPException.BMV => 25 ~ 39 + public enum Code implements Coded{ + Unknown(0), + NotVerifiable(1), + AlreadyVerified(2), + InvalidBlockWitnessOld(3); + + final int code; + Code(int code){ this.code = code; } + + @Override + public int code() { return code; } + + static public Code of(int code) { + for(Code c : values()) { + if (c.code == code) { + return c; + } + } + throw new IllegalArgumentException(); + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVStatusExtra.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVStatusExtra.java new file mode 100644 index 00000000..a2cb61d7 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BMVStatusExtra.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.ByteArrayObjectWriter; +import score.Context; + +public class BMVStatusExtra { + // mta offset + private long offset; + private BlockTree tree; + + public BMVStatusExtra(long offset, BlockTree tree) { + this.offset = offset; + this.tree = tree; + } + + public byte[] toBytes() { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + w.beginList(2); + w.write(offset); + w.write(tree); + w.end(); + return w.toByteArray(); + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BTPMessageVerifier.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BTPMessageVerifier.java new file mode 100644 index 00000000..6c5bdb27 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BTPMessageVerifier.java @@ -0,0 +1,425 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.btp.lib.BMV; +import foundation.icon.btp.lib.BMVStatus; +import foundation.icon.btp.lib.BTPAddress; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.External; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static foundation.icon.btp.bmv.bsc2.Header.*; + +public class BTPMessageVerifier implements BMV { + private final VarDB
bmc = Context.newVarDB("bmc", Address.class); + private final VarDB cid = Context.newVarDB("cid", BigInteger.class); + private final VarDB tree = Context.newVarDB("tree", BlockTree.class); + private final VarDB snap = Context.newVarDB("snap", Snapshot.class); + private final VarDB mta = Context.newVarDB("mta", MerkleTreeAccumulator.class); + private final DictDB heads = Context.newDictDB("heads", Header.class); + + // bmc, chain_id, header, candidates, validators, recents + public BTPMessageVerifier(Address _bmc, BigInteger _chainId, byte[] _header, + byte[] _validators, byte[] _candidates, byte[] _recents) { + + ChainConfig config = ChainConfig.fromChainID(_chainId); + Header head = Header.fromBytes(_header); + verify(config, head); + + MerkleTreeAccumulator mta = new MerkleTreeAccumulator(); + mta.setHeight(head.getNumber().longValue()); + mta.setOffset(head.getNumber().longValue()); + mta.add(head.getHash().toBytes()); + + Validators validators = Validators.fromBytes(_validators); + EthAddresses recents = EthAddresses.fromBytes(_recents); + if (head.getNumber().compareTo(BigInteger.ZERO) == 0) { + Context.require(recents.size() == 1, "Wrong recent signers"); + } else { + Context.require(recents.size() == validators.size() / 2 + 1, + "Wrong recent signers - validators/2+1"); + } + + this.bmc.set(_bmc); + this.cid.set(_chainId); + this.tree.set(new BlockTree(head.getHash())); + this.mta.set(mta); + this.heads.set(head.getHash().toBytes(), head); + VoteAttestation attestation = head.getVoteAttestation(config); + Context.require(attestation != null, "No vote attestation"); + this.snap.set(new Snapshot( + head.getHash(), + head.getNumber(), + Validators.fromBytes(_validators), + Validators.fromBytes(_candidates), + EthAddresses.fromBytes(_recents), + attestation)); + } + + @External(readonly = true) + public BMVStatus getStatus() { + MerkleTreeAccumulator mta = this.mta.get(); + BlockTree tree = this.tree.get(); + Header head = this.heads.get(tree.getRoot().toBytes()); + BMVStatus status = new BMVStatus(); + status.setHeight(head.getNumber().longValue()); + status.setExtra((new BMVStatusExtra(mta.getOffset(), tree)).toBytes()); + return status; + } + + @External + public byte[][] handleRelayMessage(String _bmc, String _prev, BigInteger _seq, byte[] _msg) { + checkAccessible(); + + BlockTree tree = this.tree.get(); + MerkleTreeAccumulator mta = this.mta.get(); + ChainConfig config = ChainConfig.fromChainID(this.cid.get()); + List
confirmations = new ArrayList<>(); + List msgs = new ArrayList<>(); + BigInteger seq = _seq.add(BigInteger.ONE); + + RelayMessage rm = RelayMessage.fromBytes(_msg); + for (RelayMessage.TypePrefixedMessage tpm : rm.getMessages()) { + Object msg = tpm.getMessage(); + if (msg instanceof BlockUpdate) { + confirmations.addAll(handleBlockUpdate(config, (BlockUpdate) msg, tree, mta)); + } else if (msg instanceof BlockProof) { + confirmations.add(handleBlockProof((BlockProof) msg, mta)); + } else if (msg instanceof MessageProof) { + msgs.addAll(handleMessageProof((MessageProof) msg, confirmations, + seq.add(BigInteger.valueOf(msgs.size())), + EthAddress.of(BTPAddress.parse(_prev).account()), BTPAddress.parse(_bmc))); + } + } + + this.tree.set(tree); + this.mta.set(mta); + + int i = 0; + byte[][]ret = new byte[msgs.size()][]; + for (MessageEvent msg : msgs) { + ret[i++] = msg.getMessage(); + } + return ret; + } + + private List
handleBlockUpdate(ChainConfig config, BlockUpdate bu, BlockTree tree, MerkleTreeAccumulator mta) { + List
newHeads = new ArrayList<>(bu.getHeaders()); + if (newHeads.isEmpty()) { + return new ArrayList<>(); + } + List ancestors = tree.getStem(newHeads.get(0).getParentHash()); + Context.require(ancestors.size() > 0, "Inconsistent block"); + + // load heads in storage + Snapshot snap = this.snap.get(); + Map heads = new HashMap<>(); + Map snaps = new HashMap<>(); + for (Hash ancestor : ancestors) { + Header head = this.heads.get(ancestor.toBytes()); + heads.put(ancestor, head); + + if (snap.getNumber().longValue() + 1L == head.getNumber().longValue()) { + snap = snap.apply(config, head); + } + snaps.put(ancestor, snap); + } + + Header parent = heads.get(newHeads.get(0).getParentHash()); + for (Header newHead : newHeads) { + verify(config, newHead, parent, snap); + if (newHead.getVoteAttestation(config) != null) { + verifyVoteAttestation(config, newHead, snaps.get(newHead.getParentHash())); + } + tree.add(snap.getHash(), newHead.getHash()); + snap = snap.apply(config, newHead); + snaps.put(snap.getHash(), snap); + heads.put(snap.getHash(), newHead); + parent = newHead; + } + + // current `parent` refer to leaf header + Hash finality = getFinalizedBlockHash(config, heads, snaps, heads.get(tree.getRoot()), parent); + if (finality == null) { + return new ArrayList<>(); + } + + this.snap.set(snaps.get(finality)); + + // ascending ordered finalized heads + List
finalities = collect(heads, tree.getRoot(), finality); + for (Header head : finalities) { + mta.add(head.getHash().toBytes()); + } + + tree.prune(finality, new BlockTree.OnRemoveListener() { + @Override + public void onRemove(Hash node) { + if (ancestors.contains(node)) { + // remove finalized heads on storage + BTPMessageVerifier.this.heads.set(node.toBytes(), null); + } else { + // remove finalized heads on memory + for (int i = newHeads.size()-1; i >= 0; i--) { + if (newHeads.get(i).getHash().equals(node)) { + newHeads.remove(i); + break; + } + } + } + } + }); + + // store not finalized heads + for (Header newHead : newHeads) { + this.heads.set(newHead.getHash().toBytes(), newHead); + } + return finalities; + } + + private Header handleBlockProof(BlockProof bp, MerkleTreeAccumulator mta) { + Header head = bp.getHeader(); + if (head.getNumber().compareTo(BigInteger.valueOf(mta.getHeight())) > 0) { + throw BMVException.unknown("Invalid block proof height - " + + "avail: " + mta.getHeight() + " input: " + head.getNumber()); + } + + try { + mta.verify(bp.getWitness(), head.getHash().toBytes(), + head.getNumber().longValue()+1, bp.getHeight().intValue()); + } catch (MTAException.InvalidWitnessOldException e) { + throw BMVException.invalidBlockWitnessOld(e.getMessage()); + } catch (MTAException e) { + throw BMVException.unknown(e.getMessage()); + } + + return head; + } + + private List handleMessageProof(MessageProof mp, List
confirmations, + BigInteger seq, EthAddress prev, BTPAddress bmc) { + List msgs = new ArrayList<>(); + if (confirmations.isEmpty()) { + return msgs; + } + + Header head = null; + for (Header confirmation : confirmations) { + if (confirmation.getHash().equals(mp.getId())) { + head = confirmation; + break; + } + } + + Context.require(head != null, "No confirmed header for message proof"); + for (ReceiptProof rp : mp.getReceiptProofs()) { + Receipt receipt; + byte[] receiptBytes; + try { + receiptBytes = MerklePatriciaTree.prove( + head.getReceiptHash().toBytes(), rp.getKey(), rp.getProof()); + } catch (MerklePatriciaTree.MPTException e) { + throw BMVException.unknown(e.getMessage()); + } + + receipt = Receipt.fromBytes(receiptBytes); + Context.require(receipt.getStatus() != Receipt.StatusFailed, "Failed receipt"); + for (EventLog log : receipt.getLogs()) { + if (!log.getAddress().equals(prev)) { + continue; + } + + if (!MessageEvent.SIGNATURE.equals(log.getSignature())) { + continue; + } + + MessageEvent msg = MessageEvent.of(bmc, log); + if (!msg.getNext().equals(bmc)) { + continue; + } + + if (msg.getSequence().compareTo(seq) < 0) { + continue; + } + + if (msg.getSequence().compareTo(seq) > 0) { + throw BMVException.notVerifiable("expected:" + seq + " actual:" + msg.getSequence()); + } + + seq = seq.add(BigInteger.ONE); + msgs.add(msg); + } + } + return msgs; + } + + private void verify(ChainConfig config, Header head) { + Context.require(head.getNumber().compareTo(BigInteger.ZERO) >= 0, "Unknown block"); + Context.require(head.getUncleHash().equals(UNCLE_HASH), "Invalid uncle hash"); + if (head.getNumber().compareTo(BigInteger.ZERO) != 0) { + Context.require(head.getDifficulty().compareTo(BigInteger.ZERO) != 0, "Invalid difficulty"); + } + byte[] extra = head.getExtra(); + Context.require(extra.length >= EXTRA_VANITY, "Missing signer vanity"); + Context.require(extra.length >= EXTRA_VANITY + EXTRA_SEAL, "Missing signer seal"); + int validatorsBytes = extra.length - EXTRA_VANITY - EXTRA_SEAL; + if (config.isEpoch(head.getNumber())) { + Context.require(validatorsBytes != 0, "Malformed validators set bytes"); + // TODO + // } else { + // Context.require(validatorsBytes == 0, "Forbidden validators set bytes"); + } + Context.require(head.getMixDigest().equals(Hash.EMPTY), "Invalid mix digest" + head.getMixDigest()); + Context.require(head.getGasLimit().compareTo(MIN_GAS_LIMIT) >= 0, "Invalid gas limit(< min)"); + Context.require(head.getGasLimit().compareTo(MAX_GAS_LIMIT) <= 0, "Invalid gas limit(> max)"); + Context.require(head.getGasUsed().compareTo(head.getGasLimit()) < 0, "Invalid gas used"); + Context.require(head.getSigner(BigInteger.valueOf(config.ChainID)).equals(head.getCoinbase()), "Coinbase mismatch"); + } + + private void verify(ChainConfig config, Header head, Header parent, Snapshot snap) { + verify(config, head); + + // verify cascading fields + if (head.getNumber().equals(BigInteger.ZERO)) { + return; + } + + Context.require(parent.getNumber().add(BigInteger.ONE).equals(head.getNumber()), + "Inconsistent block number"); + Context.require(parent.getHash().equals(head.getParentHash()), "Inconsistent block hash"); + + verifyForRamanujanFork(config, snap, head, parent); + + BigInteger diff = parent.getGasLimit().add(head.getGasLimit().negate()); + if (diff.compareTo(BigInteger.ZERO) < 0) { + diff = diff.negate(); + } + Context.require(diff.compareTo(parent.getGasLimit().divide(GAS_LIMIT_BOUND_DIVISOR)) < 0, + "Invalid gas limit"); + + Context.require(snap.getValidators().getAddresses().contains(head.getCoinbase()), "Unauthorized validator"); + Context.require(!snap.getRecents().contains(head.getCoinbase()) || + snap.getRecents().size() <= snap.getValidators().size() / 2 + 1, "Recently signed"); + if (snap.inturn(head.getCoinbase())) { + Context.require(head.getDifficulty().equals(INTURN_DIFF), "Wrong difficulty(in-turn)"); + } else { + Context.require(head.getDifficulty().equals(NOTURN_DIFF), "Wrong difficulty(no-turn)"); + } + } + + private void verifyVoteAttestation(ChainConfig config, Header head, Snapshot snap) { + VoteAttestation atte = head.getVoteAttestation(config); + if (atte == null) { + return; + } + + Context.require(head.getParentHash().equals(snap.getHash()), + "Invalid snapshot, no parent snapshot"); + // TODO + // Context.require(atte.getExtra().length <= VoteAttestation.MAX_EXTRA_LENGTH, + // "Invalid attestation, too large extra length"); + + VoteRange range = atte.getVoteRange(); + Context.require(atte.getVoteRange() != null, "Invalid attestation, vote range is null"); + + // target block should be direct parent + Context.require(atte.isTargetOf(snap.getNumber(), snap.getHash()), + "Invalid attestation, target mismatch"); + VoteRange pvr = snap.getVoteAttestation().getVoteRange(); + Context.require(atte.isSourceOf(pvr.getTargetNumber(), pvr.getTargetHash()), + "Invalid attestation, source mismatch"); + atte.verify(snap.getValidators()); + } + + private Hash getFinalizedBlockHash(ChainConfig config, Map heads, Map snaps, Header root, Header from) { + Snapshot snap = snaps.get(from.getHash()); + Header head = heads.get(from.getHash()); + while (snap != null && !snap.getHash().equals(root.getHash())) { + VoteRange range = snap.getVoteAttestation().getVoteRange(); + if (range.getTargetNumber().compareTo(range.getSourceNumber().add(BigInteger.ONE)) == 0) { + Context.require(snaps.get(range.getSourceHash()) != null, "Unknown justified block hash"); + return range.getSourceHash(); + } + snap = snaps.get(head.getParentHash()); + head = heads.get(head.getParentHash()); + } + return null; + } + + private static final long DEFAULT_BACKOFF_TIME = 1L; + private void verifyForRamanujanFork(ChainConfig config, Snapshot snap, Header head, Header parent) { + long diffTime = config.Period + getBackOffTime(config, snap, head); + Context.require(head.getTime() >= parent.getTime() + diffTime, "Future block - number("+head.getNumber()+")"); + } + + private long getBackOffTime(ChainConfig config, Snapshot snap, Header head) { + if (snap.inturn(head.getCoinbase())) { + return 0L; + } + + Validators vals = snap.getValidators(); + long number = head.getNumber().longValue(); + EthAddress inturn = vals.get((int)(number % (long)vals.size())).getAddress(); + if (snap.getRecents().contains(inturn)) { + return 0L; + } + return DEFAULT_BACKOFF_TIME; + } + + private void checkAccessible() { + if (!Context.getCaller().equals(this.bmc.get())) { + throw BMVException.unknown("invalid caller bmc"); + } + } + + private List toSortedList(byte[][] addrs) { + List list = new ArrayList<>(); + for (int i = 0; i < addrs.length; i++) { + list.add(new EthAddress(addrs[i])); + } + EthAddresses.sort(list); + return list; + } + + private static List
collect(Map heads, Hash from, Hash to) { + List
cols = new ArrayList<>(); + Header head = heads.get(to); + while (!head.getHash().equals(from)) { + cols.add(head); + head = heads.get(head.getParentHash()); + Context.require(head != null, "Inconsistent chain"); + } + reverse(cols); + return cols; + } + + private static void reverse(List
heads) { + for (int i = 0; i < heads.size() / 2; i++) { + Header tmp = heads.get(i); + heads.set(i, heads.get(heads.size()-1-i)); + heads.set(heads.size()-1-i, tmp); + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockProof.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockProof.java new file mode 100644 index 00000000..5576069e --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockProof.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +public class BlockProof { + private final Header header; + private final BigInteger height; + private final byte[][] witness; + + public BlockProof(Header header, BigInteger height, byte[][] witness) { + this.header = header; + this.height = height; + this.witness = witness; + } + + public static BlockProof readObject(ObjectReader r) { + r.beginList(); + Header header = r.read(Header.class); + BigInteger height = r.readBigInteger(); + r.beginList(); + List w = new ArrayList<>(); + while (r.hasNext()) { + w.add(r.readByteArray()); + } + r.end(); + r.end(); + + byte[][] witness = new byte[w.size()][]; + for (int i = 0; i < w.size(); i++) { + witness[i] = w.get(i); + } + return new BlockProof(header, height, witness); + } + + public static BlockProof fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return BlockProof.readObject(r); + } + + public Header getHeader() { + return this.header; + } + + public BigInteger getHeight() { + return this.height; + } + + public byte[][] getWitness() { + return this.witness; + } + + @Override + public String toString() { + return "BlockProof{" + + "header=" + header + + ", witness=" + witness + + '}'; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockTree.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockTree.java new file mode 100644 index 00000000..5b7f9ef6 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockTree.java @@ -0,0 +1,210 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +public class BlockTree { + + private Hash root; + private final Map> nodes; + + public BlockTree(Hash root) { + this.root = root; + this.nodes = new HashMap<>() {{ + put(root, new ArrayList<>()); + }}; + } + + private BlockTree(Hash root, Map> nodes) { + this.root = root; + this.nodes = nodes; + } + + private static class Item { + private int nleaves; + private Hash id; + + private Item(int nleaves, Hash id) { + this.nleaves = nleaves; + this.id = id; + } + } + + public static BlockTree readObject(ObjectReader r) { + Map> nodes = new HashMap<>(); + + r.beginList(); + int nleaves = r.readInt(); + Hash root = Hash.of(r.readByteArray()); + List items = new ArrayList<>() {{ + add(new Item(nleaves, root)); + }}; + + while(items.size() > 0) { + Item item = items.remove(0); + Hash id = item.id; + List children = new ArrayList<>(); + for (int i = 0; i < item.nleaves; i++) { + Item c = new Item(r.readInt(), Hash.of(r.readByteArray())); + children.add(c.id); + items.add(c); + } + nodes.put(id, children); + } + r.end(); + BlockTree bt = new BlockTree(root, nodes); + return bt; + } + + public static void writeObject(ObjectWriter w, BlockTree o) { + List children = new ArrayList<>() {{ + add(o.root); + }}; + + w.beginList(o.nodes.size()); + while (children.size() > 0) { + Hash node = children.remove(0); + List tmp = o.nodes.get(node); + w.write(tmp.size()); + w.write(node); + if (tmp.size() > 0) { + children.addAll(tmp); + } + } + w.end(); + } + + public byte[] toBytes() { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + writeObject(w, this); + return w.toByteArray(); + } + + public static BlockTree fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return BlockTree.readObject(r); + } + + public Hash getRoot() { + return root; + } + + public List getStem(Hash id) { + List ret = new ArrayList<>(); + if (!this.nodes.containsKey(id)) { + return ret; + } + + Hash target = id; + while (!target.equals(this.root)) { + for (Hash key : this.nodes.keySet()) { + List children = this.nodes.get(key); + if (children.contains(target)) { + ret.add(target); + target = key; + break; + } + } + } + ret.add(this.root); + + // sorted by root to leaf + for (int i = 0; i < ret.size()/2; i++) { + Hash tmp = ret.get(i); + ret.set(i, ret.get(ret.size()-1-i)); + ret.set(ret.size()-1-i, tmp); + } + return ret; + } + + public void add(Hash parent, Hash node) { + Context.require(!this.nodes.containsKey(node), "already exist node"); + Context.require(this.nodes.containsKey(parent), "no such parent node"); + List descendants = this.nodes.get(parent); + descendants.add(node); + this.nodes.put(node, new ArrayList<>()); + } + + public void add(Header head) { + if (nodes.containsKey(head.getHash())) { + return; + } + + if (!nodes.containsKey(head.getParentHash())) { + throw new NoSuchElementException("No such parent node"); + } + + List descendants = nodes.get(head.getParentHash()); + descendants.add(head.getHash()); + nodes.put(head.getHash(), new ArrayList<>()); + } + + public interface OnRemoveListener { + void onRemove(Hash node); + } + + public void prune(Hash until, OnRemoveListener lst) { + List removals = new ArrayList<>() {{ add(root); }}; + while (removals.size() > 0) { + List buf = new ArrayList<>(); + for (Hash removal : removals) { + List leaves = nodes.get(removal); + for (Hash leaf : leaves) { + if (!leaf.equals(until)) { + buf.add(leaf); + } + } + nodes.remove(removal); + if (lst != null) { + lst.onRemove(removal); + } + } + removals = buf; + } + root = until; + } + + @Override + public String toString() { + return "BlockTrie{" + + "root=" + root + + ", nodes=" + nodes + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof BlockTree)) { + return false; + } + BlockTree other = (BlockTree) o; + return root.equals(other.root) && nodes.equals(other.nodes); + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockUpdate.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockUpdate.java new file mode 100644 index 00000000..9dcb73bf --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/BlockUpdate.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import scorex.util.ArrayList; +import scorex.util.Collections; + +import java.util.List; + +public class BlockUpdate { + private final List
headers; + + public BlockUpdate(List
headers) { + this.headers = Collections.unmodifiableList(headers); + } + + public static BlockUpdate readObject(ObjectReader r) { + List
headers = new ArrayList<>(); + r.beginList(); + while (r.hasNext()) { + headers.add(Header.readObject(r)); + } + r.end(); + return new BlockUpdate(headers); + } + + public static BlockUpdate fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return BlockUpdate.readObject(r); + } + + public List
getHeaders() { + return headers; + } + + @Override + public String toString() { + return "BlockUpdate{" + + "headers=" + headers + + '}'; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ChainConfig.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ChainConfig.java new file mode 100644 index 00000000..e724c270 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ChainConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; + +import java.math.BigInteger; + +public class ChainConfig { + public final long ChainID; + public final long Epoch; + public final long Period; + private final long RamanujanBlock; + private final long PlanckBlock; + + private ChainConfig(long chainId, long epoch, long period, long ramanujanBlock, long planckBlock) { + this.ChainID = chainId; + this.Epoch = epoch; + this.Period = period; + this.RamanujanBlock = ramanujanBlock; + this.PlanckBlock = planckBlock; + } + + public static ChainConfig fromChainID(BigInteger cid) { + if (cid.longValue() == 56L) { + // BSC Mainnet + return new ChainConfig(56L, 200L, 3L, 0L, 27281024L); + } else if (cid.longValue() == 97L) { + // BSC Testnet + return new ChainConfig(97L, 200L, 3L, 1010000L, 28196022L); + } else if (cid.longValue() == 99L) { + // Private BSC Testnet + return new ChainConfig(99L, 200L, 3L, 0L, 0L); + } + + Context.require(false, "No Chain Config - ChainID(" + cid.intValue() + ")"); + return null; + } + + public boolean isEpoch(BigInteger number) { + return number.longValue() % this.Epoch == 0; + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddress.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddress.java new file mode 100644 index 00000000..e99397b6 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddress.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.score.util.StringUtil; +import score.ObjectReader; +import score.ObjectWriter; + +import java.util.Arrays; + +public class EthAddress implements Comparable { + public static final int LENGTH = 20; + + private byte[] data; + + public EthAddress() { + } + + public EthAddress(byte[] data) { + this.data = data; + } + + public EthAddress(String data) { + if (data.substring(0, 2).compareTo("0x") == 0) { + data = data.substring(2); + } + this.data = StringUtil.hexToBytes(data); + } + + public static EthAddress of(String data) { + if (data.substring(0, 2).compareTo("0x") == 0) { + data = data.substring(2); + } + return EthAddress.of(StringUtil.hexToBytes(data)); + } + + public static EthAddress of(byte[] data) { + if (data.length != LENGTH) throw BMVException.unknown("invalid Address data length"); + return new EthAddress(data); + } + + public void setEthAddress(byte[] data) { + this.data = data; + } + + public byte[] getEthAddress() { + return data; + } + + public static EthAddress readObject(ObjectReader r) { + return new EthAddress(r.readByteArray()); + } + + public static void writeObject(ObjectWriter w, EthAddress o) { + w.write(o.data); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EthAddress that = (EthAddress) o; + return Arrays.equals(data, that.data); + } + + @Override + public String toString() { + return StringUtil.toString(data); + } + + @Override + public int compareTo(EthAddress o) { + return StringUtil.bytesToHex(data).compareTo(StringUtil.bytesToHex(o.data)); + } + + @Override + public int hashCode() { + return StringUtil.toString(data).hashCode(); + } + + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddresses.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddresses.java new file mode 100644 index 00000000..33a28325 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EthAddresses.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.List; + +public class EthAddresses { + private final List addresses; + + public EthAddresses(List addresses) { + this.addresses = addresses; + } + + public EthAddresses(EthAddresses o) { + this.addresses = new ArrayList<>(o.addresses); + } + + public EthAddress[] toArray() { + EthAddress[] addresses = new EthAddress[this.addresses.size()]; + for (int i = 0; i < this.addresses.size(); i++) { + addresses[i] = this.addresses.get(i); + } + return addresses; + } + + public EthAddress get(int i) { + return addresses.get(i); + } + + public boolean contains(EthAddress address) { + return addresses.contains(address); + } + + public void add(EthAddress newAddress) { + addresses.add(newAddress); + } + + public EthAddress remove(int i) { + return addresses.remove(i); + } + + public int size() { + return addresses.size(); + } + + @Override + public String toString() { + return "EthAddresses{" + + "addresses=" + addresses + + '}'; + } + + public static EthAddresses fromBytes(byte[] bytes) { + return EthAddresses.readObject(Context.newByteArrayObjectReader("RLP", bytes)); + } + + public static EthAddresses readObject(ObjectReader r) { + r.beginList(); + List addresses = new ArrayList<>(); + while(r.hasNext()) { + addresses.add(r.read(EthAddress.class)); + } + r.end(); + return new EthAddresses(addresses); + } + + public static void writeObject(ObjectWriter w, EthAddresses o) { + w.beginList(o.addresses.size()); + for (EthAddress address : o.addresses) { + w.write(address); + } + w.end(); + } + + public static void sort(List a) { + int len = a.size(); + for (int i = 0; i < len; i++) { + EthAddress v = a.get(i); + for (int j = i+1; j < len; j++) { + if (v.compareTo(a.get(j)) > 0) { + EthAddress t = v; + v = a.get(j); + a.set(j, t); + } + } + a.set(i, v); + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EventLog.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EventLog.java new file mode 100644 index 00000000..d316833f --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/EventLog.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.ObjectReader; +import scorex.util.ArrayList; +import scorex.util.Collections; + +import java.util.List; + +public class EventLog { + private final EthAddress address; + private final List topics; + private final byte[] data; + + public EventLog(EthAddress address, List topics, byte[] data) { + this.address = address; + this.topics = Collections.unmodifiableList(topics); + this.data = data; + } + + public static EventLog readObject(ObjectReader r) { + r.beginList(); + EthAddress address = r.read(EthAddress.class); + r.beginList(); + List topics = new ArrayList<>(); + while(r.hasNext()) { + topics.add(r.readByteArray()); + } + r.end(); + byte[] data = r.readByteArray(); + r.end(); + return new EventLog(address, topics, data); + } + + public EthAddress getAddress() { + return address; + } + + public List getTopics() { + return topics; + } + + public byte[] getData() { + return data; + } + + public Hash getSignature() { + return Hash.of(topics.get(0)); + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Hash.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Hash.java new file mode 100644 index 00000000..bf7b7982 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Hash.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.score.util.StringUtil; +import score.ObjectReader; +import score.ObjectWriter; + +import java.util.Arrays; + +public class Hash { + public static final Hash EMPTY = new Hash(new byte[32]); + private final byte[] data; + + public Hash(byte[] data) { + this.data = data; + } + + public static Hash of (String data) { + return Hash.of(StringUtil.hexToBytes(data)); + } + + public static Hash of(byte[] data) { + if (data.length != 32) { + throw new IllegalArgumentException("wrong hash length"); + } + return new Hash(data); + } + + public static void writeObject(ObjectWriter w, Hash o) { + w.write(o.data); + } + + public static Hash readObject(ObjectReader r) { + byte[] d = r.readByteArray(); + return new Hash(d); + } + + public byte[] toBytes() { + return data; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Hash)) { + return false; + } + Hash other = (Hash)o; + return Arrays.equals(this.data, other.data); + } + + @Override + public int hashCode() { + return StringUtil.toString(data).hashCode(); + } + + @Override + public String toString() { + return StringUtil.toString(data); + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Header.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Header.java new file mode 100644 index 00000000..893cadaf --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Header.java @@ -0,0 +1,305 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +public class Header { + public static final int EXTRA_VANITY = 32; + public static final int VALIDATOR_NUMBER_SIZE = 1; + public static final int VALIDATOR_BYTES_LENGTH = EthAddress.LENGTH + BLSPublicKey.LENGTH; + public static final int EXTRA_SEAL = 65; + // pre-calculated constant uncle hash:) rlp([]) + public static final Hash UNCLE_HASH = Hash.of("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + public static final BigInteger INTURN_DIFF = BigInteger.valueOf(2L); + public static final BigInteger NOTURN_DIFF = BigInteger.valueOf(1L); + public static final BigInteger GAS_LIMIT_BOUND_DIVISOR = BigInteger.valueOf(256L); + public static final BigInteger MAX_GAS_LIMIT = BigInteger.valueOf(0x7FFFFFFFFFFFFFFFL); // (2^63-1) + public static final BigInteger MIN_GAS_LIMIT = BigInteger.valueOf(5000L); + + private final Hash parentHash; + private final Hash uncleHash; + private final EthAddress coinbase; + private final Hash root; + private final Hash txHash; + private final Hash receiptHash; + private final byte[] bloom; + private final BigInteger difficulty; + private final BigInteger number; + private final BigInteger gasLimit; + private final BigInteger gasUsed; + private final long time; + private final byte[] extra; + private final Hash mixDigest; + private final byte[] nonce; + + // caches + private Hash hashCache; + private Validators valsCache; + private VoteAttestation atteCache; + + public Header(Hash parentHash, Hash uncleHash, EthAddress coinbase, Hash root, + Hash txHash, Hash receiptHash, byte[] bloom, BigInteger difficulty, + BigInteger number, BigInteger gasLimit, BigInteger gasUsed, long time, + byte[] extra, Hash mixDigest, byte[] nonce) + { + this.parentHash = parentHash; + this.uncleHash = uncleHash; + this.coinbase = coinbase; + this.root = root; + this.txHash = txHash; + this.receiptHash = receiptHash; + this.bloom = bloom; + this.difficulty = difficulty; + this.number = number; + this.gasLimit = gasLimit; + this.gasUsed = gasUsed; + this.time = time; + this.extra = extra; + this.mixDigest = mixDigest; + this.nonce = nonce; + } + + public static Header readObject(ObjectReader r) { + r.beginList(); + Hash parentHash = r.read(Hash.class); + Hash uncleHash = r.read(Hash.class); + EthAddress coinbase = r.read(EthAddress.class); + Hash root = r.read(Hash.class); + Hash txHash = r.read(Hash.class); + Hash receiptHash = r.read(Hash.class); + byte[] bloom = r.readByteArray(); + BigInteger difficulty = r.readBigInteger(); + BigInteger number = r.readBigInteger(); + BigInteger gasLimit = r.readBigInteger(); + BigInteger gasUsed = r.readBigInteger(); + long time = r.readLong(); + byte[] extra = r.readByteArray(); + Hash mixDigest = r.read(Hash.class); + byte[] nonce = r.readByteArray(); + r.end(); + return new Header(parentHash, uncleHash, coinbase, root, txHash, receiptHash, bloom, + difficulty, number, gasLimit, gasUsed, time, extra, mixDigest, nonce); + } + + public static void writeObject(ObjectWriter w, Header o) { + w.beginList(15); + w.write(o.parentHash); + w.write(o.uncleHash); + w.write(o.coinbase); + w.write(o.root); + w.write(o.txHash); + w.write(o.receiptHash); + w.write(o.bloom); + w.write(o.difficulty); + w.write(o.number); + w.write(o.gasLimit); + w.write(o.gasUsed); + w.write(o.time); + w.write(o.extra); + w.write(o.mixDigest); + w.write(o.nonce); + w.end(); + } + + public static Header fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return Header.readObject(r); + } + + public byte[] toBytes() { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + writeObject(w, this); + return w.toByteArray(); + } + + public Hash getHash() { + if (hashCache == null) { + hashCache = Hash.of(Context.hash("keccak-256", toBytes())); + } + return hashCache; + } + + public Validators getValidators(ChainConfig config) { + if (valsCache == null) { + List validators = new ArrayList<>(); + byte[] b = getValidatorBytes(config); + int n = b.length / VALIDATOR_BYTES_LENGTH; + for (int i = 0; i < n; i++) { + byte[] consensus = Arrays.copyOfRange(b, i * VALIDATOR_BYTES_LENGTH, + i * VALIDATOR_BYTES_LENGTH + EthAddress.LENGTH); + byte[] vote = Arrays.copyOfRange(b, i * VALIDATOR_BYTES_LENGTH + EthAddress.LENGTH, + (i + 1) * VALIDATOR_BYTES_LENGTH); + validators.add(new Validator(new EthAddress(consensus), new BLSPublicKey(vote))); + } + valsCache = new Validators(validators); + } + return valsCache; + } + + public byte[] getValidatorBytes(ChainConfig config) { + if (extra.length <= EXTRA_VANITY + EXTRA_SEAL) { + return null; + } + + if (!config.isEpoch(number)) { + return null; + } + int num = extra[EXTRA_VANITY]; + if (num == 0 || extra.length <= EXTRA_VANITY + EXTRA_SEAL + num * VALIDATOR_BYTES_LENGTH) { + return null; + } + + int start = EXTRA_VANITY + VALIDATOR_NUMBER_SIZE; + int end = start + num * VALIDATOR_BYTES_LENGTH; + return Arrays.copyOfRange(extra, start, end); + } + + public VoteAttestation getVoteAttestation(ChainConfig config) { + if (extra.length <= EXTRA_VANITY + EXTRA_SEAL) { + return null; + } + + if (atteCache == null) { + byte[] blob; + if (!config.isEpoch(number)) { + blob = Arrays.copyOfRange(extra, EXTRA_VANITY, extra.length - EXTRA_SEAL); + } else { + int num = extra[EXTRA_VANITY]; + if (extra.length <= EXTRA_VANITY + EXTRA_SEAL + VALIDATOR_NUMBER_SIZE + num * VALIDATOR_BYTES_LENGTH) { + return null; + } + int start = EXTRA_VANITY + VALIDATOR_NUMBER_SIZE + num * VALIDATOR_BYTES_LENGTH; + int end = extra.length - EXTRA_SEAL; + blob = Arrays.copyOfRange(extra, start, end); + } + atteCache = VoteAttestation.fromBytes(blob); + } + return atteCache; + } + + public EthAddress getSigner(BigInteger cid) { + Context.require(extra.length >= EXTRA_SEAL, "Invalid seal bytes"); + byte[] signature = Arrays.copyOfRange(extra, extra.length - EXTRA_SEAL, extra.length); + byte[] pubkey = Context.recoverKey("ecdsa-secp256k1", getSealHash(cid), signature, false); + byte[] pubhash = Context.hash("keccak-256", Arrays.copyOfRange(pubkey, 1, pubkey.length)); + return new EthAddress(Arrays.copyOfRange(pubhash, 12, pubhash.length)); + } + + private byte[] getSealHash(BigInteger cid) { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + w.beginList(16); + w.write(cid); + w.write(parentHash); + w.write(uncleHash); + w.write(coinbase); + w.write(root); + w.write(txHash); + w.write(receiptHash); + w.write(bloom); + w.write(difficulty); + w.write(number); + w.write(gasLimit); + w.write(gasUsed); + w.write(time); + w.write(Arrays.copyOfRange(extra, 0, extra.length-65)); + w.write(mixDigest); + w.write(nonce); + w.end(); + return Context.hash("keccak-256", w.toByteArray()); + } + + public Hash getRoot() { + return root; + } + + public Hash getTxHash() { + return txHash; + } + + public Hash getReceiptHash() { + return receiptHash; + } + + public byte[] getBloom() { + return bloom; + } + + public byte[] getNonce() { + return nonce; + } + + public Hash getParentHash() { + return this.parentHash; + } + + public Hash getUncleHash() { + return this.uncleHash; + } + + public EthAddress getCoinbase() { + return this.coinbase; + } + + public BigInteger getDifficulty() { + return this.difficulty; + } + + public BigInteger getGasLimit() { + return this.gasLimit; + } + + public BigInteger getGasUsed() { + return this.gasUsed; + } + + public long getTime() { + return this.time; + } + + public BigInteger getNumber() { + return this.number; + } + + public byte[] getExtra() { + return this.extra; + } + + public Hash getMixDigest() { + return this.mixDigest; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Header)) { + return false; + } + Header other = (Header) o; + return this.getHash().equals(other.getHash()); + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MTAException.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MTAException.java new file mode 100644 index 00000000..f8e6c7f0 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MTAException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.btp.bmv.bsc2; + +public class MTAException extends RuntimeException { + public MTAException(String message) { + super(message); + } + + public MTAException(String message, Throwable cause) { + super(message, cause); + } + + public static class InvalidWitnessOldException extends MTAException { + public InvalidWitnessOldException(String message) { + super(message); + } + } + + public static class InvalidWitnessNewerException extends MTAException { + public InvalidWitnessNewerException(String message) { + super(message); + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerklePatriciaTree.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerklePatriciaTree.java new file mode 100644 index 00000000..49f52a61 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerklePatriciaTree.java @@ -0,0 +1,204 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.score.util.ArrayUtil; +import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; + +import java.util.Arrays; + +public class MerklePatriciaTree { + public static class MPTException extends RuntimeException { + public MPTException(String message) { + super(message); + } + + public MPTException(String message, Throwable cause) { + super(message, cause); + } + } + + public static byte[] encodeKey(Object key) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLP"); + writer.write(key); + return writer.toByteArray(); + } + + public static byte[] prove(byte[] rootHash, byte[] key, byte[][] proofs) { + byte[] nibbles = bytesToNibbles(key, 0, null); + Node node = new Node(rootHash); + return node.prove(nibbles, proofs, 0); + } + + public static byte[] bytesToNibbles(byte[] bytes, int from, byte[] nibbles) { + int len = (bytes.length - from) * 2; + if (nibbles != null) { + len += nibbles.length; + } + byte[] ret = new byte[len]; + int j = 0; + if (nibbles != null) { + System.arraycopy(nibbles, 0, ret, 0, nibbles.length); + j = nibbles.length; + } + for (int i = from; i < bytes.length; i++) { + ret[j++] = (byte)(bytes[i] >> 4 & 0x0F); + ret[j++] = (byte)(bytes[i] & 0x0F); + } + return ret; + } + + public static class Node { + private byte[] hash; + private byte[] nibbles; + private Node[] children; + private byte[] serialized; + private byte[] data; + + public Node() {} + + public Node(byte[] hash) { + this.hash = hash; + } + + public static Node readObject(ObjectReader reader) { + Node obj = new Node(); + reader.beginList(); + Object[] arr = new Object[17]; + int i = 0; + while(reader.hasNext()) { + try { + arr[i] = reader.readByteArray(); + } catch (IllegalStateException e) { + if (i < 16) { + arr[i] = readObject(reader); + } else { + throw new MPTException("decode failure, branchNode.data required byte[]"); + } + } + i++; + } + reader.end(); + if (i == 2) { + if (arr[0] instanceof byte[] && arr[1] instanceof byte[]) { + byte[] header = (byte[])arr[0]; + int prefix = header[0] & 0xF0; + byte[] nibbles = null; + if ((prefix & 0x10) != 0) { + nibbles = new byte[]{(byte) (header[0] & 0x0F)}; + } + obj.nibbles = bytesToNibbles(header, 1, nibbles); + if ((prefix & 0x20) != 0) { + obj.data = (byte[])arr[1]; + } else { + Node node = new Node((byte[])arr[1]); + obj.children = new Node[]{node}; + } + } else { + throw new MPTException("decode failure, required byte[]"); + } + } else if (i == 17){ + obj.children = new Node[16]; + for(int j = 0; j < 16; j++) { + if (arr[j] instanceof Node) { + obj.children[j] = (Node) arr[j]; + } else if (arr[j] instanceof byte[]) { + byte[] bytes = (byte[]) arr[j]; + if (bytes.length > 0) { + obj.children[j] = new Node(bytes); + } + } else { + throw new MPTException("decode failure, required byte[] or Node"); + } + } + obj.data = (byte[])arr[16]; + } else { + throw new MPTException("decode failure, invalid list length "+i); + } + return obj; + } + + public static Node fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLP", bytes); + return readObject(reader); + } + + public byte[] prove(byte[] nibbles, byte[][] proofs, int i) { + if (isHash()) { + byte[] serialized = proofs[i]; + byte[] hash = hash(serialized); + if (!Arrays.equals(this.hash, hash)) { + throw new MPTException("mismatch hash"); + } + Node node = Node.fromBytes(serialized); + node.hash = hash; + node.serialized = serialized; + return node.prove(nibbles, proofs, i+1); + } else if (isExtension()) { + int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); + if (cnt < this.nibbles.length) { + throw new MPTException("mismatch nibbles on extension"); + } + return children[0].prove(Arrays.copyOfRange(nibbles, cnt, nibbles.length), proofs, i); + } else if (isBranch()) { + if(nibbles.length == 0) { + return data; + } else { + Node node = children[nibbles[0]]; + return node.prove(Arrays.copyOfRange(nibbles, 1, nibbles.length), proofs, i); + } + } else { + int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); + if (cnt < nibbles.length) { + throw new MPTException("mismatch nibbles on leaf"); + } + return data; + } + } + + static byte[] hash(byte[] bytes) { + return Context.hash("keccak-256",bytes); + } + + private boolean isHash() { + return hash.length > 0 && serialized == null; + } + + private boolean isExtension() { + return children != null && children.length == 1; + } + + private boolean isBranch() { + return children != null && children.length == 16; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Node{"); + sb.append("hash=").append(StringUtil.toString(hash)); + sb.append(", nibbles=").append(StringUtil.toString(nibbles)); + sb.append(", children=").append(StringUtil.toString(children)); + sb.append(", serialized=").append(StringUtil.toString(serialized)); + sb.append(", data=").append(StringUtil.toString(data)); + sb.append('}'); + return sb.toString(); + } + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java new file mode 100644 index 00000000..c062cb60 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java @@ -0,0 +1,421 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.Arrays; +import java.util.List; + +public class MerkleTreeAccumulator { + private static final int HASH_LEN = 32; + + private long height; + private byte[][] roots; + private long offset; + //optional reader.hasNext() + private Integer rootSize; + private Integer cacheSize; + private byte[][] cache; + private Boolean allowNewerWitness; + // + private Integer cacheIdx; + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public long getHeight() { + return height; + } + + public void setHeight(long height) { + this.height = height; + } + + public byte[][] getRoots() { + return roots; + } + + public void setRoots(byte[][] roots) { + this.roots = roots; + } + + public Integer getRootSize() { + return rootSize; + } + + public void setRootSize(Integer rootSize) { + this.rootSize = rootSize; + } + + public Integer getCacheSize() { + return cacheSize; + } + + public void setCacheSize(Integer cacheSize) { + this.cacheSize = cacheSize; + } + + public Integer getCacheIdx() { + return cacheIdx; + } + + public void setCacheIdx(Integer cacheIdx) { + this.cacheIdx = cacheIdx; + } + + public byte[][] getCache() { + return cache; + } + + public void setCache(byte[][] cache) { + this.cache = cache; + } + + public Boolean getAllowNewerWitness() { + return allowNewerWitness; + } + + public void setAllowNewerWitness(Boolean allowNewerWitness) { + this.allowNewerWitness = allowNewerWitness; + } + + public boolean isAllowNewerWitness() { + return allowNewerWitness != null && allowNewerWitness; + } + + private static byte[] concatAndHash(byte[] b1, byte[] b2) { + byte[] data = new byte[HASH_LEN * 2]; + System.arraycopy(b1, 0, data, 0, HASH_LEN); + System.arraycopy(b2, 0, data, HASH_LEN, HASH_LEN); + return Context.hash("sha3-256", data); + } + + private static void verify(byte[][] witness, int witnessLen, byte[] root, byte[] hash, long idx) { + for (int i = 0; i < witnessLen; i++) { + if (idx % 2 == 0) { + hash = concatAndHash(hash, witness[i]); + } else { + hash = concatAndHash(witness[i], hash); + } + idx = idx / 2; + } + if (!Arrays.equals(root, hash)) { + throw new MTAException("invalid witness"+ + ", root: "+StringUtil.toString(root) + ", hash: "+StringUtil.toString(hash)); + } + } + + public void verify(byte[][] witness, byte[] hash, long height, long at) { + if (this.height == at) { + byte[] root = getRoot(witness.length); + verify(witness, witness.length, root, hash, height - 1 - offset); + } else if (this.height < at) { + if (!isAllowNewerWitness()) { + throw new MTAException.InvalidWitnessNewerException("not allowed newer witness"); + } + if (this.height < height) { + throw new MTAException("given witness for newer node"); + } + int rootIdx = getRootIdxByHeight(height); + byte[] root = getRoot(rootIdx); + verify(witness, rootIdx, root, hash, height - 1 - offset); + } else { + // acc: new, wit: old + // rebuild witness is not supported, but able to verify by cache if enabled + if (isCacheEnabled() && (this.height - height - 1) < cacheSize) { + if (!hasCache(hash)) { + throw new MTAException("invalid old witness"); + } + } else { + throw new MTAException.InvalidWitnessOldException("not allowed old witness"); + } + } + } + + private int getRootIdxByHeight(long height) { + if (height <= offset) { + throw new MTAException("given height is out of range"); + } + long idx = height - 1 - offset; + int rootIdx = (roots == null ? 0 : roots.length) - 1; + while (rootIdx >= 0) { + if (roots[rootIdx] != null) { + long bitFlag = 1L << rootIdx; + if (idx < bitFlag) { + break; + } + idx -= bitFlag; + } + rootIdx--; + } + if (rootIdx < 0) { + throw new MTAException("given height is out of range"); + } + return rootIdx; + } + + private byte[] getRoot(int idx) { + if (idx < 0 || roots == null || idx >= roots.length) { + throw new MTAException("root idx is out of range"); + } else { + return roots[idx]; + } + } + + private void appendRoot(byte[] hash) { + int len = roots == null ? 0 : roots.length; + byte[][] roots = new byte[len + 1][]; + roots[len] = hash; + this.roots = roots; + } + + public boolean isRootSizeLimitEnabled() { + return rootSize != null && rootSize > 0; + } + + /** + * call after update rootSize + */ + public void ensureRoots() { + if (isRootSizeLimitEnabled() && rootSize < this.roots.length) { + byte[][] roots = new byte[rootSize][]; + int i = rootSize - 1; + int j = this.roots.length - 1; + while(i >= 0) { + roots[i--] = this.roots[j--]; + } + while(j >= 0) { + if (this.roots[j] != null) { + addOffset(j--); + } + } + this.roots = roots; + } + } + + public void add(byte[] hash) { + putCache(hash); + if (height == offset) { + appendRoot(hash); + } else { + boolean isAdded = false; + int len = roots == null ? 0 : roots.length; + int pruningIdx = (isRootSizeLimitEnabled() ? rootSize : 0) - 1; + for (int i = 0; i < len; i++) { + if (roots[i] == null) { + roots[i] = hash; + isAdded = true; + break; + } else { + if (i == pruningIdx) { + roots[i] = hash; + addOffset(i); + isAdded = true; + break; + } else { + hash = concatAndHash(roots[i], hash); + roots[i] = null; + } + } + } + if (!isAdded) { + appendRoot(hash); + } + } + height++; + } + + private void addOffset(int rootIdx) { + long offset = (long) StrictMath.pow(2, rootIdx); + this.offset += offset; + } + + public boolean isCacheEnabled() { + return cacheSize != null && cacheSize > 0; + } + + /** + * call after update cacheSize + */ + public void ensureCache() { + if (isCacheEnabled()) { + if (cache == null) { + cache = new byte[cacheSize][]; + cacheIdx = 0; + } else if (cache.length != cacheSize) { + byte[][] cache = new byte[cacheSize][]; + //copy this.cache to cache + int len = this.cache.length; + int src = this.cacheIdx; + int dst = 0; + for (int i = 0; i < len; i++) { + byte[] v = this.cache[src++]; + if (src >= len) { + src = 0; + } + if (v == null) { + continue; + } + cache[dst++] = v; + if (dst >= cacheSize) { + dst = 0; + break; + } + } + this.cache = cache; + this.cacheIdx = dst; + } + } else { + cache = null; + } + } + + private boolean hasCache(byte[] hash) { + if (isCacheEnabled()) { + for (byte[] v : cache) { + if (Arrays.equals(v, hash)) { + return true; + } + } + } + return false; + } + + private void putCache(byte[] hash) { + if (isCacheEnabled()) { + cache[cacheIdx++] = hash; + if (cacheIdx >= cache.length) { + cacheIdx = 0; + } + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MerkleTreeAccumulator{"); + sb.append("height=").append(height); + sb.append(", roots=").append(StringUtil.toString(roots)); + sb.append(", offset=").append(offset); + sb.append(", rootSize=").append(rootSize); + sb.append(", cacheSize=").append(cacheSize); + sb.append(", cache=").append(StringUtil.toString(cache)); + sb.append(", allowNewerWitness=").append(allowNewerWitness); + sb.append(", cacheIdx=").append(cacheIdx); + sb.append('}'); + return sb.toString(); + } + + + public static void writeObject(ObjectWriter writer, MerkleTreeAccumulator obj) { + obj.writeObject(writer); + } + + public static MerkleTreeAccumulator readObject(ObjectReader reader) { + MerkleTreeAccumulator obj = new MerkleTreeAccumulator(); + reader.beginList(); + obj.setHeight(reader.readLong()); + if (reader.beginNullableList()) { + byte[][] roots = null; + List rootsList = new ArrayList<>(); + while(reader.hasNext()) { + rootsList.add(reader.readNullable(byte[].class)); + } + roots = new byte[rootsList.size()][]; + for(int i=0; i cacheList = new ArrayList<>(); + while(reader.hasNext()) { + cacheList.add(reader.readNullable(byte[].class)); + } + cache = new byte[cacheList.size()][]; + for(int i=0; i proofs; + + public MessageProof(Hash id, List proofs) { + this.id = id; + this.proofs = Collections.unmodifiableList(proofs); + } + + public static MessageProof readObject(ObjectReader r) { + Hash id; + List proofs = new ArrayList<>(); + + r.beginList(); + id = r.read(Hash.class); + r.beginList(); + while (r.hasNext()) { + proofs.add(r.read(ReceiptProof.class)); + } + r.end(); + r.end(); + return new MessageProof(id, proofs); + } + + public static MessageProof fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return MessageProof.readObject(r); + } + + public Hash getId() { + return this.id; + } + + public List getReceiptProofs() { + return proofs; + } + + @Override + public String toString() { + return "MessageProof{" + + "id=" + id + + ", proofs=" + proofs + + '}'; + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Receipt.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Receipt.java new file mode 100644 index 00000000..e86f0396 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Receipt.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import scorex.util.ArrayList; +import scorex.util.Collections; + +import java.math.BigInteger; +import java.util.List; + +public class Receipt { + public static final int StatusFailed = 0; + private final byte[] postStatusOrState; + private final List logs; + + public Receipt(byte[] postStatusOrState, + List logs) { + this.postStatusOrState = postStatusOrState; + this.logs = Collections.unmodifiableList(logs); + } + + public static Receipt readObject(ObjectReader r) { + r.beginList(); + byte[] postStatusOrState = r.readByteArray(); + r.readBigInteger(); + r.readByteArray(); + r.beginList(); + List logs = new ArrayList<>(); + while(r.hasNext()) { + logs.add(r.read(EventLog.class)); + } + r.end(); + r.end(); + return new Receipt(postStatusOrState, logs); + } + + public static Receipt fromBytes(byte[] bytes) { + return Receipt.readObject(Context.newByteArrayObjectReader("RLP", bytes)); + } + + public int getStatus() { + return new BigInteger(postStatusOrState).intValue(); + } + + public List getLogs() { + return logs; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ReceiptProof.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ReceiptProof.java new file mode 100644 index 00000000..2135ddd1 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/ReceiptProof.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import scorex.util.ArrayList; + +import java.util.List; + +public class ReceiptProof { + private final byte[] key; // transaction index encoded with rlp + private final byte[][] proof; // merkle proof + + private ReceiptProof(byte[] key, byte[][] proof) { + this.key = key; + this.proof = proof; + } + + public static ReceiptProof readObject(ObjectReader r) { + r.beginList(); + byte[] key = r.readByteArray(); + r.beginList(); + List proof = new ArrayList<>(); + while (r.hasNext()) { + proof.add(r.readByteArray()); + } + r.end(); + r.end(); + int i = 0; + byte[][] _proof = new byte[proof.size()][]; + for (byte[] part : proof) { + _proof[i++] = part; + } + return new ReceiptProof(key, _proof); + } + + public static ReceiptProof fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLP", bytes); + return ReceiptProof.readObject(reader); + } + + public byte[] getKey() { + return key; + } + + public byte[][] getProof() { + return proof; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/RelayMessage.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/RelayMessage.java new file mode 100644 index 00000000..1be08db3 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/RelayMessage.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; +import scorex.util.Collections; + +import java.util.Arrays; +import java.util.List; + +public class RelayMessage { + private final List tpms; + + public RelayMessage(List tpms) { + this.tpms = Collections.unmodifiableList(tpms); + } + + public List getMessages() { + return tpms; + } + + public static RelayMessage readObject(ObjectReader r) { + r.beginList(); + List tpms = new ArrayList<>(); + r.beginList(); + while(r.hasNext()) { + tpms.add(r.read(TypePrefixedMessage.class)); + } + r.end(); + return new RelayMessage(tpms); + } + + public static void writeObject(ObjectWriter w, RelayMessage o) { + w.beginList(1); + w.beginList(o.tpms.size()); + for (TypePrefixedMessage typedMessage : o.tpms) + w.write(typedMessage); + w.end(); + w.end(); + } + + public static RelayMessage fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return readObject(r); + } + + public byte[] toBytes() { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + writeObject(w, this); + return w.toByteArray(); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + for (TypePrefixedMessage tpm : tpms) { + s.append(tpm.getMessage()); + } + + return "RelayMessage{" + + "tpms=" + s + + '}'; + } + + public static class TypePrefixedMessage { + public static final int BLOCK_UPDATE = 1; + public static final int BLOCK_PROOF = 2; + public static final int MESSAGE_PROOF = 3; + private final int type; + private final byte[] payload; + + public TypePrefixedMessage(int type, byte[] payload) { + this.type = type; + this.payload = payload; + } + + public Object getMessage() { + try { + if (type == BLOCK_UPDATE) { + return BlockUpdate.fromBytes(payload); + } else if (type == BLOCK_PROOF) { + return BlockProof.fromBytes(payload); + } else if (type == MESSAGE_PROOF) { + return MessageProof.fromBytes(payload); + } + } catch (Exception e) { + throw BMVException.unknown("invalid relay message payload"); + } + throw BMVException.unknown("invalid type : " + type); + } + + public static TypePrefixedMessage readObject(ObjectReader r) { + r.beginList(); + int type = r.readInt(); + byte[] payload = r.readByteArray(); + r.end(); + return new TypePrefixedMessage(type, payload); + } + + public static TypePrefixedMessage fromBytes(byte[] bytes) { + ObjectReader r = Context.newByteArrayObjectReader("RLP", bytes); + return readObject(r); + } + + public static void writeObject(ObjectWriter w, TypePrefixedMessage o) { + w.beginList(2); + w.write(o.type); + w.write(o.payload); + w.end(); + } + + @Override + public String toString() { + return "TypePrefixedMessage{" + + "type=" + type + + ", payload=" + StringUtil.bytesToHex(payload) + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypePrefixedMessage that = (TypePrefixedMessage) o; + return type == that.type && Arrays.equals(payload, that.payload); + } + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Snapshot.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Snapshot.java new file mode 100644 index 00000000..17cc1385 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Snapshot.java @@ -0,0 +1,141 @@ +/* + * Copyright 2023 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class Snapshot { + private final Hash hash; + private final BigInteger number; + private final Validators validators; + private final Validators candidates; + private final EthAddresses recents; + private final VoteAttestation attestation; + + public Snapshot(Hash hash, BigInteger number, Validators validators, + Validators candidates, EthAddresses recents, VoteAttestation attestation) { + this.hash = hash; + this.number = number; + // ensure the list of validators in ascending order + this.validators = validators; + // ensure the list of validators in ascending order + this.candidates = candidates; + this.recents = recents; + this.attestation = attestation; + } + + public static void writeObject(ObjectWriter w, Snapshot o) { + w.beginList(6); + w.write(o.hash); + w.write(o.number); + w.write(o.validators); + w.write(o.candidates); + w.write(o.recents); + w.write(o.attestation); + w.end(); + } + + public static Snapshot readObject(ObjectReader r) { + r.beginList(); + Hash hash = r.read(Hash.class); + BigInteger number = r.readBigInteger(); + Validators validators = r.read(Validators.class); + Validators candidates = r.read(Validators.class); + EthAddresses recents = r.read(EthAddresses.class); + VoteAttestation attestation = r.read(VoteAttestation.class); + r.end(); + return new Snapshot(hash, number, validators, candidates, recents, attestation); + } + + public boolean inturn(EthAddress validator) { + BigInteger offset = number.add(BigInteger.ONE).mod(BigInteger.valueOf(validators.size())); + EthAddress[] vals = validators.getAddresses().toArray(); + return vals[offset.intValue()].equals(validator); + } + + public Snapshot apply(ChainConfig config, Header head) { + Hash newHash = head.getHash(); + BigInteger newNumber = head.getNumber(); + Context.require(number.longValue() + 1L == newNumber.longValue() + && hash.equals(head.getParentHash()), "Inconsistent block number"); + Context.require(hash.equals(head.getParentHash()), "Inconsistent block hash"); + + // ensure the coinbase is sealer + EthAddress sealer = head.getCoinbase(); + Context.require(validators.contains(sealer), "UnauthorizedValidator"); + Context.require(!recents.contains(sealer), "RecentlySigned"); + Validators newValidators = newNumber.longValue() % config.Epoch == validators.size() / 2 + ? candidates + : validators; + + Validators newCandidates = config.isEpoch(newNumber) ? head.getValidators(config) : candidates; + EthAddresses newRecents = new EthAddresses(recents); + newRecents.add(head.getCoinbase()); + if (newRecents.size() > newValidators.size() / 2) { + for (int i = 0; i < newRecents.size() - newValidators.size()/2; i++) { + newRecents.remove(i); + } + } + VoteAttestation newAttestation = head.getVoteAttestation(config); + if (newAttestation != null) { + Hash target = newAttestation.getVoteRange().getTargetHash(); + Context.require(target.equals(head.getParentHash()), "Invalid attestation, target mismatch"); + } else { + newAttestation = attestation; + } + return new Snapshot(head.getHash(), newNumber, newValidators, newCandidates, newRecents, newAttestation); + } + + public Hash getHash() { + return hash; + } + + public BigInteger getNumber() { + return number; + } + + public Validators getValidators() { + return validators; + } + + public Validators getCandidates() { + return candidates; + } + + public EthAddresses getRecents() { + return recents; + } + + public VoteAttestation getVoteAttestation() { + return attestation; + } + + @Override + public String toString() { + return "Snapshot{" + + "hash=" + hash + + ", number=" + number + + ", validators=" + validators + + ", candidates=" + candidates + + ", recents=" + recents + + '}'; + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Utils.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Utils.java new file mode 100644 index 00000000..373a5c6b --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Utils.java @@ -0,0 +1,20 @@ +package foundation.icon.btp.bmv.bsc2; + +public class Utils { + + public static byte[] copy(byte[] src) { + byte[] dst = new byte[src.length]; + int i = -1; + while (++i < src.length) { + dst[i] = src[i]; + } + return dst; + } + + public static int ceilDiv(int x, int y) { + if (y == 0) { + return 0; + } + return (x + y - 1) / y; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validator.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validator.java new file mode 100644 index 00000000..414e8f27 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validator.java @@ -0,0 +1,37 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.ObjectReader; +import score.ObjectWriter; + +public class Validator { + private final EthAddress address; + private final BLSPublicKey pubkey; + + public Validator(EthAddress address, BLSPublicKey pubkey) { + this.address = address; + this.pubkey = pubkey; + } + + public static Validator readObject(ObjectReader r) { + r.beginList(); + EthAddress address = new EthAddress(r.readByteArray()); + BLSPublicKey pubkey = new BLSPublicKey(r.readByteArray()); + r.end(); + return new Validator(address, pubkey); + } + + public static void writeObject(ObjectWriter w, Validator o) { + w.beginList(2); + w.write(o.address); + w.write(o.pubkey); + w.end(); + } + + public EthAddress getAddress() { + return address; + } + + public BLSPublicKey getPublicKey() { + return pubkey; + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validators.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validators.java new file mode 100644 index 00000000..0127d156 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Validators.java @@ -0,0 +1,84 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.List; + +public class Validators { + private final List validators; + + public Validators(List validators) { + this.validators = validators; + } + + public static Validators fromBytes(byte[] bytes) { + return Validators.readObject(Context.newByteArrayObjectReader("RLP", bytes)); + } + + public static Validators readObject(ObjectReader r) { + r.beginList(); + List validators = new ArrayList<>(); + while (r.hasNext()) { + validators.add(r.read(Validator.class)); + } + r.end(); + return new Validators(validators); + } + + public static void writeObject(ObjectWriter w, Validators o) { + w.beginList(o.validators.size()); + for (Validator validator : o.validators) { + w.write(validator); + } + w.end(); + } + + public Validator get(int i) { + return validators.get(i); + } + + public EthAddresses getAddresses() { + List addresses = new ArrayList<>(); + for (Validator validator : validators) { + addresses.add(validator.getAddress()); + } + return new EthAddresses(addresses); + } + + public List getPublicKeys() { + List keys = new ArrayList<>(); + for (Validator validator : validators) { + keys.add(validator.getPublicKey()); + } + return keys; + } + + public boolean contains(Validator validator) { + return validators.contains(validator); + } + + public boolean contains(EthAddress address) { + for (Validator validator : validators) { + if (validator.getAddress().equals(address)) { + return true; + } + } + return false; + } + + public boolean contains(BLSPublicKey pubkey) { + for (Validator validator : validators) { + if (validator.getPublicKey().equals(pubkey)) { + return true; + } + } + return false; + } + + public int size() { + return validators.size(); + } +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteAttestation.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteAttestation.java new file mode 100644 index 00000000..5e3e5a40 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteAttestation.java @@ -0,0 +1,88 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +public class VoteAttestation { + public static final int BLS_SIG_LENGTH = 96; + private final Voters voters; + private final byte[] signature; + private final VoteRange range; + // reserved + private final byte[] extra; + + public VoteAttestation(Voters voters, byte[] signature, VoteRange range, byte[] extra) { + this.voters = voters; + this.signature = Utils.copy(signature); + this.range = range; + this.extra = Utils.copy(extra); + } + + public static VoteAttestation fromBytes(byte[] bytes) { + return VoteAttestation.readObject(Context.newByteArrayObjectReader("RLP", bytes)); + } + + public static VoteAttestation readObject(ObjectReader r) { + r.beginList(); + byte[] bitset = new byte[1]; + bitset[0] = r.readByte(); + Voters voters = new Voters(bitset); + byte[] signature = r.readByteArray(); + Context.require(signature.length == BLS_SIG_LENGTH, "Invalid signature"); + VoteRange range = r.read(VoteRange.class); + byte[] extra = r.readByteArray(); + r.end(); + return new VoteAttestation(voters, signature, range, extra); + } + + public static void writeObject(ObjectWriter w, VoteAttestation o) { + w.beginList(4); + w.write(o.voters.bitset[0]); + w.write(o.signature); + w.write(o.range); + w.write(o.extra); + w.end(); + } + + public VoteRange getVoteRange() { + return range; + } + + public boolean isSourceOf(BigInteger number, Hash hash) { + return range.getSourceNumber().compareTo(number) == 0 && range.getSourceHash().equals(hash); + } + + public boolean isTargetOf(BigInteger number, Hash hash) { + return range.getTargetNumber().compareTo(number) == 0 && range.getTargetHash().equals(hash); + } + + public void verify(Validators validators) { + byte[] aggr = aggregate(validators); + Context.verifySignature("bls12-381-g2", range.hash(), signature, aggr); + } + + public byte[] aggregate(Validators validators) { + Context.require(voters.count() <= validators.size(), "Invalid vote - larger than validators"); + + List keys = new ArrayList<>(); + for (int i = 0; i < validators.size(); i++) { + if (!voters.contains(i)) { + continue; + } + keys.add(validators.get(i).getPublicKey()); + } + Context.require(keys.size() >= Utils.ceilDiv(validators.size() * 2, 3), "Short quorum"); + + byte[] aggregation = null; + for (BLSPublicKey key : keys) { + aggregation = Context.aggregate("bls12-381-g1", aggregation, key.toBytes()); + } + return aggregation; + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteRange.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteRange.java new file mode 100644 index 00000000..bd4b59bb --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/VoteRange.java @@ -0,0 +1,65 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class VoteRange { + + public final BigInteger sourceNumber; + public final Hash sourceHash; + public final BigInteger targetNumber; + public final Hash targetHash; + + public static VoteRange readObject(ObjectReader r) { + r.beginList(); + BigInteger sn = r.readBigInteger(); + Hash sh = r.read(Hash.class); + BigInteger tn = r.readBigInteger(); + Hash th = r.read(Hash.class); + r.end(); + return new VoteRange(sn, sh, tn, th); + } + + public static void writeObject(ObjectWriter w, VoteRange o) { + w.beginList(4); + w.write(o.sourceNumber); + w.write(o.sourceHash); + w.write(o.targetNumber); + w.write(o.targetHash); + w.end(); + } + + public VoteRange(BigInteger sn, Hash sh, BigInteger tn, Hash th) { + this.sourceNumber = sn; + this.sourceHash = sh; + this.targetNumber = tn; + this.targetHash = th; + } + + public BigInteger getSourceNumber() { + return sourceNumber; + } + + public Hash getSourceHash() { + return sourceHash; + } + + public BigInteger getTargetNumber() { + return targetNumber; + } + + public Hash getTargetHash() { + return targetHash; + } + + public byte[] hash() { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLP"); + writeObject(w, this); + return Context.hash("keccak-256", w.toByteArray()); + } + +} diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Voters.java b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Voters.java new file mode 100644 index 00000000..68d4dd52 --- /dev/null +++ b/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/Voters.java @@ -0,0 +1,49 @@ +package foundation.icon.btp.bmv.bsc2; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +public class Voters { + + public static final int BITSET_BYTES_LENGTH = 8; + public byte[] bitset; + + public Voters(byte[] bitset) { + Context.require(bitset.length <= BITSET_BYTES_LENGTH, "Invalid bitset size"); + this.bitset = new byte[8]; + System.arraycopy(bitset, 0, this.bitset, 0, bitset.length); + } + + public int count() { + int cnt = 0; + for (byte b : bitset) { + for (int j = 0; j < 8; j++) { + if ((b >> j & 1) == 1) { + cnt++; + } + } + } + return cnt; + } + + public boolean contains(int v) { + Context.require(v <= 64, "Invalid voter index"); + int index = v / 8; + Context.require(index < BITSET_BYTES_LENGTH, "Invalid bitset access"); + return (bitset[index] >> (v % 8) & 1) == 1; + } + + public static Voters readObject(ObjectReader r) { + r.beginList(); + byte[] bitset = r.readByteArray(); + r.end(); + return new Voters(bitset); + } + + public static void writeObject(ObjectWriter w, Voters o) { + w.beginList(1); + w.write(o.bitset); + w.end(); + } +} diff --git a/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/BMVTest.java b/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/BMVTest.java new file mode 100644 index 00000000..00cbea13 --- /dev/null +++ b/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/BMVTest.java @@ -0,0 +1,65 @@ +package foundation.icon.btp.bmv.bsc2; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; +import foundation.icon.btp.lib.BMVStatus; +import foundation.icon.btp.lib.BTPAddress; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BMVTest extends TestBase { + static final ServiceManager sm = getServiceManager(); + static final Account BMC = sm.createAccount(Integer.MAX_VALUE); + static final BTPAddress BMC_BTP_ADDR = BTPAddress.parse("btp://0x1.icon/cx123"); + + public static Score deployBmv(DataSource.Case.Deployment deployment) throws Exception { + return sm.deploy(sm.createAccount(), BTPMessageVerifier.class, + BMC.getAddress(), deployment.getChainId(), deployment.getHeader(), + deployment.getValidators(), deployment.getCandidates(), deployment.getRecents() + ); + } + + public static void handleRelayMessageTest(DataSource.Case c, Score bmv, String prev) { + System.out.println("case: " + c.getDescription()); + for (DataSource.Case.Phase p : c.getPhases()) { + System.out.println("phase: " + p.getDescription()); + byte[][] ret = (byte[][]) sm.call(BMC, BigInteger.ZERO, bmv.getAddress(), "handleRelayMessage", + BMC_BTP_ADDR.toString(), prev, BigInteger.valueOf(0), p.getMessage()); + + if (p.getResult().size() > 0) { + assertEquals(p.getResult().size(), ret.length); + for (int i=0; i cases = DataSource.loadCases("testnet.json"); + @TestFactory + public Collection handleRelayMessageTests() { + List t = new ArrayList<>(); + for (DataSource.Case c : cases) { + t.add(DynamicTest.dynamicTest(c.getDescription(), + () -> { + Score bmv = deployBmv(c.getDeployment()); + handleRelayMessageTest(c, bmv, ""); + } + )); + } + return t; + } + } +} diff --git a/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/DataSource.java b/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/DataSource.java new file mode 100644 index 00000000..548f53d5 --- /dev/null +++ b/bmv/bsc2/src/test/java/foundation/icon/btp/bmv/bsc2/DataSource.java @@ -0,0 +1,101 @@ +package foundation.icon.btp.bmv.bsc2; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import foundation.icon.btp.lib.BMVStatus; +import foundation.icon.score.util.StringUtil; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.List; + +@ToString +@Getter +@NoArgsConstructor +public class DataSource { + public static DataSource loadDataSource(String filename) { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + return mapper.readValue( + DataSource.class.getClassLoader().getResourceAsStream(filename), + mapper.getTypeFactory().constructCollectionType(List.class, DataSource.class)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static List loadCases(String filename) { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + InputStream is = DataSource.class.getClassLoader().getResourceAsStream(filename); + return mapper.readValue(is, new TypeReference<>() {}); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private List cases; + + @ToString + @Getter + @NoArgsConstructor + public static class Case { + private String description; + private Deployment deployment; + private List phases; + + @ToString + @Getter + @NoArgsConstructor + public static class Deployment { + private String header; + private BigInteger chainId; + private String validators; + private String candidates; + private String recents; + + public byte[] getHeader() { + return StringUtil.hexToBytes(header); + } + public byte[] getValidators() { + return StringUtil.hexToBytes(validators); + } + public byte[] getCandidates() { + return StringUtil.hexToBytes(candidates); + } + public byte[] getRecents() { + return StringUtil.hexToBytes(recents); + } + + public static byte[][] toBytesArray(List o) { + byte[][] ret = new byte[o.size()][]; + for (int i = 0; i < o.size(); i++) { + ret[i] = o.get(i).getEthAddress(); + } + return ret; + } + } + + @ToString + @Getter + @NoArgsConstructor + public static class Phase { + private String description; + private String message; + private List result; + private BMVStatus status; + + public byte[] getMessage() { + return StringUtil.hexToBytes(message); + } + } + } +} diff --git a/bmv/bsc2/src/test/resources/testnet.json b/bmv/bsc2/src/test/resources/testnet.json new file mode 100644 index 00000000..aaaae482 --- /dev/null +++ b/bmv/bsc2/src/test/resources/testnet.json @@ -0,0 +1,91 @@ +[ + { + "description": "Normal Block Updates(SP: 30000000)", + "deployment": { + "header": "f904aca012001aa02f91851255db861216e8a427ad74b7cb79ae71622d05d55e4e55fb80a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a073c8b32cc7caffd58a0cc1251eaa77901b9aadf2724c8ad0a463611080e763b0a005ff8001909de666ecc58e694004469770dfbbfd27408321b8d9e6efe245be6fa0c78e61479048aa0769840458d42996969c58dcaa151633bfc6a81571adb65ce1b9010000000000000000100000004400000000020000000000000200200000020000000000500000000000000010000000010000000000008200000200000000000000000000000010a000000000090028000020100000010000000000000000000800000001240202004000400000000008000800000000000000000000100000000000000000200000010000000000000000100004c000000000000000000000002000200000100000008002000002000000080000080000100000100020000100000010000a000000000000080000000000004000800010000000004082000020000000000000000002411000040000004004008000000000080100200000000000028401c9c3808402f7f59183075c6484646a60cfb902aed883010202846765746888676f312e31392e39856c696e7578000000110bea95061284214b9b9c85549ab3d2b972df0deef66ac2c9ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bc35552c16704d214347f29fa77f77da6d75d7c752b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2980a75ecd1309ea12fa2ed87a8744fbfc9b863d589037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bb71b214cb885500844365e95cd9942c7276e7fd8a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff474cf03cceff28abc65c9cbae594f725c80e12d96c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878f8b23fb860b3f778ddf7c680a28497fc68cb3edb48e18772505b1d05cee518786c8c8b5d0f181af4dca06c07e3661491ca84ab60db05471852177867e1333acedbd190366fa198b5d6004b2e16cf2547ea7a5aab82276c5d43bba70a7576b67f1fdf056744f84c8401c9c37ea03aab2fa5d001f5fd5288b315d48d44f9afaa04b96d8173039b02d21c651219e58401c9c37fa012001aa02f91851255db861216e8a427ad74b7cb79ae71622d05d55e4e55fb80803436e732745bf4f4fc2d5f2bbef5f4ae419cd680299e8b4dc966141731cea2f86459cac17a5800bd9617ac74fc164f5f550a5570f373f89745b69a143753d3d801a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "chainId": 97, + "validators": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "candidates": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "recents": "f85494a2959d3f95eae5dc7d70144ce1b73b403b7eb6e094b71b214cb885500844365e95cd9942c7276e7fd894f474cf03cceff28abc65c9cbae594f725c80e12d941284214b9b9c85549ab3d2b972df0deef66ac2c9" + }, + "phases": [ + { + "description": "BU{ 30000001 ~ 30000005 }, FN{ 30000001 ~ 30000003 }", + "message": "f90f7bf90f78f90f7501b90f71f90f6ef90313a05001074f681ec00a1b740853667698f0f811a3e526d82628be8d9be902acd9e8a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479435552c16704d214347f29fa77f77da6d75d7c752a08701826b6a71f13d179cdc1fa89f057a2aa00c990ea3ec71f4a787e8d45b5479a042ffdf0b444a75676f89a51f7d34e45b3baa3ca24a984b17bfbf85b4bb8b8226a097efb12b970466dca685419e953324ba7c11855d4273c9e350ddd3d404eb64eeb901000000000000004000081000020000000000000000000400000000020000100000000000000000000000000000000400800808420000000000400010001001000800000000000410004000000200000000241000004010a008004000000000000000008820020200000000000010001800080800c00000000000000000000000000200000000000000000000000000000000000442000000000001000000000020000000000000000000020000200000200000000000000000008000020000000000000000000080000000000008120000000000800000000010000082000020010000010000000102012000002000000000000000080000000000020000000000028401c9c3818402faed858303b43884646a60d2b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a4c7b583f3bef96bf8bdf58329bf058225d9c39b6f7a79f266f46d7771198d3036e55c6bc6bb13442988e4c53cf8558f00c37e5fda8f8f3e4cabb6592ed3f7c2a317614adf795d09bf5cafbfbdb00b51af650c3d2b48bf3ea96502e505f93cbef84c8401c9c37fa012001aa02f91851255db861216e8a427ad74b7cb79ae71622d05d55e4e55fb808401c9c380a05001074f681ec00a1b740853667698f0f811a3e526d82628be8d9be902acd9e880958bcaef2e340761f634b04c89bbf4b64e3b5912d43a7bc312b613af74e3779821dded30367e7e34c1c0d9adb3b77859e8c69df302ee003f4521caae7b613ad201a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0057dc95d4690a22b329e978e95b8b9e22b8d06becf22da0180a4d9faaa15f7eca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794980a75ecd1309ea12fa2ed87a8744fbfc9b863d5a049e8950781a22b1e8a776c4dafa32472e4962ceef8fad2b9a7572afa67a04eaca0e0c550bbda4f3b3523dc22c7268bcec4c0e79102ec67ccd6e02c55564b427fb6a05bff45064513e677f0ee03585cc6fee443f0f04f6f76852974e76810cf7d4d2cb9010000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000020000200000040000000000000080000000000000000000000000000000000000000000000000000000000000010000400000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000010000000000000000000000000000000000000000000000028401c9c3828402faf080830d494384646a60d5b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb8608267f90a9944227899bcce1b3b1e887f65fd64d233b365e3e3059aae26e6656ae1dc0bce8cceb5bbba0164b352c356040614573f897530b5283754f4668536f8bca21c5f697375e43b6b4f09efa0fa43439599ea0dabe5365b5496ae906d71f3f84c8401c9c380a05001074f681ec00a1b740853667698f0f811a3e526d82628be8d9be902acd9e88401c9c381a0057dc95d4690a22b329e978e95b8b9e22b8d06becf22da0180a4d9faaa15f7ec800d4c76bb213af36bc11dc3e6596c59c93d4b99c36b7cde6ac97f39106629d3511fc3fb60b7ff0de36cf9a5a985eb5cfb43ddfd797bd3422bc10a50459087a21400a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a046cc0a8ace9c1a60480b96b64ff0d617210c8c8af18a23153a52cf5c09c3968da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0a05b3232936859b808d1cdd332af640a2afa645ce078a10ac8271f411f826ffaeda0409dc46b874cd17820a332db55d23b5eef35679ac44a5a9b8eb9eff9f413a84fa0ec487eda5bd21c10befbf65c5e106bb5de66c690ece47544d8aa57e0c06b0b07b9010000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000010000000000000000000080000000000800000000201100000000000000000000000000000000002002020000000000000000080008040000000000000000001000000040200000002000000000000000000000000000040000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000a000000000000800000000000000000000000000000000002000020000000000000000000010000000010000000000000000000000000000000000000028401c9c3838402faf0808319c34884646a60d8b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860aa51d22c5519a8b3f47ba9f7c4b1942cbd28bac5dcfbf1e7214092a347e04632b47c4067501acb067e3527c30750d9250049658ac9b3ddc85e33eefd323e1da8ac3502003d879f9a93409d94a4f77f42f761e42a62f88086086cf629961c12eaf84c8401c9c381a0057dc95d4690a22b329e978e95b8b9e22b8d06becf22da0180a4d9faaa15f7ec8401c9c382a046cc0a8ace9c1a60480b96b64ff0d617210c8c8af18a23153a52cf5c09c3968d802650c7771fa8d5c1f4e12c4a08d9d70f69f84370f5f60e6671ad949585b1a8e246e294e61e340c79b9856329525639e81e1a59ab0609ffc7e392ea24eaa755cd01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a007611eb60d4b6fd7c185d3c5c6970690031f6740727a4d993e912dd8ea85546ca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a039e678d55a1dac27aa9b19054a55a9c2e5665fc140c079efc0ebeca39c46f63fa0a888207ddc85f2435f363eccd4c06e8109f93d59147dceb5a3f8cf3dfe2de2eda0616fa0c8058a39e78108627031d8c4d79f6f62de6b161a3018964913789fb419b9010040100000200000120000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000010008010000000040008000800002010000000400001000000000000080000000020020300400000000800000800080000000000000000420410000080000000000000000000000000010000001000000c00000000000000000200000020000400000000410000000000000000800000000000000000000040000000200002000002000000000000000000000000080001000000000000200002080020000000480000000000010000000000004000000000040000000000000000000000028401c9c3848402faf0808302e14684646a60dbb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb86096eb48b6a9f992228ebf17fd8592edf8121c82f80bf71e848e295e55b8284e5910225c584e823bc2b142a36a988ebb640df490d848e029ab3b4ef97e6e964ed7c5f0453604d9da1321790258387de89f9de4fb41aab423db283ba5cc3683b7cff84c8401c9c382a046cc0a8ace9c1a60480b96b64ff0d617210c8c8af18a23153a52cf5c09c3968d8401c9c383a007611eb60d4b6fd7c185d3c5c6970690031f6740727a4d993e912dd8ea85546c80e7bc88e061fce15c3593c46ac49d055e5402bbc6d43c3c5221e4afbd4fbe645419b4601215d6c81d19a995682859fbe7779c7f6321f65ccbbaf22d84d3cce2de00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0172221121fda86d043b652a3cde6e61430b4c2cf0b7e670b2af01b93e7e703b7a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0c4a4b48049ee8308c9b06c42d4f3acc5c8efd3a031b4cf9b1cf4cd91a269b0a3a067ed66c2795e664ae4f8474897ce561b4ee3198aa8011788b796908fcc0d878ba0967fd5c698bcdb676c7077d4e04c838b37be0a86f74f5070572e042579cd0602b9010080000000000000100000000000000000000000000000002000000000000040020000000000000000000000000000000020000000000000000200000000a00000000400000000200000000008000000002010000000000000000000000000000000000020020200000000000000000800080000000000000000000010040000000000000000000001000000000000000000000400000000000000000000000020020000000000000000000000000000000000000800000000000002200000000000100002000000000000000000000000000000000000000000000002000020000010000000000000010000000000004000100000000000000004000000000000028401c9c3858402faf08083020b2a84646a60deb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb86087d0b9604bd2fc42ee1dcd93669df0117f7800a2b22cefed104fdaf4498dc0e35a2e7340582e970abc7e86463a8b243505b84319372c3c3907acf7647ae07053f0722cd7e9919659a04e55d318dc44e30ae25fa38a122886051e1fc9fdeed1b2f84c8401c9c383a007611eb60d4b6fd7c185d3c5c6970690031f6740727a4d993e912dd8ea85546c8401c9c384a0172221121fda86d043b652a3cde6e61430b4c2cf0b7e670b2af01b93e7e703b78089566bea85eb9b833f08c4807649f6ae1c65109b8a99b5c7ead48496ef5f8c2b0fcb28efae6364147c8af18c1e57b0a8e2bbb9d6068104ddaf53e5c458bb638801a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "result": [], + "status": { + "height": 30000003, + "extra": "" + } + }, { + "description": "BU{ 30000006 }, FN{ 30000004 }", + "message": "f90323f90320f9031d01b90319f90316f90313a0e25a2f94d858eb7a2a27a02e67b98e8152ff4df6bca57b4b7ad0ee918f4e6384a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a01eca1ec3fae077ebc05114cf1de1196809d752d9bb6a2e059bbc7bc15f082faca0b53d3fd7a76fc5b657c6455aec91ce93ed8087c19fa800eb50f46e6a4a91203ca06e029f5680501773ff584e669ad37617c0035ded167dc58a82795a247f23e7d7b9010000000004000000000000000000000000100000000008000100000000000000000000021000000010000000000000004000000000000000000000020000200000000000000000000000800028000000002010000000000000000000000000000000000020020200000000000000000808080000400800000000000010001000000000000000000000000020000000008000000440000080000200002000000020020000000902000000000000000000000000000000000000001000000000000200200002000000000000000004000040001000080010000100102002100020000010000000000000498000000400000000000400010000080000000000000000028401c9c3868402f7f591835cd18c84646a60e1b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a75836ce992bea381bceea6cf2e68e75afad5b9ab74d8a5b69aa577c0a4740c63316de854690223a8b008ae214abdd06163c1ab82771d386c641aa5092ebdeba5dd7eaac5c833ace5495c808c75fe2b06749678540f3c52cac754e10277ca62df84c8401c9c384a0172221121fda86d043b652a3cde6e61430b4c2cf0b7e670b2af01b93e7e703b78401c9c385a0e25a2f94d858eb7a2a27a02e67b98e8152ff4df6bca57b4b7ad0ee918f4e638480d20e66004eca29ebfc8acb302d4a7c7c5b7f4bc4d6e2182687da8b401709e23c152f01ca1cda4050046a6fc636880e26ad12cca31ad5d6444188904afe3319c901a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "result": [], + "status": { + "height": 30000004, + "extra": "" + } + } + ] + }, { + "description": "Handle Empty Vote Attestation (1) (SP: 30021100)", + "deployment": { + "header": "f90313a05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a05c086fd546b1c5831fdb4779c721d393cffe85415edc8aaa3b5c188cd53ecf08a07b8166e2fd42b6ce20236e1eca3ddb46dee78a2c1412590840859a14501da715a05bd4c318fa16ded7bf258daf9443062e291f9c254e41415f82f9a2ca8b35092db9010000000000000040000010000200000000000000000004000000000200001000000000000000000000000000000004008000080000000000000000100010000000000000000014000040000002000800002410000000000008004000000000080000000820020200000000000000001800080800c00000000000000000000000000200000000000000000000000000000000000440000000000001000000000020000000000000000000000000200000200020000000000000008000020000000000000000000080000000000008020000000000000000000000000002000020010000010000000000010000402000000020000000080000000000000000000000028401ca15ec8402faf080830470ab84646b5813b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860ac57cd4c79cd37baaabcfa6a1a438e39474ac6218eefa19cb8585f2d46e66478af772987db7537a8c17d1304d0bbe1e10136c4d537244967831d628581488707fb931f0325ee140bd9310ecc83a13882d075ee0d938bfefac05037b2e543eec5f84c8401ca15eaa04631ccb60c91685a754734d24132bf54cc23441a83779eec6723e75c0462a7218401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059801f9651750a7e693db5b6edcaa6e27796d61e2962e0160e75e23e8bf72315a9064dbbe1978375475bde166e84be205e483e8ca05bc7ffe624d077b748a76a807b01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "chainId": 97, + "validators": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "candidates": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "recents": "f8549435552c16704d214347f29fa77f77da6d75d7c75294980a75ecd1309ea12fa2ed87a8744fbfc9b863d594a2959d3f95eae5dc7d70144ce1b73b403b7eb6e094b71b214cb885500844365e95cd9942c7276e7fd8" + }, + "phases": [ + { + "description": "BU{ 30021100 ~ 30021107 }, FN{ ~ 30021104 }", + "message": "f914f5f914f2f914ef01b914ebf914e8f90313a0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a47a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0cd37a80ed7dcc51eca55ba1499bda091c3566fa8d52655050784060e97f9d3a2a0743b82b8917e7dff16e629d7e9bb12fafec91e6529331fa71f048317182a01f6a0c9731061ad717ed45d93d9b9520ec81247b4fe5a414c137a4a87ffff5329c5edb9010000000000000000024000000010000000020000000000000000000000000080000000008000000000000000000000000008000200000000000000000000000000000000000000200080000000000000002010000040020000000000000000000000008020000200000000008010000000080020000000200000000000240000000000000000000000000000000000000000000402000000000000000400000020000200000000800000000000000000000000000800000000000000000000000000100000000000402000000000100000000000000080000010000402001000000000000000000000012000000010000000000000000000000000002000000400028401ca15ed8402faf080830c7e4c84646b5816b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb8608b0902e51294837c51fa0136735f3cb53bf84e1e326a96e2e46e3c61dac77c58fd3364d7a8c763d024ff5e6ae5732e6d18cf5b81a0cfec44c5ac46c4e3d59a9f8c414d05bcf097cd00ee543f786b1c4e1cc5e78479060064bb090cb549a669f5f84c8401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb71490598401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a4780c76cee408f8930f6c92c94c329ade71c4ac2aa4ec60c2bb73e8cc686344b2fdf4ba14789bb163b9cccfd3473a5ac9d793a58907789dfdcc11e711aedaeb5ec1e01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c6137a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a054e0d60c1191dbbf8e9b93cb1894bf7df53a84f9a9cee2f6966174ef55e352d0a0e5af895bf69713a61bf9b62baa847fd762bc8d57b2ae885b62fc8a3b237c08b3a00043321aa7197c61489011b04c51ea727387aaf868b59f9a254ca6bee3a1c9e4b901000224000000004008000801208000000400000000000200000800000200184000000060000002040000000000000040000200000000000080410400000000200000000000000000800000000890000020201000000002000000000100c20400002004002000020800004000000040082008000000000888800000001000000000010100000000100000000000000000000000044100000008000020c000008422000000201000040000000000800000400800000000040000000000000000000090000022000000000800100000000000010000000400001001004002800000010800000210000400410000010000000200000400400000680004000000800000028401ca15ee8402f7f5918311f61084646b5819b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b7ee80d64faa3f3454b16463c3a96b9a80b6fd1ad843cdbfc21b39c9f71130402d1b7c94a02a55bb785561e4f6c811f30f65d1e46f808d9cab94da3af78e8ccd87281af11ab45f5b5589f7a78f408656f37ff2bddceb7ca4ef169034a441e231f84c8401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a478401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c613780d0c91411800095fb6441df9eb850aaf100c27082a02d521616c5f532a9c5e7fd2a85d13ce5af53172b36aa28de26886d030795427905fb744a496664ea88db2800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479435552c16704d214347f29fa77f77da6d75d7c752a0419fce6b7304d9f48e07d4967a5a66368556b1dbe7be6e55dee2031ae7a0b9cca0feddb8208133abb1750095d2ce14faf3e831ad6c9d8483596be0def3ea4ace3ca0e404e2ee8fb6ce1e8cda8912aded298f17da3fef59f506dea593149e555fddaab90100022040002800420058180502c01044150010108101c4008028084240013000200000401001060200800082000004c08002c8001090000080400430001420080000100040010404806800148a060010702410108200010008004401004304040520440828320200000000200040405831080800e000400000000504102000009003000000402002401010a0800000008002080c42004000080869204000808022020008201040040000008040200ac02049200a244004000c8881028200008080580060022000a000001020980c0206440000108000040090000400a3800062010010818c00000002411004002010000000600401082008200004800002820000028401ca15ef8402faed858401efdee584646b581cb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b5b0d61ac44471707c874973a4b8c26db8b4dde18b81667e55e031b022eb4a574a42142e9bc8e54c9113c045c69a52180b7615ad4b15167a247b9e114d92ed9eaa360de111f4f94e20c520383cc8fd163edc7d15b9f3c7b41c010dcd2a0004b6f84c8401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c61378401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc80e1c158922511e0958ee9f9b48862d64d0e397a90d9c4e47344d05ef3ed271df634cc4a6de621462af31b75abcc5e137dfd80c73746a7734192d6875f957b06ba00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f37a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794980a75ecd1309ea12fa2ed87a8744fbfc9b863d5a01cb0ebc7e0eae672b4235f5e81861ca86b5136f0abbd2051c31309535ca10566a04c44ae2b7b8e108d37b4c0144792e83a0c24df4ac6312b7fbd752b985fb4650ba025b8bc67a2d3c536e5e00ad73815ba38e462006cfb657bf9486c0ffec2474617b9010000001000010000800000000002000000000000000000000000000000004000000000400000000000000000000000000000010000000000000000000200000000000000000000000080000008000000002012000000000000800000000000000000000020020200000040080000000800080000000000000000000010000000000000000000000000800000000000000010000400001000000000000000400020000000000000000000000000000000000000000100000000000000000010000000000002000000000000000000001000000000000000000000000002040220000000400000000000010000000000000000000000000000000000010000000000028401ca15f08402faf08083283c3684646b581fb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a1cfa217e5e6b34332095405684b8616c20f53d3069b50fff74b2ef5fa0a7471a57199567f5bdd04ab75f553925f14fc11f682b97cedd7fa5172d0b40e76d313b7c455337ccc75c3de1651b4ca736a2813e5f8285a777d8d78181d56d7197c35f84c8401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f3780fa61b9e6fb846255f5d34a429e1a9b6f8c7a7f7450e19814973c335b50b515bc6005badccadf0cbb3a9661643f6d18105d1782e08901719f6c68be7096ac512400a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0a06efa50dd78d0e314559fee83cb8520a25ebe62a46709b4caf27fc834f759b61ea03307559cfb8d63ecf2b9b89c2ff2bb5a7907a48cf4d8f4c4499c3f7234413b96a0abecbe1eea73ae2197d0de0eee62b422e2b90448d4c62db09827f1c85177886cb90100000580000202000100000805600024010000000004400882002880008000004004000000000002000103000000000000008024200180008200000400002000090230004002008602008000080000004120100213000800800240040000002000200000280002080020102010580000210a002030000200100049101100000100810000006020008801002000020000800108040880008000080900200000203002200800006028042800001000000000400014a0000000020000f000260002000000100a00080020501000000040328401000014000c000000108002000009008410000102000000410280000800000100000112004020200000800000200000028401ca15f18402faf0808401deda6a84646b5822b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb86082088a5edce097f357e9ca8d54c64e474726a1cb243ab7f5808add2d9d5464ac190fb57d26fdcd6711a44f8d1faa5ee3069dd8c7f8c715561f35bed84b930a5f812b220e7daf3baf7015a8bb308ffefe2fe17f5df54653418825edeec33fbc46f84c8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f378401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20803bbbba57aedad4ca8196e2bec99d1baeec509ad05c9a4e3d71c5f11cf251891a5ad375bb0a6971dd13f181066f85cb36ed5e6122d6e4a90ddf349862174bd49900a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d42415a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a0312923c91311971931d93b6c0279a67700eb7196f8709cabfee51b2e2577c3f1a0e5a3b5802764e55809e0e5f4ce73fe5ad71e4e6dd16a77034dd769c4c260881aa049de6fcfd1e1d27e1d587998f08c3046db8f989ae2a6cbd8fc96555082e540c3b9010000220000004440000011002280000001201040000205408180080202001000000028000a0000000028000200040c4080020888306010004000049008102200100c52200100340d044000000a221c007124120002102001090940c1084a10084024000828060620002400384040001834092c02e00000804408010018000000804209010061301000040030841000000000002c400004100c001920601040e020020800000041010000100c4020010028412801e0008300822084021200408000001000024002800000b02040080282f4822404040014201000800022000022310a140100000000001100220030040000201020000800000c1000800000022000028401ca15f28402faf0808401eafbde84646b5825b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a136318978f80df8112277c462401344f2b3f32da0fbb29a034d256f8769ba726a3aa89b20cb99973d6b1c8602ed06e91245ae2a2148b542f06afc84fdf2346eba7b5915be4b95bcc8becff88e0ac786501b5790de301e5f83b34ef6e9ea4130f84c8401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d208401ca15f1a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d4241580900c814301560face6d3e3b1aab7ba028e487caa25dd6b9fa902e1c914134122193ab7d21c47eaadca0fbc3d409d869e33a194038ed28ba3c2c158ab6354ea0300a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f9025ea0c93306ee601e84948d4e6e68e2893fc1255fbdd60c7f41f8e43a10007d542486a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0af33ee524362f3fe3dad564f88e4a3784741bdd9f5869fbee0ef3bb1b1d961bfa0061ab611f8ed6a2719b5ebdaf4ca87f51f77adeda14b1aab90f76f7313c92066a088f1623e3214125bce9f5e15fe5a400a2559b495e85b022f3136da1d6aff9e4bb9010004200000000000000000004080000002000000000000000000000000000002000000000000200410000080000000000000000000000008000000000800240000080004000000200000000208000100202410000000048000000000000000000000000020020200000000000000040800080040001000000000080010000000080200000000000000000000000000000000000400000000080040084000000020020000000000000000000100000000000080000800000000000020000404000000100002000000000000000000000000000060000000001000000002000060000010000000000000010000000000000000000000008000000000000000000000028401ca15f38402faf0808307528884646b5828b861d883010202846765746888676f312e31392e39856c696e7578000000110bea95c8fd8fa3756bfe308ff03e54a99d6243ed4a70baa630a9b59c4f37a230ec4b7452fd7502c4134b809f40d42b94d91d79135c114ed57413cbf29a91b94ff2e17200a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "result": [], + "status": { + "height": 30021104, + "extra": "" + } + } + ] + }, { + "description": "Handle Empty Vote Attestation (2) (SP: 30021100)", + "deployment": { + "header": "f90313a05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a05c086fd546b1c5831fdb4779c721d393cffe85415edc8aaa3b5c188cd53ecf08a07b8166e2fd42b6ce20236e1eca3ddb46dee78a2c1412590840859a14501da715a05bd4c318fa16ded7bf258daf9443062e291f9c254e41415f82f9a2ca8b35092db9010000000000000040000010000200000000000000000004000000000200001000000000000000000000000000000004008000080000000000000000100010000000000000000014000040000002000800002410000000000008004000000000080000000820020200000000000000001800080800c00000000000000000000000000200000000000000000000000000000000000440000000000001000000000020000000000000000000000000200000200020000000000000008000020000000000000000000080000000000008020000000000000000000000000002000020010000010000000000010000402000000020000000080000000000000000000000028401ca15ec8402faf080830470ab84646b5813b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860ac57cd4c79cd37baaabcfa6a1a438e39474ac6218eefa19cb8585f2d46e66478af772987db7537a8c17d1304d0bbe1e10136c4d537244967831d628581488707fb931f0325ee140bd9310ecc83a13882d075ee0d938bfefac05037b2e543eec5f84c8401ca15eaa04631ccb60c91685a754734d24132bf54cc23441a83779eec6723e75c0462a7218401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059801f9651750a7e693db5b6edcaa6e27796d61e2962e0160e75e23e8bf72315a9064dbbe1978375475bde166e84be205e483e8ca05bc7ffe624d077b748a76a807b01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "chainId": 97, + "validators": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "candidates": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "recents": "f8549435552c16704d214347f29fa77f77da6d75d7c75294980a75ecd1309ea12fa2ed87a8744fbfc9b863d594a2959d3f95eae5dc7d70144ce1b73b403b7eb6e094b71b214cb885500844365e95cd9942c7276e7fd8" + }, + "phases": [ + { + "description": "BU{ 30021100 ~ 30021108 }, FN{ ~ 30021104 }", + "message": "f9180cf91809f9180601b91802f917fff90313a0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a47a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0cd37a80ed7dcc51eca55ba1499bda091c3566fa8d52655050784060e97f9d3a2a0743b82b8917e7dff16e629d7e9bb12fafec91e6529331fa71f048317182a01f6a0c9731061ad717ed45d93d9b9520ec81247b4fe5a414c137a4a87ffff5329c5edb9010000000000000000024000000010000000020000000000000000000000000080000000008000000000000000000000000008000200000000000000000000000000000000000000200080000000000000002010000040020000000000000000000000008020000200000000008010000000080020000000200000000000240000000000000000000000000000000000000000000402000000000000000400000020000200000000800000000000000000000000000800000000000000000000000000100000000000402000000000100000000000000080000010000402001000000000000000000000012000000010000000000000000000000000002000000400028401ca15ed8402faf080830c7e4c84646b5816b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb8608b0902e51294837c51fa0136735f3cb53bf84e1e326a96e2e46e3c61dac77c58fd3364d7a8c763d024ff5e6ae5732e6d18cf5b81a0cfec44c5ac46c4e3d59a9f8c414d05bcf097cd00ee543f786b1c4e1cc5e78479060064bb090cb549a669f5f84c8401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb71490598401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a4780c76cee408f8930f6c92c94c329ade71c4ac2aa4ec60c2bb73e8cc686344b2fdf4ba14789bb163b9cccfd3473a5ac9d793a58907789dfdcc11e711aedaeb5ec1e01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c6137a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a054e0d60c1191dbbf8e9b93cb1894bf7df53a84f9a9cee2f6966174ef55e352d0a0e5af895bf69713a61bf9b62baa847fd762bc8d57b2ae885b62fc8a3b237c08b3a00043321aa7197c61489011b04c51ea727387aaf868b59f9a254ca6bee3a1c9e4b901000224000000004008000801208000000400000000000200000800000200184000000060000002040000000000000040000200000000000080410400000000200000000000000000800000000890000020201000000002000000000100c20400002004002000020800004000000040082008000000000888800000001000000000010100000000100000000000000000000000044100000008000020c000008422000000201000040000000000800000400800000000040000000000000000000090000022000000000800100000000000010000000400001001004002800000010800000210000400410000010000000200000400400000680004000000800000028401ca15ee8402f7f5918311f61084646b5819b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b7ee80d64faa3f3454b16463c3a96b9a80b6fd1ad843cdbfc21b39c9f71130402d1b7c94a02a55bb785561e4f6c811f30f65d1e46f808d9cab94da3af78e8ccd87281af11ab45f5b5589f7a78f408656f37ff2bddceb7ca4ef169034a441e231f84c8401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a478401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c613780d0c91411800095fb6441df9eb850aaf100c27082a02d521616c5f532a9c5e7fd2a85d13ce5af53172b36aa28de26886d030795427905fb744a496664ea88db2800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479435552c16704d214347f29fa77f77da6d75d7c752a0419fce6b7304d9f48e07d4967a5a66368556b1dbe7be6e55dee2031ae7a0b9cca0feddb8208133abb1750095d2ce14faf3e831ad6c9d8483596be0def3ea4ace3ca0e404e2ee8fb6ce1e8cda8912aded298f17da3fef59f506dea593149e555fddaab90100022040002800420058180502c01044150010108101c4008028084240013000200000401001060200800082000004c08002c8001090000080400430001420080000100040010404806800148a060010702410108200010008004401004304040520440828320200000000200040405831080800e000400000000504102000009003000000402002401010a0800000008002080c42004000080869204000808022020008201040040000008040200ac02049200a244004000c8881028200008080580060022000a000001020980c0206440000108000040090000400a3800062010010818c00000002411004002010000000600401082008200004800002820000028401ca15ef8402faed858401efdee584646b581cb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b5b0d61ac44471707c874973a4b8c26db8b4dde18b81667e55e031b022eb4a574a42142e9bc8e54c9113c045c69a52180b7615ad4b15167a247b9e114d92ed9eaa360de111f4f94e20c520383cc8fd163edc7d15b9f3c7b41c010dcd2a0004b6f84c8401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c61378401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc80e1c158922511e0958ee9f9b48862d64d0e397a90d9c4e47344d05ef3ed271df634cc4a6de621462af31b75abcc5e137dfd80c73746a7734192d6875f957b06ba00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f37a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794980a75ecd1309ea12fa2ed87a8744fbfc9b863d5a01cb0ebc7e0eae672b4235f5e81861ca86b5136f0abbd2051c31309535ca10566a04c44ae2b7b8e108d37b4c0144792e83a0c24df4ac6312b7fbd752b985fb4650ba025b8bc67a2d3c536e5e00ad73815ba38e462006cfb657bf9486c0ffec2474617b9010000001000010000800000000002000000000000000000000000000000004000000000400000000000000000000000000000010000000000000000000200000000000000000000000080000008000000002012000000000000800000000000000000000020020200000040080000000800080000000000000000000010000000000000000000000000800000000000000010000400001000000000000000400020000000000000000000000000000000000000000100000000000000000010000000000002000000000000000000001000000000000000000000000002040220000000400000000000010000000000000000000000000000000000010000000000028401ca15f08402faf08083283c3684646b581fb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a1cfa217e5e6b34332095405684b8616c20f53d3069b50fff74b2ef5fa0a7471a57199567f5bdd04ab75f553925f14fc11f682b97cedd7fa5172d0b40e76d313b7c455337ccc75c3de1651b4ca736a2813e5f8285a777d8d78181d56d7197c35f84c8401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f3780fa61b9e6fb846255f5d34a429e1a9b6f8c7a7f7450e19814973c335b50b515bc6005badccadf0cbb3a9661643f6d18105d1782e08901719f6c68be7096ac512400a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0a06efa50dd78d0e314559fee83cb8520a25ebe62a46709b4caf27fc834f759b61ea03307559cfb8d63ecf2b9b89c2ff2bb5a7907a48cf4d8f4c4499c3f7234413b96a0abecbe1eea73ae2197d0de0eee62b422e2b90448d4c62db09827f1c85177886cb90100000580000202000100000805600024010000000004400882002880008000004004000000000002000103000000000000008024200180008200000400002000090230004002008602008000080000004120100213000800800240040000002000200000280002080020102010580000210a002030000200100049101100000100810000006020008801002000020000800108040880008000080900200000203002200800006028042800001000000000400014a0000000020000f000260002000000100a00080020501000000040328401000014000c000000108002000009008410000102000000410280000800000100000112004020200000800000200000028401ca15f18402faf0808401deda6a84646b5822b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb86082088a5edce097f357e9ca8d54c64e474726a1cb243ab7f5808add2d9d5464ac190fb57d26fdcd6711a44f8d1faa5ee3069dd8c7f8c715561f35bed84b930a5f812b220e7daf3baf7015a8bb308ffefe2fe17f5df54653418825edeec33fbc46f84c8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f378401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20803bbbba57aedad4ca8196e2bec99d1baeec509ad05c9a4e3d71c5f11cf251891a5ad375bb0a6971dd13f181066f85cb36ed5e6122d6e4a90ddf349862174bd49900a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d42415a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a0312923c91311971931d93b6c0279a67700eb7196f8709cabfee51b2e2577c3f1a0e5a3b5802764e55809e0e5f4ce73fe5ad71e4e6dd16a77034dd769c4c260881aa049de6fcfd1e1d27e1d587998f08c3046db8f989ae2a6cbd8fc96555082e540c3b9010000220000004440000011002280000001201040000205408180080202001000000028000a0000000028000200040c4080020888306010004000049008102200100c52200100340d044000000a221c007124120002102001090940c1084a10084024000828060620002400384040001834092c02e00000804408010018000000804209010061301000040030841000000000002c400004100c001920601040e020020800000041010000100c4020010028412801e0008300822084021200408000001000024002800000b02040080282f4822404040014201000800022000022310a140100000000001100220030040000201020000800000c1000800000022000028401ca15f28402faf0808401eafbde84646b5825b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a136318978f80df8112277c462401344f2b3f32da0fbb29a034d256f8769ba726a3aa89b20cb99973d6b1c8602ed06e91245ae2a2148b542f06afc84fdf2346eba7b5915be4b95bcc8becff88e0ac786501b5790de301e5f83b34ef6e9ea4130f84c8401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d208401ca15f1a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d4241580900c814301560face6d3e3b1aab7ba028e487caa25dd6b9fa902e1c914134122193ab7d21c47eaadca0fbc3d409d869e33a194038ed28ba3c2c158ab6354ea0300a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f9025ea0c93306ee601e84948d4e6e68e2893fc1255fbdd60c7f41f8e43a10007d542486a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0af33ee524362f3fe3dad564f88e4a3784741bdd9f5869fbee0ef3bb1b1d961bfa0061ab611f8ed6a2719b5ebdaf4ca87f51f77adeda14b1aab90f76f7313c92066a088f1623e3214125bce9f5e15fe5a400a2559b495e85b022f3136da1d6aff9e4bb9010004200000000000000000004080000002000000000000000000000000000002000000000000200410000080000000000000000000000008000000000800240000080004000000200000000208000100202410000000048000000000000000000000000020020200000000000000040800080040001000000000080010000000080200000000000000000000000000000000000400000000080040084000000020020000000000000000000100000000000080000800000000000020000404000000100002000000000000000000000000000060000000001000000002000060000010000000000000010000000000000000000000008000000000000000000000028401ca15f38402faf0808307528884646b5828b861d883010202846765746888676f312e31392e39856c696e7578000000110bea95c8fd8fa3756bfe308ff03e54a99d6243ed4a70baa630a9b59c4f37a230ec4b7452fd7502c4134b809f40d42b94d91d79135c114ed57413cbf29a91b94ff2e17200a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a0245f3f377de5f866f4469240966bf4503b50a70700391dd6a7b67cc0d8b07a35a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a0e34d638607050905074ff81d1038709e034f265efa7dab25fd7d42e6ecfdd08ba04cf5ca7b67e27fe47eabf931d0a3c938be42844ed835b10ed7c15395768a5593a00ca4570aca7a3904ed797f45e48da3a9c8d271f330897e877a890d6b2d98fa69b90100042000000140408040100022c2000005000000080005008000488228001000008000000202040000008800000104d080000d00010000000022083200302000000010000000040411c080000a90001670241200068200010840400000400a000020000828220240128000c00040c81a00080880f0000000000021001000004100420000004020000000402080400080840004044000000008080d20400040102002000002104200a000002000200040204000002100000000108040020010800000024202000080488039240008021244001001000c44001050000002000222410010810600000404410800002008003020200201080080280000810001100010028401ca15f48402f7f59184015f312d84646b582bb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a7a66d0d630c4ae4bb8f5d793363d7f8cf04d85a920962d394b596d265667a3e44c0b2ec2bca885552ee4e766c5eb4570e25bbfddbef3f99598e031ec1efa59a4e4d5d5bfebe42283d3f9149a203254dba24bc2e1bbd33d1c3c9131ab5b3f38ef84c8401ca15f1a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d424158401ca15f3a0245f3f377de5f866f4469240966bf4503b50a70700391dd6a7b67cc0d8b07a35804bd93932f6f3e9347bd2156a9d2c44534160632aba264a15dc16907413e0289e3d80ef4893c00d6fd9e1877a21ecac58cf3b8a9aaa7f13e5388001d8e9b70c4100a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "result": [], + "status": { + "height": 30021104, + "extra": "" + } + } + ] + }, { + "description": "Handle Empty Vote Attestation (3) (SP: 30021100)", + "deployment": { + "header": "f90313a05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a05c086fd546b1c5831fdb4779c721d393cffe85415edc8aaa3b5c188cd53ecf08a07b8166e2fd42b6ce20236e1eca3ddb46dee78a2c1412590840859a14501da715a05bd4c318fa16ded7bf258daf9443062e291f9c254e41415f82f9a2ca8b35092db9010000000000000040000010000200000000000000000004000000000200001000000000000000000000000000000004008000080000000000000000100010000000000000000014000040000002000800002410000000000008004000000000080000000820020200000000000000001800080800c00000000000000000000000000200000000000000000000000000000000000440000000000001000000000020000000000000000000000000200000200020000000000000008000020000000000000000000080000000000008020000000000000000000000000002000020010000010000000000010000402000000020000000080000000000000000000000028401ca15ec8402faf080830470ab84646b5813b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860ac57cd4c79cd37baaabcfa6a1a438e39474ac6218eefa19cb8585f2d46e66478af772987db7537a8c17d1304d0bbe1e10136c4d537244967831d628581488707fb931f0325ee140bd9310ecc83a13882d075ee0d938bfefac05037b2e543eec5f84c8401ca15eaa04631ccb60c91685a754734d24132bf54cc23441a83779eec6723e75c0462a7218401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb7149059801f9651750a7e693db5b6edcaa6e27796d61e2962e0160e75e23e8bf72315a9064dbbe1978375475bde166e84be205e483e8ca05bc7ffe624d077b748a76a807b01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "chainId": 97, + "validators": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "candidates": "f901b0f846941284214b9b9c85549ab3d2b972df0deef66ac2c9b0ab1757500d6f4fdee439b17cf8e43267f94bc759162fb68de676d2fe10cc4cde26dd06be7e345e9cbf4b1dbf86b262bcf8469435552c16704d214347f29fa77f77da6d75d7c752b0b742ad4855bae330426b823e742da31f816cc83bc16d69a9134be0cfb4a1d17ec34f1b5b32d5c20440b8536b1e88f0f2f84694980a75ecd1309ea12fa2ed87a8744fbfc9b863d5b089037a9ace3b590165ea1c0c5ac72bf600b7c88c1e435f41932c1132aae1bfa0bb68e46b96ccb12c3415e4d82af717d8f84694a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0b0b973c2d38487e58fd6e145491b110080fb14ac915a0411fc78f19e09a399ddee0d20c63a75d8f930f1694544ad2dc01bf84694b71b214cb885500844365e95cd9942c7276e7fd8b0a2750ec6dded3dcdc2f351782310b0eadc077db59abca0f0cd26776e2e7acb9f3bce40b1fa5221fd1561226c6263cc5ff84694f474cf03cceff28abc65c9cbae594f725c80e12db096c9b86c3400e529bfe184056e257c07940bb664636f689e8d2027c834681f8f878b73445261034e946bb2d901b4b878", + "recents": "f8549435552c16704d214347f29fa77f77da6d75d7c75294980a75ecd1309ea12fa2ed87a8744fbfc9b863d594a2959d3f95eae5dc7d70144ce1b73b403b7eb6e094b71b214cb885500844365e95cd9942c7276e7fd8" + }, + "phases": [ + { + "description": "BU{ 30021100 ~ 30021110 }, FN{ ~ 30021108 }", + "message": "f91e38f91e35f91e3201b91e2ef91e2bf90313a0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a47a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0cd37a80ed7dcc51eca55ba1499bda091c3566fa8d52655050784060e97f9d3a2a0743b82b8917e7dff16e629d7e9bb12fafec91e6529331fa71f048317182a01f6a0c9731061ad717ed45d93d9b9520ec81247b4fe5a414c137a4a87ffff5329c5edb9010000000000000000024000000010000000020000000000000000000000000080000000008000000000000000000000000008000200000000000000000000000000000000000000200080000000000000002010000040020000000000000000000000008020000200000000008010000000080020000000200000000000240000000000000000000000000000000000000000000402000000000000000400000020000200000000800000000000000000000000000800000000000000000000000000100000000000402000000000100000000000000080000010000402001000000000000000000000012000000010000000000000000000000000002000000400028401ca15ed8402faf080830c7e4c84646b5816b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb8608b0902e51294837c51fa0136735f3cb53bf84e1e326a96e2e46e3c61dac77c58fd3364d7a8c763d024ff5e6ae5732e6d18cf5b81a0cfec44c5ac46c4e3d59a9f8c414d05bcf097cd00ee543f786b1c4e1cc5e78479060064bb090cb549a669f5f84c8401ca15eba05ae1e1c01f7a159c18a7b0906f42543d643ab8a597485877f2406cddb71490598401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a4780c76cee408f8930f6c92c94c329ade71c4ac2aa4ec60c2bb73e8cc686344b2fdf4ba14789bb163b9cccfd3473a5ac9d793a58907789dfdcc11e711aedaeb5ec1e01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c6137a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a054e0d60c1191dbbf8e9b93cb1894bf7df53a84f9a9cee2f6966174ef55e352d0a0e5af895bf69713a61bf9b62baa847fd762bc8d57b2ae885b62fc8a3b237c08b3a00043321aa7197c61489011b04c51ea727387aaf868b59f9a254ca6bee3a1c9e4b901000224000000004008000801208000000400000000000200000800000200184000000060000002040000000000000040000200000000000080410400000000200000000000000000800000000890000020201000000002000000000100c20400002004002000020800004000000040082008000000000888800000001000000000010100000000100000000000000000000000044100000008000020c000008422000000201000040000000000800000400800000000040000000000000000000090000022000000000800100000000000010000000400001001004002800000010800000210000400410000010000000200000400400000680004000000800000028401ca15ee8402f7f5918311f61084646b5819b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b7ee80d64faa3f3454b16463c3a96b9a80b6fd1ad843cdbfc21b39c9f71130402d1b7c94a02a55bb785561e4f6c811f30f65d1e46f808d9cab94da3af78e8ccd87281af11ab45f5b5589f7a78f408656f37ff2bddceb7ca4ef169034a441e231f84c8401ca15eca0ccde97c15ee5bd040a937bd820fbd52e012e0677daf5b947bda4b83ac80d7a478401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c613780d0c91411800095fb6441df9eb850aaf100c27082a02d521616c5f532a9c5e7fd2a85d13ce5af53172b36aa28de26886d030795427905fb744a496664ea88db2800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479435552c16704d214347f29fa77f77da6d75d7c752a0419fce6b7304d9f48e07d4967a5a66368556b1dbe7be6e55dee2031ae7a0b9cca0feddb8208133abb1750095d2ce14faf3e831ad6c9d8483596be0def3ea4ace3ca0e404e2ee8fb6ce1e8cda8912aded298f17da3fef59f506dea593149e555fddaab90100022040002800420058180502c01044150010108101c4008028084240013000200000401001060200800082000004c08002c8001090000080400430001420080000100040010404806800148a060010702410108200010008004401004304040520440828320200000000200040405831080800e000400000000504102000009003000000402002401010a0800000008002080c42004000080869204000808022020008201040040000008040200ac02049200a244004000c8881028200008080580060022000a000001020980c0206440000108000040090000400a3800062010010818c00000002411004002010000000600401082008200004800002820000028401ca15ef8402faed858401efdee584646b581cb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860b5b0d61ac44471707c874973a4b8c26db8b4dde18b81667e55e031b022eb4a574a42142e9bc8e54c9113c045c69a52180b7615ad4b15167a247b9e114d92ed9eaa360de111f4f94e20c520383cc8fd163edc7d15b9f3c7b41c010dcd2a0004b6f84c8401ca15eda0997a04c31be228e77daf34365400d3aadef2e66c7e8bfbd188372ac76d5c61378401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc80e1c158922511e0958ee9f9b48862d64d0e397a90d9c4e47344d05ef3ed271df634cc4a6de621462af31b75abcc5e137dfd80c73746a7734192d6875f957b06ba00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f37a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794980a75ecd1309ea12fa2ed87a8744fbfc9b863d5a01cb0ebc7e0eae672b4235f5e81861ca86b5136f0abbd2051c31309535ca10566a04c44ae2b7b8e108d37b4c0144792e83a0c24df4ac6312b7fbd752b985fb4650ba025b8bc67a2d3c536e5e00ad73815ba38e462006cfb657bf9486c0ffec2474617b9010000001000010000800000000002000000000000000000000000000000004000000000400000000000000000000000000000010000000000000000000200000000000000000000000080000008000000002012000000000000800000000000000000000020020200000040080000000800080000000000000000000010000000000000000000000000800000000000000010000400001000000000000000400020000000000000000000000000000000000000000100000000000000000010000000000002000000000000000000001000000000000000000000000002040220000000400000000000010000000000000000000000000000000000010000000000028401ca15f08402faf08083283c3684646b581fb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a1cfa217e5e6b34332095405684b8616c20f53d3069b50fff74b2ef5fa0a7471a57199567f5bdd04ab75f553925f14fc11f682b97cedd7fa5172d0b40e76d313b7c455337ccc75c3de1651b4ca736a2813e5f8285a777d8d78181d56d7197c35f84c8401ca15eea09c8859ff8303f4cf6b007a932c6fb32af528f525823ffcd239c0bb1da1df4edc8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f3780fa61b9e6fb846255f5d34a429e1a9b6f8c7a7f7450e19814973c335b50b515bc6005badccadf0cbb3a9661643f6d18105d1782e08901719f6c68be7096ac512400a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794a2959d3f95eae5dc7d70144ce1b73b403b7eb6e0a06efa50dd78d0e314559fee83cb8520a25ebe62a46709b4caf27fc834f759b61ea03307559cfb8d63ecf2b9b89c2ff2bb5a7907a48cf4d8f4c4499c3f7234413b96a0abecbe1eea73ae2197d0de0eee62b422e2b90448d4c62db09827f1c85177886cb90100000580000202000100000805600024010000000004400882002880008000004004000000000002000103000000000000008024200180008200000400002000090230004002008602008000080000004120100213000800800240040000002000200000280002080020102010580000210a002030000200100049101100000100810000006020008801002000020000800108040880008000080900200000203002200800006028042800001000000000400014a0000000020000f000260002000000100a00080020501000000040328401000014000c000000108002000009008410000102000000410280000800000100000112004020200000800000200000028401ca15f18402faf0808401deda6a84646b5822b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb86082088a5edce097f357e9ca8d54c64e474726a1cb243ab7f5808add2d9d5464ac190fb57d26fdcd6711a44f8d1faa5ee3069dd8c7f8c715561f35bed84b930a5f812b220e7daf3baf7015a8bb308ffefe2fe17f5df54653418825edeec33fbc46f84c8401ca15efa0e7c6e4ecabbbab442e2d6ad700382e8a64a7ffc7bf5640d733271b4db0bf0f378401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d20803bbbba57aedad4ca8196e2bec99d1baeec509ad05c9a4e3d71c5f11cf251891a5ad375bb0a6971dd13f181066f85cb36ed5e6122d6e4a90ddf349862174bd49900a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d42415a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b71b214cb885500844365e95cd9942c7276e7fd8a0312923c91311971931d93b6c0279a67700eb7196f8709cabfee51b2e2577c3f1a0e5a3b5802764e55809e0e5f4ce73fe5ad71e4e6dd16a77034dd769c4c260881aa049de6fcfd1e1d27e1d587998f08c3046db8f989ae2a6cbd8fc96555082e540c3b9010000220000004440000011002280000001201040000205408180080202001000000028000a0000000028000200040c4080020888306010004000049008102200100c52200100340d044000000a221c007124120002102001090940c1084a10084024000828060620002400384040001834092c02e00000804408010018000000804209010061301000040030841000000000002c400004100c001920601040e020020800000041010000100c4020010028412801e0008300822084021200408000001000024002800000b02040080282f4822404040014201000800022000022310a140100000000001100220030040000201020000800000c1000800000022000028401ca15f28402faf0808401eafbde84646b5825b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a136318978f80df8112277c462401344f2b3f32da0fbb29a034d256f8769ba726a3aa89b20cb99973d6b1c8602ed06e91245ae2a2148b542f06afc84fdf2346eba7b5915be4b95bcc8becff88e0ac786501b5790de301e5f83b34ef6e9ea4130f84c8401ca15f0a05ed2b9f7dbc17e68cde8ffb7469de534caafdcbbf87b70f729769062d3b19d208401ca15f1a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d4241580900c814301560face6d3e3b1aab7ba028e487caa25dd6b9fa902e1c914134122193ab7d21c47eaadca0fbc3d409d869e33a194038ed28ba3c2c158ab6354ea0300a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f9025ea0c93306ee601e84948d4e6e68e2893fc1255fbdd60c7f41f8e43a10007d542486a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794f474cf03cceff28abc65c9cbae594f725c80e12da0af33ee524362f3fe3dad564f88e4a3784741bdd9f5869fbee0ef3bb1b1d961bfa0061ab611f8ed6a2719b5ebdaf4ca87f51f77adeda14b1aab90f76f7313c92066a088f1623e3214125bce9f5e15fe5a400a2559b495e85b022f3136da1d6aff9e4bb9010004200000000000000000004080000002000000000000000000000000000002000000000000200410000080000000000000000000000008000000000800240000080004000000200000000208000100202410000000048000000000000000000000000020020200000000000000040800080040001000000000080010000000080200000000000000000000000000000000000400000000080040084000000020020000000000000000000100000000000080000800000000000020000404000000100002000000000000000000000000000060000000001000000002000060000010000000000000010000000000000000000000008000000000000000000000028401ca15f38402faf0808307528884646b5828b861d883010202846765746888676f312e31392e39856c696e7578000000110bea95c8fd8fa3756bfe308ff03e54a99d6243ed4a70baa630a9b59c4f37a230ec4b7452fd7502c4134b809f40d42b94d91d79135c114ed57413cbf29a91b94ff2e17200a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90314a0245f3f377de5f866f4469240966bf4503b50a70700391dd6a7b67cc0d8b07a35a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941284214b9b9c85549ab3d2b972df0deef66ac2c9a0e34d638607050905074ff81d1038709e034f265efa7dab25fd7d42e6ecfdd08ba04cf5ca7b67e27fe47eabf931d0a3c938be42844ed835b10ed7c15395768a5593a00ca4570aca7a3904ed797f45e48da3a9c8d271f330897e877a890d6b2d98fa69b90100042000000140408040100022c2000005000000080005008000488228001000008000000202040000008800000104d080000d00010000000022083200302000000010000000040411c080000a90001670241200068200010840400000400a000020000828220240128000c00040c81a00080880f0000000000021001000004100420000004020000000402080400080840004044000000008080d20400040102002000002104200a000002000200040204000002100000000108040020010800000024202000080488039240008021244001001000c44001050000002000222410010810600000404410800002008003020200201080080280000810001100010028401ca15f48402f7f59184015f312d84646b582bb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860a7a66d0d630c4ae4bb8f5d793363d7f8cf04d85a920962d394b596d265667a3e44c0b2ec2bca885552ee4e766c5eb4570e25bbfddbef3f99598e031ec1efa59a4e4d5d5bfebe42283d3f9149a203254dba24bc2e1bbd33d1c3c9131ab5b3f38ef84c8401ca15f1a0ccff55fd105e74ccba9e40ee09b9172f00a0bc97887b035a67d21008c0d424158401ca15f3a0245f3f377de5f866f4469240966bf4503b50a70700391dd6a7b67cc0d8b07a35804bd93932f6f3e9347bd2156a9d2c44534160632aba264a15dc16907413e0289e3d80ef4893c00d6fd9e1877a21ecac58cf3b8a9aaa7f13e5388001d8e9b70c4100a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a09f6f11418dfe951af274fca0c8d483782622474b9a2334b353ca91600a66b1d6a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479435552c16704d214347f29fa77f77da6d75d7c752a043dcd90b2d4290ee617dda8ef370f932b505fa9248b77c593626889057f0ac8aa0dc489a815af046585b6749a46980f66ebec41a4aeb77fc53f4726e9663c7891fa09ccf6a5a89aa841d1aa813828ade50c562b65cb162440651fc96703e1903d310b9010000800000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000200000000000408000008000000002010000000000020000000000000000000000020000200000000000000000000080000000000000000000010000000000000000000000000000000000000000000000400000800000000000000000020000000000000000000000000000000000000000000000000000000000000020000000002000000000000000000000000000000800001002000000082000000000000000000080002010000000000000000000000000000000000000000000000028401ca15f58402faed8583064f8884646b582eb90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860aa4196149fc930f0d95f0bd1313407d3f26875f1dd49f5b878966275a5cb65737abf93ab8b4d1bd3d31358c0dfda192215cd71f61cccb88ba44cef418e52e0e469d682f292d881193bc40e98d904df38cce98867da3d515bda89adca43de77adf84c8401ca15f3a0245f3f377de5f866f4469240966bf4503b50a70700391dd6a7b67cc0d8b07a358401ca15f4a09f6f11418dfe951af274fca0c8d483782622474b9a2334b353ca91600a66b1d68024945e2dfe3c049162b5d2a68fe6b73277538ed5220a0b40064f48e638a3d2d2421df2da22adab64c7474e0686e9c6ac4f4fae73f093997bc42eb9cffec82fb401a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f90313a040f18742dc8ecf9e2ca427971ffc7cd0e02526294e81e019c0fb767d5b45251da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794980a75ecd1309ea12fa2ed87a8744fbfc9b863d5a090a8852d139b434c3b722884c889b0b3152d353c09aacac4b8c9e22bba4b7603a0d58ef16aaad10c6aaa2d01f4df16eacc3fb7aad2f7e22a5f6d53e4850b955815a06202bbb8e9beb30c0d1e1e2f5bdfe2bb56eea184cf127dcba96dcdaefbc7de76b9010000000000000000000000000000000000000000000000000100000000000000400000400000000000000000000000000000000000000000000000000000200000000000000000000000040000000000002010000000000000000000000000000000000020000200401040000000000000080000000000000000400000000080000000000000000000000000000000000010000400000000000000000000008020020000000000440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000010000000000000010000000000000000000000040000010008000000000820028401ca15f68402faf08083028a9584646b5831b90115d883010202846765746888676f312e31392e39856c696e7578000000110bea95f8b23fb860ab4a503ba45b02bad53cc5a0f4eeed6a0f0ec5ebc8c35e5a012f30970b699acee2fbfdf7236484f3e9499b119623564715a0b3f69c1e00666e326fdfa9fdb8954f037f809458e7d8c5db57dc06127ff0d790797a38ed3e2c8236cce18587d51df84c8401ca15f4a09f6f11418dfe951af274fca0c8d483782622474b9a2334b353ca91600a66b1d68401ca15f5a040f18742dc8ecf9e2ca427971ffc7cd0e02526294e81e019c0fb767d5b45251d805e3b66c3e78ef035bfa433c41c6495c156d1d8d1675653d2af91d8f117f231077fd5494f24ba80709c7d691281467bac95fffba8f2428d9cbefcf7aa0b99527c01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000", + "result": [], + "status": { + "height": 30021108, + "extra": "" + } + } + ] + } +] diff --git a/settings.gradle b/settings.gradle index 7dfd39ac..4bd2b152 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include ( 'bmc', 'bmv:bridge', 'bmv:bsc', + 'bmv:bsc2', 'bmv:btpblock', 'bmv:eth2', 'bmv:icon',