Skip to content

Cardano: Aiken contract scaffold (Endpoint, ULN302, Executor, DVN)#16

Open
tiljrd wants to merge 72 commits into
devfrom
devin/1755949843-cardano-aiken-scaffold
Open

Cardano: Aiken contract scaffold (Endpoint, ULN302, Executor, DVN)#16
tiljrd wants to merge 72 commits into
devfrom
devin/1755949843-cardano-aiken-scaffold

Conversation

@tiljrd

@tiljrd tiljrd commented Aug 23, 2025

Copy link
Copy Markdown
Collaborator

User description

Cardano: Aiken contract scaffold (Endpoint, ULN302, Executor, DVN)

Summary

This PR implements a complete scaffolding of LayerZero V2 contracts for Cardano using Aiken, adapting the EVM protocol to work with Cardano's eUTXO model. The implementation includes:

  • Core Protocol Types: Comprehensive type definitions for cross-chain messaging (Eid, Nonce, packet headers, etc.)
  • Codec Layer: Header encoding/decoding functions that maintain exact compatibility with EVM LayerZero packet format
  • Contract Validators: Aiken implementations of all core LayerZero contracts:
    • Endpoint: Main entry point with per-(origin, receiver) channel UTXOs for nonce tracking
    • ULN302 Send/Receive: Message library for outbound instructions and DVN verification
    • DVN: Decentralized verification with Ed25519 multisig quorum support
    • Executor: Commit-and-execute pattern for message execution
  • Helper Libraries: State management utilities and business logic helpers
  • Comprehensive Tests: Unit tests covering encoding, parsing, and validator logic
  • Documentation: Updated README with eUTXO architecture and transaction flows

⚠️ Important: This is scaffolding code with placeholder implementations. Many validators contain stub logic and are not production-ready.

Review & Testing Checklist for Human

  • Protocol Compliance: Verify packet header encoding matches EVM LayerZero exactly (81-byte layout, field offsets) by comparing with EVM reference
  • eUTXO State Design: Review the per-channel UTXO approach for nonce tracking and executable state - ensure it makes sense for Cardano
  • Data Parsing Logic: Test the attestation parsing in DVN/ULN302 Receive - the builtin Data destructuring was complex to implement correctly
  • Placeholder Logic: Confirm that stub implementations (fees, config validation, admin operations) are appropriate for this scaffolding phase
  • Test Coverage: Run aiken check and execute tests to ensure all validators compile and basic functionality works

Recommended Test Plan:

  1. Compile project with aiken check (should pass with 0 errors)
  2. Run codec round-trip tests to verify EVM compatibility
  3. Test DVN attestation parsing with sample Data
  4. Review README examples match actual validator interfaces

Diagram

%%{ init : { "theme" : "default" }}%%
graph TD
    types["lib/layerzero/types.ak<br/>Core Protocol Types"]:::major-edit
    codec["lib/layerzero/codec.ak<br/>Header Encode/Decode"]:::major-edit
    constants["lib/layerzero/constants.ak<br/>Protocol Constants"]:::major-edit
    
    endpoint["validators/endpoint.ak<br/>Main Entry Point"]:::major-edit
    uln_send["validators/uln302_send.ak<br/>Outbound Messages"]:::major-edit
    uln_recv["validators/uln302_receive.ak<br/>Inbound Verification"]:::major-edit
    dvn["validators/dvn.ak<br/>Attestation Network"]:::major-edit
    executor["validators/executor.ak<br/>Message Execution"]:::major-edit
    
    helpers_ep["lib/layerzero/endpoint_helpers.ak<br/>Nonce & State Utils"]:::minor-edit
    helpers_dvn["lib/layerzero/dvn_helpers.ak<br/>Quorum Logic"]:::minor-edit
    
    tests["tests/*.tests.ak<br/>Unit Test Suite"]:::major-edit
    readme["README.md<br/>Architecture Docs"]:::minor-edit
    
    types --> endpoint
    types --> uln_send
    types --> uln_recv
    types --> dvn
    types --> executor
    
    codec --> uln_send
    codec --> uln_recv
    codec --> tests
    
    constants --> codec
    
    helpers_ep --> endpoint
    helpers_dvn --> dvn
    
    dvn --> uln_recv
    uln_recv --> executor
    
    subgraph Legend
        L1["Major Edit"]:::major-edit
        L2["Minor Edit"]:::minor-edit
        L3["Context/No Edit"]:::context
    end

