@@ -1861,14 +1861,18 @@ def zcash_sign_pczt(self, address_n, actions, account=None,
18611861 header_digest = None , transparent_digest = None ,
18621862 sapling_digest = None , orchard_digest = None ,
18631863 orchard_flags = None , orchard_value_balance = None ,
1864- orchard_anchor = None , transparent_inputs = None ,
1865- expected_seed_fingerprint = None ):
1864+ orchard_anchor = None , tx_version = None ,
1865+ version_group_id = None , lock_time = None ,
1866+ expiry_height = None , transparent_outputs = None ,
1867+ transparent_inputs = None ,
1868+ expected_seed_fingerprint = None ,
1869+ return_transparent_signatures = False ):
18661870 """Sign a Zcash Orchard shielded transaction via PCZT protocol.
18671871
1868- Phase 2: Sends ZcashSignPCZT , then loops on ZcashPCZTActionAck
1869- feeding Orchard actions one at a time.
1870- Phase 3: If transparent_inputs provided, handles ZcashTransparentSig
1871- loop for transparent-to-shielded (shielding) transactions .
1872+ Streams transparent outputs , then transparent inputs, then Orchard
1873+ actions in the exact order requested by firmware 7.15. Orchard
1874+ signatures are compact: the response contains one signature for each
1875+ action whose explicit ``is_spend`` value is true, in action order .
18721876
18731877 Args:
18741878 address_n: ZIP-32 derivation path [32', 133', account']
@@ -1884,14 +1888,36 @@ def zcash_sign_pczt(self, address_n, actions, account=None,
18841888 orchard_flags: bundle flags byte (enables digest verification)
18851889 orchard_value_balance: signed i64 value balance
18861890 orchard_anchor: 32-byte anchor
1891+ tx_version: transaction version used to verify header_digest
1892+ version_group_id: transaction version group ID
1893+ lock_time: transaction lock time
1894+ expiry_height: transaction expiry height
1895+ transparent_outputs: output dicts matching ZcashTransparentOutput
1896+ transparent_inputs: input dicts matching ZcashTransparentInput;
1897+ host-provided per-input sighashes are rejected by RC18
1898+ return_transparent_signatures: when true, return a tuple of
1899+ (ZcashSignedPCZT, [DER transparent signatures])
18871900
18881901 Returns:
1889- ZcashSignedPCZT with .signatures list and optional .txid
1902+ ZcashSignedPCZT with compact Orchard signatures and optional txid,
1903+ or a tuple including transparent signatures when requested.
18901904 """
18911905 n_actions = len (actions )
18921906 if n_actions == 0 :
18931907 raise ValueError ("Must have at least one action" )
18941908
1909+ for idx , action in enumerate (actions ):
1910+ if 'is_spend' not in action or not isinstance (action ['is_spend' ], bool ):
1911+ raise ValueError (
1912+ "Orchard action %d must explicitly set boolean is_spend" % idx )
1913+
1914+ transparent_outputs = transparent_outputs or []
1915+ transparent_inputs = transparent_inputs or []
1916+ for inp in transparent_inputs :
1917+ if 'sighash' in inp :
1918+ raise ValueError (
1919+ "Host-provided transparent sighash is rejected by firmware 7.15" )
1920+
18951921 # Build the initial signing request — only send address_n,
18961922 # let firmware derive account from the path. Only set account
18971923 # explicitly if the caller passed it.
@@ -1918,42 +1944,112 @@ def zcash_sign_pczt(self, address_n, actions, account=None,
19181944 kwargs ['orchard_value_balance' ] = orchard_value_balance
19191945 if orchard_anchor is not None :
19201946 kwargs ['orchard_anchor' ] = orchard_anchor
1947+ if tx_version is not None :
1948+ kwargs ['tx_version' ] = tx_version
1949+ if version_group_id is not None :
1950+ kwargs ['version_group_id' ] = version_group_id
1951+ if lock_time is not None :
1952+ kwargs ['lock_time' ] = lock_time
1953+ if expiry_height is not None :
1954+ kwargs ['expiry_height' ] = expiry_height
1955+ if transparent_outputs :
1956+ kwargs ['n_transparent_outputs' ] = len (transparent_outputs )
1957+ if transparent_inputs :
1958+ kwargs ['n_transparent_inputs' ] = len (transparent_inputs )
19211959 if expected_seed_fingerprint is not None :
19221960 kwargs ['expected_seed_fingerprint' ] = expected_seed_fingerprint
19231961
19241962 resp = self .call (zcash_proto .ZcashSignPCZT (** kwargs ))
19251963
1926- # Phase 2: Orchard action-ack loop — device asks for actions one at a time
1964+ # Transparent plaintext is streamed outputs-first. Firmware uses field
1965+ # presence to distinguish output and input acknowledgments, so never
1966+ # infer a missing index as zero.
1967+ sent_outputs = 0
1968+ while (sent_outputs < len (transparent_outputs ) and
1969+ isinstance (resp , zcash_proto .ZcashTransparentAck )):
1970+ if not resp .HasField ('next_output_index' ):
1971+ raise Exception ("Device did not request the next transparent output" )
1972+ idx = resp .next_output_index
1973+ if idx != sent_outputs :
1974+ raise Exception (
1975+ "Device requested transparent output %d after %d outputs"
1976+ % (idx , sent_outputs ))
1977+ if idx >= len (transparent_outputs ):
1978+ raise Exception (
1979+ "Device requested transparent output %d but only %d provided"
1980+ % (idx , len (transparent_outputs )))
1981+ output = dict (transparent_outputs [idx ])
1982+ output .pop ('index' , None )
1983+ resp = self .call (zcash_proto .ZcashTransparentOutput (index = idx , ** output ))
1984+ sent_outputs += 1
1985+
1986+ sent_inputs = 0
1987+ while (sent_inputs < len (transparent_inputs ) and
1988+ isinstance (resp , zcash_proto .ZcashTransparentAck )):
1989+ if not resp .HasField ('next_input_index' ):
1990+ raise Exception ("Device did not request the next transparent input" )
1991+ idx = resp .next_input_index
1992+ if idx != sent_inputs :
1993+ raise Exception (
1994+ "Device requested transparent input %d after %d inputs"
1995+ % (idx , sent_inputs ))
1996+ if idx >= len (transparent_inputs ):
1997+ raise Exception (
1998+ "Device requested transparent input %d but only %d provided"
1999+ % (idx , len (transparent_inputs )))
2000+ inp = dict (transparent_inputs [idx ])
2001+ inp .pop ('index' , None )
2002+ resp = self .call (zcash_proto .ZcashTransparentInput (index = idx , ** inp ))
2003+ sent_inputs += 1
2004+
2005+ if sent_outputs != len (transparent_outputs ):
2006+ raise Exception ("Device did not request every transparent output" )
2007+ if sent_inputs != len (transparent_inputs ):
2008+ raise Exception ("Device did not request every transparent input" )
2009+
2010+ # Orchard action-ack loop: the device chooses the next action index.
2011+ sent_actions = set ()
19272012 while isinstance (resp , zcash_proto .ZcashPCZTActionAck ):
2013+ if not resp .HasField ('next_index' ):
2014+ raise Exception ("Device did not identify the next Orchard action" )
19282015 idx = resp .next_index
19292016 if idx >= n_actions :
19302017 raise Exception (
19312018 "Device requested action index %d but only %d actions provided"
19322019 % (idx , n_actions ))
2020+ if idx in sent_actions :
2021+ raise Exception ("Device requested Orchard action %d twice" % idx )
19332022 action = actions [idx ]
19342023 resp = self .call (zcash_proto .ZcashPCZTAction (index = idx , ** action ))
2024+ sent_actions .add (idx )
2025+
2026+ if sent_actions != set (range (n_actions )):
2027+ raise Exception ("Device did not request every Orchard action" )
19352028
1936- # Phase 3: Transparent input signing — device sends back signatures
1937- # and may request transparent inputs for shielding transactions
2029+ # RC18 defers transparent signatures until every Orchard action, digest,
2030+ # and fee has passed. They are emitted immediately before SignedPCZT.
19382031 transparent_sigs = []
1939- while isinstance (resp , zcash_proto .ZcashTransparentSig ):
1940- transparent_sigs .append (resp )
1941- if not transparent_inputs :
1942- raise Exception (
1943- "Device sent ZcashTransparentSig but no transparent_inputs provided" )
1944- if resp .next_index >= len (transparent_inputs ):
1945- raise Exception (
1946- "Device requested transparent input %d but only %d provided"
1947- % (resp .next_index , len (transparent_inputs )))
1948- inp = transparent_inputs [resp .next_index ]
1949- resp = self .call (zcash_proto .ZcashTransparentInput (** inp ))
2032+ if isinstance (resp , zcash_proto .ZcashTransparentSigned ):
2033+ transparent_sigs = list (resp .signatures )
2034+ resp = self .transport .read_blocking ()
19502035
19512036 if isinstance (resp , proto .Failure ):
19522037 raise Exception ("Zcash signing failed: %s" % resp .message )
19532038
19542039 if not isinstance (resp , zcash_proto .ZcashSignedPCZT ):
19552040 raise Exception ("Unexpected response type: %s" % type (resp ))
19562041
2042+ expected_signatures = sum (1 for action in actions if action ['is_spend' ])
2043+ if len (resp .signatures ) != expected_signatures :
2044+ raise Exception (
2045+ "Device returned %d Orchard signatures for %d real spends"
2046+ % (len (resp .signatures ), expected_signatures ))
2047+ for signature in resp .signatures :
2048+ if len (signature ) != 64 :
2049+ raise Exception ("Device returned an invalid RedPallas signature" )
2050+
2051+ if return_transparent_signatures :
2052+ return resp , transparent_sigs
19572053 return resp
19582054
19592055 # ── Hive ────────────────────────────────────────────────────
0 commit comments