From 065eef26b90ff8e03ff7016d421e2ed968014a17 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:27 +0000 Subject: [PATCH 01/72] cardano: add contracts folder with README scaffold --- src/cardano/contracts/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/cardano/contracts/README.md diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md new file mode 100644 index 0000000..eeaa752 --- /dev/null +++ b/src/cardano/contracts/README.md @@ -0,0 +1,19 @@ +# Cardano LayerZero Contracts (Aiken) + +This directory contains the Cardano eUTXO adaptations of LayerZero V2 core contracts implemented in Aiken. + +Structure: +- aiken/ Aiken project root + - aiken.toml Project config + - validators/ On-chain validators (scripts) + - lib/ Shared libraries (types, codecs, utils) + - tests/ Aiken tests (to be added) + +Target modules: +- Endpoint: endpoint state, verification, and execution state machine +- ULN302 Send/Receive: message libraries and config handling +- Executor: commit-and-execute flow bridging verification to endpoint execution +- DVN: quorum-based verification and job assignment +- Shared: packet/header codecs, types, and constants + +This is an initial scaffold to establish the project layout and shared types/codec. Subsequent commits will add full validator logic for Endpoint, ULN302, Executor, and DVN. From f49bca73b57e0cb5a45b40eea600df1a106c97e5 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 02/72] cardano: initialize Aiken project config (aiken.toml) --- src/cardano/contracts/aiken/aiken.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/cardano/contracts/aiken/aiken.toml diff --git a/src/cardano/contracts/aiken/aiken.toml b/src/cardano/contracts/aiken/aiken.toml new file mode 100644 index 0000000..dfd5cd5 --- /dev/null +++ b/src/cardano/contracts/aiken/aiken.toml @@ -0,0 +1,19 @@ +name = "lzeroanalytics/layerzero-cardano" +version = "0.1.0" +license = "MIT" +description = "LayerZero V2 core contracts adapted for Cardano in Aiken (Endpoint, ULN302, Executor, DVN)." + +[repository] +user = "LZeroAnalytics" +project = "layerzero-package" + +[dependencies] + +[build] +entrypoints = [ + "validators/endpoint.ak", + "validators/uln302_send.ak", + "validators/uln302_receive.ak", + "validators/executor.ak", + "validators/dvn.ak", +] From 61b06a9c0bb13718e739aa6672cb7f1adc30ea61 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 03/72] cardano: add shared Types and Codec modules (packet header v1, payload hash) --- .../contracts/aiken/lib/LayerZero/Codec.ak | 39 ++++++++++++ .../contracts/aiken/lib/LayerZero/Types.ak | 61 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/cardano/contracts/aiken/lib/LayerZero/Codec.ak create mode 100644 src/cardano/contracts/aiken/lib/LayerZero/Types.ak 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..a351f92 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/LayerZero/Codec.ak @@ -0,0 +1,39 @@ +use aiken/builtin +use aiken/hash/blake2b +use aiken/bytearray + +use LayerZero/Types + +pub fn encode_address32(Address32(bytes)) -> ByteArray { + bytes +} + +pub fn encode_address20(Address20(bytes)) -> ByteArray { + bytes +} + +pub fn encode_eid(Eid(e)) -> ByteArray { + builtin.serialize_data(e) +} + +pub fn encode_nonce(Nonce(n)) -> ByteArray { + builtin.serialize_data(n) +} + +pub fn encode_header_v1(h: Types.PacketHeaderV1) -> ByteArray { + let b0 = bytearray.from_int(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(h.receiver_b20) + bytearray.concat_list([b0, b1, b2, b3, b4, b5]) +} + +pub fn payload_hash(guid: Types.Guid, message: ByteArray) -> ByteArray { + blake2b.hash(bytearray.concat(unwrap_guid(guid), message)) +} + +fn unwrap_guid(Guid(b)) -> ByteArray { + b +} 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..9cf35bf --- /dev/null +++ b/src/cardano/contracts/aiken/lib/LayerZero/Types.ak @@ -0,0 +1,61 @@ +pub type Eid { + Eid(Int) +} + +pub type Address20 { + Address20(ByteArray) +} + +pub type Address32 { + Address32(ByteArray) +} + +pub type Nonce { + Nonce(Int) +} + +pub type Guid { + Guid(ByteArray) +} + +pub type Origin { + Origin { + src_eid: Eid, + sender: Address32, + nonce: Nonce, + } +} + +pub type WorkerOption { + WorkerOption(ByteArray) +} + +pub type VerificationState { + Verifiable + Verifying + NotVerifiable +} + +pub type ExecutorConfig { + ExecutorConfig { + gas_limit: Int, + gas_for_fee_cap: Int, + } +} + +pub type UlnConfig { + UlnConfig { + quorum: Int, + confirmations: Int, + } +} + +pub type PacketHeaderV1 { + PacketHeaderV1 { + nonce: Nonce, + src_eid: Eid, + sender: Address32, + dst_eid: Eid, + receiver_b20: Address20, + } +} From 27cf9dbe5b06b85912c92e296327ed3f5940ed62 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 04/72] cardano: scaffold Endpoint validator and datum/redeemer types --- .../contracts/aiken/validators/endpoint.ak | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/cardano/contracts/aiken/validators/endpoint.ak diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak new file mode 100644 index 0000000..006485c --- /dev/null +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -0,0 +1,50 @@ +use aiken/builtin +use aiken/list + +use LayerZero/Types +use LayerZero/Codec + +pub type EndpointDatum { + EndpointDatum { + local_eid: Types.Eid + } +} + +pub type ChannelDatum { + ChannelDatum { + 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, + } +} + +pub fn validator(d: EndpointDatum, r: EndpointRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("endpoint:dispatch") + case r { + SetConfig { .. } -> true + Verify { .. } -> true + LzReceive { .. } -> true + } +} From 245d88eed74e6db6e01ac13825b0b5be876e9930 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 05/72] cardano: scaffold ULN302 send validator and config types --- .../contracts/aiken/validators/uln302_send.ak | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/cardano/contracts/aiken/validators/uln302_send.ak 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..9333f7d --- /dev/null +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -0,0 +1,37 @@ +use aiken/builtin + +use LayerZero/Types + +pub type SendDatum { + SendDatum { + oapp: Types.Address32, + dst_eid: Types.Eid, + executor_cfg: Types.ExecutorConfig, + uln_cfg: Types.UlnConfig, + } +} + +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(Types.WorkerOption), + } +} + +pub fn validator(d: SendDatum, r: SendRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("uln302_send:dispatch") + case r { + SetConfigExec { .. } -> true + SetConfigUln { .. } -> true + Send { .. } -> true + } +} From e2342b98abc6320361c32c61c8f3c9d5e59b7a8d Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 06/72] cardano: scaffold ULN302 receive validator with commit/verify entrypoints --- .../aiken/validators/uln302_receive.ak | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/cardano/contracts/aiken/validators/uln302_receive.ak 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..c6498b3 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -0,0 +1,36 @@ +use aiken/builtin + +use LayerZero/Types +use LayerZero/Codec + +pub type ReceiveDatum { + ReceiveDatum { + local_eid: Types.Eid + } +} + +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, + } +} + +pub fn validator(d: ReceiveDatum, r: ReceiveRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("uln302_receive:dispatch") + case r { + SetConfigUln { .. } -> true + CommitVerification { .. } -> true + Verify { .. } -> true + } +} From cd5d1f2edbaf0e37b5541537689729f63b722b3c Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 07/72] cardano: scaffold Executor validator with commitAndExecute entrypoint --- .../contracts/aiken/validators/executor.ak | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/cardano/contracts/aiken/validators/executor.ak diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak new file mode 100644 index 0000000..c0043d6 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -0,0 +1,43 @@ +use aiken/builtin + +use LayerZero/Types + +pub type ExecutorDatum { + ExecutorDatum { + local_eid: Types.Eid + } +} + +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(NativeDropParam), + } +} + +pub fn validator(d: ExecutorDatum, r: ExecutorRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("executor:commit_and_execute") + case r { + CommitAndExecute { .. } -> true + } +} From f436ed86a5de64a3dabadbff19402d8eeca43231 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 11:54:28 +0000 Subject: [PATCH 08/72] cardano: scaffold DVN validator with quorum-related redeemers --- src/cardano/contracts/aiken/validators/dvn.ak | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/cardano/contracts/aiken/validators/dvn.ak diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak new file mode 100644 index 0000000..1292cb5 --- /dev/null +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -0,0 +1,55 @@ +use aiken/builtin + +use LayerZero/Types + +pub type DvnDatum { + DvnDatum { + vid: Int, + quorum: Int, + } +} + +pub type ExecuteParam { + ExecuteParam { + vid: Int, + target: ByteArray, + call_data: ByteArray, + expiration: Int, + signatures: ByteArray, + } +} + +pub type AssignJobParam { + AssignJobParam { + dst_eid: Types.Eid, + confirmations: Int, + sender: Types.Address32, + } +} + +pub type DvnRedeemer { + SetSigner { + signer: Types.Address32, + active: Bool, + } + SetQuorum { + quorum: Int, + } + AssignJob { + param: AssignJobParam, + options: ByteArray, + } + Execute { + params: List(ExecuteParam), + } +} + +pub fn validator(d: DvnDatum, r: DvnRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("dvn:dispatch") + case r { + SetSigner { .. } -> true + SetQuorum { .. } -> true + AssignJob { .. } -> true + Execute { .. } -> true + } +} From 41665b3a85ba57ac90ef804bfdd04ba3dba75b36 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:01:18 +0000 Subject: [PATCH 09/72] cardano(aiken): update aiken.toml to current schema (plutus v3, stdlib dep) --- src/cardano/contracts/aiken/aiken.toml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/cardano/contracts/aiken/aiken.toml b/src/cardano/contracts/aiken/aiken.toml index dfd5cd5..f9ec1d2 100644 --- a/src/cardano/contracts/aiken/aiken.toml +++ b/src/cardano/contracts/aiken/aiken.toml @@ -1,19 +1,10 @@ -name = "lzeroanalytics/layerzero-cardano" +name = "LZeroAnalytics/layerzero-cardano" version = "0.1.0" -license = "MIT" +plutusVersion = "v3" +licences = ["MIT"] description = "LayerZero V2 core contracts adapted for Cardano in Aiken (Endpoint, ULN302, Executor, DVN)." -[repository] -user = "LZeroAnalytics" -project = "layerzero-package" - -[dependencies] - -[build] -entrypoints = [ - "validators/endpoint.ak", - "validators/uln302_send.ak", - "validators/uln302_receive.ak", - "validators/executor.ak", - "validators/dvn.ak", -] +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v2" +source = "github" From 2a23994fa559dd2ac75d82c1bf0bcf0044aa8807 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:02:17 +0000 Subject: [PATCH 10/72] cardano(aiken): use lowercase module paths; add lib/layerzero types & codec --- .../contracts/aiken/lib/layerzero/codec.ak | 39 ++++++++++++ .../contracts/aiken/lib/layerzero/types.ak | 61 +++++++++++++++++++ src/cardano/contracts/aiken/validators/dvn.ak | 2 +- .../contracts/aiken/validators/endpoint.ak | 4 +- .../contracts/aiken/validators/executor.ak | 2 +- .../aiken/validators/uln302_receive.ak | 4 +- .../contracts/aiken/validators/uln302_send.ak | 2 +- 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 src/cardano/contracts/aiken/lib/layerzero/codec.ak create mode 100644 src/cardano/contracts/aiken/lib/layerzero/types.ak 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..a2a77e3 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -0,0 +1,39 @@ +use aiken/builtin +use aiken/hash/blake2b +use aiken/bytearray + +use layerzero/types + +pub fn encode_address32(Address32(bytes)) -> ByteArray { + bytes +} + +pub fn encode_address20(Address20(bytes)) -> ByteArray { + bytes +} + +pub fn encode_eid(Eid(e)) -> ByteArray { + builtin.serialize_data(e) +} + +pub fn encode_nonce(Nonce(n)) -> ByteArray { + builtin.serialize_data(n) +} + +pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { + let b0 = bytearray.from_int(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(h.receiver_b20) + bytearray.concat_list([b0, b1, b2, b3, b4, b5]) +} + +pub fn payload_hash(guid: types.Guid, message: ByteArray) -> ByteArray { + blake2b.hash(bytearray.concat(unwrap_guid(guid), message)) +} + +fn unwrap_guid(Guid(b)) -> ByteArray { + b +} 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..9cf35bf --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/types.ak @@ -0,0 +1,61 @@ +pub type Eid { + Eid(Int) +} + +pub type Address20 { + Address20(ByteArray) +} + +pub type Address32 { + Address32(ByteArray) +} + +pub type Nonce { + Nonce(Int) +} + +pub type Guid { + Guid(ByteArray) +} + +pub type Origin { + Origin { + src_eid: Eid, + sender: Address32, + nonce: Nonce, + } +} + +pub type WorkerOption { + WorkerOption(ByteArray) +} + +pub type VerificationState { + Verifiable + Verifying + NotVerifiable +} + +pub type ExecutorConfig { + ExecutorConfig { + gas_limit: Int, + gas_for_fee_cap: Int, + } +} + +pub type UlnConfig { + UlnConfig { + quorum: Int, + confirmations: Int, + } +} + +pub type PacketHeaderV1 { + PacketHeaderV1 { + nonce: Nonce, + src_eid: Eid, + sender: Address32, + dst_eid: Eid, + receiver_b20: Address20, + } +} diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 1292cb5..9ef8a04 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -1,6 +1,6 @@ use aiken/builtin -use LayerZero/Types +use layerzero/types pub type DvnDatum { DvnDatum { diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 006485c..af860b6 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -1,8 +1,8 @@ use aiken/builtin use aiken/list -use LayerZero/Types -use LayerZero/Codec +use layerzero/types +use layerzero/codec pub type EndpointDatum { EndpointDatum { diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index c0043d6..52f05c9 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,6 +1,6 @@ use aiken/builtin -use LayerZero/Types +use layerzero/types pub type ExecutorDatum { ExecutorDatum { diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index c6498b3..6dcdb82 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -1,7 +1,7 @@ use aiken/builtin -use LayerZero/Types -use LayerZero/Codec +use layerzero/types +use layerzero/codec pub type ReceiveDatum { ReceiveDatum { diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 9333f7d..3ab9128 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -1,6 +1,6 @@ use aiken/builtin -use LayerZero/Types +use layerzero/types pub type SendDatum { SendDatum { From 0c32a38caad7ece3ec638c283dc5d515f5a4601e Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:03:10 +0000 Subject: [PATCH 11/72] cardano(aiken): switch to lowercase module alias 'types' and remove old capitalized lib files --- .../contracts/aiken/lib/LayerZero/Codec.ak | 39 ------------ .../contracts/aiken/lib/LayerZero/Types.ak | 61 ------------------- src/cardano/contracts/aiken/validators/dvn.ak | 6 +- .../contracts/aiken/validators/endpoint.ak | 22 +++---- .../contracts/aiken/validators/executor.ak | 10 +-- .../aiken/validators/uln302_receive.ak | 8 +-- .../contracts/aiken/validators/uln302_send.ak | 22 +++---- 7 files changed, 34 insertions(+), 134 deletions(-) delete mode 100644 src/cardano/contracts/aiken/lib/LayerZero/Codec.ak delete mode 100644 src/cardano/contracts/aiken/lib/LayerZero/Types.ak diff --git a/src/cardano/contracts/aiken/lib/LayerZero/Codec.ak b/src/cardano/contracts/aiken/lib/LayerZero/Codec.ak deleted file mode 100644 index a351f92..0000000 --- a/src/cardano/contracts/aiken/lib/LayerZero/Codec.ak +++ /dev/null @@ -1,39 +0,0 @@ -use aiken/builtin -use aiken/hash/blake2b -use aiken/bytearray - -use LayerZero/Types - -pub fn encode_address32(Address32(bytes)) -> ByteArray { - bytes -} - -pub fn encode_address20(Address20(bytes)) -> ByteArray { - bytes -} - -pub fn encode_eid(Eid(e)) -> ByteArray { - builtin.serialize_data(e) -} - -pub fn encode_nonce(Nonce(n)) -> ByteArray { - builtin.serialize_data(n) -} - -pub fn encode_header_v1(h: Types.PacketHeaderV1) -> ByteArray { - let b0 = bytearray.from_int(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(h.receiver_b20) - bytearray.concat_list([b0, b1, b2, b3, b4, b5]) -} - -pub fn payload_hash(guid: Types.Guid, message: ByteArray) -> ByteArray { - blake2b.hash(bytearray.concat(unwrap_guid(guid), message)) -} - -fn unwrap_guid(Guid(b)) -> ByteArray { - b -} diff --git a/src/cardano/contracts/aiken/lib/LayerZero/Types.ak b/src/cardano/contracts/aiken/lib/LayerZero/Types.ak deleted file mode 100644 index 9cf35bf..0000000 --- a/src/cardano/contracts/aiken/lib/LayerZero/Types.ak +++ /dev/null @@ -1,61 +0,0 @@ -pub type Eid { - Eid(Int) -} - -pub type Address20 { - Address20(ByteArray) -} - -pub type Address32 { - Address32(ByteArray) -} - -pub type Nonce { - Nonce(Int) -} - -pub type Guid { - Guid(ByteArray) -} - -pub type Origin { - Origin { - src_eid: Eid, - sender: Address32, - nonce: Nonce, - } -} - -pub type WorkerOption { - WorkerOption(ByteArray) -} - -pub type VerificationState { - Verifiable - Verifying - NotVerifiable -} - -pub type ExecutorConfig { - ExecutorConfig { - gas_limit: Int, - gas_for_fee_cap: Int, - } -} - -pub type UlnConfig { - UlnConfig { - quorum: Int, - confirmations: Int, - } -} - -pub type PacketHeaderV1 { - PacketHeaderV1 { - nonce: Nonce, - src_eid: Eid, - sender: Address32, - dst_eid: Eid, - receiver_b20: Address20, - } -} diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 9ef8a04..603660a 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -21,15 +21,15 @@ pub type ExecuteParam { pub type AssignJobParam { AssignJobParam { - dst_eid: Types.Eid, + dst_eid: types.Eid, confirmations: Int, - sender: Types.Address32, + sender: types.Address32, } } pub type DvnRedeemer { SetSigner { - signer: Types.Address32, + signer: types.Address32, active: Bool, } SetQuorum { diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index af860b6..eb0cd69 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -6,35 +6,35 @@ use layerzero/codec pub type EndpointDatum { EndpointDatum { - local_eid: Types.Eid + local_eid: types.Eid } } pub type ChannelDatum { ChannelDatum { - origin: Types.Origin, - receiver: Types.Address20, - lazy_inbound_nonce: Types.Nonce, + origin: types.Origin, + receiver: types.Address20, + lazy_inbound_nonce: types.Nonce, executed: Bool, } } pub type EndpointRedeemer { SetConfig { - oapp: Types.Address32, - dst_eid: Types.Eid, + oapp: types.Address32, + dst_eid: types.Eid, config_type: Int, config: ByteArray, } Verify { - origin: Types.Origin, - receiver: Types.Address20, + origin: types.Origin, + receiver: types.Address20, payload_hash: ByteArray, } LzReceive { - origin: Types.Origin, - receiver: Types.Address20, - guid: Types.Guid, + origin: types.Origin, + receiver: types.Address20, + guid: types.Guid, message: ByteArray, extra_data: ByteArray, } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 52f05c9..fe9c4bd 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -4,22 +4,22 @@ use layerzero/types pub type ExecutorDatum { ExecutorDatum { - local_eid: Types.Eid + local_eid: types.Eid } } pub type NativeDropParam { NativeDropParam { - receiver: Types.Address32, + receiver: types.Address32, amount: Int, } } pub type LzReceiveParam { LzReceiveParam { - origin: Types.Origin, - receiver: Types.Address32, - guid: Types.Guid, + origin: types.Origin, + receiver: types.Address32, + guid: types.Guid, message: ByteArray, extra_data: ByteArray, gas: Int, diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 6dcdb82..d5d7aec 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -5,15 +5,15 @@ use layerzero/codec pub type ReceiveDatum { ReceiveDatum { - local_eid: Types.Eid + local_eid: types.Eid } } pub type ReceiveRedeemer { SetConfigUln { - oapp: Types.Address32, - src_eid: Types.Eid, - cfg: Types.UlnConfig, + oapp: types.Address32, + src_eid: types.Eid, + cfg: types.UlnConfig, } CommitVerification { packet_header: ByteArray, diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 3ab9128..6008b4c 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -4,26 +4,26 @@ use layerzero/types pub type SendDatum { SendDatum { - oapp: Types.Address32, - dst_eid: Types.Eid, - executor_cfg: Types.ExecutorConfig, - uln_cfg: Types.UlnConfig, + oapp: types.Address32, + dst_eid: types.Eid, + executor_cfg: types.ExecutorConfig, + uln_cfg: types.UlnConfig, } } pub type SendRedeemer { SetConfigExec { - oapp: Types.Address32, - dst_eid: Types.Eid, - cfg: Types.ExecutorConfig, + oapp: types.Address32, + dst_eid: types.Eid, + cfg: types.ExecutorConfig, } SetConfigUln { - oapp: Types.Address32, - dst_eid: Types.Eid, - cfg: Types.UlnConfig, + oapp: types.Address32, + dst_eid: types.Eid, + cfg: types.UlnConfig, } Send { - options: List(Types.WorkerOption), + options: List(types.WorkerOption), } } From 94a8bc1ac5542feca60a59a4d3a4916dd38f25b0 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:04:33 +0000 Subject: [PATCH 12/72] cardano(aiken): convert dvn to validator block syntax; fix generics and module aliasing --- src/cardano/contracts/aiken/validators/dvn.ak | 18 ++++++++++-------- .../contracts/aiken/validators/endpoint.ak | 14 ++++++++------ .../contracts/aiken/validators/executor.ak | 12 +++++++----- .../aiken/validators/uln302_receive.ak | 14 ++++++++------ .../contracts/aiken/validators/uln302_send.ak | 16 +++++++++------- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 603660a..9b424a0 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -40,16 +40,18 @@ pub type DvnRedeemer { options: ByteArray, } Execute { - params: List(ExecuteParam), + params: List, } } -pub fn validator(d: DvnDatum, r: DvnRedeemer, ctx: builtin.ScriptContext) -> Bool { - builtin.trace("dvn:dispatch") - case r { - SetSigner { .. } -> true - SetQuorum { .. } -> true - AssignJob { .. } -> true - Execute { .. } -> true +validator dvn { + fn spend(d: DvnDatum, r: DvnRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("dvn:dispatch") + case r { + SetSigner { .. } -> true + SetQuorum { .. } -> true + AssignJob { .. } -> true + Execute { .. } -> true + } } } diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index eb0cd69..7ed38e4 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -40,11 +40,13 @@ pub type EndpointRedeemer { } } -pub fn validator(d: EndpointDatum, r: EndpointRedeemer, ctx: builtin.ScriptContext) -> Bool { - builtin.trace("endpoint:dispatch") - case r { - SetConfig { .. } -> true - Verify { .. } -> true - LzReceive { .. } -> true +validator endpoint { + fn spend(d: EndpointDatum, r: EndpointRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("endpoint:dispatch") + case r { + SetConfig { .. } -> true + Verify { .. } -> true + LzReceive { .. } -> true + } } } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index fe9c4bd..a63a5e3 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -31,13 +31,15 @@ pub type ExecutorRedeemer { CommitAndExecute { receive_lib: ByteArray, lz_receive_param: LzReceiveParam, - native_drops: List(NativeDropParam), + native_drops: List, } } -pub fn validator(d: ExecutorDatum, r: ExecutorRedeemer, ctx: builtin.ScriptContext) -> Bool { - builtin.trace("executor:commit_and_execute") - case r { - CommitAndExecute { .. } -> true +validator executor { + fn spend(d: ExecutorDatum, r: ExecutorRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("executor:commit_and_execute") + case r { + CommitAndExecute { .. } -> true + } } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index d5d7aec..477d90b 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -26,11 +26,13 @@ pub type ReceiveRedeemer { } } -pub fn validator(d: ReceiveDatum, r: ReceiveRedeemer, ctx: builtin.ScriptContext) -> Bool { - builtin.trace("uln302_receive:dispatch") - case r { - SetConfigUln { .. } -> true - CommitVerification { .. } -> true - Verify { .. } -> true +validator uln302_receive { + fn spend(d: ReceiveDatum, r: ReceiveRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("uln302_receive:dispatch") + case r { + SetConfigUln { .. } -> true + CommitVerification { .. } -> true + Verify { .. } -> true + } } } diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 6008b4c..46c9297 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -23,15 +23,17 @@ pub type SendRedeemer { cfg: types.UlnConfig, } Send { - options: List(types.WorkerOption), + options: List, } } -pub fn validator(d: SendDatum, r: SendRedeemer, ctx: builtin.ScriptContext) -> Bool { - builtin.trace("uln302_send:dispatch") - case r { - SetConfigExec { .. } -> true - SetConfigUln { .. } -> true - Send { .. } -> true +validator uln302_send { + fn spend(d: SendDatum, r: SendRedeemer, ctx: builtin.ScriptContext) -> Bool { + builtin.trace("uln302_send:dispatch") + case r { + SetConfigExec { .. } -> true + SetConfigUln { .. } -> true + Send { .. } -> true + } } } From c3caf07233b0864e83be60dfe16a91a839f36a00 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:06:03 +0000 Subject: [PATCH 13/72] cardano(aiken): adopt current validator syntax (spend/else, standard spend signature) and import cardano/transaction --- src/cardano/contracts/aiken/validators/dvn.ak | 14 ++++++++++++-- src/cardano/contracts/aiken/validators/endpoint.ak | 14 ++++++++++++-- src/cardano/contracts/aiken/validators/executor.ak | 14 ++++++++++++-- .../contracts/aiken/validators/uln302_receive.ak | 14 ++++++++++++-- .../contracts/aiken/validators/uln302_send.ak | 14 ++++++++++++-- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 9b424a0..87da175 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -1,4 +1,5 @@ use aiken/builtin +use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -45,13 +46,22 @@ pub type DvnRedeemer { } validator dvn { - fn spend(d: DvnDatum, r: DvnRedeemer, ctx: builtin.ScriptContext) -> Bool { + spend( + datum: Option, + redeemer: DvnRedeemer, + _: OutputReference, + _: Transaction, + ) { builtin.trace("dvn:dispatch") - case r { + case redeemer { SetSigner { .. } -> true SetQuorum { .. } -> true AssignJob { .. } -> true Execute { .. } -> true } } + + else(_) { + fail + } } diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 7ed38e4..3ef948a 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -1,5 +1,6 @@ use aiken/builtin use aiken/list +use cardano/transaction.{OutputReference, Transaction} use layerzero/types use layerzero/codec @@ -41,12 +42,21 @@ pub type EndpointRedeemer { } validator endpoint { - fn spend(d: EndpointDatum, r: EndpointRedeemer, ctx: builtin.ScriptContext) -> Bool { + spend( + datum: Option, + redeemer: EndpointRedeemer, + _: OutputReference, + _: Transaction, + ) { builtin.trace("endpoint:dispatch") - case r { + case redeemer { SetConfig { .. } -> true Verify { .. } -> true LzReceive { .. } -> true } } + + else(_) { + fail + } } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index a63a5e3..6e5f832 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,4 +1,5 @@ use aiken/builtin +use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -36,10 +37,19 @@ pub type ExecutorRedeemer { } validator executor { - fn spend(d: ExecutorDatum, r: ExecutorRedeemer, ctx: builtin.ScriptContext) -> Bool { + spend( + datum: Option, + redeemer: ExecutorRedeemer, + _: OutputReference, + _: Transaction, + ) { builtin.trace("executor:commit_and_execute") - case r { + case redeemer { CommitAndExecute { .. } -> true } } + + else(_) { + fail + } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 477d90b..3e4a6da 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -1,4 +1,5 @@ use aiken/builtin +use cardano/transaction.{OutputReference, Transaction} use layerzero/types use layerzero/codec @@ -27,12 +28,21 @@ pub type ReceiveRedeemer { } validator uln302_receive { - fn spend(d: ReceiveDatum, r: ReceiveRedeemer, ctx: builtin.ScriptContext) -> Bool { + spend( + datum: Option, + redeemer: ReceiveRedeemer, + _: OutputReference, + _: Transaction, + ) { builtin.trace("uln302_receive:dispatch") - case r { + case redeemer { SetConfigUln { .. } -> true CommitVerification { .. } -> true Verify { .. } -> true } } + + else(_) { + fail + } } diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 46c9297..44bdd59 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -1,4 +1,5 @@ use aiken/builtin +use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -28,12 +29,21 @@ pub type SendRedeemer { } validator uln302_send { - fn spend(d: SendDatum, r: SendRedeemer, ctx: builtin.ScriptContext) -> Bool { + spend( + datum: Option, + redeemer: SendRedeemer, + _: OutputReference, + _: Transaction, + ) { builtin.trace("uln302_send:dispatch") - case r { + case redeemer { SetConfigExec { .. } -> true SetConfigUln { .. } -> true Send { .. } -> true } } + + else(_) { + fail + } } From f1faf5c4a77ee1fa2f2321d57c7620d6a7c0e608 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:06:28 +0000 Subject: [PATCH 14/72] cardano(aiken): remove builtin.trace from validators to satisfy parser --- src/cardano/contracts/aiken/validators/dvn.ak | 1 - src/cardano/contracts/aiken/validators/endpoint.ak | 1 - src/cardano/contracts/aiken/validators/executor.ak | 1 - src/cardano/contracts/aiken/validators/uln302_receive.ak | 1 - src/cardano/contracts/aiken/validators/uln302_send.ak | 1 - 5 files changed, 5 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 87da175..488f0e8 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -52,7 +52,6 @@ validator dvn { _: OutputReference, _: Transaction, ) { - builtin.trace("dvn:dispatch") case redeemer { SetSigner { .. } -> true SetQuorum { .. } -> true diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 3ef948a..981c93f 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -48,7 +48,6 @@ validator endpoint { _: OutputReference, _: Transaction, ) { - builtin.trace("endpoint:dispatch") case redeemer { SetConfig { .. } -> true Verify { .. } -> true diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 6e5f832..ddba567 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -43,7 +43,6 @@ validator executor { _: OutputReference, _: Transaction, ) { - builtin.trace("executor:commit_and_execute") case redeemer { CommitAndExecute { .. } -> true } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 3e4a6da..fc6ef9b 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -34,7 +34,6 @@ validator uln302_receive { _: OutputReference, _: Transaction, ) { - builtin.trace("uln302_receive:dispatch") case redeemer { SetConfigUln { .. } -> true CommitVerification { .. } -> true diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 44bdd59..59b4ccc 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -35,7 +35,6 @@ validator uln302_send { _: OutputReference, _: Transaction, ) { - builtin.trace("uln302_send:dispatch") case redeemer { SetConfigExec { .. } -> true SetConfigUln { .. } -> true From b89c338789e2c05ca2262230b1ed37e0a25b6ae7 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:06:59 +0000 Subject: [PATCH 15/72] cardano(aiken): switch pattern matches from to for enum variants --- src/cardano/contracts/aiken/validators/dvn.ak | 8 ++++---- src/cardano/contracts/aiken/validators/endpoint.ak | 6 +++--- src/cardano/contracts/aiken/validators/executor.ak | 2 +- src/cardano/contracts/aiken/validators/uln302_receive.ak | 6 +++--- src/cardano/contracts/aiken/validators/uln302_send.ak | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 488f0e8..567f965 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -53,10 +53,10 @@ validator dvn { _: Transaction, ) { case redeemer { - SetSigner { .. } -> true - SetQuorum { .. } -> true - AssignJob { .. } -> true - Execute { .. } -> true + SetSigner(_) -> true + SetQuorum(_) -> true + AssignJob(_) -> true + Execute(_) -> true } } diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 981c93f..90b53bb 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -49,9 +49,9 @@ validator endpoint { _: Transaction, ) { case redeemer { - SetConfig { .. } -> true - Verify { .. } -> true - LzReceive { .. } -> true + SetConfig(_) -> true + Verify(_) -> true + LzReceive(_) -> true } } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index ddba567..407c9a7 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -44,7 +44,7 @@ validator executor { _: Transaction, ) { case redeemer { - CommitAndExecute { .. } -> true + CommitAndExecute(_) -> true } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index fc6ef9b..33b3617 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -35,9 +35,9 @@ validator uln302_receive { _: Transaction, ) { case redeemer { - SetConfigUln { .. } -> true - CommitVerification { .. } -> true - Verify { .. } -> true + SetConfigUln(_) -> true + CommitVerification(_) -> true + Verify(_) -> true } } diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 59b4ccc..eb8a221 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -36,9 +36,9 @@ validator uln302_send { _: Transaction, ) { case redeemer { - SetConfigExec { .. } -> true - SetConfigUln { .. } -> true - Send { .. } -> true + SetConfigExec(_) -> true + SetConfigUln(_) -> true + Send(_) -> true } } From b09684e6461b1cd0a2c9f5c116bc708e50c871a5 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:08:55 +0000 Subject: [PATCH 16/72] cardano(aiken): replace 'case' with 'when ... is' for redeemer pattern matching --- src/cardano/contracts/aiken/validators/dvn.ak | 2 +- src/cardano/contracts/aiken/validators/endpoint.ak | 2 +- src/cardano/contracts/aiken/validators/executor.ak | 2 +- src/cardano/contracts/aiken/validators/uln302_receive.ak | 2 +- src/cardano/contracts/aiken/validators/uln302_send.ak | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 567f965..81f5a84 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -52,7 +52,7 @@ validator dvn { _: OutputReference, _: Transaction, ) { - case redeemer { + when redeemer is { SetSigner(_) -> true SetQuorum(_) -> true AssignJob(_) -> true diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 90b53bb..ad4d15f 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -48,7 +48,7 @@ validator endpoint { _: OutputReference, _: Transaction, ) { - case redeemer { + when redeemer is { SetConfig(_) -> true Verify(_) -> true LzReceive(_) -> true diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 407c9a7..91ad256 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -43,7 +43,7 @@ validator executor { _: OutputReference, _: Transaction, ) { - case redeemer { + when redeemer is { CommitAndExecute(_) -> true } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 33b3617..0df7c7b 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -34,7 +34,7 @@ validator uln302_receive { _: OutputReference, _: Transaction, ) { - case redeemer { + when redeemer is { SetConfigUln(_) -> true CommitVerification(_) -> true Verify(_) -> true diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index eb8a221..b7cdaa0 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -35,7 +35,7 @@ validator uln302_send { _: OutputReference, _: Transaction, ) { - case redeemer { + when redeemer is { SetConfigExec(_) -> true SetConfigUln(_) -> true Send(_) -> true From 4eccc4bd7cd67d5b493d9a0145607122ac71b5dd Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:10:12 +0000 Subject: [PATCH 17/72] cardano(aiken): use builtin.blake2b_256; track aiken.lock --- src/cardano/contracts/aiken/aiken.lock | 16 ++++++++++++++++ .../contracts/aiken/lib/layerzero/codec.ak | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/cardano/contracts/aiken/aiken.lock diff --git a/src/cardano/contracts/aiken/aiken.lock b/src/cardano/contracts/aiken/aiken.lock new file mode 100644 index 0000000..0e44550 --- /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 = 1755950936, nanos_since_epoch = 208175049 }, "25c8d0802b8266feca04b47933382c5dee3cadb422208a5d3810d9d2df108c2e"] diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index a2a77e3..e1bfb6d 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -1,5 +1,4 @@ use aiken/builtin -use aiken/hash/blake2b use aiken/bytearray use layerzero/types @@ -31,7 +30,7 @@ pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { } pub fn payload_hash(guid: types.Guid, message: ByteArray) -> ByteArray { - blake2b.hash(bytearray.concat(unwrap_guid(guid), message)) + builtin.blake2b_256(bytearray.concat(unwrap_guid(guid), message)) } fn unwrap_guid(Guid(b)) -> ByteArray { From 96fa1fc23c27185e9cfb5dae6661b2b818b41988 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:10:54 +0000 Subject: [PATCH 18/72] cardano(aiken): name OutputReference/Transaction args uniquely (_0, _1) to satisfy parser --- src/cardano/contracts/aiken/validators/dvn.ak | 4 ++-- src/cardano/contracts/aiken/validators/endpoint.ak | 4 ++-- src/cardano/contracts/aiken/validators/executor.ak | 4 ++-- src/cardano/contracts/aiken/validators/uln302_receive.ak | 4 ++-- src/cardano/contracts/aiken/validators/uln302_send.ak | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 81f5a84..35c00a2 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -49,8 +49,8 @@ validator dvn { spend( datum: Option, redeemer: DvnRedeemer, - _: OutputReference, - _: Transaction, + _0: OutputReference, + _1: Transaction, ) { when redeemer is { SetSigner(_) -> true diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index ad4d15f..128dd4c 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -45,8 +45,8 @@ validator endpoint { spend( datum: Option, redeemer: EndpointRedeemer, - _: OutputReference, - _: Transaction, + _0: OutputReference, + _1: Transaction, ) { when redeemer is { SetConfig(_) -> true diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 91ad256..d7db0c7 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -40,8 +40,8 @@ validator executor { spend( datum: Option, redeemer: ExecutorRedeemer, - _: OutputReference, - _: Transaction, + _0: OutputReference, + _1: Transaction, ) { when redeemer is { CommitAndExecute(_) -> true diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 0df7c7b..31c1b39 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -31,8 +31,8 @@ validator uln302_receive { spend( datum: Option, redeemer: ReceiveRedeemer, - _: OutputReference, - _: Transaction, + _0: OutputReference, + _1: Transaction, ) { when redeemer is { SetConfigUln(_) -> true diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index b7cdaa0..d66a181 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -32,8 +32,8 @@ validator uln302_send { spend( datum: Option, redeemer: SendRedeemer, - _: OutputReference, - _: Transaction, + _0: OutputReference, + _1: Transaction, ) { when redeemer is { SetConfigExec(_) -> true From 83ef0050125e5f673f36874caa1ec51e7912c1f1 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:11:15 +0000 Subject: [PATCH 19/72] cardano(aiken): destructure CommitAndExecute record in executor validator --- src/cardano/contracts/aiken/validators/executor.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index d7db0c7..9272f07 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -44,7 +44,7 @@ validator executor { _1: Transaction, ) { when redeemer is { - CommitAndExecute(_) -> true + CommitAndExecute { receive_lib, lz_receive_param, native_drops } -> true } } From 9e9bda7cae8bda9d302516923a90fe80eb853a11 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:11:43 +0000 Subject: [PATCH 20/72] cardano(aiken): import bytearray from aiken/primitive/bytearray --- src/cardano/contracts/aiken/lib/layerzero/codec.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index e1bfb6d..bb2eeaa 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -1,5 +1,5 @@ use aiken/builtin -use aiken/bytearray +use aiken/primitive/bytearray use layerzero/types From 9a7540535b694cd8769fa1b46d628d550135d233 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:12:17 +0000 Subject: [PATCH 21/72] cardano(aiken): qualify type constructors in codec; use bytearray.from_int_big_endian and concat() --- src/cardano/contracts/aiken/lib/layerzero/codec.ak | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index bb2eeaa..f98ab4d 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -3,36 +3,36 @@ use aiken/primitive/bytearray use layerzero/types -pub fn encode_address32(Address32(bytes)) -> ByteArray { +pub fn encode_address32(types.Address32(bytes)) -> ByteArray { bytes } -pub fn encode_address20(Address20(bytes)) -> ByteArray { +pub fn encode_address20(types.Address20(bytes)) -> ByteArray { bytes } -pub fn encode_eid(Eid(e)) -> ByteArray { +pub fn encode_eid(types.Eid(e)) -> ByteArray { builtin.serialize_data(e) } -pub fn encode_nonce(Nonce(n)) -> ByteArray { +pub fn encode_nonce(types.Nonce(n)) -> ByteArray { builtin.serialize_data(n) } pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { - let b0 = bytearray.from_int(1) + 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(h.receiver_b20) - bytearray.concat_list([b0, b1, b2, b3, b4, b5]) + bytearray.concat([b0, b1, b2, b3, b4, b5]) } pub fn payload_hash(guid: types.Guid, message: ByteArray) -> ByteArray { builtin.blake2b_256(bytearray.concat(unwrap_guid(guid), message)) } -fn unwrap_guid(Guid(b)) -> ByteArray { +fn unwrap_guid(types.Guid(b)) -> ByteArray { b } From c67724e312531fad4a631e21fdc0cccd3e716d5c Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:12:57 +0000 Subject: [PATCH 22/72] cardano(aiken): import constructors and pattern-match inside functions; replace concat_list with nested bytearray.concat --- .../contracts/aiken/lib/layerzero/codec.ak | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index f98ab4d..0cd1770 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -2,21 +2,30 @@ use aiken/builtin use aiken/primitive/bytearray use layerzero/types +use layerzero/types.{Address32, Address20, Eid, Nonce, Guid, PacketHeaderV1} -pub fn encode_address32(types.Address32(bytes)) -> ByteArray { - bytes +pub fn encode_address32(a) -> ByteArray { + when a is { + Address32(bytes) -> bytes + } } -pub fn encode_address20(types.Address20(bytes)) -> ByteArray { - bytes +pub fn encode_address20(a) -> ByteArray { + when a is { + Address20(bytes) -> bytes + } } -pub fn encode_eid(types.Eid(e)) -> ByteArray { - builtin.serialize_data(e) +pub fn encode_eid(a) -> ByteArray { + when a is { + Eid(e) -> builtin.serialize_data(e) + } } -pub fn encode_nonce(types.Nonce(n)) -> ByteArray { - builtin.serialize_data(n) +pub fn encode_nonce(a) -> ByteArray { + when a is { + Nonce(n) -> builtin.serialize_data(n) + } } pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { @@ -26,13 +35,18 @@ pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { let b3 = encode_address32(h.sender) let b4 = encode_eid(h.dst_eid) let b5 = encode_address20(h.receiver_b20) - bytearray.concat([b0, b1, b2, b3, b4, b5]) + 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)) } -fn unwrap_guid(types.Guid(b)) -> ByteArray { - b +fn unwrap_guid(g) -> ByteArray { + when g is { + Guid(b) -> b + } } From 29af9bc061c9976ae8110b29d95b3bf5a8974235 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:13:42 +0000 Subject: [PATCH 23/72] cardano(aiken): use labeled record patterns in when-branches across validators --- src/cardano/contracts/aiken/validators/dvn.ak | 8 ++++---- src/cardano/contracts/aiken/validators/endpoint.ak | 6 +++--- src/cardano/contracts/aiken/validators/executor.ak | 2 +- src/cardano/contracts/aiken/validators/uln302_receive.ak | 6 +++--- src/cardano/contracts/aiken/validators/uln302_send.ak | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 35c00a2..9afc1af 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -53,10 +53,10 @@ validator dvn { _1: Transaction, ) { when redeemer is { - SetSigner(_) -> true - SetQuorum(_) -> true - AssignJob(_) -> true - Execute(_) -> true + SetSigner { signer: _, active: _ } -> true + SetQuorum { quorum: _ } -> true + AssignJob { param: _, options: _ } -> true + Execute { params: _ } -> true } } diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 128dd4c..c66bf25 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -49,9 +49,9 @@ validator endpoint { _1: Transaction, ) { when redeemer is { - SetConfig(_) -> true - Verify(_) -> true - LzReceive(_) -> true + SetConfig { oapp: _, dst_eid: _, config_type: _, config: _ } -> true + Verify { origin: _, receiver: _, payload_hash: _ } -> true + LzReceive { origin: _, receiver: _, guid: _, message: _, extra_data: _ } -> true } } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 9272f07..f934337 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -44,7 +44,7 @@ validator executor { _1: Transaction, ) { when redeemer is { - CommitAndExecute { receive_lib, lz_receive_param, native_drops } -> true + CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: _ } -> true } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 31c1b39..55521c5 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -35,9 +35,9 @@ validator uln302_receive { _1: Transaction, ) { when redeemer is { - SetConfigUln(_) -> true - CommitVerification(_) -> true - Verify(_) -> true + SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> true + CommitVerification { packet_header: _, payload_hash: _ } -> true + Verify { packet_header: _, payload_hash: _, confirmations: _ } -> true } } diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index d66a181..3278a0b 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -36,9 +36,9 @@ validator uln302_send { _1: Transaction, ) { when redeemer is { - SetConfigExec(_) -> true - SetConfigUln(_) -> true - Send(_) -> true + SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> true + SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> true + Send { options: _ } -> true } } From 21c94ee57c0ef17d05160807e2619c952df6531d Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:13:56 +0000 Subject: [PATCH 24/72] cardano(aiken): use builtin.serialise_data for Eid/Nonce encoding --- src/cardano/contracts/aiken/lib/layerzero/codec.ak | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 0cd1770..0d89840 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -18,13 +18,13 @@ pub fn encode_address20(a) -> ByteArray { pub fn encode_eid(a) -> ByteArray { when a is { - Eid(e) -> builtin.serialize_data(e) + Eid(e) -> builtin.serialise_data(e) } } pub fn encode_nonce(a) -> ByteArray { when a is { - Nonce(n) -> builtin.serialize_data(n) + Nonce(n) -> builtin.serialise_data(n) } } From c4959d898e2777e62e1b7cf725bc0993b4ef3428 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:14:23 +0000 Subject: [PATCH 25/72] cardano(aiken): use capitalized True in validator match branches --- src/cardano/contracts/aiken/validators/dvn.ak | 8 ++++---- src/cardano/contracts/aiken/validators/endpoint.ak | 6 +++--- src/cardano/contracts/aiken/validators/executor.ak | 2 +- src/cardano/contracts/aiken/validators/uln302_receive.ak | 6 +++--- src/cardano/contracts/aiken/validators/uln302_send.ak | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 9afc1af..fb8dd78 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -53,10 +53,10 @@ validator dvn { _1: Transaction, ) { when redeemer is { - SetSigner { signer: _, active: _ } -> true - SetQuorum { quorum: _ } -> true - AssignJob { param: _, options: _ } -> true - Execute { params: _ } -> true + SetSigner { signer: _, active: _ } -> True + SetQuorum { quorum: _ } -> True + AssignJob { param: _, options: _ } -> True + Execute { params: _ } -> True } } diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index c66bf25..a4023bd 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -49,9 +49,9 @@ validator endpoint { _1: Transaction, ) { when redeemer is { - SetConfig { oapp: _, dst_eid: _, config_type: _, config: _ } -> true - Verify { origin: _, receiver: _, payload_hash: _ } -> true - LzReceive { origin: _, receiver: _, guid: _, message: _, extra_data: _ } -> true + SetConfig { oapp: _, dst_eid: _, config_type: _, config: _ } -> True + Verify { origin: _, receiver: _, payload_hash: _ } -> True + LzReceive { origin: _, receiver: _, guid: _, message: _, extra_data: _ } -> True } } diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index f934337..eb3d944 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -44,7 +44,7 @@ validator executor { _1: Transaction, ) { when redeemer is { - CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: _ } -> true + CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: _ } -> True } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 55521c5..6190737 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -35,9 +35,9 @@ validator uln302_receive { _1: Transaction, ) { when redeemer is { - SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> true - CommitVerification { packet_header: _, payload_hash: _ } -> true - Verify { packet_header: _, payload_hash: _, confirmations: _ } -> true + SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> True + CommitVerification { packet_header: _, payload_hash: _ } -> True + Verify { packet_header: _, payload_hash: _, confirmations: _ } -> True } } diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 3278a0b..0a42fbf 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -36,9 +36,9 @@ validator uln302_send { _1: Transaction, ) { when redeemer is { - SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> true - SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> true - Send { options: _ } -> true + SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> True + SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> True + Send { options: _ } -> True } } From 13e0881e6c17e2d55194f8178a0359b23d877afa Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:14:35 +0000 Subject: [PATCH 26/72] cardano(aiken): remove invalid/unused aiken/list import --- src/cardano/contracts/aiken/validators/endpoint.ak | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index a4023bd..f8eae37 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -1,5 +1,4 @@ use aiken/builtin -use aiken/list use cardano/transaction.{OutputReference, Transaction} use layerzero/types From edfff3707a98f9407a2e2dbc64137a126f759c2e Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:17:31 +0000 Subject: [PATCH 27/72] cardano(aiken): refactor codec pattern-matching to let-bindings and add basic codec tests --- .../contracts/aiken/lib/layerzero/codec.ak | 25 ++++++-------- .../contracts/aiken/tests/codec.tests.ak | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 src/cardano/contracts/aiken/tests/codec.tests.ak diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 0d89840..75afe0e 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -5,27 +5,23 @@ use layerzero/types use layerzero/types.{Address32, Address20, Eid, Nonce, Guid, PacketHeaderV1} pub fn encode_address32(a) -> ByteArray { - when a is { - Address32(bytes) -> bytes - } + let Address32(bytes) = a + bytes } pub fn encode_address20(a) -> ByteArray { - when a is { - Address20(bytes) -> bytes - } + let Address20(bytes) = a + bytes } pub fn encode_eid(a) -> ByteArray { - when a is { - Eid(e) -> builtin.serialise_data(e) - } + let Eid(e) = a + builtin.serialise_data(e) } pub fn encode_nonce(a) -> ByteArray { - when a is { - Nonce(n) -> builtin.serialise_data(n) - } + let Nonce(n) = a + builtin.serialise_data(n) } pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { @@ -46,7 +42,6 @@ pub fn payload_hash(guid: types.Guid, message: ByteArray) -> ByteArray { } fn unwrap_guid(g) -> ByteArray { - when g is { - Guid(b) -> b - } + let Guid(b) = g + b } 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..b72eef5 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -0,0 +1,33 @@ +use aiken/primitive/bytearray + +use layerzero/types +use layerzero/codec + +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) + + // version byte + encoded fields; we assert prefix is version 1 + let ver = bytearray.slice(encoded, 0, 1) + expect ver == bytearray.from_int_big_endian(1, 1) +} + +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) + + // sanity checks: length 32 and deterministic for same inputs + expect bytearray.length(h) == 32 + let h2 = codec.payload_hash(guid, msg) + expect h == h2 +} From a8d2a4aa865f820608500a126b3e48e28a2e9dea Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:19:22 +0000 Subject: [PATCH 28/72] cardano(aiken): fix codec to pad Address20 using from_int_big_endian(0,12); aiken check passes --- src/cardano/contracts/aiken/lib/layerzero/codec.ak | 13 +++++++------ src/cardano/contracts/aiken/tests/codec.tests.ak | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 75afe0e..a5168c0 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -9,19 +9,20 @@ pub fn encode_address32(a) -> ByteArray { bytes } -pub fn encode_address20(a) -> ByteArray { - let Address20(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 - builtin.serialise_data(e) + bytearray.from_int_big_endian(e, 4) } pub fn encode_nonce(a) -> ByteArray { let Nonce(n) = a - builtin.serialise_data(n) + bytearray.from_int_big_endian(n, 8) } pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { @@ -30,7 +31,7 @@ pub fn encode_header_v1(h: types.PacketHeaderV1) -> ByteArray { let b2 = encode_eid(h.src_eid) let b3 = encode_address32(h.sender) let b4 = encode_eid(h.dst_eid) - let b5 = encode_address20(h.receiver_b20) + 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) diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index b72eef5..2c6a7e9 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -14,9 +14,15 @@ test encode_header_v1_layout_simple() { let encoded = codec.encode_header_v1(header) - // version byte + encoded fields; we assert prefix is version 1 + // version byte + encoded fields; we assert prefix is version 1 and total length is 81 bytes let ver = bytearray.slice(encoded, 0, 1) expect ver == bytearray.from_int_big_endian(1, 1) + expect bytearray.length(encoded) == 81 + // quick spot-check offsets: nonce is 8 bytes after version, srcEid next 4 bytes + let nonce_be = bytearray.slice(encoded, 1, 9) + expect nonce_be == bytearray.from_int_big_endian(1, 8) + let src_eid_be = bytearray.slice(encoded, 9, 13) + expect src_eid_be == bytearray.from_int_big_endian(100, 4) } test payload_hash_concatenation() { From 0dda3d8e2b9566d54ce9d99aa1fbc0f16ed1d13e Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:20:34 +0000 Subject: [PATCH 29/72] cardano(aiken): add codec.assert_header_v1 and header_receiver_b20; wire into uln302_receive CommitVerification/Verify; add tests for header assertion --- .../contracts/aiken/lib/layerzero/codec.ak | 20 +++++++++++++++++++ .../contracts/aiken/tests/codec.tests.ak | 13 ++++++++++++ .../aiken/validators/uln302_receive.ak | 15 ++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index a5168c0..1e0c257 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -46,3 +46,23 @@ 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/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index 2c6a7e9..32f0d2b 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -37,3 +37,16 @@ test payload_hash_concatenation() { 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 +} diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 6190737..147504b 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -36,8 +36,19 @@ validator uln302_receive { ) { when redeemer is { SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> True - CommitVerification { packet_header: _, payload_hash: _ } -> True - Verify { packet_header: _, payload_hash: _, confirmations: _ } -> True + CommitVerification { packet_header: ph, payload_hash: _ } -> { + // For now only assert header against local_eid in datum if present + when datum is { + Some(ReceiveDatum { local_eid: le }) -> codec.assert_header_v1(ph, le) + None -> False + } + } + Verify { packet_header: ph, payload_hash: _, confirmations: _ } -> { + when datum is { + Some(ReceiveDatum { local_eid: le }) -> codec.assert_header_v1(ph, le) + None -> False + } + } } } From 54c8a363974c0a13842ccee5a1a59f3df01556f1 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:23:23 +0000 Subject: [PATCH 30/72] cardano(docs): expand README with architecture, state model, flows, and next steps --- src/cardano/contracts/README.md | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md index eeaa752..6bc5add 100644 --- a/src/cardano/contracts/README.md +++ b/src/cardano/contracts/README.md @@ -17,3 +17,66 @@ Target modules: - Shared: packet/header codecs, types, and constants This is an initial scaffold to establish the project layout and shared types/codec. Subsequent commits will add full validator logic for Endpoint, ULN302, Executor, and DVN. + +# LayerZero V2 on Cardano (Aiken) + +This folder contains the Cardano (Aiken) implementation of the core LayerZero V2 components, adapted to the eUTXO model: +- Endpoint +- ULN302 MessageLib (Send, Receive) +- Executor +- DVN + +Status +- Scaffold and shared modules exist and compile (aiken check). +- Packet V1 header codec matches EVM fixed-width layout: version(1) + nonce(u64=8) + srcEid(u32=4) + sender(bytes32) + dstEid(u32=4) + receiver(bytes32 padded from address20) => 81 bytes. +- Basic tests for codec hashing and header layout are in tests/. + +Architecture overview +- State is modeled via UTXOs: + - Endpoint Global UTXO: holds localEid and platform-level settings. + - Per-Channel UTXOs: one per (origin, receiver) tracking inbound nonce and execution flags for concurrency. + - ULN302 Config UTXOs: per-oapp per-dstEid configs for ULN and Executor, set via SetConfig calls. + - DVN State UTXO: signer set, quorum, and replay map anchoring. + - Verification/Attestation UTXOs: created by DVN/ReceiveLib to enable execution by Executor. +- Off-chain roles: + - OApp sends messages via ULN302 Send, producing an outbound instruction UTXO and fee outputs. + - DVN watchers pick up jobs, attest and produce verification UTXOs based on quorum. + - Executor watchers commit-and-execute messages once Endpoint and ULN verification conditions are satisfied. +- Fees: + - Modeled as lovelace on specific outputs; precise distribution to DVN/Executor is performed by off-chain submitters. + +Core flows +1) Send (ULN302 Send) + - Input: OApp + options + - Output: Outbound instruction UTXO with serialized header + payload, along with fee outputs for DVN/Executor workers. + - Config: Uses ExecutorConfig and UlnConfig resolved per oapp/dstEid. Options are split similar to EVM. + +2) Verify (ULN302 Receive + Endpoint) + - ReceiveUln302.assertHeader(header, localEid) checks version and dstEid. + - DVN quorum attests to (headerHash, payloadHash), producing verification UTXOs. + - Endpoint Verify consumes verification UTXO to mark channel nonce as verifiable/executable for the payload hash. + +3) Execute (Executor) + - If executable, consume verification and call Endpoint.lzReceive to finalize execution and update nonce state. + - Emits native drops as specified in options by constructing appropriate outputs. + +Data types and codecs +- Types: see aiken/lib/layerzero/types.ak for Eid, Address20/32, Nonce, Origin, Guid, configs. +- Codec: see aiken/lib/layerzero/codec.ak for Packet V1 header encoder, payload hashing, and helpers. +- Header v1 is byte-for-byte compatible with EVM’s PacketV1Codec.sol. + +Notes +- DVN signatures use Ed25519 multisig with configurable quorum; mapping to Cardano signatures will be enforced via redeemer signatures and datum state. +- Per-(origin, receiver) channel UTXOs maximize concurrency and avoid global bottlenecks. +- Endpoint and Receive library “view” functions are modeled by datum-driven states that off-chain components can query. + +Next steps +- Flesh out Endpoint validator to update per-channel nonce and executable state. +- Implement ULN302 Send/Receive config set/get and verification quorum checks. +- Implement DVN signer management, job assignment, and attestation UTXO creation. +- Implement Executor commit-and-execute state transitions and native drops output shaping. +- Extend tests: DVN quorum satisfied/not, endpoint nonce monotonicity, and end-to-end header->verify gating. + +Security and compatibility +- No keys or secrets are embedded in code; off-chain components sign and submit. +- Packet header layout and payload hashing are aligned with EVM to preserve cross-chain compatibility. From 03bbf9dc6f6378dfe9e29284af78bb321e9501dd Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:24:43 +0000 Subject: [PATCH 31/72] cardano(aiken/tests): add test for codec.header_receiver_b20 to confirm correct extraction from Packet V1 header --- src/cardano/contracts/aiken/tests/codec.tests.ak | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index 32f0d2b..c177c2c 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -50,3 +50,17 @@ test assert_header_v1_checks() { 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) +} From e4c49a43a35cbe9ca3c4148901249c0f34d266a3 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:29:53 +0000 Subject: [PATCH 32/72] cardano(aiken/codec): add header field helpers + constants module (lowercase identifiers); update tests --- .../contracts/aiken/lib/layerzero/codec.ak | 27 +++++++++++++++++++ .../aiken/lib/layerzero/constants.ak | 2 ++ .../contracts/aiken/tests/codec.tests.ak | 24 ++++++++++++----- 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/cardano/contracts/aiken/lib/layerzero/constants.ak diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 1e0c257..4bd3f1a 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -3,6 +3,7 @@ use aiken/primitive/bytearray use layerzero/types use layerzero/types.{Address32, Address20, Eid, Nonce, Guid, PacketHeaderV1} +use layerzero/constants pub fn encode_address32(a) -> ByteArray { let Address32(bytes) = a @@ -42,6 +43,32 @@ 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) +} + + + fn unwrap_guid(g) -> ByteArray { let Guid(b) = g b 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..2f5e994 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/constants.ak @@ -0,0 +1,2 @@ +const header_v1_version = 1 +const header_v1_len = 81 diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index c177c2c..f57b908 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -2,6 +2,7 @@ use aiken/primitive/bytearray use layerzero/types use layerzero/codec +use layerzero/constants test encode_header_v1_layout_simple() { let header = types.PacketHeaderV1 { @@ -14,15 +15,24 @@ test encode_header_v1_layout_simple() { let encoded = codec.encode_header_v1(header) - // version byte + encoded fields; we assert prefix is version 1 and total length is 81 bytes - let ver = bytearray.slice(encoded, 0, 1) - expect ver == bytearray.from_int_big_endian(1, 1) - expect bytearray.length(encoded) == 81 - // quick spot-check offsets: nonce is 8 bytes after version, srcEid next 4 bytes - let nonce_be = bytearray.slice(encoded, 1, 9) + 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 = bytearray.slice(encoded, 9, 13) + + 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() { From e0244896761456eb618a59faa35093ce4246f307 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:30:25 +0000 Subject: [PATCH 33/72] cardano(endpoint/helpers): add pure helpers for nonce monotonicity and executable gating with unit tests --- .../aiken/lib/layerzero/endpoint_helpers.ak | 38 ++++++++++++++++ .../contracts/aiken/tests/endpoint.tests.ak | 44 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/cardano/contracts/aiken/lib/layerzero/endpoint_helpers.ak create mode 100644 src/cardano/contracts/aiken/tests/endpoint.tests.ak 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/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 +} From 45f06626baa1fa2bdfdaff23c9387911422825bf Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:31:25 +0000 Subject: [PATCH 34/72] cardano(aiken/codec): add parse_header_v1 and round-trip test --- .../contracts/aiken/lib/layerzero/codec.ak | 35 +++++++++++++++++++ .../contracts/aiken/tests/codec.tests.ak | 20 +++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 4bd3f1a..ae3748d 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -67,6 +67,41 @@ 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 types.Address20(recv_b20) = header_receiver_b20(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 { diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index f57b908..172d13f 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -74,3 +74,23 @@ test header_receiver_b20_extracts_correct_address() { 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 + } +} From 93a8fb8f1653b307ad0658e61edfa53d1ff787e6 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:31:54 +0000 Subject: [PATCH 35/72] cardano(aiken): remove unused import/binding in codec; add dvn_helpers with quorum tests --- src/cardano/contracts/aiken/lib/layerzero/codec.ak | 2 -- .../contracts/aiken/lib/layerzero/dvn_helpers.ak | 3 +++ src/cardano/contracts/aiken/tests/dvn.tests.ak | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak create mode 100644 src/cardano/contracts/aiken/tests/dvn.tests.ak diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index ae3748d..826adf5 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -3,7 +3,6 @@ use aiken/primitive/bytearray use layerzero/types use layerzero/types.{Address32, Address20, Eid, Nonce, Guid, PacketHeaderV1} -use layerzero/constants pub fn encode_address32(a) -> ByteArray { let Address32(bytes) = a @@ -87,7 +86,6 @@ pub fn parse_header_v1(header: ByteArray) -> Option { 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 types.Address20(recv_b20) = header_receiver_b20(header) let recv20 = header_receiver_b20(header) Some(types.PacketHeaderV1 { nonce: nonce, 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..5a8e370 --- /dev/null +++ b/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak @@ -0,0 +1,3 @@ +pub fn quorum_satisfied(signer_count: Int, quorum: Int) -> Bool { + signer_count >= quorum +} 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..bb3bad3 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/dvn.tests.ak @@ -0,0 +1,13 @@ +use layerzero/dvn_helpers + +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 +} From a612064754a1970ab046d527bda3220d6dae0c58 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:32:46 +0000 Subject: [PATCH 36/72] cardano(endpoint): refactor datum to Global|Channel and enforce nonce monotonicity + executable gating --- .../contracts/aiken/validators/endpoint.ak | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index f8eae37..53dc79f 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -2,16 +2,13 @@ use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types -use layerzero/codec +use layerzero/endpoint_helpers pub type EndpointDatum { - EndpointDatum { + Global { local_eid: types.Eid } -} - -pub type ChannelDatum { - ChannelDatum { + Channel { origin: types.Origin, receiver: types.Address20, lazy_inbound_nonce: types.Nonce, @@ -49,8 +46,34 @@ validator endpoint { ) { when redeemer is { SetConfig { oapp: _, dst_eid: _, config_type: _, config: _ } -> True - Verify { origin: _, receiver: _, payload_hash: _ } -> True - LzReceive { origin: _, receiver: _, guid: _, message: _, extra_data: _ } -> 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 + } + } } } From 77c41ab61eee43b79cfc4f663043bbb1d46020db Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:35:20 +0000 Subject: [PATCH 37/72] cardano(aiken/validators): fix datum reference in uln302_receive; keep endpoint gating; minor cleanup across validators --- src/cardano/contracts/aiken/validators/dvn.ak | 3 +-- src/cardano/contracts/aiken/validators/endpoint.ak | 1 - src/cardano/contracts/aiken/validators/executor.ak | 12 +++++++----- .../contracts/aiken/validators/uln302_receive.ak | 1 - .../contracts/aiken/validators/uln302_send.ak | 3 +-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index fb8dd78..990d795 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -1,4 +1,3 @@ -use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -47,7 +46,7 @@ pub type DvnRedeemer { validator dvn { spend( - datum: Option, + _datum: Option, redeemer: DvnRedeemer, _0: OutputReference, _1: Transaction, diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 53dc79f..4bdf22f 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -1,4 +1,3 @@ -use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index eb3d944..8bbb8f3 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,4 +1,3 @@ -use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -38,14 +37,17 @@ pub type ExecutorRedeemer { validator executor { spend( - datum: Option, + _datum: Option, redeemer: ExecutorRedeemer, _0: OutputReference, _1: Transaction, ) { - when redeemer is { - CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: _ } -> True - } + let CommitAndExecute { + receive_lib: _, + lz_receive_param: _, + native_drops: _, + } = redeemer + True } else(_) { diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 147504b..aff8149 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -1,4 +1,3 @@ -use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 0a42fbf..584d670 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -1,4 +1,3 @@ -use aiken/builtin use cardano/transaction.{OutputReference, Transaction} use layerzero/types @@ -30,7 +29,7 @@ pub type SendRedeemer { validator uln302_send { spend( - datum: Option, + _datum: Option, redeemer: SendRedeemer, _0: OutputReference, _1: Transaction, From 1ce8b95682a108216d24af3a64927717f2f8c987 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:35:50 +0000 Subject: [PATCH 38/72] cardano(aiken): make header constants public; silence unused pattern bindings in endpoint --- src/cardano/contracts/aiken/lib/layerzero/constants.ak | 4 ++-- src/cardano/contracts/aiken/validators/endpoint.ak | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/constants.ak b/src/cardano/contracts/aiken/lib/layerzero/constants.ak index 2f5e994..d10aaec 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/constants.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/constants.ak @@ -1,2 +1,2 @@ -const header_v1_version = 1 -const header_v1_len = 81 +pub const header_v1_version = 1 +pub const header_v1_len = 81 diff --git a/src/cardano/contracts/aiken/validators/endpoint.ak b/src/cardano/contracts/aiken/validators/endpoint.ak index 4bdf22f..b30f4b1 100644 --- a/src/cardano/contracts/aiken/validators/endpoint.ak +++ b/src/cardano/contracts/aiken/validators/endpoint.ak @@ -48,7 +48,7 @@ validator endpoint { Verify { origin: og, receiver: rcvr, payload_hash: _ } -> { when datum is { Some(Channel { - origin: ch_og, + origin: _ch_og, receiver: ch_rcvr, lazy_inbound_nonce: ch_nonce, executed: _, @@ -62,7 +62,7 @@ validator endpoint { LzReceive { origin: og, receiver: rcvr, guid: _, message: _, extra_data: _ } -> { when datum is { Some(Channel { - origin: ch_og, + origin: _ch_og, receiver: ch_rcvr, lazy_inbound_nonce: ch_nonce, executed: True, From 795a9e9910fe9b1e7d9729e6663daef324bd76fd Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:36:46 +0000 Subject: [PATCH 39/72] cardano(aiken/tests): strengthen codec tests for wrong length/version and header field lengths --- .../contracts/aiken/tests/codec.tests.ak | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index 172d13f..e90f2bc 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -35,6 +35,134 @@ test encode_header_v1_layout_simple() { 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 +} + +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) From 99a1ce920c55ba1ba4fc6a11c35452a444ba3fc5 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:40:18 +0000 Subject: [PATCH 40/72] cardano(aiken/codec,tests): use builtin.blake2b_256 for header_hash_v1; remove duplicated imports/tests in codec tests --- .../contracts/aiken/lib/layerzero/codec.ak | 5 + .../contracts/aiken/tests/codec.tests.ak | 98 ++----------------- 2 files changed, 13 insertions(+), 90 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/codec.ak b/src/cardano/contracts/aiken/lib/layerzero/codec.ak index 826adf5..dd63bc3 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/codec.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/codec.ak @@ -1,9 +1,14 @@ 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 diff --git a/src/cardano/contracts/aiken/tests/codec.tests.ak b/src/cardano/contracts/aiken/tests/codec.tests.ak index e90f2bc..850c270 100644 --- a/src/cardano/contracts/aiken/tests/codec.tests.ak +++ b/src/cardano/contracts/aiken/tests/codec.tests.ak @@ -126,99 +126,17 @@ test header_src_and_sender_lengths() { expect bytearray.length(sender_b32) == 32 } -use aiken/primitive/bytearray - -use layerzero/types -use layerzero/codec -use layerzero/constants - -test encode_header_v1_layout_simple() { +test header_hash_v1_deterministic_and_32bytes() { 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) - - // sanity checks: length 32 and deterministic for same inputs - 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), + nonce: types.Nonce(5), + src_eid: types.Eid(111), sender: types.Address32(bytearray.new(32)), - dst_eid: types.Eid(200), + dst_eid: types.Eid(222), 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 - } + let h1 = codec.header_hash_v1(enc) + let h2 = codec.header_hash_v1(enc) + expect bytearray.length(h1) == 32 + expect h1 == h2 } From b93f6a18bb87d5d3269f217451dc787c27d79564 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:55:10 +0000 Subject: [PATCH 41/72] cardano(aiken): use builtin.head_list/tail_list for Data lists in attestation parsers; fix Option/List mismatch --- src/cardano/contracts/aiken/validators/dvn.ak | 66 ++++++++++++++++--- .../aiken/validators/uln302_receive.ak | 52 +++++++++++++-- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 990d795..29ab9bf 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -1,6 +1,9 @@ -use cardano/transaction.{OutputReference, Transaction} +use aiken/collection/list +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction} use layerzero/types +use layerzero/dvn_helpers pub type DvnDatum { DvnDatum { @@ -12,10 +15,9 @@ pub type DvnDatum { pub type ExecuteParam { ExecuteParam { vid: Int, - target: ByteArray, - call_data: ByteArray, - expiration: Int, - signatures: ByteArray, + header_hash: ByteArray, + payload_hash: ByteArray, + signer_count: Int, } } @@ -44,18 +46,66 @@ pub type DvnRedeemer { } } +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 _idx = pr.1st + 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_attestation_output(outputs: List, hh: ByteArray, ph: ByteArray, min_signers: Int) -> 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 && dvn_helpers.quorum_satisfied(sc, min_signers) + None -> False + } + } + _ -> False + } + }) +} + validator dvn { spend( - _datum: Option, + datum: Option, redeemer: DvnRedeemer, _0: OutputReference, - _1: Transaction, + tx: Transaction, ) { when redeemer is { SetSigner { signer: _, active: _ } -> True SetQuorum { quorum: _ } -> True AssignJob { param: _, options: _ } -> True - Execute { params: _ } -> True + Execute { params: ps } -> { + when datum is { + Some(DvnDatum { vid: _, quorum: q }) -> { + let ok = ps + |> list.any(fn(p) { + let ExecuteParam { vid: _, header_hash: hdrh, payload_hash: plh, signer_count: sc } = p + dvn_helpers.quorum_satisfied(sc, q) && has_attestation_output(tx.outputs, hdrh, plh, q) + }) + ok + } + None -> False + } + } } } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index aff8149..d46c702 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -1,7 +1,10 @@ -use cardano/transaction.{OutputReference, Transaction} +use aiken/collection/list +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction} use layerzero/types use layerzero/codec +use layerzero/constants pub type ReceiveDatum { ReceiveDatum { @@ -26,25 +29,64 @@ pub type ReceiveRedeemer { } } +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 _idx = pr.1st + 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 + } + }) +} + validator uln302_receive { spend( datum: Option, redeemer: ReceiveRedeemer, _0: OutputReference, - _1: Transaction, + tx: Transaction, ) { when redeemer is { SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> True CommitVerification { packet_header: ph, payload_hash: _ } -> { - // For now only assert header against local_eid in datum if present when datum is { Some(ReceiveDatum { local_eid: le }) -> codec.assert_header_v1(ph, le) None -> False } } - Verify { packet_header: ph, payload_hash: _, confirmations: _ } -> { + Verify { packet_header: ph, payload_hash: plh, confirmations: _ } -> { when datum is { - Some(ReceiveDatum { local_eid: le }) -> codec.assert_header_v1(ph, le) + Some(ReceiveDatum { local_eid: le }) -> { + 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) + ok_hdr && ok_att + } None -> False } } From c6dfc68285093b02777b8b9b5bca3bb935abf70a Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:55:31 +0000 Subject: [PATCH 42/72] cardano(aiken/types): stage previously modified types; keep shared AttestationDatum and protocol types in sync --- .../contracts/aiken/lib/layerzero/types.ak | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/types.ak b/src/cardano/contracts/aiken/lib/layerzero/types.ak index 9cf35bf..de1ed8f 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/types.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/types.ak @@ -1,17 +1,18 @@ + pub type Eid { Eid(Int) } -pub type Address20 { - Address20(ByteArray) +pub type Nonce { + Nonce(Int) } pub type Address32 { Address32(ByteArray) } -pub type Nonce { - Nonce(Int) +pub type Address20 { + Address20(ByteArray) } pub type Guid { @@ -26,36 +27,47 @@ pub type Origin { } } -pub type WorkerOption { - WorkerOption(ByteArray) -} - -pub type VerificationState { - Verifiable - Verifying - NotVerifiable +pub type PacketHeaderV1 { + PacketHeaderV1 { + nonce: Nonce, + src_eid: Eid, + sender: Address32, + dst_eid: Eid, + receiver_b20: Address20, + } } pub type ExecutorConfig { ExecutorConfig { - gas_limit: Int, - gas_for_fee_cap: Int, + placeholder: Int } } pub type UlnConfig { UlnConfig { - quorum: Int, confirmations: Int, + quorum: Int } } -pub type PacketHeaderV1 { - PacketHeaderV1 { - nonce: Nonce, - src_eid: Eid, - sender: Address32, - dst_eid: Eid, - receiver_b20: Address20, +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 } } From 77c199dcddfd1972b057b132ae2d9459dc0e89c7 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:55:53 +0000 Subject: [PATCH 43/72] cardano(aiken): remove unused import and unused _idx binding in attestation parsers; silence warnings --- src/cardano/contracts/aiken/validators/dvn.ak | 1 - src/cardano/contracts/aiken/validators/uln302_receive.ak | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 29ab9bf..cb4a333 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -50,7 +50,6 @@ 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 _idx = pr.1st let fs = pr.2nd let f0 = builtin.head_list(fs) let fs1 = builtin.tail_list(fs) diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index d46c702..711a012 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -4,7 +4,6 @@ use cardano/transaction.{Datum, Output, OutputReference, Transaction} use layerzero/types use layerzero/codec -use layerzero/constants pub type ReceiveDatum { ReceiveDatum { @@ -33,7 +32,6 @@ 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 _idx = pr.1st let fs = pr.2nd let f0 = builtin.head_list(fs) let fs1 = builtin.tail_list(fs) From a5de8d6bfda54718d074af8669e5a4024b676800 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:59:12 +0000 Subject: [PATCH 44/72] cardano(aiken/tests): add ULN302 Receive tests for attestation parsing and header checks --- .../aiken/tests/uln302_receive.tests.ak | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/cardano/contracts/aiken/tests/uln302_receive.tests.ak 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..8592b46 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak @@ -0,0 +1,100 @@ +use aiken/builtin +use cardano/address +use cardano/assets +use cardano/transaction + +use layerzero/types +use layerzero/codec +use layerzero/validators/uln302_receive + +test uln302_verify_accepts_with_matching_attestation() { + let local_eid = types.Eid(200) + 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(bytearray.new(20)), + } + 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 = 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(uln302_receive.ReceiveDatum { local_eid: local_eid }) + 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_mismatched_attestation() { + 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 guid_bad = types.Guid(bytearray.from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + let msg_bad = bytearray.from_utf8("bad") + let plh_bad = codec.payload_hash(guid_bad, msg_bad) + + let att_wrong: Data = + builtin.constr_data( + 0, + builtin.list_data([builtin.b_data(hh), builtin.b_data(plh_bad), builtin.i_data(1)]), + ) + + let out = transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(att_wrong), + reference_script: None, + } + + let tx = transaction.Transaction { + ..transaction.placeholder, + outputs: [out], + } + + let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid }) + 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 +} From 2bc73b1fc1557450c33d25d303063987570a0e7e Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 12:59:47 +0000 Subject: [PATCH 45/72] cardano(aiken/tests): add DVN Execute tests for quorum satisfied/unsatisfied via InlineDatum attestations --- .../aiken/tests/dvn_execute.tests.ak | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/cardano/contracts/aiken/tests/dvn_execute.tests.ak 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..0d94f4f --- /dev/null +++ b/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak @@ -0,0 +1,88 @@ +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 +} From 651490483cdee10edcf773793367b6390dbf3fc9 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:00:32 +0000 Subject: [PATCH 46/72] cardano(aiken/tests): add Endpoint validator tests for Verify and LzReceive gating (nonce monotonicity, executed flag) --- .../aiken/tests/endpoint_validator.tests.ak | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/cardano/contracts/aiken/tests/endpoint_validator.tests.ak 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 +} From 177a4b169614864d3a97cabaa0405c2c88a87ce7 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:02:42 +0000 Subject: [PATCH 47/72] cardano(aiken/uln302_send): validate outbound instruction InlineDatum (81-byte header, options count) + tests --- .../contracts/aiken/lib/layerzero/types.ak | 8 ++ .../aiken/tests/uln302_send.tests.ak | 83 +++++++++++++++++++ .../contracts/aiken/validators/uln302_send.ak | 45 +++++++++- 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/cardano/contracts/aiken/tests/uln302_send.tests.ak diff --git a/src/cardano/contracts/aiken/lib/layerzero/types.ak b/src/cardano/contracts/aiken/lib/layerzero/types.ak index de1ed8f..24c37fe 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/types.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/types.ak @@ -71,3 +71,11 @@ pub type AttestationDatum { signer_count: Int } } + +pub type OutboundDatum { + OutboundDatum { + header: ByteArray, + payload: ByteArray, + options_len: Int + } +} 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..b3a2de0 --- /dev/null +++ b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak @@ -0,0 +1,83 @@ +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 }, + }) + + 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 }, + }) + + 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/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 584d670..e3d3a2a 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -1,4 +1,7 @@ -use cardano/transaction.{OutputReference, Transaction} +use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/transaction.{Datum, Output, OutputReference, Transaction} use layerzero/types @@ -27,17 +30,53 @@ pub type SendRedeemer { } } +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, - _1: Transaction, + tx: Transaction, ) { when redeemer is { SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> True SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> True - Send { options: _ } -> True + Send { options: opts } -> { + has_valid_outbound(tx.outputs, list.length(opts)) + } } } From 37a498d4a4ae97f88a957cf8da82b686e4e7ea98 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:03:42 +0000 Subject: [PATCH 48/72] cardano(docs): document OutboundDatum and AttestationDatum InlineDatum schemas; note new ULN302 Send/Receive and DVN tests --- src/cardano/contracts/README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md index 6bc5add..f83d26a 100644 --- a/src/cardano/contracts/README.md +++ b/src/cardano/contracts/README.md @@ -7,7 +7,7 @@ Structure: - aiken.toml Project config - validators/ On-chain validators (scripts) - lib/ Shared libraries (types, codecs, utils) - - tests/ Aiken tests (to be added) + - tests/ Aiken tests Target modules: - Endpoint: endpoint state, verification, and execution state machine @@ -29,7 +29,7 @@ This folder contains the Cardano (Aiken) implementation of the core LayerZero V2 Status - Scaffold and shared modules exist and compile (aiken check). - Packet V1 header codec matches EVM fixed-width layout: version(1) + nonce(u64=8) + srcEid(u32=4) + sender(bytes32) + dstEid(u32=4) + receiver(bytes32 padded from address20) => 81 bytes. -- Basic tests for codec hashing and header layout are in tests/. +- Tests cover: codec hashing/layout/parse, ULN302 Receive header+attestation gating, DVN Execute quorum, Endpoint nonce/executable gating, and ULN302 Send outbound instruction validation. Architecture overview - State is modeled via UTXOs: @@ -45,11 +45,24 @@ Architecture overview - Fees: - Modeled as lovelace on specific outputs; precise distribution to DVN/Executor is performed by off-chain submitters. +UTXO schemas (InlineDatum) +- AttestationDatum (used by DVN and ReceiveUln302 tests): + - constructor index 0 with fields: + - header_hash: ByteArray + - payload_hash: ByteArray + - signer_count: Int +- OutboundDatum (used by ULN302 Send tests): + - constructor index 0 with fields: + - header: ByteArray (must be 81 bytes for PacketV1) + - payload: ByteArray + - options_len: Int + Core flows 1) Send (ULN302 Send) - Input: OApp + options - Output: Outbound instruction UTXO with serialized header + payload, along with fee outputs for DVN/Executor workers. - Config: Uses ExecutorConfig and UlnConfig resolved per oapp/dstEid. Options are split similar to EVM. + - Validation (current): presence of OutboundDatum with header length 81 and options_len matching the redeemer’s options length. 2) Verify (ULN302 Receive + Endpoint) - ReceiveUln302.assertHeader(header, localEid) checks version and dstEid. @@ -61,7 +74,7 @@ Core flows - Emits native drops as specified in options by constructing appropriate outputs. Data types and codecs -- Types: see aiken/lib/layerzero/types.ak for Eid, Address20/32, Nonce, Origin, Guid, configs. +- Types: see aiken/lib/layerzero/types.ak for Eid, Address20/32, Nonce, Origin, Guid, configs, AttestationDatum, OutboundDatum. - Codec: see aiken/lib/layerzero/codec.ak for Packet V1 header encoder, payload hashing, and helpers. - Header v1 is byte-for-byte compatible with EVM’s PacketV1Codec.sol. From fb7786ef181bdadc2b2d42c7fb9572b0fdafb59e Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:04:06 +0000 Subject: [PATCH 49/72] cardano(aiken/tests): add ULN302 Receive CommitVerification header assertion tests (accept/reject) --- .../tests/uln302_receive_commit.tests.ak | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak 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..7a57deb --- /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 }) + 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 }) + 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 +} From ce2b98b690cdf5a05f8cce4d6a298a011a777018 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:04:45 +0000 Subject: [PATCH 50/72] cardano(aiken/tests): add Executor CommitAndExecute minimal tests (accept with empty/some native drops) --- .../aiken/tests/executor_commit.tests.ak | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/cardano/contracts/aiken/tests/executor_commit.tests.ak 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..7c7361c --- /dev/null +++ b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak @@ -0,0 +1,58 @@ +use cardano/transaction + +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, + } +} + +test executor_commit_and_execute_accepts_with_empty_native_drops() { + let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) + let redeemer = executor.ExecutorRedeemer.CommitAndExecute { + receive_lib: #"", + lz_receive_param: mk_lz_receive_param(), + native_drops: [], + } + let tx = transaction.placeholder + + 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 datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) + 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 tx = transaction.placeholder + + expect executor.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == True +} From 46417c3d6b8f7ea433a758baceeacf69c8dad47f Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:07:13 +0000 Subject: [PATCH 51/72] cardano(executor): enforce native drop coverage with total outputs lovelace; add negative test for insufficient funds --- .../aiken/tests/executor_commit.tests.ak | 36 ++++++++++++++++++- .../contracts/aiken/validators/executor.ak | 29 +++++++++++---- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak index 7c7361c..5b28ec1 100644 --- a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak +++ b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak @@ -1,4 +1,6 @@ use cardano/transaction +use cardano/address +use cardano/assets use layerzero/types use layerzero/validators/executor @@ -19,6 +21,15 @@ fn mk_lz_receive_param() -> executor.LzReceiveParam { } } +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, + } +} + test executor_commit_and_execute_accepts_with_empty_native_drops() { let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) let redeemer = executor.ExecutorRedeemer.CommitAndExecute { @@ -47,7 +58,8 @@ test executor_commit_and_execute_accepts_with_some_native_drops() { lz_receive_param: mk_lz_receive_param(), native_drops: drops, } - let tx = transaction.placeholder + let tx = + transaction.Transaction { ..transaction.placeholder, outputs: [mk_out(30)] } expect executor.spend( datum, @@ -56,3 +68,25 @@ test executor_commit_and_execute_accepts_with_some_native_drops() { tx ) == True } + +test executor_commit_and_execute_rejects_when_insufficient_lovelace_for_drops() { + let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) + 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 tx = + transaction.Transaction { ..transaction.placeholder, outputs: [mk_out(20)] } + + expect executor.spend( + datum, + redeemer, + transaction.OutputReference { transaction_id: tx.id, output_index: 0 }, + tx + ) == False +} diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 8bbb8f3..d0b68dd 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,4 +1,6 @@ +use aiken/collection/list use cardano/transaction.{OutputReference, Transaction} +use cardano/assets use layerzero/types @@ -35,19 +37,32 @@ pub type ExecutorRedeemer { } } +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 }, + ) +} + validator executor { spend( _datum: Option, redeemer: ExecutorRedeemer, _0: OutputReference, - _1: Transaction, + tx: Transaction, ) { - let CommitAndExecute { - receive_lib: _, - lz_receive_param: _, - native_drops: _, - } = redeemer - True + when redeemer is { + CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: drops } -> { + let need = sum_native_drops(drops) + let have = total_outputs_lovelace(tx) + have >= need && need >= 0 + } + } } else(_) { From ab9e3bbf6b379cc2af3f84cb6a059b3617e34899 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:10:32 +0000 Subject: [PATCH 52/72] cardano(uln302/receive): fix type mismatch by unwrapping Nonce to Int before decrement; aiken check passes --- .../aiken/tests/uln302_receive.tests.ak | 48 +++++++++---- .../aiken/validators/uln302_receive.ak | 68 ++++++++++++++++--- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak index 8592b46..cb3a825 100644 --- a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak @@ -7,14 +7,40 @@ use layerzero/types use layerzero/codec use layerzero/validators/uln302_receive -test uln302_verify_accepts_with_matching_attestation() { +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(bytearray.new(20)), + receiver_b20: types.Address20(receiver_b20), } let ph = codec.encode_header_v1(header) let hh = codec.header_hash_v1(ph) @@ -29,16 +55,18 @@ test uln302_verify_accepts_with_matching_attestation() { builtin.list_data([builtin.b_data(hh), builtin.b_data(plh), builtin.i_data(3)]), ) - let out = transaction.Output { + 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], + outputs: [out_att, out_ep], } let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid }) @@ -51,7 +79,7 @@ test uln302_verify_accepts_with_matching_attestation() { expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == True } -test uln302_verify_rejects_with_mismatched_attestation() { +test uln302_verify_rejects_without_endpoint_channel_output() { let local_eid = types.Eid(200) let header = types.PacketHeaderV1 { nonce: types.Nonce(2), @@ -67,20 +95,16 @@ test uln302_verify_rejects_with_mismatched_attestation() { let msg_ok = bytearray.from_utf8("ok") let plh_ok = codec.payload_hash(guid_ok, msg_ok) - let guid_bad = types.Guid(bytearray.from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) - let msg_bad = bytearray.from_utf8("bad") - let plh_bad = codec.payload_hash(guid_bad, msg_bad) - - let att_wrong: Data = + let att_ok: Data = builtin.constr_data( 0, - builtin.list_data([builtin.b_data(hh), builtin.b_data(plh_bad), builtin.i_data(1)]), + 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_wrong), + datum: transaction.Datum.InlineDatum(att_ok), reference_script: None, } diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 711a012..326ea1d 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -50,16 +50,64 @@ fn parse_attestation(d: Data) -> Option<(ByteArray, ByteArray, Int)> { 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 + when o.datum is { + Datum.InlineDatum(d) -> { + when parse_attestation(d) is { + Some((ah, ap, _)) -> ah == hh && ap == ph + None -> False + } } + _ -> 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 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 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 + } + }) } validator uln302_receive { @@ -83,7 +131,11 @@ validator uln302_receive { 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) - ok_hdr && ok_att + 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) + ok_hdr && ok_att && ok_ep } None -> False } From f3e0b7672baed094b46d397362f3da7695277aff Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:11:01 +0000 Subject: [PATCH 53/72] cardano(aiken): update lockfile --- src/cardano/contracts/aiken/aiken.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cardano/contracts/aiken/aiken.lock b/src/cardano/contracts/aiken/aiken.lock index 0e44550..ba5fa13 100644 --- a/src/cardano/contracts/aiken/aiken.lock +++ b/src/cardano/contracts/aiken/aiken.lock @@ -13,4 +13,4 @@ requirements = [] source = "github" [etags] -"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1755950936, nanos_since_epoch = 208175049 }, "25c8d0802b8266feca04b47933382c5dee3cadb422208a5d3810d9d2df108c2e"] +"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1755954588, nanos_since_epoch = 724920106 }, "25c8d0802b8266feca04b47933382c5dee3cadb422208a5d3810d9d2df108c2e"] From 1d3fec4e5f616396f5826b8d64fa69b9008620b0 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:11:36 +0000 Subject: [PATCH 54/72] cardano(aiken): tidy warnings (executor let-pattern; silence unused var in uln302_receive) --- src/cardano/contracts/aiken/validators/executor.ak | 11 ++++------- .../contracts/aiken/validators/uln302_receive.ak | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index d0b68dd..ccf12cb 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -56,13 +56,10 @@ validator executor { _0: OutputReference, tx: Transaction, ) { - when redeemer is { - CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: drops } -> { - let need = sum_native_drops(drops) - let have = total_outputs_lovelace(tx) - have >= need && need >= 0 - } - } + let CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: drops } = redeemer + let need = sum_native_drops(drops) + let have = total_outputs_lovelace(tx) + have >= need && need >= 0 } else(_) { diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 326ea1d..7ae1a88 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -69,7 +69,7 @@ fn parse_endpoint_channel(d: Data) -> Option<(types.Address20, types.Nonce, Bool let tag = pr.1st if tag == 1 { let fs = pr.2nd - let f_origin = builtin.head_list(fs) + 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) From faa566433491745824aef7fbca11b1fe7bf863d7 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 55/72] cardano(dvn_helpers): replace local recursion in unique_signers with fold; fix lambda body; keep signers_within_set clean --- .../aiken/lib/layerzero/dvn_helpers.ak | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak b/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak index 5a8e370..7cf0ed2 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/dvn_helpers.ak @@ -1,3 +1,62 @@ -pub fn quorum_satisfied(signer_count: Int, quorum: Int) -> Bool { - signer_count >= quorum +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 } From 6e94920571d52d352fdce34d2a7f9d7eff6e07dd Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 56/72] cardano(uln302/receive): clean ReceiveRedeemer; split find_sigs; replace list.find_map with fold; enforce atomic Endpoint.Verify; fix pattern matches --- .../aiken/validators/uln302_receive.ak | 71 +++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 7ae1a88..60768c5 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -4,10 +4,15 @@ use cardano/transaction.{Datum, Output, OutputReference, Transaction} use layerzero/types use layerzero/codec +use layerzero/dvn_helpers +use aiken/primitive/bytearray pub type ReceiveDatum { ReceiveDatum { - local_eid: types.Eid + local_eid: types.Eid, + dvn_vid: Int, + required_quorum: Int, + required_confirmations: Int, } } @@ -110,6 +115,53 @@ fn has_matching_endpoint_verify_output( }) } +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 + } + } + } + }, + ) +} + validator uln302_receive { spend( datum: Option, @@ -121,13 +173,13 @@ validator uln302_receive { SetConfigUln { oapp: _, src_eid: _, cfg: _ } -> True CommitVerification { packet_header: ph, payload_hash: _ } -> { when datum is { - Some(ReceiveDatum { local_eid: le }) -> codec.assert_header_v1(ph, le) + 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: _ } -> { + Verify { packet_header: ph, payload_hash: plh, confirmations: confs } -> { when datum is { - Some(ReceiveDatum { local_eid: le }) -> { + Some(ReceiveDatum { local_eid: le, dvn_vid: _vid, required_quorum: q, 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) @@ -135,7 +187,16 @@ validator uln302_receive { 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) - ok_hdr && ok_att && ok_ep + 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) + let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q) + ok_hdr && ok_att && ok_ep && ok_conf && unique && ok_ms + } + None -> False + } } None -> False } From 09e8b0b4c398b1933b5e269413722ac57128d06f Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 57/72] cardano(dvn): close parse_attestation; replace list.find_map with fold in find_sigs; tidy braces; compile cleanly --- src/cardano/contracts/aiken/validators/dvn.ak | 80 ++++++++++++++++--- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index cb4a333..4495511 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -9,6 +9,7 @@ pub type DvnDatum { DvnDatum { vid: Int, quorum: Int, + signers: List, } } @@ -30,9 +31,8 @@ pub type AssignJobParam { } pub type DvnRedeemer { - SetSigner { - signer: types.Address32, - active: Bool, + SetSignerSet { + signers: List, } SetQuorum { quorum: Int, @@ -65,14 +65,41 @@ fn parse_attestation(d: Data) -> Option<(ByteArray, ByteArray, Int)> { } } -fn has_attestation_output(outputs: List, hh: ByteArray, ph: ByteArray, min_signers: Int) -> Bool { +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 && dvn_helpers.quorum_satisfied(sc, min_signers) + Some((ah, ap, sc)) -> ah == hh && ap == ph && sc >= 0 None -> False } } @@ -81,6 +108,29 @@ fn has_attestation_output(outputs: List, hh: ByteArray, ph: ByteArray, m }) } +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, @@ -89,16 +139,26 @@ validator dvn { tx: Transaction, ) { when redeemer is { - SetSigner { signer: _, active: _ } -> True + SetSignerSet { signers: _ } -> True SetQuorum { quorum: _ } -> True AssignJob { param: _, options: _ } -> True Execute { params: ps } -> { when datum is { - Some(DvnDatum { vid: _, quorum: q }) -> { + 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: sc } = p - dvn_helpers.quorum_satisfied(sc, q) && has_attestation_output(tx.outputs, hdrh, plh, q) + 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 } From 33dc26a30e4d5a71184af572950df899dbed1a20 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 58/72] cardano(uln302_send,executor): close sum types; rename datum param; minor parser fixes --- .../contracts/aiken/validators/executor.ak | 96 ++++++++++++++++++- .../contracts/aiken/validators/uln302_send.ak | 24 ++++- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index ccf12cb..99613bf 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,5 +1,6 @@ use aiken/collection/list -use cardano/transaction.{OutputReference, Transaction} +use aiken/builtin +use cardano/transaction.{Datum, Output, OutputReference, Transaction} use cardano/assets use layerzero/types @@ -37,6 +38,90 @@ pub type ExecutorRedeemer { } } +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_output(prev_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 == 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 }) } @@ -56,10 +141,15 @@ validator executor { _0: OutputReference, tx: Transaction, ) { - let CommitAndExecute { receive_lib: _, lz_receive_param: _, native_drops: drops } = redeemer + let CommitAndExecute { receive_lib: _, lz_receive_param: lp, native_drops: drops } = redeemer let need = sum_native_drops(drops) let have = total_outputs_lovelace(tx) - have >= need && need >= 0 + 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 = has_endpoint_prev_output(prev, True, tx.outputs) + let ok_curr = has_endpoint_curr_output(og_nonce, False, tx.outputs) + ok_prev && ok_curr && have >= need && need >= 0 } else(_) { diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index e3d3a2a..3bbb4c9 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -2,6 +2,7 @@ use aiken/builtin use aiken/collection/list use aiken/primitive/bytearray use cardano/transaction.{Datum, Output, OutputReference, Transaction} +use cardano/assets use layerzero/types @@ -11,6 +12,8 @@ pub type SendDatum { dst_eid: types.Eid, executor_cfg: types.ExecutorConfig, uln_cfg: types.UlnConfig, + executor_fee_addr: types.Address32, + dvn_fee_addr: types.Address32, } } @@ -30,6 +33,17 @@ pub type SendRedeemer { } } +fn count_fee_recipients(outputs: List) -> Int { + list.foldr( + outputs, + 0, + fn(o, acc) { + let lov = assets.lovelace_of(o.value) + acc + if lov > 0 { 1 } else { 0 } + }, + ) +} + fn parse_outbound(d: Data) -> Option<(ByteArray, ByteArray, Int)> { let is_constr = builtin.choose_data(d, True, False, False, False, False) if is_constr { @@ -66,7 +80,7 @@ fn has_valid_outbound(outputs: List, expected_opts_len: Int) -> Bool { validator uln302_send { spend( - _datum: Option, + datum: Option, redeemer: SendRedeemer, _0: OutputReference, tx: Transaction, @@ -75,7 +89,13 @@ validator uln302_send { SetConfigExec { oapp: _, dst_eid: _, cfg: _ } -> True SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> True Send { options: opts } -> { - has_valid_outbound(tx.outputs, list.length(opts)) + when datum is { + Some(SendDatum { oapp: _, dst_eid: _, executor_fee_addr: _, dvn_fee_addr: _, executor_cfg: _, uln_cfg: _ }) -> { + has_valid_outbound(tx.outputs, list.length(opts)) && + count_fee_recipients(tx.outputs) >= 2 + } + None -> False + } } } } From fcf1e2408e33f5dd35dc5877dc4e519237f09660 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 59/72] cardano(tests): sync tests with validator signatures and parsing fixes --- .../aiken/tests/dvn_execute.tests.ak | 34 +++++++++++++++ .../aiken/tests/executor_commit.tests.ak | 42 +++++++++++++++++- .../aiken/tests/uln302_receive.tests.ak | 4 +- .../tests/uln302_receive_commit.tests.ak | 4 +- .../aiken/tests/uln302_send.tests.ak | 43 +++++++++++++++++++ 5 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak b/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak index 0d94f4f..c0fb7c6 100644 --- a/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak +++ b/src/cardano/contracts/aiken/tests/dvn_execute.tests.ak @@ -1,4 +1,38 @@ 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 diff --git a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak index 5b28ec1..a655308 100644 --- a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak +++ b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak @@ -1,3 +1,4 @@ +use aiken/builtin use cardano/transaction use cardano/address use cardano/assets @@ -39,6 +40,39 @@ test executor_commit_and_execute_accepts_with_empty_native_drops() { } let tx = transaction.placeholder +fn mk_endpoint_channel_input(prev_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(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])) + transaction.Output { + address: address.from_verification_key(#"00"), + value: assets.zero, + datum: transaction.Datum.InlineDatum(channel_d), + reference_script: None, + } +} + +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, + } +} expect executor.spend( datum, redeemer, @@ -58,8 +92,10 @@ test executor_commit_and_execute_accepts_with_some_native_drops() { 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, outputs: [mk_out(30)] } + transaction.Transaction { ..transaction.placeholder, outputs: [mk_out(30), ep_in, ep_out] } expect executor.spend( datum, @@ -80,8 +116,10 @@ test executor_commit_and_execute_rejects_when_insufficient_lovelace_for_drops() 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, outputs: [mk_out(20)] } + transaction.Transaction { ..transaction.placeholder, outputs: [mk_out(20), ep_in, ep_out] } expect executor.spend( datum, diff --git a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak index cb3a825..d8918da 100644 --- a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak @@ -69,7 +69,7 @@ test uln302_verify_accepts_with_matching_attestation_and_endpoint_channel_output outputs: [out_att, out_ep], } - let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid }) + 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, @@ -113,7 +113,7 @@ test uln302_verify_rejects_without_endpoint_channel_output() { outputs: [out], } - let datum = Some(uln302_receive.ReceiveDatum { local_eid: local_eid }) + 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, diff --git a/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak index 7a57deb..5f0dc02 100644 --- a/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_receive_commit.tests.ak @@ -15,7 +15,7 @@ test uln302_commitverification_accepts_when_header_dst_matches_local_eid() { } let ph = codec.encode_header_v1(header) - let datum = Some(uln302_receive.ReceiveDatum { local_eid: local }) + 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), @@ -41,7 +41,7 @@ test uln302_commitverification_rejects_when_header_dst_mismatch() { } let ph = codec.encode_header_v1(header) - let datum = Some(uln302_receive.ReceiveDatum { local_eid: local }) + 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), diff --git a/src/cardano/contracts/aiken/tests/uln302_send.tests.ak b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak index b3a2de0..6f27391 100644 --- a/src/cardano/contracts/aiken/tests/uln302_send.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak @@ -1,4 +1,47 @@ use aiken/builtin +use cardano/transaction +use cardano/address +use cardano/assets + +use layerzero/types +use layerzero/validators/uln302_send + +fn mk_fee_out(addr_bytes: ByteArray) -> transaction.Output { + transaction.Output { + address: address.from_verification_key(addr_bytes), + value: assets.from_lovelace(1), + datum: transaction.Datum.NoDatum, + reference_script: None, + } +} + +test uln302_send_requires_fee_outputs_to_executor_and_dvn() { + let efa = types.Address32(#"01") + let dfa = types.Address32(#"02") + let datum = Some(uln302_send.SendDatum { + oapp: types.Address32(#"00"), + dst_eid: types.Eid(200), + executor_fee_addr: efa, + dvn_fee_addr: dfa, + executor_cfg: types.ExecutorConfig { placeholder: 0 }, + uln_cfg: types.UlnConfig { confirmations: 0, quorum: 1 }, + }) + 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(#"01"), mk_fee_out(#"02")], + } + 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 From bdb9e3037b99de3ed8cb47950dcf083175ea9817 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:33:06 +0000 Subject: [PATCH 60/72] cardano(types): keep in sync with new DVN/ULN structures (minor updates) --- src/cardano/contracts/aiken/lib/layerzero/types.ak | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/cardano/contracts/aiken/lib/layerzero/types.ak b/src/cardano/contracts/aiken/lib/layerzero/types.ak index 24c37fe..f672500 100644 --- a/src/cardano/contracts/aiken/lib/layerzero/types.ak +++ b/src/cardano/contracts/aiken/lib/layerzero/types.ak @@ -72,6 +72,15 @@ pub type AttestationDatum { } } +pub type SigsDatum { + SigsDatum { + header_hash: ByteArray, + payload_hash: ByteArray, + sigs: List, + pubs: List + } +} + pub type OutboundDatum { OutboundDatum { header: ByteArray, From 7068a17e746a081466a08685ff1b02deeeb35f15 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:38:45 +0000 Subject: [PATCH 61/72] cardano(executor): require Endpoint channel prev input (executed=True at prev nonce) and curr output (executed=False at current); enforce atomicity with Endpoint.LzReceive --- .../contracts/aiken/validators/executor.ak | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 99613bf..76200c2 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -1,6 +1,6 @@ use aiken/collection/list use aiken/builtin -use cardano/transaction.{Datum, Output, OutputReference, Transaction} +use cardano/transaction.{Datum, Output, OutputReference, Transaction, Input} use cardano/assets use layerzero/types @@ -92,6 +92,23 @@ fn parse_endpoint_channel(d: Data) -> Option<(types.Origin, types.Address20, typ 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_prev_output(prev_nonce: types.Nonce, executed_flag: Bool, outputs: List) -> Bool { outputs @@ -147,9 +164,9 @@ validator executor { 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 = has_endpoint_prev_output(prev, True, tx.outputs) - let ok_curr = has_endpoint_curr_output(og_nonce, False, tx.outputs) - ok_prev && ok_curr && have >= need && need >= 0 + 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) + ok_prev_in && ok_curr_out && have >= need && need >= 0 } else(_) { From 02fab06a317bc3f9d7c732d5f47e43d7c10dd6ac Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:41:28 +0000 Subject: [PATCH 62/72] cardano(dvn): add admin key gating for signer/quorum updates; cardano(uln302_send): enforce fee outputs to configured VKHs --- src/cardano/contracts/aiken/validators/dvn.ak | 18 +++++++++++--- .../contracts/aiken/validators/uln302_send.ak | 24 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/dvn.ak b/src/cardano/contracts/aiken/validators/dvn.ak index 4495511..3b0d22b 100644 --- a/src/cardano/contracts/aiken/validators/dvn.ak +++ b/src/cardano/contracts/aiken/validators/dvn.ak @@ -1,6 +1,7 @@ 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 @@ -10,6 +11,7 @@ pub type DvnDatum { vid: Int, quorum: Int, signers: List, + admin: VerificationKeyHash, } } @@ -139,12 +141,22 @@ validator dvn { tx: Transaction, ) { when redeemer is { - SetSignerSet { signers: _ } -> True - SetQuorum { quorum: _ } -> True + 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 }) -> { + 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 diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index 3bbb4c9..dd0d00a 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -3,6 +3,8 @@ 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 @@ -12,8 +14,8 @@ pub type SendDatum { dst_eid: types.Eid, executor_cfg: types.ExecutorConfig, uln_cfg: types.UlnConfig, - executor_fee_addr: types.Address32, - dvn_fee_addr: types.Address32, + executor_fee_vkh: VerificationKeyHash, + dvn_fee_vkh: VerificationKeyHash, } } @@ -29,10 +31,22 @@ pub type SendRedeemer { cfg: types.UlnConfig, } Send { - options: List, + 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 count_fee_recipients(outputs: List) -> Int { list.foldr( outputs, @@ -90,9 +104,9 @@ validator uln302_send { SetConfigUln { oapp: _, dst_eid: _, cfg: _ } -> True Send { options: opts } -> { when datum is { - Some(SendDatum { oapp: _, dst_eid: _, executor_fee_addr: _, dvn_fee_addr: _, executor_cfg: _, uln_cfg: _ }) -> { + 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)) && - count_fee_recipients(tx.outputs) >= 2 + has_fee_outputs(tx.outputs, ex, dv) } None -> False } From 96a29959318f7552de776edb529f3525d91f14a0 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:50:06 +0000 Subject: [PATCH 63/72] cardano(uln302_receive): dedup DVN signer helpers; fix find_sigs; compile cleanly --- .../contracts/aiken/validators/executor.ak | 39 ++++++++++---- .../aiken/validators/uln302_receive.ak | 54 ++++++++++++++++++- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 76200c2..3cae8bb 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -2,12 +2,15 @@ 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 + local_eid: types.Eid, + executor_fee_vkh: VerificationKeyHash } } @@ -151,22 +154,38 @@ fn total_outputs_lovelace(tx: Transaction) -> Int { ) } +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, + datum: Option, redeemer: ExecutorRedeemer, _0: OutputReference, tx: Transaction, ) { let CommitAndExecute { receive_lib: _, lz_receive_param: lp, native_drops: drops } = redeemer - 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) - ok_prev_in && ok_curr_out && have >= need && need >= 0 + 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(_) { diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 60768c5..078e9df 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -6,6 +6,7 @@ use layerzero/types use layerzero/codec use layerzero/dvn_helpers use aiken/primitive/bytearray +use cardano/transaction.{Input} pub type ReceiveDatum { ReceiveDatum { @@ -162,6 +163,49 @@ fn find_sigs(outputs: List, hh: ByteArray, ph: ByteArray) -> Option<(Lis ) } + +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 _f_vid = builtin.head_list(fs) + let fs1 = builtin.tail_list(fs) + let _f_quorum = builtin.head_list(fs1) + let fs2 = builtin.tail_list(fs1) + let f_signers = builtin.head_list(fs2) + let fs3 = builtin.tail_list(fs2) + let _f_admin = builtin.head_list(fs3) + 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 + } + } + } + }, + ) +} + validator uln302_receive { spend( datum: Option, @@ -192,8 +236,14 @@ validator uln302_receive { Some((sigs, pubs)) -> { let msg = bytearray.concat(hh, plh) let unique = dvn_helpers.unique_signers(pubs) - let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q) - ok_hdr && ok_att && ok_ep && ok_conf && unique && ok_ms + when find_dvn_signers(tx.reference_inputs) is { + Some(set) -> { + 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 } From e7b2cf46cc4816df2878738303383fdbfa31b97b Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:51:20 +0000 Subject: [PATCH 64/72] cardano(uln302_receive): source quorum from DVN reference input; keep confirmations from Receive config; maintain Endpoint.Verify atomicity --- .../aiken/validators/uln302_receive.ak | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 078e9df..6b39cf0 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -205,6 +205,39 @@ fn find_dvn_signers(inputs: List) -> Option> { }, ) } +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 _f_vid = builtin.head_list(fs) + 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( @@ -223,7 +256,7 @@ validator uln302_receive { } Verify { packet_header: ph, payload_hash: plh, confirmations: confs } -> { when datum is { - Some(ReceiveDatum { local_eid: le, dvn_vid: _vid, required_quorum: q, required_confirmations: req_c }) -> { + 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) @@ -238,9 +271,14 @@ validator uln302_receive { let unique = dvn_helpers.unique_signers(pubs) when find_dvn_signers(tx.reference_inputs) is { Some(set) -> { - 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 + 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 } From 9e52d300b8a12066d41244da5390354eaf9f8c97 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:52:40 +0000 Subject: [PATCH 65/72] cardano(docs): document DVN quorum via reference input, ULN Send fee routing, and Executor/Endpoint atomicity --- src/cardano/contracts/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md index f83d26a..a8c2bba 100644 --- a/src/cardano/contracts/README.md +++ b/src/cardano/contracts/README.md @@ -78,6 +78,24 @@ Data types and codecs - Codec: see aiken/lib/layerzero/codec.ak for Packet V1 header encoder, payload hashing, and helpers. - Header v1 is byte-for-byte compatible with EVM’s PacketV1Codec.sol. +Updated semantics (aligned with EVM/altVM) +- ULN302 Receive + - Quorum is sourced from the DVN datum via a reference input (alongside the signer set). Confirmations are enforced from the Receive config datum. + - Requires both AttestationDatum (header_hash, payload_hash) and a SigsDatum with signatures and pubs that are unique and members of the DVN signer-set. + - Requires an atomic Endpoint.Verify output for the same (receiver, prev_nonce) in the same transaction. + +- ULN302 Send + - Requires an OutboundDatum inline datum with header length 81 and options length matching the redeemer’s options. + - Requires fee outputs to both configured recipients (executor_fee_vkh and dvn_fee_vkh) from the Send config datum. + +- Executor (CommitAndExecute) + - Requires exactly one Endpoint channel input at prev nonce with executed=True and the corresponding output at current nonce with executed=False (atomic LzReceive transition). + - Requires a fee output to the configured executor_fee_vkh and covers native_drops with total lovelace in outputs. + +- DVN + - Stores signer set and quorum in DvnDatum with an admin VKH for updates. + - Multisig uses Ed25519 verification on message = blake2b_256(header_hash || payload_hash) with unique signers and membership enforced. + Notes - DVN signatures use Ed25519 multisig with configurable quorum; mapping to Cardano signatures will be enforced via redeemer signatures and datum state. - Per-(origin, receiver) channel UTXOs maximize concurrency and avoid global bottlenecks. From d550f0b7e0d92ac782ea30673679888c93bea15f Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 13:57:54 +0000 Subject: [PATCH 66/72] cardano(tests): update ULN302 Send tests for VKH fee recipients; adjust Executor tests for fee output and endpoint IO atomicity --- .../aiken/tests/executor_commit.tests.ak | 41 +++++++++++++------ .../aiken/tests/uln302_send.tests.ak | 18 ++++---- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak index a655308..0760d0d 100644 --- a/src/cardano/contracts/aiken/tests/executor_commit.tests.ak +++ b/src/cardano/contracts/aiken/tests/executor_commit.tests.ak @@ -31,16 +31,16 @@ fn mk_out(lovelace: Int) -> transaction.Output { } } -test executor_commit_and_execute_accepts_with_empty_native_drops() { - let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) - let redeemer = executor.ExecutorRedeemer.CommitAndExecute { - receive_lib: #"", - lz_receive_param: mk_lz_receive_param(), - native_drops: [], +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, } - let tx = transaction.placeholder +} -fn mk_endpoint_channel_input(prev_nonce: Int) -> transaction.Output { +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)])) @@ -49,12 +49,13 @@ fn mk_endpoint_channel_input(prev_nonce: Int) -> transaction.Output { 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 { + 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 { @@ -73,6 +74,18 @@ fn mk_endpoint_channel_output(curr_nonce: Int) -> transaction.Output { 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, @@ -82,7 +95,8 @@ fn mk_endpoint_channel_output(curr_nonce: Int) -> transaction.Output { } test executor_commit_and_execute_accepts_with_some_native_drops() { - let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) + 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 }, @@ -95,7 +109,7 @@ test executor_commit_and_execute_accepts_with_some_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, outputs: [mk_out(30), ep_in, ep_out] } + transaction.Transaction { ..transaction.placeholder, inputs: [ep_in], outputs: [mk_out(30), mk_executor_fee_out(fee_vkh), ep_out] } expect executor.spend( datum, @@ -106,7 +120,8 @@ test executor_commit_and_execute_accepts_with_some_native_drops() { } test executor_commit_and_execute_rejects_when_insufficient_lovelace_for_drops() { - let datum = Some(executor.ExecutorDatum { local_eid: types.Eid(200) }) + 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 }, @@ -119,7 +134,7 @@ test executor_commit_and_execute_rejects_when_insufficient_lovelace_for_drops() let ep_in = mk_endpoint_channel_input(1 - 1) let ep_out = mk_endpoint_channel_output(1) let tx = - transaction.Transaction { ..transaction.placeholder, outputs: [mk_out(20), ep_in, ep_out] } + transaction.Transaction { ..transaction.placeholder, inputs: [ep_in], outputs: [mk_out(20), mk_executor_fee_out(fee_vkh), ep_out] } expect executor.spend( datum, diff --git a/src/cardano/contracts/aiken/tests/uln302_send.tests.ak b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak index 6f27391..a7045a3 100644 --- a/src/cardano/contracts/aiken/tests/uln302_send.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_send.tests.ak @@ -6,9 +6,9 @@ use cardano/assets use layerzero/types use layerzero/validators/uln302_send -fn mk_fee_out(addr_bytes: ByteArray) -> transaction.Output { +fn mk_fee_out(vkh: ByteArray) -> transaction.Output { transaction.Output { - address: address.from_verification_key(addr_bytes), + address: address.from_verification_key(vkh), value: assets.from_lovelace(1), datum: transaction.Datum.NoDatum, reference_script: None, @@ -16,15 +16,15 @@ fn mk_fee_out(addr_bytes: ByteArray) -> transaction.Output { } test uln302_send_requires_fee_outputs_to_executor_and_dvn() { - let efa = types.Address32(#"01") - let dfa = types.Address32(#"02") + let ex_vkh = #"01" + let dvn_vkh = #"02" let datum = Some(uln302_send.SendDatum { oapp: types.Address32(#"00"), dst_eid: types.Eid(200), - executor_fee_addr: efa, - dvn_fee_addr: dfa, 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)])) @@ -36,7 +36,7 @@ test uln302_send_requires_fee_outputs_to_executor_and_dvn() { } let tx = transaction.Transaction { ..transaction.placeholder, - outputs: [out_ob, mk_fee_out(#"01"), mk_fee_out(#"02")], + 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 @@ -78,6 +78,8 @@ test uln302_send_accepts_when_outbound_present_and_options_len_matches() { 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 = [ @@ -108,6 +110,8 @@ test uln302_send_rejects_when_header_wrong_length_or_options_mismatch() { 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 = [ From cacbaab83f867ae1af079a5ef407d1c743a92b44 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:29:09 +0000 Subject: [PATCH 67/72] cardano(tests): ULN302 Receive positive-path with real Ed25519 signature (quorum=1) using SigsDatum + DVN ref input --- .../aiken/tests/uln302_receive.tests.ak | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak index d8918da..438ee35 100644 --- a/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak +++ b/src/cardano/contracts/aiken/tests/uln302_receive.tests.ak @@ -1,4 +1,6 @@ use aiken/builtin +use aiken/collection/list +use aiken/primitive/bytearray use cardano/address use cardano/assets use cardano/transaction @@ -122,3 +124,224 @@ test uln302_verify_rejects_without_endpoint_channel_output() { 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 +} From a245754c2fb05cebaa59daad60345670bf5debeb Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:29:09 +0000 Subject: [PATCH 68/72] cardano(docs): document SigsDatum schema and ULN302 Receive/DVN verification semantics --- src/cardano/contracts/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md index a8c2bba..f91333a 100644 --- a/src/cardano/contracts/README.md +++ b/src/cardano/contracts/README.md @@ -111,3 +111,23 @@ Next steps Security and compatibility - No keys or secrets are embedded in code; off-chain components sign and submit. - Packet header layout and payload hashing are aligned with EVM to preserve cross-chain compatibility. +Updates + +- SigsDatum (used by DVN and ULN302 Receive) + - constructor index 1 with fields: + - header_hash: ByteArray + - payload_hash: ByteArray + - sigs: List (each 64-byte Ed25519 signature) + - pubs: List (each 32-byte Ed25519 public key) + - Message to sign: msg = blake2b_256(header) || payload_hash + - header is the raw PacketV1 header bytes (81 bytes) + - payload_hash is computed via codec.payload_hash(guid, message) +- ULN302 Receive.Verify now requires: + - AttestationDatum (constructor 0) with matching header_hash and payload_hash + - SigsDatum (constructor 1) with signatures over msg + - DVN config provided as a reference input containing signer set and quorum + - Endpoint.Verify atomic output for (receiver, prev_nonce), and confirmations >= required_confirmations +- ULN302 Send: + - Fee outputs must go to executor_fee_vkh and dvn_fee_vkh +- Executor: + - Enforces Endpoint IO atomicity (prev input executed=True at prev nonce; curr output executed=False at current nonce) and an executor fee output to executor_fee_vkh From a2e4f821c52c21e14766b9c4a93ec91fb7fa740a Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:29:09 +0000 Subject: [PATCH 69/72] cardano(tests): DVN admin flows (SetSignerSet, SetQuorum) and quorum helpers --- .../contracts/aiken/tests/dvn.tests.ak | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/cardano/contracts/aiken/tests/dvn.tests.ak b/src/cardano/contracts/aiken/tests/dvn.tests.ak index bb3bad3..a9922ae 100644 --- a/src/cardano/contracts/aiken/tests/dvn.tests.ak +++ b/src/cardano/contracts/aiken/tests/dvn.tests.ak @@ -1,5 +1,42 @@ +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 } @@ -11,3 +48,54 @@ test quorum_satisfied_over() { 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 +} From 45e82801238b0027d477623498cb14418a16aa99 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:29:09 +0000 Subject: [PATCH 70/72] cardano(cleanup): minor validator tidy and warnings reduction (no logic changes) --- src/cardano/contracts/aiken/validators/executor.ak | 14 -------------- .../contracts/aiken/validators/uln302_send.ak | 10 ---------- 2 files changed, 24 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/executor.ak b/src/cardano/contracts/aiken/validators/executor.ak index 3cae8bb..4267f47 100644 --- a/src/cardano/contracts/aiken/validators/executor.ak +++ b/src/cardano/contracts/aiken/validators/executor.ak @@ -113,20 +113,6 @@ fn has_endpoint_prev_input(prev_nonce: types.Nonce, executed_flag: Bool, inputs: -fn has_endpoint_prev_output(prev_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 == prev_nonce && executed == executed_flag - None -> False - } - } - _ -> False - } - }) -} fn has_endpoint_curr_output(curr_nonce: types.Nonce, executed_flag: Bool, outputs: List) -> Bool { outputs diff --git a/src/cardano/contracts/aiken/validators/uln302_send.ak b/src/cardano/contracts/aiken/validators/uln302_send.ak index dd0d00a..094d2ec 100644 --- a/src/cardano/contracts/aiken/validators/uln302_send.ak +++ b/src/cardano/contracts/aiken/validators/uln302_send.ak @@ -47,16 +47,6 @@ fn has_fee_outputs(outputs: List, ex_vkh: VerificationKeyHash, dvn_vkh: has_ex && has_dvn } -fn count_fee_recipients(outputs: List) -> Int { - list.foldr( - outputs, - 0, - fn(o, acc) { - let lov = assets.lovelace_of(o.value) - acc + if lov > 0 { 1 } else { 0 } - }, - ) -} fn parse_outbound(d: Data) -> Option<(ByteArray, ByteArray, Int)> { let is_constr = builtin.choose_data(d, True, False, False, False, False) From d34227bf6aabecae9d6ebc21f73527eb0db349f8 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:32:19 +0000 Subject: [PATCH 71/72] cardano(aiken): remove discarded let-bindings in ULN302 Receive parsers to silence warnings (no logic change) --- src/cardano/contracts/aiken/validators/uln302_receive.ak | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/cardano/contracts/aiken/validators/uln302_receive.ak b/src/cardano/contracts/aiken/validators/uln302_receive.ak index 6b39cf0..e02b129 100644 --- a/src/cardano/contracts/aiken/validators/uln302_receive.ak +++ b/src/cardano/contracts/aiken/validators/uln302_receive.ak @@ -75,7 +75,6 @@ fn parse_endpoint_channel(d: Data) -> Option<(types.Address20, types.Nonce, Bool 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) @@ -169,13 +168,9 @@ fn parse_dvn_signers(d: Data) -> Option> { if is_constr { let pr = builtin.un_constr_data(d) let fs = pr.2nd - let _f_vid = builtin.head_list(fs) let fs1 = builtin.tail_list(fs) - let _f_quorum = builtin.head_list(fs1) let fs2 = builtin.tail_list(fs1) let f_signers = builtin.head_list(fs2) - let fs3 = builtin.tail_list(fs2) - let _f_admin = builtin.head_list(fs3) let signers_data = builtin.un_list_data(f_signers) let signers = list.map(signers_data, fn(x) { let prx = builtin.un_constr_data(x) @@ -210,7 +205,6 @@ fn parse_dvn_quorum(d: Data) -> Option { if is_constr { let pr = builtin.un_constr_data(d) let fs = pr.2nd - let _f_vid = builtin.head_list(fs) let fs1 = builtin.tail_list(fs) let f_quorum = builtin.head_list(fs1) let q = builtin.un_i_data(f_quorum) From 110128efabcdbaf2b484f583710ab76d94c33570 Mon Sep 17 00:00:00 2001 From: Til Jordan Date: Sat, 23 Aug 2025 15:39:21 +0000 Subject: [PATCH 72/72] docs(cardano/aiken): concise README with structure and how to run tests --- src/cardano/contracts/README.md | 181 +++++++++----------------------- 1 file changed, 50 insertions(+), 131 deletions(-) diff --git a/src/cardano/contracts/README.md b/src/cardano/contracts/README.md index f91333a..1085f93 100644 --- a/src/cardano/contracts/README.md +++ b/src/cardano/contracts/README.md @@ -1,133 +1,52 @@ -# Cardano LayerZero Contracts (Aiken) - -This directory contains the Cardano eUTXO adaptations of LayerZero V2 core contracts implemented in Aiken. - -Structure: -- aiken/ Aiken project root - - aiken.toml Project config - - validators/ On-chain validators (scripts) - - lib/ Shared libraries (types, codecs, utils) - - tests/ Aiken tests - -Target modules: -- Endpoint: endpoint state, verification, and execution state machine -- ULN302 Send/Receive: message libraries and config handling -- Executor: commit-and-execute flow bridging verification to endpoint execution -- DVN: quorum-based verification and job assignment -- Shared: packet/header codecs, types, and constants - -This is an initial scaffold to establish the project layout and shared types/codec. Subsequent commits will add full validator logic for Endpoint, ULN302, Executor, and DVN. - -# LayerZero V2 on Cardano (Aiken) - -This folder contains the Cardano (Aiken) implementation of the core LayerZero V2 components, adapted to the eUTXO model: -- Endpoint -- ULN302 MessageLib (Send, Receive) -- Executor -- DVN - -Status -- Scaffold and shared modules exist and compile (aiken check). -- Packet V1 header codec matches EVM fixed-width layout: version(1) + nonce(u64=8) + srcEid(u32=4) + sender(bytes32) + dstEid(u32=4) + receiver(bytes32 padded from address20) => 81 bytes. -- Tests cover: codec hashing/layout/parse, ULN302 Receive header+attestation gating, DVN Execute quorum, Endpoint nonce/executable gating, and ULN302 Send outbound instruction validation. - -Architecture overview -- State is modeled via UTXOs: - - Endpoint Global UTXO: holds localEid and platform-level settings. - - Per-Channel UTXOs: one per (origin, receiver) tracking inbound nonce and execution flags for concurrency. - - ULN302 Config UTXOs: per-oapp per-dstEid configs for ULN and Executor, set via SetConfig calls. - - DVN State UTXO: signer set, quorum, and replay map anchoring. - - Verification/Attestation UTXOs: created by DVN/ReceiveLib to enable execution by Executor. -- Off-chain roles: - - OApp sends messages via ULN302 Send, producing an outbound instruction UTXO and fee outputs. - - DVN watchers pick up jobs, attest and produce verification UTXOs based on quorum. - - Executor watchers commit-and-execute messages once Endpoint and ULN verification conditions are satisfied. -- Fees: - - Modeled as lovelace on specific outputs; precise distribution to DVN/Executor is performed by off-chain submitters. - -UTXO schemas (InlineDatum) -- AttestationDatum (used by DVN and ReceiveUln302 tests): - - constructor index 0 with fields: - - header_hash: ByteArray - - payload_hash: ByteArray - - signer_count: Int -- OutboundDatum (used by ULN302 Send tests): - - constructor index 0 with fields: - - header: ByteArray (must be 81 bytes for PacketV1) - - payload: ByteArray - - options_len: Int - -Core flows -1) Send (ULN302 Send) - - Input: OApp + options - - Output: Outbound instruction UTXO with serialized header + payload, along with fee outputs for DVN/Executor workers. - - Config: Uses ExecutorConfig and UlnConfig resolved per oapp/dstEid. Options are split similar to EVM. - - Validation (current): presence of OutboundDatum with header length 81 and options_len matching the redeemer’s options length. - -2) Verify (ULN302 Receive + Endpoint) - - ReceiveUln302.assertHeader(header, localEid) checks version and dstEid. - - DVN quorum attests to (headerHash, payloadHash), producing verification UTXOs. - - Endpoint Verify consumes verification UTXO to mark channel nonce as verifiable/executable for the payload hash. - -3) Execute (Executor) - - If executable, consume verification and call Endpoint.lzReceive to finalize execution and update nonce state. - - Emits native drops as specified in options by constructing appropriate outputs. - -Data types and codecs -- Types: see aiken/lib/layerzero/types.ak for Eid, Address20/32, Nonce, Origin, Guid, configs, AttestationDatum, OutboundDatum. -- Codec: see aiken/lib/layerzero/codec.ak for Packet V1 header encoder, payload hashing, and helpers. -- Header v1 is byte-for-byte compatible with EVM’s PacketV1Codec.sol. - -Updated semantics (aligned with EVM/altVM) -- ULN302 Receive - - Quorum is sourced from the DVN datum via a reference input (alongside the signer set). Confirmations are enforced from the Receive config datum. - - Requires both AttestationDatum (header_hash, payload_hash) and a SigsDatum with signatures and pubs that are unique and members of the DVN signer-set. - - Requires an atomic Endpoint.Verify output for the same (receiver, prev_nonce) in the same transaction. - -- ULN302 Send - - Requires an OutboundDatum inline datum with header length 81 and options length matching the redeemer’s options. - - Requires fee outputs to both configured recipients (executor_fee_vkh and dvn_fee_vkh) from the Send config datum. - -- Executor (CommitAndExecute) - - Requires exactly one Endpoint channel input at prev nonce with executed=True and the corresponding output at current nonce with executed=False (atomic LzReceive transition). - - Requires a fee output to the configured executor_fee_vkh and covers native_drops with total lovelace in outputs. - -- DVN - - Stores signer set and quorum in DvnDatum with an admin VKH for updates. - - Multisig uses Ed25519 verification on message = blake2b_256(header_hash || payload_hash) with unique signers and membership enforced. +# 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 -- DVN signatures use Ed25519 multisig with configurable quorum; mapping to Cardano signatures will be enforced via redeemer signatures and datum state. -- Per-(origin, receiver) channel UTXOs maximize concurrency and avoid global bottlenecks. -- Endpoint and Receive library “view” functions are modeled by datum-driven states that off-chain components can query. - -Next steps -- Flesh out Endpoint validator to update per-channel nonce and executable state. -- Implement ULN302 Send/Receive config set/get and verification quorum checks. -- Implement DVN signer management, job assignment, and attestation UTXO creation. -- Implement Executor commit-and-execute state transitions and native drops output shaping. -- Extend tests: DVN quorum satisfied/not, endpoint nonce monotonicity, and end-to-end header->verify gating. - -Security and compatibility -- No keys or secrets are embedded in code; off-chain components sign and submit. -- Packet header layout and payload hashing are aligned with EVM to preserve cross-chain compatibility. -Updates - -- SigsDatum (used by DVN and ULN302 Receive) - - constructor index 1 with fields: - - header_hash: ByteArray - - payload_hash: ByteArray - - sigs: List (each 64-byte Ed25519 signature) - - pubs: List (each 32-byte Ed25519 public key) - - Message to sign: msg = blake2b_256(header) || payload_hash - - header is the raw PacketV1 header bytes (81 bytes) - - payload_hash is computed via codec.payload_hash(guid, message) -- ULN302 Receive.Verify now requires: - - AttestationDatum (constructor 0) with matching header_hash and payload_hash - - SigsDatum (constructor 1) with signatures over msg - - DVN config provided as a reference input containing signer set and quorum - - Endpoint.Verify atomic output for (receiver, prev_nonce), and confirmations >= required_confirmations -- ULN302 Send: - - Fee outputs must go to executor_fee_vkh and dvn_fee_vkh -- Executor: - - Enforces Endpoint IO atomicity (prev input executed=True at prev nonce; curr output executed=False at current nonce) and an executor fee output to executor_fee_vkh +- No secrets are embedded; tests use deterministic vectors. +- Behavior mirrors LayerZero V2 EVM/altVM where applicable, adapted to Cardano’s eUTXO model.