Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EOS on chain event parsing (3/4) #52

Merged
merged 8 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions cpp/contracts/pam.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace eosio {
using bytes = std::vector<uint8_t>;
namespace pam {
// aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906
const bytes CHAIN_ID = {
0xac, 0xa3, 0x76, 0xf2, 0x06, 0xb8, 0xfc, 0x25,
0xa6, 0xed, 0x44, 0xdb, 0xdc, 0x66, 0x54, 0x7c,
Expand Down Expand Up @@ -65,6 +66,10 @@ namespace eosio {
}

void check_authorization(name adapter, const operation& operation, const metadata& metadata, checksum256& event_id) {
// Metadata preimage format:
// | version | protocol | origin | blockHash | txHash | eventPayload |
// | 1B | 1B | 32B | 32B | 32B | varlen |
// +----------- context ---------+------------- event ---------------+
check(context_checks(operation, metadata), "unexpected context");

tee_pubkey _tee_pubkey(adapter, adapter.value);
Expand All @@ -84,6 +89,9 @@ namespace eosio {
public_key recovered_pubkey = recover_key(event_id, sig);
check(recovered_pubkey == tee_key, "invalid signature");

// Event payload format
// | emitter | topic-0 | topics-1 | topics-2 | topics-3 | eventBytes |
// | 32B | 32B | 32B | 32B | 32B | varlen |
offset = 0;
bytes event_payload(metadata.preimage.begin() + 98, metadata.preimage.end());
bytes emitter = extract_32bytes(event_payload, offset);
Expand All @@ -94,39 +102,59 @@ namespace eosio {
check(topic_zero == exp_topic_zero && !is_all_zeros(topic_zero), "unexpected topic zero");
offset += 32 * 4; // skip other topics

// check nonce
bytes event_data(event_payload.begin() + offset, event_payload.end());
// Checking the protocol id against 0x02 (EOS chains)
// If the condition is satified we expect data content to be
// a JSON string like:
//
// '{"event_bytes":"00112233445566"}'
//
// in hex would be
//
// 7b226576656e745f6279746573223a223030313132323333343435353636227d
//
// We want to extract 00112233445566, so this is performed by skipping
// the first 16 chars and the trailing 2 chars
uint8_t protocol_id = metadata.preimage[1];
auto start = protocol_id == 2 // EOSIO protocol
? event_payload.begin() + offset + 16
: event_payload.begin() + offset;

auto end = protocol_id == 2
? event_payload.end() - 2
: event_payload.end();

bytes raw_data(start, end);

bytes event_data = protocol_id == 2
? from_utf8_encoded_to_bytes(raw_data)
: raw_data;

offset = 0;
bytes nonce = extract_32bytes(event_data, offset);
uint64_t nonce_int = bytes32_to_uint64(nonce);

check(operation.nonce == nonce_int, "nonce do not match");
offset += 32;

// check origin token
bytes token = extract_32bytes(event_data, offset);
checksum256 token_hash = bytes32_to_checksum256(token);
check(operation.token == token_hash, "token address do not match");
offset += 32;

// check destination chain id
bytes dest_chain_id = extract_32bytes(event_data, offset);
check(operation.destinationChainId == dest_chain_id, "destination chain id does not match with the expected one");
check(CHAIN_ID == dest_chain_id, "destination chain id does not match with the current chain");
offset += 32;

// check amount
bytes amount = extract_32bytes(event_data, offset);
uint128_t amount_num = bytes32_to_uint128(amount);
check(to_wei(operation.amount) == amount_num, "amount do not match");
offset += 32;

// check sender address
bytes sender = extract_32bytes(event_data, offset);
check(operation.sender == sender, "sender do not match");
offset += 32;

// check recipient address
bytes recipient_len = extract_32bytes(event_data, offset);
offset += 32;
uint128_t recipient_len_num = bytes32_to_uint128(recipient_len);
Expand Down
32 changes: 30 additions & 2 deletions cpp/contracts/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ namespace eosio {
}

bytes extract_32bytes(const bytes& data, uint128_t offset) {
bytes _data(data.begin() + offset, data.begin() + offset + 32);
return _data;
bytes _data(data.begin() + offset, data.begin() + offset + 32);
return _data;
}

signature convert_bytes_to_signature(const bytes& input_bytes) {
Expand Down Expand Up @@ -214,4 +214,32 @@ namespace eosio {
name name_value(name_str);
return name_value;
}

uint8_t from_hex_char_to_uint8(uint8_t x) {
if ((x >= 97) && (x <= 102)) { // [a, b, c, ..., f]
x -= 87;
} else if ((x >= 65) && (x <= 70)) { // [A, B, C, ..., F]
x -= 55;
} else if ((x >= 48) && (x <= 57)) { // [0, 1, 2, ... ,9]
x -= 48;
}

return x;
}

bytes from_utf8_encoded_to_bytes(const bytes &utf8_encoded) {
check(utf8_encoded.size() % 2 == 0, "invalid utf-8 encoded string");

bytes x(utf8_encoded.size() / 2, 0); // fill it with zeros

uint64_t k = 0;
uint8_t b1, b2;
for (uint64_t i = 0; i < utf8_encoded.size(); i += 2) {
b1 = from_hex_char_to_uint8(utf8_encoded[i]);
b2 = from_hex_char_to_uint8(utf8_encoded[i + 1]);
x[k++] = b1 * 16 + b2;
}

return x;
}
}
74 changes: 73 additions & 1 deletion cpp/test/pam.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ describe('PAM testing', () => {

const attestation = []
const blockchain = new Blockchain()
const eosChainId =
'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906'
const operation = getOperationSample({
amount: '1337.0000 TKN',
sender: '0000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf',
token: '000000000000000000000000f2e246bb76df876cef8b38ae84130f4f55de395b',
chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
chainId: eosChainId,
recipient,
})
const data =
Expand Down Expand Up @@ -314,5 +316,75 @@ describe('PAM testing', () => {
expect(pam.contract.bc.console).to.be.equal(expectedEventId)
})
})

it('Should authorize an EOSIO operation successfully', async () => {
const blockId =
'179ed57f474f446f2c9f6ea6702724cdad0cf26422299b368755ed93c0134a35'
const txId =
'27598a45ee610287d85695f823f8992c10602ce5bf3240ee20635219de4f734f'
const nonce = 0
const token =
'0000000000000000000000000000000000000000000000746b6e2e746f6b656e'
const originChainId = no0x(Chains(Protocols.Eos).Jungle)
const destinationChainId = eosChainId
const amount = '9.9825 TKN'
const sender =
'0000000000000000000000000000000000000000000000000000000075736572'
const recipient = 'recipient'
const data = ''
const operation2 = getOperationSample({
blockId,
txId,
nonce,
token,
originChainId,
destinationChainId,
amount,
sender,
recipient,
data,
})

const eosEmitter = Buffer.from('adapter')
.toString('hex')
.padStart(64, '0')
const eosTopic0 = Buffer.from('swap').toString('hex').padStart(64, '0')

const ea2 = new ProofcastEventAttestator({
version: Versions.V1,
protocolId: Protocols.Eos,
chainId: Chains(Protocols.Eos).Jungle,
privateKey,
})

const eventData = {
event_bytes:
'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746b6e2e746f6b656eaca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e9060000000000000000000000000000000000000000000000008a88f6dc4656400000000000000000000000000000000000000000000000000000000000757365720000000000000000000000000000000000000000000000000000000000000009726563697069656e74',
}

const event2 = {
blockHash: operation2.blockId,
transactionHash: operation2.txId,
account: 'adapter',
action: 'swap',
data: eventData,
}

await adapter.contract.actions
.setorigin([operation2.originChainId, eosEmitter, eosTopic0])
.send(active(adapter.account))

const metadata2 = getMetadataSample({
signature: no0x(ea2.formatEosSignature(ea2.sign(event2))),
preimage: no0x(ea2.getEventPreImage(event2)),
})

await pam.contract.actions
.isauthorized([operation2, metadata2])
.send(active(user))
expect(pam.contract.bc.console).to.be.equal(
'42cde5d898147a7bd21006e0fe541092151262cb2bde3a3244587e7993c473e0',
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ProofcastEventAttestator {
return concat([
zeroPadValue(Buffer.from(event.account, 'utf-8'), 32),
...topics,
event.data,
Buffer.from(JSON.stringify(event.data), 'utf-8'),
gitmp01 marked this conversation as resolved.
Show resolved Hide resolved
])
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ describe('Proofcast Event Attestator Tests', () => {

// We are going to extract this = require( the a subfield of the)
// official data
const data =
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746b6e2e746f6b656e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000008a88f6dc465640000000000000000000000000000000000000000000000000000000000075736572000000000000000000000000000000000000000000000000000000000000002a307836386262656436613437313934656666316366353134623530656139313839353539376663393165'
const data = {
event_bytes:
'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746b6e2e746f6b656e00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000008a88f6dc465640000000000000000000000000000000000000000000000000000000000075736572000000000000000000000000000000000000000000000000000000000000002a307836386262656436613437313934656666316366353134623530656139313839353539376663393165',
}

const ea = new ProofcastEventAttestator({
version: Versions.V1,
Expand All @@ -73,7 +75,7 @@ describe('Proofcast Event Attestator Tests', () => {
}

const expectedSignature =
'0x1cfc81a6dc16147e5d82d3b9c2fef1ef2125403b92f328439da20ddd5903aef1276adefaeaca2579a342b00149c6916f93d988f81514ab535ebf19dd4eebed3519'
'0x1b546cb297b24aab5b445756f1d0beece3dad851d2cbd8d973f89f69e83f82b77016c87be815fa95bf25d37fb10c3f884cb200d38495e2d1c1bb686e9de38842a5'

expect(ea.formatEosSignature(ea.sign(event))).toStrictEqual(
expectedSignature,
Expand Down
71 changes: 53 additions & 18 deletions solidity/src/contracts/PAM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ contract PAM is Ownable, IPAM {

if (ECDSA.recover(eventId, metadata.signature) != teeAddress)
return (false, eventId);

// Event payload format
// | emitter | topic-0 | topics-1 | topics-2 | topics-3 | eventBytes |
// | 32B | 32B | 32B | 32B | 32B | varlen |
Expand All @@ -110,7 +109,28 @@ contract PAM is Ownable, IPAM {

offset += 32 * 3; // skip other topics

if (!this.doesContentMatchOperation(eventPayload[offset:], operation))
// Checking the protocol id against 0x02 (EOS chains)
gitmp01 marked this conversation as resolved.
Show resolved Hide resolved
// If the condition is satified we expect data content to be
// a JSON string like:
//
// '{"event_bytes":"00112233445566"}'
//
// in hex would be
//
// 7b226576656e745f6279746573223a223030313132323333343435353636227d
//
// We want to extract 00112233445566, so this is performed by skipping
// the first 16 chars and the trailing 2 chars
//
// Can't factor out these into variables because otherwise it would
// raise the "stack too deep" error
bytes memory eventBytes = uint8(metadata.preimage[1]) == 0x02
? _fromUTF8EncodedToBytes(
eventPayload[(offset + 16):(eventPayload.length - 2)]
)
: eventPayload[offset:];

if (!this.doesContentMatchOperation(eventBytes, operation))
return (false, eventId);

return (true, eventId);
Expand Down Expand Up @@ -150,6 +170,7 @@ contract PAM is Ownable, IPAM {
Metadata calldata metadata
) internal pure returns (bool) {
uint16 offset = 2; // skip protocol, version

bytes32 originChainId = bytes32(metadata.preimage[offset:offset += 32]);

if (originChainId != operation.originChainId) return false;
Expand All @@ -163,28 +184,42 @@ contract PAM is Ownable, IPAM {
return true;
}

function _fromHexCharToUint8(uint8 x) internal pure returns (uint8) {
gitmp01 marked this conversation as resolved.
Show resolved Hide resolved
if ((x >= 97) && (x <= 102)) {
x -= 87;
} else if ((x >= 65) && (x <= 70)) {
x -= 55;
} else if ((x >= 48) && (x <= 57)) {
x -= 48;
}
return x;
}

function _fromUTF8EncodedToBytes(
bytes memory utf8Encoded
) internal pure returns (bytes memory) {
require(utf8Encoded.length % 2 == 0, "invalid utf-8 encoded string");
bytes memory x = new bytes(utf8Encoded.length / 2);

uint k;
uint8 b1;
uint8 b2;
for (uint i = 0; i < utf8Encoded.length; i += 2) {
b1 = _fromHexCharToUint8(uint8(utf8Encoded[i]));
b2 = _fromHexCharToUint8(uint8(utf8Encoded[i + 1]));
x[k++] = bytes1(b1 * 16 + b2);
}
return x;
}

function _bytesToAddress(bytes memory tmp) internal pure returns (address) {
uint160 iaddr = 0;
uint160 b1;
uint160 b2;
for (uint256 i = 2; i < 2 + 2 * 20; i += 2) {
iaddr *= 256;
b1 = uint160(uint8(tmp[i]));
b2 = uint160(uint8(tmp[i + 1]));
if ((b1 >= 97) && (b1 <= 102)) {
b1 -= 87;
} else if ((b1 >= 65) && (b1 <= 70)) {
b1 -= 55;
} else if ((b1 >= 48) && (b1 <= 57)) {
b1 -= 48;
}
if ((b2 >= 97) && (b2 <= 102)) {
b2 -= 87;
} else if ((b2 >= 65) && (b2 <= 70)) {
b2 -= 55;
} else if ((b2 >= 48) && (b2 <= 57)) {
b2 -= 48;
}
b1 = uint160(_fromHexCharToUint8(uint8(tmp[i])));
b2 = uint160(_fromHexCharToUint8(uint8(tmp[i + 1])));
iaddr += (b1 * 16 + b2);
}
return address(iaddr);
Expand Down
Loading