Removed epoch number in transfer action. Added a mechanism for requeueing failed transfer messages.
This commit is contained in:
parent
f3077bd4c0
commit
2dec546b03
|
@ -4,4 +4,5 @@ demo/validator*/development
|
|||
demo/validator*/staging
|
||||
src/contracts/*/build
|
||||
src/contracts/*/coverage
|
||||
src/contracts/*/coverageEnv
|
||||
src/tss/multi-party-ecdsa
|
||||
|
|
|
@ -30,22 +30,43 @@ contract BridgeMessageProcessor is BridgeTransitions {
|
|||
}
|
||||
|
||||
event AppliedMessage(bytes message);
|
||||
event RescheduleTransferMessage(bytes32 msgHash);
|
||||
|
||||
mapping(bytes32 => bool) public handledMessages;
|
||||
|
||||
function applyMessage(bytes memory message, bytes memory signatures) public {
|
||||
(bytes32 msgHash, uint16 msgEpoch) = checkSignedMessage(message, signatures);
|
||||
handledMessages[msgHash] = true;
|
||||
|
||||
Action msgAction = Action(uint8(message[0]));
|
||||
uint16 msgEpoch;
|
||||
bytes32 msgHash = message._hash();
|
||||
|
||||
if (msgAction == Action.CONFIRM_KEYGEN || msgAction == Action.CANCEL_KEYGEN) {
|
||||
require(msgEpoch == nextEpoch, "Incorrect message epoch");
|
||||
} else if (msgAction == Action.TRANSFER) {
|
||||
require(msgEpoch <= epoch, "Incorrect message epoch");
|
||||
// In case of transfer action, it is possible that a new epoch will start,
|
||||
// until a correspondent transfer action transaction will be processed.
|
||||
// In such case, if a list of provided signatures for old epoch is not sufficient,
|
||||
// a message should be automatically reprocessed.
|
||||
// Special event helps to find such stuck messages.
|
||||
if (msgAction == Action.TRANSFER) {
|
||||
// solhint-disable-next-line avoid-low-level-calls
|
||||
(bool success,) = address(this).call(abi.encodeWithSelector(
|
||||
this.checkSignedMessage.selector,
|
||||
epoch,
|
||||
msgHash,
|
||||
signatures
|
||||
));
|
||||
if (!success) {
|
||||
emit RescheduleTransferMessage(msgHash);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
require(msgEpoch == epoch, "Incorrect message epoch");
|
||||
msgEpoch = message._decodeEpoch();
|
||||
checkSignedMessage(msgEpoch, msgHash, signatures);
|
||||
|
||||
if (msgAction == Action.CONFIRM_KEYGEN || msgAction == Action.CANCEL_KEYGEN) {
|
||||
require(msgEpoch == nextEpoch, "Incorrect message epoch");
|
||||
} else {
|
||||
require(msgEpoch == epoch, "Incorrect message epoch");
|
||||
}
|
||||
}
|
||||
handledMessages[msgHash] = true;
|
||||
|
||||
if (msgAction == Action.CONFIRM_KEYGEN) {
|
||||
// [3,22] - foreign address bytes
|
||||
|
@ -90,8 +111,8 @@ contract BridgeMessageProcessor is BridgeTransitions {
|
|||
require(message.length == 32, "Incorrect message length");
|
||||
_cancelKeygen();
|
||||
} else if (msgAction == Action.TRANSFER) {
|
||||
// [3,34] - txHash, [35,54] - address, [55,66] - value
|
||||
require(message.length == 67, "Incorrect message length");
|
||||
// [1,32] - txHash, [33,52] - address, [53,64] - value
|
||||
require(message.length == 65, "Incorrect message length");
|
||||
(address to, uint96 value) = message._decodeTransfer();
|
||||
_transfer(to, value);
|
||||
} else if (msgAction == Action.CHANGE_MIN_PER_TX_LIMIT) {
|
||||
|
@ -114,30 +135,25 @@ contract BridgeMessageProcessor is BridgeTransitions {
|
|||
require(message.length == 32, "Incorrect message length");
|
||||
uint96 limit = message._decodeUint96();
|
||||
_decreaseExecutionMinLimit(limit);
|
||||
} else { // Action.CHANGE_RANGE_SIZE
|
||||
} else {// Action.CHANGE_RANGE_SIZE
|
||||
// [3,4] - rangeSize, [5,31] - extra data
|
||||
require(message.length == 32, "Incorrect message length");
|
||||
uint16 rangeSize = message._decodeUint16();
|
||||
_changeRangeSize(rangeSize);
|
||||
} // invalid actions will not reach this line, since casting uint8 to Action will revert execution
|
||||
}
|
||||
// invalid actions will not reach this line, since casting uint8 to Action will revert execution
|
||||
|
||||
emit AppliedMessage(message);
|
||||
}
|
||||
|
||||
function checkSignedMessage(bytes memory message, bytes memory signatures) public view returns (bytes32, uint16) {
|
||||
function checkSignedMessage(uint16 msgEpoch, bytes32 msgHash, bytes memory signatures) public view {
|
||||
require(signatures.length % SIGNATURE_SIZE == 0, "Incorrect signatures length");
|
||||
|
||||
bytes32 msgHash = message._hash();
|
||||
require(!handledMessages[msgHash], "Tx was already handled");
|
||||
|
||||
uint16 msgEpoch;
|
||||
assembly {
|
||||
msgEpoch := mload(add(message, 3))
|
||||
}
|
||||
require(msgEpoch > 0 && msgEpoch <= nextEpoch, "Invalid epoch number");
|
||||
require(msgEpoch > 0 && (msgEpoch == epoch || msgEpoch == nextEpoch), "Invalid epoch number");
|
||||
|
||||
uint signaturesNum = signatures.length / SIGNATURE_SIZE;
|
||||
require(signaturesNum >= getThreshold(msgEpoch), "Not enough signatures");
|
||||
|
||||
address[] memory possibleValidators = getValidators(msgEpoch);
|
||||
|
||||
|
@ -145,6 +161,7 @@ contract BridgeMessageProcessor is BridgeTransitions {
|
|||
bytes32 s;
|
||||
uint8 v;
|
||||
|
||||
uint16 validSignatures = 0;
|
||||
for (uint i = 0; i < signaturesNum; i++) {
|
||||
uint offset = i * SIGNATURE_SIZE;
|
||||
|
||||
|
@ -155,15 +172,14 @@ contract BridgeMessageProcessor is BridgeTransitions {
|
|||
}
|
||||
|
||||
address signer = ecrecover(msgHash, v, r, s);
|
||||
uint j;
|
||||
for (j = 0; j < possibleValidators.length; j++) {
|
||||
for (uint j = 0; j < possibleValidators.length; j++) {
|
||||
if (possibleValidators[j] == signer) {
|
||||
delete possibleValidators[j];
|
||||
validSignatures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
require(j != possibleValidators.length, "Not a validator signature");
|
||||
}
|
||||
return (msgHash, msgEpoch);
|
||||
require(validSignatures >= getThreshold(msgEpoch), "Not enough valid signatures");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ contract BridgeTransitions is BridgeEpochs, BridgeStates, BridgeConfig {
|
|||
nextEpoch++;
|
||||
_initNextEpoch(getValidators(), getThreshold(), getCloseEpoch());
|
||||
|
||||
_forceSign();
|
||||
|
||||
if (getCloseEpoch()) {
|
||||
state = State.CLOSING_EPOCH;
|
||||
emit EpochClose(epoch);
|
||||
|
@ -52,8 +54,6 @@ contract BridgeTransitions is BridgeEpochs, BridgeStates, BridgeConfig {
|
|||
state = State.VOTING;
|
||||
emit EpochEnd(epoch);
|
||||
}
|
||||
|
||||
_forceSign();
|
||||
}
|
||||
|
||||
function _addValidator(address validator) internal voting {
|
||||
|
|
|
@ -4,6 +4,11 @@ library MessageDecode {
|
|||
// [0] - action type
|
||||
// [1,2] - epoch
|
||||
// [3..] - payload
|
||||
function _decodeEpoch(bytes memory message) internal pure returns (uint16 a) {
|
||||
assembly {
|
||||
a := mload(add(message, 3))
|
||||
}
|
||||
}
|
||||
function _decodeUint16(bytes memory message) internal pure returns (uint16 a) {
|
||||
assembly {
|
||||
a := mload(add(message, 5))
|
||||
|
@ -30,8 +35,8 @@ library MessageDecode {
|
|||
|
||||
function _decodeTransfer(bytes memory message) internal pure returns (address a, uint96 b) {
|
||||
assembly {
|
||||
a := mload(add(message, 55))
|
||||
b := mload(add(message, 67))
|
||||
a := mload(add(message, 53))
|
||||
b := mload(add(message, 65))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ library MessageHash {
|
|||
if (message.length == 32) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message));
|
||||
}
|
||||
if (message.length == 67) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n67", message));
|
||||
if (message.length == 65) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n65", message));
|
||||
}
|
||||
revert("Incorrect message length");
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
const { expect } = require('chai')
|
||||
|
||||
const {
|
||||
State, Action, getDeployResult, expectEventInLogs, buildMessage, sign, stripHex, skipBlocks
|
||||
State, Action, getDeployResult, expectEventInLogs, buildMessage, sign, stripHex, skipBlocks,
|
||||
keccak256
|
||||
} = require('./utils')
|
||||
|
||||
const EthToBncBridge = artifacts.require('EthToBncBridge')
|
||||
|
@ -125,80 +126,54 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
})
|
||||
|
||||
describe('signatures checks', async () => {
|
||||
const keygenMessage = buildMessage(Action.CONFIRM_KEYGEN, 1, '0000000000000000000000000000000000000000')
|
||||
const votingMessage0 = buildMessage(Action.START_VOTING, 0)
|
||||
const votingMessage1 = buildMessage(Action.START_VOTING, 1)
|
||||
const validatorMessage = buildMessage(Action.ADD_VALIDATOR, 1, stripHex(accounts[0]), '000000000000000000')
|
||||
const transferMessage = buildMessage(Action.TRANSFER, 1, '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
|
||||
const messages = [keygenMessage, votingMessage1, validatorMessage, transferMessage]
|
||||
const invalidMessage = `${votingMessage1}00`
|
||||
const message = '0x010203'
|
||||
const messageHash = keccak256(message)
|
||||
let signature1
|
||||
let signature2
|
||||
let signature3
|
||||
|
||||
before(async () => {
|
||||
bridge = await deployBridge()
|
||||
signature1 = stripHex(await sign(validators[0], message))
|
||||
signature2 = stripHex(await sign(validators[1], message))
|
||||
signature3 = stripHex(await sign(validators[2], message))
|
||||
})
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const message = messages[i]
|
||||
it(`should accept 2 or 3 correct signatures for message of length ${(message.length - 2) / 2} bytes`, async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], message))
|
||||
const signature2 = stripHex(await sign(validators[1], message))
|
||||
const signature3 = stripHex(await sign(validators[2], message))
|
||||
|
||||
await bridge.checkSignedMessage(message, `0x${signature1}${signature2}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(message, `0x${signature2}${signature1}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(message, `0x${signature2}${signature3}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(message, `0x${signature3}${signature1}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(message, `0x${signature1}${signature2}${signature3}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(message, `0x${signature3}${signature1}${signature2}`).should.be.fulfilled
|
||||
})
|
||||
}
|
||||
|
||||
it(`should not accept correct signatures for message of length ${(invalidMessage.length - 2) / 2} bytes`, async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], invalidMessage))
|
||||
const signature2 = stripHex(await sign(validators[1], invalidMessage))
|
||||
|
||||
await bridge.checkSignedMessage(invalidMessage, `0x${signature1}${signature2}`).should.be.rejected
|
||||
it('should accept 2 or 3 correct signatures', async () => {
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature2}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature2}${signature1}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature2}${signature3}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature3}${signature1}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature2}${signature3}`).should.be.fulfilled
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature3}${signature1}${signature2}`).should.be.fulfilled
|
||||
})
|
||||
|
||||
it('should not accept empty signatures', async () => {
|
||||
await bridge.checkSignedMessage(keygenMessage, '0x').should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, '0x').should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept signatures of wrong length', async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], keygenMessage))
|
||||
const signature2 = stripHex(await sign(validators[1], keygenMessage))
|
||||
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}${signature2}00`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature2}00`).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept 1 correct signature', async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], keygenMessage))
|
||||
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}`).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept message with 0 epoch', async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], votingMessage0))
|
||||
const signature2 = stripHex(await sign(validators[1], votingMessage0))
|
||||
|
||||
await bridge.checkSignedMessage(votingMessage0, `0x${signature1}${signature2}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(0, messageHash, `0x${signature1}${signature2}`).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept repeated correct signatures', async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], keygenMessage))
|
||||
const signature2 = stripHex(await sign(validators[1], keygenMessage))
|
||||
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}${signature1}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}${signature2}${signature1}`).should.be.rejected
|
||||
it('should not accept repeated correct signatures as valid', async () => {
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature1}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature2}${signature1}`).should.be.fulfilled
|
||||
})
|
||||
|
||||
it('should accept signatures only from validators', async () => {
|
||||
const signature1 = stripHex(await sign(validators[0], keygenMessage))
|
||||
const signature2 = stripHex(await sign(validators[1], keygenMessage))
|
||||
const wrongSignature = stripHex(await sign(accounts[5], keygenMessage))
|
||||
it('should accept signatures if enough valid signatures', async () => {
|
||||
const wrongSignature = stripHex(await sign(accounts[5], message))
|
||||
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}${wrongSignature}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(keygenMessage, `0x${signature1}${signature2}${wrongSignature}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${wrongSignature}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, messageHash, `0x${signature1}${signature2}${wrongSignature}`).should.be.fulfilled
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -241,39 +216,34 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
const startVotingMessage2 = buildMessage(Action.START_VOTING, 2)
|
||||
const transferMessage15min = buildMessage(
|
||||
Action.TRANSFER,
|
||||
1,
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
stripHex(accounts[5]),
|
||||
'0000000000000002540be401'
|
||||
)
|
||||
const transferMessage15min1s = buildMessage(
|
||||
Action.TRANSFER,
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
stripHex(accounts[5]),
|
||||
'0000000000000002540be402'
|
||||
)
|
||||
const transferMessage15max = buildMessage(
|
||||
Action.TRANSFER,
|
||||
1,
|
||||
'2222222222222222222222222222222222222222222222222222222222222222',
|
||||
stripHex(accounts[5]),
|
||||
'00000000000000e8d4a51001'
|
||||
)
|
||||
const transferMessage15low = buildMessage(
|
||||
Action.TRANSFER,
|
||||
1,
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
stripHex(accounts[5]),
|
||||
'000000000000000000000064'
|
||||
)
|
||||
const transferMessage15high = buildMessage(
|
||||
Action.TRANSFER,
|
||||
1,
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
stripHex(accounts[5]),
|
||||
'00000000000000e8d4a51002'
|
||||
)
|
||||
const transferMessage25min = buildMessage(
|
||||
Action.TRANSFER,
|
||||
2,
|
||||
'3333333333333333333333333333333333333333333333333333333333333333',
|
||||
stripHex(accounts[5]),
|
||||
'0000000000000002540be401'
|
||||
)
|
||||
const transferMessage1err = buildMessage(Action.TRANSFER, 1)
|
||||
const changeMinPerTxLimit1 = buildMessage(Action.CHANGE_MIN_PER_TX_LIMIT, 1, '0000000000000002540be401', '0000000000000000000000000000000000')
|
||||
const changeMinPerTxLimit1high = buildMessage(Action.CHANGE_MIN_PER_TX_LIMIT, 1, '100000000000000000000000', '0000000000000000000000000000000000')
|
||||
|
@ -298,6 +268,7 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
Action.UNKNOWN_MESSAGE,
|
||||
1
|
||||
)
|
||||
const invalidMessage = buildMessage(Action.CONFIRM_KEYGEN, 1, '00')
|
||||
const validMessages = [
|
||||
confirmKeygenMessage11, confirmKeygenMessage12, confirmKeygenMessage1err, startVotingMessage1,
|
||||
startVotingMessage1err, confirmCloseEpochMessage1, confirmCloseEpochMessage1err,
|
||||
|
@ -311,14 +282,13 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
confirmFundsTransferMessage1, confirmFundsTransferMessage1err, cancelKeygenMessage1,
|
||||
cancelKeygenMessage21, cancelKeygenMessage22, cancelKeygenMessage2err, confirmKeygenMessage2,
|
||||
startVotingMessage2, transferMessage15min, transferMessage15max, transferMessage15low,
|
||||
transferMessage15high, transferMessage25min, transferMessage1err, changeMinPerTxLimit1,
|
||||
changeMinPerTxLimit1high, changeMinPerTxLimit1low, changeMinPerTxLimit1err,
|
||||
changeMaxPerTxLimit1, changeMaxPerTxLimit1low, changeMaxPerTxLimit1err,
|
||||
decreaseExecutionMinLimit1low, decreaseExecutionMinLimit11, decreaseExecutionMinLimit12,
|
||||
decreaseExecutionMinLimit1err, increaseExecutionMaxLimit1low, increaseExecutionMaxLimit11,
|
||||
increaseExecutionMaxLimit12, increaseExecutionMaxLimit1err, changeRangeSizeMessage10,
|
||||
changeRangeSizeMessage11, changeRangeSizeMessage1max, changeRangeSizeMessage1err,
|
||||
unknownMessage
|
||||
transferMessage15high, transferMessage1err, changeMinPerTxLimit1, changeMinPerTxLimit1high,
|
||||
changeMinPerTxLimit1low, changeMinPerTxLimit1err, changeMaxPerTxLimit1,
|
||||
changeMaxPerTxLimit1low, changeMaxPerTxLimit1err, decreaseExecutionMinLimit1low,
|
||||
decreaseExecutionMinLimit11, decreaseExecutionMinLimit12, decreaseExecutionMinLimit1err,
|
||||
increaseExecutionMaxLimit1low, increaseExecutionMaxLimit11, increaseExecutionMaxLimit12,
|
||||
increaseExecutionMaxLimit1err, changeRangeSizeMessage10, changeRangeSizeMessage11,
|
||||
changeRangeSizeMessage1max, changeRangeSizeMessage1err, unknownMessage, invalidMessage
|
||||
]
|
||||
|
||||
const validSignatures = {}
|
||||
|
@ -328,6 +298,7 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
}
|
||||
|
||||
before(async () => {
|
||||
validSignatures[transferMessage15min1s] = await sign(validators[0], transferMessage15min1s)
|
||||
for (let i = 0; i < validMessages.length; i += 1) {
|
||||
const message = validMessages[i]
|
||||
|
||||
|
@ -370,7 +341,11 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
const signature2 = stripHex(await sign(validators[1], confirmKeygenMessage11))
|
||||
|
||||
await applyMessage(confirmKeygenMessage11)
|
||||
await bridge.checkSignedMessage(confirmKeygenMessage11, `0x${signature1}${signature2}`).should.be.rejected
|
||||
await bridge.checkSignedMessage(1, keccak256(confirmKeygenMessage11), `0x${signature1}${signature2}`).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept invalid length message', async () => {
|
||||
await applyMessage(invalidMessage).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not be able to apply keygen confirm message for 2nd epoch', async () => {
|
||||
|
@ -948,23 +923,16 @@ contract('EthToBncBridge', async (accounts) => {
|
|||
await applyMessage(transferMessage15high).should.be.rejected
|
||||
})
|
||||
|
||||
it('should not accept transfer message for next epoch', async () => {
|
||||
await applyMessage(startVotingMessage1)
|
||||
await token.transfer(bridge.address, EXECUTION_MIN_LIMIT, { from: accounts[1] })
|
||||
await applyMessage(transferMessage25min).should.be.rejected
|
||||
})
|
||||
|
||||
it('should accept transfer message for previous epoch', async () => {
|
||||
await applyMessage(startVotingMessage1)
|
||||
await applyMessage(startKeygenMessage11)
|
||||
await applyMessage(confirmKeygenMessage2)
|
||||
await applyMessage(confirmFundsTransferMessage1)
|
||||
await applyMessage(transferMessage15min).should.be.fulfilled
|
||||
it('should request transfer message reschedule', async () => {
|
||||
const { logs } = await applyMessage(transferMessage15min1s).should.be.fulfilled
|
||||
expectEventInLogs(logs, 'RescheduleTransferMessage', {
|
||||
msgHash: keccak256(transferMessage15min1s)
|
||||
})
|
||||
expect(await token.balanceOf(bridge.address)).to.bignumber.equal('0')
|
||||
expect(await token.balanceOf(accounts[5])).to.bignumber.equal('0')
|
||||
expect(
|
||||
await token.allowance(bridge.address, accounts[5])
|
||||
).to.bignumber.equal(EXECUTION_MIN_LIMIT)
|
||||
).to.bignumber.equal('0')
|
||||
})
|
||||
|
||||
it('should not accept message with wrong length', async () => {
|
||||
|
|
|
@ -94,8 +94,12 @@ function padZeros(arg, len) {
|
|||
return s
|
||||
}
|
||||
|
||||
function buildMessage(type, epoch, ...args) {
|
||||
return `0x${padZeros(type, 2)}${padZeros(epoch, 4)}${args.reduce(((previousValue, currentValue) => previousValue + currentValue), '')}`
|
||||
function buildMessage(type, ...args) {
|
||||
const epoch = args[0]
|
||||
if (typeof epoch === 'number') {
|
||||
return `0x${padZeros(type, 2)}${padZeros(epoch, 4)}${args.slice(1).reduce(((previousValue, currentValue) => previousValue + currentValue), '')}`
|
||||
}
|
||||
return `0x${padZeros(type, 2)}${args.reduce(((previousValue, currentValue) => previousValue + currentValue), '')}`
|
||||
}
|
||||
|
||||
async function sign(address, data) {
|
||||
|
@ -119,6 +123,10 @@ async function skipBlocks(n = 1) {
|
|||
}
|
||||
}
|
||||
|
||||
function keccak256(message) {
|
||||
return web3.eth.accounts.hashMessage(message)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
State,
|
||||
Action,
|
||||
|
@ -127,5 +135,6 @@ module.exports = {
|
|||
buildMessage,
|
||||
stripHex,
|
||||
sign,
|
||||
skipBlocks
|
||||
skipBlocks,
|
||||
keccak256
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ library MessageHash {
|
|||
if (message.length == 32) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message));
|
||||
}
|
||||
if (message.length == 67) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n67", message));
|
||||
if (message.length == 65) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n65", message));
|
||||
}
|
||||
revert("Incorrect message length");
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ contract('SharedDB', async (accounts) => {
|
|||
'0x000102',
|
||||
'0x000102030405060708090a0b0c0d0e0f10111213141516',
|
||||
'0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||
'0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142'
|
||||
'0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40'
|
||||
]
|
||||
|
||||
describe('add signature', async () => {
|
||||
|
|
|
@ -9,6 +9,6 @@ COPY ./bncWatcher/package.json ./
|
|||
RUN npm install
|
||||
|
||||
COPY ./bncWatcher/index.js ./src/
|
||||
COPY ./shared/db.js ./shared/logger.js ./shared/crypto.js ./shared/amqp.js ./shared/wait.js ./shared/binanceClient.js ./shared/
|
||||
COPY ./shared/db.js ./shared/logger.js ./shared/crypto.js ./shared/amqp.js ./shared/wait.js ./shared/binanceClient.js ./shared/ethProvider.js ./shared/
|
||||
|
||||
ENTRYPOINT ["node", "src/index.js"]
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
const axios = require('axios')
|
||||
const BN = require('bignumber.js')
|
||||
const fs = require('fs')
|
||||
const { computeAddress } = require('ethers').utils
|
||||
const ethers = require('ethers')
|
||||
|
||||
const logger = require('../shared/logger')
|
||||
const redis = require('../shared/db')
|
||||
const { publicKeyToAddress } = require('../shared/crypto')
|
||||
const createProvider = require('../shared/ethProvider')
|
||||
const { hexAddressToBncAddress } = require('../shared/crypto')
|
||||
const { delay } = require('../shared/wait')
|
||||
const { connectRabbit, assertQueue } = require('../shared/amqp')
|
||||
const { getTx, getBlockTime, fetchNewTransactions } = require('../shared/binanceClient')
|
||||
|
||||
const {
|
||||
PROXY_URL, RABBITMQ_URL
|
||||
PROXY_URL, RABBITMQ_URL, HOME_RPC_URL, HOME_BRIDGE_ADDRESS
|
||||
} = process.env
|
||||
|
||||
const FOREIGN_FETCH_INTERVAL = parseInt(process.env.FOREIGN_FETCH_INTERVAL, 10)
|
||||
|
@ -20,17 +20,19 @@ const FOREIGN_FETCH_MAX_TIME_INTERVAL = parseInt(process.env.FOREIGN_FETCH_MAX_T
|
|||
|
||||
const proxyHttpClient = axios.create({ baseURL: PROXY_URL })
|
||||
|
||||
const bridgeAbi = [
|
||||
'function getForeignAddress(uint16 epoch) view returns (bytes20)'
|
||||
]
|
||||
|
||||
const homeProvider = createProvider(HOME_RPC_URL)
|
||||
const bridge = new ethers.Contract(HOME_BRIDGE_ADDRESS, bridgeAbi, homeProvider)
|
||||
|
||||
let channel
|
||||
let epochTimeIntervalsQueue
|
||||
|
||||
function getForeignAddress(epoch) {
|
||||
const keysFile = `/keys/keys${epoch}.store`
|
||||
try {
|
||||
const publicKey = JSON.parse(fs.readFileSync(keysFile))[5]
|
||||
return publicKeyToAddress(publicKey)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
async function getForeignAddress(epoch) {
|
||||
const foreignHexAddress = await bridge.getForeignAddress(epoch)
|
||||
return hexAddressToBncAddress(foreignHexAddress)
|
||||
}
|
||||
|
||||
async function fetchTimeIntervalsQueue() {
|
||||
|
@ -99,15 +101,7 @@ async function loop() {
|
|||
return
|
||||
}
|
||||
|
||||
const address = getForeignAddress(epoch)
|
||||
|
||||
if (!address) {
|
||||
logger.debug('Validator is not included in current epoch')
|
||||
await redis.set(`foreignTime${epoch}`, endTime)
|
||||
await delay(FOREIGN_FETCH_INTERVAL)
|
||||
return
|
||||
}
|
||||
|
||||
const address = await getForeignAddress(epoch)
|
||||
const transactions = await fetchNewTransactions(address, startTime, endTime)
|
||||
|
||||
if (transactions.length === 0) {
|
||||
|
@ -125,11 +119,9 @@ async function loop() {
|
|||
if (tx.memo === '') {
|
||||
const publicKeyEncoded = (await getTx(tx.txHash)).signatures[0].pub_key.value
|
||||
await proxyHttpClient.post('/transfer', {
|
||||
to: computeAddress(Buffer.from(publicKeyEncoded, 'base64')),
|
||||
value: new BN(tx.value).multipliedBy('1e18')
|
||||
.toString(16),
|
||||
hash: tx.txHash,
|
||||
epoch
|
||||
to: ethers.utils.computeAddress(Buffer.from(publicKeyEncoded, 'base64')),
|
||||
value: new BN(tx.value).multipliedBy('1e18').toString(16),
|
||||
hash: tx.txHash
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,15 +111,16 @@ services:
|
|||
<<: *common-environment
|
||||
PROXY_URL: http://local_proxy:8001
|
||||
REDIS_HOST: redis
|
||||
HOME_RPC_URL:
|
||||
HOME_BRIDGE_ADDRESS:
|
||||
FOREIGN_URL:
|
||||
FOREIGN_ASSET:
|
||||
FOREIGN_FETCH_MAX_TIME_INTERVAL:
|
||||
FOREIGN_FETCH_INTERVAL:
|
||||
FOREIGN_FETCH_BLOCK_TIME_OFFSET:
|
||||
volumes:
|
||||
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
|
||||
networks:
|
||||
- test_network
|
||||
- ethereum_home_rpc_net
|
||||
- binance_net
|
||||
side-watcher:
|
||||
build:
|
||||
|
|
|
@ -104,13 +104,13 @@ services:
|
|||
<<: *common-environment
|
||||
PROXY_URL: http://proxy:8001
|
||||
REDIS_HOST: redis
|
||||
HOME_RPC_URL:
|
||||
HOME_BRIDGE_ADDRESS:
|
||||
FOREIGN_URL:
|
||||
FOREIGN_ASSET:
|
||||
FOREIGN_FETCH_MAX_TIME_INTERVAL:
|
||||
FOREIGN_FETCH_INTERVAL:
|
||||
FOREIGN_FETCH_BLOCK_TIME_OFFSET:
|
||||
volumes:
|
||||
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
|
||||
networks:
|
||||
- oracle_network
|
||||
side-watcher:
|
||||
|
|
|
@ -26,6 +26,7 @@ const bridgeAbi = [
|
|||
'event EpochClose(uint16 indexed epoch)',
|
||||
'event ForceSign()',
|
||||
'event RangeSizeChanged(uint16 rangeSize)',
|
||||
'event RescheduleTransferMessage(bytes32 msgHash)',
|
||||
'function getForeignAddress(uint16 epoch) view returns (bytes20)',
|
||||
'function getThreshold(uint16 epoch) view returns (uint16)',
|
||||
'function getParties(uint16 epoch) view returns (uint16)',
|
||||
|
@ -41,6 +42,7 @@ let signQueue
|
|||
let keygenQueue
|
||||
let cancelKeygenQueue
|
||||
let epochTimeIntervalsQueue
|
||||
let rescheduleTransferQueue
|
||||
let chainId
|
||||
let blockNumber
|
||||
let epoch
|
||||
|
@ -185,6 +187,17 @@ async function sendEpochClose() {
|
|||
redisTx.incr(`foreignNonce${epoch}`)
|
||||
}
|
||||
|
||||
async function rescheduleTransferMessage(transactionHash, msgHash) {
|
||||
const tx = await provider.getTransaction(transactionHash)
|
||||
if (tx.from === validatorAddress) {
|
||||
logger.debug(`Rescheduling transfer action for message ${msgHash}`)
|
||||
rescheduleTransferQueue.send({
|
||||
msgHash,
|
||||
blockNumber: tx.blockNumber
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
channel = await connectRabbit(RABBITMQ_URL)
|
||||
exchangeQueue = await assertQueue(channel, 'exchangeQueue')
|
||||
|
@ -192,6 +205,7 @@ async function initialize() {
|
|||
keygenQueue = await assertQueue(channel, 'keygenQueue')
|
||||
cancelKeygenQueue = await assertQueue(channel, 'cancelKeygenQueue')
|
||||
epochTimeIntervalsQueue = await assertQueue(channel, 'epochTimeIntervalsQueue')
|
||||
rescheduleTransferQueue = await assertQueue(channel, 'rescheduleTransferQueue')
|
||||
|
||||
activeEpoch = !!(await redis.get('activeEpoch'))
|
||||
|
||||
|
@ -244,6 +258,7 @@ async function initialize() {
|
|||
await resetFutureMessages(channel, exchangeQueue, blockNumber)
|
||||
await resetFutureMessages(channel, signQueue, blockNumber)
|
||||
await resetFutureMessages(channel, epochTimeIntervalsQueue, blockNumber)
|
||||
await resetFutureMessages(channel, rescheduleTransferQueue, blockNumber)
|
||||
logger.debug('Sending start commands')
|
||||
await axios.get(`${KEYGEN_CLIENT_URL}/start`)
|
||||
await axios.get(`${SIGN_CLIENT_URL}/start`)
|
||||
|
@ -339,6 +354,9 @@ async function loop() {
|
|||
rangeSizeStartBlock = curBlockNumber
|
||||
logger.debug(`Range size updated to ${rangeSize} at block ${rangeSizeStartBlock}`)
|
||||
break
|
||||
case 'RescheduleTransferMessage':
|
||||
await rescheduleTransferMessage(bridgeEvents[i].transactionHash, event.values.msgHash)
|
||||
break
|
||||
default:
|
||||
logger.warn('Unknown event %o', event)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ const bridgeAbi = [
|
|||
'function getNextCloseEpoch() view returns (bool)',
|
||||
'function state() view returns (uint8)',
|
||||
'function getNextPartyId(address partyAddress) view returns (uint16)',
|
||||
'function handledMessages(bytes32 msgHash) view returns (bool)',
|
||||
'function applyMessage(bytes message, bytes signatures)'
|
||||
]
|
||||
const sharedDbAbi = [
|
||||
|
|
|
@ -335,11 +335,16 @@ async function voteIncreaseExecutionMaxLimit(req, res) {
|
|||
async function transfer(req, res) {
|
||||
logger.info('Transfer start')
|
||||
const {
|
||||
hash, to, value, epoch
|
||||
hash, to, value
|
||||
} = req.body
|
||||
if (ethers.utils.isHexString(to, 20)) {
|
||||
logger.info(`Calling transfer to ${to}, 0x${value} tokens`)
|
||||
await processMessage(Action.TRANSFER, epoch, hash, to, padZeros(value, 24))
|
||||
const message = `${padZeros(Action.TRANSFER.toString(16), 2)}${hash}${to.slice(2)}${padZeros(value, 24)}`
|
||||
if (await bridge.handledMessages(ethers.utils.hashMessage(message))) {
|
||||
logger.info('This tx hash was already processed, skipping')
|
||||
} else {
|
||||
await processMessage(Action.TRANSFER, hash, to, padZeros(value, 24))
|
||||
}
|
||||
}
|
||||
res.send()
|
||||
logger.info('Transfer end')
|
||||
|
|
|
@ -16,7 +16,9 @@ const SIDE_MAX_FETCH_RANGE_SIZE = parseInt(process.env.SIDE_MAX_FETCH_RANGE_SIZE
|
|||
const bridgeAbi = [
|
||||
'function applyMessage(bytes message, bytes signatures)',
|
||||
'function getThreshold(uint16 epoch) view returns (uint16)',
|
||||
'function getValidators(uint16 epoch) view returns (address[])'
|
||||
'function getValidators(uint16 epoch) view returns (address[])',
|
||||
'function handledMessages(bytes32 msgHash) view returns (bool)',
|
||||
'function epoch() view returns (uint16)'
|
||||
]
|
||||
const sharedDbAbi = [
|
||||
'event NewSignature(address indexed signer, bytes32 msgHash)',
|
||||
|
@ -24,6 +26,7 @@ const sharedDbAbi = [
|
|||
'function getSignatures(bytes32 msgHash, address[] validators) view returns (bytes)',
|
||||
'function isResponsibleToSend(bytes32 msgHash, address[] validators, uint16 threshold, address validatorAddress) view returns (bool)'
|
||||
]
|
||||
const ACTION_TRANSFER = 10
|
||||
|
||||
const sideProvider = createProvider(SIDE_RPC_URL)
|
||||
const homeProvider = createProvider(HOME_RPC_URL)
|
||||
|
@ -35,13 +38,16 @@ const validatorAddress = ethers.utils.computeAddress(`0x${VALIDATOR_PRIVATE_KEY}
|
|||
|
||||
let blockNumber
|
||||
let homeSendQueue
|
||||
let rescheduleTransferQueue
|
||||
let channel
|
||||
let curBlockNumber
|
||||
|
||||
async function handleNewSignature(event) {
|
||||
const { msgHash } = event.values
|
||||
async function handleNewMessage(msgHash) {
|
||||
const message = await sharedDb.signedMessages(msgHash)
|
||||
const epoch = parseInt(message.slice(4, 8), 16)
|
||||
const messageAction = parseInt(message.slice(2, 4), 16)
|
||||
const epoch = messageAction === ACTION_TRANSFER
|
||||
? await bridge.epoch()
|
||||
: parseInt(message.slice(4, 8), 16)
|
||||
const [threshold, validators] = await Promise.all([
|
||||
bridge.getThreshold(epoch),
|
||||
bridge.getValidators(epoch)
|
||||
|
@ -56,20 +62,27 @@ async function handleNewSignature(event) {
|
|||
|
||||
if (isResponsibleToSend) {
|
||||
logger.info(`This validator is responsible to send message ${message}`)
|
||||
const signatures = await retry(
|
||||
() => sharedDb.getSignatures(msgHash, validators),
|
||||
-1,
|
||||
(curSignatures) => (curSignatures.length - 2) / 130 >= threshold
|
||||
)
|
||||
const isAlreadyProcessed = await bridge.handledMessages(msgHash)
|
||||
if (isAlreadyProcessed) {
|
||||
logger.debug('This message was already processed')
|
||||
} else {
|
||||
const signatures = await retry(
|
||||
() => sharedDb.getSignatures(msgHash, validators),
|
||||
-1,
|
||||
(curSignatures) => (curSignatures.length - 2) / 130 >= threshold
|
||||
)
|
||||
|
||||
const requiredSignatures = signatures.slice(0, 2 + 130 * threshold)
|
||||
const requiredSignatures = signatures.slice(0, 2 + 130 * threshold)
|
||||
|
||||
const data = await bridge.interface.functions.applyMessage.encode([message, requiredSignatures])
|
||||
const data = await bridge.interface.functions.applyMessage.encode(
|
||||
[message, requiredSignatures]
|
||||
)
|
||||
|
||||
homeSendQueue.send({
|
||||
data,
|
||||
blockNumber: curBlockNumber
|
||||
})
|
||||
homeSendQueue.send({
|
||||
data,
|
||||
blockNumber: curBlockNumber
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.debug(`This validator is not responsible to send message ${message}`)
|
||||
}
|
||||
|
@ -100,7 +113,7 @@ async function loop() {
|
|||
curBlockNumber = bridgeEvents[i].blockNumber
|
||||
const event = sharedDb.interface.parseLog(bridgeEvents[i])
|
||||
logger.trace('Consumed event %o %o', event, bridgeEvents[i])
|
||||
await handleNewSignature(event)
|
||||
await handleNewMessage(event.values.msgHash)
|
||||
}
|
||||
|
||||
blockNumber = endBlock + 1
|
||||
|
@ -112,6 +125,13 @@ async function loop() {
|
|||
async function initialize() {
|
||||
channel = await connectRabbit(RABBITMQ_URL)
|
||||
homeSendQueue = await assertQueue(channel, 'homeSendQueue')
|
||||
rescheduleTransferQueue = await assertQueue(channel, 'rescheduleTransferQueue')
|
||||
|
||||
rescheduleTransferQueue.consume(async (msg) => {
|
||||
const { msgHash } = JSON.parse(msg.content)
|
||||
await handleNewMessage(msgHash)
|
||||
channel.ack(msg)
|
||||
})
|
||||
|
||||
blockNumber = (parseInt(await redis.get('sideBlock'), 10) + 1) || parseInt(SIDE_START_BLOCK, 10)
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"name": "get-bridge-history",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"ethers": "4.0.37"
|
||||
"ethers": "4.0.37",
|
||||
"bech32": "1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const ethers = require('ethers')
|
||||
const bech32 = require('bech32')
|
||||
|
||||
const {
|
||||
HOME_RPC_URL, HOME_BRIDGE_ADDRESS, WITH_SIGNATURES, SIDE_RPC_URL, SIDE_SHARED_DB_ADDRESS
|
||||
|
@ -42,16 +43,26 @@ const actionNames = [
|
|||
'ADD_VALIDATOR',
|
||||
'REMOVE_VALIDATOR',
|
||||
'CHANGE_THRESHOLD',
|
||||
'CHANGE_RANGE_SIZE',
|
||||
'CHANGE_CLOSE_EPOCH',
|
||||
'START_KEYGEN',
|
||||
'CANCEL_KEYGEN',
|
||||
'TRANSFER'
|
||||
'TRANSFER',
|
||||
'CHANGE_MIN_PER_TX_LIMIT',
|
||||
'CHANGE_MAX_PER_TX_LIMIT',
|
||||
'INCREASE_EXECUTION_MAX_TX_LIMIT',
|
||||
'DECREASE_EXECUTION_MIN_TX_LIMIT',
|
||||
'CHANGE_RANGE_SIZE'
|
||||
]
|
||||
|
||||
let bridge
|
||||
let sharedDb
|
||||
|
||||
function hexAddressToBncAddress(hexAddress) {
|
||||
const addressBytes = Buffer.from(hexAddress, 'hex')
|
||||
const words = bech32.toWords(addressBytes)
|
||||
return bech32.encode('tbnb', words)
|
||||
}
|
||||
|
||||
function processEvent(event) {
|
||||
const { message } = event.values
|
||||
|
||||
|
@ -67,8 +78,7 @@ function processEvent(event) {
|
|||
case Action.CONFIRM_KEYGEN:
|
||||
return {
|
||||
...baseMsg,
|
||||
x: message.slice(8, 72),
|
||||
y: message.slice(72, 136)
|
||||
foreignAddress: hexAddressToBncAddress(message.slice(8, 48))
|
||||
}
|
||||
case Action.CONFIRM_FUNDS_TRANSFER:
|
||||
case Action.CONFIRM_CLOSE_EPOCH:
|
||||
|
@ -112,6 +122,15 @@ function processEvent(event) {
|
|||
to: `0x${message.slice(72, 112)}`,
|
||||
value: `0x${message.slice(112, 136)}`
|
||||
}
|
||||
case Action.CHANGE_MIN_PER_TX_LIMIT:
|
||||
case Action.CHANGE_MAX_PER_TX_LIMIT:
|
||||
case Action.INCREASE_EXECUTION_MAX_TX_LIMIT:
|
||||
case Action.DECREASE_EXECUTION_MIN_TX_LIMIT:
|
||||
return {
|
||||
...baseMsg,
|
||||
limit: `0x${message.slice(8, 32)}`,
|
||||
attempt: parseInt(message.slice(32, 66), 16)
|
||||
}
|
||||
default:
|
||||
return {
|
||||
...baseMsg,
|
||||
|
|
Loading…
Reference in New Issue