classDef major-edit fill:#90EE90
classDef minor-edit fill:#87CEEB  
classDef context fill:#FFFFFF
Loading

Notes

  • Session: Requested by Til Jordan (@tiljrd) in session 9722fb63f04c454783724a647de425ff
  • Architecture Decision: Uses per-(origin, receiver) channel UTXOs for state tracking instead of global contract state
  • Ed25519 Multisig: DVN implements configurable quorum signatures as requested
  • EVM Parity: Header format maintains exact 81-byte layout for cross-chain compatibility
  • Next Steps: This scaffolding needs business logic implementation, fee handling, and integration testing before production use

PR Type

Enhancement


Description

  • Complete Aiken-based LayerZero V2 implementation for Cardano

  • Core validators: Endpoint, ULN302, Executor, DVN with eUTXO adaptation

  • Packet header codec maintaining EVM compatibility

  • Comprehensive test suite with multisig verification


Diagram Walkthrough

flowchart LR
  types["Core Types"] --> validators["Validators"]
  codec["Header Codec"] --> validators
  validators --> endpoint["Endpoint"]
  validators --> uln["ULN302 Send/Receive"]
  validators --> executor["Executor"]
  validators --> dvn["DVN"]
  helpers["Helper Libraries"] --> validators
  tests["Test Suite"] --> validators
Loading

File Walkthrough

Relevant files
Documentation
1 files
README.md
Add comprehensive project documentation                                   
+52/-0   
Configuration changes
1 files
aiken.toml
Initialize Aiken project configuration                                     
+10/-0   
Enhancement
10 files
types.ak
Define core LayerZero protocol types                                         
+90/-0   
codec.ak
Implement packet header encoding/decoding functions           
+133/-0 
constants.ak
Add protocol constants                                                                     
+2/-0     
endpoint_helpers.ak
Add endpoint state management utilities                                   
+38/-0   
dvn_helpers.ak
Add DVN multisig and quorum logic                                               
+62/-0   
endpoint.ak
Implement main endpoint validator                                               
+82/-0   
uln302_send.ak
Implement outbound message validator                                         
+110/-0 
uln302_receive.ak
Implement inbound verification validator                                 
+292/-0 
executor.ak
Implement commit-and-execute validator                                     
+180/-0 
dvn.ak
Implement decentralized verification network validator     
+186/-0 
Tests
9 files
codec.tests.ak
Add codec round-trip tests                                                             
+142/-0 
endpoint.tests.ak
Add endpoint helper function tests                                             
+44/-0   
endpoint_validator.tests.ak
Add endpoint validator tests                                                         
+148/-0 
uln302_send.tests.ak
Add ULN302 send validator tests                                                   
+130/-0 
uln302_receive.tests.ak
Add ULN302 receive validator tests                                             
+347/-0 
uln302_receive_commit.tests.ak
Add ULN302 commit verification tests                                         
+57/-0   
executor_commit.tests.ak
Add executor commit-and-execute tests                                       
+145/-0 
dvn.tests.ak
Add DVN admin operation tests                                                       
+101/-0 
dvn_execute.tests.ak
Add DVN execution and multisig tests                                         
+122/-0 

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

tiljrd added 21 commits August 23, 2025 12:01
…spend signature) and import cardano/transaction
…s; replace concat_list with nested bytearray.concat
…ire into uln302_receive CommitVerification/Verify; add tests for header assertion
tiljrd added 25 commits August 23, 2025 13:03
…m schemas; note new ULN302 Send/Receive and DVN tests
…velace; add negative test for insufficient funds
…fold; fix lambda body; keep signers_within_set clean
…ace list.find_map with fold; enforce atomic Endpoint.Verify; fix pattern matches
…d in find_sigs; tidy braces; compile cleanly
… at prev nonce) and curr output (executed=False at current); enforce atomicity with Endpoint.LzReceive
…(uln302_send): enforce fee outputs to configured VKHs
… confirmations from Receive config; maintain Endpoint.Verify atomicity
…st Executor tests for fee output and endpoint IO atomicity
…ure (quorum=1) using SigsDatum + DVN ref input
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 Security concerns

Input Data validation:
Several validators parse inline Data without fully validating structure (constructor tags, list lengths), which could allow crafted Data to bypass checks or cause unexpected behavior. For example, parse_attestation and find_sigs in both dvn.ak and uln302_receive.ak assume list heads/tails exist. Add explicit tag checks and length guards before accessing fields.

⚡ Recommended focus areas for review

Duplicate Import

