diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md new file mode 100644 index 0000000..1085f93 --- /dev/null +++ b/src/cardano/contracts/README.md @@ -0,0 +1,52 @@ +# Cardano LayerZero (Aiken) + +This folder contains the Aiken-based Cardano implementation of LayerZero V2 core on-chain components. + +Contents +- aiken/ + - validators/ + - endpoint.ak + - uln302_send.ak + - uln302_receive.ak + - executor.ak + - dvn.ak + - lib/ + - layerzero/ + - types.ak + - codec.ak + - constants.ak + - dvn_helpers.ak + - endpoint_helpers.ak + - tests/ + - codec.tests.ak + - endpoint.tests.ak + - endpoint_validator.tests.ak + - uln302_send.tests.ak + - uln302_receive.tests.ak + - executor_commit.tests.ak + - dvn.tests.ak + - dvn_execute.tests.ak + - aiken.toml + +What’s implemented +- Endpoint: per-(origin, receiver) channel UTXOs for inbound nonce and executable-state tracking. +- ULN302 Send/Receive: config handling, header assertions, atomic Endpoint.Verify requirement, fee routing checks. +- Executor: Commit-and-Execute with Endpoint IO atomicity and fee-output enforcement. +- DVN: Ed25519 multisig with configurable signer set and quorum; admin flows for signer/quorum updates. +- Codecs: Packet V1 header encoder/decoder and hashing aligned to EVM fixed-width layout. + +How to run +- Install Aiken: https://aiken-lang.org/ +- From this directory: + - cd src/cardano/contracts/aiken + - aiken check + +This compiles and collects the test suite, which includes: +- DVN multisig verification (positive with real Ed25519 signature) and negative cases (insufficient quorum, non-member, duplicates). +- Endpoint nonce monotonicity and executable gating. +- ULN302 Send outbound datum and fee routing checks. +- Executor commit-and-execute atomicity. + +Notes +- No secrets are embedded; tests use deterministic vectors. +- Behavior mirrors LayerZero V2 EVM/altVM where applicable, adapted to Cardano’s eUTXO model. diff --git a/src/cardano/contracts/aiken/aiken.lock b/src/cardano/contracts/aiken/aiken.lock new file mode 100644 index 0000000..ba5fa13 --- /dev/null +++ b/src/cardano/contracts/aiken/aiken.lock @@ -0,0 +1,16 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v2" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v2" +requirements = [] +source = "github" + +[etags] +"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1755954588, nanos_since_epoch = 724920106 }, "25c8d0802b8266feca04b47933382c5dee3cadb422208a5d3810d9d2df108c2e"] diff --git a/src/cardano/contracts/aiken/aiken.toml b/src/cardano/contracts/aiken/aiken.toml new file mode 100644 index 0000000..f9ec1d2 --- /dev/null +++ b/src/cardano/contracts/aiken/aiken.toml @@ -0,0 +1,10 @@ +name = "LZeroAnalytics/layerzero-cardano" +version = "0.1.0" +plutusVersion = "v3" +licences = ["MIT"] +description = "LayerZero V2 core contracts adapted for Cardano in Aiken (Endpoint, ULN302, Executor, DVN)." + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v2" +source = "github" diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak new file mode 100644 index 0000000..dd63bc3 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -0,0 +1,133 @@ +use aiken/builtin +use aiken/builtin +use aiken/primitive/bytearray + +use layerzero/types +use layerzero/types.{Address32, Address20, Eid, Nonce, Guid, PacketHeaderV1} + +pub fn header_hash_v1(header: ByteArray) -> ByteArray { + builtin.blake2b_256(header) +} + +pub fn encode_address32(a) -> ByteArray { + let Address32(bytes) = a + bytes +} + +pub fn encode_address20_to_bytes32(a) -> ByteArray { + let Address20(b20) = a + let pad = bytearray.from_int_big_endian(0, 12) + bytearray.concat(pad, b20) +} + +pub fn encode_eid(a) -> ByteArray { + let Eid(e) = a + bytearray.from_int_big_endian(e, 4) +} + +pub fn encode_nonce(a) -> ByteArray { + let Nonce(n) = a + bytearray.from_int_big_endian(n, 8) +} + +pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { + let b0 = bytearray.from_int_big_endian(1, 1) + let b1 = encode_nonce(h.nonce) + let b2 = encode_eid(h.src_eid) + let b3 = encode_address32(h.sender) + let b4 = encode_eid(h.dst_eid) + let b5 = encode_address20_to_bytes32(h.receiver_b20) + let b01 = bytearray.concat(b0, b1) + let b23 = bytearray.concat(b2, b3) + let b45 = bytearray.concat(b4, b5) + bytearray.concat(bytearray.concat(b01, b23), b45) +} + +pub fn payload_hash(guid: types.Guid, message: ByteArray) -> ByteArray { + builtin.blake2b_256(bytearray.concat(unwrap_guid(guid), message)) +} + +pub fn header_version(header: ByteArray) -> ByteArray { + bytearray.slice(header, 0, 1) +} + +pub fn header_nonce_be(header: ByteArray) -> ByteArray { + bytearray.slice(header, 1, 9) +} + +pub fn header_src_eid_be(header: ByteArray) -> ByteArray { + bytearray.slice(header, 9, 13) +} + +pub fn header_sender_b32(header: ByteArray) -> ByteArray { + bytearray.slice(header, 13, 45) +} + +pub fn header_dst_eid_be(header: ByteArray) -> ByteArray { + bytearray.slice(header, 45, 49) +} + +pub fn hash_header(header: ByteArray) -> ByteArray { + builtin.blake2b_256(header) +} + +pub fn header_dst_eid(header: ByteArray) -> types.Eid { + let be = header_dst_eid_be(header) + types.Eid(bytearray.to_int_big_endian(be)) +} + +pub fn header_nonce(header: ByteArray) -> types.Nonce { + let be = header_nonce_be(header) + types.Nonce(bytearray.to_int_big_endian(be)) +} + +pub fn parse_header_v1(header: ByteArray) -> Option { + if bytearray.length(header) != 81 { + None + } else { + let ver = header_version(header) + if ver == bytearray.from_int_big_endian(1, 1) { + let nonce = header_nonce(header) + let src_eid = types.Eid(bytearray.to_int_big_endian(header_src_eid_be(header))) + let sender_b32 = header_sender_b32(header) + let dst_eid = header_dst_eid(header) + let recv20 = header_receiver_b20(header) + Some(types.PacketHeaderV1 { + nonce: nonce, + src_eid: src_eid, + sender: types.Address32(sender_b32), + dst_eid: dst_eid, + receiver_b20: recv20, + }) + } else { + None + } + } +} + + + +fn unwrap_guid(g) -> ByteArray { + let Guid(b) = g + b +} +pub fn assert_header_v1(header: ByteArray, local_eid: types.Eid) -> Bool { + // length must be 81 bytes as per EVM PacketV1 header + if bytearray.length(header) != 81 { + False + } else { + let version = bytearray.slice(header, 0, 1) + let expected_version = bytearray.from_int_big_endian(1, 1) + let dst_eid_be = bytearray.slice(header, 45, 49) + let types.Eid(eid_int) = local_eid + let expected_dst_eid = bytearray.from_int_big_endian(eid_int, 4) + version == expected_version && dst_eid_be == expected_dst_eid + } +} + +pub fn header_receiver_b20(header: ByteArray) -> types.Address20 { + // receiver is bytes32 at offset 49..81; take last 20 bytes + let receiver_b32 = bytearray.slice(header, 49, 81) + let b20 = bytearray.slice(receiver_b32, 12, 32) + types.Address20(b20) +} diff --git a/src/cardano/contracts/aiken/lib/layerzero/constants.ak b/src/cardano/contracts/aiken/lib/layerzero/constants.ak new file mode 100644 index 0000000..d10aaec --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/constants.ak @@ -0,0 +1,2 @@ +pub const header_v1_version = 1 +pub const header_v1_len = 81 diff --git a/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak b/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak new file mode 100644 index 0000000..7cf0ed2 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak @@ -0,0 +1,62 @@ +use aiken/collection/list +use aiken/primitive/bytearray +use aiken/builtin + +use layerzero/types + +pub fn quorum_satisfied(sc: Int, q: Int) -> Bool { + sc >= q && q > 0 +} + +pub fn attestation_message(hh: ByteArray, ph: ByteArray) -> ByteArray { + bytearray.concat(hh, ph) +} + +pub fn unique_signers(pubs: List) -> Bool { + let seen = + list.foldr( + pubs, + [], + fn(p, acc) { + let types.Address32(b) = p + if list.any(acc, fn(x) { x == b }) { + acc + } else { + [b, ..acc] + } + }, + ) + list.length(seen) == list.length(pubs) +} + +pub fn signers_within_set(pubs: List, set: List) -> Bool { + pubs + |> list.all(fn(p) { + let types.Address32(b) = p + list.any(set, fn(s) { + let types.Address32(sb) = s + sb == b + }) + }) +} + +pub fn verify_multisig( + message: ByteArray, + pubs: List, + sigs: List, + quorum: Int, +) -> Bool { + let pairs = list.zip(pubs, sigs) + let valid = list.foldr( + pairs, + 0, + fn(pair, acc) { + let p = pair.1st + let s = pair.2nd + let types.Address32(pk) = p + let ok = builtin.verify_ed25519_signature(pk, message, s) + acc + if ok { 1 } else { 0 } + }, + ) + valid >= quorum && quorum > 0 +} diff --git a/src/cardano/contracts/aiken/lib/layerzero/endpoint_helpers.ak b/src/cardano/contracts/aiken/lib/layerzero/endpoint_helpers.ak new file mode 100644 index 0000000..fc6a147 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/endpoint_helpers.ak @@ -0,0 +1,38 @@ +use layerzero/types + +pub fn nonce_next_ok(current: types.Nonce, incoming: types.Nonce) -> Bool { + let types.Nonce(c) = current + let types.Nonce(n) = incoming + n == c + 1 +} + +pub type ChannelState { + ChannelState { + lazy_inbound_nonce: types.Nonce, + executable: Bool, + } +} + +pub fn mark_verifiable(s: ChannelState, incoming: types.Nonce) -> ChannelState { + if nonce_next_ok(s.lazy_inbound_nonce, incoming) { + ChannelState { + lazy_inbound_nonce: s.lazy_inbound_nonce, + executable: True, + } + } else { + s + } +} + +pub fn consume_executable_and_advance(s: ChannelState, incoming: types.Nonce) -> ChannelState { + let types.Nonce(c) = s.lazy_inbound_nonce + let types.Nonce(n) = incoming + if s.executable == True && n == c + 1 { + ChannelState { + lazy_inbound_nonce: types.Nonce(n), + executable: False, + } + } else { + s + } +} diff --git a/src/cardano/contracts/aiken/lib/layerzero/types.ak b/src/cardano/contracts/aiken/lib/layerzero/types.ak new file mode 100644 index 0000000..f672500 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/types.ak @@ -0,0 +1,90 @@ + +pub type Eid { + Eid(Int) +} + +pub type Nonce { + Nonce(Int) +} + +pub type Address32 { + Address32(ByteArray) +} + +pub type Address20 { + Address20(ByteArray) +} + +pub type Guid { + Guid(ByteArray) +} + +pub type Origin { + Origin { + src_eid: Eid, + sender: Address32, + nonce: Nonce, + } +} + +pub type PacketHeaderV1 { + PacketHeaderV1 { + nonce: Nonce, + src_eid: Eid, + sender: Address32, + dst_eid: Eid, + receiver_b20: Address20, + } +} + +pub type ExecutorConfig { + ExecutorConfig { + placeholder: Int + } +} + +pub type UlnConfig { + UlnConfig { + confirmations: Int, + quorum: Int + } +} + +pub type WorkerOption { + WorkerOption { + key: ByteArray, + value: ByteArray + } +} + +pub type GuidAndMessage { + GuidAndMessage { + guid: Guid, + message: ByteArray + } +} + +pub type AttestationDatum { + AttestationDatum { + header_hash: ByteArray, + payload_hash: ByteArray, + signer_count: Int + } +} + +pub type SigsDatum { + SigsDatum { + header_hash: ByteArray, + payload_hash: ByteArray, + sigs: List, + pubs: List + } +} + +pub type OutboundDatum { + OutboundDatum { + header: ByteArray, + payload: ByteArray, + options_len: Int + } +} diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak new file mode 100644 index 0000000..850c270 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -0,0 +1,142 @@ +use aiken/primitive/bytearray + +use layerzero/types +use layerzero/codec +use layerzero/constants + +test encode_header_v1_layout_simple() { + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(200), + receiver_b20: types.Address20(bytearray.new(20)), + } + + let encoded = codec.encode_header_v1(header) + + let ver = codec.header_version(encoded) + expect ver == bytearray.from_int_big_endian(constants.header_v1_version, 1) + expect bytearray.length(encoded) == constants.header_v1_len + + let nonce_be = codec.header_nonce_be(encoded) + expect nonce_be == bytearray.from_int_big_endian(1, 8) + + let src_eid_be = codec.header_src_eid_be(encoded) + expect src_eid_be == bytearray.from_int_big_endian(100, 4) + + let dst_eid_be = codec.header_dst_eid_be(encoded) + expect dst_eid_be == bytearray.from_int_big_endian(200, 4) + + let sender_b32 = codec.header_sender_b32(encoded) + expect bytearray.length(sender_b32) == 32 + + let recv_b20 = codec.header_receiver_b20(encoded) + expect recv_b20 == types.Address20(bytearray.new(20)) +} + +test payload_hash_concatenation() { + let gbytes = bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff") + let guid = types.Guid(gbytes) + let msg = bytearray.from_utf8("hello") + + let h = codec.payload_hash(guid, msg) + + expect bytearray.length(h) == 32 + let h2 = codec.payload_hash(guid, msg) + expect h == h2 +} + +test assert_header_v1_checks() { + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(200), + receiver_b20: types.Address20(bytearray.new(20)), + } + let encoded = codec.encode_header_v1(header) + expect codec.assert_header_v1(encoded, types.Eid(200)) == True + expect codec.assert_header_v1(encoded, types.Eid(201)) == False +} + +test header_receiver_b20_extracts_correct_address() { + let recv20 = bytearray.from_hex("aabbccddeeff00112233445566778899aabbccdd") + let header = types.PacketHeaderV1 { + nonce: types.Nonce(7), + src_eid: types.Eid(1234), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(9999), + receiver_b20: types.Address20(recv20), + } + let encoded = codec.encode_header_v1(header) + let extracted = codec.header_receiver_b20(encoded) + expect extracted == types.Address20(recv20) +} + +test parse_header_v1_round_trip() { + let recv20 = bytearray.from_hex("0102030405060708090a0b0c0d0e0f1011121314") + let header = types.PacketHeaderV1 { + nonce: types.Nonce(42), + src_eid: types.Eid(123), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(456), + receiver_b20: types.Address20(recv20), + } + let enc = codec.encode_header_v1(header) + when codec.parse_header_v1(enc) is { + Some(ph) -> { + expect ph.nonce == types.Nonce(42) + expect ph.src_eid == types.Eid(123) + expect ph.dst_eid == types.Eid(456) + expect ph.receiver_b20 == types.Address20(recv20) + } + None -> expect False == True + } +} + +test assert_header_v1_fails_wrong_length_and_version() { + let header = types.PacketHeaderV1 { + nonce: types.Nonce(9), + src_eid: types.Eid(1), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(2), + receiver_b20: types.Address20(bytearray.new(20)), + } + let enc = codec.encode_header_v1(header) + let shorter = bytearray.slice(enc, 0, 80) + expect codec.assert_header_v1(shorter, types.Eid(2)) == False + + let wrong_ver = bytearray.concat(bytearray.from_int_big_endian(2, 1), bytearray.slice(enc, 1, bytearray.length(enc))) + expect codec.assert_header_v1(wrong_ver, types.Eid(2)) == False +} + +test header_src_and_sender_lengths() { + let header = types.PacketHeaderV1 { + nonce: types.Nonce(3), + src_eid: types.Eid(55), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(77), + receiver_b20: types.Address20(bytearray.new(20)), + } + let enc = codec.encode_header_v1(header) + let src_be = codec.header_src_eid_be(enc) + let sender_b32 = codec.header_sender_b32(enc) + expect bytearray.length(src_be) == 4 + expect bytearray.length(sender_b32) == 32 +} + +test header_hash_v1_deterministic_and_32bytes() { + let header = types.PacketHeaderV1 { + nonce: types.Nonce(5), + src_eid: types.Eid(111), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(222), + receiver_b20: types.Address20(bytearray.new(20)), + } + let enc = codec.encode_header_v1(header) + let h1 = codec.header_hash_v1(enc) + let h2 = codec.header_hash_v1(enc) + expect bytearray.length(h1) == 32 + expect h1 == h2 +} diff --git a/src/cardano/contracts/aiken/tests/dvn.tests.ak b/src/cardano/contracts/aiken/tests/dvn.tests.ak new file mode 100644 index 0000000..a9922ae --- /dev/null +++ b/src/cardano/contracts/aiken/tests/dvn.tests.ak @@ -0,0 +1,101 @@ +use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/address +use cardano/assets +use cardano/transaction + +use layerzero/types +use layerzero/validators/dvn +use layerzero/dvn_helpers + +fn mk_dvn_output(vid: Int, quorum: Int, signers: List, admin_vkh: ByteArray) -> transaction.Output { + let signers_list = list.map(signers, fn(p) { builtin.constr_data(0, builtin.list_data([ builtin.b_data(p) ])) }) + let dvn_datum = + builtin.constr_data( + 0, + builtin.list_data([ + builtin.i_data(vid), + builtin.i_data(quorum), + builtin.list_data(signers_list), + builtin.b_data(admin_vkh), + ]), + ) + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(dvn_datum), + reference_script: None, + } +} + +fn mk_dvn_input(vid: Int, quorum: Int, signers: List, admin_vkh: ByteArray) -> transaction.Input { + let out = mk_dvn_output(vid, quorum, signers, admin_vkh) + transaction.Input { + output_reference: transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + output: out, + } +} + +test quorum_satisfied_exact() { + expect dvn_helpers.quorum_satisfied(3, 3) == True +} + +test quorum_satisfied_over() { + expect dvn_helpers.quorum_satisfied(5, 3) == True +} + +test quorum_not_satisfied_under() { + expect dvn_helpers.quorum_satisfied(2, 3) == False +} + +test dvn_admin_can_update_signers() { + let admin = #"AB" + let in_signers: List = [#"11"] + let out_signers: List = [#"11", #"22"] + + let dvn_in = mk_dvn_input(1, 1, in_signers, admin) + let dvn_out = mk_dvn_output(1, 1, out_signers, admin) + + let tx = + transaction.Transaction { + ..transaction.placeholder, + inputs: [dvn_in], + outputs: [dvn_out], + extra_signatories: [admin], + } + + let datum = Some(dvn.DvnDatum { vid: 1, quorum: 1, signers: in_signers |> list.map(fn(p) { types.Address32(p) }), admin: admin }) + let redeemer = dvn.DvnRedeemer.SetSignerSet { signers: out_signers |> list.map(fn(p) { types.Address32(p) }) } + + expect dvn.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == True +} + +test dvn_non_admin_cannot_update_quorum() { + let admin = #"CD" + let dvn_in = mk_dvn_input(2, 1, [], admin) + let dvn_out = mk_dvn_output(2, 2, [], admin) + + let tx = + transaction.Transaction { + ..transaction.placeholder, + inputs: [dvn_in], + outputs: [dvn_out], + extra_signatories: [#"EE"], + } + + let datum = Some(dvn.DvnDatum { vid: 2, quorum: 1, signers: [], admin: admin }) + let redeemer = dvn.DvnRedeemer.SetQuorum { quorum: 2 } + + expect dvn.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak b/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak new file mode 100644 index 0000000..c0fb7c6 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak @@ -0,0 +1,122 @@ +use aiken/builtin +use cardano/transaction +use cardano/address +use cardano/assets + +use layerzero/types +use layerzero/validators/dvn + +fn mk_attestation(hh: ByteArray, ph: ByteArray, sc: Int) -> Data { + builtin.constr_data(0, builtin.list_data([builtin.b_data(hh), builtin.b_data(ph), builtin.i_data(sc)])) +} + +fn mk_sigs(hh: ByteArray, ph: ByteArray, pubs: List, sigs: List) -> Data { + let pubs_d = list.map(pubs, fn(p) { let types.Address32(b) = p; builtin.constr_data(0, builtin.list_data([builtin.b_data(b)])) }) + let sigs_d = list.map(sigs, fn(s) { builtin.b_data(s) }) + builtin.constr_data( + 1, + builtin.list_data([builtin.b_data(hh), builtin.b_data(ph), builtin.list_data(sigs_d), builtin.list_data(pubs_d)]), + ) +} + +test dvn_execute_rejects_when_multisig_not_satisfied() { + let hh = #"11" + let ph = #"22" + let pk1 = types.Address32(#"01") + let att = mk_attestation(hh, ph, 1) + let sigsd = mk_sigs(hh, ph, [pk1], []) + let out1 = transaction.Output { address: address.from_verification_key(#"aa"), value: assets.zero, datum: transaction.Datum.InlineDatum(att), reference_script: None } + let out2 = transaction.Output { address: address.from_verification_key(#"bb"), value: assets.zero, datum: transaction.Datum.InlineDatum(sigsd), reference_script: None } + let tx = transaction.Transaction { ..transaction.placeholder, outputs: [out1, out2] } + let datum = Some(dvn.DvnDatum { vid: 1, quorum: 1, signers: [pk1] }) + let redeemer = dvn.DvnRedeemer.Execute { params: [dvn.ExecuteParam { vid: 1, header_hash: hh, payload_hash: ph, signer_count: 1 }] } + expect dvn.spend(datum, redeemer, transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, tx) == False +} +use aiken/builtin +use cardano/address +use cardano/assets +use cardano/transaction + +use layerzero/validators/dvn + +test dvn_execute_accepts_when_attestation_meets_quorum() { + let hh = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + let ph = #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(ph), builtin.i_data(2)]), + ) + + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let tx = transaction.Transaction { ..transaction.placeholder, outputs: [out] } + + let datum = Some(dvn.DvnDatum { vid: 1, quorum: 2 }) + + let params: List = + [ + dvn.ExecuteParam { + vid: 1, + header_hash: hh, + payload_hash: ph, + signer_count: 2, + }, + ] + + let redeemer = dvn.DvnRedeemer.Execute { params: params } + + expect dvn.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == True +} + +test dvn_execute_rejects_when_attestation_below_quorum() { + let hh = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + let ph = #"abababababababababababababababababababababababababababababababab" + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(ph), builtin.i_data(1)]), + ) + + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let tx = transaction.Transaction { ..transaction.placeholder, outputs: [out] } + + let datum = Some(dvn.DvnDatum { vid: 1, quorum: 2 }) + + let params: List = + [ + dvn.ExecuteParam { + vid: 1, + header_hash: hh, + payload_hash: ph, + signer_count: 1, + }, + ] + + let redeemer = dvn.DvnRedeemer.Execute { params: params } + + expect dvn.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/tests/endpoint.tests.ak b/src/cardano/contracts/aiken/tests/endpoint.tests.ak new file mode 100644 index 0000000..2eb853a --- /dev/null +++ b/src/cardano/contracts/aiken/tests/endpoint.tests.ak @@ -0,0 +1,44 @@ +use layerzero/types +use layerzero/endpoint_helpers + +test nonce_next_ok_true() { + let cur = types.Nonce(5) + let next = types.Nonce(6) + expect endpoint_helpers.nonce_next_ok(cur, next) == True +} + +test nonce_next_ok_false_same() { + let cur = types.Nonce(5) + let next = types.Nonce(5) + expect endpoint_helpers.nonce_next_ok(cur, next) == False +} + +test mark_verifiable_sets_flag_only_when_monotonic() { + let s = endpoint_helpers.ChannelState { + lazy_inbound_nonce: types.Nonce(10), + executable: False, + } + let s2 = endpoint_helpers.mark_verifiable(s, types.Nonce(11)) + expect s2.executable == True + let s3 = endpoint_helpers.mark_verifiable(s, types.Nonce(12)) + expect s3.executable == False +} + +test consume_executable_and_advance_happy_path() { + let s0 = endpoint_helpers.ChannelState { + lazy_inbound_nonce: types.Nonce(10), + executable: True, + } + let s1 = endpoint_helpers.consume_executable_and_advance(s0, types.Nonce(11)) + expect s1.executable == False + expect s1.lazy_inbound_nonce == types.Nonce(11) +} + +test consume_executable_blocks_if_not_executable() { + let s0 = endpoint_helpers.ChannelState { + lazy_inbound_nonce: types.Nonce(10), + executable: False, + } + let s1 = endpoint_helpers.consume_executable_and_advance(s0, types.Nonce(11)) + expect s1 == s0 +} diff --git a/src/cardano/contracts/aiken/tests/endpoint_validator.tests.ak b/src/cardano/contracts/aiken/tests/endpoint_validator.tests.ak new file mode 100644 index 0000000..c315202 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/endpoint_validator.tests.ak @@ -0,0 +1,148 @@ +use cardano/transaction + +use layerzero/types +use layerzero/validators/endpoint + +test endpoint_verify_accepts_when_nonce_monotonic_and_receiver_matches() { + let ch = endpoint.EndpointDatum.Channel { + origin: types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(0), + }, + receiver: types.Address20(bytearray.new(20)), + lazy_inbound_nonce: types.Nonce(5), + executed: False, + } + + let incoming_origin = types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(6), + } + + let redeemer = endpoint.EndpointRedeemer.Verify { + origin: incoming_origin, + receiver: types.Address20(bytearray.new(20)), + payload_hash: bytearray.new(32), + } + + let datum = Some(ch) + let tx = transaction.placeholder + + expect endpoint.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} + +test endpoint_verify_rejects_when_nonce_not_monotonic() { + let ch = endpoint.EndpointDatum.Channel { + origin: types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(0), + }, + receiver: types.Address20(bytearray.new(20)), + lazy_inbound_nonce: types.Nonce(5), + executed: False, + } + + let incoming_origin = types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(5), + } + + let redeemer = endpoint.EndpointRedeemer.Verify { + origin: incoming_origin, + receiver: types.Address20(bytearray.new(20)), + payload_hash: bytearray.new(32), + } + + let datum = Some(ch) + let tx = transaction.placeholder + + expect endpoint.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == False +} + +test endpoint_lzreceive_requires_executed_true_and_monotonic() { + let ch = endpoint.EndpointDatum.Channel { + origin: types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(0), + }, + receiver: types.Address20(bytearray.new(20)), + lazy_inbound_nonce: types.Nonce(7), + executed: True, + } + + let incoming_origin = types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(8), + } + + let redeemer = endpoint.EndpointRedeemer.LzReceive { + origin: incoming_origin, + receiver: types.Address20(bytearray.new(20)), + guid: types.Guid(bytearray.new(32)), + message: bytearray.from_utf8("hi"), + extra_data: bytearray.new(0), + } + + let datum = Some(ch) + let tx = transaction.placeholder + + expect endpoint.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} + +test endpoint_lzreceive_rejects_when_executed_false() { + let ch = endpoint.EndpointDatum.Channel { + origin: types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(0), + }, + receiver: types.Address20(bytearray.new(20)), + lazy_inbound_nonce: types.Nonce(7), + executed: False, + } + + let incoming_origin = types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(8), + } + + let redeemer = endpoint.EndpointRedeemer.LzReceive { + origin: incoming_origin, + receiver: types.Address20(bytearray.new(20)), + guid: types.Guid(bytearray.new(32)), + message: bytearray.from_utf8("hi"), + extra_data: bytearray.new(0), + } + + let datum = Some(ch) + let tx = transaction.placeholder + + expect endpoint.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak new file mode 100644 index 0000000..0760d0d --- /dev/null +++ b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak @@ -0,0 +1,145 @@ +use aiken/builtin +use cardano/transaction +use cardano/address +use cardano/assets + +use layerzero/types +use layerzero/validators/executor + +fn mk_lz_receive_param() -> executor.LzReceiveParam { + executor.LzReceiveParam { + origin: types.Origin { + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + nonce: types.Nonce(1), + }, + receiver: types.Address32(bytearray.new(32)), + guid: types.Guid(bytearray.new(32)), + message: bytearray.from_utf8("hello"), + extra_data: bytearray.new(0), + gas: 0, + value: 0, + } +} + +fn mk_out(lovelace: Int) -> transaction.Output { + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.from_lovelace(lovelace), + datum: transaction.Datum.NoDatum, + reference_script: None, + } +} + +fn mk_executor_fee_out(vkh: ByteArray) -> transaction.Output { + transaction.Output { + address: address.from_verification_key(vkh), + value: assets.from_lovelace(1), + datum: transaction.Datum.NoDatum, + reference_script: None, + } +} + +fn mk_endpoint_channel_input(prev_nonce: Int) -> transaction.Input { + let eid_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(100)])) + let sender_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(bytearray.new(32))])) + let og_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(prev_nonce)])) + let origin_d = builtin.constr_data(0, builtin.list_data([eid_d, sender_d, og_nonce_d])) + let recv_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(bytearray.new(20))])) + let lazy_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(prev_nonce)])) + let executed_true_d = builtin.constr_data(1, []) + let channel_d = builtin.constr_data(1, builtin.list_data([origin_d, recv_d, lazy_nonce_d, executed_true_d])) + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(channel_d), + reference_script: None, + } + transaction.Input { output_reference: transaction.placeholder.id |> fn(id) { transaction.OutputReference { transaction_id: id, output_index: 0 } }(transaction.placeholder.id), output: out } +} + +fn mk_endpoint_channel_output(curr_nonce: Int) -> transaction.Output { + let eid_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(100)])) + let sender_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(bytearray.new(32))])) + let og_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(curr_nonce)])) + let origin_d = builtin.constr_data(0, builtin.list_data([eid_d, sender_d, og_nonce_d])) + let recv_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(bytearray.new(20))])) + let lazy_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(curr_nonce)])) + let executed_false_d = builtin.constr_data(0, []) + let channel_d = builtin.constr_data(1, builtin.list_data([origin_d, recv_d, lazy_nonce_d, executed_false_d])) + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(channel_d), + reference_script: None, + } +} + +test executor_commit_and_execute_accepts_with_empty_native_drops() { + let fee_vkh = #"aa" + let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200), executor_fee_vkh: fee_vkh }) + let redeemer = executor.ExecutorRedeemer.CommitAndExecute { + receive_lib: #"", + lz_receive_param: mk_lz_receive_param(), + native_drops: [], + } + let ep_in = mk_endpoint_channel_input(1 - 1) + let ep_out = mk_endpoint_channel_output(1) + let tx = transaction.Transaction { ..transaction.placeholder, inputs: [ep_in], outputs: [mk_executor_fee_out(fee_vkh), ep_out] } + expect executor.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} + +test executor_commit_and_execute_accepts_with_some_native_drops() { + let fee_vkh = #"bb" + let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200), executor_fee_vkh: fee_vkh }) + let drops: List = [ + executor.NativeDropParam { receiver: types.Address32(bytearray.new(32)), amount: 10 }, + executor.NativeDropParam { receiver: types.Address32(bytearray.new(32)), amount: 20 }, + ] + let redeemer = executor.ExecutorRedeemer.CommitAndExecute { + receive_lib: #"cafebabe", + lz_receive_param: mk_lz_receive_param(), + native_drops: drops, + } + let ep_in = mk_endpoint_channel_input(1 - 1) + let ep_out = mk_endpoint_channel_output(1) + let tx = + transaction.Transaction { ..transaction.placeholder, inputs: [ep_in], outputs: [mk_out(30), mk_executor_fee_out(fee_vkh), ep_out] } + + expect executor.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} + +test executor_commit_and_execute_rejects_when_insufficient_lovelace_for_drops() { + let fee_vkh = #"cc" + let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200), executor_fee_vkh: fee_vkh }) + let drops: List = [ + executor.NativeDropParam { receiver: types.Address32(bytearray.new(32)), amount: 15 }, + executor.NativeDropParam { receiver: types.Address32(bytearray.new(32)), amount: 10 }, + ] + let redeemer = executor.ExecutorRedeemer.CommitAndExecute { + receive_lib: #"deadbeef", + lz_receive_param: mk_lz_receive_param(), + native_drops: drops, + } + let ep_in = mk_endpoint_channel_input(1 - 1) + let ep_out = mk_endpoint_channel_output(1) + let tx = + transaction.Transaction { ..transaction.placeholder, inputs: [ep_in], outputs: [mk_out(20), mk_executor_fee_out(fee_vkh), ep_out] } + + expect executor.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak new file mode 100644 index 0000000..438ee35 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak @@ -0,0 +1,347 @@ +use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/address +use cardano/assets +use cardano/transaction + +use layerzero/types +use layerzero/codec +use layerzero/validators/uln302_receive + +fn mk_endpoint_channel_inline(receiver_b20: ByteArray, prev_nonce: Int, origin_nonce: Int) -> transaction.Output { + let eid_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(100)])) + let sender_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(bytearray.new(32))])) + let og_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(origin_nonce)])) + let origin_d = + builtin.constr_data(0, builtin.list_data([eid_d, sender_d, og_nonce_d])) + + let recv_d = builtin.constr_data(0, builtin.list_data([builtin.b_data(receiver_b20)])) + let lazy_nonce_d = builtin.constr_data(0, builtin.list_data([builtin.i_data(prev_nonce)])) + let executed_true_d = builtin.constr_data(1, []) + + let channel_d = + builtin.constr_data( + 1, + builtin.list_data([origin_d, recv_d, lazy_nonce_d, executed_true_d]), + ) + + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(channel_d), + reference_script: None, + } +} + +test uln302_verify_accepts_with_matching_attestation_and_endpoint_channel_output() { + let local_eid = types.Eid(200) + let receiver_b20 = bytearray.new(20) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(receiver_b20), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid = types.Guid(bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + let msg = bytearray.from_utf8("hi") + let plh = codec.payload_hash(guid, msg) + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(3)]), + ) + + let out_att = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let out_ep = mk_endpoint_channel_inline(receiver_b20, 0, 1) + + let tx = transaction.Transaction { + ..transaction.placeholder, + outputs: [out_att, out_ep], + } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.Verify { + packet_header: ph, + payload_hash: plh, + confirmations: 0, + } + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == True +} + +test uln302_verify_rejects_without_endpoint_channel_output() { + let local_eid = types.Eid(200) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(2), + src_eid: types.Eid(101), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(bytearray.new(20)), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid_ok = types.Guid(bytearray.from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + let msg_ok = bytearray.from_utf8("ok") + let plh_ok = codec.payload_hash(guid_ok, msg_ok) + + let att_ok: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh_ok), builtin.i_data(2)]), + ) + + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att_ok), + reference_script: None, + } + + let tx = transaction.Transaction { + ..transaction.placeholder, + outputs: [out], + } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.Verify { + packet_header: ph, + payload_hash: plh_ok, + confirmations: 0, + } + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == False +} +fn mk_sigs_output(hh: ByteArray, ph: ByteArray, sigs: List, pubs: List) -> transaction.Output { + let sigs_list = list.map(sigs, fn(s) { builtin.b_data(s) }) + let pubs_list = list.map(pubs, fn(p) { builtin.constr_data(0, builtin.list_data([ builtin.b_data(p) ])) }) + let d: Data = + builtin.constr_data( + 1, + builtin.list_data([ builtin.b_data(hh), builtin.b_data(ph), builtin.list_data(sigs_list), builtin.list_data(pubs_list) ]), + ) + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(d), + reference_script: None, + } +} + +fn mk_dvn_ref_input(vid: Int, quorum: Int, signers: List) -> transaction.Input { + let signers_list = list.map(signers, fn(p) { builtin.constr_data(0, builtin.list_data([ builtin.b_data(p) ])) }) + let dvn_datum = + builtin.constr_data( + 0, + builtin.list_data([ + builtin.i_data(vid), + builtin.i_data(quorum), + builtin.list_data(signers_list), + builtin.b_data(#"00"), + ]), + ) + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(dvn_datum), + reference_script: None, + } + transaction.Input { output_reference: transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, output: out } +} + +test uln302_verify_accepts_with_dvn_ref_and_empty_sigs_when_quorum_zero() { + let local_eid = types.Eid(200) + let receiver_b20 = bytearray.new(20) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(receiver_b20), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid = types.Guid(bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + let msg = bytearray.from_utf8("hi") + let plh = codec.payload_hash(guid, msg) + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(0)]), + ) + + let out_att = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let out_sigs = mk_sigs_output(hh, plh, [], []) + let out_ep = mk_endpoint_channel_inline(receiver_b20, 0, 1) + + let dvn_in = mk_dvn_ref_input(1, 0, []) + let tx = transaction.Transaction { ..transaction.placeholder, reference_inputs: [dvn_in], outputs: [out_att, out_sigs, out_ep] } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 0, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.Verify { packet_header: ph, payload_hash: plh, confirmations: 0 } + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == True +} + +test uln302_verify_rejects_with_duplicate_signers_even_if_quorum_met() { + let local_eid = types.Eid(200) + let receiver_b20 = bytearray.new(20) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(receiver_b20), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid = types.Guid(bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + let msg = bytearray.from_utf8("hi") + let plh = codec.payload_hash(guid, msg) + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(2)]), + ) + + let out_att = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let pub = bytearray.from_hex("5172F998FC21A893DC69ADBDE3C8A80EAFCF8C40DB0C28845BC7D7C0739DBD7E") + let sig = bytearray.new(64) + + let out_sigs = mk_sigs_output(hh, plh, [sig, sig], [pub, pub]) + let out_ep = mk_endpoint_channel_inline(receiver_b20, 0, 1) + + let dvn_in = mk_dvn_ref_input(1, 2, [pub]) + let tx = transaction.Transaction { ..transaction.placeholder, reference_inputs: [dvn_in], outputs: [out_att, out_sigs, out_ep] } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 2, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.Verify { packet_header: ph, payload_hash: plh, confirmations: 0 } + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == False +} + +test uln302_verify_rejects_with_non_member_signer() { + let local_eid = types.Eid(200) + let receiver_b20 = bytearray.new(20) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(receiver_b20), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid = types.Guid(bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + let msg = bytearray.from_utf8("hi") + let plh = codec.payload_hash(guid, msg) + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(1)]), + ) + + let out_att = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let pub_non_member = bytearray.from_hex("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + let sig = bytearray.new(64) + + let out_sigs = mk_sigs_output(hh, plh, [sig], [pub_non_member]) + let out_ep = mk_endpoint_channel_inline(receiver_b20, 0, 1) + + let dvn_member = bytearray.from_hex("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + let dvn_in = mk_dvn_ref_input(1, 1, [dvn_member]) + let tx = transaction.Transaction { ..transaction.placeholder, reference_inputs: [dvn_in], outputs: [out_att, out_sigs, out_ep] } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = + uln302_receive.ReceiveRedeemer.Verify { packet_header: ph, payload_hash: plh, confirmations: 0 } + + expect uln302_receive.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == False +} + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == False +} +test uln302_verify_accepts_with_real_ed25519_signature_quorum_1() { + let local_eid = types.Eid(200) + let receiver_b20 = bytearray.new(20) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(1), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local_eid, + receiver_b20: types.Address20(receiver_b20), + } + let ph = codec.encode_header_v1(header) + let hh = codec.header_hash_v1(ph) + + let guid = types.Guid(bytearray.from_hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + let msg = bytearray.from_utf8("hi") + let plh = codec.payload_hash(guid, msg) + + let att: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(1)]), + ) + let out_att = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att), + reference_script: None, + } + + let pub = bytearray.from_hex("5172F998FC21A893DC69ADBDE3C8A80EAFCF8C40DB0C28845BC7D7C0739DBD7E") + let sig = bytearray.from_hex("DD309BFBC91098A3F410BA69E3DBA06CE448A4AB9C8A8720697D117E1BBB81DA5F1A30AD1F9F0A84DD89E5B57504D5264468DF41E138242611BFDEC61CA7330A") + + let out_sigs = mk_sigs_output(hh, plh, [sig], [pub]) + let out_ep = mk_endpoint_channel_inline(receiver_b20, 0, 1) + + let dvn_in = mk_dvn_ref_input(1, 1, [pub]) + let tx = transaction.Transaction { ..transaction.placeholder, reference_inputs: [dvn_in], outputs: [out_att, out_sigs, out_ep] } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.Verify { packet_header: ph, payload_hash: plh, confirmations: 0 } + + expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == True +} diff --git a/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak new file mode 100644 index 0000000..5f0dc02 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak @@ -0,0 +1,57 @@ +use cardano/transaction + +use layerzero/types +use layerzero/codec +use layerzero/validators/uln302_receive + +test uln302_commitverification_accepts_when_header_dst_matches_local_eid() { + let local = types.Eid(200) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(10), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: local, + receiver_b20: types.Address20(bytearray.new(20)), + } + let ph = codec.encode_header_v1(header) + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.CommitVerification { + packet_header: ph, + payload_hash: bytearray.new(32), + } + let tx = transaction.placeholder + + expect uln302_receive.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} + +test uln302_commitverification_rejects_when_header_dst_mismatch() { + let local = types.Eid(201) + let header = types.PacketHeaderV1 { + nonce: types.Nonce(11), + src_eid: types.Eid(100), + sender: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(200), + receiver_b20: types.Address20(bytearray.new(20)), + } + let ph = codec.encode_header_v1(header) + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local, dvn_vid: 1, required_quorum: 1, required_confirmations: 0 }) + let redeemer = uln302_receive.ReceiveRedeemer.CommitVerification { + packet_header: ph, + payload_hash: bytearray.new(32), + } + let tx = transaction.placeholder + + expect uln302_receive.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/tests/uln302_send.tests.ak b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak new file mode 100644 index 0000000..a7045a3 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak @@ -0,0 +1,130 @@ +use aiken/builtin +use cardano/transaction +use cardano/address +use cardano/assets + +use layerzero/types +use layerzero/validators/uln302_send + +fn mk_fee_out(vkh: ByteArray) -> transaction.Output { + transaction.Output { + address: address.from_verification_key(vkh), + value: assets.from_lovelace(1), + datum: transaction.Datum.NoDatum, + reference_script: None, + } +} + +test uln302_send_requires_fee_outputs_to_executor_and_dvn() { + let ex_vkh = #"01" + let dvn_vkh = #"02" + let datum = Some(uln302_send.SendDatum { + oapp: types.Address32(#"00"), + dst_eid: types.Eid(200), + executor_cfg: types.ExecutorConfig { placeholder: 0 }, + uln_cfg: types.UlnConfig { confirmations: 0, quorum: 1 }, + executor_fee_vkh: ex_vkh, + dvn_fee_vkh: dvn_vkh, + }) + let hdr = bytearray.new(81) + let outbound = builtin.constr_data(0, builtin.list_data([builtin.b_data(hdr), builtin.b_data(#"aa"), builtin.i_data(0)])) + let out_ob = transaction.Output { + address: address.from_verification_key(#"ff"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(outbound), + reference_script: None, + } + let tx = transaction.Transaction { + ..transaction.placeholder, + outputs: [out_ob, mk_fee_out(ex_vkh), mk_fee_out(dvn_vkh)], + } + let redeemer = uln302_send.SendRedeemer.Send { options: [] } + expect uln302_send.spend(datum, redeemer, transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, tx) == True +} +use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/address +use cardano/assets +use cardano/transaction + +use layerzero/types +use layerzero/validators/uln302_send + +fn mk_outbound_inline(header: ByteArray, payload: ByteArray, options_len: Int) -> transaction.Output { + let datum: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(header), builtin.b_data(payload), builtin.i_data(options_len)]), + ) + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(datum), + reference_script: None, + } +} + +test uln302_send_accepts_when_outbound_present_and_options_len_matches() { + let header = bytearray.from_int_big_endian(1, 1) + |> bytearray.concat(bytearray.from_int_big_endian(0, 80)) + let payload = bytearray.from_utf8("msg") + let out = mk_outbound_inline(header, payload, 2) + + let tx = transaction.Transaction { ..transaction.placeholder, outputs: [out] } + + let datum = Some(uln302_send.SendDatum { + oapp: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(200), + executor_cfg: types.ExecutorConfig { placeholder: 0 }, + uln_cfg: types.UlnConfig { confirmations: 0, quorum: 0 }, + executor_fee_vkh: #"aa", + dvn_fee_vkh: #"bb", + }) + + let opts: List = [ + types.WorkerOption { key: #"01", value: #"aa" }, + types.WorkerOption { key: #"02", value: #"bb" }, + ] + + let redeemer = uln302_send.SendRedeemer.Send { options: opts } + + expect uln302_send.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == True +} + +test uln302_send_rejects_when_header_wrong_length_or_options_mismatch() { + let bad_header = bytearray.from_int_big_endian(1, 1) + |> bytearray.concat(bytearray.from_int_big_endian(0, 79)) + let payload = bytearray.from_utf8("msg") + let out = mk_outbound_inline(bad_header, payload, 1) + + let tx = transaction.Transaction { ..transaction.placeholder, outputs: [out] } + + let datum = Some(uln302_send.SendDatum { + oapp: types.Address32(bytearray.new(32)), + dst_eid: types.Eid(200), + executor_cfg: types.ExecutorConfig { placeholder: 0 }, + uln_cfg: types.UlnConfig { confirmations: 0, quorum: 0 }, + executor_fee_vkh: #"aa", + dvn_fee_vkh: #"bb", + }) + + let opts: List = [ + types.WorkerOption { key: #"01", value: #"aa" }, + types.WorkerOption { key: #"02", value: #"bb" }, + ] + + let redeemer = uln302_send.SendRedeemer.Send { options: opts } + + expect uln302_send.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak new file mode 100644 index 0000000..3b0d22b --- /dev/null +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -0,0 +1,186 @@ +use aiken/collection/list +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction} +use aiken/crypto.{VerificationKeyHash} + +use layerzero/types +use layerzero/dvn_helpers + +pub type DvnDatum { + DvnDatum { + vid: Int, + quorum: Int, + signers: List, + admin: VerificationKeyHash, + } +} + +pub type ExecuteParam { + ExecuteParam { + vid: Int, + header_hash: ByteArray, + payload_hash: ByteArray, + signer_count: Int, + } +} + +pub type AssignJobParam { + AssignJobParam { + dst_eid: types.Eid, + confirmations: Int, + sender: types.Address32, + } +} + +pub type DvnRedeemer { + SetSignerSet { + signers: List, + } + SetQuorum { + quorum: Int, + } + AssignJob { + param: AssignJobParam, + options: ByteArray, + } + Execute { + params: List, + } +} + +fn parse_attestation(d: Data) -> Option<(ByteArray, ByteArray, Int)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let f0 = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f1 = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f2 = builtin.head_list(fs2) + let hh = builtin.un_b_data(f0) + let ph = builtin.un_b_data(f1) + let sc = builtin.un_i_data(f2) + Some((hh, ph, sc)) + } else { + None + } +} + +fn parse_sigs(d: Data) -> Option<(ByteArray, ByteArray, List, List)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let f0 = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f1 = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f2 = builtin.head_list(fs2) + let fs3 = builtin.tail_list(fs2) + let f3 = builtin.head_list(fs3) + let hh = builtin.un_b_data(f0) + let ph = builtin.un_b_data(f1) + let sigs_data = builtin.un_list_data(f2) + let pubs_data = builtin.un_list_data(f3) + let sigs = list.map(sigs_data, fn(x) { builtin.un_b_data(x) }) + let pubs = list.map(pubs_data, fn(x) { + let prx = builtin.un_constr_data(x) + let b = builtin.un_b_data(builtin.head_list(prx.2nd)) + types.Address32(b) + }) + Some((hh, ph, sigs, pubs)) + } else { + None + } +} + +fn find_attestation(outputs: List, hh: ByteArray, ph: ByteArray) -> Bool { + outputs + |> list.any(fn(o) { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_attestation(d) is { + Some((ah, ap, sc)) -> ah == hh && ap == ph && sc >= 0 + None -> False + } + } + _ -> False + } + }) +} + +fn find_sigs(outputs: List, hh: ByteArray, ph: ByteArray) -> Option<(List, List)> { + list.foldr( + outputs, + None, + fn(o, acc) { + when acc is { + Some(_) -> acc + None -> { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_sigs(d) is { + Some((ah, ap, sigs, pubs)) -> if ah == hh && ap == ph { Some((sigs, pubs)) } else { None } + None -> None + } + } + _ -> None + } + } + } + }, + ) +} + +validator dvn { + spend( + datum: Option, + redeemer: DvnRedeemer, + _0: OutputReference, + tx: Transaction, + ) { + when redeemer is { + SetSignerSet { signers: _ } -> { + when datum is { + Some(DvnDatum { admin: a, .. }) -> list.any(tx.extra_signatories, fn(v) { v == a }) + None -> False + } + } + SetQuorum { quorum: _ } -> { + when datum is { + Some(DvnDatum { admin: a, .. }) -> list.any(tx.extra_signatories, fn(v) { v == a }) + None -> False + } + } + AssignJob { param: _, options: _ } -> True + Execute { params: ps } -> { + when datum is { + Some(DvnDatum { vid: _, quorum: q, signers: ss, .. }) -> { + let ok = ps + |> list.any(fn(p) { + let ExecuteParam { vid: _, header_hash: hdrh, payload_hash: plh, signer_count: _ } = p + let has_att = find_attestation(tx.outputs, hdrh, plh) + when find_sigs(tx.outputs, hdrh, plh) is { + Some((sigs, pubs)) -> { + let msg = dvn_helpers.attestation_message(hdrh, plh) + let within_set = dvn_helpers.signers_within_set(pubs, ss) + let unique = dvn_helpers.unique_signers(pubs) + let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q) + has_att && within_set && unique && ok_ms + } + None -> False + } + }) + ok + } + None -> False + } + } + } + } + + else(_) { + fail + } +} diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak new file mode 100644 index 0000000..b30f4b1 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -0,0 +1,82 @@ +use cardano/transaction.{OutputReference, Transaction} + +use layerzero/types +use layerzero/endpoint_helpers + +pub type EndpointDatum { + Global { + local_eid: types.Eid + } + Channel { + origin: types.Origin, + receiver: types.Address20, + lazy_inbound_nonce: types.Nonce, + executed: Bool, + } +} + +pub type EndpointRedeemer { + SetConfig { + oapp: types.Address32, + dst_eid: types.Eid, + config_type: Int, + config: ByteArray, + } + Verify { + origin: types.Origin, + receiver: types.Address20, + payload_hash: ByteArray, + } + LzReceive { + origin: types.Origin, + receiver: types.Address20, + guid: types.Guid, + message: ByteArray, + extra_data: ByteArray, + } +} + +validator endpoint { + spend( + datum: Option, + redeemer: EndpointRedeemer, + _0: OutputReference, + _1: Transaction, + ) { + when redeemer is { + SetConfig { oapp: _, dst_eid: _, config_type: _, config: _ } -> True + Verify { origin: og, receiver: rcvr, payload_hash: _ } -> { + when datum is { + Some(Channel { + origin: _ch_og, + receiver: ch_rcvr, + lazy_inbound_nonce: ch_nonce, + executed: _, + }) -> { + let types.Origin { src_eid: _, sender: _, nonce: in_nonce } = og + ch_rcvr == rcvr && endpoint_helpers.nonce_next_ok(ch_nonce, in_nonce) + } + _ -> False + } + } + LzReceive { origin: og, receiver: rcvr, guid: _, message: _, extra_data: _ } -> { + when datum is { + Some(Channel { + origin: _ch_og, + receiver: ch_rcvr, + lazy_inbound_nonce: ch_nonce, + executed: True, + }) -> { + let types.Origin { src_eid: _, sender: _, nonce: in_nonce } = og + ch_rcvr == rcvr && endpoint_helpers.nonce_next_ok(ch_nonce, in_nonce) + } + _ -> False + } + } + } + } + + else(_) { + fail + } +} diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak new file mode 100644 index 0000000..4267f47 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -0,0 +1,180 @@ +use aiken/collection/list +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction, Input} +use cardano/assets +use aiken/crypto.{VerificationKeyHash} +use cardano/address.{Credential} + +use layerzero/types + +pub type ExecutorDatum { + ExecutorDatum { + local_eid: types.Eid, + executor_fee_vkh: VerificationKeyHash + } +} + +pub type NativeDropParam { + NativeDropParam { + receiver: types.Address32, + amount: Int, + } +} + +pub type LzReceiveParam { + LzReceiveParam { + origin: types.Origin, + receiver: types.Address32, + guid: types.Guid, + message: ByteArray, + extra_data: ByteArray, + gas: Int, + value: Int, + } +} + +pub type ExecutorRedeemer { + CommitAndExecute { + receive_lib: ByteArray, + lz_receive_param: LzReceiveParam, + native_drops: List, + } +} + +fn parse_endpoint_channel(d: Data) -> Option<(types.Origin, types.Address20, types.Nonce, Bool)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let tag = pr.1st + if tag == 1 { + let fs = pr.2nd + let f_origin = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f_receiver = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f_nonce = builtin.head_list(fs2) + let fs3 = builtin.tail_list(fs2) + let f_executed = builtin.head_list(fs3) + + let og_pr = builtin.un_constr_data(f_origin) + let og_fs = og_pr.2nd + let f_eid = builtin.head_list(og_fs) + let og_fs1 = builtin.tail_list(og_fs) + let f_sender = builtin.head_list(og_fs1) + let og_fs2 = builtin.tail_list(og_fs1) + let f_og_nonce = builtin.head_list(og_fs2) + + let eid_pr = builtin.un_constr_data(f_eid) + let eid_i = builtin.un_i_data(builtin.head_list(eid_pr.2nd)) + + let snd_pr = builtin.un_constr_data(f_sender) + let snd_b = builtin.un_b_data(builtin.head_list(snd_pr.2nd)) + + let on_pr = builtin.un_constr_data(f_og_nonce) + let on_i = builtin.un_i_data(builtin.head_list(on_pr.2nd)) + + let rc_pr = builtin.un_constr_data(f_receiver) + let rc_b = builtin.un_b_data(builtin.head_list(rc_pr.2nd)) + + let ln_pr = builtin.un_constr_data(f_nonce) + let ln_i = builtin.un_i_data(builtin.head_list(ln_pr.2nd)) + + let ex_pr = builtin.un_constr_data(f_executed) + let executed = ex_pr.1st == 1 + + Some(( + types.Origin { src_eid: types.Eid(eid_i), sender: types.Address32(snd_b), nonce: types.Nonce(on_i) }, + types.Address20(rc_b), + types.Nonce(ln_i), + executed + )) + } else { + None + } + } else { + None + } +} +fn has_endpoint_prev_input(prev_nonce: types.Nonce, executed_flag: Bool, inputs: List) -> Bool { + inputs + |> list.any(fn(i) { + when i.output.datum is { + Datum.InlineDatum(d) -> { + when parse_endpoint_channel(d) is { + Some((_og, _rc, lazy_nonce, executed)) -> lazy_nonce == prev_nonce && executed == executed_flag + None -> False + } + } + _ -> False + } + }) +} + + + + + +fn has_endpoint_curr_output(curr_nonce: types.Nonce, executed_flag: Bool, outputs: List) -> Bool { + outputs + |> list.any(fn(o) { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_endpoint_channel(d) is { + Some((_og, _rc, lazy_nonce, executed)) -> lazy_nonce == curr_nonce && executed == executed_flag + None -> False + } + } + _ -> False + } + }) +} +fn sum_native_drops(ds: List) -> Int { + list.foldr(ds, 0, fn(d, acc) { d.amount + acc }) +} + +fn total_outputs_lovelace(tx: Transaction) -> Int { + list.foldr( + tx.outputs, + 0, + fn(o, acc) { assets.lovelace_of(o.value) + acc }, + ) +} + +fn has_executor_fee_output(outputs: List, vkh: VerificationKeyHash) -> Bool { + outputs + |> list.any(fn(o) { + when o.address.payment_credential is { + Credential.VerificationKey(k) -> k == vkh && assets.lovelace_of(o.value) > 0 + _ -> False + } + }) +} + +validator executor { + spend( + datum: Option, + redeemer: ExecutorRedeemer, + _0: OutputReference, + tx: Transaction, + ) { + let CommitAndExecute { receive_lib: _, lz_receive_param: lp, native_drops: drops } = redeemer + when datum is { + Some(ExecutorDatum { local_eid: _, executor_fee_vkh: fee_vkh }) -> { + let need = sum_native_drops(drops) + let have = total_outputs_lovelace(tx) + let types.Origin { nonce: og_nonce, .. } = lp.origin + let types.Nonce(n_i) = og_nonce + let prev = types.Nonce(n_i - 1) + let ok_prev_in = has_endpoint_prev_input(prev, True, tx.inputs) + let ok_curr_out = has_endpoint_curr_output(og_nonce, False, tx.outputs) + let ok_fee = has_executor_fee_output(tx.outputs, fee_vkh) + ok_prev_in && ok_curr_out && ok_fee && have >= need && need >= 0 + } + None -> False + } + } + + else(_) { + fail + } +} diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak new file mode 100644 index 0000000..e02b129 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -0,0 +1,292 @@ +use aiken/collection/list +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction} + +use layerzero/types +use layerzero/codec +use layerzero/dvn_helpers +use aiken/primitive/bytearray +use cardano/transaction.{Input} + +pub type ReceiveDatum { + ReceiveDatum { + local_eid: types.Eid, + dvn_vid: Int, + required_quorum: Int, + required_confirmations: Int, + } +} + +pub type ReceiveRedeemer { + SetConfigUln { + oapp: types.Address32, + src_eid: types.Eid, + cfg: types.UlnConfig, + } + CommitVerification { + packet_header: ByteArray, + payload_hash: ByteArray, + } + Verify { + packet_header: ByteArray, + payload_hash: ByteArray, + confirmations: Int, + } +} + +fn parse_attestation(d: Data) -> Option<(ByteArray, ByteArray, Int)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let f0 = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f1 = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f2 = builtin.head_list(fs2) + let hh = builtin.un_b_data(f0) + let ph = builtin.un_b_data(f1) + let sc = builtin.un_i_data(f2) + Some((hh, ph, sc)) + } else { + None + } +} + +fn has_matching_attestation(outputs: List, hh: ByteArray, ph: ByteArray) -> Bool { + outputs + |> list.any(fn(o) { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_attestation(d) is { + Some((ah, ap, _)) -> ah == hh && ap == ph + None -> False + } + } + _ -> False + } + }) +} + +fn parse_endpoint_channel(d: Data) -> Option<(types.Address20, types.Nonce, Bool)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let tag = pr.1st + if tag == 1 { + let fs = pr.2nd + let fs1 = builtin.tail_list(fs) + let f_receiver = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f_nonce = builtin.head_list(fs2) + let fs3 = builtin.tail_list(fs2) + let f_executed = builtin.head_list(fs3) + let r_pr = builtin.un_constr_data(f_receiver) + let r_b = builtin.un_b_data(builtin.head_list(r_pr.2nd)) + let n_pr = builtin.un_constr_data(f_nonce) + let n_i = builtin.un_i_data(builtin.head_list(n_pr.2nd)) + let e_pr = builtin.un_constr_data(f_executed) + let executed = e_pr.1st == 1 + Some((types.Address20(r_b), types.Nonce(n_i), executed)) + } else { + None + } + } else { + None + } +} + +fn has_matching_endpoint_verify_output( + outputs: List, + receiver: types.Address20, + prev_nonce: types.Nonce, +) -> Bool { + outputs + |> list.any(fn(o) { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_endpoint_channel(d) is { + Some((rcvr, lazy_nonce, executed)) -> rcvr == receiver && executed && lazy_nonce == prev_nonce + None -> False + } + } + _ -> False + } + }) +} + +fn find_sigs(outputs: List, hh: ByteArray, ph: ByteArray) -> Option<(List, List)> { + list.foldr( + outputs, + None, + fn(o, acc) { + when acc is { + Some(_) -> acc + None -> { + when o.datum is { + Datum.InlineDatum(d) -> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let tag = pr.1st + if tag == 1 { + let fs = pr.2nd + let f0 = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f1 = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f2 = builtin.head_list(fs2) + let fs3 = builtin.tail_list(fs2) + let f3 = builtin.head_list(fs3) + let ah = builtin.un_b_data(f0) + let ap = builtin.un_b_data(f1) + let sigs = list.map(builtin.un_list_data(f2), fn(x) { builtin.un_b_data(x) }) + let pubs = list.map(builtin.un_list_data(f3), fn(x) { + let prx = builtin.un_constr_data(x) + let b = builtin.un_b_data(builtin.head_list(prx.2nd)) + types.Address32(b) + }) + if ah == hh && ap == ph { Some((sigs, pubs)) } else { None } + } else { + None + } + } else { + None + } + } + _ -> None + } + } + } + }, + ) +} + + +fn parse_dvn_signers(d: Data) -> Option> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let fs1 = builtin.tail_list(fs) + let fs2 = builtin.tail_list(fs1) + let f_signers = builtin.head_list(fs2) + let signers_data = builtin.un_list_data(f_signers) + let signers = list.map(signers_data, fn(x) { + let prx = builtin.un_constr_data(x) + let b = builtin.un_b_data(builtin.head_list(prx.2nd)) + types.Address32(b) + }) + Some(signers) + } else { + None + } +} + +fn find_dvn_signers(inputs: List) -> Option> { + list.foldr( + inputs, + None, + fn(i, acc) { + when acc is { + Some(_) -> acc + None -> { + when i.output.datum is { + Datum.InlineDatum(d) -> parse_dvn_signers(d) + _ -> None + } + } + } + }, + ) +} +fn parse_dvn_quorum(d: Data) -> Option { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let fs1 = builtin.tail_list(fs) + let f_quorum = builtin.head_list(fs1) + let q = builtin.un_i_data(f_quorum) + Some(q) + } else { + None + } +} + +fn find_dvn_quorum(inputs: List) -> Option { + list.foldr( + inputs, + None, + fn(i, acc) { + when acc is { + Some(_) -> acc + None -> { + when i.output.datum is { + Datum.InlineDatum(d) -> parse_dvn_quorum(d) + _ -> None + } + } + } + }, + ) +} + + +validator uln302_receive { + spend( + datum: Option, + redeemer: ReceiveRedeemer, + _0: OutputReference, + tx: Transaction, + ) { + when redeemer is { + SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> True + CommitVerification { packet_header: ph, payload_hash: _ } -> { + when datum is { + Some(ReceiveDatum { local_eid: le, dvn_vid: _, required_quorum: _, required_confirmations: _ }) -> codec.assert_header_v1(ph, le) + None -> False + } + } + Verify { packet_header: ph, payload_hash: plh, confirmations: confs } -> { + when datum is { + Some(ReceiveDatum { local_eid: le, dvn_vid: _vid, required_quorum: _dq, required_confirmations: req_c }) -> { + let ok_hdr = codec.assert_header_v1(ph, le) + let hh = codec.header_hash_v1(ph) + let ok_att = has_matching_attestation(tx.outputs, hh, plh) + let types.Nonce(in_i) = codec.header_nonce(ph) + let rcvr = codec.header_receiver_b20(ph) + let prev_nonce = types.Nonce(in_i - 1) + let ok_ep = has_matching_endpoint_verify_output(tx.outputs, rcvr, prev_nonce) + let ok_conf = confs >= req_c + when find_sigs(tx.outputs, hh, plh) is { + Some((sigs, pubs)) -> { + let msg = bytearray.concat(hh, plh) + let unique = dvn_helpers.unique_signers(pubs) + when find_dvn_signers(tx.reference_inputs) is { + Some(set) -> { + when find_dvn_quorum(tx.reference_inputs) is { + Some(q) -> { + let within = dvn_helpers.signers_within_set(pubs, set) + let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q) + ok_hdr && ok_att && ok_ep && ok_conf && unique && within && ok_ms + } + None -> False + } + } + None -> False + } + } + None -> False + } + } + None -> False + } + } + } + } + + else(_) { + fail + } +} diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak new file mode 100644 index 0000000..094d2ec --- /dev/null +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -0,0 +1,110 @@ +use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/transaction.{Datum, Output, OutputReference, Transaction} +use cardano/assets +use cardano/address.{Credential} +use aiken/crypto.{VerificationKeyHash} + +use layerzero/types + +pub type SendDatum { + SendDatum { + oapp: types.Address32, + dst_eid: types.Eid, + executor_cfg: types.ExecutorConfig, + uln_cfg: types.UlnConfig, + executor_fee_vkh: VerificationKeyHash, + dvn_fee_vkh: VerificationKeyHash, + } +} + +pub type SendRedeemer { + SetConfigExec { + oapp: types.Address32, + dst_eid: types.Eid, + cfg: types.ExecutorConfig, + } + SetConfigUln { + oapp: types.Address32, + dst_eid: types.Eid, + cfg: types.UlnConfig, + } + Send { + options: List + } +} + +fn has_fee_outputs(outputs: List, ex_vkh: VerificationKeyHash, dvn_vkh: VerificationKeyHash) -> Bool { + let pays_to = fn(o: Output, who: VerificationKeyHash) -> Bool { + when o.address.payment_credential is { + Credential.VerificationKey(vkh) -> vkh == who && assets.lovelace_of(o.value) > 0 + _ -> False + } + } + let has_ex = list.any(outputs, fn(o) { pays_to(o, ex_vkh) }) + let has_dvn = list.any(outputs, fn(o) { pays_to(o, dvn_vkh) }) + has_ex && has_dvn +} + + +fn parse_outbound(d: Data) -> Option<(ByteArray, ByteArray, Int)> { + let is_constr = builtin.choose_data(d, True, False, False, False, False) + if is_constr { + let pr = builtin.un_constr_data(d) + let fs = pr.2nd + let f0 = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let f1 = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f2 = builtin.head_list(fs2) + let header = builtin.un_b_data(f0) + let payload = builtin.un_b_data(f1) + let opts_len = builtin.un_i_data(f2) + Some((header, payload, opts_len)) + } else { + None + } +} + +fn has_valid_outbound(outputs: List, expected_opts_len: Int) -> Bool { + outputs + |> list.any(fn(o) { + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_outbound(d) is { + Some((hdr, _pl, ol)) -> bytearray.length(hdr) == 81 && ol == expected_opts_len + None -> False + } + } + _ -> False + } + }) +} + +validator uln302_send { + spend( + datum: Option, + redeemer: SendRedeemer, + _0: OutputReference, + tx: Transaction, + ) { + when redeemer is { + SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> True + SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> True + Send { options: opts } -> { + when datum is { + Some(SendDatum { oapp: _, dst_eid: _, executor_cfg: _, uln_cfg: _, executor_fee_vkh: ex, dvn_fee_vkh: dv }) -> { + has_valid_outbound(tx.outputs, list.length(opts)) && + has_fee_outputs(tx.outputs, ex, dv) + } + None -> False + } + } + } + } + + else(_) { + fail + } +}