Browse Source

open-sourcing wallet and choo upgrades to build jam files at compile-time

GitHub Actions Bot 7 months ago
parent
commit
6284e1ed8d
40 changed files with 1998 additions and 360 deletions
  1. 6 0
      .gitignore
  2. 1 0
      Cargo.toml
  3. 21 0
      LICENSE
  4. 11 7
      Makefile
  5. 2 2
      README.md
  6. 1 1
      crates/nockapp/apps/choo/Cargo.toml
  7. 1 0
      crates/nockapp/apps/choo/README.md
  8. BIN
      crates/nockapp/apps/choo/bootstrap/choo.jam
  9. 81 33
      crates/nockapp/apps/choo/bootstrap/kernel.hoon
  10. 11 13
      crates/nockapp/apps/choo/src/lib.rs
  11. 2 2
      crates/nockapp/apps/choo/src/main.rs
  12. 2 2
      crates/nockapp/crown/src/drivers/exit.rs
  13. 2 2
      crates/nockapp/crown/src/drivers/file.rs
  14. 12 12
      crates/nockapp/crown/src/drivers/one_punch.rs
  15. 3 3
      crates/nockapp/crown/src/kernel/boot.rs
  16. 2 2
      crates/nockapp/crown/src/kernel/checkpoint.rs
  17. 6 6
      crates/nockapp/crown/src/kernel/form.rs
  18. 13 13
      crates/nockapp/crown/src/nockapp/mod.rs
  19. 114 73
      crates/nockchain-libp2p-io/src/nc.rs
  20. 28 25
      crates/nockchain-libp2p-io/src/p2p.rs
  21. 6 11
      crates/nockchain/src/lib.rs
  22. 26 0
      crates/wallet/Cargo.toml
  23. 120 0
      crates/wallet/README.md
  24. 62 0
      crates/wallet/src/error.rs
  25. 1311 0
      crates/wallet/src/main.rs
  26. 2 9
      hoon/apps/dumbnet/inner.hoon
  27. 0 16
      hoon/apps/dumbnet/lib/admin.hoon
  28. 5 8
      hoon/apps/dumbnet/lib/types.hoon
  29. 18 22
      hoon/apps/wallet/wallet.hoon
  30. 2 2
      hoon/common/nock-common.hoon
  31. 4 3
      hoon/common/nock-prover.hoon
  32. 4 3
      hoon/common/nock-verifier.hoon
  33. 2 2
      hoon/common/pow.hoon
  34. 1 5
      hoon/common/stark/prover.hoon
  35. 2 1
      hoon/common/stark/verifier.hoon
  36. 1 1
      hoon/common/tx-engine.hoon
  37. 7 81
      hoon/common/ztd/eight.hoon
  38. 1 0
      hoon/common/ztd/seven.hoon
  39. 95 0
      hoon/dat/constraints.hoon
  40. 10 0
      hoon/dat/stark-config.hoon

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.data.*
+test-*
+.nockchain-identity
+.nockchain-identity.peerid
+target
+assets/*.jam

+ 1 - 0
Cargo.toml

@@ -12,6 +12,7 @@ members = [
     "crates/sword/rust/murmur3",
     "crates/sword/rust/sword_macros",
     "crates/sword/rust/sword",
+    "crates/wallet",
     "crates/zkvm-jetpack",
 ]
 

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Zorp Corp
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 11 - 7
Makefile

@@ -4,6 +4,7 @@ export RUST_LOG := info,nockchain=debug,nockchain_libp2p_io=info,libp2p=info,lib
 export MINIMAL_LOG_FORMAT := true
 export MINING_PUBKEY := EHmKL2U3vXfS5GYAY5aVnGdukfDWwvkQPCZXnjvZVShsSQi3UAuA4tQQpVwGJMzc9FfpTY8pLDkqhBGfWutiF4prrCktUH9oAWJxkXQBzAavKDc95NR3DjmYwnnw8GuugnK
 
+
 ## Build everything
 .PHONY: build
 build:
@@ -19,6 +20,10 @@ install-choo: nuke-choo-data ## Install choo from this repo
 	$(call show_env_vars)
 	cargo install --locked --force --path crates/nockapp/apps/choo --bin choo
 
+update-choo:
+	$(call show_env_vars)
+	cargo install --locked --path crates/nockapp/apps/choo --bin choo
+
 .PHONY: ensure-dirs
 ensure-dirs:
 	mkdir -p hoon
@@ -46,35 +51,34 @@ build-hoon-fresh: nuke-assets nuke-choo-data install-choo ensure-dirs build-triv
 	$(call show_env_vars)
 
 .PHONY: build-hoon-new
-build-hoon-all: ensure-dirs build-trivial-new $(HOON_TARGETS)
+build-hoon-all: ensure-dirs update-choo build-trivial-new $(HOON_TARGETS)
 	$(call show_env_vars)
 
 .PHONY: build-hoon
-build-hoon: ensure-dirs $(HOON_TARGETS)
+build-hoon: ensure-dirs update-choo $(HOON_TARGETS)
 	$(call show_env_vars)
 
 .PHONY: run-nockchain-leader
 run-nockchain-leader:  # Run nockchain mode in leader mode
 	$(call show_env_vars)
-	mkdir -p test-leader && cd test-leader && RUST_BACKTRACE=1 cargo run --release --bin nockchain -- --fakenet --genesis-leader --npc-socket nockchain.sock --mining-pubkey $(MINING_PUBKEY) --bind /ip4/0.0.0.0/udp/3005/quic-v1 --peer /ip4/127.0.0.1/udp/3006/quic-v1 --new-peer-id --no-default-peers
+	mkdir -p test-leader && cd test-leader && rm -f nockchain.sock && RUST_BACKTRACE=1 cargo run --release --bin nockchain -- --fakenet --genesis-leader --npc-socket nockchain.sock --mining-pubkey $(MINING_PUBKEY) --bind /ip4/0.0.0.0/udp/3005/quic-v1 --peer /ip4/127.0.0.1/udp/3006/quic-v1 --new-peer-id --no-default-peers
 
 .PHONY: run-nockchain-follower
 run-nockchain-follower:  # Run nockchain mode in follower mode
 	$(call show_env_vars)
-	mkdir -p test-follower && cd test-follower && RUST_BACKTRACE=1 cargo run --release --bin nockchain -- --fakenet --genesis-watcher --npc-socket nockchain.sock --mining-pubkey $(MINING_PUBKEY) --bind /ip4/0.0.0.0/udp/3006/quic-v1 --peer /ip4/127.0.0.1/udp/3005/quic-v1 --new-peer-id --no-default-peers
-
+	mkdir -p test-follower && cd test-follower && rm -f nockchain.sock && RUST_BACKTRACE=1 cargo run --release --bin nockchain -- --fakenet --genesis-watcher --npc-socket nockchain.sock --mining-pubkey $(MINING_PUBKEY) --bind /ip4/0.0.0.0/udp/3006/quic-v1 --peer /ip4/127.0.0.1/udp/3005/quic-v1 --new-peer-id --no-default-peers
 
 
 HOON_SRCS := $(find hoon -type file -name '*.hoon')
 
 ## Build dumb.jam with choo
-assets/dumb.jam: hoon/apps/dumbnet/outer.hoon $(HOON_SRCS)
+assets/dumb.jam: update-choo hoon/apps/dumbnet/outer.hoon $(HOON_SRCS)
 	$(call show_env_vars)
 	RUST_LOG=trace choo hoon/apps/dumbnet/outer.hoon hoon
 	mv out.jam assets/dumb.jam
 
 ## Build wal.jam with choo
-assets/wal.jam: hoon/apps/wallet/wallet.hoon $(HOON_SRCS)
+assets/wal.jam: update-choo hoon/apps/wallet/wallet.hoon $(HOON_SRCS)
 	$(call show_env_vars)
 	RUST_LOG=trace choo hoon/apps/wallet/wallet.hoon hoon
 	mv out.jam assets/wal.jam

+ 2 - 2
README.md

@@ -39,6 +39,8 @@ To run a Nockchain node that waits for the genesis block:
 make run-nockchain-follower
 ```
 
+To run the wallet, see the wallet [README](./crates/wallet/README.md).
+
 
 To run the test suite:
 
@@ -46,5 +48,3 @@ To run the test suite:
 make test
 ```
 
-
-

+ 1 - 1
crates/nockapp/apps/choo/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "choo"
-version = "0.1.9"
+version = "0.2.0"
 edition.workspace = true
 
 [dependencies]

+ 1 - 0
crates/nockapp/apps/choo/README.md

@@ -55,6 +55,7 @@ runes:
 - `/-` load from `/sur` (currently discouraged in NockApps)
 - `/=` load from specified path (required `%hoon` mark)
 - `/*` load from specified path via specified mark (presumptively `%hoon` or `%jock`)
+- `/#` load and kick from `/dat`. Used when you have some nock computation you want to precompute.
 - `/?` version pinning (ignored)
 
 ## Developer Troubleshooting

BIN
crates/nockapp/apps/choo/bootstrap/choo.jam


+ 81 - 33
crates/nockapp/apps/choo/bootstrap/kernel.hoon

@@ -1,13 +1,21 @@
 /+  *wrapper
 =>
 |%
-+$  state-0  [%0 cached-hoon=(unit (trap vase)) ~]
-+$  state-1  [%1 cached-hoon=(unit (trap vase)) bc=build-cache pc=parse-cache]
++$  state-0  [%0 *]
++$  state-1  [%1 *]
++$  state-2  [%2 cached-hoon=(unit (trap vase)) bc=build-cache pc=parse-cache]
+::
+++  empty-trap-vase
+  ^-  (trap vase)
+  =>  vaz=!>(~)
+  |.(vaz)
+::
 +$  versioned-state
   $%  state-0
       state-1
+      state-2
   ==
-+$  choo-state  state-1
++$  choo-state  state-2
 ::
 ++  moat  (keep choo-state)
 +$  cause
@@ -33,6 +41,8 @@
 +$  entry  [pat=path tex=(unit cord)]
 ::
 +$  hash  @
+::
+::  $build-cache: holds up to date cached build artifacts, keyed by merkle hash
 +$  build-cache  (map hash (trap vase))
 ::
 ::  $build-result: result of a build
@@ -51,10 +61,12 @@
   $:  sur=(list taut)  ::  /-
       lib=(list taut)  ::  /+
       raw=(list [face=(unit term) pax=path])  ::  /=
-      bar=(list [face=term mark=@tas =path])
+      bar=(list [face=term mark=@tas =path])  ::  /*
+      hax=(list taut)                         ::  /#
       =hoon
   ==
 ::
+::
 ::  $parse-cache: content addressed cache of preprocessed hoon files.
 ::
 +$  parse-cache  (map hash pile)
@@ -77,23 +89,24 @@
   ::  We do not use the result of the soft because
   ::  clamming (trap vase) overwrites the contents
   ::  with the bunt resulting in the honc and the build
-  ::  artifacts being replaced with *(trap vase).
+  ::  artifacts being replaced with empty-trap-vase.
   ::
   ?~  ((soft versioned-state) old)
-    ~&  "choo: +load old state does not nest under versioned-state"
+    ~>  %slog.[0 leaf+"choo: +load old state does not nest under versioned-state. Try booting with --new to start from scratch."]
     !!
   ?-    -.old
       %0
-    ~&  >>  %upgrade-0-to-1
-    :*  %1
-        cached-hoon.old
-        *build-cache
-        *parse-cache
-    ==
+    ~>  %slog.[0 leaf+"update 0-to-2, starting from scratch"]
+    *choo-state
   ::
       %1
-    ~&  >>  %no-upgrade
+    ~>  %slog.[0 leaf+"update 1-to-2, starting from scratch"]
+    *choo-state
+  ::
+      %2
+    ~>  %slog.[0 leaf+"no update"]
     old
+  ::
   ==
 ::
 ::  +peek: external inspect
@@ -110,9 +123,10 @@
   ^-  [(list effect) choo-state]
   =/  cause=(unit cause)  ((soft cause) dat)
   ?~  cause
-    ~&  >>>  "input is not a proper cause"
+    ~&  "input is not a proper cause"
     !!
   =/  cause  u.cause
+  ~&  -.cause
   ?-    -.cause
       %file
     ?:  success.cause
@@ -149,7 +163,7 @@
           path=out.cause
           contents=(jam compiled)
       ==
-    =/  success  !=(compiled *(trap vase))
+    =/  success  !=(compiled empty-trap-vase)
     ?:  success
       ~&  >>>  "choo: build succeeded, sending out write effect"
       [write-effect]~
@@ -174,6 +188,7 @@
       lib=(list raut)
       raw=(list raut)
       bar=(list raut)
+      hax=(list raut)
       =hoon
   ==
 ::
@@ -267,6 +282,10 @@
       ;~(pfix cen sym)
       ;~(pfix stap)
     ==
+  ::
+    %+  cook  (bake zing (list (list taut)))
+    %+  rune  hax
+    (most ;~(plug com gaw) taut-rule)
   ::
     %+  stag  %tssg
     (most gap tall:(vang & pax))
@@ -353,6 +372,8 @@
     =/  pax-rear  (rear pax)
     ^-  raut
     [`face `path`(snoc (snip pax-snip) `@ta`(rap 3 ~[pax-hind %'.' pax-rear]))]
+  ::
+    (turn hax.pile |=(taut ^-(raut [face (need (get-fit %dat pax dir))])))
   ==
 --
 ::
@@ -363,7 +384,7 @@
 ++  build-honc
   |=  hoon-txt=cord
   ^-  (trap vase)
-  (swet *(trap vase) (ream hoon-txt))
+  (swet empty-trap-vase (ream hoon-txt))
 ::
 +$  octs  [p=@ud q=@]
 ::
@@ -375,6 +396,7 @@
       ::  holds only outgoing edges
       deps=(list raut)
       leaf=graph-leaf
+      eval=?  :: whether or not to kick it
   ==
 ::
 +$  graph-leaf
@@ -393,7 +415,7 @@
 ::    returns a trap, a build-cache, and a parse-cache
 ++  create
   |=  [=entry dir=(map path cord)]
-  ^-  [(trap) build-cache parse-cache]
+  ^-  [* build-cache parse-cache]
   =/  dir-hash  `@uvI`(mug dir)
   ~&  >>  dir-hash+dir-hash
   =/  compile
@@ -403,7 +425,7 @@
   ::  +shot calls the kernel gate to tell it the hash of the dependency directory
   :_  [build-cache parse-cache]
   ::  build failure, just return the bunted trap
-  ?:  =(ker-gen *(trap vase))  ker-gen
+  ?:  =(ker-gen empty-trap-vase)  ker-gen
   =>  %+  shot  ker-gen
     =>  d=!>(dir-hash)
     |.(d)
@@ -413,15 +435,14 @@
 ::
 ::    .entry: the entry to build
 ::    .dir: the directory to get dependencies from
-::
 ::    returns a trap, a build-cache, and a parse-cache
 ++  create-arbitrary
    |=  [=entry dir=(map path cord)]
-   ^-  [(trap) build-cache parse-cache]
+   ^-  [* build-cache parse-cache]
    =/  [tase=(trap) =build-cache =parse-cache]
      (create-target entry dir)
    :_  [build-cache parse-cache]
-   ?:  =(tase *(trap vase))
+   ?:  =(tase empty-trap-vase)
      tase
    =>  tase
    |.(+:^$)
@@ -435,7 +456,8 @@
 ++  create-target
   |=  [=entry dir=(map path cord)]
   ^-  [(trap vase) build-cache parse-cache]
-  =/  [parsed-dir=(map path node) pc=parse-cache]  (parse-dir entry dir)
+  =^  parsed-dir=(map path node)  pc
+    (parse-dir entry dir)
   =/  all-nodes=(map path node)  parsed-dir
   =/  [dep-dag=merk-dag =path-dag]  (build-merk-dag all-nodes)
   ::
@@ -467,57 +489,71 @@
 ++  parse-dir
   |=  [suf=entry dir=(map path cord)]
   ^-  [(map path node) parse-cache]
+  =|  new-pc=parse-cache
   ~&  >  parsing+pat.suf
   |^
   =/  file=cord  (get-file suf dir)                   ::  get target file
   =/  hash=@  (shax file)                             ::  hash target file
-  =/  target=node
+  =^  target=node  new-pc
     ?.  (is-hoon pat.suf)
+      :_  new-pc
       :*  pat.suf                                       ::  path
           hash                                          ::  hash
           ~                                             ::  deps
           [%octs [(met 3 file) file]]                   ::  octs
+          %.n                                           ::  eval
       ==
-    =/  =pile  (parse-pile pat.suf (trip file))         ::  parse target file
+    =/  =pile
+      ?:  (~(has by pc) hash)
+        ~&  parse-cache-hit+pat.suf
+        (~(got by pc) hash)
+      ~&  parse-cache-miss+pat.suf
+      (parse-pile pat.suf (trip file))         ::  parse target file
     =/  deps=(list raut)  (resolve-pile pile dir)       ::  resolve deps
+    :_  (~(put by new-pc) hash pile)
     :*  pat.suf                                         ::  path
         hash                                            ::  hash
         deps                                            ::  deps
         [%hoon hoon.pile]                               ::  hoon
+        (is-dat pat.suf)                                ::  eval
     ==
   =|  nodes=(map path node)                             ::  init empty node map
   =.  nodes  (~(put by nodes) pat.suf target)           ::  add target node
   =/  seen=(set path)  (~(put in *(set path)) pat.suf)
-  (resolve-all nodes seen deps.target)
+  (resolve-all nodes seen deps.target new-pc)
   ::
   ++  resolve-all
-    |=  [nodes=(map path node) seen=(set path) deps=(list raut)]
+    |=  [nodes=(map path node) seen=(set path) deps=(list raut) new-pc=parse-cache]
     ^-  [(map path node) parse-cache]
-    ?~  deps  [nodes pc]                                ::  done if no deps
+    ?~  deps  [nodes new-pc]                            ::  done if no deps
     ?.  (~(has in seen) pax.i.deps)                     ::  skip if seen
       ~&  >>  parsing+pax.i.deps
       =/  dep-file  (get-file [pax.i.deps ~] dir)       ::  get dep file
       =/  dep-hash  (shax dep-file)                     ::  hash dep file
-      =^  dep-node=node  pc
+      =^  dep-node=node  new-pc
         ?.  (is-hoon pax.i.deps)
           :_  pc
           :*  pax.i.deps                                  ::  path
               dep-hash                                    ::  hash
               ~                                           ::  deps
               [%octs [(met 3 dep-file) dep-file]]         ::  octs
+              %.n
           ==
         =/  dep-pile
           ?:  (~(has by pc) dep-hash)                     ::  check cache
+            ~&  parse-cache-hit+pax.i.deps
             (~(got by pc) dep-hash)
+          ~&  parse-cache-miss+pax.i.deps
           (parse-pile pax.i.deps (trip dep-file))         ::  parse dep file
         ~&  >>  parsed+pax.i.deps
         =/  dep-deps  (resolve-pile dep-pile dir)         ::  resolve dep deps
         ~&  >>  resolved+pax.i.deps
-        :_  (~(put by pc) dep-hash dep-pile)              ::  cache parse
+        :_  (~(put by new-pc) dep-hash dep-pile)              ::  cache parse
         :*  pax.i.deps
             dep-hash
             dep-deps
             [%hoon hoon.dep-pile]
+            (is-dat pax.i.deps)                             ::  eval
         ==
       =.  nodes  (~(put by nodes) pax.i.deps dep-node)  ::  add dep node
       =.  seen  (~(put in seen) pax.i.deps)             ::  mark as seen
@@ -525,6 +561,7 @@
         nodes  nodes
         seen   seen
         deps   (weld t.deps deps.dep-node)                   ::  add new deps
+        new-pc  new-pc
       ==
     $(deps t.deps)                                      ::  next dep
   ::
@@ -642,14 +679,14 @@
   =/  next=(map path node)  (update-next nodes graph)
   =|  failed=_|
   |-  ^-  [(trap vase) build-cache]
-  ?:  failed  [*(trap vase) bc]
+  ?:  failed  [empty-trap-vase bc]
   ?:  .=(~ next)
     =/  [=build-result new-bc=build-cache]
       (compile-node n path-dag bc)
     ?-  -.build-result
       ::
       %|  ~&  >>>  "compile-target: failed: {<pat>}"
-          [*(trap vase) new-bc]
+          [empty-trap-vase new-bc]
       ::
       %&  [p.build-result new-bc]
     ==
@@ -719,11 +756,18 @@
         ::  because imports have higher precedence when resolving faces.
         ::  To avoid shadowing issues with hoon.hoon, attach faces to your
         ::  imports or avoid shadowed names altogether.
-        (swet (slat dep-vaz honc) hoon.leaf.n)
+        =/  swetted=(trap vase)  (swet (slat dep-vaz honc) hoon.leaf.n)
+        ?.  eval.n
+          swetted
+        ~&  "node {<path.n>} is eval, kicking"
+        =>  [swetted=swetted vase=vase]
+        =/  vaz=vase  $:swetted
+        =>  vaz=vaz
+        |.(vaz)
       =>  octs=!>(octs.leaf.n)
       |.(octs)
     %+  roll  deps.n
-    |=  [r=raut vaz=(trap vase)]
+    |:  [r=`raut`*raut vaz=empty-trap-vase]
     ~&  >  grabbing-dep+pax.r
     =/  [dep-hash=@ dep-node=node]
       ~|  "couldn't find dep hash for {<pax.r>}"
@@ -864,4 +908,8 @@
   =/  end  (rear pax)
   !=(~ (find ".hoon" (trip end)))
 ::
+++  is-dat
+  |=  pax=path
+  ^-  ?
+  =('dat' (head pax))
 --

+ 11 - 13
crates/nockapp/apps/choo/src/lib.rs

@@ -62,7 +62,7 @@ pub async fn choo_data_dir() -> PathBuf {
     choo_data_dir
 }
 
-/// Builds and interprets a Hoon generator to produce a list of pokes
+/// Builds and interprets a Hoon generator.
 ///
 /// This function:
 /// 1. Builds the specified Hoon generator into a jam
@@ -257,11 +257,9 @@ pub async fn initialize_choo_(
             directory_noun = T(&mut slab, &[entry_cell, directory_noun]);
         }
     }
-    let arbitrary_noun = if arbitrary { D(0) } else { D(1) };
 
     let out_path_string = if let Some(path) = &out {
         let parent = path.parent().unwrap_or_else(|| Path::new("."));
-        // TODO: this breaks on everything except Bazel if an output path is specified
         let filename = path.file_name().unwrap_or_else(|| OsStr::new(OUT_JAM_NAME));
         let parent_canonical = canonicalize_and_string(parent);
         format!("{}/{}", parent_canonical, filename.to_string_lossy())
@@ -272,12 +270,13 @@ pub async fn initialize_choo_(
     debug!("Output path: {:?}", out_path_string);
     let out_path = Atom::from_value(&mut slab, out_path_string.clone())?.as_noun();
 
+    let arbitrary_noun = if arbitrary { D(0) } else { D(1) };
+
     let poke = T(
         &mut slab,
         &[D(tas!(b"build")), entry_path, entry_contents, directory_noun, arbitrary_noun, out_path],
     );
     slab.set_root(poke);
-
     nockapp
         .add_io_driver(crown::one_punch_driver(slab, Operation::Poke))
         .await;
@@ -299,19 +298,18 @@ pub fn is_valid_file_or_dir(entry: &DirEntry) -> bool {
         })
         .is_dir();
 
-    let is_hoon = entry
+    let is_valid = entry
         .file_name()
         .to_str()
-        .map(|s| s.ends_with(".hoon"))
-        .unwrap_or(false);
-
-    let is_jock = entry
-        .file_name()
-        .to_str()
-        .map(|s| s.ends_with(".jock"))
+        .map(|s| {
+            s.ends_with(".jock")
+                || s.ends_with(".hoon")
+                || s.ends_with(".txt")
+                || s.ends_with(".jam")
+        })
         .unwrap_or(false);
 
-    is_dir || is_hoon || is_jock
+    is_dir || is_valid
 }
 
 #[instrument]

+ 2 - 2
crates/nockapp/apps/choo/src/main.rs

@@ -1,13 +1,13 @@
+use choo::*;
 use clap::Parser;
 use crown::kernel::boot;
 use futures::FutureExt;
-
-use choo::*;
 use sword::mem::{AllocationError, NewStackError};
 
 #[tokio::main]
 async fn main() -> Result<(), Error> {
     let cli = ChooCli::parse();
+
     boot::init_default_tracing(&cli.boot.clone());
     let result = std::panic::AssertUnwindSafe(async {
         let (nockapp, _) = initialize_choo(cli).await?;

+ 2 - 2
crates/nockapp/crown/src/drivers/exit.rs

@@ -1,6 +1,6 @@
 use crate::nockapp::driver::{make_driver, IODriverFn};
 use crate::NounExt;
-use tracing::{error, info};
+use tracing::{debug, error};
 
 /// Creates an IO driver function for handling exit signals.
 ///
@@ -12,7 +12,7 @@ use tracing::{error, info};
 /// An `IODriverFn` that can be used with the NockApp to handle exit signals.
 pub fn exit() -> IODriverFn {
     make_driver(|handle| async move {
-        info!("exit_driver: waiting for effect");
+        debug!("exit_driver: waiting for effect");
         loop {
             tokio::select! {
                 eff = handle.next_effect() => {

+ 2 - 2
crates/nockapp/crown/src/drivers/file.rs

@@ -5,7 +5,7 @@ use crate::noun::FromAtom;
 use crate::AtomExt;
 use sword::noun::{IndirectAtom, Noun, D, NO, T, YES};
 use sword_macros::tas;
-use tracing::{error, info};
+use tracing::{debug, error};
 
 pub enum FileWire {
     Read,
@@ -110,7 +110,7 @@ pub fn file() -> IODriverFn {
                     };
                     let path = path_atom.into_string()?;
                     let contents = contents_atom.as_ne_bytes();
-                    info!("file driver: writing {} bytes to: {}", contents.len(), path);
+                    debug!("file driver: writing {} bytes to: {}", contents.len(), path);
 
                     // Create parent directories if they don't exist
                     if let Some(parent) = std::path::Path::new(&path).parent() {

+ 12 - 12
crates/nockapp/crown/src/drivers/one_punch.rs

@@ -5,7 +5,7 @@ use crate::noun::slab::NounSlab;
 use either::Either::{self, Left, Right};
 use sword::noun::D;
 use sword_macros::tas;
-use tracing::{debug, error, info};
+use tracing::debug;
 
 pub enum OnePunchWire {
     Poke,
@@ -34,7 +34,7 @@ pub fn one_punch_man(data: NounSlab, op: Operation) -> IODriverFn {
             },
             _ = tokio::time::sleep(tokio::time::Duration::from_secs(600)) => {
                 //TODO what is a good timeout for tests?
-                info!("poke_once_driver: no effect received after 10 minutes");
+                debug!("poke_once_driver: no effect received after 10 minutes");
                 Err(NockAppError::Timeout)
             }
         }
@@ -65,29 +65,29 @@ async fn handle_result(
     match op {
         Operation::Poke => match result {
             Left(PokeResult::Ack) => {
-                info!("Poke successful");
+                debug!("Poke successful");
                 Ok(())
             }
             Left(PokeResult::Nack) => {
-                error!("Poke nacked");
+                debug!("Poke nacked");
                 Err(NockAppError::PokeFailed)
             }
             Right(_) => {
-                error!("Unexpected result for poke operation");
+                debug!("Unexpected result for poke operation");
                 Err(NockAppError::UnexpectedResult)
             }
         },
         Operation::Peek => match result {
             Left(_) => {
-                error!("Unexpected result for peek operation");
+                debug!("Unexpected result for peek operation");
                 Err(NockAppError::UnexpectedResult)
             }
             Right(Some(peek_result)) => {
-                info!("Peek result: {:?}", peek_result);
+                debug!("Peek result: {:?}", peek_result);
                 Ok(())
             }
             Right(_) => {
-                error!("Peek returned no result");
+                debug!("Peek returned no result");
                 Err(NockAppError::PeekFailed)
             }
         },
@@ -141,15 +141,15 @@ async fn handle_effect(
             {
                 tas!(b"gossip") => {
                     // Ignore gossip data
-                    info!("Ignoring gossip data");
+                    debug!("Ignoring gossip data");
                 }
                 tas!(b"request") => {
-                    info!("Processing request effect");
+                    debug!("Processing request effect");
                     let request_data = npc_effect_cell.tail();
-                    info!("Request data: {:?}", request_data);
+                    debug!("Request data: {:?}", request_data);
                     // handle.poke(create_response(request_data)).await?;
                 }
-                _ => info!("Received unknown npc effect"),
+                _ => debug!("Received unknown npc effect"),
             }
         }
     }

+ 3 - 3
crates/nockapp/crown/src/kernel/boot.rs

@@ -246,17 +246,17 @@ pub async fn setup_(
 
     if !jams_dir.exists() {
         std::fs::create_dir_all(&jams_dir)?;
-        info!("Created jams directory: {:?}", jams_dir);
+        debug!("Created jams directory: {:?}", jams_dir);
     }
 
     if pma_dir.exists() {
         std::fs::remove_dir_all(&pma_dir)?;
-        info!("Deleted existing pma directory: {:?}", pma_dir);
+        debug!("Deleted existing pma directory: {:?}", pma_dir);
     }
 
     if cli.new && jams_dir.exists() {
         std::fs::remove_dir_all(&jams_dir)?;
-        info!("Deleted existing checkpoint directory: {:?}", jams_dir);
+        debug!("Deleted existing checkpoint directory: {:?}", jams_dir);
     }
 
     let jam_paths = JamPaths::new(&jams_dir);

+ 2 - 2
crates/nockapp/crown/src/kernel/checkpoint.rs

@@ -9,7 +9,7 @@ use sword::mem::NockStack;
 use sword::noun::{Noun, T};
 use sword_macros::tas;
 use thiserror::Error;
-use tracing::{debug, error, info, warn};
+use tracing::{debug, error, warn};
 
 #[derive(Clone)]
 pub struct Checkpoint {
@@ -222,7 +222,7 @@ impl JamPaths {
             }
             (Ok(c), Err(e)) | (Err(e), Ok(c)) => {
                 warn!("{e}");
-                info!("Loading checkpoint, checksum: {}", c.checksum);
+                debug!("Loading checkpoint, checksum: {}", c.checksum);
                 Checkpoint::load(stack, c)
             }
             (Err(e1), Err(e2)) => {

+ 6 - 6
crates/nockapp/crown/src/kernel/form.rs

@@ -124,10 +124,10 @@ impl SerfThread {
             .spawn(move || {
                 let mut stack = NockStack::new(nock_stack_size, 0);
                 let checkpoint = if jam_paths.checkpoint_exists() {
-                    info!("Checkpoint file(s) found, validating and loading from jam");
+                    info!("Found existing state - restoring from checkpoint");
                     jam_paths.load_checkpoint(&mut stack).ok()
                 } else {
-                    info!("No checkpoint file found, starting from scratch");
+                    info!("No existing state found - initializing fresh state");
                     None
                 };
                 let buffer_toggle = Arc::new(AtomicBool::new(
@@ -163,7 +163,7 @@ impl SerfThread {
         loop {
             interval.tick().await;
             if self.handle.is_finished() {
-                info!("Serf finished");
+                debug!("Serf finished");
                 break;
             }
         }
@@ -354,7 +354,7 @@ fn extract_state_from_bytes(stack: &mut NockStack, state_bytes: &[u8]) -> Result
     // First try to decode as JammedCheckpoint
     match extract_from_checkpoint(stack, state_bytes) {
         Ok(noun) => {
-            info!("Successfully loaded state from JammedCheckpoint format");
+            debug!("Successfully loaded state from JammedCheckpoint format");
             Ok(noun)
         }
         Err(e1) => {
@@ -363,7 +363,7 @@ fn extract_state_from_bytes(stack: &mut NockStack, state_bytes: &[u8]) -> Result
             // Then try to decode as ExportedState
             match extract_from_exported_state(stack, state_bytes) {
                 Ok(noun) => {
-                    info!("Successfully loaded state from ExportedState format");
+                    debug!("Successfully loaded state from ExportedState format");
                     Ok(noun)
                 }
                 Err(e2) => {
@@ -579,7 +579,7 @@ impl Kernel {
             .await
         {
             Ok(_) => {
-                info!("Successfully loaded state from bytes");
+                debug!("Successfully loaded state from bytes");
                 Ok(kernel)
             }
             Err(e) => {

+ 13 - 13
crates/nockapp/crown/src/nockapp/mod.rs

@@ -17,7 +17,7 @@ use tokio::sync::{broadcast, mpsc, Mutex, OwnedMutexGuard};
 use tokio::time::{interval, Duration, Interval};
 use tokio::{fs, select};
 use tokio_util::task::TaskTracker;
-use tracing::{debug, error, info, instrument, trace};
+use tracing::{debug, error, instrument, trace};
 
 use crate::kernel::form::Kernel;
 use crate::noun::slab::NounSlab;
@@ -259,7 +259,7 @@ impl NockApp {
                 Err(NockAppError::Exit(code)) => {
                     if code == 0 {
                         // zero is success, we're simply done.
-                        info!("nockapp exited successfully with code: {}", code);
+                        debug!("nockapp exited successfully with code: {}", code);
                         break Ok(());
                     } else {
                         error!("nockapp exited with error code: {}", code);
@@ -275,14 +275,14 @@ impl NockApp {
     }
 
     pub async fn join(self) -> NockAppResult {
-        info!("Awaiting serf stop");
+        debug!("Awaiting serf stop");
         self.kernel.serf.stop().await;
-        info!("Joining serf thread");
+        debug!("Joining serf thread");
         self.kernel
             .serf
             .join()
             .map_err(|e| NockAppError::SerfThreadError(e))?;
-        info!("Serf thread joined");
+        debug!("Serf thread joined");
         Ok(())
     }
 
@@ -322,12 +322,12 @@ impl NockApp {
                 Ok(NockAppRun::Done)
             },
             shutdown = &mut self.shutdown_recv => {
-                info!("Shutdown channel received");
+                debug!("Shutdown channel received");
                 self.metrics.handle_shutdown.increment();
                 self.cleanup_socket();
                 match shutdown {
                     Ok(Ok(())) => {
-                        info!("Shutdown triggered, exiting");
+                        debug!("Shutdown triggered, exiting");
                         Ok(NockAppRun::Done)
                     },
                     Ok(Err(e)) => {
@@ -347,7 +347,7 @@ impl NockApp {
             },
             exit = self.exit_recv.recv() => {
                 self.metrics.handle_exit.increment();
-                info!("Exit signal received");
+                debug!("Exit signal received");
                 if let Some(code) = exit {
                     self.handle_exit(code).await
                 } else {
@@ -469,9 +469,9 @@ impl NockApp {
         // TODO: See if exit_status is duplicative of what the cancel token is for.
         self.exit_status.store(true, Ordering::SeqCst);
         let exit_event_num = self.kernel.serf.event_number.load(Ordering::SeqCst);
-        info!(
-            "Exit request received, waiting for save checkpoint with event_num {}",
-            exit_event_num
+        debug!(
+            "Exit request received, waiting for save checkpoint with event_num {} (code {})",
+            exit_event_num, code
         );
 
         let mut recv = self.watch_recv.clone();
@@ -501,7 +501,7 @@ impl NockApp {
             .await
             .expect("Failed to wait for saves to catch up to exit_event_num");
             Self::cleanup_socket_(&socket_path);
-            info!("Save event_num reached, finishing with code {}", code);
+            debug!("Save event_num reached, finishing with code {}", code);
             let shutdown_result = if code == 0 {
                 Ok(())
             } else {
@@ -510,7 +510,7 @@ impl NockApp {
             // Ensure we send the shutdown result before canceling so that
             // we don't get a race condition where the yielded result is
             // "canceled" instead of the actual result.
-            info!("Sending shutdown result");
+            debug!("Sending shutdown result");
             let _ = shutdown_send.send(shutdown_result);
         });
         Ok(NockAppRun::Pending)

+ 114 - 73
crates/nockchain-libp2p-io/src/nc.rs

@@ -13,12 +13,17 @@ use crown::utils::scry::*;
 use crown::{AtomExt, NounExt};
 use either::{Either, Left, Right};
 use futures::{Future, StreamExt};
+use libp2p::allow_block_list;
+use libp2p::connection_limits;
 use libp2p::identify::Event::Received;
+use libp2p::identity::Keypair;
+use libp2p::kad::NoKnownPeers;
+use libp2p::memory_connection_limits;
 use libp2p::request_response::Event::*;
 use libp2p::request_response::Message::*;
 use libp2p::request_response::{self};
 use libp2p::swarm::SwarmEvent;
-use libp2p::{PeerId, Swarm};
+use libp2p::{Multiaddr, PeerId, Swarm};
 use serde_bytes::ByteBuf;
 use sword::noun::{Atom, Noun, D, T};
 use sword_macros::tas;
@@ -176,23 +181,36 @@ impl FromStr for MiningKeyConfig {
     }
 }
 
-#[instrument(skip(swarm, equix_builder))]
+#[instrument(skip(keypair, bind, allowed, limits, memory_limits, equix_builder))]
 pub fn make_libp2p_driver(
-    mut swarm: Swarm<NockchainBehaviour>,
+    keypair: Keypair,
+    bind: Vec<Multiaddr>,
+    allowed: Option<allow_block_list::Behaviour<allow_block_list::AllowedPeers>>,
+    limits: connection_limits::ConnectionLimits,
+    memory_limits: Option<memory_connection_limits::Behaviour>,
+    initial_peers: &[Multiaddr],
     equix_builder: equix::EquiXBuilder,
     mining_config: Option<Vec<MiningKeyConfig>>,
     init_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
 ) -> IODriverFn {
+    let initial_peers = Vec::from(initial_peers);
     Box::new(|mut handle| {
         let metrics = NockchainP2PMetrics::register(gnort::global_metrics_registry())
             .expect("Failed to register metrics!");
 
         Box::pin(async move {
+            let mut swarm = crate::p2p::start_swarm(keypair, bind, allowed, limits, memory_limits)
+                .map_err(|e| {
+                    warn!("Could not create swarm: {}", e);
+                    NockAppError::OtherError
+                })?;
             let (swarm_tx, mut swarm_rx) = mpsc::channel::<SwarmAction>(1000); // number needs to be high enough to send gossips to peers
             let mut join_set = TrackedJoinSet::<Result<(), NockAppError>>::new();
             let message_tracker = Arc::new(Mutex::new(MessageTracker::new()));
+            let mut kad_bootstrap = tokio::time::interval(KADEMLIA_BOOTSTRAP_INTERVAL);
 
-            send_init_stark_config_poke(&handle).await?;
+            let mut initial_peer_retries_remaining = INITIAL_PEER_RETRIES;
+            dial_initial_peers(&mut swarm, &initial_peers)?;
 
             if let Some(configs) = mining_config {
                 if configs.len() == 1
@@ -306,6 +324,18 @@ pub fn make_libp2p_driver(
                             },
                         }
                     },
+                    _ = kad_bootstrap.tick() => {
+                        // If we don't have any peers, we should retry dialing our initial peers
+                        if let Err(NoKnownPeers())= swarm.behaviour_mut().kad.bootstrap() {
+                            if initial_peer_retries_remaining > 0 {
+                                info!("Failed to bootstrap: {}", NoKnownPeers());
+                                initial_peer_retries_remaining -= 1;
+                                dial_initial_peers(&mut swarm, &initial_peers)?;
+                            } else {
+                                warn!("Failed to bootstrap after {} retries, will not attempt to redial initial peers.", INITIAL_PEER_RETRIES);
+                            }
+                        }
+                    },
                     Some(result) = join_set.join_next() => {
                         if let Err(e) = result {
                             error!("Task error: {:?}", e);
@@ -509,19 +539,6 @@ async fn enable_mining(handle: &NockAppHandle, enable: bool) -> Result<PokeResul
     handle.poke(wire.to_wire(), enable_mining_slab).await
 }
 
-#[instrument(skip(handle))]
-async fn send_init_stark_config_poke(handle: &NockAppHandle) -> Result<PokeResult, NockAppError> {
-    let mut init_stark_config_slab = NounSlab::new();
-    let tag = make_tas(&mut init_stark_config_slab, "init-stark-config").as_noun();
-    let poke = T(
-        &mut init_stark_config_slab,
-        &[D(tas!(b"command")), tag, D(0)],
-    );
-    init_stark_config_slab.set_root(poke);
-    let wire = NockchainWire::Local;
-    handle.poke(wire.to_wire(), init_stark_config_slab).await
-}
-
 async fn handle_effect(
     noun_slab: NounSlab,
     swarm_tx: mpsc::Sender<SwarmAction>,
@@ -880,69 +897,79 @@ async fn handle_request_response(
                     let request_noun = request_slab.cue_into(message_bytes)?;
                     trace!("handle_request_response: Gossip noun parsed");
 
-                    let head = request_noun.as_cell()?.head();
-                    if head.eq_bytes(b"heard-block") {
-                        let page = request_noun.as_cell()?.tail();
-                        let block_id = page.as_cell()?.head();
-                        let block_id_str = tip5_hash_to_base58(block_id)?;
-                        let tracker = message_tracker.lock().await;
-                        if tracker.seen_blocks.contains(&block_id_str) {
-                            debug!("Block already seen, not processing: {:?}", block_id_str);
-                            metrics.block_seen_cache_hits.increment();
-                            return Ok(());
-                        } else {
-                            debug!("block not seen, processing: {:?}", block_id_str);
-                            metrics.block_seen_cache_misses.increment();
+                    let send_response: tokio::task::JoinHandle<Result<(), NockAppError>> =
+                        tokio::spawn(async move {
+                            let response = NockchainResponse::Ack;
+                            swarm_tx
+                                .send(SwarmAction::SendResponse { channel, response })
+                                .await
+                                .map_err(|_| NockAppError::OtherError)?;
+                            Ok(())
+                        });
+
+                    let poke_kernel = tokio::task::spawn(async move {
+                        let head = request_noun.as_cell()?.head();
+                        if head.eq_bytes(b"heard-block") {
+                            let page = request_noun.as_cell()?.tail();
+                            let block_id = page.as_cell()?.head();
+                            let block_id_str = tip5_hash_to_base58(block_id)?;
+                            let tracker = message_tracker.lock().await;
+                            if tracker.seen_blocks.contains(&block_id_str) {
+                                debug!("Block already seen, not processing: {:?}", block_id_str);
+                                metrics.block_seen_cache_hits.increment();
+                                return Ok(());
+                            } else {
+                                debug!("block not seen, processing: {:?}", block_id_str);
+                                metrics.block_seen_cache_misses.increment();
+                            }
                         }
-                    }
 
-                    if head.eq_bytes(b"heard-tx") {
-                        let raw_tx = request_noun.as_cell()?.tail();
-                        let tx_id = raw_tx.as_cell()?.head();
-                        let tracker = message_tracker.lock().await;
-                        let tx_id_str = tip5_hash_to_base58(tx_id)?;
-                        if tracker.seen_txs.contains(&tx_id_str) {
-                            debug!("Tx already seen, not processing: {:?}", tx_id_str);
-                            metrics.tx_seen_cache_hits.increment();
-                            return Ok(());
-                        } else {
-                            debug!("tx not seen, processing: {:?}", tx_id_str);
-                            metrics.tx_seen_cache_misses.increment();
+                        if head.eq_bytes(b"heard-tx") {
+                            let raw_tx = request_noun.as_cell()?.tail();
+                            let tx_id = raw_tx.as_cell()?.head();
+                            let tracker = message_tracker.lock().await;
+                            let tx_id_str = tip5_hash_to_base58(tx_id)?;
+                            if tracker.seen_txs.contains(&tx_id_str) {
+                                debug!("Tx already seen, not processing: {:?}", tx_id_str);
+                                metrics.tx_seen_cache_hits.increment();
+                                return Ok(());
+                            } else {
+                                debug!("tx not seen, processing: {:?}", tx_id_str);
+                                metrics.tx_seen_cache_misses.increment();
+                            }
                         }
-                    }
 
-                    let request_fact = prepend_tas(&mut request_slab, "fact", request_noun)?;
-                    request_slab.set_root(request_fact);
-                    let wire = Libp2pWire::Gossip(peer);
+                        let request_fact = prepend_tas(&mut request_slab, "fact", request_noun)?;
+                        request_slab.set_root(request_fact);
+                        let wire = Libp2pWire::Gossip(peer);
 
-                    match nockapp.try_poke(wire.to_wire(), request_slab).await {
-                        Ok(PokeResult::Ack) => {
-                            metrics.gossip_acked.increment();
-                        }
-                        Ok(PokeResult::Nack) => {
-                            metrics.gossip_nacked.increment();
-                            trace!("handle_request_response: gossip poke nacked");
-                            return Ok(());
-                        }
-                        Err(NockAppError::MPSCFullError(act)) => {
-                            metrics.gossip_dropped.increment();
-                            trace!(
+                        match nockapp.try_poke(wire.to_wire(), request_slab).await {
+                            Ok(PokeResult::Ack) => {
+                                metrics.gossip_acked.increment();
+                            }
+                            Ok(PokeResult::Nack) => {
+                                metrics.gossip_nacked.increment();
+                                trace!("handle_request_response: gossip poke nacked");
+                                return Ok(());
+                            }
+                            Err(NockAppError::MPSCFullError(act)) => {
+                                metrics.gossip_dropped.increment();
+                                trace!(
                                 "handle_request_response: gossip poke dropped due to backpressure"
                             );
-                            return Err(NockAppError::MPSCFullError(act));
-                        }
-                        Err(err) => {
-                            metrics.gossip_erred.increment();
-                            trace!("handle_request_response: Poke errored");
-                            return Err(err);
-                        }
-                    };
-                    trace!("handle_request_response: Poke successful");
-                    let response = NockchainResponse::Ack;
-                    swarm_tx
-                        .send(SwarmAction::SendResponse { channel, response })
-                        .await
-                        .map_err(|_| NockAppError::OtherError)?;
+                                return Err(NockAppError::MPSCFullError(act));
+                            }
+                            Err(err) => {
+                                metrics.gossip_erred.increment();
+                                trace!("handle_request_response: Poke errored");
+                                return Err(err);
+                            }
+                        };
+                        trace!("handle_request_response: Poke successful");
+                        Ok(())
+                    });
+                    send_response.await??;
+                    poke_kernel.await??;
                 }
             }
         }
@@ -1993,3 +2020,17 @@ mod tests {
         assert!(contains, "tx ID should be marked as seen");
     }
 }
+
+fn dial_initial_peers(
+    swarm: &mut Swarm<NockchainBehaviour>,
+    peers: &[Multiaddr],
+) -> Result<(), NockAppError> {
+    for peer in peers {
+        let peer = peer.clone();
+        swarm.dial(peer.clone()).map_err(|e| {
+            error!("Failed to dial initial peer {}: {}", peer, e);
+            NockAppError::OtherError
+        })?;
+    }
+    Ok(())
+}

+ 28 - 25
crates/nockchain-libp2p-io/src/p2p.rs

@@ -17,42 +17,50 @@ use tracing::{debug, trace};
 
 use crate::nc::*;
 
+// Kademlia constants
 /** How often we should run a kademlia bootstrap to keep our peer table fresh */