The file imports aiken/builtin twice and also redefines hash_header with the same logic as header_hash_v1. Consider removing the duplicate import and consolidating hash helpers to avoid confusion.

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)
}
Attestation Parsing Robustness

Parsing functions assume constructor tags/field counts without validating tags for attestation and sigs; malformed Data could slip through. Consider checking constructor tag and list length explicitly before indexing.

fn parse_attestation(d: Data) -> Option<(ByteArray, ByteArray, Int)> {
  let is_constr = builtin.choose_data(d, True, False, False, False, False)
  if is_constr {
    let pr = builtin.un_constr_data(d)
    let fs = pr.2nd
    let f0 = builtin.head_list(fs)
    let fs1 = builtin.tail_list(fs)
    let f1 = builtin.head_list(fs1)
    let fs2 = builtin.tail_list(fs1)
    let f2 = builtin.head_list(fs2)
    let hh = builtin.un_b_data(f0)
    let ph = builtin.un_b_data(f1)
    let sc = builtin.un_i_data(f2)
    Some((hh, ph, sc))
  } else {
    None
  }
}

fn has_matching_attestation(outputs: List<Output>, hh: ByteArray, ph: ByteArray) -> Bool {
  outputs
    |> list.any(fn(o) {
      when o.datum is {
        Datum.InlineDatum(d) -> {
          when parse_attestation(d) is {
            Some((ah, ap, _)) -> ah == hh && ap == ph
            None -> False
          }
        }
        _ -> False
      }
    })
}

fn parse_endpoint_channel(d: Data) -> Option<(types.Address20, types.Nonce, Bool)> {
  let is_constr = builtin.choose_data(d, True, False, False, False, False)
  if is_constr {
    let pr = builtin.un_constr_data(d)
    let tag = pr.1st
    if tag == 1 {
      let fs = pr.2nd
      let fs1 = builtin.tail_list(fs)
      let f_receiver = builtin.head_list(fs1)
      let fs2 = builtin.tail_list(fs1)
      let f_nonce = builtin.head_list(fs2)
      let fs3 = builtin.tail_list(fs2)
      let f_executed = builtin.head_list(fs3)
      let r_pr = builtin.un_constr_data(f_receiver)
      let r_b = builtin.un_b_data(builtin.head_list(r_pr.2nd))
      let n_pr = builtin.un_constr_data(f_nonce)
      let n_i = builtin.un_i_data(builtin.head_list(n_pr.2nd))
      let e_pr = builtin.un_constr_data(f_executed)
      let executed = e_pr.1st == 1
      Some((types.Address20(r_b), types.Nonce(n_i), executed))
    } else {
      None
    }
  } else {
    None
  }
}

fn has_matching_endpoint_verify_output(
  outputs: List<Output>,
  receiver: types.Address20,
  prev_nonce: types.Nonce,
) -> Bool {
  outputs
    |> list.any(fn(o) {
      when o.datum is {
        Datum.InlineDatum(d) -> {
          when parse_endpoint_channel(d) is {
            Some((rcvr, lazy_nonce, executed)) -> rcvr == receiver && executed && lazy_nonce == prev_nonce
            None -> False
          }
        }
        _ -> False
      }
    })
}

fn find_sigs(outputs: List<Output>, hh: ByteArray, ph: ByteArray) -> Option<(List<ByteArray>, List<types.Address32>)> {
  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
          }
        }
      }
    },
  )
}
Stray Line/Block

There appears to be an extra dangling expectation outside a test block, which may break compilation or test execution. Clean up the stray lines to keep tests valid.

  expect uln302_receive.spend(datum, redeemer, transaction.OutputReference { transaction_id: transaction.placeholder.id, output_index: 0 }, tx) == False
}

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Missing DVN identity binding

The receive path trusts any DVN reference input that structurally looks like a
DVN state (quorum/signers) without binding it to the configured dvn_vid or a
script identity, enabling an attacker to supply a different DVN UTXO with looser
quorum/signers. Introduce explicit DVN identity checks (e.g., reference script
hash or policy plus datum vid == configured dvn_vid) and enforce that the
referenced DVN input matches the contract’s configured DVN instance before
accepting attestations or signatures.

Examples:

src/cardano/contracts/aiken/validators/uln302_receive.ak [266-278]
                when find_dvn_signers(tx.reference_inputs) is {
                  Some(set) -> {
                    when find_dvn_quorum(tx.reference_inputs) is {
                      Some(q) -> {
                        let within = dvn_helpers.signers_within_set(pubs, set)
                        let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q)
                        ok_hdr && ok_att && ok_ep && ok_conf && unique && within && ok_ms
                      }
                      None -> False
                    }

 ... (clipped 3 lines)

Solution Walkthrough:

Before:

// In uln302_receive validator
spend(datum: ReceiveDatum, redeemer: Verify, tx: Transaction) {
  // ...
  let maybe_dvn_signers = find_dvn_signers(tx.reference_inputs)
  let maybe_dvn_quorum = find_dvn_quorum(tx.reference_inputs)

  // The quorum and signers are blindly trusted from ANY reference input
  // that has a datum with the correct structure.
  // There is no check that this is the CORRECT DVN contract.
  when (maybe_dvn_signers, maybe_dvn_quorum) is {
    (Some(signers), Some(quorum)) -> {
      let ok_ms = verify_multisig(message, pubs, sigs, quorum)
      let within_set = signers_within_set(pubs, signers)
      // ... proceed with verification
    }
    _ -> False
  }
}

After:

// In uln302_receive validator
spend(datum: ReceiveDatum, redeemer: Verify, tx: Transaction) {
  // ...
  // 1. Find the DVN reference input that matches the configured DVN identity.
  let maybe_dvn_ref = find_dvn_reference_input(
    tx.reference_inputs,
    // e.g. a known script hash for the DVN validator
    predefined_dvn_script_hash,
  )

  when maybe_dvn_ref is {
    Some(dvn_ref) -> {
      // 2. Parse DVN state and verify its `vid` matches the configured `dvn_vid`.
      let dvn_datum = parse_dvn_datum(dvn_ref.output.datum)
      if dvn_datum.vid != datum.dvn_vid { fail }

      // 3. Use the now-trusted quorum and signers for verification.
      let ok_ms = verify_multisig(message, pubs, sigs, dvn_datum.quorum)
      let within_set = signers_within_set(pubs, dvn_datum.signers)
      // ... proceed with verification
    }
    None -> False // Fail if no valid DVN reference input is found.
  }
}
Suggestion importance[1-10]: 10

__

Why: This suggestion identifies a critical security vulnerability where the DVN's identity is not verified, allowing an attacker to bypass the entire security model by providing a malicious DVN reference input.

High
Security
Enforce VID match per execution

Ensure each ExecuteParam’s vid matches the DVN instance’s vid to prevent
cross-instance attestations from being accepted. Add a guard comparing the
parameter vid to the datum vid before validating signatures.

src/cardano/contracts/aiken/validators/dvn.ak [160-174]

 let ok = ps
   |> list.any(fn(p) {
-    let ExecuteParam { vid: _, header_hash: hdrh, payload_hash: plh, signer_count: _ } = p
-    let has_att = find_attestation(tx.outputs, hdrh, plh)
-    when find_sigs(tx.outputs, hdrh, plh) is {
-      Some((sigs, pubs)) -> {
-        let msg = dvn_helpers.attestation_message(hdrh, plh)
-        let within_set = dvn_helpers.signers_within_set(pubs, ss)
-        let unique = dvn_helpers.unique_signers(pubs)
-        let ok_ms = dvn_helpers.verify_multisig(msg, pubs, sigs, q)
-        has_att && within_set && unique && ok_ms
+    let ExecuteParam { vid: pvid, header_hash: hdrh, payload_hash: plh, signer_count: _ } = p
+    if pvid != vid {
+      False
+    } else {
+      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
       }
-      None -> False
     }
   })
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This suggestion addresses a security vulnerability by adding a missing check to ensure the vid from the redeemer matches the vid in the datum, preventing cross-instance attacks.

High
Possible issue
Fix stray duplicate expect block

Delete the stray duplicated expect ... == False block and the unmatched closing
brace before the next test. This fixes a syntax error that will break
compilation of the test suite.

src/cardano/contracts/aiken/tests/uln302_receive.tests.ak [303-305]

-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() {

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a copy-paste error that introduces a syntax error, which would cause the test suite to fail compilation.

Medium
General
Remove duplicate import

Remove the duplicated use aiken/builtin import to avoid unnecessary repeated
imports and potential lint failures. This is a simple cleanup that prevents
confusion and keeps the module header minimal.

src/cardano/contracts/aiken/lib/layerzero/codec.ak [1-3]

-use aiken/builtin
 use aiken/builtin
 use aiken/primitive/bytearray
  • Apply / Chat
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a duplicate use aiken/builtin import, and removing it improves code style and cleanliness.

Low
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant