Compare commits

...

5 Commits

Author SHA1 Message Date
waalge 62b4aa0523 tidy 2025-02-16 19:05:17 +00:00
waalge 194492234e wip 2025-02-16 11:30:41 +00:00
waalge 712c7cac44 hydra is cool 2023-09-20 13:20:11 +00:00
waalge c3f39d430d tweaks to blog view 2023-09-02 21:07:08 +00:00
waalge b340cfd2f0 tracing aiken build: proofread 2023-09-02 20:05:48 +00:00
62 changed files with 3779 additions and 862 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.direnv/
_site
docs/
_cache
dist

View File

@ -1,16 +1,5 @@
## Commands
# Kompact.io site
recompile css
```sh
tailwindcss -i ./content/css/main.css -o ./content/css/mini.css --minify
```
## Commands
build, serve and watch
```sh
cabal run site -- watch
```
deploy
```sh
rsync -r --delete ./_site/* genesis:/var/www/kompactio-landing/
```
Enter devshell, and run `menu` See flake for details.

108
assets/css/main.css Normal file
View File

@ -0,0 +1,108 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
/* Set in tailwindconfig */
font-family: "jetbrains-mono";
src:
local("jetbrains-mono"),
url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
article {
margin-bottom: 2rem;
}
article > section > :is(pre, p, h1, h2, h3, h4, h5, h6) {
margin-top: 2rem;
}
article > section {
font-family:
"Lucida" Grande,
sans-serif;
}
article > section > :is(h1, h2, h3, h4, h5, h6, code) {
font-family: "jetbrains-mono";
}
article > section > blockquote {
padding: 1rem;
border-left-width: 4px;
border-color: rgb(239 68 68);
font-style: italic;
}
article > section > h1 {
margin-top: 2rem;
font-size: 3rem;
}
article > section > h1::before {
content: "# ";
}
article > section > h2 {
font-size: 2rem;
}
article > section > h2::before {
content: "## ";
}
article > section > h3 {
font-size: 1.5rem;
}
article > section > h3::before {
content: "### ";
}
article > section > h4 {
font-size: 1.3rem;
}
article > section > h4::before {
content: "#### ";
}
article > section {
margin-top: 4rem;
}
article a {
text-decoration-color: rgb(239 68 68);
text-decoration-thickness: 4px;
text-decoration-line: underline;
transition-duration: 70ms;
}
article a:hover {
text-decoration-thickness: 8px;
text-decoration-color: rgb(185 28 28);
text-decoration-line: underline;
}
article ul {
margin-left: 1rem;
list-style-type: "- ";
}
article ol {
margin-left: 1rem;
list-style: decimal inside;
}
#footnotes {
padding-top: 1rem;
}
#footnotes > ol > li {
margin-top: 1rem;
}
#footnotes > ol > li > p {
display: inline;
}

1
assets/css/mini.css Normal file

File diff suppressed because one or more lines are too long

118
assets/css/prism.css Normal file
View File