-pub const KADEMLIA_BOOTSTRAP_INTERVAL_SECS: Duration = Duration::from_secs(25);
+pub const KADEMLIA_BOOTSTRAP_INTERVAL: Duration = Duration::from_secs(300);
 
 /** How long we should keep a peer connection alive with no traffic */
-pub const PEER_TIMEOUT_SECS: u64 = 300;
+pub const SWARM_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
 
+// Core protocol (QUIC/ping/etc) constants
+/** How many times we should retry dialing our initial peers if we can't get Kademlia initialized */
+// TODO: Make command-line configurable
+pub const INITIAL_PEER_RETRIES: u32 = 5;
+/** How often we should send a keep-alive message to a peer */
+pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);
 /** How long should we wait before timing out the connection */
-pub const CONNECTION_TIMEOUT_SECS: u64 = PEER_TIMEOUT_SECS;
-pub const MAX_IDLE_TIMEOUT_MILLISECS: u32 = PEER_TIMEOUT_SECS as u32 * 1_000;
-pub const KEEP_ALIVE_INTERVAL_SECS: u64 = 30;
-pub const HANDSHAKE_TIMEOUT_SECS: u64 = PEER_TIMEOUT_SECS;
+pub const CONNECTION_TIMEOUT: Duration = SWARM_IDLE_TIMEOUT;
+/** How long should we wait before timing out the handshake */
+pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(15);
+/** How long QUIC should wait before timing out an idle connection */
+pub const MAX_IDLE_TIMEOUT_MILLISECS: u32 = CONNECTION_TIMEOUT.as_millis() as u32;
+/** How often we should send an identify message to a peer */
+pub const IDENTIFY_INTERVAL: Duration = KADEMLIA_BOOTSTRAP_INTERVAL;
 
-// TODO: command-line/configure
 /** Maximum number of established *incoming* connections */
 pub const MAX_ESTABLISHED_INCOMING_CONNECTIONS: u32 = 32;
 
-// TODO: command-line/configure
 /** Maximum number of established *incoming* connections */
 pub const MAX_ESTABLISHED_OUTGOING_CONNECTIONS: u32 = 32;
 
-// TODO: command-line/configure
 /** Maximum number of established connections */
 pub const MAX_ESTABLISHED_CONNECTIONS: u32 = 64;
 
-// TODO: command-line/configure
 /** Maximum number of established connections with a single peer ID */
 pub const MAX_ESTABLISHED_CONNECTIONS_PER_PEER: u32 = 2;
 
-// TODO: command-line/configure
 /** Maximum pending incoming connections */
 pub const MAX_PENDING_INCOMING_CONNECTIONS: u32 = 16;
 
-// TODO: command-line/configure
 /** Maximum pending outcoing connections */
 pub const MAX_PENDING_OUTGOING_CONNECTIONS: u32 = 16;
 
+// Request/response constants
+pub const REQUEST_RESPONSE_MAX_CONCURRENT_STREAMS: usize = MAX_ESTABLISHED_CONNECTIONS as usize * 2;
+pub const REQUEST_RESPONSE_TIMEOUT: Duration = Duration::from_secs(20);
+
 // ALL PROTOCOLS MUST HAVE UNIQUE VERSIONS
 pub const REQ_RES_PROTOCOL_VERSION: &str = "/nockchain-1-req-res";
 pub const KAD_PROTOCOL_VERSION: &str = "/nockchain-1-kad";
@@ -106,21 +114,18 @@ impl NockchainBehaviour {
 
             let identify_config =
                 identify::Config::new(IDENTIFY_PROTOCOL_VERSION.to_string(), keypair.public())
-                    .with_interval(Duration::from_secs(15))
+                    .with_interval(IDENTIFY_INTERVAL)
                     .with_hide_listen_addrs(true); // Only send externally confirmed addresses so we don't send loopback addresses
             let identify_behaviour = identify::Behaviour::new(identify_config);
 
             let memory_store = kad::store::MemoryStore::new(peer_id);
 
-            let mut kad_config =
-                kad::Config::new(libp2p::StreamProtocol::new(KAD_PROTOCOL_VERSION));
-            // kademlia config methods return an &mut so we can't do this in the let binding.
-            kad_config.set_periodic_bootstrap_interval(Some(KADEMLIA_BOOTSTRAP_INTERVAL_SECS));
+            let kad_config = kad::Config::new(libp2p::StreamProtocol::new(KAD_PROTOCOL_VERSION));
             let kad_behaviour = kad::Behaviour::with_config(peer_id, memory_store, kad_config);
 
             let request_response_config = request_response::Config::default()
-                .with_max_concurrent_streams(200)
-                .with_request_timeout(std::time::Duration::from_secs(300));
+                .with_max_concurrent_streams(REQUEST_RESPONSE_MAX_CONCURRENT_STREAMS)
+                .with_request_timeout(REQUEST_RESPONSE_TIMEOUT);
 
             let request_response_behaviour = cbor::Behaviour::new(
                 [(
@@ -181,16 +186,14 @@ pub fn start_swarm(
         .with_tokio()
         .with_quic_config(|mut cfg| {
             cfg.max_idle_timeout = MAX_IDLE_TIMEOUT_MILLISECS;
-            cfg.keep_alive_interval = Duration::from_secs(KEEP_ALIVE_INTERVAL_SECS);
-            cfg.handshake_timeout = Duration::from_secs(HANDSHAKE_TIMEOUT_SECS);
+            cfg.keep_alive_interval = KEEP_ALIVE_INTERVAL;
+            cfg.handshake_timeout = HANDSHAKE_TIMEOUT;
             cfg
         })
         .with_dns_config(resolver_config, resolver_opts)
         .with_behaviour(NockchainBehaviour::pre_new(allowed, limits, memory_limits))?
-        .with_swarm_config(|cfg| {
-            cfg.with_idle_connection_timeout(Duration::from_secs(PEER_TIMEOUT_SECS))
-        })
-        .with_connection_timeout(std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS))
+        .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(SWARM_IDLE_TIMEOUT))
+        .with_connection_timeout(CONNECTION_TIMEOUT)
         .build();
 
     for bind_addr in bind {

+ 6 - 11
crates/nockchain/src/lib.rs

@@ -444,10 +444,6 @@ pub async fn init_with_kernel(
             Some(memory_connection_limits::Behaviour::with_max_bytes(max_bytes))
         } else { c.max_system_memory_fraction.map(memory_connection_limits::Behaviour::with_max_percentage) }
     });
-    // TODO parse CLI
-    let mut swarm = nockchain_libp2p_io::p2p::start_swarm(
-        keypair, bind_multiaddrs, allowed, limits, memory_limits,
-    )?;
 
     let backbone_peers = BACKBONE_NODES
         .iter()
@@ -481,12 +477,6 @@ pub async fn init_with_kernel(
 
     debug!("peer_multiaddrs: {:?}", peer_multiaddrs);
 
-    // Dial initial peers
-    for remote in peer_multiaddrs {
-        swarm.dial(remote.clone())?;
-        info!("Dialed initial peer {remote}");
-    }
-
     let equix_builder = equix::EquiXBuilder::new();
 
     // Create driver initialization signals. the idea here is that we want to wait for
@@ -534,7 +524,12 @@ pub async fn init_with_kernel(
     }
 
     let libp2p_driver = nockchain_libp2p_io::nc::make_libp2p_driver(
-        swarm,
+        keypair,
+        bind_multiaddrs,
+        allowed,
+        limits,
+        memory_limits,
+        &peer_multiaddrs,
         equix_builder,
         cli.as_ref().and_then(|c| {
             if let Some(pubkey) = &c.mining_pubkey {

+ 26 - 0
crates/wallet/Cargo.toml

@@ -0,0 +1,26 @@
+[package]
+name = "wallet"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+crown = { workspace = true }
+kernels = { workspace = true, features = ["wallet"] }
+sword = { workspace = true }
+sword_macros = { workspace = true }
+
+bardecoder = { workspace = true }
+clap = { workspace = true, features = ["derive"] }
+crossterm.workspace = true
+either.workspace = true
+getrandom.workspace = true
+image = { workspace = true }
+qrcode = { workspace = true }
+ratatui.workspace = true
+tempfile.workspace = true
+termimad.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["full"] }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
+zkvm-jetpack = { workspace = true }

+ 120 - 0
crates/wallet/README.md

@@ -0,0 +1,120 @@
+# Nockchain Wallet
+
+## Key Management
+
+### Generate New Key Pair
+
+```bash
+# Generate a new key pair with random entropy
+wallet keygen
+```
+
+### Import Private Key
+
+```bash
+# Import keys from a jammed file
+wallet import-keys --input path/to/keys.jam
+
+# Import a master public key and chain code
+wallet import-master-pubkey --key <base58-key> --knot <base58-chain-code>
+```
+
+### Generate Master Private Key from Seed Phrase
+
+```bash
+wallet gen-master-privkey --seedphrase "your seed phrase here"
+```
+
+Creates a master private key deterministically from a BIP39-style seed phrase.
+
+### Generate Master Public Key from Private Key
+
+```bash
+wallet gen-master-pubkey --master-privkey <private-key>
+```
+
+Derives the master public key from a master private key.
+
+### Derive Child Key
+
+```bash
+wallet derive-child --key-type <pub|priv> --index <0-255>
+```
+
+Derives a child public or private key at the given index from the current master key.
+
+
+## Connecting to Nockchain
+
+The wallet needs to connect to a running nockchain instance to perform operations like checking balances, broadcasting transactions, etc.
+
+```bash
+# Connect to nockchain using a Unix domain socket
+wallet --nockchain-socket ./test-leader/nockchain.sock <command>
+
+# Example: checking balance while connected to nockchain
+wallet --nockchain-socket ./test-leader/nockchain.sock balance
+
+# Example: broadcasting a transaction
+wallet --nockchain-socket ./test-leader/nockchain.sock make-tx --draft my_draft
+```
+
+Note: Make sure nockchain is running and the socket path matches your nockchain configuration.
+
+
+## Listing Notes
+
+### List All Notes
+
+```bash
+wallet list-notes
+```
+
+Displays all notes (UTXOs) currently managed by the wallet, sorted by assets.
+
+### List Notes by Public Key
+
+```bash
+wallet list-notes-by-pubkey --pubkey <public-key>
+```
+
+Shows only the notes associated with the specified public key. Useful for filtering wallet contents by address or for multisig scenarios.
+
+
+## Transaction Creation
+
+#### Components of transaction creation
+
+1. **Seeds**: Define where funds are going and how much
+2. **Inputs**: Specify which notes (UTXOs) to spend
+3. **Draft**: Combine inputs into a complete transaction
+4. **Sign**: Authorize the transaction with private keys
+5. **Make Transaction**: Create the final transaction for broadcasting
+
+### What is a draft?
+
+A draft represents a transaction that is being prepared for submission to the network. It is a collection of partially assembled `seeds` and `inputs` that can be persisted to disk and later signed and submitted.
+
+### Create a Draft
+
+```bash
+# Create a draft using simple-spend
+wallet simple-spend \
+  --names "[first1 last1],[first2 last2]" \
+  --recipients "[1 pk1],[2 pk2,pk3]" \
+  --gifts "100,200" \
+  --fee 10
+```
+
+### Make Transaction from Draft
+
+```bash
+# Sign the transaction
+wallet sign-tx --draft path/to/draft.draft
+
+# Make and broadcast the signed transaction
+wallet make-tx --draft path/to/draft.draft
+```
+
+Note: The draft file will be saved in `./drafts/` directory with a `.draft` extension.
+

+ 62 - 0
crates/wallet/src/error.rs

@@ -0,0 +1,62 @@
+use std::{fmt, io};
+
+use crown::nockapp::NockAppError;
+
+#[derive(Debug)]
+pub enum WalletError {
+    Io(io::Error),
+    NockApp(NockAppError),
+    Parse(String),
+    Command(String),
+}
+
+impl fmt::Display for WalletError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            WalletError::Io(e) => write!(f, "IO error: {}", e),
+            WalletError::NockApp(e) => write!(f, "NockApp error: {}", e),
+            WalletError::Parse(s) => write!(f, "Parse error: {}", s),
+            WalletError::Command(s) => write!(f, "Command error: {}", s),
+        }
+    }
+}
+
+impl std::error::Error for WalletError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            WalletError::Io(e) => Some(e),
+            WalletError::NockApp(e) => Some(e),
+            WalletError::Parse(_) => None,
+            WalletError::Command(_) => None,
+        }
+    }
+}
+
+impl From<io::Error> for WalletError {
+    fn from(err: io::Error) -> Self {
+        WalletError::Io(err)
+    }
+}
+
+impl From<NockAppError> for WalletError {
+    fn from(err: NockAppError) -> Self {
+        WalletError::NockApp(err)
+    }
+}
+
+impl From<WalletError> for NockAppError {
+    fn from(err: WalletError) -> Self {
+        match err {
+            WalletError::NockApp(e) => e,
+            WalletError::Io(e) => NockAppError::IoError(e),
+            WalletError::Parse(_) => NockAppError::OtherError,
+            WalletError::Command(_) => NockAppError::OtherError,
+        }
+    }
+}
+
+impl From<crown::CrownError> for WalletError {
+    fn from(err: crown::CrownError) -> Self {
+        WalletError::Parse(err.to_string())
+    }
+}

+ 1311 - 0
crates/wallet/src/main.rs

@@ -0,0 +1,1311 @@
+#![allow(clippy::doc_overindented_list_items)]
+
+use std::fs;
+use std::path::PathBuf;
+
+use clap::{Parser, Subcommand};
+use crown::utils::bytes::Byts;
+use getrandom::getrandom;
+use sword::jets::cold::Nounable;
+use sword::noun::{Atom, Cell, IndirectAtom, Noun, D, SIG, T};
+use tokio::net::UnixStream;
+use tracing::{error, info};
+use zkvm_jetpack::hot::produce_prover_hot_state;
+
+mod error;
+
+use crown::kernel::boot::{self, Cli as BootCli};
+use crown::nockapp::driver::*;
+use crown::nockapp::wire::{Wire, WireRepr};
+use crown::nockapp::{NockApp, NockAppError};
+use crown::noun::slab::NounSlab;
+use crown::noun::IntoNoun;
+use crown::utils::make_tas;
+use crown::{exit_driver, file_driver, markdown_driver, one_punch_driver, CrownError, ToBytesExt};
+use kernels::wallet::KERNEL;
+
+#[derive(Parser, Debug, Clone)]
+#[command(author, version, about, long_about = None)]
+struct WalletCli {
+    #[command(flatten)]
+    boot: BootCli,
+
+    #[command(subcommand)]
+    command: Commands,
+
+    #[arg(long, value_name = "PATH")]
+    nockchain_socket: Option<PathBuf>,
+}
+
+#[derive(Debug)]
+pub enum WalletWire {
+    ListNotes,
+    UpdateBalance,
+    UpdateBlock,
+    Exit,
+    Command(Commands),
+}
+
+impl Wire for WalletWire {
+    const VERSION: u64 = 1;
+    const SOURCE: &str = "wallet";
+
+    fn to_wire(&self) -> WireRepr {
+        let tags = match self {
+            WalletWire::ListNotes => vec!["list-notes".into()],
+            WalletWire::UpdateBalance => vec!["update-balance".into()],
+            WalletWire::UpdateBlock => vec!["update-block".into()],
+            WalletWire::Exit => vec!["exit".into()],
+            WalletWire::Command(command) => {
+                vec!["command".into(), command.as_wire_tag().into()]
+            }
+        };
+        WireRepr::new(WalletWire::SOURCE, WalletWire::VERSION, tags)
+    }
+}
+
+/// Represents a Noun that the wallet kernel can handle
+type CommandNoun<T> = Result<(T, Operation), NockAppError>;
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum Commands {
+    /// Display the current wallet balance
+    Balance,
+
+    /// Generate a new key pair
+    Keygen,
+
+    /// Derive a child key from the current master key
+    DeriveChild {
+        /// Type of key to derive (e.g., "pub", "priv")
+        #[arg(short, long)]
+        key_type: String,
+
+        /// Index of the child key to derive
+        #[arg(short, long, value_parser = clap::value_parser!(u64).range(0..=255))]
+        index: u64,
+    },
+
+    /// Import keys from a file
+    ImportKeys {
+        /// Path to the jammed keys file
+        #[arg(short, long, value_name = "FILE")]
+        input: String,
+    },
+
+    /// Signs a transaction
+    SignTx {
+        /// Path to input bundle file
+        #[arg(short, long)]
+        draft: String,
+
+        /// Optional key index to use for signing (0-255)
+        #[arg(short, long, value_parser = clap::value_parser!(u64).range(0..=255))]
+        index: Option<u64>,
+    },
+
+    /// Show the balance of a specific address at a given block
+    ShowBalance {
+        /// Block to show balance at
+        #[arg(short, long)]
+        block: String,
+    },
+
+    /// Generate a master private key from a seed phrase
+    GenMasterPrivkey {
+        /// Seed phrase to generate master private key
+        #[arg(short, long)]
+        seedphrase: String,
+    },
+
+    /// Generate a master public key from a master private key
+    GenMasterPubkey {
+        /// Master private key to generate master public key
+        #[arg(short, long)]
+        master_privkey: String,
+    },
+
+    /// Perform a simple scan of the blockchain
+    Scan {
+        /// Master public key to scan for
+        #[arg(short, long)]
+        master_pubkey: String,
+        /// Optional search depth (default 100)
+        #[arg(short, long, default_value = "100")]
+        search_depth: u64,
+        /// Include timelocks in scan
+        #[arg(long, default_value = "false")]
+        include_timelocks: bool,
+        /// Include multisig in scan
+        #[arg(long, default_value = "false")]
+        include_multisig: bool,
+    },
+
+    /// List all notes in the wallet
+    ListNotes,
+
+    /// List notes by public key
+    ListNotesByPubkey {
+        /// Optional public key to filter notes
+        #[arg(short, long)]
+        pubkey: Option<String>,
+    },
+
+    /// Perform a simple spend operation
+    SimpleSpend {
+        /// Names of notes to spend (comma-separated)
+        #[arg(long)]
+        names: String,
+        /// Recipient addresses (comma-separated)
+        #[arg(long)]
+        recipients: String,
+        /// Amounts to send (comma-separated)
+        #[arg(long)]
+        gifts: String,
+        /// Transaction fee
+        #[arg(long)]
+        fee: u64,
+    },
+
+    /// Create a transaction from a draft file
+    MakeTx {
+        /// Draft file to create transaction from
+        #[arg(short, long)]
+        draft: String,
+    },
+
+    /// Update the wallet balance
+    UpdateBalance,
+
+    /// Import a master public key
+    ImportMasterPubkey {
+        /// Base58-encoded public key
+        #[arg(short, long)]
+        key: String,
+        /// Base58-encoded chain code
+        #[arg(short, long)]
+        knot: String,
+    },
+
+    /// Lists all public keys in the wallet
+    ListPubkeys,
+}
+
+impl Commands {
+    fn as_wire_tag(&self) -> &'static str {
+        match self {
+            Commands::Balance => "balance",
+            Commands::Keygen => "keygen",
+            Commands::DeriveChild { .. } => "derive-child",
+            Commands::ImportKeys { .. } => "import-keys",
+            Commands::SignTx { .. } => "sign-tx",
+            Commands::ShowBalance { .. } => "show-balance",
+            Commands::GenMasterPrivkey { .. } => "gen-master-privkey",
+            Commands::GenMasterPubkey { .. } => "gen-master-pubkey",
+            Commands::Scan { .. } => "scan",
+            Commands::ListNotes => "list-notes",
+            Commands::ListNotesByPubkey { .. } => "list-notes-by-pubkey",
+            Commands::SimpleSpend { .. } => "simple-spend",
+            Commands::MakeTx { .. } => "make-tx",
+            Commands::UpdateBalance => "update-balance",
+            Commands::ImportMasterPubkey { .. } => "import-master-pubkey",
+            Commands::ListPubkeys => "list-pubkeys",
+        }
+    }
+}
+
+pub struct Wallet {
+    app: NockApp,
+}
+
+#[derive(Debug, Clone)]
+pub enum KeyType {
+    Pub,
+    Prv,
+}
+impl KeyType {
+    fn to_string(&self) -> &'static str {
+        match self {
+            KeyType::Pub => "pub",
+            KeyType::Prv => "prv",
+        }
+    }
+}
+
+impl Wallet {
+    /// Creates a new `Wallet` instance with the given kernel.
+    ///
+    /// This wraps the kernel in a NockApp, which exposes a substrate
+    /// for kernel interaction with IO driver semantics.
+    ///
+    /// # Arguments
+    ///
+    /// * `kernel` - The kernel to initialize the wallet with.
+    ///
+    /// # Returns
+    ///
+    /// A new `Wallet` instance with the kernel initialized
+    /// as a NockApp.
+    fn new(nockapp: NockApp) -> Self {
+        Wallet { app: nockapp }
+    }
+
+    /// Wraps a command with sync-run to ensure it runs after block and balance updates
+    ///
+    /// # Arguments
+    ///
+    /// * `command_noun_slab` - The command noun to wrap
+    /// * `operation` - The operation type (Poke or Peek)
+    ///
+    /// # Returns
+    ///
+    /// A result containing the wrapped command noun and operation, or an error
+    fn wrap_with_sync_run(
+        command_noun_slab: NounSlab,
+        operation: Operation,
+    ) -> Result<(NounSlab, Operation), NockAppError> {
+        let original_root_noun_clone = unsafe { command_noun_slab.root() };
+        let mut sync_slab = command_noun_slab.clone();
+        let sync_tag = make_tas(&mut sync_slab, "sync-run");
+        let tag_noun = sync_tag.as_noun();
+        let sync_run_cell = Cell::new(&mut sync_slab, tag_noun, *original_root_noun_clone);
+        let sync_run_noun = sync_run_cell.as_noun();
+        sync_slab.set_root(sync_run_noun);
+
+        Ok((sync_slab, operation))
+    }
+
+    /// Prepares a wallet command for execution.
+    ///
+    /// # Arguments
+    ///
+    /// * `command` - The command to execute.
+    /// * `args` - The arguments for the command.
+    /// * `operation` - The operation type (Poke or Peek).
+    /// * `slab` - The NounSlab to use for the command.
+    ///
+    /// # Returns
+    ///
+    /// A `CommandNoun` containing the prepared NounSlab and operation.
+    fn wallet(
+        command: &str,
+        args: &[Noun],
+        operation: Operation,
+        slab: &mut NounSlab,
+    ) -> CommandNoun<NounSlab> {
+        let head = make_tas(slab, command).as_noun();
+
+        let tail = match args.len() {
+            0 => D(0),
+            1 => args[0],
+            _ => T(slab, args),
+        };
+
+        let full = T(slab, &[head, tail]);
+
+        slab.set_root(full);
+        Ok((slab.clone(), operation))
+    }
+
+    /// Retrieves the wallet balance.
+    fn wallet_balance() -> CommandNoun<NounSlab> {
+        Self::wallet("balance", &[], Operation::Peek, &mut NounSlab::new())
+    }
+
+    /// Generates a new key pair.
+    ///
+    /// # Arguments
+    ///
+    /// * `entropy` - The entropy to use for key generation.
+    fn keygen(entropy: &[u8; 32], sal: &[u8; 16]) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let ent: Byts = Byts::new(entropy.to_vec());
+        let ent_noun = ent.into_noun(&mut slab);
+        let sal: Byts = Byts::new(sal.to_vec());
+        let sal_noun = sal.into_noun(&mut slab);
+        Self::wallet("keygen", &[ent_noun, sal_noun], Operation::Poke, &mut slab)
+    }
+
+    // Derives a child key from current master key.
+    //
+    // # Arguments
+    //
+    // * `key_type` - The type of key to derive (e.g., "pub", "priv")
+    // * `index` - The index of the child key to derive
+    // TODO: add label if necessary
+    fn derive_child(key_type: KeyType, index: u64) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let key_type_noun = make_tas(&mut slab, key_type.to_string()).as_noun();
+        let index_noun = D(index);
+
+        Self::wallet(
+            "derive-child",
+            &[key_type_noun, index_noun, SIG],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Signs a transaction.
+    ///
+    /// # Arguments
+    ///
+    /// * `draft_path` - Path to the draft file
+    /// * `index` - Optional index of the key to use for signing
+    fn sign_tx(draft_path: &str, index: Option<u64>) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+
+        // Validate index is within range (though clap should prevent this)
+        if let Some(idx) = index {
+            if idx > 255 {
+                return Err(CrownError::Unknown("Key index must not exceed 255".into()).into());
+            }
+        }
+
+        // Read and decode the input bundle
+        let draft_data = fs::read(draft_path)
+            .map_err(|e| CrownError::Unknown(format!("Failed to read draft: {}", e)))?;
+
+        // Convert the bundle data into a noun using cue
+        let draft_noun = slab
+            .cue_into(draft_data.as_bytes()?)
+            .map_err(|e| CrownError::Unknown(format!("Failed to decode draft: {}", e)))?;
+
+        let index_noun = match index {
+            Some(i) => D(i),
+            None => D(0),
+        };
+
+        // Generate random entropy
+        let mut entropy_bytes = [0u8; 32];
+        getrandom(&mut entropy_bytes).map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let entropy = from_bytes(&mut slab, &entropy_bytes).as_noun();
+
+        Self::wallet(
+            "sign-tx",
+            &[draft_noun, index_noun, entropy],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Shows the balance of a specific address at a given block.
+    ///
+    /// # Arguments
+    ///
+    /// * `block` - The block hash or height to show the balance at.
+    fn balance_at_block(block: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let block_noun = IntoNoun::into_noun(block);
+        Self::wallet("show-balance", &[block_noun], Operation::Poke, &mut slab)
+    }
+
+    /// Generates a master private key from a seed phrase.
+    ///
+    /// # Arguments
+    ///
+    /// * `seedphrase` - The seed phrase to generate the master private key from.
+    fn gen_master_privkey(seedphrase: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let seedphrase_noun = make_tas(&mut slab, seedphrase).as_noun();
+        Self::wallet(
+            "gen-master-privkey",
+            &[seedphrase_noun],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Generates a master public key from a master private key.
+    ///
+    /// # Arguments
+    ///
+    /// * `master_privkey` - The master private key to generate the public key from.
+    fn gen_master_pubkey(master_privkey: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let master_privkey_noun = make_tas(&mut slab, master_privkey).as_noun();
+        Self::wallet(
+            "gen-master-pubkey",
+            &[master_privkey_noun],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Imports keys.
+    ///
+    /// # Arguments
+    ///
+    /// * `input_path` - Path to jammed keys file
+    fn import_keys(input_path: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+
+        let key_data = fs::read(input_path)
+            .map_err(|e| CrownError::Unknown(format!("Failed to read master pubkeys: {}", e)))?;
+
+        let pubkey_noun = slab
+            .cue_into(key_data.as_bytes()?)
+            .map_err(|e| CrownError::Unknown(format!("Failed to decode master pubkeys: {}", e)))?;
+
+        Self::wallet("import-keys", &[pubkey_noun], Operation::Poke, &mut slab)
+    }
+
+    /// Performs a simple scan of the blockchain.
+    ///
+    /// # Arguments
+    ///
+    /// * `master_pubkey` - The master public key to scan for.
+    /// * `search_depth` - How many addresses to scan (default 100)
+    fn scan(
+        master_pubkey: &str,
+        search_depth: u64,
+        include_timelocks: bool,
+        include_multisig: bool,
+    ) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let master_pubkey_noun = make_tas(&mut slab, master_pubkey).as_noun();
+        let search_depth_noun = D(search_depth);
+        let include_timelocks_noun = D(include_timelocks as u64);
+        let include_multisig_noun = D(include_multisig as u64);
+
+        Self::wallet(
+            "scan",
+            &[
+                master_pubkey_noun, search_depth_noun, include_timelocks_noun,
+                include_multisig_noun,
+            ],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Performs a simple spend operation by creating transaction inputs from notes.
+    ///
+    /// Takes a list of note names, recipient addresses, and gift amounts to create
+    /// transaction inputs. The fee is subtracted from the first note that has sufficient
+    /// assets to cover both the fee and its corresponding gift amount.
+    ///
+    /// # Arguments
+    ///
+    /// * `names` - Comma-separated list of note name pairs in format "[first last]"
+    ///             Example: "[first1 last1],[first2 last2]"
+    ///
+    /// * `recipients` - Comma-separated list of recipient $locks
+    ///                 Example: "[1 pk1],[2 pk2,pk3,pk4]"
+    ///                 A simple comma-separated list is also supported: "pk1,pk2,pk3",
+    ///                 where it is presumed that all recipients are single-signature,
+    ///                 that is to say, it is the same as "[1 pk1],[1 pk2],[1 pk3]"
+    ///
+    /// * `gifts` - Comma-separated list of amounts to send to each recipient
+    ///             Example: "100,200"
+    ///
+    /// * `fee` - Transaction fee to be subtracted from one of the input notes
+    ///
+    /// # Returns
+    ///
+    /// Returns a `CommandNoun` containing:
+    /// - A `NounSlab` with the encoded simple-spend command
+    /// - The `Operation` type (Poke)
+    ///
+    /// # Errors
+    ///
+    /// Returns `NockAppError` if:
+    /// - Name pairs are not properly formatted as "[first last]"
+    /// - Number of names, recipients, and gifts don't match
+    /// - Any input parsing fails
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// let names = "[first1 last1],[first2 last2]";
+    /// let recipients = "[1 pk1],[2 pk2,pk3,pk4]";
+    /// let gifts = "100,200";
+    /// let fee = 10;
+    /// wallet.simple_spend(names.to_string(), recipients.to_string(), gifts.to_string(), fee)?;
+    /// ```
+    fn simple_spend(
+        names: String,
+        recipients: String,
+        gifts: String,
+        fee: u64,
+    ) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+
+        // Split the comma-separated inputs
+        // Each name should be in format "[first last]"
+        let names_vec: Vec<(String, String)> = names
+            .split(',')
+            .filter_map(|pair| {
+                let pair = pair.trim();
+                if pair.starts_with('[') && pair.ends_with(']') {
+                    let inner = &pair[1..pair.len() - 1];
+                    let parts: Vec<&str> = inner.split_whitespace().collect();
+                    if parts.len() == 2 {
+                        Some((parts[0].to_string(), parts[1].to_string()))
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        // Convert recipients to list of [number pubkeys] pairs
+        let recipients_vec: Vec<(u64, Vec<String>)> = if recipients.contains('[') {
+            // Parse complex format: "[1 pk1],[2 pk2,pk3,pk4]"
+            recipients
+                .split(',')
+                .filter_map(|pair| {
+                    let pair = pair.trim();
+                    if pair.starts_with('[') && pair.ends_with(']') {
+                        let inner = &pair[1..pair.len() - 1];
+                        let mut parts = inner.splitn(2, ' ');
+
+                        // Parse the number
+                        let number = parts.next()?.parse().ok()?;
+
+                        // Parse the pubkeys
+                        let pubkeys = parts
+                            .next()?
+                            .split(',')
+                            .map(|s| s.trim().to_string())
+                            .collect();
+
+                        Some((number, pubkeys))
+                    } else {
+                        None
+                    }
+                })
+                .collect()
+        } else {
+            // Parse simple format: "pk1,pk2,pk3"
+            recipients
+                .split(',')
+                .map(|addr| (1, vec![addr.trim().to_string()]))
+                .collect()
+        };
+
+        let gifts_vec: Vec<u64> = gifts.split(',').filter_map(|s| s.parse().ok()).collect();
+
+        // Verify equal lengths
+        if names_vec.len() != recipients_vec.len() || names_vec.len() != gifts_vec.len() {
+            return Err(CrownError::Unknown(
+                "Invalid input - names, recipients, and gifts must have the same length"
+                    .to_string(),
+            )
+            .into());
+        }
+
+        // Convert names to list of pairs
+        let names_noun = names_vec
+            .into_iter()
+            .rev()
+            .fold(D(0), |acc, (first, last)| {
+                // Create a tuple [first_name last_name] for each name pair
+                let first_noun = make_tas(&mut slab, &first).as_noun();
+                let last_noun = make_tas(&mut slab, &last).as_noun();
+                let name_pair = T(&mut slab, &[first_noun, last_noun]);
+                Cell::new(&mut slab, name_pair, acc).as_noun()
+            });
+
+        // Convert recipients to list
+        let recipients_noun = recipients_vec
+            .into_iter()
+            .rev()
+            .fold(D(0), |acc, (num, pubkeys)| {
+                // Create the inner list of pubkeys
+                let pubkeys_noun = pubkeys.into_iter().rev().fold(D(0), |acc, pubkey| {
+                    let pubkey_noun = make_tas(&mut slab, &pubkey).as_noun();
+                    Cell::new(&mut slab, pubkey_noun, acc).as_noun()
+                });
+
+                // Create the pair of [number pubkeys_list]
+                let pair = T(&mut slab, &[D(num), pubkeys_noun]);
+                Cell::new(&mut slab, pair, acc).as_noun()
+            });
+
+        // Convert gifts to list
+        let gifts_noun = gifts_vec.into_iter().rev().fold(D(0), |acc, amount| {
+            Cell::new(&mut slab, D(amount), acc).as_noun()
+        });
+
+        let fee_noun = D(fee);
+
+        Self::wallet(
+            "simple-spend",
+            &[names_noun, recipients_noun, gifts_noun, fee_noun],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    fn update_balance() -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        Self::wallet("update-balance", &[], Operation::Poke, &mut slab)
+    }
+
+    /// Lists all notes in the wallet.
+    ///
+    /// Retrieves and displays all notes from the wallet's balance, sorted by assets.
+    fn list_notes() -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        Self::wallet("list-notes", &[], Operation::Poke, &mut slab)
+    }
+
+    /// Imports a master public key.
+    ///
+    /// # Arguments
+    ///
+    /// * `key` - Base58-encoded public key
+    /// * `knot` - Base58-encoded chain code
+    fn import_master_pubkey(key: &str, knot: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let key_noun = make_tas(&mut slab, key).as_noun();
+        let knot_noun = make_tas(&mut slab, knot).as_noun();
+
+        Self::wallet(
+            "import-master-pubkey",
+            &[key_noun, knot_noun],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+
+    /// Creates a transaction from a draft file.
+    ///
+    /// # Arguments
+    ///
+    /// * `draft_path` - Path to the draft file to create transaction from
+    fn make_tx(draft_path: &str) -> CommandNoun<NounSlab> {
+        // Read and decode the draft file
+        let draft_data = fs::read(draft_path)
+            .map_err(|e| CrownError::Unknown(format!("Failed to read draft file: {}", e)))?;
+
+        let mut slab = NounSlab::new();
+        let draft_noun = slab
+            .cue_into(draft_data.as_bytes()?)
+            .map_err(|e| CrownError::Unknown(format!("Failed to decode draft data: {}", e)))?;
+
+        Self::wallet("make-tx", &[draft_noun], Operation::Poke, &mut slab)
+    }
+
+    /// Lists all public keys in the wallet.
+    fn list_pubkeys() -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        Self::wallet("list-pubkeys", &[], Operation::Poke, &mut slab)
+    }
+
+    /// Lists notes by public key
+    fn list_notes_by_pubkey(pubkey: &str) -> CommandNoun<NounSlab> {
+        let mut slab = NounSlab::new();
+        let pubkey_noun = make_tas(&mut slab, pubkey).as_noun();
+        Self::wallet(
+            "list-notes-by-pubkey",
+            &[pubkey_noun],
+            Operation::Poke,
+            &mut slab,
+        )
+    }
+}
+
+#[tokio::main]
+async fn main() -> Result<(), NockAppError> {
+    let cli = WalletCli::parse();
+    boot::init_default_tracing(&cli.boot.clone()); // Init tracing early
+
+    let prover_hot_state = produce_prover_hot_state();
+
+    let kernel = boot::setup(
+        KERNEL,
+        Some(cli.boot.clone()),
+        prover_hot_state.as_slice(),
+        "wallet",
+        None,
+    )
+    .await
+    .map_err(|e| CrownError::Unknown(format!("Kernel setup failed: {}", e)))?;
+
+    let mut wallet = Wallet::new(kernel);
+
+    // Determine if this command requires chain synchronization
+    let requires_sync = match &cli.command {
+        // Commands that DON'T need sync
+        Commands::Keygen
+        | Commands::DeriveChild { .. }
+        | Commands::ImportKeys { .. }
+        | Commands::SignTx { .. }
+        | Commands::MakeTx { .. }
+        | Commands::GenMasterPrivkey { .. }
+        | Commands::GenMasterPubkey { .. }
+        | Commands::ImportMasterPubkey { .. }
+        | Commands::ListPubkeys
+        | Commands::SimpleSpend { .. } => false,
+
+        // All other commands DO need sync
+        _ => true,
+    };
+
+    // Check if we need sync but don't have a socket
+    if requires_sync && cli.nockchain_socket.is_none() {
+        return Err(CrownError::Unknown(
+            "This command requires connection to a nockchain node. Please provide --nockchain-socket"
+            .to_string()
+        ).into());
+    }
+
+    // Generate the command noun and operation
+    let poke = match &cli.command {
+        Commands::Balance => Wallet::wallet_balance(),
+        Commands::Keygen => {
+            let mut entropy = [0u8; 32];
+            let mut salt = [0u8; 16];
+            getrandom(&mut entropy).map_err(|e| CrownError::Unknown(e.to_string()))?;
+            getrandom(&mut salt).map_err(|e| CrownError::Unknown(e.to_string()))?;
+            Wallet::keygen(&entropy, &salt)
+        }
+        Commands::DeriveChild { key_type, index } => {
+            // Validate key_type is either "pub" or "priv"
+            let key_type = match key_type.as_str() {
+                "pub" => KeyType::Pub,
+                "priv" => KeyType::Prv,
+                _ => {
+                    return Err(CrownError::Unknown(
+                        "Key type must be either 'pub' or 'priv'".into(),
+                    )
+                    .into())
+                }
+            };
+            Wallet::derive_child(key_type, *index)
+        }
+        Commands::SignTx { draft, index } => Wallet::sign_tx(draft, *index),
+        Commands::ShowBalance { block } => Wallet::balance_at_block(block),
+        Commands::ImportKeys { input } => Wallet::import_keys(input),
+        Commands::GenMasterPrivkey { seedphrase } => Wallet::gen_master_privkey(seedphrase),
+        Commands::GenMasterPubkey { master_privkey } => Wallet::gen_master_pubkey(master_privkey),
+        Commands::Scan {
+            master_pubkey,
+            search_depth,
+            include_timelocks,
+            include_multisig,
+        } => Wallet::scan(
+            master_pubkey, *search_depth, *include_timelocks, *include_multisig,
+        ),
+        Commands::ListNotes => Wallet::list_notes(),
+        Commands::ListNotesByPubkey { pubkey } => {
+            if let Some(pk) = pubkey {
+                Wallet::list_notes_by_pubkey(pk)
+            } else {
+                return Err(CrownError::Unknown("Public key is required".into()).into());
+            }
+        }
+        Commands::SimpleSpend {
+            names,
+            recipients,
+            gifts,
+            fee,
+        } => Wallet::simple_spend(names.clone(), recipients.clone(), gifts.clone(), *fee),
+        Commands::MakeTx { draft } => Wallet::make_tx(draft),
+        Commands::UpdateBalance => Wallet::update_balance(),
+        Commands::ImportMasterPubkey { key, knot } => Wallet::import_master_pubkey(key, knot),
+        Commands::ListPubkeys => Wallet::list_pubkeys(),
+    }?;
+
+    // If this command requires sync and we have a socket, wrap it with sync-run
+    let final_poke = if requires_sync && cli.nockchain_socket.is_some() {
+        Wallet::wrap_with_sync_run(poke.0, poke.1)?
+    } else {
+        poke
+    };
+
+    wallet
+        .app
+        .add_io_driver(one_punch_driver(final_poke.0, final_poke.1))
+        .await;
+
+    {
+        if let Some(socket_path) = cli.nockchain_socket {
+            match UnixStream::connect(&socket_path).await {
+                Ok(stream) => {
+                    info!("Connected to nockchain NPC socket at {:?}", socket_path);
+                    wallet
+                        .app
+                        .add_io_driver(crown::npc_client_driver(stream))
+                        .await;
+                }
+                Err(e) => {
+                    error!(
+                        "Failed to connect to nockchain NPC socket at {:?}: {}\n\
+                         This could mean:\n\
+                         1. Nockchain is not running\n\
+                         2. The socket path is incorrect\n\
+                         3. The socket file exists but is stale (try removing it)\n\
+                         4. Insufficient permissions to access the socket",
+                        socket_path, e
+                    );
+                }
+            }
+        }
+
+        wallet.app.add_io_driver(file_driver()).await;
+        wallet.app.add_io_driver(markdown_driver()).await;
+        wallet.app.add_io_driver(exit_driver()).await;
+
+        wallet.app.run().await?;
+        Ok(())
+    }
+}
+
+pub fn from_bytes(stack: &mut NounSlab, bytes: &[u8]) -> Atom {
+    unsafe {
+        let mut tas_atom = IndirectAtom::new_raw_bytes(stack, bytes.len(), bytes.as_ptr());
+        tas_atom.normalize_as_atom()
+    }
+}
+
+// TODO: all these tests need to also validate the results and not
+// just ensure that the wallet can be poked with the expected noun.
+#[allow(warnings)]
+#[cfg(test)]
+mod tests {
+    use std::sync::Once;
+
+    use crown::kernel::boot::{self, Cli as BootCli};
+    use crown::nockapp::wire::SystemWire;
+    use crown::{exit_driver, Bytes};
+    use tokio::sync::mpsc;
+
+    use super::*;
+
+    static INIT: Once = Once::new();
+
+    fn init_tracing() {
+        INIT.call_once(|| {
+            let cli = boot::default_boot_cli(true);
+            boot::init_default_tracing(&cli);
+        });
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_keygen() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&["--new"]);
+
+        let prover_hot_state = produce_prover_hot_state();
+        let nockapp = boot::setup(
+            KERNEL,
+            Some(cli.clone()),
+            prover_hot_state.as_slice(),
+            "wallet",
+            None,
+        )
+        .await
+        .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let mut entropy = [0u8; 32];
+        let mut salt = [0u8; 16];
+        getrandom(&mut entropy).map_err(|e| CrownError::Unknown(e.to_string()))?;
+        getrandom(&mut salt).map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let (noun, op) = Wallet::keygen(&entropy, &salt)?;
+
+        let wire = WalletWire::Command(Commands::Keygen).to_wire();
+
+        let keygen_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        println!("keygen result: {:?}", keygen_result);
+        assert!(
+            keygen_result.len() == 1,
+            "Expected keygen result to be a list of 1 noun slab"
+        );
+        let exit_cause = unsafe { keygen_result[0].root() };
+        let code = exit_cause.as_cell()?.tail();
+        assert!(unsafe { code.raw_equals(&D(0)) }, "Expected exit code 0");
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_derive_child() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&["--new"]);
+
+        let prover_hot_state = produce_prover_hot_state();
+        let nockapp = boot::setup(
+            KERNEL,
+            Some(cli.clone()),
+            prover_hot_state.as_slice(),
+            "wallet",
+            None,
+        )
+        .await
+        .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let key_type = KeyType::Prv;
+
+        // Generate a new key pair
+        let mut entropy = [0u8; 32];
+        let mut salt = [0u8; 16];
+        let (noun, op) = Wallet::keygen(&entropy, &salt)?;
+        let wire = WalletWire::Command(Commands::Keygen).to_wire();
+        let _ = wallet.app.poke(wire, noun.clone()).await?;
+
+        // Derive a child key
+        let index = 0;
+        let (noun, op) = Wallet::derive_child(key_type.clone(), index)?;
+
+        let wire = WalletWire::Command(Commands::DeriveChild {
+            key_type: key_type.clone().to_string().to_owned(),
+            index,
+        })
+        .to_wire();
+
+        let derive_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        assert!(
+            derive_result.len() == 1,
+            "Expected derive result to be a list of 1 noun slab"
+        );
+
+        let exit_cause = unsafe { derive_result[0].root() };
+        let code = exit_cause.as_cell()?.tail();
+        assert!(unsafe { code.raw_equals(&D(0)) }, "Expected exit code 0");
+
+        Ok(())
+    }
+
+    // TODO make this a real test by creating and signing a real draft
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_tx() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        // Create a temporary input bundle file
+        let bundle_path = "test_bundle.jam";
+        let test_data = vec![0u8; 32]; // TODO make this a real input bundle
+        fs::write(bundle_path, &test_data).map_err(|e| NockAppError::IoError(e))?;
+
+        let wire = WalletWire::Command(Commands::SignTx {
+            draft: bundle_path.to_string(),
+            index: None,
+        })
+        .to_wire();
+
+        // Test signing with valid indices
+        let (noun, op) = Wallet::sign_tx(bundle_path, None)?;
+        let sign_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        println!("sign_result: {:?}", sign_result);
+
+        let wire = WalletWire::Command(Commands::SignTx {
+            draft: bundle_path.to_string(),
+            index: Some(1),
+        })
+        .to_wire();
+
+        let (noun, op) = Wallet::sign_tx(bundle_path, Some(1))?;
+        let sign_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        println!("sign_result: {:?}", sign_result);
+
+        let wire = WalletWire::Command(Commands::SignTx {
+            draft: bundle_path.to_string(),
+            index: Some(255),
+        })
+        .to_wire();
+
+        let (noun, op) = Wallet::sign_tx(bundle_path, Some(255))?;
+        let sign_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        println!("sign_result: {:?}", sign_result);
+
+        // Cleanup
+        fs::remove_file(bundle_path).map_err(|e| NockAppError::IoError(e))?;
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_show_balance() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let block = "block123";
+        let (noun, op) = Wallet::balance_at_block(block)?;
+        let wire = WalletWire::Command(Commands::Balance {}).to_wire();
+        let balance_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("balance_result: {:?}", balance_result);
+        // Verify balance
+        Ok(())
+    }
+
+    // Tests for Cold Side Commands
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_gen_master_privkey() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let seedphrase = "correct horse battery staple";
+        let (noun, op) = Wallet::gen_master_privkey(seedphrase)?;
+        println!("privkey_slab: {:?}", noun);
+        let wire = WalletWire::Command(Commands::GenMasterPrivkey {
+            seedphrase: seedphrase.to_string(),
+        })
+        .to_wire();
+        let privkey_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("privkey_result: {:?}", privkey_result);
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_gen_master_pubkey() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let master_privkey = "privkey123";
+        let (noun, op) = Wallet::gen_master_pubkey(master_privkey)?;
+        let wire = WalletWire::Command(Commands::GenMasterPubkey {
+            master_privkey: master_privkey.to_string(),
+        })
+        .to_wire();
+        let pubkey_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("pubkey_result: {:?}", pubkey_result);
+        Ok(())
+    }
+
+    // Tests for Hot Side Commands
+    // TODO: fix this test by adding a real key file
+    #[tokio::test]
+    #[ignore]
+    async fn test_import_keys() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&["--new"]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        // Create test key file
+        let test_path = "test_keys.jam";
+        let test_data = vec![0u8; 32]; // TODO: Use real jammed key data
+        fs::write(test_path, &test_data).expect(&format!(
+            "Called `expect()` at {}:{} (git sha: {})",
+            file!(),
+            line!(),
+            option_env!("GIT_SHA").unwrap_or("unknown")
+        ));
+
+        let (noun, op) = Wallet::import_keys(test_path)?;
+        let wire = SystemWire.to_wire();
+        let import_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        fs::remove_file(test_path).expect(&format!(
+            "Called `expect()` at {}:{} (git sha: {})",
+            file!(),
+            line!(),
+            option_env!("GIT_SHA").unwrap_or("unknown")
+        ));
+
+        println!("import result: {:?}", import_result);
+        assert!(
+            !import_result.is_empty(),
+            "Expected non-empty import result"
+        );
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_simple_scan() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+        let master_pubkey = "pubkey123";
+        let (noun, op) = Wallet::scan(master_pubkey, 100, false, false)?;
+        let wire = WalletWire::Command(Commands::Scan {
+            master_pubkey: master_pubkey.to_string(),
+            search_depth: 100,
+            include_timelocks: false,
+            include_multisig: false,
+        })
+        .to_wire();
+        let scan_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("scan_result: {:?}", scan_result);
+        Ok(())
+    }
+
+    // TODO: fix this test
+    #[tokio::test]
+    #[ignore]
+    async fn test_simple_spend_multisig_format() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        let names = "[first1 last1],[first2 last2]".to_string();
+        let recipients = "[1 pk1],[2 pk2,pk3,pk4]".to_string();
+        let gifts = "1,2".to_string();
+        let fee = 1;
+
+        let (noun, op) =
+            Wallet::simple_spend(names.clone(), recipients.clone(), gifts.clone(), fee)?;
+        let wire = WalletWire::Command(Commands::SimpleSpend {
+            names: names.clone(),
+            recipients: recipients.clone(),
+            gifts: gifts.clone(),
+            fee: fee.clone(),
+        })
+        .to_wire();
+        let spend_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("spend_result: {:?}", spend_result);
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_simple_spend_single_sig_format() -> Result<(), NockAppError> {
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        init_tracing();
+        let mut wallet = Wallet::new(nockapp);
+
+        // these should be valid names of notes in the wallet balance
+        let names = "[Amt4GcpYievY4PXHfffiWriJ1sYfTXFkyQsGzbzwMVzewECWDV3Ad8Q BJnaDB3koU7ruYVdWCQqkFYQ9e3GXhFsDYjJ1vSmKFdxzf6Y87DzP4n]".to_string();
+        let recipients = "EHmKL2U3vXfS5GYAY5aVnGdukfDWwvkQPCZXnjvZVShsSQi3UAuA4tQ".to_string();
+        let gifts = "0".to_string();
+        let fee = 0;
+
+        // generate keys
+        let (genkey_noun, genkey_op) = Wallet::gen_master_privkey("correct horse battery staple")?;
+        let (spend_noun, spend_op) =
+            Wallet::simple_spend(names.clone(), recipients.clone(), gifts.clone(), fee)?;
+
+        let wire1 = WalletWire::Command(Commands::GenMasterPrivkey {
+            seedphrase: "correct horse battery staple".to_string(),
+        })
+        .to_wire();
+        let genkey_result = wallet.app.poke(wire1, genkey_noun.clone()).await?;
+        println!("genkey_result: {:?}", genkey_result);
+
+        let wire2 = WalletWire::Command(Commands::SimpleSpend {
+            names: names.clone(),
+            recipients: recipients.clone(),
+            gifts: gifts.clone(),
+            fee: fee.clone(),
+        })
+        .to_wire();
+        let spend_result = wallet.app.poke(wire2, spend_noun.clone()).await?;
+        println!("spend_result: {:?}", spend_result);
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_update_balance() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&["--new"]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        let (noun, _) = Wallet::update_balance()?;
+
+        let wire = WalletWire::Command(Commands::UpdateBalance {}).to_wire();
+        let update_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("update_result: {:?}", update_result);
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    #[cfg_attr(miri, ignore)]
+    async fn test_list_notes() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        // Test listing notes
+        let (noun, op) = Wallet::list_notes()?;
+        let wire = WalletWire::Command(Commands::ListNotes {}).to_wire();
+        let list_result = wallet.app.poke(wire, noun.clone()).await?;
+        println!("list_result: {:?}", list_result);
+
+        Ok(())
+    }
+
+    // TODO: fix this test by adding a real draft
+    #[tokio::test]
+    #[ignore]
+    async fn test_make_tx_from_draft() -> Result<(), NockAppError> {
+        init_tracing();
+        let cli = BootCli::parse_from(&[""]);
+        let nockapp = boot::setup(KERNEL, Some(cli.clone()), &[], "wallet", None)
+            .await
+            .map_err(|e| CrownError::Unknown(e.to_string()))?;
+        let mut wallet = Wallet::new(nockapp);
+
+        // use the draft in .drafts/
+        let draft_path = ".drafts/test_draft.draft";
+        let test_data = vec![0u8; 32]; // TODO: Use real draft data
+        fs::write(draft_path, &test_data).expect(&format!(
+            "Called `expect()` at {}:{} (git sha: {})",
+            file!(),
+            line!(),
+            option_env!("GIT_SHA").unwrap_or("unknown")
+        ));
+
+        let (noun, op) = Wallet::make_tx(draft_path)?;
+        let wire = WalletWire::Command(Commands::MakeTx {
+            draft: draft_path.to_string(),
+        })
+        .to_wire();
+        let tx_result = wallet.app.poke(wire, noun.clone()).await?;
+
+        fs::remove_file(draft_path).expect(&format!(
+            "Called `expect()` at {}:{} (git sha: {})",
+            file!(),
+            line!(),
+            option_env!("GIT_SHA").unwrap_or("unknown")
+        ));
+
+        println!("transaction result: {:?}", tx_result);
+        assert!(
+            !tx_result.is_empty(),
+            "Expected non-empty transaction result"
+        );
+
+        Ok(())
+    }
+}

+ 2 - 9
hoon/apps/dumbnet/inner.hoon

@@ -2,7 +2,6 @@
 /=  sp  /common/stark/prover
 /=  c-transact  /common/tx-engine
 /=  dumb-miner  /apps/dumbnet/lib/miner
-/=  dumb-admin  /apps/dumbnet/lib/admin
 /=  dumb-pending  /apps/dumbnet/lib/pending
 /=  dumb-derived  /apps/dumbnet/lib/derived
 /=  dumb-consensus  /apps/dumbnet/lib/consensus
@@ -21,11 +20,9 @@
 ++  inner
   |_  k=kernel-state:dk
   +*  min      ~(. dumb-miner m.k constants.k)
-      adm      ~(. dumb-admin a.k)
       pen      ~(. dumb-pending p.k constants.k)
       der      ~(. dumb-derived d.k constants.k)
       con      ~(. dumb-consensus c.k constants.k)
-      ver      ~(. nv stark-config.a.k)
       t        ~(. c-transact constants.k)
   ::
   ::  We should be calling the inner kernel load in case of update
@@ -373,7 +370,7 @@
       ::
       ::  validate the powork. this is done separately since the
       ::  other checks are much cheaper.
-      =/  pow-res=?  (verify:ver u.pow.pag ~ eny)
+      =/  pow-res=?  (verify:nv u.pow.pag ~ eny)
       ?.  pow-res
         [%.n %pow-failed-to-verify]
       [%.y ~]
@@ -652,9 +649,6 @@
       ::
           %btc-data
         do-btc-data
-      ::
-          %init-stark-config
-        `k(a produce-stark-config:adm)
       ::
           %set-constants
         `k(constants p.command)
@@ -696,9 +690,8 @@
           `k
         =/  commit=block-commitment:t
           (block-commitment:page:t candidate-block.m.k)
-        =.  a.k  produce-stark-config:adm
         =/  [prf=proof:sp dig=tip5-hash-atom:zeke]
-          (prove-block:mine commit p.command stark-config.a.k)
+          (prove-block:mine commit p.command)
         ?:  %+  check-target:mine  dig
             (~(got z-by targets.c.k) parent.candidate-block.m.k)
           =.  m.k  (set-pow:min prf)

+ 0 - 16
hoon/apps/dumbnet/lib/admin.hoon

@@ -1,16 +0,0 @@
-/=  *  /common/zeke
-/=  dk  /apps/dumbnet/lib/types
-/=  nock-common  /common/nock-common
-::
-|_  a=admin-state:dk
-++  produce-stark-config
-  ^-  admin-state:dk
-  ?^  prep.stark-config.a  a
-  =/  in=stark-input
-    %*  .
-        *stark-input
-        all-verifier-funcs
-        all-verifier-funcs:nock-common
-    ==
-  a(prep.stark-config (some ~(preprocess-data stark-engine in)))
---

+ 5 - 8
hoon/apps/dumbnet/lib/types.hoon

@@ -54,12 +54,11 @@
 ::
 +$  admin-state
   $+  admin-state
-  $:  desk-hash=(unit @uvI)           ::  hash of zkvm desk
-      init=init-phase                 ::  boolean flag denoting whether kernel is in the init phase.
-      =stark-config:dt                ::  prover/verifier settings
-      retain=$~([~ 20] (unit @))      ::  how long to retain transactions before dropping
-                                      ::  value of ~ indicates never drop transactions,
-                                      ::  value of [~ 0] indicates drop everything every new block
+  $:  desk-hash=(unit @uvI)               ::  hash of zkvm desk
+      init=init-phase                     ::  boolean flag denoting whether kernel is in the init phase.
+      retain=$~([~ 20] (unit @))          ::  how long to retain transactions before dropping
+                                          ::  value of ~ indicates never drop transactions,
+                                          ::  value of [~ 0] indicates drop everything every new block
   ==
 ::
 +$  derived-state
@@ -98,7 +97,6 @@
       :: set expected btc height and msg hash of genesis block
       [%set-genesis-seal p=[height=page-number:dt msg-hash=@t]]
       [%btc-data p=btc-hash:dt]  ::  data from BTC RPC node
-      [%init-stark-config ~]  :: init stark config
       test-command
   ==
 ::
@@ -115,7 +113,6 @@
       %btc-data
       %genesis
       %born
-      %init-stark-config
   ==
 ::  commands that can only be performed if init-phase is %.n
 +$  non-init-command  ?(%timer)

+ 18 - 22
hoon/apps/wallet/wallet.hoon

@@ -106,9 +106,9 @@
     +$  form  (unit coil)
     ++  public
       |=  =form
-      ~|  "master public key not found"
-      ?<  ?=(~ form)
-      u.form
+      ?:  ?=(^ form)
+        u.form
+      ~|("master public key not found" !!)
     ::
     ++  to-b58
       |=  =form
@@ -384,8 +384,7 @@
       *preinput
     =/  input-result  (~(get-input plan draft-tree.state) u.active-input.state)
     ?~  input-result
-      ~|  "active input not found in draft-tree"
-      !!
+      ~|("active input not found in draft-tree" !!)
     u.input-result
   ::    +add-seed: add a seed to the input
   ::
@@ -585,7 +584,6 @@
     ++  by-index
       |=  index=@ud
       ^-  coil
-      ~|  "key not found at index {<index>}"
       =/  =trek  (welp key-path /[ud/index])
       =/  =meta  (~(got of keys.state) trek)
       ?>  ?=(%coil -.meta)
@@ -593,12 +591,10 @@
     ::
     ++  seed
       ^-  meta
-      ~|  "key not found at {<seed-path>}"
       (~(got of keys.state) seed-path)
     ::
     ++  by-label
       |=  label=@t
-      ~|  "key not found with label {<label>}"
       %+  murn  keys
       |=  [t=trek =meta]
       ?:(&(?=(%label -.meta) =(label +.meta)) `t ~)
@@ -672,8 +668,9 @@
   ++  get-note
     |=  name=nname:transact
     ^-  nnote:transact
-    ~|  "note not found: {<name>}"
-    (~(got z-by:zo balance.state) name)
+    ?:  (~(has z-by:zo balance.state) name)
+      (~(got z-by:zo balance.state) name)
+    ~|("note not found: {<name>}" !!)
   ::
   ++  get-note-from-hash
     |=  has=hash:transact
@@ -960,7 +957,7 @@
   --
 --
 ::
-%-  (moat |)
+%-  (moat &)
 ^-  fort:moat
 |_  =state
 +*  v  ~(. vault state)
@@ -1054,7 +1051,8 @@
         %npc-bind
       =/  pid  pid.npc-cause
       =/  peek-type
-        ~|  "no peek request found for pid: {<pid>}"
+        ?.  (~(has by peek-requests.state) pid)
+          ~|("no peek request found for pid: {<pid>}" !!)
         (~(got by peek-requests.state) pid)
       =/  result  result.npc-cause
       ?-  peek-type
@@ -1225,7 +1223,8 @@
     =/  pid=(unit @ud)  (generate-pid:v %balance)
     ?~  pid  `state
     =/  bid=block-id:transact
-      ~|  "no last block found, not updating balance"
+      ?:  ?=(~ last-block.state)
+        ~|("no last block found, not updating balance" !!)
       (need last-block.state)
     =/  =path  (snoc /balance (to-b58:block-id:transact bid))
     =/  =effect  [%npc u.pid %peek path]
@@ -1343,7 +1342,6 @@
     |=  =cause
     ?>  ?=(%scan -.cause)
     %-  (debug "scan: scanning {<search-depth.cause>} addresses")
-    ~|  "something went wrong! state: {<state>}"
     ?>  ?=(^ master.state)
     ::  get all public keys up to search depth
     =/  index=@ud  search-depth.cause
@@ -1362,8 +1360,8 @@
         keys  ?^(key (snoc keys u.key) keys)
       ==
     ::  fail when no coils
-    ~|  "no coils for master key"
-    ?<  ?=(~ coils)
+    ?:  ?=(~ coils)
+      ~|("no coils for master key" !!)
     ::  generate first names of notes owned by each pubkey
     =/  first-names=(list [hash:transact schnorr-pubkey:transact])
       %+  turn  coils
@@ -1495,7 +1493,6 @@
     =/  names=(list nname:transact)
       %+  turn  names.cause
       |=  [first=@t last=@t]
-      ~|  "could not parse names: {<names.cause>}"
       (from-b58:nname:transact [first last])
     =/  recipients=(list lock:transact)
       %+  turn  recipients.cause
@@ -1504,15 +1501,14 @@
       %-  ~(gas z-in:zo *(z-set:zo schnorr-pubkey:transact))
       %+  turn  pks
       |=  pk=@t
-      ~|  "could not parse recipients: {<recipients.cause>}"
       (from-b58:schnorr-pubkey:transact pk)
     ::
     =/  gifts=(list coins:transact)  gifts.cause
     ::
-    ~|  "different number of names/recipients/gifts"
-    ?>  ?&  =((lent names) (lent recipients))
+    ?.  ?&  =((lent names) (lent recipients))
             =((lent names) (lent gifts))
         ==
+      ~|("different number of names/recipients/gifts" !!)
     =|  ledger=(list [name=nname:transact recipient=lock:transact gifts=coins:transact])
     =.  ledger
       |-
@@ -1569,8 +1565,8 @@
     ?.  spent-fee
       ~|("no note suitable to subtract fee from, aborting operation" !!)
     =/  ins-draft=inputs:transact  (multi:new:inputs:transact ins)
-    ~|  "last-block unknown!"
-    ?>  ?=(^ last-block.state)
+    ?:  ?=(~ last-block.state)
+      ~|("last-block unknown!" !!)
     ::  name is the b58-encoded name of the first input
     =/  draft-name=@t
       %-  head

+ 2 - 2
hoon/common/nock-common.hoon

@@ -154,8 +154,8 @@
   ==
 ::
 ++  remove-unused-constraints
-  |=  [pre=preprocess table-names=(list term) override=(unit (list term))]
-  ^-  preprocess
+  |=  [pre=preprocess-0 table-names=(list term) override=(unit (list term))]
+  ^-  preprocess-0
   ::
   =?  cd.pre  !=(~ override)
     %-  ~(gas by *table-to-constraint-degree)

+ 4 - 3
hoon/common/nock-prover.hoon

@@ -1,16 +1,17 @@
 /=  *  /common/zeke
 /=  stark-prover  /common/stark/prover
 /=  common  /common/nock-common
+/#  sc=stark-config
+::
+|%
 ::
-|_  stark-config
-++  sam  +<
 ++  prover
   =|  in=stark-input
   ::  +<+< = stark-engine door sample wrt stark-verifier core
   %_    stark-prover
       +<+<
     %_  in
-      stark-config        sam
+      stark-config        sc
       all-verifier-funcs  all-verifier-funcs:common
     ==
   ==

+ 4 - 3
hoon/common/nock-verifier.hoon

@@ -1,16 +1,17 @@
 /=  *  /common/zeke
 /=  stark-verifier  /common/stark/verifier
 /=  common  /common/nock-common
+/#  sc=stark-config
+::
+|%
 ::
-|_  stark-config
-++  sam  +<
 ++  verifier
   =|  in=stark-input
   ::  +<+< = stark-engine door sample wrt stark-verifier core
   %_    stark-verifier
       +<+<
     %_  in
-      stark-config        sam
+      stark-config        sc
       all-verifier-funcs  all-verifier-funcs:common
     ==
   ==

+ 2 - 2
hoon/common/pow.hoon

@@ -13,10 +13,10 @@
 ::
 ::  +prove-block-inner
 ++  prove-block-inner
-  |=  [length=@ block-commitment=noun-digest:tip5 nonce=noun-digest:tip5 sc=stark-config]
+  |=  [length=@ block-commitment=noun-digest:tip5 nonce=noun-digest:tip5]
   ^-  [proof:sp tip5-hash-atom]
   =/  =prove-result:sp
-    (~(prove np sc) block-commitment nonce length ~)
+    (prove:np block-commitment nonce length ~)
   ?>  ?=(%& -.prove-result)
   =/  =proof:sp  p.prove-result
   =/  proof-hash=tip5-hash-atom  (digest-to-atom:tip5 (hash-proof proof))

+ 1 - 5
hoon/common/stark/prover.hoon

@@ -15,7 +15,6 @@
 +$  prove-result  (each =proof err=prove-err)
 +$  prove-err     $%([%too-big heights=(list @)])
 +$  prover-output    [=proof deep-codeword=fpoly]
-::
 ::  +prove: prove the Nock computation [s f]
 ::
 ::
@@ -74,10 +73,7 @@
   ::~&  heights+heights
   ::
   =.  proof  (~(push proof-stream proof) [%heights heights])
-  =/  pre=preprocess
-    ?^  prep.stark-config
-      u.prep.stark-config
-    preprocess-data
+  =/  pre=preprocess-0  prep.stark-config
   ::
   ::  remove preprocess data for unused tables
   =.  pre

+ 2 - 1
hoon/common/stark/verifier.hoon

@@ -48,7 +48,8 @@
   ?>  =(num-tables (lent core-table-names:nock-common))
   ::~&  table-heights+heights
   ::
-  =/  pre  ?~(prep.stark-config preprocess-data u.prep.stark-config)
+  =/  c  constraints
+  =/  pre=preprocess-0  prep.stark-config
   ::
   ::
   ::  remove preprocess data for unused tables

+ 1 - 1
hoon/common/tx-engine.hoon

@@ -50,7 +50,7 @@
           ::  which a new block's timestamp must be after to be considered valid
           min-past-blocks=11
           ::TODO determine appropriate genesis target
-          genesis-target-atom=^~((div max-tip5-atom:tip5 (bex 0)))
+          genesis-target-atom=^~((div max-tip5-atom:tip5 (bex 2)))
           ::TODO determine a real max-target-atom. BTC uses 32 leading zeroes
           max-target-atom=max-tip5-atom:tip5
           ::  whether or not to check the pow of blocks

+ 7 - 81
hoon/common/ztd/eight.hoon

@@ -27,9 +27,10 @@
       terminal=@
   ==
 ::
-::  $preprocess: This holds everything that must be precomputed before we generate proofs.
-+$  preprocess
-  $:  cd=table-to-constraint-degree       :: maximum degree of constraints for each table
+::  $preprocess-0: preprocess with a version tag attached
++$  preprocess-0
+  $:  %0
+      cd=table-to-constraint-degree       :: maximum degree of constraints for each table
       constraint-map=(map @ constraints)  :: map from table number -> constraints
       count-map=(map @ constraint-counts) :: map from table number -> constraint-counts
   ==
@@ -37,7 +38,7 @@
 ::  $stark-config: prover+verifier parameters unrelated to a particular computation
 +$  stark-config
   $:  conf=[log-expand-factor=_6 security-level=_100]
-      prep=(unit preprocess)
+      prep=preprocess-0
   ==
 ::TODO this type could potentially be improved
 +$  stark-input
@@ -848,7 +849,6 @@
 ~%  %stark-engine  ..puzzle-nock  ~
 ::    stark-engine
 |%
-::
 ++  stark-engine-jet-hook
   ~/  %stark-engine-jet-hook
   |=  n=@
@@ -931,18 +931,6 @@
     |=  [a=@ d=_d]
     (max d a)
   ::
-  ::
-  ::    compute max degree of the constraints for each table
-  ++  compute-table-to-constraint-degree
-    ^-  table-to-constraint-degree
-    %-  ~(gas by *(map @ constraint-degrees))
-    ::  turn over all verifier funcs except for the %jute table
-    ::%+  iturn  (scag (dec (lent all-verifier-funcs)) all-verifier-funcs)
-    %+  iturn  all-verifier-funcs
-    |=  [i=@ funcs=verifier-funcs]
-    ^-  [@ constraint-degrees]
-    [i (compute-constraint-degree funcs ~)]
-  ::
   ++  get-max-constraint-degree
     ~/  %get-max-constraint-degree
     |=  tab=table-to-constraint-degree
@@ -951,67 +939,5 @@
     %+  roll  ~(val by tab)
     |=  [cd=constraint-degrees acc=@]
     (max acc (max boundary.cd (max row.cd (max [transition terminal]:cd))))
-  ::
-  ++  compute-constraints
-    ^-  (map @ constraints)
-    |^
-    %-  ~(gas by *(map @ constraints))
-    ::  turn over all verifier funcs except for the %jute table
-    ::%+  iturn  (scag (dec (lent all-verifier-funcs)) all-verifier-funcs)
-    %+  iturn  all-verifier-funcs
-    |=  [i=@ funcs=verifier-funcs]
-    :-  i
-    :*  (build-constraint-data boundary-constraints:funcs)
-        (build-constraint-data row-constraints:funcs)
-        (build-constraint-data transition-constraints:funcs)
-        (build-constraint-data terminal-constraints:funcs)
-    ==
-    ::
-    ++  build-constraint-data
-      |=  cs=(map term mp-ultra)
-      ^-  (list constraint-data)
-      %+  turn  (unlabel-constraints:util cs)
-      |=  c=mp-ultra
-      [c (mp-degree-ultra c)]
-    --
-  ::
-  ++  count-constraints
-    ^-  (map @ constraint-counts)
-    |^
-    =/  vrf-funcs  all-verifier-funcs
-    %-  ~(gas by *(map @ constraint-counts))
-    %+  iturn
-      all-verifier-funcs
-    |=  [i=@ funcs=verifier-funcs]
-    :-  i
-    :*  (count (unlabel-constraints:util boundary-constraints:funcs))
-        (count (unlabel-constraints:util row-constraints:funcs))
-        (count (unlabel-constraints:util transition-constraints:funcs))
-        (count (unlabel-constraints:util terminal-constraints:funcs))
-    ==
-    ++  count
-      |=  cs=(list mp-ultra)
-      ^-  @
-      %+  roll
-        cs
-      |=  [mp=mp-ultra num=@]
-      ?-    -.mp
-          %mega  +(num)
-          %comp  (add num (lent com.mp))
-      ==
-    --
-  ::
-  ::  +preprocess-data: precompute all data necessary to run the prover/verifier
-  ++  preprocess-data
-    ^-  preprocess
-    ~&  %generating-preprocess-data
-    =/  cd  compute-table-to-constraint-degree
-    =/  constraints  compute-constraints
-    =/  count-map  count-constraints
-    :*  cd
-        constraints
-        count-map
-    ==
-  ::
-  --
---  ::stark-engine
+  --  ::stark-engine
+--

+ 1 - 0
hoon/common/ztd/seven.hoon

@@ -3,6 +3,7 @@
 ~%  %table-lib  ..fri-door  ~
 ::    table-lib
 |%
+::
 +|  %jute-types
 +$  jute-data  [name=@tas sam=tree-data prod=tree-data]
 +$  jute-map  (map @tas @)

+ 95 - 0
hoon/dat/constraints.hoon

@@ -0,0 +1,95 @@
+/=  z  /common/zeke
+/=  nock-common  /common/nock-common
+=<  preprocess-data
+|%
+::  +preprocess-data: precompute all data necessary to run the prover/verifier
+++  preprocess-data
+  ^-  preprocess-0:z
+  |^
+  ~&  %computing-preprocess-data
+  =/  cd  compute-table-to-constraint-degree
+  =/  constraints  compute-constraints
+  =/  count-map  count-constraints
+  :*  %0
+      cd
+      constraints
+      count-map
+  ==
+  ::
+  ::    compute max degree of the constraints for each table
+  ++  compute-table-to-constraint-degree
+    ^-  table-to-constraint-degree:z
+    %-  ~(gas by *(map @ constraint-degrees:z))
+    %+  iturn:z  all-verifier-funcs:nock-common
+    |=  [i=@ funcs=verifier-funcs:z]
+    ^-  [@ constraint-degrees:z]
+    [i (compute-constraint-degree funcs)]
+  ::
+  ++  compute-constraint-degree
+    |=  funcs=verifier-funcs:z
+    ^-  constraint-degrees:z
+    =-  [(snag 0 -) (snag 1 -) (snag 2 -) (snag 3 -)]
+    %+  turn
+      :~  (unlabel-constraints:constraint-util:z boundary-constraints:funcs)
+          (unlabel-constraints:constraint-util:z row-constraints:funcs)
+          (unlabel-constraints:constraint-util:z transition-constraints:funcs)
+          (unlabel-constraints:constraint-util:z terminal-constraints:funcs)
+      ==
+    |=  l=(list mp-ultra:z)
+    %+  roll
+      l
+    |=  [constraint=mp-ultra:z d=@]
+    %+  roll
+      (mp-degree-ultra:z constraint)
+    |=  [a=@ d=_d]
+    (max d a)
+  ::
+  ++  compute-constraints
+    ^-  (map @ constraints:z)
+    |^
+    %-  ~(gas by *(map @ constraints:z))
+    %+  iturn:z  all-verifier-funcs:nock-common
+    |=  [i=@ funcs=verifier-funcs:z]
+    :-  i
+    :*  (build-constraint-data boundary-constraints:funcs)
+        (build-constraint-data row-constraints:funcs)
+        (build-constraint-data transition-constraints:funcs)
+        (build-constraint-data terminal-constraints:funcs)
+    ==
+    ::
+    ++  build-constraint-data
+      |=  cs=(map term mp-ultra:z)
+      ^-  (list constraint-data:z)
+      %+  turn  (unlabel-constraints:constraint-util:z cs)
+      |=  c=mp-ultra:z
+      [c (mp-degree-ultra:z c)]
+    --
+  ::
+  ++  count-constraints
+    ^-  (map @ constraint-counts:z)
+    |^
+    =/  vrf-funcs  all-verifier-funcs:nock-common
+    %-  ~(gas by *(map @ constraint-counts:z))
+    %+  iturn:z
+      all-verifier-funcs:nock-common
+    |=  [i=@ funcs=verifier-funcs:z]
+    :-  i
+    :*  (count (unlabel-constraints:constraint-util:z boundary-constraints:funcs))
+        (count (unlabel-constraints:constraint-util:z row-constraints:funcs))
+        (count (unlabel-constraints:constraint-util:z transition-constraints:funcs))
+        (count (unlabel-constraints:constraint-util:z terminal-constraints:funcs))
+    ==
+    ::
+    ++  count
+      |=  cs=(list mp-ultra:z)
+      ^-  @
+      %+  roll
+        cs
+      |=  [mp=mp-ultra:z num=@]
+      ?-    -.mp
+          %mega  +(num)
+          %comp  (add num (lent com.mp))
+      ==
+    --
+  --  :: |^
+--  :: %pre

+ 10 - 0
hoon/dat/stark-config.hoon

@@ -0,0 +1,10 @@
+/=  z  /common/zeke
+/#  constraints-0=constraints
+::
+::  We place constraints in a different file
+::  so the stark parameters can be modified
+::  without blowing the cache
+^-  stark-config:z
+%*  .  *stark-config:z
+  prep  constraints-0
+==