@ -0,0 +1,118 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+haskell+json+nix+racket+rust+scheme */
code[class*="language-"],
pre[class*="language-"] {
color: #000;
background: 0 0;
text-shadow: 0 1px #fff;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
pre[class*="language-"]::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
code[class*="language-"] ::selection,
code[class*="language-"]::selection,
pre[class*="language-"] ::selection,
pre[class*="language-"]::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog {
color: #708090;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: 0.7;
}
.token.boolean,
.token.constant,
.token.deleted,
.token.number,
.token.property,
.token.symbol,
.token.tag {
color: #905;
}
.token.attr-name,
.token.builtin,
.token.char,
.token.inserted,
.token.selector,
.token.string {
color: #690;
}
.language-css .token.string,
.style .token.string,
.token.entity,
.token.operator,
.token.url {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.class-name,
.token.function {
color: #dd4a68;
}
.token.important,
.token.regex,
.token.variable {
color: #e90;
}
.token.bold,
.token.important {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 672 B

After

Width:  |  Height:  |  Size: 672 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

19
assets/scripts/nav.js Normal file
View File

@ -0,0 +1,19 @@
document.addEventListener("DOMContentLoaded", function () {
// Get all "navbar-burger" elements
var $navbarBurgers = Array.prototype.slice.call(
document.querySelectorAll(".navbar-burger"),
0,
);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach(function ($el) {
$el.addEventListener("click", function () {
// Get the "main-nav" element
var $target = document.getElementById("main-nav");
// Toggle the class on "main-nav"
$target.classList.toggle("hidden");
});
});
}
});

1201
assets/scripts/prism.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
@font-face {
/* Set in tailwindconfig */
font-family: "jetbrains-mono";
src: local("jetbrains-mono"),
src:
local("jetbrains-mono"),
url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@ -13,37 +14,61 @@ article {
margin-bottom: 2rem;
}
article>section> :is(p, h1, h2, h3, h4, h5, h6) {
article > section > :is(pre, p, h1, h2, h3, h4, h5, h6) {
margin-top: 2rem;
}
article>section>h1 {
article > section {
font-family:
"Lucida" Grande,
sans-serif;
}
article > section > :is(h1, h2, h3, h4, h5, h6, code) {
font-family: "jetbrains-mono";
}
article > section > blockquote {
padding: 1rem;
border-left-width: 4px;
border-color: rgb(239 68 68);
font-style: italic;
}
article > section > h1 {
margin-top: 2rem;
font-size: 3rem;
}
article>section>h1::before {
article > section > h1::before {
content: "# ";
}
article>section>h2 {
margin-top: 2rem;
article > section > h2 {
font-size: 2rem;
}
article>section>h2::before {
article > section > h2::before {
content: "## ";
}
article>section>h3::before {
article > section > h3 {
font-size: 1.5rem;
}
article > section > h3::before {
content: "### ";
}
article>section>h4::before {
article > section > h4 {
font-size: 1.3rem;
}
article > section > h4::before {
content: "#### ";
}
article>section {
article > section {
margin-top: 4rem;
}
@ -61,6 +86,23 @@ article a:hover {
}
article ul {
margin-left: 2rem;
list-style-type: " ";
}
margin-left: 1rem;
list-style-type: "- ";
}
article ol {
margin-left: 1rem;
list-style: decimal inside;
}
#footnotes {
padding-top: 1rem;
}
#footnotes > ol > li {
margin-top: 1rem;
}
#footnotes > ol > li > p {
display: inline;
}

File diff suppressed because one or more lines are too long

View File

@ -1,275 +0,0 @@
Aims:
> Describe the pipeline and components getting from Aiken to Uplc.
## The Preface
### Motivations
The motivation for writing this came from a desire to add additional features to Aiken not yet available.
One such feature would evaluate an arbitrary function in Aiken callable from JavaScript.
This would help a lot with testing trying to align on and off-chain code.
Another more pipe dreamy, ad-hoc function extraction - from a span of code, generate a function.
A digression to answer _why would this be at all helpful?!_
Validator logic often needs a broad context throughout.
How then to best factor code?
Possible solutions:
1. Introduce types / structs
2. Have functions with lots of arguments
3. Don't
The problems are:
1. Requires relentless constructing and deconstructing across the function call.
And this is adds costs in Aiken.
2. Becomes tedious aligning the definition and function call.
3. End up with very long validators which are hard to unit test.
My current preferred way is to accept that validator functions are long.
Ad-hoc function extraction would allow for sections of code to be tested without needing to be factored out.
To do either of these, we need to get to grips with the Aiken compilation pipeline.
### This won't age well
Aiken is undergoing active development.
This post was started life with Aiken ~v1.14.
With Aiken v1.15, there were already reasonably significant changes to the compilation pipeline.
The word is that there aren't as big changes in the near future,
but this article will undoubtedly begin to diverge from the current code-base even before publishing.
### Limitations of narrating code
Narrating code becomes a compromise between being honest and accurate, and being readable and digestible.
Following the command `aiken build` covers well in excess of 10,000 LoC.
The writing of this post ground slowly to a halt as it progressed deeper into the code
with the details seeming to increase in importance.
At some point I had to draw a line and resign to fact that some parts will remain black boxes for now.
## Aiken build
Tracing `aiken build`, the pipeline is roughly:
```
. -> Project::read_source_files ->
Vec<Source> -> Project::parse_sources ->
ParsedModules -> Project::type_check ->
CheckedModules -> CodeGenerator::build ->
AirTree -> AirTree::to_vec ->
Vec<Air> -> CodeGenerator::uplc_code_gen ->
Program / Term<Name> -> serialize ->
.
```
We'll pick our way through these steps
At a high level we are trying to do something straightforward: reformulate Aiken code as Uplc.
Some Aiken expressions are relatively easy to handle for example an Aiken `Int` goes to an `Int` in Uplc.
Some Aiken expressions require more involved handling, for example an Aiken `If... If Else... Else `
must have the branches "nested" in Uplc.
Aiken also have lots of nice-to-haves like pattern matching, modules, and generics.
Uplc has none of these.
### The Preamble
#### Cli handling
The cli enters at `aiken/src/cmd/mod.rs` which parses the command.
With some establishing of context, the program enters `Project::build` (`crates/aiken-project/src/lib.rs`),
which in turn calls `Project::compile`.
#### File crawl
The program looks for Aiken files in both `./lib` and `./validator` sub-directories.
For each it walks over all contents (recursively) looking for `.ak` extensions.
It treats these two sets of files a little differently.
For example, only validator files can contain the special validator functions.
#### Parse and Type check
`Project::parse_sources` parses the module source code.
The heavy lifting is done by `aiken_lang::parser::module`, which is evaluated on each file.
It produces a `Module` containing a list of parsed definitions of the file: functions, types _etc_,
together with metadata like docstrings and the file path.
`Project::type_check` inspects the parsed modules and, as the name implies, checks the types.
It flags type level warnings and errors and constructs a hash map of `CheckedModule`s.
#### Code generator
The code generator `CodeGenerator` (`aiken-lang/src/gen_uplc.rs`) is given
the definitions found from the previous step,
together with the plutus builtins.
It has additional fields for things like debugging.
This is handed over to a `Blueprint` (`aiken-project/src/blueprint/mod.rs`).
The blueprint does little more than find the validators on which to run the code gen.
The heavy lifting is done by `CodeGenerator::generate`.
We are now ready to take the source code and create plutus.
### In the air
Things become a bit intimidating at this point in terms of sheer lines of code:
`gen_uplc.rs` and three modules in `gen_uplc/` totals > 8500 LoC.
Aiken has its own _intermediate representation_ called `air` (as in Aiken Intermediate Representation).
These are common in compiled languages.
`Air` is defined in `aiken-lang/src/gen_uplc/air.rs`.
Unsurprisingly, it looks little bit like a language between Aiken and plutus.
In fact, Aiken has another intermediate representation: `AirTree`.
This is constructed between the `TypedExpr` and `Vec<Air>` ie between parsed Aiken and air.
#### Climbing the AirTree
Within `CodeGenerator::generate`, `CodeGenerator::build` is called on the function body.
This takes a `TypedExpr` and constructs and returns an `AirTree`.
The construction is recursive as it traverses the recursive `TypedExpr` data structure.
More on what an airtree is and its construction below.
At the same time `self` is treated as `mut`, so we need to keep an eye on this too.
The method which is called and uses this mutability of self is `self.assignment`.
It does so by
```sample
self.assignment >> self.expect_type_assign >> self.code_gen_functions.insert
```
and thus is creating a hashmap of all the functions that appear in the definition.
From the call to return of `assign` covers > 600 LoC so we'll leave this as otherwise a black box.
(`self.handle_each_clause` is also called with `mut` which in turn calls `self.build` for which `mut` it is needed.)
Validators in Aiken are boolean functions while in Uplc they are unit-valued (aka void-valued) functions.
Thus the air tree is wrapped such that `false` results in an error (`wrap_validator_condition`).
I don't know why there is a prevailing thought that boolean functions are preferable than functions
that error if anything is wrong - which is what validators are.
`check_validator_args` again extends the airtree from the previous step,
and again calls `self.assignment` mutating self.
Something interesting is happening here.
Script context is the final argument of a validator - for any script purpose.
`check_validator_args` treats the script context like it is an unused argument.
The importance of this is not immediate, and I've still yet to appreciate why this happens.
Let's take a look at what AirTree actually is
```rust
pub enum AirTree {
Statement {
statement: AirStatement,
hoisted_over: Option<Box<AirTree>>,
},
Expression(AirExpression),
UnhoistedSequence(Vec<AirTree>),
}
```
Note that `AirStatement` and `AirExpression` are mutually recursive definitions with `AirTree`.
Otherwise, it would be unclear from first inspection how tree-like this really is.
`AirExpression` has multiple constructors. These include (non-exhaustive)
- air primitives (including all the ones that appear in plutus)
- constructors `Call` and `Fn` to handle anonymous functions
- binary and unary operators
- handling when and if
- handling error and tracing
`AirStatement` also has multiple constructors. These include
- let assignments and named function definitions
- handling expect assignments
- pattern matching
- unwrapping data structures
Note that `AirTree` has many methods that are partial functions,
as in there are possible states that are not considered legitimate
at different points of its construction and use.
For example `hoist_over` will throw an error if called on an `Expression`.
As `AirTree` is for internal use only, the scope for potential problems is reasonably contained.
It seems likely this is to avoid similar-yet-different IRs between steps.
However, the trade off is that it partially obfuscates what is a valid state where.
What is hoisting? hoisting gives the airtree depth.
The motivation is that by the time we hit Uplc it is "generally better"
that
- function definitions appear once rather than being inlined multiple times
- the definition appears as close to use as possible
Hoisting creates tree paths.
The final airtree to airtree step is`self.hoist_functions_to_validator` traverses the paths.
There is a lot of mutating of self, making it quite hard to keep a handle on things.
In all this (several thousand?) LoC, it is essentially ascertaining in which node of the tree
to insert each function definition.
In a resource constrained environment like plutus, this effort is warranted.
At the same time this function deals with
- monomophisation - no more generics
- erasing opaque types
Neither of which exist at the Uplc level.
#### Into Air
The `to_vec : AirTree -> Vec<Air>` is much easier to digest.
For one, it is not evaluated in the context of the code generator,
and two, there is no mutation of the airtree.
The function recursively takes nodes of the tree and maps them to entries in a mutable vector.
It flattens the tree to a vec.
### Down to Uplc
Next we go from `Vec<Air> -> Term<Name>`.
This step is a little more involved than the previous.
For one, this is executed in the context of the code generator.
Moreover, the code generator is treated mutable - ouch.
On further inspection we see that the only mutation is setting `self.needs_field_access = true`.
This flag informs the compiler that, if true, additional terms must be added in one of the final steps
(see `CodeGenerator::finalize`).
As noted above, some of the mappings from air to terms are immediate like `Air::Bool -> Term::bool`.
Others are less so.
Some examples:
- `Air::Var` require 100 LoC to do case handling on different constructors.
- Lists in air have no immediate analogue in uplc
- builtins, as in built-in functions (standard shorthand), have to mediated
with some combination of `force` and `delay` in order to behave as they should.
- user functions must be "uncurried", ie treated as a sequence of single argument functions,
and recursion must be handled
- Do some magic in order to efficiently allow "record updates".
#### Cranking the Optimizer
There is a sequence of operations performed on the Uplc mapping `Term<Name> -> Term<Name>`.
These remove inconsequential parts of the logic which will appear.
These include:
- removing application of the identity function
- directly substituting where apply lambda is applied to a constant or builtin
- inline or simplify where apply lambda is applied to a parameter that appears once or not at all
Each of these optimizing methods has a its own relatively narrow focus,
and so although there is a fair number of LoC, it's reasonably straightforward to follow.
Some are applied multiple times.
### The End
The generated program can now be serialized and included in the blueprint.
### Plutus Core Signposting
All this fuss is to get us to a point where we can write Uplc - and good Uplc at that.
Note that there's many ways to generate code and most of them are bad.
The various design decisions and compilation steps make more sense
when we have a better understanding of the target language.
Uplc is a lambda calculus.
For a comprehensive definition on Uplc checkout the specification found
[here](https://github.com/input-output-hk/plutus/#specifications-and-design) from the plutus GitHub repo.
(I imagine this link will be maintained longer than the current actual link.)
If you're not at all familiar with lambda calculus I recommend
[an unpacking](https://crypto.stanford.edu/~blynn/lambda/) by Ben Lynn.
### What next?
I think it would be helpful to have some examples... Watch this space.

View File

@ -1,3 +1,5 @@
---
title: Kompact.io
---
---
## Hero

View File

@ -3,74 +3,74 @@ title: Are we zk-Cardano yet?
date: 2023-08-07
---
Not so long ago Emurgo announced they were doing a Cardano centered hackathon.
It was a welcome prospect - very few similar such events seem to exist in the space.
Things went monotonically south ever since the announcement, but that's a different story.
Not so long ago Emurgo announced they were doing a Cardano centered hackathon.
It was a welcome prospect - very few similar such events seem to exist in the
space. Things went monotonically south ever since the announcement, but that's a
different story.
One particularly interesting quirk was that of the three "tracks" of the hackathon,
one was _Zero Knowledge_ (aka zk).
Why particularly interesting quirk? In some sense it is not surprising:
zk has been very trendy these last few years around blockchains.
However, building on Cardano is notoriously challenging.
Building with zk on a zk-native blockchain is itself a very steep learning curve.
So combining the two, zk on Cardano seemed... a bit mad.
One particularly interesting quirk was that of the three "tracks" of the
hackathon, one was _Zero Knowledge_ (aka zk). Why particularly interesting
quirk? In some sense it is not surprising: zk has been very trendy these last
few years around blockchains. However, building on Cardano is notoriously
challenging. Building with zk on a zk-native blockchain is itself a very steep
learning curve. So combining the two, zk on Cardano seemed... a bit mad.
This post is borne out of a best effort of how far "zk on Cardano" can be pushed.
This post is borne out of a best effort of how far "zk on Cardano" can be
pushed.
## What is zk?
There is no shortage of explanations describing what zk is
( _eg_ [by Vitalik](https://vitalik.ca/general/2021/01/26/snarks.html){target="_blank"} or
[a full mooc](https://zk-learning.org/){target="_blank"} ).
There is also a reasonable breath to the field of zk that includes things like distributed compute.
Zk involves some really neat maths that lets you do some seemingly magical feats
and pairs well with blockchain in extending what is functionally possible.
Let's stick to a simple and prototypical example.
There is no shortage of explanations describing what zk is ( _eg_
[by Vitalik](https://vitalik.ca/general/2021/01/26/snarks.html){target="\_blank"}
or [a full mooc](https://zk-learning.org/){target="\_blank"} ). There is also a
reasonable breath to the field of zk that includes things like distributed
compute. Zk involves some really neat maths that lets you do some seemingly
magical feats and pairs well with blockchain in extending what is functionally
possible. Let's stick to a simple and prototypical example.
Suppose Alice and Bob are playing battleships.
The game begins with Alice and Bob placing their ships within their own coordinate grid.
They then take turns picking coordinates to "strike".
If they hit nothing then their turn ends, but if they hit a ship then they strike again.
The winner is the first to strike all coordinates containing their opponent's ships.
Suppose Alice and Bob are playing battleships. The game begins with Alice and
Bob placing their ships within their own coordinate grid. They then take turns
picking coordinates to "strike". If they hit nothing then their turn ends, but
if they hit a ship then they strike again. The winner is the first to strike all
coordinates containing their opponent's ships.
Alice knows Bob as being a notorious liar; how can she enjoy the game?
Each guess she makes, Bob gleefully shouts "Miss!".
She can't ask Bob to show he's not lying by revealing the actual locations of the ships.
She could ask Charlie to independently verify Bob's not lying,
but then what if Charlie is actually on team Bob and also lies.
Or Bob might suspect Charlie is actually on team Alice, slyly brought in to give Alice some hints.
Each guess she makes, Bob gleefully shouts "Miss!". She can't ask Bob to show
he's not lying by revealing the actual locations of the ships. She could ask
Charlie to independently verify Bob's not lying, but then what if Charlie is
actually on team Bob and also lies. Or Bob might suspect Charlie is actually on
team Alice, slyly brought in to give Alice some hints.
Is there a way that Bob can prove to Alice that each guess is a miss,
but without revealing the locations of the ships either to Alice or anyone else?
Is there a way that Bob can prove to Alice that each guess is a miss, but
without revealing the locations of the ships either to Alice or anyone else?
The answer is yes.
Using zk Bob can produce a proof each time Alice's guess misses if and only if it honestly does.
Alice can inspect each proof and verify Bob's response.
Alice can interrogate the proof as much as she wants, but she won't learn anything more than
her guess was a miss.
The answer is yes. Using zk Bob can produce a proof each time Alice's guess
misses if and only if it honestly does. Alice can inspect each proof and verify
Bob's response. Alice can interrogate the proof as much as she wants, but she
won't learn anything more than her guess was a miss.
There are a multitude of different ways to do this,
but essentially it involves modeling the problem as a bunch of algebra
over finite fields - like a lot of cryptography.
What's the _snark_ of zk-snark?
Snark stands for _Succinct Non-Interactive Argument of Knowledge_.
And without saying anything more, it means that Alice has to do way less algebra than Bob.
In applications this is important because Bob might not be able to lie anymore but he could still waste Alice's time.
There are a multitude of different ways to do this, but essentially it involves
modeling the problem as a bunch of algebra over finite fields - like a lot of
cryptography.
What's the _snark_ of zk-snark? Snark stands for _Succinct Non-Interactive
Argument of Knowledge_. And without saying anything more, it means that Alice
has to do way less algebra than Bob. In applications this is important because
Bob might not be able to lie anymore but he could still waste Alice's time.
## Sudoku snark
Sudoku snark was the entrant to Emurgo's hackathon.
The summary-pitch-story deck is [here](https://pub.kompact.io/sudoku-snark){target="_blank"}.
Links to the associated repos: [plutus-zk](https://github.com/waalge/plutus-zk){target="_blank"}
and [sudoku-snark](https://github.com/waalge/sudoku-snark){target="_blank"}.
Sudoku snark was the entrant to Emurgo's hackathon. The summary-pitch-story deck
is [here](https://pub.kompact.io/sudoku-snark){target="\_blank"}. Links to the
associated repos:
[plutus-zk](https://github.com/waalge/plutus-zk){target="\_blank"} and
[sudoku-snark](https://github.com/waalge/sudoku-snark){target="\_blank"}.
Just after the hackathon got underway there was a
[large PR merged](https://github.com/input-output-hk/plutus/pull/5231){target="_blank"}
into the main branch of plutus.
It's a mammoth culmination of many many months of work.
In it were some fundamental primitives needed for running zk algorithms.
Just after the hackathon got underway there was a
[large PR merged](https://github.com/input-output-hk/plutus/pull/5231){target="\_blank"}
into the main branch of plutus. It's a mammoth culmination of many many months
of work. In it were some fundamental primitives needed for running zk
algorithms.
The idea of the project was as follows:
@ -79,49 +79,55 @@ The idea of the project was as follows:
- try to get a version of hydra running this newest version of plutus
- wrap up in a gui
Unsurprisingly to anyone who's hung around the Cardano ecosystem long enough,
this third part is where things got stuck.
We did get as far as running a cluster of nodes in the Conway era with the latest version of plutus
but unrelated changes seemed to thwart any chance of building transactions here.
Unsurprisingly to anyone who's hung around the Cardano ecosystem long enough,
this third part is where things got stuck. We did get as far as running a
cluster of nodes in the Conway era with the latest version of plutus but
unrelated changes seemed to thwart any chance of building transactions here.
A quick shout-out to the [modulo-p.io](https://modulo-p.io/){target="_blank"} team.
They had a different approach and managed to implement a zk algorithm with the existing plutus primitives.
This spared the need to play the foolhardy dependency bumping game with the Cardano node.
However, because zk is so arithmetically intense,
the app wont run outside a hydra head and with very generous max unit budgets (afaics).
This approach won't be necessary when we have the new version of plutus available.
Nonetheless, it's very neat to see it done and they packaged it very nicely.
A quick shout-out to the [modulo-p.io](https://modulo-p.io/){target="\_blank"}
team. They had a different approach and managed to implement a zk algorithm with
the existing plutus primitives. This spared the need to play the foolhardy
dependency bumping game with the Cardano node. However, because zk is so
arithmetically intense, the app wont run outside a hydra head and with very
generous max unit budgets (afaics). This approach won't be necessary when we
have the new version of plutus available. Nonetheless, it's very neat to see it
done and they packaged it very nicely.
The validator in Sudoku snark uses [groth16](https://eprint.iacr.org/2016/260.pdf).
In part because this was already mostly available from the plutus repo itself.
It is also the most obvious candidate to begin with.
It's relatively mature, relatively simple, can be implemented from the new primitives,
and importantly in Cardano land has small proof size.
(As far as I know, the smallest of comparable algorithms.)
The validator in Sudoku snark uses
[groth16](https://eprint.iacr.org/2016/260.pdf). In part because this was
already mostly available from the plutus repo itself. It is also the most
obvious candidate to begin with. It's relatively mature, relatively simple, can
be implemented from the new primitives, and importantly in Cardano land has
small proof size. (As far as I know, the smallest of comparable algorithms.)
The program to generate the setup and proofs uses the Arkworks framework.
Again this choice was initially inspired by a script from the IOG team,
but again it seems like a smart choice.
Arkworks is a well conceived, highly modular framework for zk,
which makes it easy to pull in the bits we need to perform our off-chain logic.
The program to generate the setup and proofs uses the Arkworks framework. Again
this choice was initially inspired by a script from the IOG team, but again it
seems like a smart choice. Arkworks is a well conceived, highly modular
framework for zk, which makes it easy to pull in the bits we need to perform our
off-chain logic.
The choice of game, sudoku, was in turn inspired by an arkworks example.
It's not the most compelling of choices, but it's simple and it did for now.
Battleships would have been more compelling or mastermind as the modulo-p team used.
The choice of game, sudoku, was in turn inspired by an arkworks example. It's
not the most compelling of choices, but it's simple and it did for now.
Battleships would have been more compelling or mastermind as the modulo-p team
used.
The intended game play involved locking Ada at a utxo correspondinig to a sudoku puzzle,
and spendable only if a player could provide proof they knew the solution.
Through the magic of zk they'd not disclose to the other competitors the solution itself.
Other details were TBC: is it first and second prizes? are players whitelisted? _etc_.
The intended game play involved locking Ada at a utxo correspondinig to a sudoku
puzzle, and spendable only if a player could provide proof they knew the
solution. Through the magic of zk they'd not disclose to the other competitors
the solution itself. Other details were TBC: is it first and second prizes? are
players whitelisted? _etc_.
## So are we zk-Cardano yet?
We're close.
There is potentially still quite a while before these new primitives in plutus reach mainnet.
The word on the street is that it might happen before the end of 2023.
There is potentially still quite a while before these new primitives in plutus
reach mainnet. The word on the street is that it might happen before the end
of 2023.
Even sooner, there will be versions of the Cardano node available with the new primitives,
and so possibly plumb-able into hydra without causing oneself an aneurysm.
Even sooner, there will be versions of the Cardano node available with the new
primitives, and so possibly plumb-able into hydra without causing oneself an
aneurysm.
In development time that's not so long: we can start thinking about what to build with zk on Cardano.
In development time that's not so long: we can start thinking about what to
build with zk on Cardano.

View File

@ -1,108 +0,0 @@
---
title: Are we zk-Cardano yet?
date: 2023-08-07
---
Not so long ago Emurgo announced they were doing a Cardano centered hackathon.
It was a welcome prospect - very few similar such events seem to exist in the space.
Things went monotonically south ever since the announcement, but that's a different story.
One particularly interesting quirk was that of the three "tracks" of the hackathon,
one was _Zero Knowledge_ (aka zk).
Why particularly interesting quirk? In some sense it is not suprising:
zK has been very trendy these last few years around blockchains.
However, building on Cardano is notoriously challenging.
Building with zk on a zk-native blockchain is itself a very steep learning curve.
So combining the two, zk on Cardano seemed... a bit mad.
This post is bourne out of a best effort of how far "zk on cardano" can be pushed.
## What is zk?
There is no shortage of explanations describing what zk is [TODO: Links].
There is also a reasonable breath to the field of zk that includes things like distributed compute.
Zk involves some really neat maths that lets you do some seemingly magical feats,
and pairs well with blockchain in extending what is functionally possible.
Let's stick to a simple and prototypical example.
Suppose Alice and Bob are playing battleships.
The game begins with Alice and Bob placing their ships within their own coordinate grid.
They then take in terms picking coordinates to "bomb".
If they hit nothing, then their turn ends, but if they hit a ship then they guess again.
The winner is the first to sink all their oponents ships.
Alice knows Bob has a reputation of being a notorious liar; how can she enjoy the game?
Each guess she makes, Bob says gleefully shouts "Miss!".
She can't ask Bob to show he's not lying by revealing the actual locations of the ships.
She could ask Charlie to independently verify Bob's not lying,
but then what if Charlie is actually on team Bob and also lies.
Or Bob might suspect Charlie is actually on team Alice, slyly brought in to give could Alice some hints.
Is there a way that Bob can prove to Alice that each guess is a miss,
but without revealing the locations of the ships either to Alice or anyone else?
The answer is yes.
Using zk Bob can produce a proof each time Alice's guess misses if and only if it honestly does.
Alice can inspect each proof and verify Bob's response.
Alice can interogate the proof as much as she wants, but she won't learn anything more than
her guess was a miss.
There are multiplitude of different ways to do this,
but essentially it involves modelling the problem as a bunch of algebra
over finite fields - like a lot of cryptography.
What's the snark of zk-snark?
Snark stands for _Succinct Non-Interactive Argument of Knowledge_.
And without saying anything more: it means that Alice has to do way less algebra than Bob.
In applications this is important, because Bob might not be able to lie anymore,
but he could still waste Alice's time.
## Sudoku snark
Sudoku snark was the entrant to Emurgo hackathon.
The summary/ pitch/ story deck is [here](https://pub.kompact.io/sudoku-snark).
Links to associated repos [plutus-zk]() and [sudoku-snark]().
Just after the hackathon got underway there was a large PR merged into the main branch of plutus.
It's a mammoth PR that is the culmination of many many months of work.
In it were some fundamental primitives needed for running zk algos.
The idea of the project was as follows:
- write a validator implementing a zk algorithm with the new primitives
- write a programme to generate the setup and proofs
- try to get a version of hydra running this newest version of plutus.
Unsurprisingly to anyone who's hung around the Cardano repos long enough,
this final part is where things got stuck.
Things got as far as running a cluster of nodes in the conway era supposedly with the latest plutus
but some unrelated changes seemed to thwart any chance of building transactions.
The validator uses [groth16].
In part because this was already mostly available from the plutus repo itself.
It is also the most obvious candidate to begin with.
It's relatively mature, relatively simple, can be implemented from the new primitives,
and, importantly in cardano land, has small proof size.
(As far as I know, the smallest of comparable algos.)
The program to generate the setup and proofs uses the arkworks framework.
Again this was initially inspired by a script from the IOG team.
The choice of game, sudoku, was in turn inspired by an arkworks example.
It's not the most compelling of choices, but it did for now.
The intended game play involved locking ada at a utxo
spendable only if a player could provide proof you knew the solution.
And through the magic of zk, not disclosing to the competition the solution itself.
Other details were TBC: is it first and second prizes? are players whitelisted?
## So are we zk-Cardano yet?
We're close.
There is potentially still quite a stretch between being in the plutus repo and being run on-chain.
The word on the street is that it might happen before the end of 2023.
Before it's available on mainnet there will be versions the cardano node available,
and so possibly plumbable into hydra without causing oneself an aneurysm.

View File

@ -0,0 +1,103 @@
---
title: "Hydra is cool: You don't need Hydra"
date: 2023-09-20
---
## Hydra is cool
Hydra[^1] is a very cool project. It is a layer 2 for Cardano that is
_isomorphic_ to the L1. Here isomorphic means that Plutus runs in Hydra just
like it does on the L1. That dapp you've just toiled over for months to run on
the L1 can be put in Hydra and 'just work'.
[^1]:
This post does not distinguish between Hydra and Hydra Head referring to
both as Hydra. If you want to know more about Hydra, then check out their
[explainers](https://hydra.family/head-protocol/core-concepts).
## Hydra's compromise
Hydra boasts it can achieve higher throughput and lower transaction fees
compared to the Cardano L1 as well as near instant settling and no roll-backs.
You may be asking _If my dapp just works on Hydra and it's better in all key
respects, then why don't we all just use Hydra?_. The answer is because these
improvements come at a cost. Consensus in Hydra differs from that on the L1.
Hydra doesn't use ouroboros. Instead all participating hydra nodes must sign-off
on all updates to the chain state. Practically speaking, far fewer nodes can
participate in Hydra and one quiet node stops the whole Hydra chain updating.
Not great for an L1.
## You don't need Hydra
Hydra is an example of a way to do state channels. A state channel relies on the
integrity of the L1, while accumulating state separately from it (L2). At some
point the layers are brought into sync. This is when funds on the L1 can be
unlocked, and/or the state of the L2 updated.
Hydra could be thought to be providing some future-proofing. It is possible for
a Hydra instance to run indefinitely and Plutus scripts not yet written will be
executable in some already running instance. However, because Hydra's consensus
is so brittle the longevity of an instance is not something to depend on. Each
and any transaction may be its last.
A key question when considering Hydra is _Do I need isomorphic-ness?_. If you
know all your business logic before instantiation then the answer is **no, you
don't care for isomorphic-ness**. Instead, you can roll-your-own L2. It depends
on your use case as to how much work that ends up being. It can be very simple.
## You don't want Hydra
In Hydra, the latest agreed state in the L2 is the one that the L1 will accept
as the most legitimate. This is a sensible default.
Suppose however you have a game of poker where one player learns that they've
lost and rage quits. From the game's perspective, that final transaction should
be forced through - the player's loss is inevitable. At present this isn't
possible with Hydra. If a party doesn't sign, then a state isn't valid.
In another use case, suppose there is some particularly intense on-chain
verification that would be prohibitive on the L1 but that you'd like the results
of which to persist onto the L1 and/or be recovered in future L2 instances. This
could be done with validity tokens but anything minted in the L2 won't persist
onto the L1.
Another key question then is _What is the right way to sync the L1 and L2
states?_. Hydra has a way of it doing it which might or might not be appropriate
for your use case. Rolling your own L2 means that the sync logic can fit your
business needs. Both the cases above are resolvable with custom sync logic.
## An Example: Subbit.xyz
Probably the simplest, non-trivial example using state channels is
[Subbit.xyz](https://subbit.xyz). Subbit.xyz is premised on the observation that
subscription is a very common use case: there are two parties where one pays the
other incrementally. It sacrifices generality to gain absolutely minimal
overhead for both parties.
In Subbit.xyz, Alice, a consumer, subscribes to some service of Bob, a provider.
Alice instantiates the channel by locking funds, similar to Hydra. There are
only two mechanisms for unlocking - one for Alice and the other for Bob. All
logic is known at instantiation.
A consumer needs only to keep track of their account balance, ascertain the cost
of each outgoing request, and produce valid signatures for a few dozen bytes of
data at a time. They don't need to watch the L1 and it's a non-chatty protocol.
The low resource needs opens it up to applications on intermittently connected
user devices such as laptops and mobile, and even micro-controllers. High
throughput remains achievable.
A provider must track each subscriber's account, and periodically check the
state of the L1. This could conceivably be as little as once a week or once a
month. The low resource needs for a provider means they have the ability to
serve more with less.
## Hydra for QoL
When Hydra reaches a point of maturity that it's plug and play, it's potentially
far easier to deploy with Hydra then roll-your-own L2. Isomorphic-ness gives
Hydra incredible flexibility and generality. You don't need isomorphic-ness but
because of it, Hydra could be an easy and convenient solution.
As for custom sync logic, it is surely the case that there is a tranche of
interesting applications where it's far easier and more effective to reuse Hydra
infra and modify it than creating your own L2 from scratch.

272
content/posts/principles.md Normal file
View File

@ -0,0 +1,272 @@
---
title: "Principles of dapp design"
date: 2024-11-26
status: published
---
There are a collection of disparate thoughts on what makes for good design that
inform the decisions we make when designing dapps.
We thought it helpful to articulate them for ourselves, for any future
collaborators, and any wider audience interested.
These thoughts are expressed as diktats but are intended as perspective
from which to pick through there shortcomings.
These "principles" don't fit neatly into some SOLID like framework. Some are
high-level general security concious software dev stuff, others are quite
specifically reflecting on a "building an L2 on cardano". Some implications
manifest at the software architecture level, while others inform the development
process.
## Context
Before unpacking the principles, it's worth reflecting on _devving in
Cardanoland_.
The Cardano ledger is a lean by design: it is to provide a kernel of integrity
for larger applications. It was never to, say, host and execute all the
components of an application. It is even a little too lean in places (eg using
key hashes over keys in the script context).
On-chain code is the code executed in the ledger. All integrity guarantees of an
application built over ledger ultimately rely on the on-chain code. On-chain
code is purely discriminatory: show it a transaction and it will succeed or
fail. It cannot generate valid transactions. That is the responsibility for
other, off-chain code. Off-chain code is a necessary part of any such
application, but does not provide integrity guarantees.
We'll call applications, structured such that their integrity is backed by the
ledger, "dapps". Typically the term has other assumptions: a decentralised
application should be run-able by anyone with internet access, reasonable
hardware, and without need to seek permission from some authority.
On-chain code takes form as Plutus validators. Writing validators as
**extreme programming**.
Lets immediately disambiguate with the [XP methedology][xp-wiki].
It's extreme in the sense that it is highly unusual.
[xp-wiki]: https://en.wikipedia.org/wiki/Extreme_programming
It's extreme in the following ways:
- Highly constrained execution environment, which has multiple implications
- Diminishing returns of code re-use
- Conflicting motivation on factorization
- Un-patch-ablity
- High cost of failure
- Functions that do nothing but fail
Resource limitations at runtime are incredibly restrictive. Moreover, all
resources used have to be paid for. This is a concern is shared to some extent
with low level programming but even there is relatively niche on modern
hardware. There are many implications of this. A key one is that implementation
matter.
Libraries have limited use. As noted, the efficiency of an implementation
matters. If one needs to aggregate multiple values over a list of inputs, it is
generally cheaper to do this in a single fold, and without
constructing/deconstructing across a function boundary. Implementation details
cannot be abstracted away in simple one-size-fits-all manner, at least at zero
cost (cf plutus apps and the bloated outputs it would return). In real world
examples this cost is significant enough, that the use of stock library methods
must be considered. One saving grace is that this is manageable. The resource
limitations mean that anything over a couple thousand lines of code risks not
being usable in a transaction anyway.
Factorizing code in way that communicates purpose to a reader should be a strong
consideration. However, we already have another, possibly conflicting,
consideration: implementation efficiency. Some might say this is a compiler
problem, and that we should have clever compilers. To which the most immediate
answer is "yes, but right now we don't". It is also not a full panacea. A clever
compiler is harder to reason about, harder to verify its correctness, and it can
become more obscure to prod the compiler into pathways known to be more
efficient.
Validators are, a priori, not patchable. Depending on the validator, once
'deployed' it may be impossible to update. There is no way to bump or patch a
validator once it is in use, without such functionality being designed in. This
not unique. It was standard in pre-internet software where rolling updates were
infeasible, and still exists for devices that aren't internet enabled. However,
it is now far more the niche than the mainstream.
The correctness of a validator is high stakes. This is a property shared with
any security critical software. It is not the same league as, say, aviation
software, but it is much closer to that than a game or web app. If there is a
bug, then user funds are at risk. This compounds being not patchable. Great
efforts must be spent verifying validator correctness.
Validators are very unusual functions particularly in a functional paradigm.
They take as input a very restricted set of args, and either fail or return
unit. That's all we care about: discriminating acceptable transactions from
unacceptable transactions. Sure, this akin to writing a test, or an assert
condition - but these are commonly auxiliary rather than the culmination of the
code base. Writing Plutus is not akin to, say, some web based API or an ETL
pipeline. There is the potential for the code to be utilized by third parties if
they desired to build their own transactions that involve the validators.
However this use is generally secondary to optimizing for the intended set of
transaction builders.
## Principles
### On-chain code is to keep participants safe from others
On-chain code **is** responsible for keeping the user safe from others. It is
its fundamental responsibility.
On-chain code is **not** responsible for keeping the user safe from themselves.
A user can compromise themselves completely and totally by, say, publishing their signing key,
voiding any guarantees provided by on-chain code.
Thus such guarantees are generally superfluous.
Off-chain code is responsible for keeping the user safe, and it is off-chain
code that should make it hard for them to shoot themselves in the foot.
On-chain code is also not there to facilitate politeness. Good and bad behaviour
is a binary distinction, discriminated thoroughly by a validator. A partner may
stop responding for legitimate or malicious reasons. We do not need to
speculate; we need only ensure that the other partner's funds are safe and can
be recovered.
Suppose there is a dapp that requires the depositing of funds, with a pathway in which the funds
may be returned. Further, that this return is verified by a signature belonging
associated to key provided by the depositor.
Some may wish on-chain code to check everything it is possible to check.
This includes keeping the user safe from themself.
From this perspective the validator should
have a constraint that this key has signed the tx, verifying that the depositor
has the associated signing key.
According to this principle, the validator should not.
The larger code base should.
The documentation should be crystal clear what this key is for and how it should be managed.
But the on-chain code should be solely focused on keeping the user safe from others, and other from the user.
There is a loose sense of the [streetlight effect](https://en.wikipedia.org/wiki/Streetlight_effect) bourne out in code.
### Simplicity invites security
Particularly for on-chain code, err on the side of simplicity. Design and build
such that reasoning around scenarios is straightforward, and answering "what if
..." questions are easy.
This should not be at the expense of being feature complete, although the
principle then becomes a little grey on application. In places it translates to:
develop an abstraction in which the features become simple. In other places, we
will have to find the happy compromise between simple-ness and feature-ful-ness.
For example, in the case of designing [Cardano Lightning](cardano-lightning.org)
can a partner close two of their channels in a single transaction?
This might be important but perhaps could be handled by an abstraction in the application
hiding this detail to the user.
There is overlap here with the previous principle.
By having the on-chain code laser focused, we don't have nice-to-haves,
cluttering the code base, possibly obscuring our view from otherwise glaring bug.
The current principle further justifies excluding logic in the validator
that a self interested participant would be motivated from ensuring themselves.
For example: a validator must check that a thread token never leaves a script address;
it need not check that a participant has paid themselves their due funds.
### Prioritise user autonomy
Deprioritise collaborative actions.
This principle is only employable only in specific circumstances.
In Cardano Lightning a user is responsible for their own funds, and only their
own funds. They cannot spend their partners funds. For example, when winding
down a channel, it requires each user to submit evidence of what they are owed.
The pathway could have enforced that one partner left an address to which the
other partner's tx would output the correct funds. It would potentially save a
transaction in the winding down process. However, it also invites questions of
double satisfaction, and resolutions to this make it harder to reason about.
Instead, following this principle,
the design prioritises multi-channel management.
A fundamental participant type in a healthy Lightning network is the Gateway.
A gateway participant is highly
connected within the network and is (financially) motivated to route payments
between participants. A gateway node in particular needs to manage their
channels, and manage their capital amongst channels as efficiently as possible.
This in the whole network's interest.
Cardano Lightning does permit mutual agreed actions.
However, such actions is considered as a secondary
pathway. Any channel has (at most) the funds of only the two partners of the
channel. Mutual actions are verified by the presence of both partners'
signatures on a transaction. As we assume that any participant will act in a
self interested manner, and is responsible for keeping themselves safe, few
checks are done beyond this.
### Distinguish safety from convenience
Safety comes first, but we also need things to be practical and preferably even
convenient. Make explicit when a feature has been included for safety, or is to
do with convenience.
In some respects, this is restating of the first and or second principles.
The on-chain code is precisely what keeps users safe.
The small distinction is that this extends to off-code too.
There are aspects of off-chain code that are integral, and other parts that are convenient.
### Spec first, implement second
A spec:
- Bridges from intent to code
- Expels ambiguity early
- Says no to feature creep
A spec bridges the design intentions to the implementation. Code is halfway
house between what a human can parse and what a machine can parse. Where that
halfway falls is a question for language design(ers), and there is a full
spectrum of positions available with all the possible languages. Very roughly,
the closer it is to human readable, the less efficient it ends up being executed
by the machine.
Regardless of a language's expressiveness, it doesn't replace the need for
additional documentation.
"Self-describing code" is a nice idea and not without strong merits. Naming
should follow conventions, and should not be knowingly obscure. In software at
large, the problem of separate documentation falling out of sync is observed ad
nauseum. But. The idea that descriptive names and some inline comments are
sufficient to communicate design and intent is not credible. [I agree with
Jef][jef-raskin-blog].
[jef-raskin-blog]: https://queue.acm.org/detail.cfm?id=1053354
The above is especially acute in the context of the extreme programming paradigm
that is Plutus development. We can and should demand more attention from a dev
engaging with the application. They should not expect to "intuitively"
understand how to interface with the code. Again - not to be justify any
obscurity of design or code, but a validator is not simply just another library
they'd be interfacing with. The stakes are too high. They must read the spec.
We suffer less from docs/code divergence than is experienced in "normal" development.
For the same reason that we have un-patch-ablity we have a fixed feature set.
It is in evolving software and its code base, by adding new features or modifying existing ones,
when code diverges from docs.
A spec helps expel ambiguity early. It provides an opportunity to check that
everyone is on the same page before any lines of code are written, and without
having to unpick lines of code after the fact.
A spec helps make the implementation stage straightforward and intentional. It
greatly reduces the required bandwidth since each part of the code has a
prescribed purpose. This is also reduces the cost of writing wrong things,
before settling on something acceptable.
Having a spec combats feature creep. Adding feature requirements part way
through implementation can lead to convoluted and design and code, and
ultimately greatly increase the chance of bugs. As discussed for on-chain code,
rolling updates are not generally possible. We need to make sure from the start
what the feature requirements are (and as importantly what they aren't).
## Summary
The principles have been arrived up over numerous projects,
most explicitly and recently while working on Cardano Lightning.
As alluded to in the introduction, these principles should ...
well, be treated more like [guidelines](https://youtu.be/k9ojK9Q_ARE).
If you comments, questions, suggested, or criticisms, please get in touch.

View File

@ -0,0 +1,295 @@
---
title: Tracing Aiken Build
date: 2023-09-02
---
Aims:
> Describe the pipeline and components getting from Aiken to Uplc.
## The Preface
### Motivations
The motivation for writing this came from a desire to add additional features to
Aiken not yet available. One such feature would evaluate an arbitrary function
in Aiken callable from JavaScript. This would help a lot with testing and when
trying to align on and off-chain code.
Another more pipe dreamy, ad-hoc function extraction - from a span of code,
generate a function. A digression to answer _why would this be at all helpful?!_
Validator logic often needs a broad context throughout. How then to best factor
code? Possible solutions:
1. Introduce types / structs
2. Have functions with lots of arguments
3. Don't
The problems are:
1. Requires relentless constructing and deconstructing across the function call.
This adds costs.
2. Becomes tedious aligning the definition and function call.
3. Ends up with very long validators which are hard to unit test.
My current preferred way is to accept that validator functions are long. Ad-hoc
function extraction would allow for sections of code to be tested without
needing to be factored out.
To do either of these, we need to get to grips with the Aiken compilation
pipeline.
### This won't age well
Aiken is undergoing active development. This post started life with Aiken
~v1.14. Aiken v1.15 introduced reasonably significant changes to the compilation
pipeline. The word is that there aren't any more big changes in the near future,
but this article will undoubtedly begin to diverge from the current code-base
even before publishing.
### Limitations of narrating code
Narrating code becomes a compromise between being honest and accurate, and being
readable and digestible. The command `aiken build` covers well in excess of
10,000 LoC. The writing of this post ground to a halt as it reached deeper into
the code-base. To redeem it, some (possibly large) sections remain black boxes.
## Aiken build
Tracing `aiken build`, the pipeline is roughly:
```sample
. -> Project::read_source_files ->
Vec<Source> -> Project::parse_sources ->
ParsedModules -> Project::type_check ->
CheckedModules -> CodeGenerator::build ->
AirTree -> AirTree::to_vec ->
Vec<Air> -> CodeGenerator::uplc_code_gen ->
Program / Term<Name> -> serialize ->
.
```
We'll pick our way through these steps
At a high level we are trying to do something straightforward: reformulate Aiken
code as Uplc. Some Aiken expressions are relatively easy to handle for example
an Aiken `Int` goes to an `Int` in Uplc. Some Aiken expressions require more
involved handling, for example an Aiken `If... If Else... Else ` must have the
branches "nested" in Uplc. Aiken has lots of nice-to-haves like pattern
matching, modules, and generics; Uplc has none of these.
### The Preamble
#### Cli handling
The cli enters at `aiken/src/cmd/mod.rs` which parses the command. With some
establishing of context, the program enters `Project::build`
(`crates/aiken-project/src/lib.rs`), which in turn calls `Project::compile`.
#### File crawl
The program looks for Aiken files in both `./lib` and `./validator`
sub-directories. For each it walks over all contents (recursively) looking for
`.ak` extensions. It treats these two sets of files a little differently. For
example, only validator files can contain the special validator functions.
#### Parse and Type check
`Project::parse_sources` parses the module source code. The heavy lifting is
done by `aiken_lang::parser::module`, which is evaluated on each file. It
produces a `Module` containing a list of parsed definitions of the file:
functions, types _etc_, together with metadata like docstrings and the file
path.
`Project::type_check` inspects the parsed modules and, as the name implies,
checks the types. It flags type level warnings and errors and constructs a hash
map of `CheckedModule`s.
#### Code generator
The code generator `CodeGenerator` (`aiken-lang/src/gen_uplc.rs`) is given the
definitions found from the previous step, together with the plutus builtins. It
has additional fields for things like debugging.
This is handed over to a `Blueprint` (`aiken-project/src/blueprint/mod.rs`). The
blueprint does little more than find the validators on which to run the code
gen. The heavy lifting is done by `CodeGenerator::generate`.
We are now ready to take the source code and create plutus.
### In the air
Things become a bit intimidating at this point in terms of sheer lines of code:
`gen_uplc.rs` and three modules in `gen_uplc/` totals > 8500 LoC.
Aiken has its own _intermediate representation_ called `air` (as in Aiken
Intermediate Representation). Intermediate representations are common in
compiled languages. `Air` is defined in `aiken-lang/src/gen_uplc/air.rs`.
Unsurprisingly, it looks a little bit like a language between Aiken and plutus.
In fact, Aiken has another intermediate representation: `AirTree`. This is
constructed between the `TypedExpr` and `Vec<Air>` ie between parsed Aiken and
air.
#### Climbing the AirTree
Within `CodeGenerator::generate`, `CodeGenerator::build` is called on the
function body. This takes a `TypedExpr` and constructs and returns an `AirTree`.
The construction is recursive as it traverses the recursive `TypedExpr` data
structure. More on what an airtree is and its construction below. At the same
time `self` is treated as `mut`, so we need to keep an eye on this too. The
method which is called and uses this mutability of self is `self.assignment`. It
does so by
```sample
- self.assignment
└ self.expect_type_assign
└ self.code_gen_functions.insert
```
and thus is creating a hashmap of all the functions that appear in the
definition. From the call to return of `assign` covers > 600 LoC so we'll leave
this as a black box. (`self.handle_each_clause` is also called with `mut` which
in turn calls `self.build` for which `mut` it is needed.)
Validators in Aiken are boolean functions while in Uplc they are unit-valued
(aka void-valued) functions. Thus the air tree is wrapped such that `false`
results in an error (`wrap_validator_condition`). I don't know why there is a
prevailing thought that boolean functions are preferable to functions that error
if anything is wrong - which is what validators are.
`check_validator_args` again extends the airtree from the previous step, and
again calls `self.assignment` mutating self. Something interesting is happening
here. Script context is the final argument of a validator - for any script
purpose. `check_validator_args` treats the script context like it is an unused
argument. The importance of this is not immediate, and I've still yet to
appreciate why this happens.
Let's take a look at what AirTree actually is
```language-rust
pub enum AirTree {
Statement {
statement: AirStatement,
hoisted_over: Option<Box<AirTree>>,
},
Expression(AirExpression),
UnhoistedSequence(Vec<AirTree>),
}
```
Note that `AirStatement` and `AirExpression` are mutually recursive definitions
with `AirTree`. Otherwise, it would be unclear from first inspection how
tree-like this really is.
`AirExpression` has multiple constructors. These include (non-exhaustive)
- air primitives (including all the ones that appear in plutus)
- constructors `Call` and `Fn` to handle anonymous functions
- binary and unary operators
- handling when and if
- handling error and tracing
`AirStatement` also has multiple constructors. These include
- let assignments and named function definitions
- handling expect assignments
- pattern matching
- unwrapping data structures
Note that `AirTree` has many methods that are partial functions, as in there are
possible states that are not considered legitimate at different points of its
construction and use. For example `hoist_over` will throw an error if called on
an `Expression`. As `AirTree` is for internal use only, the scope for potential
problems is reasonably contained. It seems likely this is to avoid
similar-yet-different IRs between steps. However, the trade off is that it
partially obfuscates what is a valid state where.
What is hoisting? Hoisting gives the airtree depth. The motivation is that by
the time we hit Uplc it is "generally better" that
- function definitions appear once rather than being inlined multiple times
- the definition appears as close to use as possible
Hoisting creates tree paths. The final airtree to airtree step,
`self.hoist_functions_to_validator`, traverses these paths. There is a lot of
mutating of self, making it quite hard to keep a handle on things. In all this
(several thousand?) LoC, it is essentially ascertaining in which node of the
tree to insert each function definition. In a resource constrained environment
like plutus, this effort is warranted.
At the same time this function deals with
- monomophisation - no more generics
- erasing opaque types
Neither of which exist at the Uplc level.
#### Into Air
The `to_vec : AirTree -> Vec<Air>` is much easier to digest. For one, it is not
evaluated in the context of the code generator, and two, there is no mutation of
the airtree. The function recursively takes nodes of the tree and maps them to
entries in a mutable vector. It flattens the tree to a vec.
### Down to Uplc
Next we go from `Vec<Air> -> Term<Name>`. This step is a little more involved
than the previous. For one, this is executed in the context of the code
generator. Moreover, the code generator is treated as mutable - ouch.
On further inspection we see that the only mutation is setting
`self.needs_field_access = true`. This flag informs the compiler that, if true,
additional terms must be added in one of the final steps (see
`CodeGenerator::finalize`).
As noted above, some of the mappings from air to terms are immediate like
`Air::Bool -> Term::bool`.
Others are less so. Some examples:
- `Air::Var` require 100 LoC to do case handling on different constructors.
- Lists in air have no immediate analogue in uplc
- builtins, as in built-in functions (standard shorthand), have to be mediated
with some combination of `force` and `delay` in order to behave as they
should.
- user functions must be "uncurried", ie treated as a sequence of single
argument functions, and recursion must be handled
- Do some magic in order to efficiently allow "record updates".
#### Cranking the Optimizer
There is a sequence of operations performed on the Uplc, mapping
`Term<Name> -> Term<Name>`. This removes inconsequential parts of the logic
which have been generated, including:
- removing application of the identity function
- directly substituting where apply lambda is applied to a constant or builtin
- inline or simplify where apply lambda is applied to a parameter that appears
once or not at all
Each of these optimizing methods has a its own relatively narrow focus, and so
although there is a fair number of LoC, it's reasonably straightforward to
follow. Some are applied multiple times.
### The End
The generated program can now be serialized and included in the blueprint.
### Plutus Core Signposting
All this fuss is to get us to a point where we can write Uplc - and good Uplc at
that. Note that there are many ways to generate code and most of them are bad.
The various design decisions and compilation steps make more sense when we have
a better understanding of the target language.
Uplc is a lambda calculus. For a comprehensive definition on Uplc checkout the
specification found
[here](https://github.com/input-output-hk/plutus/#specifications-and-design)
from the plutus GitHub repo. (I imagine this link will be maintained longer than
the current actual link.) If you're not at all familiar with lambda calculus I
recommend [an unpacking](https://crypto.stanford.edu/~blynn/lambda/) by Ben
Lynn.
### What next?
I think it would be helpful to have some examples... Watch this space.

View File

@ -0,0 +1,60 @@
---
title: why is building txs hard?
draft: true
---
## What is a dapp?
A typical dapp has a number of components:
- Validators: also called the _on-chain_ part.
The decentralized network of nodes that maintain the chain run this code as part of the process of deciding
whether a tx is to be added to the chain or rejected.
- Chain indexing: watches the chain and records the data relevant to the dapp.
- Pretty front-end: typically how a user interacts with a dapp.
- Tx-building code: A component of the frontend.
It takes data from the user, the user's wallet, the chain-indexer and possibly elsewhere
to construct txs to be submitted to the chain that the validators will deem acceptable.
Here we have really described a browser-based plutus dapp.
Considering the term _dapp_ more generally, the Daedalus wallet is a dapp which is neither browser based nor involves any Plutus.
Cli-based dapps also exist, such as multi-sig using native scripts.
## What is a tx?
At its core, the chain is a list of transaction outputs.
The chain is changed by submitting a tx which "spends" existing unspent transaction outputs (utxos) and appending new ones.
(There's other possible modifications too, like minting native assets and staking _etc_, but the key part is spending.)
The on-chain part is where the dapp has its integrity, but users can only interact with the on-chain part of the dapp by submitting txs.
The on-chain part is relatively simple.
It inspects each tx that it is concerned with, and if it does not like what it sees, it fails.
Cardano is, in this sense, a lean chain.
The off-chain part is relatively complex.
There may be no, one, or many potential valid txs that would satisfy the user's intent.
At its core, a transaction is a list of inputs and outputs.
The inputs are spent, and the outputs created.
It must also contain the necessary signatures.
## Some history
When Plutus was first dreamed up, it wasn't just a language.
It was whole environment in which dapps would be engaged with.
The on-chain and off-chain code were coupled into a single framework with seamless extensive testing.
Dapps ran in the _PAB_, Plutus Application Backend.
Everything was great.
Except that it didn't work.
The chain-indexer would periodically fall-over,
it required users had to maintain a full node,
and the api never matured in to something stable, complete, and bug-free.
In addition, the validators it produced were bloated and un-optimized and would quickly hit the constraints of the cardano blockchain.
As a result teams turned to coming up with alternatives.
One of the first was MLabs and co in creating pluto and plutarch.
Later Aiken appeared to meet similar needs.
These began resolving issues with the on-chain part.
This left the off-chain part wanting.

View File

@ -1,16 +0,0 @@
document.addEventListener('DOMContentLoaded', function () {
// Get all "navbar-burger" elements
var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach(function ($el) {
$el.addEventListener('click', function () {
// Get the "main-nav" element
var $target = document.getElementById('main-nav');
// Toggle the class on "main-nav"
$target.classList.toggle('hidden');
});
});
}
});

View File

@ -2,15 +2,14 @@
"nodes": {
"devshell": {
"inputs": {
"nixpkgs": "nixpkgs",
"systems": "systems"
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1688380630,
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=",
"lastModified": 1735644329,
"narHash": "sha256-tO3HrHriyLvipc4xr+Ewtdlo7wM1OjXNjlWRgmM7peY=",
"owner": "numtide",
"repo": "devshell",
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205",
"rev": "f7795ede5b02664b57035b3b757876703e2c3eac",
"type": "github"
},
"original": {
@ -24,11 +23,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"lastModified": 1738453229,
"narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
"type": "github"
},
"original": {
@ -38,11 +37,11 @@
},
"flake-root": {
"locked": {
"lastModified": 1680964220,
"narHash": "sha256-dIdTYcf+KW9a4pKHsEbddvLVSfR1yiAJynzg2x0nfWg=",
"lastModified": 1723604017,
"narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=",
"owner": "srid",
"repo": "flake-root",
"rev": "f1c0b93d05bdbea6c011136ba1a135c80c5b326c",
"rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e",
"type": "github"
},
"original": {
@ -53,11 +52,11 @@
},
"haskell-flake": {
"locked": {
"lastModified": 1684180957,
"narHash": "sha256-qtEZf4gcmQU5ePbFtltqpAS0PajWLURVC7nuoS46dSk=",
"lastModified": 1739669127,
"narHash": "sha256-2s3wYTqKq7aBa41VHWg/G2XAOii8MW+WAMtLdgy1cek=",
"owner": "srid",
"repo": "haskell-flake",
"rev": "4e1c76de8795608bb47295c018b37a563c492fd2",
"rev": "eabf8cf32e5f6a267ea637e1b3eabc9b7ddf29e1",
"type": "github"
},
"original": {
@ -68,11 +67,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1677383253,
"narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=",
"lastModified": 1722073938,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9952d6bc395f5841262b006fbace8dd7e143b634",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"type": "github"
},
"original": {
@ -84,29 +83,23 @@
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
"lastModified": 1738452942,
"narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1684385584,
"narHash": "sha256-O7y0gK8OLIDqz+LaHJJyeu09IGiXlZIS3+JgEzGmmJA=",
"lastModified": 1739446958,
"narHash": "sha256-+/bYK3DbPxMIvSL4zArkMX0LQvS7rzBKXnDXLfKyRVc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48a0fb7aab511df92a17cf239c37f2bd2ec9ae3a",
"rev": "2ff53fe64443980e139eaa286017f53f88336dd0",
"type": "github"
},
"original": {
@ -118,16 +111,16 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1680945546,
"narHash": "sha256-8FuaH5t/aVi/pR1XxnF0qi4WwMYC+YxlfdsA0V+TEuQ=",
"lastModified": 1735554305,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d9f759f2ea8d265d974a6e1259bd510ac5844c5d",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
@ -142,31 +135,16 @@
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1684416994,
"narHash": "sha256-KkZ9diPRl3Y05TngWYs/QhZKnI/3tA3s+2Hhmei8FnE=",
"lastModified": 1738953846,
"narHash": "sha256-yrK3Hjcr8F7qS/j2F+r7C7o010eVWWlm4T1PrbKBOxQ=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "42045102f90cfd23ca44ae4ef8362180fefcd7fd",
"rev": "4f09b473c936d41582dd744e19f34ec27592c5fd",
"type": "github"
},
"original": {

152
flake.nix
View File

@ -9,7 +9,7 @@
flake-root.url = "github:srid/flake-root";
};
outputs = inputs@{ flake-parts, ... }:
outputs = inputs @ { flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
# To import a flake module
@ -22,70 +22,118 @@
inputs.flake-root.flakeModule
];
systems = [ "x86_64-linux" "aarch64-darwin" ];
perSystem = { config, self', inputs', pkgs, system, ... }: {
# Per-system attributes can be defined here. The self' and inputs'
# module parameters provide easy access to attributes of the same
# system.
perSystem =
{ config
, self'
, inputs'
, pkgs
, system
, ...
}: {
# Per-system attributes can be defined here. The self' and inputs'
# module parameters provide easy access to attributes of the same
# system.
haskellProjects.default = {
# packages.haskell-template.root = ./.; # Auto-discovered by haskell-flake
overrides = self: super: { };
devShell = {
tools = hp: {
treefmt = config.treefmt.build.wrapper;
} // config.treefmt.build.programs;
hlsCheck.enable = false;
haskellProjects.default = {
devShell = {
tools = hp:
{
fourmolu = hp.fourmolu;
hoogle = hp.hoogle;
haskell-language-server = hp.haskell-language-server;
treefmt = config.treefmt.build.wrapper;
}
// config.treefmt.build.programs;
hlsCheck.enable = false;
};
autoWire = [ "packages" "apps" "checks" ]; # Wire all but the devShell
};
autoWire = [ "packages" "apps" "checks" ]; # Wire all but the devShell
};
packages.default = self'.packages.kompact-site;
packages.default = self'.packages.kompact-site;
treefmt.config = {
inherit (config.flake-root) projectRootFile;
package = pkgs.treefmt;
flakeFormatter = false; # For https://github.com/numtide/treefmt-nix/issues/55
treefmt.config = {
inherit (config.flake-root) projectRootFile;
package = pkgs.treefmt;
flakeFormatter = false; # For https://github.com/numtide/treefmt-nix/issues/55
programs.ormolu.enable = true;
programs.nixpkgs-fmt.enable = true;
programs.cabal-fmt.enable = true;
programs.hlint.enable = true;
programs.ormolu.enable = true;
programs.nixpkgs-fmt.enable = true;
programs.cabal-fmt.enable = true;
programs.hlint.enable = true;
programs.alejandra.enable = true;
# We use fourmolu
programs.ormolu.package = pkgs.haskellPackages.fourmolu;
settings.formatter.ormolu = {
options = [
"--ghc-opt"
"-XImportQualifiedPost"
];
# We use fourmolu
programs.ormolu.package = pkgs.haskellPackages.fourmolu;
settings.formatter.ormolu = {
options = [
"--ghc-opt"
"-XImportQualifiedPost"
];
};
programs.prettier = {
enable = true;
settings = {
printWidth = 80;
proseWrap = "always";
};
};
};
programs.prettier.enable = true;
};
# Equivalent to inputs'.nixpkgs.legacyPackages.hello;
devShells.default = pkgs.mkShell {
inputsFrom = [
config.haskellProjects.default.outputs.devShell
config.flake-root.devShell
];
packages = with pkgs; [
caddy
nil
nodePackages_latest.vscode-langservers-extracted
nodePackages_latest.tailwindcss
nodePackages_latest.typescript-language-server
haskellPackages.hakyll
zlib
];
# Equivalent to inputs'.nixpkgs.legacyPackages.hello;
devShells.default =
let
menu =
pkgs.writeShellScriptBin "menu"
''
echo -e "\nCommands available: \n${
builtins.foldl' (x: y: x + " -> " + (pkgs.lib.getName y) + "\n") "" my-packages
}"
'';
my-packages = [
menu
build
watch
tailwind
deploy
];
tailwind = pkgs.writeShellScriptBin "tailwind" ''
tailwindcss -i ./content/css/main.css -o ./assets/css/mini.css --minify
'';
build = pkgs.writeShellScriptBin "build" ''
${tailwind}/bin/tailwind
cabal run site -- build
'';
watch = pkgs.writeShellScriptBin "watch" ''
${tailwind}/bin/tailwind
cabal run site -- watch
'';
deploy = pkgs.writeShellScriptBin "deploy" ''
rsync -r --delete ./docs/* genesis:/var/www/kompactio-landing/
'';
in
pkgs.mkShell {
inputsFrom = [
config.haskellProjects.default.outputs.devShell
config.flake-root.devShell
];
packages = with pkgs;
[
caddy
nil
nodePackages_latest.vscode-langservers-extracted
nodePackages_latest.tailwindcss
nodePackages_latest.typescript-language-server
haskellPackages.hakyll
zlib
]
++ my-packages;
};
};
};
flake = {
# The usual flake attributes can be defined here, including system-
# agnostic ones like nixosModule and system-enumerating ones, although
# those are more easily expressed in perSystem.
};
};
}

View File

@ -1,13 +0,0 @@
name: example
version: 0.1.0.0
build-type: Simple
cabal-version: >= 1.10
executable site
main-is: site.hs
build-depends: base == 4.*
, hakyll == 4.15.*
, hip == 1.5.*
, filepath
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010

15
site.cabal Normal file
View File

@ -0,0 +1,15 @@
name: site
version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10
executable site
main-is: site.hs
build-depends:
base >=4 && <5
, filepath
, hakyll >=4.16 && <4.17
, hip >=1.5 && <1.6
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010

56
site.hs
View File

@ -1,55 +1,62 @@
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid (mappend)
import Hakyll
import System.FilePath (splitExtension, joinPath, splitDirectories, replaceExtension)
import Data.Monoid (mappend)
import Hakyll
import System.FilePath (joinPath, replaceExtension, splitDirectories, splitExtension)
--------------------------------------------------------------------------------
myConfiguration :: Configuration
myConfiguration =
defaultConfiguration
{ destinationDirectory = "docs"
}
main :: IO ()
main = hakyll $ do
match "content/favicon.png" $ do
main = hakyllWith myConfiguration $ do
match "assets/favicon.png" $ do
route rmPrefix
compile copyFileCompiler
match "content/images/*" $ do
match "assets/images/*" $ do
route rmPrefix
compile copyFileCompiler
match "content/scripts/*" $ do
route rmPrefix
match "assets/scripts/*" $ do
route rmPrefix
compile copyFileCompiler
match "content/css/*" $ do
route rmPrefix
match "assets/css/*" $ do
route rmPrefix
compile compressCssCompiler
match "content/fonts/*" $ do
route rmPrefix
match "assets/fonts/*" $ do
route rmPrefix
compile copyFileCompiler
match "content/posts/*.md" $ do
matchMetadata "content/posts/*.md" ((Just "true" /=) . lookupString "draft") $ do
route rmPrefixMd
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
compile $
pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
create ["blog.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "content/posts/*.md"
let archiveCtx =
listField "posts" postCtx (return posts) `mappend`
constField "title" "Blog" `mappend`
defaultContext
listField "posts" postCtx (return posts)
`mappend` constField "title" "Blog"
`mappend` defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/blog.html" archiveCtx
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
>>= relativizeUrls
match "content/index.md" $ do
route rmPrefixMd
compile $ do
@ -63,18 +70,17 @@ main = hakyll $ do
match "templates/*" $ compile templateBodyCompiler
--------------------------------------------------------------------------------
postCtx :: Context String
postCtx =
dateField "date" "%Y-%m-%d" `mappend`
defaultContext
dateField "date" "%Y-%m-%d"
`mappend` defaultContext
setExtensionInner :: String -> FilePath -> FilePath
setExtensionInner = flip replaceExtension
rmPrefixInner :: FilePath -> FilePath
rmPrefixInner = joinPath . tail . splitDirectories
rmPrefixInner = joinPath . tail . splitDirectories
rmPrefix :: Routes
rmPrefix = customRoute $ rmPrefixInner . toFilePath

View File

@ -1,19 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./content/**/*.{html,js}",
"./templates/**/*.{html,js}",
],
content: ["./content/**/*.{html,js}", "./templates/**/*.{html,js}"],
theme: {
extend: {},
fontFamily: {
'sans' : ['jetbrains-mono',],
sans: ["jetbrains-mono"],
},
typography: (theme) => ({}),
},
darkMode: 'class',
darkMode: "class",
variants: {},
// plugins: [require('@tailwindcss/typography')],
plugins: [],
}
};

View File

@ -1,27 +1,15 @@
<section id="about" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# about
</header>
<header class="text-3xl"># about</header>
<div>
Kompact.io is dapp dev house.
Our focus:
Kompact.io is dapp dev house. Our focus:
<ul class="list-decoration">
<li>
safety-first
</li>
<li>
fast turn around
</li>
<li>
integration support
</li>
<li>safety-first</li>
<li>fast turn around</li>
<li>integration support</li>
</ul>
<div>
Our typical process:
<div>
Idea -> Spec -> Impl -> Test -> Handover
</div>
<div>Idea -> Spec -> Impl -> Test -> Handover</div>
</div>
</div>
</section>
</section>

View File

@ -1,7 +1,5 @@
<section id="services" class="py-6 px-2 flex flex-col gap-12">
<header class="text-3xl">
# blog
</header>
<header class="text-3xl"># blog</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
A nascent initiative sharing some of the things happening at Kompact.io.
</div>
@ -9,4 +7,4 @@
<section class="py-6 px-2 flex flex-col gap-12">
$partial("templates/post-list.html")$
</section>
</section>

View File

@ -1,11 +1,10 @@
<section id="contact" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# contact
</header>
<header class="text-3xl"># contact</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Questions? We'll be happy to help answer any of your questions. Send us an email and we'll get back to you shortly.
Questions? We'll be happy to help answer any of your questions. Send us an
email and we'll get back to you shortly.
</div>
<div>
Reach us on : <a href="mailto:kompactio@proton.me">kompactio@proton.me</a>
</div>
</section>
</section>

View File

@ -1,40 +1,41 @@
<!doctype html>
<html class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.png">
<link href="/css/mini.css" rel="stylesheet">
<title>$title$</title>
</head>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
function updateTheme() {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.png" />
<link href="/css/mini.css" rel="stylesheet" />
<link href="/css/prism.css" rel="stylesheet" />
<title>$title$</title>
</head>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
function updateTheme() {
if (
localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
updateTheme();
</script>
}
updateTheme()
</script>
<body
class="bg-white text-gray-900 min-h-screen dark:bg-gradient-to-br dark:from-slate-950 dark:to-black dark:text-white"
>
<div class="container mx-auto">
<hr />
$partial("templates/nav.html")$
<hr />
$body$
<hr />
$partial("templates/footer.html")$
</div>
</body>
<body class="bg-white text-gray-900 min-h-screen
dark:bg-gradient-to-br dark:from-slate-950 dark:to-black dark:text-white">
<div class="container mx-auto ">
<hr />
$partial("templates/nav.html")$
<hr />
$body$
<hr />
$partial("templates/footer.html")$
</div>
</body>
</html>
<script src="/scripts/prism.js"></script>
</html>

View File

@ -1,22 +1,34 @@
<section id="footer" class="py-12 px-2 flex flex-row gap-12 mx-2 sm:mx-4 items-start justify-between
text-gray-800 dark:text-gray-200 dark:fill-white">
<div class="text-sm">
&reg; 2023 kompact.io &trade; All Rights Reserved.
</div>
<section
id="footer"
class="py-12 px-2 flex flex-row gap-12 mx-2 sm:mx-4 items-start justify-between text-gray-800 dark:text-gray-200 dark:fill-white"
>
<div class="text-sm">&reg; 2023 kompact.io &trade; All Rights Reserved.</div>
<div class="flex flex-row gap-4">
<a href="https://www.linkedin.com/in/dominic-algernon-wallis-123b42187/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" height="20" preserveAspectRatio="xMidYMid meet">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
height="20"
preserveAspectRatio="xMidYMid meet"
>
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z" />
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
/>
</svg>
</a>
<a href="https://twitter.com/waalge">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="20" preserveAspectRatio="xMidYMid meet">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
height="20"
preserveAspectRatio="xMidYMid meet"
>
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
/>
</svg>
</a>
</div>
</section>
</section>

View File

@ -1,12 +1,13 @@
<section id="hero" class="py-8 px-2 h-96 min-h-[50vh] m-auto">
<div class="h-full flex justify-around align-center items-center">
<div class="text-6xl">
⟨K⟩
</div>
<div class="text-6xl">⟨K⟩</div>
<div class="flex flex-col gap-2 truncate">
<div>withKompact &#36; <span class="text-red-500 dark:text-yellow-400">do</span> </div>
<div><span class="text-gray-400">· ·</span> dapp <- lean dev </div>
<div><span class="text-gray-400">· ·</span> run dapp </div>
<div>
withKompact &#36;
<span class="text-red-500 dark:text-yellow-400">do</span>
</div>
<div><span class="text-gray-400">· ·</span> dapp <- lean dev</div>
<div><span class="text-gray-400">· ·</span> run dapp</div>
</div>
</section>
</div>
</section>

View File

@ -4,4 +4,4 @@ $partial("templates/services.html")$
<hr />
$partial("templates/pricing.html")$
<hr />
$partial("templates/contact.html")$
$partial("templates/contact.html")$

7
templates/item.html Normal file
View File

@ -0,0 +1,7 @@
<div class="text-1xl font-bold">
$title$
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
$body$
</div>

7
templates/list.html Normal file
View File

@ -0,0 +1,7 @@
<div class="text-1xl font-bold">
$title$
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
$body$
</div>

5
templates/lists.html Normal file
View File

@ -0,0 +1,5 @@
<div class="index flex flex-col justify-between gap-4 sm:gap-8">
$for(sections)$
$body$
$endfor$
</div>

View File

@ -9,7 +9,8 @@
<div>
<ul class="flex flex-row gap-4 md:gap-8">
<li>
<button onClick="
<button
onClick="
(() => {
if (!('theme' in localStorage)) {
localStorage.theme = 'light'
@ -22,19 +23,18 @@
}
updateTheme()
})()
">◧</button>
"
>
</button>
</li>
<li>
<a href="/index.html#contact">
contact
</a>
<a href="/index.html#contact"> contact </a>
</li>
<li>
<a href="/blog.html">
blog
</a>
<a href="/blog.html"> blog </a>
</li>
</ul>
</div>
</div>
</nav>
</nav>

View File

@ -2,11 +2,9 @@
$for(posts)$
<li class="mt-4">
<a href="$url$">
<span class="text-gray-800 dark:text-gray-200">
$date$ ::
</span>
<span class="text-gray-800 dark:text-gray-200"> $date$ :: </span>
$title$
</a>
</li>
$endfor$
</ul>
</ul>

View File

@ -1,20 +1,11 @@
<article>
<article class="mx-auto px-4 max-w-prose">
<section class="header">
<h1>
$title$
</h1>
<h1>$title$</h1>
$if(date)$
<p>
Posted on $date$
</p>
$endif$
$if(author)$
<p>
by $author$
</p>
<p>Posted on $date$</p>
$endif$ $if(author)$
<p>by $author$</p>
$endif$
</section>
<section>
$body$
</section>
</article>
<section>$body$</section>
</article>

View File

@ -1,46 +1,39 @@
<section id="pricing" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# pricing
</header>
<header class="text-3xl"># pricing</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Plutus development has traditionally meant long development schedules, and expensive ( &#36; 25k+/mo FTE) engineers.
We can work with you at competitive rates in either deliverable or retainer based engagements.
Plutus development has traditionally meant long development schedules, and
expensive ( &#36; 25k+/mo FTE) engineers. We can work with you at
competitive rates in either deliverable or retainer based engagements.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-8 md:mx-24">
<div class="max-w-48">
<div class="text-1xl font-bold">
## retainer
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Time-based
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:gap-8">
<div class="flex-1">
<div class="text-1xl font-bold">## retainer</div>
<p class="text-gray-800 dark:text-gray-200 mt-4">Time-based</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Still figuring out your project scope?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Need an extra pair of hands on an existing project?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Then a retainer based engagement is for you.
</div>
</p>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## deliverable
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Output-based
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
<div class="flex-1">
<div class="text-1xl font-bold">## deliverable</div>
<p class="text-gray-800 dark:text-gray-200 mt-4">Output-based</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
You know what you want and need help implementing it?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
We'll first produce a spec on how the dapp will operate technically.
This involves discussing different options and trade-offs on things from UX to validator complexity.
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Once settled we'll begin the implementation phase and finally integration phase.
</div>
This involves discussing different options and trade-offs on things from
UX to validator complexity.
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Once settled we'll begin the implementation phase and finally
integration phase.
</p>
</div>
</div>
</section>
</section>

View File

@ -1,35 +1,30 @@
<section id="services" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# services
</header>
<header class="text-3xl"># services</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We are cardano native dapp dev outfit focused on helping you going from 0 to launch ASAP.
We are cardano native dapp dev outfit focused on helping you going from 0 to
launch ASAP.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3 sm:gap-8">
<div class="max-w-48">
<div class="text-1xl font-bold">
## strategy
</div>
<div class="text-1xl font-bold">## strategy</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We'll work with you to validate your concept, and translate it into an implementable Proof of Concept
We'll work with you to validate your concept, and translate it into an
implementable Proof of Concept
</div>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## implementation
</div>
<div class="text-1xl font-bold">## implementation</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Cook up appropriate Plutus validators to meet your needs
</div>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## deployment
</div>
<div class="text-1xl font-bold">## deployment</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We facilitate integrating the on-chain aspects with the rest of your stack
We facilitate integrating the on-chain aspects with the rest of your
stack
</div>
</div>
</div>
</section>
</section>

View File

@ -3,55 +3,135 @@
<div class="relative flex h-16 items-center justify-between">
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<!-- Mobile menu button-->
<button type="button" class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
<button
type="button"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!--
Icon when menu is closed.
Menu open: "hidden", Menu closed: "block"
-->
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
<svg
class="block h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
<!--
Icon when menu is open.
Menu open: "block", Menu closed: "hidden"
-->
<svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
<svg
class="hidden h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div
class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start"
>
<div class="flex flex-shrink-0 items-center">
<img class="block h-8 w-auto lg:hidden" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
<img class="hidden h-8 w-auto lg:block" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
<img
class="block h-8 w-auto lg:hidden"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
alt="Your Company"
/>
<img
class="hidden h-8 w-auto lg:block"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
alt="Your Company"
/>
</div>
<div class="hidden sm:ml-6 sm:block">
<div class="flex space-x-4">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Team</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Projects</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Calendar</a>
<a
href="#"
class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium"
aria-current="page"
>Dashboard</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Team</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Projects</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Calendar</a
>
</div>
</div>
</div>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<button type="button" class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
<div
class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"
>
<button
type="button"
class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
>
<span class="sr-only">View notifications</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
</button>
<!-- Profile dropdown -->
<div class="relative ml-3">
<div>
<button type="button" class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<button
type="button"
class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true"
>
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
<img
class="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
@ -65,11 +145,38 @@
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
-->
<div class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<div
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1"
>
<!-- Active: "bg-gray-100", Not Active: "" -->
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-0"
>Your Profile</a
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-1"
>Settings</a
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-2"
>Sign out</a
>
</div>
</div>
</div>
@ -80,10 +187,27 @@
<div class="sm:hidden" id="mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium" aria-current="page">Dashboard</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Team</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Projects</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Calendar</a>
<a
href="#"
class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium"
aria-current="page"
>Dashboard</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Team</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Projects</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Calendar</a
>
</div>
</div>
</nav>
</nav>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
@ -12,13 +12,13 @@
/>
<meta name="theme-color" content="#ffffff" />
<title>$title$</title>
<meta
name="description"
content="Lean dapp development"
<meta name="description" content="Lean dapp development" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/normalize.css" />
<link rel="stylesheet" href="/css/terminal.css" />
<link rel="stylesheet" href="/css/custom.css" />